From 150334d029ebd755367e2d3555e0af6bc55559a3 Mon Sep 17 00:00:00 2001 From: Mai Nguyen <123816878+in-mai-space@users.noreply.github.com> Date: Wed, 8 Jan 2025 23:08:45 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=AB=A8=20fix:=20apply=20schema=20changes,?= =?UTF-8?q?=20and=20fix=20associated=20tests=20and=20fe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTRIBUTING.md | 5 - backend/src/database/migrate.ts | 21 +- backend/src/database/reset.ts | 2 - backend/src/entities/schema.ts | 22 +- backend/src/entities/users/service.ts | 4 - backend/src/entities/users/transaction.ts | 2 +- .../20250109030057_curious_proteus.sql | 22 + .../meta/20250109030057_snapshot.json | 679 ++++++++++++++++++ backend/src/migrations/meta/_journal.json | 7 + backend/src/tests/user/create.test.ts | 14 +- backend/src/tests/user/full.test.ts | 5 +- backend/src/tests/user/get.test.ts | 4 +- backend/src/tests/user/update.test.ts | 6 +- backend/src/types/api/routes/healthcheck.ts | 2 +- backend/src/types/api/schemas/error.ts | 2 +- frontend/app/(auth)/components/login-form.tsx | 5 +- .../app/(auth)/components/register-form.tsx | 8 +- frontend/auth/store.ts | 4 +- frontend/package.json | 6 +- openapi.yaml | 46 +- taskfile.yaml | 4 - 21 files changed, 779 insertions(+), 91 deletions(-) create mode 100644 backend/src/migrations/20250109030057_curious_proteus.sql create mode 100644 backend/src/migrations/meta/20250109030057_snapshot.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e34d853..a7b933a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -150,11 +150,6 @@ task backend:prod task frontend:dev ``` -Or run both of them at the same time -```bash -task start -``` - ----- ### Add new libraries or packages diff --git a/backend/src/database/migrate.ts b/backend/src/database/migrate.ts index 8a2b0ae..e134883 100644 --- a/backend/src/database/migrate.ts +++ b/backend/src/database/migrate.ts @@ -3,15 +3,16 @@ import { migrate } from "drizzle-orm/postgres-js/migrator"; import { Configuration } from "../types/config"; export const automigrateDB = async (db: PostgresJsDatabase, config: Configuration) => { - const originalLog = console.log; - console.log = () => {}; - - try { - await migrate(db, config.automigrate); - } catch (error) { - console.error(error); - console.log("Failed to auto-migrate database"); - } finally { - console.log = originalLog; + if (config.environment !== "production") { + const originalLog = console.log; + console.log = () => {}; + try { + await migrate(db, config.automigrate); + } catch (error) { + console.error(error); + console.log("Failed to auto-migrate database"); + } finally { + console.log = originalLog; + } } }; diff --git a/backend/src/database/reset.ts b/backend/src/database/reset.ts index 25ec7c9..2397671 100644 --- a/backend/src/database/reset.ts +++ b/backend/src/database/reset.ts @@ -8,7 +8,6 @@ import { notificationsTable, invitationsTable, linksTable, - mediaTable, likesTable, commentsTable, } from "../entities/schema"; @@ -22,7 +21,6 @@ export const resetDB = async (db: PostgresJsDatabase) => { notificationsTable, invitationsTable, linksTable, - mediaTable, likesTable, commentsTable, }; diff --git a/backend/src/entities/schema.ts b/backend/src/entities/schema.ts index 3e38adb..ff8a694 100644 --- a/backend/src/entities/schema.ts +++ b/backend/src/entities/schema.ts @@ -1,9 +1,7 @@ -import { timestamp, uuid, pgEnum, pgTable, varchar } from "drizzle-orm/pg-core"; +import { timestamp, uuid, pgEnum, pgTable, varchar, boolean } from "drizzle-orm/pg-core"; import { relations } from "drizzle-orm"; -export const ageGroupEnum = pgEnum("ageGroup", ["CHILD", "TEEN", "ADULT", "SENIOR"]); export const userModeEnum = pgEnum("mode", ["BASIC", "ADVANCED"]); -export const mediaTypeEnum = pgEnum("mediaType", ["IMAGE", "VIDEO", "AUDIO"]); export const memberRoleEnum = pgEnum("role", ["MEMBER", "MANAGER"]); export const referenceTypeEnum = pgEnum("referenceType", [ "POST", @@ -18,9 +16,9 @@ export const usersTable = pgTable("users", { id: uuid().primaryKey().defaultRandom(), name: varchar({ length: 100 }).notNull(), username: varchar({ length: 100 }).notNull().unique(), - ageGroup: ageGroupEnum().notNull(), mode: userModeEnum().notNull().default("BASIC"), profilePhoto: varchar(), + notificationsEnabled: boolean().notNull().default(true), deviceTokens: varchar({ length: 152 }).array().default([]), }); @@ -43,19 +41,7 @@ export const postsTable = pgTable("posts", { .references(() => usersTable.id, { onDelete: "cascade" }), createdAt: timestamp().notNull().defaultNow(), caption: varchar({ length: 500 }), - thumbnail: varchar(), -}); - -export const mediaTable = pgTable("media", { - id: uuid().primaryKey().defaultRandom(), - mediaType: mediaTypeEnum().notNull(), - media: varchar().notNull(), - postId: uuid() - .notNull() - .references(() => postsTable.id, { onDelete: "cascade" }), - commentId: uuid() - .notNull() - .references(() => commentsTable.id, { onDelete: "cascade" }), + media: varchar().array().default([]), }); export const membersTable = pgTable("members", { @@ -88,6 +74,7 @@ export const commentsTable = pgTable("comments", { .notNull() .references(() => postsTable.id, { onDelete: "cascade" }), content: varchar({ length: 500 }), + voiceMemo: varchar(), }); export const notificationsTable = pgTable("notifications", { @@ -154,7 +141,6 @@ export const postRelations = relations(postsTable, ({ one, many }) => ({ fields: [postsTable.userId], references: [usersTable.id], }), - media: many(mediaTable), comments: many(commentsTable), likes: many(likesTable), })); diff --git a/backend/src/entities/users/service.ts b/backend/src/entities/users/service.ts index 24a3dc6..4d60c1c 100644 --- a/backend/src/entities/users/service.ts +++ b/backend/src/entities/users/service.ts @@ -1,5 +1,3 @@ -import { Mode } from "../../constants/database"; -import { AgeGroup } from "../../constants/database"; import { InternalServerError, NotFoundError } from "../../utilities/errors/app-error"; import { handleServiceError } from "../../utilities/errors/service-error"; import { CreateUserPayload, UpdateUserPayload, User } from "./validator"; @@ -23,9 +21,7 @@ export class UserServiceImpl implements UserService { async createUser(payload: CreateUserPayload): Promise { const createUserImpl = async () => { - const mode = payload.ageGroup === AgeGroup.SENIOR ? Mode.BASIC : Mode.ADVANCED; const user = await this.userTransaction.insertUser({ - mode, ...payload, }); if (!user) { diff --git a/backend/src/entities/users/transaction.ts b/backend/src/entities/users/transaction.ts index fb2b0f4..5f7311e 100644 --- a/backend/src/entities/users/transaction.ts +++ b/backend/src/entities/users/transaction.ts @@ -35,10 +35,10 @@ export class UserTransactionImpl implements UserTransaction { .set({ name: payload.name, username: payload.username, - ageGroup: payload.ageGroup, mode: payload.mode, profilePhoto: payload.profilePhoto, deviceTokens: payload.deviceTokens, + notificationsEnabled: payload.notificationsEnabled, }) .where(eq(usersTable.id, id)) .returning(); diff --git a/backend/src/migrations/20250109030057_curious_proteus.sql b/backend/src/migrations/20250109030057_curious_proteus.sql new file mode 100644 index 0000000..4f7b570 --- /dev/null +++ b/backend/src/migrations/20250109030057_curious_proteus.sql @@ -0,0 +1,22 @@ +DROP TABLE IF EXISTS "media" CASCADE; -- statement-breakpoint + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = 'posts' AND column_name = 'thumbnail' + ) THEN + ALTER TABLE "posts" RENAME COLUMN "thumbnail" TO "media"; + END IF; +END $$; -- statement-breakpoint + +ALTER TABLE "comments" ADD COLUMN IF NOT EXISTS "voiceMemo" varchar; -- statement-breakpoint + +ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "notificationsEnabled" boolean DEFAULT true NOT NULL; -- statement-breakpoint + +ALTER TABLE "users" DROP COLUMN IF EXISTS "ageGroup"; -- statement-breakpoint + +DROP TYPE IF EXISTS "public"."ageGroup"; -- statement-breakpoint + +DROP TYPE IF EXISTS "public"."mediaType"; -- statement-breakpoint diff --git a/backend/src/migrations/meta/20250109030057_snapshot.json b/backend/src/migrations/meta/20250109030057_snapshot.json new file mode 100644 index 0000000..342d281 --- /dev/null +++ b/backend/src/migrations/meta/20250109030057_snapshot.json @@ -0,0 +1,679 @@ +{ + "id": "fe236de3-c346-4b02-80fe-e041973d8f49", + "prevId": "4fb32a94-662e-4ae9-83b3-55e303bc5463", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.comments": { + "name": "comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "postId": { + "name": "postId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "voiceMemo": { + "name": "voiceMemo", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "comments_userId_users_id_fk": { + "name": "comments_userId_users_id_fk", + "tableFrom": "comments", + "tableTo": "users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comments_postId_posts_id_fk": { + "name": "comments_postId_posts_id_fk", + "tableFrom": "comments", + "tableTo": "posts", + "columnsFrom": ["postId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.groups": { + "name": "groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "managerId": { + "name": "managerId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "groups_managerId_users_id_fk": { + "name": "groups_managerId_users_id_fk", + "tableFrom": "groups", + "tableTo": "users", + "columnsFrom": ["managerId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitations": { + "name": "invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "groupId": { + "name": "groupId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invitationLinkId": { + "name": "invitationLinkId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "recipientId": { + "name": "recipientId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invitations_groupId_groups_id_fk": { + "name": "invitations_groupId_groups_id_fk", + "tableFrom": "invitations", + "tableTo": "groups", + "columnsFrom": ["groupId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitations_invitationLinkId_links_id_fk": { + "name": "invitations_invitationLinkId_links_id_fk", + "tableFrom": "invitations", + "tableTo": "links", + "columnsFrom": ["invitationLinkId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitations_recipientId_users_id_fk": { + "name": "invitations_recipientId_users_id_fk", + "tableFrom": "invitations", + "tableTo": "users", + "columnsFrom": ["recipientId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.likes": { + "name": "likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "postId": { + "name": "postId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "likes_userId_users_id_fk": { + "name": "likes_userId_users_id_fk", + "tableFrom": "likes", + "tableTo": "users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "likes_postId_posts_id_fk": { + "name": "likes_postId_posts_id_fk", + "tableFrom": "likes", + "tableTo": "posts", + "columnsFrom": ["postId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.links": { + "name": "links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "groupId": { + "name": "groupId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "links_groupId_groups_id_fk": { + "name": "links_groupId_groups_id_fk", + "tableFrom": "links", + "tableTo": "groups", + "columnsFrom": ["groupId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.members": { + "name": "members", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "groupId": { + "name": "groupId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "joinedAt": { + "name": "joinedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'MEMBER'" + } + }, + "indexes": {}, + "foreignKeys": { + "members_userId_users_id_fk": { + "name": "members_userId_users_id_fk", + "tableFrom": "members", + "tableTo": "users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "members_groupId_groups_id_fk": { + "name": "members_groupId_groups_id_fk", + "tableFrom": "members", + "tableTo": "groups", + "columnsFrom": ["groupId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "actorId": { + "name": "actorId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "receiverId": { + "name": "receiverId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "groupId": { + "name": "groupId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "referenceType": { + "name": "referenceType", + "type": "referenceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "postId": { + "name": "postId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "commentId": { + "name": "commentId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "likeId": { + "name": "likeId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "invitationId": { + "name": "invitationId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(300)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "notifications_actorId_users_id_fk": { + "name": "notifications_actorId_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": ["actorId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_receiverId_users_id_fk": { + "name": "notifications_receiverId_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": ["receiverId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_groupId_groups_id_fk": { + "name": "notifications_groupId_groups_id_fk", + "tableFrom": "notifications", + "tableTo": "groups", + "columnsFrom": ["groupId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_postId_posts_id_fk": { + "name": "notifications_postId_posts_id_fk", + "tableFrom": "notifications", + "tableTo": "posts", + "columnsFrom": ["postId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_commentId_comments_id_fk": { + "name": "notifications_commentId_comments_id_fk", + "tableFrom": "notifications", + "tableTo": "comments", + "columnsFrom": ["commentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_likeId_likes_id_fk": { + "name": "notifications_likeId_likes_id_fk", + "tableFrom": "notifications", + "tableTo": "likes", + "columnsFrom": ["likeId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_invitationId_invitations_id_fk": { + "name": "notifications_invitationId_invitations_id_fk", + "tableFrom": "notifications", + "tableTo": "invitations", + "columnsFrom": ["invitationId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.posts": { + "name": "posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "groupId": { + "name": "groupId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "caption": { + "name": "caption", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "media": { + "name": "media", + "type": "varchar[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + } + }, + "indexes": {}, + "foreignKeys": { + "posts_groupId_groups_id_fk": { + "name": "posts_groupId_groups_id_fk", + "tableFrom": "posts", + "tableTo": "groups", + "columnsFrom": ["groupId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "posts_userId_users_id_fk": { + "name": "posts_userId_users_id_fk", + "tableFrom": "posts", + "tableTo": "users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'BASIC'" + }, + "profilePhoto": { + "name": "profilePhoto", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "notificationsEnabled": { + "name": "notificationsEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "deviceTokens": { + "name": "deviceTokens", + "type": "varchar(152)[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": ["username"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.status": { + "name": "status", + "schema": "public", + "values": ["PENDING", "ACCEPTED"] + }, + "public.role": { + "name": "role", + "schema": "public", + "values": ["MEMBER", "MANAGER"] + }, + "public.referenceType": { + "name": "referenceType", + "schema": "public", + "values": ["POST", "COMMENT", "LIKE", "INVITE", "NUDGE"] + }, + "public.mode": { + "name": "mode", + "schema": "public", + "values": ["BASIC", "ADVANCED"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/backend/src/migrations/meta/_journal.json b/backend/src/migrations/meta/_journal.json index 2fb3756..8047162 100644 --- a/backend/src/migrations/meta/_journal.json +++ b/backend/src/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1736182586903, "tag": "20250106165626_nosy_old_lace", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1736391657831, + "tag": "20250109030057_curious_proteus", + "breakpoints": true } ] } diff --git a/backend/src/tests/user/create.test.ts b/backend/src/tests/user/create.test.ts index 15acade..29ae932 100644 --- a/backend/src/tests/user/create.test.ts +++ b/backend/src/tests/user/create.test.ts @@ -13,7 +13,6 @@ describe("POST /users", () => { const requestBody = { name: "Jane Doe", username: "janedoe", - ageGroup: "TEEN", mode: "BASIC", }; @@ -45,16 +44,16 @@ describe("POST /users", () => { deviceTokens: [], mode: "BASIC", profilePhoto: null, + notificationsEnabled: true, }) .assertFieldNotEqual("id", testId); }); - const fields = ["name", "username", "ageGroup"]; + const fields = ["name", "username"]; it.each(fields)("should return 400 if missing %s", async (field) => { const requestBody = { name: field !== "name" ? "Jane Doe" : undefined, username: field !== "username" ? "jdoe" : undefined, - ageGroup: field !== "ageGroup" ? "ADULT" : undefined, }; ( @@ -101,7 +100,7 @@ describe("POST /users", () => { }); }); - it("should return 400 if invalid age group", async () => { + it("should return 400 if invalid mode", async () => { ( await testBuilder.request({ app, @@ -110,7 +109,7 @@ describe("POST /users", () => { requestBody: { name: "jane doe", username: "jdoe", - ageGroup: "BABY", + mode: "EASY", }, }) ) @@ -119,9 +118,8 @@ describe("POST /users", () => { message: "Validation failed", errors: [ { - message: - "Invalid enum value. Expected 'CHILD' | 'TEEN' | 'ADULT' | 'SENIOR', received 'BABY'", - path: "ageGroup", + message: "Invalid enum value. Expected 'BASIC' | 'ADVANCED', received 'EASY'", + path: "mode", }, ], }); diff --git a/backend/src/tests/user/full.test.ts b/backend/src/tests/user/full.test.ts index 85465ee..5299246 100644 --- a/backend/src/tests/user/full.test.ts +++ b/backend/src/tests/user/full.test.ts @@ -11,12 +11,11 @@ describe("End-to-end User CRUD", () => { const originalBody = { name: "Jane Doe", username: "janedoe", - ageGroup: "SENIOR", + mode: "BASIC", }; const updatedBody = { name: "John Smith", username: "johnsmith", - ageGroup: "SENIOR", mode: "ADVANCED", }; const userId = generateUUID(); @@ -49,6 +48,7 @@ describe("End-to-end User CRUD", () => { deviceTokens: [], mode: "BASIC", profilePhoto: null, + notificationsEnabled: true, }); }); @@ -67,6 +67,7 @@ describe("End-to-end User CRUD", () => { deviceTokens: [], mode: "BASIC", profilePhoto: null, + notificationsEnabled: true, }); }); diff --git a/backend/src/tests/user/get.test.ts b/backend/src/tests/user/get.test.ts index 61663c5..68b5860 100644 --- a/backend/src/tests/user/get.test.ts +++ b/backend/src/tests/user/get.test.ts @@ -11,7 +11,6 @@ describe("GET /users/:id", () => { const requestBody = { name: "Jane Doe", username: "janedoe", - ageGroup: "TEEN", }; beforeAll(async () => { @@ -40,7 +39,8 @@ describe("GET /users/:id", () => { id, ...requestBody, deviceTokens: [], - mode: "ADVANCED", + mode: "BASIC", + notificationsEnabled: true, profilePhoto: null, }) .assertStatusCode(Status.OK); diff --git a/backend/src/tests/user/update.test.ts b/backend/src/tests/user/update.test.ts index 005d5cf..3ae07e7 100644 --- a/backend/src/tests/user/update.test.ts +++ b/backend/src/tests/user/update.test.ts @@ -11,7 +11,8 @@ describe("PUT /users/me", () => { const requestBody = { name: "Jane Doe", username: "janedoe", - ageGroup: "TEEN", + mode: "BASIC", + notificationsEnabled: false, }; const userId = generateUUID(); const jwt = generateJWTToken(3600, getConfigurations().authorization.jwtSecretKey, userId); @@ -40,7 +41,6 @@ describe("PUT /users/me", () => { .assertFields({ name: "Jane Doe", username: "janedoe", - ageGroup: "TEEN", }); const irrelevantID = generateUUID(); @@ -53,7 +53,6 @@ describe("PUT /users/me", () => { requestBody: { id: irrelevantID, // ignored field name: "John Smith", - ageGroup: "SENIOR", mode: "BASIC", }, ...authPayload, @@ -62,7 +61,6 @@ describe("PUT /users/me", () => { .assertFields({ name: "John Smith", username: "janedoe", - ageGroup: "SENIOR", mode: "BASIC", id: userId, }) diff --git a/backend/src/types/api/routes/healthcheck.ts b/backend/src/types/api/routes/healthcheck.ts index 699b88f..78ade51 100644 --- a/backend/src/types/api/routes/healthcheck.ts +++ b/backend/src/types/api/routes/healthcheck.ts @@ -1,5 +1,5 @@ import { TypedResponse } from "hono"; -import { paths } from "../../../gen/schema"; +import { paths } from "../../../gen/openapi"; export type HEALTHCHECK = TypedResponse< paths["/healthcheck"]["get"]["responses"]["200"]["content"]["application/json"] diff --git a/backend/src/types/api/schemas/error.ts b/backend/src/types/api/schemas/error.ts index 6ba593b..3700624 100644 --- a/backend/src/types/api/schemas/error.ts +++ b/backend/src/types/api/schemas/error.ts @@ -1,3 +1,3 @@ -import { components } from "../../../gen/schema"; +import { components } from "../../../gen/openapi"; export type API_ERROR = components["schemas"]["ValidationError"] | components["schemas"]["Error"]; diff --git a/frontend/app/(auth)/components/login-form.tsx b/frontend/app/(auth)/components/login-form.tsx index e822ea4..28f66d9 100644 --- a/frontend/app/(auth)/components/login-form.tsx +++ b/frontend/app/(auth)/components/login-form.tsx @@ -36,9 +36,10 @@ const LoginForm = () => { if (isAuthenticated) { router.push("/(app)/(tabs)"); } - } catch (err: any) { + } catch (err: unknown) { if (err instanceof ZodError) { - Alert.alert(err.errors[0].message); + const errorMessages = err.errors.map((error) => error.message).join("\n"); + Alert.alert("Validation Errors", errorMessages); } } }; diff --git a/frontend/app/(auth)/components/register-form.tsx b/frontend/app/(auth)/components/register-form.tsx index ac9ae18..9f02493 100644 --- a/frontend/app/(auth)/components/register-form.tsx +++ b/frontend/app/(auth)/components/register-form.tsx @@ -9,6 +9,7 @@ import Input from "@/design-system/components/input"; import Button from "@/design-system/components/button"; import { AuthRequest } from "@/types/auth"; import Box from "@/design-system/base/box"; +import { Mode } from "@/types/mode"; type RegisterFormData = AuthRequest & { name: string; @@ -52,7 +53,7 @@ const RegisterForm = () => { const validData = REGISTER_SCHEMA.parse(signupData); const data = { ...validData, - ageGroup: "TEEN", + mode: "BASIC" as Mode, }; await register(data); @@ -60,9 +61,10 @@ const RegisterForm = () => { if (isAuthenticated) { router.push("/(app)/(tabs)"); } - } catch (err: any) { + } catch (err: unknown) { if (err instanceof ZodError) { - Alert.alert(err.errors[0].message); + const errorMessages = err.errors.map((error) => error.message).join("\n"); + Alert.alert("Validation Errors", errorMessages); } } }; diff --git a/frontend/auth/store.ts b/frontend/auth/store.ts index 287570b..e4e859a 100644 --- a/frontend/auth/store.ts +++ b/frontend/auth/store.ts @@ -65,7 +65,7 @@ export const useAuthStore = create()( register: async ({ name, username, - ageGroup, + mode, email, password, }: CreateUserPayload & AuthRequest) => { @@ -75,7 +75,7 @@ export const useAuthStore = create()( email, password, }); - const user = await createUser({ name, username, ageGroup }); + const user = await createUser({ name, username, mode }); set({ mode: user.mode as Mode, }); diff --git a/frontend/package.json b/frontend/package.json index 98c95bd..b9f8712 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,7 +25,7 @@ "@shopify/restyle": "^2.4.4", "@supabase/supabase-js": "^2.47.6", "@tanstack/react-query": "^5.62.0", - "expo": "^52.0.20", + "expo": "^52.0.24", "expo-blur": "~14.0.1", "expo-constants": "~17.0.3", "expo-device": "^7.0.1", @@ -33,9 +33,9 @@ "expo-haptics": "~14.0.0", "expo-linking": "~7.0.3", "expo-notifications": "^0.29.11", - "expo-router": "4.0.15", + "expo-router": "4.0.16", "expo-secure-store": "~14.0.0", - "expo-splash-screen": "^0.29.18", + "expo-splash-screen": "^0.29.19", "expo-status-bar": "~2.0.0", "expo-symbols": "~0.2.0", "expo-system-ui": "^4.0.6", diff --git a/openapi.yaml b/openapi.yaml index 11fbb48..74a9343 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -56,14 +56,10 @@ components: format: uuid example: 5e91507e-5630-4efd-9fd4-799178870b10 description: A unique identifier for the user. - ageGroup: - type: string - enum: ["CHILD", "TEEN", "ADULT", "SENIOR"] - description: The age group of the user. mode: type: string enum: ["BASIC", "ADVANCED", null] - description: The mode for the user (e.g., "BASIC" or "ADVANCED"). + description: The mode for the user (e.g., "BASIC" or "ADVANCED"). Default to "BASIC" if no mode is provided. profilePhoto: type: string nullable: true @@ -74,11 +70,12 @@ components: type: string nullable: true description: The device tokens for notifications. + notificationsEnabled: + type: boolean + description: True when user enables notification. required: - name - username - - id - - ageGroup - mode paths: @@ -119,10 +116,6 @@ paths: type: string minLength: 1 description: The username of the user. - ageGroup: - type: string - enum: ["CHILD", "TEEN", "ADULT", "SENIOR"] - description: The age group of the user. mode: type: string enum: ["BASIC", "ADVANCED", null] @@ -140,7 +133,6 @@ paths: required: - name - username - - ageGroup additionalProperties: true responses: 201: @@ -235,15 +227,31 @@ paths: schema: type: object properties: - firstName: + name: + type: string + minLength: 1 + description: Display name of the user. + username: type: string - description: The first name of the user. - lastName: + minLength: 1 + description: The username of the user. + mode: type: string - description: The last name of the user. - required: - - firstName - - lastName + enum: ["BASIC", "ADVANCED", null] + description: The mode for the user (e.g., "BASIC" or "ADVANCED"). + profilePhoto: + type: string + nullable: true + description: A URL to the user's profile photo. + deviceTokens: + type: array + items: + type: string + nullable: true + description: The device tokens for notifications. + notificationsEnabled: + type: boolean + description: True when user wants to receive notifications. additionalProperties: true responses: 200: diff --git a/taskfile.yaml b/taskfile.yaml index 53ed6e6..1733c85 100644 --- a/taskfile.yaml +++ b/taskfile.yaml @@ -75,10 +75,6 @@ tasks: cmds: - cd backend && bun run test - start: - desc: "Start frontend and backend production server" - deps: [frontend:dev, backend:prod] - generate: desc: "Generate Schema from OpenAPI Specification" cmds: