From 6981ae58f09d4c47ff6e8fbc394e1029f61a9134 Mon Sep 17 00:00:00 2001 From: Kenny Date: Thu, 26 Oct 2023 21:41:24 -0300 Subject: [PATCH 1/8] feat: implement task entity based on requirements and ideas --- src/db/schema/task.ts | 63 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/db/schema/task.ts diff --git a/src/db/schema/task.ts b/src/db/schema/task.ts new file mode 100644 index 00000000..69ca8b41 --- /dev/null +++ b/src/db/schema/task.ts @@ -0,0 +1,63 @@ +import { relations, sql } from "drizzle-orm"; +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import { createInsertSchema, createSelectSchema } from "drizzle-zod"; +import { moduleTable } from "./module"; +import { subtask } from "./subtask"; + +export const task = sqliteTable("task", { + id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), + + title: text("title").notNull(), + description: text("description").notNull(), + + notes: text("notes").notNull(), + + done: integer("done", { mode: "boolean" }).default(false), + doneAt: text("doneAt"), + + dueDate: text("dueDate").notNull(), + + priority: text("priority", { enum: ["LOW", "MEDIUM", "HIGH"] }).notNull(), + + tags: text("tags", { mode: "json" }).$type(), + + moduleId: integer("module_id") + .references(() => moduleTable.id) + .notNull(), + + createdAt: text("createdAt") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + + updatedAt: text("updatedAt") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export type Task = typeof task.$inferSelect; +export type NewTask = typeof task.$inferInsert; + +export const taskRelations = relations(task, ({ one, many }) => ({ + module: one(moduleTable), + subtask: many(subtask), +})); + +export const insertTaskSchema = createInsertSchema(task).omit({ + id: true, + done: true, + doneAt: true, + createdAt: true, +}); + +export const updateTaskSchema = createInsertSchema(task).omit({ + moduleId: true, + done: true, +}); + +export const selectTaskSchema = createSelectSchema(task).omit({ + createdAt: true, + description: true, + dueDate: true, + priority: true, + title: true, +}); From 9540d3b89f865f86cfa5e5bce3b7da24a50e37e6 Mon Sep 17 00:00:00 2001 From: Kenny Date: Thu, 26 Oct 2023 21:41:46 -0300 Subject: [PATCH 2/8] feat: implement subtask entity used on task (parent entity) --- src/db/schema/subtask.ts | 60 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/db/schema/subtask.ts diff --git a/src/db/schema/subtask.ts b/src/db/schema/subtask.ts new file mode 100644 index 00000000..99e5a043 --- /dev/null +++ b/src/db/schema/subtask.ts @@ -0,0 +1,60 @@ +import { relations, sql } from "drizzle-orm"; +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import { createInsertSchema, createSelectSchema } from "drizzle-zod"; +import { task } from "./task"; + +export const subtask = sqliteTable("subTask", { + id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), + + title: text("title").notNull(), + description: text("description").notNull(), + + notes: text("notes").notNull(), + + done: integer("done", { mode: "boolean" }).default(false), + doneAt: text("doneAt"), + + taskId: integer("task_id") + .references(() => task.id) + .notNull(), + + createdAt: text("createdAt") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + + updatedAt: text("updatedAt") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export type Subtask = typeof subtask.$inferSelect; +export type NewSubtask = typeof subtask.$inferInsert; + +export const subtaskRelations = relations(subtask, ({ one }) => ({ + task: one(task), +})); + +export const insertSubtaskSchema = createInsertSchema(subtask).omit({ + id: true, + createdAt: true, + done: true, + doneAt: true, +}); + +export const updateSubtaskSchema = createInsertSchema(subtask).omit({ + moduleId: true, + taskId: true, + doneAt: true, + createdAt: true, +}); + +export const selectSubtaskSchema = createSelectSchema(subtask).omit({ + createdAt: true, + description: true, + dueDate: true, + priority: true, + title: true, + notes: true, + doneAt: true, + done: true, +}); From 603459ca9afca700d22fa3227dad8e7f4748c972 Mon Sep 17 00:00:00 2001 From: Kenny Date: Tue, 31 Oct 2023 13:37:40 -0300 Subject: [PATCH 3/8] feat: schema updated adding task and subtask entities --- migrations/0001_wandering_ogun.sql | 26 ++ migrations/meta/0001_snapshot.json | 457 +++++++++++++++++++++++++++++ migrations/meta/_journal.json | 7 + src/db/schema/index.ts | 2 + src/db/schema/module.ts | 2 + src/db/schema/subtask.ts | 24 +- src/db/schema/task.ts | 28 +- 7 files changed, 521 insertions(+), 25 deletions(-) create mode 100644 migrations/0001_wandering_ogun.sql create mode 100644 migrations/meta/0001_snapshot.json diff --git a/migrations/0001_wandering_ogun.sql b/migrations/0001_wandering_ogun.sql new file mode 100644 index 00000000..05172994 --- /dev/null +++ b/migrations/0001_wandering_ogun.sql @@ -0,0 +1,26 @@ +CREATE TABLE `noodle_subtask` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `title` text NOT NULL, + `notes` text NOT NULL, + `done` integer DEFAULT false, + `doneAt` text, + `task_id` integer NOT NULL, + `createdAt` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updatedAt` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`task_id`) REFERENCES `noodle_task`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `noodle_task` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `title` text NOT NULL, + `notes` text NOT NULL, + `done` integer DEFAULT false, + `doneAt` text, + `dueDate` text NOT NULL, + `priority` text NOT NULL, + `tags` text, + `module_id` integer NOT NULL, + `createdAt` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + `updatedAt` text DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (`module_id`) REFERENCES `noodle_module`(`id`) ON UPDATE no action ON DELETE no action +); diff --git a/migrations/meta/0001_snapshot.json b/migrations/meta/0001_snapshot.json new file mode 100644 index 00000000..fbefbb81 --- /dev/null +++ b/migrations/meta/0001_snapshot.json @@ -0,0 +1,457 @@ +{ + "version": "5", + "dialect": "sqlite", + "id": "a46dbab8-7965-4ddd-9b23-e51540474c87", + "prevId": "14800464-77e1-40db-85cb-670940d14a4c", + "tables": { + "noodle_feedback": { + "name": "noodle_feedback", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "noodle_module": { + "name": "noodle_module", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'graduation-cap'" + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'primary'" + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "lastVisited": { + "name": "lastVisited", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "noodle_notebooks": { + "name": "noodle_notebooks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'Untitled'" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'book'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "moduleId": { + "name": "moduleId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "lastVisited": { + "name": "lastVisited", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "noodle_notebooks_moduleId_noodle_module_id_fk": { + "name": "noodle_notebooks_moduleId_noodle_module_id_fk", + "tableFrom": "noodle_notebooks", + "tableTo": "noodle_module", + "columnsFrom": [ + "moduleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "noodle_subtask": { + "name": "noodle_subtask", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "done": { + "name": "done", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "doneAt": { + "name": "doneAt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "noodle_subtask_task_id_noodle_task_id_fk": { + "name": "noodle_subtask_task_id_noodle_task_id_fk", + "tableFrom": "noodle_subtask", + "tableTo": "noodle_task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "noodle_task": { + "name": "noodle_task", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "done": { + "name": "done", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "doneAt": { + "name": "doneAt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dueDate": { + "name": "dueDate", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "module_id": { + "name": "module_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "noodle_task_module_id_noodle_module_id_fk": { + "name": "noodle_task_module_id_noodle_module_id_fk", + "tableFrom": "noodle_task", + "tableTo": "noodle_module", + "columnsFrom": [ + "module_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "noodle_waitlist": { + "name": "noodle_waitlist", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "invitationSentAt": { + "name": "invitationSentAt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "noodle_waitlist_email_unique": { + "name": "noodle_waitlist_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index d4aed5da..0630fff0 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1698253502434, "tag": "0000_faithful_thaddeus_ross", "breakpoints": true + }, + { + "idx": 1, + "version": "5", + "when": 1698770214982, + "tag": "0001_wandering_ogun", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema/index.ts b/src/db/schema/index.ts index 315abc01..b3208cf3 100644 --- a/src/db/schema/index.ts +++ b/src/db/schema/index.ts @@ -1,4 +1,6 @@ export * from "./feedback"; export * from "./module"; export * from "./notebook"; +export * from "./subtask"; +export * from "./task"; export * from "./waitlist"; diff --git a/src/db/schema/module.ts b/src/db/schema/module.ts index e3149fa4..39fb9fd3 100644 --- a/src/db/schema/module.ts +++ b/src/db/schema/module.ts @@ -4,6 +4,7 @@ import { createInsertSchema, createSelectSchema } from "drizzle-zod"; import { z } from "zod"; import { sqliteTable } from "./noodle_table"; import { notebooks } from "./notebook"; +import { taskTable } from "./task"; export const moduleTable = sqliteTable("module", { id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), @@ -26,6 +27,7 @@ export const moduleTable = sqliteTable("module", { export const moduleTableRelations = relations(moduleTable, ({ many }) => ({ notebooks: many(notebooks), + tasks: many(taskTable) })); export const insertModuleSchema = createInsertSchema(moduleTable, { diff --git a/src/db/schema/subtask.ts b/src/db/schema/subtask.ts index 99e5a043..81a7a735 100644 --- a/src/db/schema/subtask.ts +++ b/src/db/schema/subtask.ts @@ -1,13 +1,13 @@ import { relations, sql } from "drizzle-orm"; -import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import { integer, text } from "drizzle-orm/sqlite-core"; import { createInsertSchema, createSelectSchema } from "drizzle-zod"; -import { task } from "./task"; +import { sqliteTable } from "./noodle_table"; +import { taskTable } from "./task"; -export const subtask = sqliteTable("subTask", { +export const subtaskTable = sqliteTable("subtask", { id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), title: text("title").notNull(), - description: text("description").notNull(), notes: text("notes").notNull(), @@ -15,7 +15,7 @@ export const subtask = sqliteTable("subTask", { doneAt: text("doneAt"), taskId: integer("task_id") - .references(() => task.id) + .references(() => taskTable.id) .notNull(), createdAt: text("createdAt") @@ -27,28 +27,28 @@ export const subtask = sqliteTable("subTask", { .default(sql`CURRENT_TIMESTAMP`), }); -export type Subtask = typeof subtask.$inferSelect; -export type NewSubtask = typeof subtask.$inferInsert; +export type Subtask = typeof subtaskTable.$inferSelect; +export type NewSubtask = typeof subtaskTable.$inferInsert; -export const subtaskRelations = relations(subtask, ({ one }) => ({ - task: one(task), +export const subtaskRelations = relations(subtaskTable, ({ one }) => ({ + task: one(taskTable), })); -export const insertSubtaskSchema = createInsertSchema(subtask).omit({ +export const insertSubtaskSchema = createInsertSchema(subtaskTable).omit({ id: true, createdAt: true, done: true, doneAt: true, }); -export const updateSubtaskSchema = createInsertSchema(subtask).omit({ +export const updateSubtaskSchema = createInsertSchema(subtaskTable).omit({ moduleId: true, taskId: true, doneAt: true, createdAt: true, }); -export const selectSubtaskSchema = createSelectSchema(subtask).omit({ +export const selectSubtaskSchema = createSelectSchema(subtaskTable).omit({ createdAt: true, description: true, dueDate: true, diff --git a/src/db/schema/task.ts b/src/db/schema/task.ts index 69ca8b41..6834a5e9 100644 --- a/src/db/schema/task.ts +++ b/src/db/schema/task.ts @@ -1,14 +1,14 @@ import { relations, sql } from "drizzle-orm"; -import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import { integer, text } from "drizzle-orm/sqlite-core"; import { createInsertSchema, createSelectSchema } from "drizzle-zod"; import { moduleTable } from "./module"; -import { subtask } from "./subtask"; +import { sqliteTable } from "./noodle_table"; +import { subtaskTable } from "./subtask"; -export const task = sqliteTable("task", { +export const taskTable = sqliteTable("task", { id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), title: text("title").notNull(), - description: text("description").notNull(), notes: text("notes").notNull(), @@ -34,30 +34,32 @@ export const task = sqliteTable("task", { .default(sql`CURRENT_TIMESTAMP`), }); -export type Task = typeof task.$inferSelect; -export type NewTask = typeof task.$inferInsert; +export type Task = typeof taskTable.$inferSelect; +export type NewTask = typeof taskTable.$inferInsert; -export const taskRelations = relations(task, ({ one, many }) => ({ - module: one(moduleTable), - subtask: many(subtask), +export const taskRelations = relations(taskTable, ({ one, many }) => ({ + module: one(moduleTable, { + fields: [taskTable.moduleId], + references: [moduleTable.id], + }), + subtask: many(subtaskTable), })); -export const insertTaskSchema = createInsertSchema(task).omit({ +export const insertTaskSchema = createInsertSchema(taskTable).omit({ id: true, done: true, doneAt: true, createdAt: true, }); -export const updateTaskSchema = createInsertSchema(task).omit({ +export const updateTaskSchema = createInsertSchema(taskTable).omit({ moduleId: true, done: true, }); -export const selectTaskSchema = createSelectSchema(task).omit({ +export const selectTaskSchema = createSelectSchema(taskTable).omit({ createdAt: true, description: true, - dueDate: true, priority: true, title: true, }); From d56eea03af717d2fed376cff17ef33c7ebe5bf55 Mon Sep 17 00:00:00 2001 From: Kenny Date: Tue, 31 Oct 2023 13:38:02 -0300 Subject: [PATCH 4/8] feat: task router and subtask router + add tasks to module getter --- src/server/api/root.ts | 4 + src/server/api/routers/module.ts | 5 +- src/server/api/routers/subtask.ts | 97 +++++++++++++++ src/server/api/routers/task.ts | 190 ++++++++++++++++++++++++++++++ 4 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 src/server/api/routers/subtask.ts create mode 100644 src/server/api/routers/task.ts diff --git a/src/server/api/root.ts b/src/server/api/root.ts index e0649c9f..a89395d0 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -1,5 +1,7 @@ import { feedbackRouter } from "./routers/feedback"; import { moduleRouter } from "./routers/module"; +import { subtaskRouter } from "./routers/subtask"; +import { taskRouter } from "./routers/task"; import { waitlistRouter } from "./routers/waitlist"; import { createTRPCRouter } from "./trpc"; @@ -7,6 +9,8 @@ export const appRouter = createTRPCRouter({ waitlist: waitlistRouter, feedback: feedbackRouter, module: moduleRouter, + task: taskRouter, + subtask: subtaskRouter, }); export type AppRouter = typeof appRouter; diff --git a/src/server/api/routers/module.ts b/src/server/api/routers/module.ts index 764fc6e7..4e5e1fdf 100644 --- a/src/server/api/routers/module.ts +++ b/src/server/api/routers/module.ts @@ -1,6 +1,6 @@ import { insertModuleSchema, moduleTable, selectModuleSchema } from "@/db"; -import { createTRPCRouter, protectedProcedure } from "../trpc"; import { and, eq } from "drizzle-orm"; +import { createTRPCRouter, protectedProcedure } from "../trpc"; export const moduleRouter = createTRPCRouter({ get: createTRPCRouter({ @@ -8,6 +8,9 @@ export const moduleRouter = createTRPCRouter({ try { const modules = await ctx.db.query.moduleTable.findMany({ where: (table, { eq }) => eq(table.userId, ctx.auth.userId), + with:{ + tasks: true + } }); return modules; diff --git a/src/server/api/routers/subtask.ts b/src/server/api/routers/subtask.ts new file mode 100644 index 00000000..d8940809 --- /dev/null +++ b/src/server/api/routers/subtask.ts @@ -0,0 +1,97 @@ +import { + insertSubtaskSchema, + moduleTable, + selectSubtaskSchema, + subtaskTable, + taskTable, +} from "@/db"; +import { TRPCError } from "@trpc/server"; +import { and, eq } from "drizzle-orm"; +import { createTRPCRouter, protectedProcedure } from "../trpc"; + +export const subtaskRouter = createTRPCRouter({ + get: createTRPCRouter({ + byTask: protectedProcedure + .input(selectSubtaskSchema.pick({ taskId: true })) + .query(async ({ ctx, input }) => { + try { + const [taskWithModule] = await ctx.db + .select() + .from(taskTable) + .leftJoin(moduleTable, eq(taskTable.moduleId, moduleTable.id)) + .where( + and( + eq(moduleTable.userId, ctx.auth.userId), + eq(taskTable.id, input.taskId), + ), + ) + .limit(1); + + if (!taskWithModule) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + "The task you are trying to get subtasks for does not exist", + }); + } + + const { task } = taskWithModule; + const subtasks = await ctx.db.query.subtaskTable.findMany({ + where: (table, { eq }) => eq(table.taskId, task.id), + }); + + return subtasks; + } catch (err) { + console.error(err); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error ocurred while getting subtasks", + }); + } + }), + }), + post: createTRPCRouter({ + create: protectedProcedure + .input(insertSubtaskSchema) + .mutation(async ({ ctx, input }) => { + try { + const [taskWithModule] = await ctx.db + .select() + .from(taskTable) + .leftJoin(moduleTable, eq(taskTable.moduleId, moduleTable.id)) + .where( + and( + eq(moduleTable.userId, ctx.auth.userId), + eq(taskTable.id, input.taskId), + ), + ) + .limit(1); + + if (!taskWithModule) { + throw new TRPCError({ + code: "NOT_FOUND", + message: + "The task you are trying to add a subtask to does not exist.", + }); + } + + const { task } = taskWithModule; + const createdSubtask = await ctx.db + .insert(subtaskTable) + .values({ + ...input, + taskId: task.id, + }) + .returning(); + + return createdSubtask; + } catch (err) { + console.error(err); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error ocurred while creating subtask.", + }); + } + }), + }), +}); diff --git a/src/server/api/routers/task.ts b/src/server/api/routers/task.ts new file mode 100644 index 00000000..2b36695f --- /dev/null +++ b/src/server/api/routers/task.ts @@ -0,0 +1,190 @@ +import { + insertTaskSchema, + moduleTable, + selectTaskSchema, + taskTable, +} from "@/db"; +import { TRPCError } from "@trpc/server"; +import { and, eq } from "drizzle-orm"; +import { createTRPCRouter, protectedProcedure } from "../trpc"; + +export const taskRouter = createTRPCRouter({ + get: createTRPCRouter({ + all: protectedProcedure.query(async ({ ctx }) => { + try { + const tasks = ctx.db + .select() + .from(taskTable) + .leftJoin(moduleTable, eq(taskTable.moduleId, moduleTable.id)) + .where(eq(moduleTable.userId, ctx.auth.userId)); + + return tasks; + } catch (err) { + console.error(err); + + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error ocurred while getting your tasks.", + }); + } + }), + byModule: protectedProcedure + .input(selectTaskSchema.pick({ moduleId: true })) + .query(async ({ ctx, input }) => { + try { + const tasksWithModule = await ctx.db + .select() + .from(taskTable) + .leftJoin(moduleTable, eq(taskTable.moduleId, moduleTable.id)) + .where( + and( + eq(moduleTable.userId, ctx.auth.userId), + eq(taskTable.moduleId, input.moduleId), + ), + ); + + return tasksWithModule.map((taskWithModule) => taskWithModule.task); + } catch (err) { + console.error(err); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error occurred while getting tasks by module.", + }); + } + }), + byDate: protectedProcedure + .input(selectTaskSchema.pick({ dueDate: true })) + .query(async ({ ctx, input }) => { + try { + const tasksWithModule = await ctx.db + .select() + .from(taskTable) + .leftJoin(moduleTable, eq(taskTable.moduleId, moduleTable.id)) + .where( + and( + eq(taskTable.dueDate, input.dueDate), + eq(moduleTable.userId, ctx.auth.userId), + ), + ); + + return tasksWithModule.map((taskWithModule) => taskWithModule.task); + } catch (err) { + console.error(err); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error occurred while getting tasks by date.", + }); + } + }), + }), + post: createTRPCRouter({ + create: protectedProcedure + .input(insertTaskSchema) + .mutation(async ({ ctx, input }) => { + try { + const createdTask = await ctx.db + .insert(taskTable) + .values({ + ...input, + tags: [], + }) + .returning(); + + return createdTask; + } catch (err) { + console.error(err); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error occurred while creating the task.", + }); + } + }), + repeat: protectedProcedure + .input(selectTaskSchema.pick({ id: true })) + .mutation(async ({ ctx, input }) => { + try { + const [taskWithModule] = await ctx.db + .select() + .from(taskTable) + .leftJoin(moduleTable, eq(moduleTable.id, taskTable.moduleId)) + .where( + and( + eq(taskTable.id, input.id), + eq(moduleTable.userId, ctx.auth.userId), + ), + ) + .limit(1); + + if (!taskWithModule) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "The task you are trying to repeat does not exist.", + }); + } + + const { id, createdAt, updatedAt, done, doneAt, ...rest } = + taskWithModule.task; + const repeatedTask = await ctx.db + .insert(taskTable) + .values({ + ...rest, + }) + .returning(); + + return repeatedTask; + } catch (err) { + console.error(err); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error occurred while repeating the task.", + }); + } + }), + }), + patch: createTRPCRouter({ + switchDone: protectedProcedure + .input(selectTaskSchema.pick({ id: true })) + .mutation(async ({ ctx, input }) => { + try { + const [taskWithModule] = await ctx.db + .select() + .from(taskTable) + .leftJoin(moduleTable, eq(moduleTable.id, taskTable.moduleId)) + .where( + and( + eq(taskTable.id, input.id), + eq(moduleTable.userId, ctx.auth.userId), + ), + ) + .limit(1); + + if (!taskWithModule) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `The task you are trying to mark as done does not exist`, + }); + } + + const { task } = taskWithModule; + + const res = await ctx.db + .update(taskTable) + .set({ + done: !task.done, + doneAt: !task.done ? new Date().toString() : null, + }) + .where(eq(taskTable.id, input.id)) + .returning(); + + return res; + } catch (err) { + console.error(err); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "An error occurred while switching the task's done status.", + }); + } + }), + }), +}); From b83cb11cc493bf7e2b25f352d449e235ee693ac7 Mon Sep 17 00:00:00 2001 From: Kenny Date: Tue, 31 Oct 2023 13:38:14 -0300 Subject: [PATCH 5/8] feat: use real tasks for module card --- .../(dashboard)/_components/module-card.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/app/(dashboard)/_components/module-card.tsx b/src/app/(dashboard)/_components/module-card.tsx index 9facc601..ff339dc5 100644 --- a/src/app/(dashboard)/_components/module-card.tsx +++ b/src/app/(dashboard)/_components/module-card.tsx @@ -1,4 +1,5 @@ import { Icon, type IconNames } from "@/components/icon"; +import { Task } from "@/db"; import { cn } from "@/utils/cn"; import { convertHexToRGBA, primary } from "@/utils/colors"; import { Card } from "@nextui-org/card"; @@ -14,6 +15,7 @@ type ModuleCardProps = { name: string; icon: IconNames; credits: number; + tasks: Task[]; }; export const ModuleCard: FC = ({ @@ -22,10 +24,17 @@ export const ModuleCard: FC = ({ name, icon, credits, + tasks, }) => { const moduleColor = color === "primary" ? primary : colors[color as keyof typeof colors]; + const undone = tasks.filter((task) => !task.done); + const done = tasks.filter((task) => task.done); + const donePercentage = tasks.length + ? (done.length / tasks.length) * 100 + : 100; + return (
  • = ({

    {credits} Credits

    -

    8 Tasks remaining

    +

    + {undone.length} Tasks remaining +

    From 07fd0885c17a035200e37059a73441a7a68e2ec0 Mon Sep 17 00:00:00 2001 From: Kenny Date: Thu, 2 Nov 2023 19:49:10 -0300 Subject: [PATCH 6/8] feat: add delete by id router ep --- src/server/api/routers/subtask.ts | 42 +++++++++++++++++++++++++++++++ src/server/api/routers/task.ts | 41 ++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/src/server/api/routers/subtask.ts b/src/server/api/routers/subtask.ts index d8940809..29c46de5 100644 --- a/src/server/api/routers/subtask.ts +++ b/src/server/api/routers/subtask.ts @@ -94,4 +94,46 @@ export const subtaskRouter = createTRPCRouter({ } }), }), + delete: createTRPCRouter({ + byId: protectedProcedure + .input(selectSubtaskSchema.pick({ id: true })) + .mutation(async ({ ctx, input }) => { + try { + const [subtaskWithTask] = await ctx.db + .select() + .from(subtaskTable) + .leftJoin(taskTable, eq(taskTable.id, subtaskTable.taskId)) + .leftJoin(moduleTable, eq(moduleTable.id, taskTable.moduleId)) + .where( + and( + eq(subtaskTable.id, input.id), + eq(moduleTable.userId, ctx.auth.userId), + ), + ) + .limit(1); + + if (!subtaskWithTask) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `The task you are trying to delete does not exist`, + }); + } + + const { subtask } = subtaskWithTask; + + const deleteResult = await ctx.db + .delete(subtaskTable) + .where(eq(subtaskTable.id, subtask.id)) + .returning(); + + return deleteResult; + } catch (err) { + console.error(err); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error occurred while deleting the task.", + }); + } + }), + }), }); diff --git a/src/server/api/routers/task.ts b/src/server/api/routers/task.ts index 2b36695f..225d9758 100644 --- a/src/server/api/routers/task.ts +++ b/src/server/api/routers/task.ts @@ -187,4 +187,45 @@ export const taskRouter = createTRPCRouter({ } }), }), + delete: createTRPCRouter({ + byId: protectedProcedure + .input(selectTaskSchema.pick({ id: true })) + .mutation(async ({ ctx, input }) => { + try { + const [taskWithModule] = await ctx.db + .select() + .from(taskTable) + .leftJoin(moduleTable, eq(moduleTable.id, taskTable.moduleId)) + .where( + and( + eq(taskTable.id, input.id), + eq(moduleTable.userId, ctx.auth.userId), + ), + ) + .limit(1); + + if (!taskWithModule) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `The task you are trying to delete does not exist`, + }); + } + + const { task } = taskWithModule; + + const deleteResult = await ctx.db + .delete(taskTable) + .where(eq(taskTable.id, task.id)) + .returning(); + + return deleteResult; + } catch (err) { + console.error(err); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error occurred while deleting the task.", + }); + } + }), + }), }); From 15a196a8bd038e74c25fe7a339c9f1fc32bbb34f Mon Sep 17 00:00:00 2001 From: Kenny Date: Sun, 5 Nov 2023 13:26:03 -0300 Subject: [PATCH 7/8] feat: add tag + project schemas --- src/db/schema/project.ts | 25 +++++++++++++++++++ src/db/schema/tag.ts | 52 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 src/db/schema/project.ts create mode 100644 src/db/schema/tag.ts diff --git a/src/db/schema/project.ts b/src/db/schema/project.ts new file mode 100644 index 00000000..ccf16e4f --- /dev/null +++ b/src/db/schema/project.ts @@ -0,0 +1,25 @@ +import { relations, sql } from "drizzle-orm"; +import { text } from "drizzle-orm/sqlite-core"; +import { sqliteTable } from "./noodle_table"; +import { taskTable } from "./task"; + +export const projectTable = sqliteTable("project", { + id: text("id").notNull(), + + userId: text("userId").notNull(), + + name: text("title").notNull(), + description: text("description").notNull(), + + createdAt: text("createdAt") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + + updatedAt: text("updatedAt") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const projectRelations = relations(projectTable, ({ many }) => ({ + tasks: many(taskTable), +})); diff --git a/src/db/schema/tag.ts b/src/db/schema/tag.ts new file mode 100644 index 00000000..80ab1c8d --- /dev/null +++ b/src/db/schema/tag.ts @@ -0,0 +1,52 @@ +import { relations, sql } from "drizzle-orm"; +import { primaryKey, text } from "drizzle-orm/sqlite-core"; +import { sqliteTable } from "./noodle_table"; +import { taskTable } from "./task"; + +export const tagTable = sqliteTable("tag", { + id: text("id").notNull(), + + userId: text("userId").notNull(), + + name: text("name").notNull(), + + color: text("color").notNull().default("primary"), + + createdAt: text("createdAt") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + + updatedAt: text("updatedAt") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const tagRelation = relations(tagTable, ({ many }) => ({ + tasks: many(taskTable), +})); + +export const tasksToTags = sqliteTable( + "tasksToTags", + { + taskId: text("taskId") + .notNull() + .references(() => taskTable.id), + tagId: text("tagId") + .notNull() + .references(() => tagTable.id), + }, + (t) => ({ + pk: primaryKey(t.taskId, t.tagId), + }), +); + +export const tasksToTagsRelations = relations(tasksToTags, ({ one }) => ({ + task: one(taskTable, { + fields: [tasksToTags.taskId], + references: [taskTable.id], + }), + tag: one(tagTable, { + fields: [tasksToTags.tagId], + references: [tagTable.id], + }), +})); From fc5721bf4dfb67e031b5f127ac0799a90cbad6d9 Mon Sep 17 00:00:00 2001 From: Kenny Date: Sun, 5 Nov 2023 13:26:38 -0300 Subject: [PATCH 8/8] feat: update task and subtask schemas --- src/db/schema/subtask.ts | 6 ++---- src/db/schema/task.ts | 16 ++++++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/db/schema/subtask.ts b/src/db/schema/subtask.ts index 81a7a735..d803d468 100644 --- a/src/db/schema/subtask.ts +++ b/src/db/schema/subtask.ts @@ -9,10 +9,8 @@ export const subtaskTable = sqliteTable("subtask", { title: text("title").notNull(), - notes: text("notes").notNull(), - - done: integer("done", { mode: "boolean" }).default(false), - doneAt: text("doneAt"), + completed: integer("completed", { mode: "boolean" }).default(false), + completedAt: text("completedAt"), taskId: integer("task_id") .references(() => taskTable.id) diff --git a/src/db/schema/task.ts b/src/db/schema/task.ts index 6834a5e9..96e3baaf 100644 --- a/src/db/schema/task.ts +++ b/src/db/schema/task.ts @@ -4,6 +4,7 @@ import { createInsertSchema, createSelectSchema } from "drizzle-zod"; import { moduleTable } from "./module"; import { sqliteTable } from "./noodle_table"; import { subtaskTable } from "./subtask"; +import { tagTable } from "./tag"; export const taskTable = sqliteTable("task", { id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), @@ -12,12 +13,14 @@ export const taskTable = sqliteTable("task", { notes: text("notes").notNull(), - done: integer("done", { mode: "boolean" }).default(false), - doneAt: text("doneAt"), + completed: integer("completed", { mode: "boolean" }).default(false), + completedAt: text("completedAt"), dueDate: text("dueDate").notNull(), + personalDueDate: text("dueDate").notNull(), + reminderDate: text("reminderDate").notNull(), - priority: text("priority", { enum: ["LOW", "MEDIUM", "HIGH"] }).notNull(), + priority: text("priority", { enum: ["LOW", "MEDIUM", "URGENT"] }).notNull(), tags: text("tags", { mode: "json" }).$type(), @@ -34,14 +37,12 @@ export const taskTable = sqliteTable("task", { .default(sql`CURRENT_TIMESTAMP`), }); -export type Task = typeof taskTable.$inferSelect; -export type NewTask = typeof taskTable.$inferInsert; - export const taskRelations = relations(taskTable, ({ one, many }) => ({ module: one(moduleTable, { fields: [taskTable.moduleId], references: [moduleTable.id], }), + tags: many(tagTable), subtask: many(subtaskTable), })); @@ -63,3 +64,6 @@ export const selectTaskSchema = createSelectSchema(taskTable).omit({ priority: true, title: true, }); + +export type Task = typeof taskTable.$inferSelect; +export type NewTask = typeof taskTable.$inferInsert;