Skip to content
Open
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
47 changes: 47 additions & 0 deletions artifacts/sunsama-api/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { build as esbuild } from "esbuild";
import { rm } from "node:fs/promises";

globalThis.require = createRequire(import.meta.url);

const artifactDir = path.dirname(fileURLToPath(import.meta.url));

async function buildAll() {
const distDir = path.resolve(artifactDir, "dist");
await rm(distDir, { recursive: true, force: true });

await esbuild({
entryPoints: [path.resolve(artifactDir, "src/index.ts")],
platform: "node",
bundle: true,
format: "esm",
outdir: distDir,
outExtension: { ".js": ".mjs" },
logLevel: "info",
external: [
"*.node",
"pg-native",
"better-sqlite3",
"sqlite3",
"bcrypt",
],
sourcemap: "linked",
banner: {
js: `import { createRequire as __bannerCrReq } from 'node:module';
import __bannerPath from 'node:path';
import __bannerUrl from 'node:url';

globalThis.require = __bannerCrReq(import.meta.url);
globalThis.__filename = __bannerUrl.fileURLToPath(import.meta.url);
globalThis.__dirname = __bannerPath.dirname(globalThis.__filename);
`,
},
});
}

buildAll().catch((err) => {
console.error(err);
process.exit(1);
});
25 changes: 25 additions & 0 deletions artifacts/sunsama-api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@workspace/sunsama-api",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "export NODE_ENV=development && pnpm run build && pnpm run start",
"build": "node ./build.mjs",
"start": "node --enable-source-maps ./dist/index.mjs",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@workspace/db": "workspace:*",
"cors": "^2",
"drizzle-orm": "catalog:",
"express": "^5",
"zod": "catalog:"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/node": "catalog:",
"esbuild": "^0.27.3"
}
}
24 changes: 24 additions & 0 deletions artifacts/sunsama-api/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import express from "express";
import cors from "cors";
import taskRoutes from "./routes/tasks";
import { ensureTempUser } from "./utils/seedUser";

const app = express();
const PORT = process.env.SUNSAMA_API_PORT || 3002;

app.use(cors());
app.use(express.json());

ensureTempUser().catch(() => {
/* seed may fail if DB not ready yet */
});

app.get("/health", (_req, res) => {
res.json({ status: "ok", message: "Sunsama API is running" });
});

app.use("/api/tasks", taskRoutes);

app.listen(PORT, () => {
console.log(`Sunsama API running on http://localhost:${PORT}`);
});
265 changes: 265 additions & 0 deletions artifacts/sunsama-api/src/routes/tasks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
import { Router } from "express";
import { db, sunsamaTasksTable } from "@workspace/db";
import { eq, and, gte, lte } from "drizzle-orm";
import { randomUUID } from "node:crypto";
import { generateInstances } from "../utils/recurringTasks";

const router = Router();

router.get("/", async (_req, res) => {
try {
const userId = "temp-user-id";
const tasks = await db
.select()
.from(sunsamaTasksTable)
.where(eq(sunsamaTasksTable.userId, userId))
.orderBy(sunsamaTasksTable.createdAt);

res.json(tasks);
} catch {
res.status(500).json({ error: "Failed to fetch tasks" });
}
});

router.post("/", async (req, res) => {
try {
const {
title,
description,
dueDate,
timeEstimate,
isRecurring,
recurrencePattern,
recurrenceDays,
recurrenceInterval,
} = req.body;
const userId = "temp-user-id";

const [task] = await db
.insert(sunsamaTasksTable)
.values({
id: randomUUID(),
title,
description: description ?? null,
dueDate: dueDate ? new Date(dueDate) : null,
timeEstimate: timeEstimate ?? null,
userId,
isRecurring: isRecurring ?? false,
recurrencePattern: recurrencePattern ?? null,
recurrenceDays: recurrenceDays ?? null,
recurrenceInterval: recurrenceInterval ?? null,
})
.returning();

res.status(201).json(task);
} catch {
res.status(500).json({ error: "Failed to create task" });
}
});

router.patch("/:id/toggle", async (req, res) => {
try {
const { id } = req.params;

const [task] = await db
.select()
.from(sunsamaTasksTable)
.where(eq(sunsamaTasksTable.id, id))
.limit(1);

if (!task) {
res.status(404).json({ error: "Task not found" });
return;
}

const [updatedTask] = await db
.update(sunsamaTasksTable)
.set({ completed: !task.completed })
.where(eq(sunsamaTasksTable.id, id))
.returning();

res.json(updatedTask);
} catch {
res.status(500).json({ error: "Failed to update task" });
}
});

router.delete("/:id", async (req, res) => {
try {
const { id } = req.params;
await db.delete(sunsamaTasksTable).where(eq(sunsamaTasksTable.id, id));
res.status(204).send();
} catch {
res.status(500).json({ error: "Failed to delete task" });
}
});

router.patch("/:id/plan", async (req, res) => {
try {
const { id } = req.params;
const { plannedDate } = req.body;

const [task] = await db
.select()
.from(sunsamaTasksTable)
.where(eq(sunsamaTasksTable.id, id))
.limit(1);

if (!task) {
res.status(404).json({ error: "Task not found" });
return;
}

const [updatedTask] = await db
.update(sunsamaTasksTable)
.set({ plannedDate: plannedDate ? new Date(plannedDate) : null })
.where(eq(sunsamaTasksTable.id, id))
.returning();

res.json(updatedTask);
} catch {
res.status(500).json({ error: "Failed to update planned date for task" });
}
});

router.patch("/:id", async (req, res) => {
try {
const { id } = req.params;
const {
title,
description,
dueDate,
plannedDate,
timeEstimate,
startTime,
actualTime,
isRecurring,
recurrencePattern,
recurrenceDays,
recurrenceInterval,
} = req.body;

const [task] = await db
.select()
.from(sunsamaTasksTable)
.where(eq(sunsamaTasksTable.id, id))
.limit(1);

if (!task) {
res.status(404).json({ error: "Task not found" });
return;
}

const updateData: Record<string, unknown> = {};
if (title !== undefined) updateData.title = title;
if (description !== undefined) updateData.description = description;
if (dueDate !== undefined) updateData.dueDate = dueDate ? new Date(dueDate) : null;
if (plannedDate !== undefined) updateData.plannedDate = plannedDate ? new Date(plannedDate) : null;
if (timeEstimate !== undefined) updateData.timeEstimate = timeEstimate;
if (startTime !== undefined) updateData.startTime = startTime;
if (actualTime !== undefined) updateData.actualTime = actualTime;
if (isRecurring !== undefined) updateData.isRecurring = isRecurring;
if (recurrencePattern !== undefined) updateData.recurrencePattern = recurrencePattern;
if (recurrenceDays !== undefined) updateData.recurrenceDays = recurrenceDays;
if (recurrenceInterval !== undefined) updateData.recurrenceInterval = recurrenceInterval;

const [updatedTask] = await db
.update(sunsamaTasksTable)
.set(updateData)
.where(eq(sunsamaTasksTable.id, id))
.returning();

res.json(updatedTask);
} catch {
res.status(500).json({ error: "Failed to update task" });
}
});

router.post("/:id/generate-instances", async (req, res) => {
try {
const { id } = req.params;
const { weeks = 2 } = req.body;

const [template] = await db
.select()
.from(sunsamaTasksTable)
.where(eq(sunsamaTasksTable.id, id))
.limit(1);

if (!template) {
res.status(404).json({ error: "Task not found" });
return;
}

if (!template.isRecurring) {
res.status(400).json({ error: "Task not recurring" });
return;
}

const startDate = new Date();
startDate.setHours(0, 0, 0, 0);

const endDate = new Date();
endDate.setDate(endDate.getDate() + weeks * 7);
endDate.setHours(0, 0, 0, 0);

const instances = generateInstances(
{
id: template.id,
title: template.title,
description: template.description,
timeEstimate: template.timeEstimate,
recurrencePattern: template.recurrencePattern,
recurrenceDays: template.recurrenceDays,
recurrenceInterval: template.recurrenceInterval,
userId: template.userId,
},
startDate,
endDate
);

const existingInstances = await db
.select()
.from(sunsamaTasksTable)
.where(
and(
eq(sunsamaTasksTable.parentTaskId, template.id),
gte(sunsamaTasksTable.plannedDate, startDate),
lte(sunsamaTasksTable.plannedDate, endDate)
)
);

const existingDates = new Set(
existingInstances.map((t: { plannedDate: Date | null }) =>
t.plannedDate?.toISOString().split("T")[0]
)
);

const newInstances = instances.filter(
(inst) => !existingDates.has(inst.plannedDate.toISOString().split("T")[0])
);

if (newInstances.length > 0) {
await db.insert(sunsamaTasksTable).values(
newInstances.map((inst) => ({
id: randomUUID(),
title: inst.title,
description: inst.description,
timeEstimate: inst.timeEstimate,
plannedDate: inst.plannedDate,
userId: inst.userId,
parentTaskId: inst.parentTaskId,
}))
);
}

res.json({
message: `Generated ${newInstances.length} new instances`,
total: newInstances.length,
});
} catch {
res.status(500).json({ error: "Failed to generate instances" });
}
});

export default router;
Loading