From 7573dcb0b4cfa441bf4e13223168a271daee7bb6 Mon Sep 17 00:00:00 2001 From: shubham-01-star Date: Wed, 22 Apr 2026 21:05:38 +0530 Subject: [PATCH 1/7] feat(graphql): migrate core domain typedefs to TypeScript Port Activity, Activities, Pagination, Comment, Group, Post, Posts, Quote, User, UserInvite, UserReputation, Vote SDL typedefs from the legacy JS package to TypeScript under app/data/types/. Each export is annotated `: string` and uses the `#graphql` tag for IDE syntax highlighting (matches the convention in solidTypeDefs.ts). Legacy SDL field names (_id, admin, _followingId, enable_voting, etc.) are preserved verbatim so existing frontend queries remain compatible; Prisma's @map() directives bridge these to the renamed storage fields. Co-Authored-By: Claude Opus 4.7 --- .../app/data/types/Activities.ts | 6 +++ quotevote-backend/app/data/types/Activity.ts | 18 +++++++ quotevote-backend/app/data/types/Comment.ts | 15 ++++++ quotevote-backend/app/data/types/Group.ts | 14 ++++++ .../app/data/types/Pagination.ts | 7 +++ quotevote-backend/app/data/types/Post.ts | 29 +++++++++++ quotevote-backend/app/data/types/Posts.ts | 6 +++ quotevote-backend/app/data/types/Quote.ts | 14 ++++++ quotevote-backend/app/data/types/User.ts | 26 ++++++++++ .../app/data/types/UserInvite.ts | 9 ++++ .../app/data/types/UserReputation.ts | 50 +++++++++++++++++++ quotevote-backend/app/data/types/Vote.ts | 15 ++++++ 12 files changed, 209 insertions(+) create mode 100644 quotevote-backend/app/data/types/Activities.ts create mode 100644 quotevote-backend/app/data/types/Activity.ts create mode 100644 quotevote-backend/app/data/types/Comment.ts create mode 100644 quotevote-backend/app/data/types/Group.ts create mode 100644 quotevote-backend/app/data/types/Pagination.ts create mode 100644 quotevote-backend/app/data/types/Post.ts create mode 100644 quotevote-backend/app/data/types/Posts.ts create mode 100644 quotevote-backend/app/data/types/Quote.ts create mode 100644 quotevote-backend/app/data/types/User.ts create mode 100644 quotevote-backend/app/data/types/UserInvite.ts create mode 100644 quotevote-backend/app/data/types/UserReputation.ts create mode 100644 quotevote-backend/app/data/types/Vote.ts diff --git a/quotevote-backend/app/data/types/Activities.ts b/quotevote-backend/app/data/types/Activities.ts new file mode 100644 index 00000000..919ebc6a --- /dev/null +++ b/quotevote-backend/app/data/types/Activities.ts @@ -0,0 +1,6 @@ +export const Activities: string = `#graphql + type Activities { + entities: [Activity] + pagination: Pagination + } +`; diff --git a/quotevote-backend/app/data/types/Activity.ts b/quotevote-backend/app/data/types/Activity.ts new file mode 100644 index 00000000..4f9ebb17 --- /dev/null +++ b/quotevote-backend/app/data/types/Activity.ts @@ -0,0 +1,18 @@ +export const Activity: string = `#graphql + type Activity { + _id: String + created: Date + activityType: String + postId: String + post: Post + voteId: String + vote: Vote + quoteId: String + quote: Quote + commentId: String + comment: Comment + content: String + userId: String + user: User + } +`; diff --git a/quotevote-backend/app/data/types/Comment.ts b/quotevote-backend/app/data/types/Comment.ts new file mode 100644 index 00000000..91d64e03 --- /dev/null +++ b/quotevote-backend/app/data/types/Comment.ts @@ -0,0 +1,15 @@ +export const Comment: string = `#graphql + type Comment { + _id: String + created: Date + content: String + userId: String + startWordIndex: Int + endWordIndex: Int + postId: String + url: String + reaction: String + deleted: Boolean + user: User + } +`; diff --git a/quotevote-backend/app/data/types/Group.ts b/quotevote-backend/app/data/types/Group.ts new file mode 100644 index 00000000..783f1c20 --- /dev/null +++ b/quotevote-backend/app/data/types/Group.ts @@ -0,0 +1,14 @@ +export const Group: string = `#graphql + type Group { + _id: String! + creatorId: String! + created: String! + title: String! + description: String! + url: String! + privacy: String! + allowedUserIds: [String!] + adminIds: [String!] + pendingUsers: [User] + } +`; diff --git a/quotevote-backend/app/data/types/Pagination.ts b/quotevote-backend/app/data/types/Pagination.ts new file mode 100644 index 00000000..347f603b --- /dev/null +++ b/quotevote-backend/app/data/types/Pagination.ts @@ -0,0 +1,7 @@ +export const Pagination: string = `#graphql + type Pagination { + total_count: Int + limit: Int + offset: Int + } +`; diff --git a/quotevote-backend/app/data/types/Post.ts b/quotevote-backend/app/data/types/Post.ts new file mode 100644 index 00000000..2180ddaa --- /dev/null +++ b/quotevote-backend/app/data/types/Post.ts @@ -0,0 +1,29 @@ +export const Post: string = `#graphql + type Post { + _id: String + userId: String + created: Date + groupId: String + title: String + text: String + citationUrl: String + url: String + deleted: Boolean + upvotes: Int + downvotes: Int + reportedBy: [String] + approvedBy: [String] + rejectedBy: [String] + votedBy: [String] + bookmarkedBy: [String] + dayPoints: Int + pointTimestamp: String + featuredSlot: Int + enable_voting: Boolean + creator: User + comments: [Comment] + votes: [Vote] + quotes: [Quote] + messageRoom: MessageRoom + } +`; diff --git a/quotevote-backend/app/data/types/Posts.ts b/quotevote-backend/app/data/types/Posts.ts new file mode 100644 index 00000000..9641ff04 --- /dev/null +++ b/quotevote-backend/app/data/types/Posts.ts @@ -0,0 +1,6 @@ +export const Posts: string = `#graphql + type Posts { + entities: [Post] + pagination: Pagination + } +`; diff --git a/quotevote-backend/app/data/types/Quote.ts b/quotevote-backend/app/data/types/Quote.ts new file mode 100644 index 00000000..3e3a9d10 --- /dev/null +++ b/quotevote-backend/app/data/types/Quote.ts @@ -0,0 +1,14 @@ +export const Quote: string = `#graphql + type Quote { + _id: String + created: Date + postId: Int + quote: String + quoted: String + quoter: String + startWordIndex: Int + endWordIndex: Int + deleted: Boolean + user: User + } +`; diff --git a/quotevote-backend/app/data/types/User.ts b/quotevote-backend/app/data/types/User.ts new file mode 100644 index 00000000..a77cc6f3 --- /dev/null +++ b/quotevote-backend/app/data/types/User.ts @@ -0,0 +1,26 @@ +export const User: string = `#graphql + type User { + _id: String! + userId: ID + joined: String + username: String + name: String + email: String + tokens: Int + _wallet: String + avatar: JSON + _followersId: [String] + _followingId: [String] + _votesId: Int + favorited: Int + admin: Boolean + upvotes: Int + downvotes: Int + contributorBadge: Boolean + reputation: UserReputation + botReports: Int + accountStatus: String + lastBotReportDate: String + themePreference: String + } +`; diff --git a/quotevote-backend/app/data/types/UserInvite.ts b/quotevote-backend/app/data/types/UserInvite.ts new file mode 100644 index 00000000..8cf11ed0 --- /dev/null +++ b/quotevote-backend/app/data/types/UserInvite.ts @@ -0,0 +1,9 @@ +export const UserInvite: string = `#graphql + type UserInvite { + _id: String! + email: String! + status: String + _userId: String + joined: Date + } +`; diff --git a/quotevote-backend/app/data/types/UserReputation.ts b/quotevote-backend/app/data/types/UserReputation.ts new file mode 100644 index 00000000..54c97fd1 --- /dev/null +++ b/quotevote-backend/app/data/types/UserReputation.ts @@ -0,0 +1,50 @@ +export const UserReputation: string = `#graphql + type UserReputation { + _id: String! + _userId: String! + overallScore: Int! + inviteNetworkScore: Int! + conductScore: Int! + activityScore: Int! + metrics: ReputationMetrics! + history: [ReputationHistory!]! + lastCalculated: String! + createdAt: String! + updatedAt: String! + } + + type ReputationMetrics { + totalInvitesSent: Int! + totalInvitesAccepted: Int! + totalInvitesDeclined: Int! + averageInviteeReputation: Int! + totalReportsReceived: Int! + totalReportsResolved: Int! + totalUpvotes: Int! + totalDownvotes: Int! + totalPosts: Int! + totalComments: Int! + } + + type ReputationHistory { + date: String! + overallScore: Int! + inviteNetworkScore: Int! + conductScore: Int! + activityScore: Int! + reason: String! + } + + type UserReport { + _id: String! + _reporterId: String! + _reportedUserId: String! + reason: String! + description: String + status: String! + severity: String! + adminNotes: String + createdAt: String! + updatedAt: String! + } +`; diff --git a/quotevote-backend/app/data/types/Vote.ts b/quotevote-backend/app/data/types/Vote.ts new file mode 100644 index 00000000..78dcabd4 --- /dev/null +++ b/quotevote-backend/app/data/types/Vote.ts @@ -0,0 +1,15 @@ +export const Vote: string = `#graphql + type Vote { + _id: String + created: Date + postId: String + userId: String + type: String + tags: String + startWordIndex: Int + endWordIndex: Int + deleted: Boolean + user: User + content: String + } +`; From 179930000f1a01c5f65cb97aeae1bd9a967b64cd Mon Sep 17 00:00:00 2001 From: shubham-01-star Date: Wed, 22 Apr 2026 21:06:19 +0530 Subject: [PATCH 2/7] feat(graphql): migrate messaging, presence, and social typedefs to TypeScript Port ChatRoom, Message, MessageRoom, Notification, Reaction, Presence, Roster, TypingIndicator SDL typedefs from JS to TypeScript under app/data/types/. Preserves all enum definitions (PresenceStatus, RosterStatus) and response types (HeartbeatResponse, TypingResponse, BuddyWithPresence, DeletedRoster, PostDetails) verbatim. Co-Authored-By: Claude Opus 4.7 --- quotevote-backend/app/data/types/ChatRooms.ts | 10 ++++++ quotevote-backend/app/data/types/Message.ts | 17 +++++++++ .../app/data/types/MessageRoom.ts | 24 +++++++++++++ .../app/data/types/Notification.ts | 13 +++++++ quotevote-backend/app/data/types/Presence.ts | 35 +++++++++++++++++++ quotevote-backend/app/data/types/Reaction.ts | 10 ++++++ quotevote-backend/app/data/types/Roster.ts | 34 ++++++++++++++++++ .../app/data/types/TypingIndicator.ts | 15 ++++++++ 8 files changed, 158 insertions(+) create mode 100644 quotevote-backend/app/data/types/ChatRooms.ts create mode 100644 quotevote-backend/app/data/types/Message.ts create mode 100644 quotevote-backend/app/data/types/MessageRoom.ts create mode 100644 quotevote-backend/app/data/types/Notification.ts create mode 100644 quotevote-backend/app/data/types/Presence.ts create mode 100644 quotevote-backend/app/data/types/Reaction.ts create mode 100644 quotevote-backend/app/data/types/Roster.ts create mode 100644 quotevote-backend/app/data/types/TypingIndicator.ts diff --git a/quotevote-backend/app/data/types/ChatRooms.ts b/quotevote-backend/app/data/types/ChatRooms.ts new file mode 100644 index 00000000..e5f154c2 --- /dev/null +++ b/quotevote-backend/app/data/types/ChatRooms.ts @@ -0,0 +1,10 @@ +export const ChatRoom: string = `#graphql + type ChatRoom { + _id: ID! + users: JSON + messageType: String + created: Date + unreadMessages: Int + user: User + } +`; diff --git a/quotevote-backend/app/data/types/Message.ts b/quotevote-backend/app/data/types/Message.ts new file mode 100644 index 00000000..a63d0daf --- /dev/null +++ b/quotevote-backend/app/data/types/Message.ts @@ -0,0 +1,17 @@ +export const Message: string = `#graphql + type Message { + _id: ID! + messageRoomId: String + userAvatar: String! + userName: String + userId: String + title: String + text: String + created: Date + type: String + mutation_type: String + deleted: Boolean + user: User + readBy: JSON + } +`; diff --git a/quotevote-backend/app/data/types/MessageRoom.ts b/quotevote-backend/app/data/types/MessageRoom.ts new file mode 100644 index 00000000..e60e18c0 --- /dev/null +++ b/quotevote-backend/app/data/types/MessageRoom.ts @@ -0,0 +1,24 @@ +export const MessageRoom: string = `#graphql + type MessageRoom { + _id: ID! + users: JSON + messageType: String + created: Date + lastActivity: Date + lastMessageTime: Date + title: String + avatar: JSON + unreadMessages: Int + postId: String + messages: [Message] + postDetails: PostDetails + } + + type PostDetails { + _id: ID + title: String + text: String + userId: ID + url: String + } +`; diff --git a/quotevote-backend/app/data/types/Notification.ts b/quotevote-backend/app/data/types/Notification.ts new file mode 100644 index 00000000..a57eae1b --- /dev/null +++ b/quotevote-backend/app/data/types/Notification.ts @@ -0,0 +1,13 @@ +export const Notification: string = `#graphql + type Notification { + _id: String + userId: String + userIdBy: String + userBy: User + post: Post + notificationType: String + label: String + status: String + created: Date + } +`; diff --git a/quotevote-backend/app/data/types/Presence.ts b/quotevote-backend/app/data/types/Presence.ts new file mode 100644 index 00000000..ab70e04e --- /dev/null +++ b/quotevote-backend/app/data/types/Presence.ts @@ -0,0 +1,35 @@ +export const Presence: string = `#graphql + # Presence status enum + enum PresenceStatus { + online + away + dnd + invisible + offline + } + + # User presence type + type Presence { + _id: String! + userId: String! + status: PresenceStatus! + statusMessage: String + lastHeartbeat: Date! + lastSeen: Date + user: User + } + + # Presence update for subscriptions + type PresenceUpdate { + userId: String! + status: PresenceStatus! + statusMessage: String + lastSeen: Date + } + + # Heartbeat response + type HeartbeatResponse { + success: Boolean! + timestamp: Date! + } +`; diff --git a/quotevote-backend/app/data/types/Reaction.ts b/quotevote-backend/app/data/types/Reaction.ts new file mode 100644 index 00000000..cb07143a --- /dev/null +++ b/quotevote-backend/app/data/types/Reaction.ts @@ -0,0 +1,10 @@ +export const Reaction: string = `#graphql + type Reaction { + _id: String + created: String + userId: String + messageId: String + actionId: String + emoji: String + } +`; diff --git a/quotevote-backend/app/data/types/Roster.ts b/quotevote-backend/app/data/types/Roster.ts new file mode 100644 index 00000000..ea2409f1 --- /dev/null +++ b/quotevote-backend/app/data/types/Roster.ts @@ -0,0 +1,34 @@ +export const Roster: string = `#graphql + # Roster status enum + enum RosterStatus { + pending + accepted + blocked + } + + # Roster entry (buddy relationship) + type Roster { + _id: String! + userId: String! + buddyId: String! + status: RosterStatus! + initiatedBy: String! + buddy: User + presence: Presence + created: Date! + updated: Date! + } + + # Buddy with presence information + type BuddyWithPresence { + user: User! + presence: Presence + roster: Roster! + } + + # Deleted roster response + type DeletedRoster { + _id: String! + success: Boolean! + } +`; diff --git a/quotevote-backend/app/data/types/TypingIndicator.ts b/quotevote-backend/app/data/types/TypingIndicator.ts new file mode 100644 index 00000000..899c7603 --- /dev/null +++ b/quotevote-backend/app/data/types/TypingIndicator.ts @@ -0,0 +1,15 @@ +export const TypingIndicator: string = `#graphql + # Typing indicator type + type TypingIndicator { + messageRoomId: String! + userId: String! + user: User + isTyping: Boolean! + timestamp: Date! + } + + # Typing mutation response + type TypingResponse { + success: Boolean! + } +`; From 1d121dddd99cf1cfced0f17c140fdc2eebdc319d Mon Sep 17 00:00:00 2001 From: shubham-01-star Date: Wed, 22 Apr 2026 21:06:37 +0530 Subject: [PATCH 3/7] feat(graphql): add soft-delete typedefs and typedef barrel export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port DeletedPost, DeletedQuote, DeletedComment, DeletedVote, DeletedMessage soft-delete response typedefs to TypeScript and add an index.ts barrel that re-exports all 25 typedef modules in the same order as the legacy JS package. This completes the GraphQL typedef migration — app/data/types/ now contains the full set of 25 TypeScript SDL modules backing the Apollo Server schema. Ready to be composed into the server's schema pipeline once resolver migration lands. Co-Authored-By: Claude Opus 4.7 --- .../app/data/types/DeletedComment.ts | 5 ++++ .../app/data/types/DeletedMessage.ts | 5 ++++ .../app/data/types/DeletedPost.ts | 5 ++++ .../app/data/types/DeletedQuote.ts | 5 ++++ .../app/data/types/DeletedVote.ts | 5 ++++ quotevote-backend/app/data/types/index.ts | 25 +++++++++++++++++++ 6 files changed, 50 insertions(+) create mode 100644 quotevote-backend/app/data/types/DeletedComment.ts create mode 100644 quotevote-backend/app/data/types/DeletedMessage.ts create mode 100644 quotevote-backend/app/data/types/DeletedPost.ts create mode 100644 quotevote-backend/app/data/types/DeletedQuote.ts create mode 100644 quotevote-backend/app/data/types/DeletedVote.ts create mode 100644 quotevote-backend/app/data/types/index.ts diff --git a/quotevote-backend/app/data/types/DeletedComment.ts b/quotevote-backend/app/data/types/DeletedComment.ts new file mode 100644 index 00000000..a5967454 --- /dev/null +++ b/quotevote-backend/app/data/types/DeletedComment.ts @@ -0,0 +1,5 @@ +export const DeletedComment: string = `#graphql + type DeletedComment { + _id: String + } +`; diff --git a/quotevote-backend/app/data/types/DeletedMessage.ts b/quotevote-backend/app/data/types/DeletedMessage.ts new file mode 100644 index 00000000..2b556107 --- /dev/null +++ b/quotevote-backend/app/data/types/DeletedMessage.ts @@ -0,0 +1,5 @@ +export const DeletedMessage: string = `#graphql + type DeletedMessage { + _id: String + } +`; diff --git a/quotevote-backend/app/data/types/DeletedPost.ts b/quotevote-backend/app/data/types/DeletedPost.ts new file mode 100644 index 00000000..be5567be --- /dev/null +++ b/quotevote-backend/app/data/types/DeletedPost.ts @@ -0,0 +1,5 @@ +export const DeletedPost: string = `#graphql + type DeletedPost { + _id: String + } +`; diff --git a/quotevote-backend/app/data/types/DeletedQuote.ts b/quotevote-backend/app/data/types/DeletedQuote.ts new file mode 100644 index 00000000..a33e5daf --- /dev/null +++ b/quotevote-backend/app/data/types/DeletedQuote.ts @@ -0,0 +1,5 @@ +export const DeletedQuote: string = `#graphql + type DeletedQuote { + _id: String + } +`; diff --git a/quotevote-backend/app/data/types/DeletedVote.ts b/quotevote-backend/app/data/types/DeletedVote.ts new file mode 100644 index 00000000..4eba5029 --- /dev/null +++ b/quotevote-backend/app/data/types/DeletedVote.ts @@ -0,0 +1,5 @@ +export const DeletedVote: string = `#graphql + type DeletedVote { + _id: String + } +`; diff --git a/quotevote-backend/app/data/types/index.ts b/quotevote-backend/app/data/types/index.ts new file mode 100644 index 00000000..6f9400e8 --- /dev/null +++ b/quotevote-backend/app/data/types/index.ts @@ -0,0 +1,25 @@ +export * from './Activity'; +export * from './Activities'; +export * from './Pagination'; +export * from './Comment'; +export * from './ChatRooms'; +export * from './Group'; +export * from './Message'; +export * from './MessageRoom'; +export * from './Notification'; +export * from './Post'; +export * from './Posts'; +export * from './Quote'; +export * from './Reaction'; +export * from './User'; +export * from './UserInvite'; +export * from './UserReputation'; +export * from './Vote'; +export * from './DeletedPost'; +export * from './DeletedQuote'; +export * from './DeletedComment'; +export * from './DeletedVote'; +export * from './DeletedMessage'; +export * from './Presence'; +export * from './Roster'; +export * from './TypingIndicator'; From ca2e73d4c91e5d64917fcc3006f13e49707a21dd Mon Sep 17 00:00:00 2001 From: shubham-01-star Date: Wed, 22 Apr 2026 21:37:32 +0530 Subject: [PATCH 4/7] feat(graphql): add domain scalars and enums for programmatic typedefs Introduce JSONScalar and DateScalar via GraphQLScalarType so Date and arbitrary JSON (avatar, readBy, users map) are first-class in the schema instead of leaking as plain String. Introduce PresenceStatusEnum and RosterStatusEnum via GraphQLEnumType, mirroring the Common.PresenceStatus / Common.RosterStatus string unions. Consumed by the per-type GraphQLObjectType modules in the follow-up commits. Co-Authored-By: Claude Opus 4.7 --- quotevote-backend/app/data/types/enums.ts | 26 +++++++++ quotevote-backend/app/data/types/scalars.ts | 63 +++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 quotevote-backend/app/data/types/enums.ts create mode 100644 quotevote-backend/app/data/types/scalars.ts diff --git a/quotevote-backend/app/data/types/enums.ts b/quotevote-backend/app/data/types/enums.ts new file mode 100644 index 00000000..fb16f84f --- /dev/null +++ b/quotevote-backend/app/data/types/enums.ts @@ -0,0 +1,26 @@ +/** + * Shared GraphQL enum types used by the domain schema. + * Values mirror the `Common.PresenceStatus` / `Common.RosterStatus` string unions. + */ + +import { GraphQLEnumType } from 'graphql'; + +export const PresenceStatusEnum = new GraphQLEnumType({ + name: 'PresenceStatus', + values: { + online: { value: 'online' }, + away: { value: 'away' }, + dnd: { value: 'dnd' }, + invisible: { value: 'invisible' }, + offline: { value: 'offline' }, + }, +}); + +export const RosterStatusEnum = new GraphQLEnumType({ + name: 'RosterStatus', + values: { + pending: { value: 'pending' }, + accepted: { value: 'accepted' }, + blocked: { value: 'blocked' }, + }, +}); diff --git a/quotevote-backend/app/data/types/scalars.ts b/quotevote-backend/app/data/types/scalars.ts new file mode 100644 index 00000000..7b38a9cd --- /dev/null +++ b/quotevote-backend/app/data/types/scalars.ts @@ -0,0 +1,63 @@ +/** + * Custom GraphQL scalar types used by the domain schema. + * + * - `DateScalar` -> bridges JS Date/string/number values (serialised as ISO string) + * - `JSONScalar` -> passthrough for arbitrary JSON blobs (avatar, readBy, users map, etc.) + */ + +import { GraphQLScalarType, Kind, type ValueNode } from 'graphql'; + +function parseLiteralJSON(ast: ValueNode): unknown { + switch (ast.kind) { + case Kind.STRING: + case Kind.BOOLEAN: + return ast.value; + case Kind.INT: + case Kind.FLOAT: + return Number(ast.value); + case Kind.OBJECT: { + const obj: Record = {}; + ast.fields.forEach((f) => { + obj[f.name.value] = parseLiteralJSON(f.value); + }); + return obj; + } + case Kind.LIST: + return ast.values.map(parseLiteralJSON); + case Kind.NULL: + return null; + default: + return null; + } +} + +export const JSONScalar = new GraphQLScalarType({ + name: 'JSON', + description: 'Arbitrary JSON value (objects, arrays, primitives).', + serialize: (value: unknown) => value, + parseValue: (value: unknown) => value, + parseLiteral: (ast) => parseLiteralJSON(ast), +}); + +export const DateScalar = new GraphQLScalarType({ + name: 'Date', + description: 'ISO-8601 date string (also accepts epoch ms on input).', + serialize(value: unknown): string | null { + if (value == null) return null; + if (value instanceof Date) return value.toISOString(); + if (typeof value === 'number') return new Date(value).toISOString(); + if (typeof value === 'string') return value; + return String(value); + }, + parseValue(value: unknown): Date | string | number | null { + if (value == null) return null; + if (typeof value === 'string' || typeof value === 'number') return new Date(value); + if (value instanceof Date) return value; + return null; + }, + parseLiteral(ast): Date | string | number | null { + if (ast.kind === Kind.STRING) return new Date(ast.value); + if (ast.kind === Kind.INT) return new Date(Number(ast.value)); + return null; + }, +}); From 8793671825ecc1f2c87e49e4d7e8c393bfbd5171 Mon Sep 17 00:00:00 2001 From: shubham-01-star Date: Wed, 22 Apr 2026 21:37:53 +0530 Subject: [PATCH 5/7] feat(graphql): convert leaf typedefs to programmatic GraphQLObjectType Rewrite the self-contained typedef modules using new GraphQLObjectType bound to their shared Common.* interface via GraphQLFieldConfigMap. Covered here: - Pagination -> Common.Pagination - DeletedPost/Quote/Comment/Vote/Message response payloads - Reaction -> Common.Reaction - UserInvite -> Common.UserInvite - UserReputation -> Common.Reputation (plus ReputationMetrics / ReputationHistory / UserReport sub-types) Field types now carry compile-time checks against the shared domain/Prisma type system (e.g. reputation metrics use GraphQLInt aligned with the numeric fields on Common.ReputationMetrics). Co-Authored-By: Claude Opus 4.7 --- .../app/data/types/DeletedComment.ts | 22 ++- .../app/data/types/DeletedMessage.ts | 22 ++- .../app/data/types/DeletedPost.ts | 22 ++- .../app/data/types/DeletedQuote.ts | 22 ++- .../app/data/types/DeletedVote.ts | 22 ++- .../app/data/types/Pagination.ts | 23 ++- quotevote-backend/app/data/types/Reaction.ts | 29 ++- .../app/data/types/UserInvite.ts | 36 +++- .../app/data/types/UserReputation.ts | 177 +++++++++++++----- 9 files changed, 277 insertions(+), 98 deletions(-) diff --git a/quotevote-backend/app/data/types/DeletedComment.ts b/quotevote-backend/app/data/types/DeletedComment.ts index a5967454..fe604de0 100644 --- a/quotevote-backend/app/data/types/DeletedComment.ts +++ b/quotevote-backend/app/data/types/DeletedComment.ts @@ -1,5 +1,17 @@ -export const DeletedComment: string = `#graphql - type DeletedComment { - _id: String - } -`; +import { GraphQLObjectType, GraphQLString, type GraphQLFieldConfigMap } from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; + +interface DeletedIdPayload { + _id?: string; +} + +export const DeletedCommentType: GraphQLObjectType = + new GraphQLObjectType({ + name: 'DeletedComment', + description: 'Response payload for a soft-deleted Comment mutation.', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: GraphQLString }, + }), + }); + +export const DeletedComment = DeletedCommentType; diff --git a/quotevote-backend/app/data/types/DeletedMessage.ts b/quotevote-backend/app/data/types/DeletedMessage.ts index 2b556107..9da9fc76 100644 --- a/quotevote-backend/app/data/types/DeletedMessage.ts +++ b/quotevote-backend/app/data/types/DeletedMessage.ts @@ -1,5 +1,17 @@ -export const DeletedMessage: string = `#graphql - type DeletedMessage { - _id: String - } -`; +import { GraphQLObjectType, GraphQLString, type GraphQLFieldConfigMap } from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; + +interface DeletedIdPayload { + _id?: string; +} + +export const DeletedMessageType: GraphQLObjectType = + new GraphQLObjectType({ + name: 'DeletedMessage', + description: 'Response payload for a soft-deleted Message mutation.', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: GraphQLString }, + }), + }); + +export const DeletedMessage = DeletedMessageType; diff --git a/quotevote-backend/app/data/types/DeletedPost.ts b/quotevote-backend/app/data/types/DeletedPost.ts index be5567be..5c285d9c 100644 --- a/quotevote-backend/app/data/types/DeletedPost.ts +++ b/quotevote-backend/app/data/types/DeletedPost.ts @@ -1,5 +1,17 @@ -export const DeletedPost: string = `#graphql - type DeletedPost { - _id: String - } -`; +import { GraphQLObjectType, GraphQLString, type GraphQLFieldConfigMap } from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; + +interface DeletedIdPayload { + _id?: string; +} + +export const DeletedPostType: GraphQLObjectType = + new GraphQLObjectType({ + name: 'DeletedPost', + description: 'Response payload for a soft-deleted Post mutation.', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: GraphQLString }, + }), + }); + +export const DeletedPost = DeletedPostType; diff --git a/quotevote-backend/app/data/types/DeletedQuote.ts b/quotevote-backend/app/data/types/DeletedQuote.ts index a33e5daf..5842b90e 100644 --- a/quotevote-backend/app/data/types/DeletedQuote.ts +++ b/quotevote-backend/app/data/types/DeletedQuote.ts @@ -1,5 +1,17 @@ -export const DeletedQuote: string = `#graphql - type DeletedQuote { - _id: String - } -`; +import { GraphQLObjectType, GraphQLString, type GraphQLFieldConfigMap } from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; + +interface DeletedIdPayload { + _id?: string; +} + +export const DeletedQuoteType: GraphQLObjectType = + new GraphQLObjectType({ + name: 'DeletedQuote', + description: 'Response payload for a soft-deleted Quote mutation.', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: GraphQLString }, + }), + }); + +export const DeletedQuote = DeletedQuoteType; diff --git a/quotevote-backend/app/data/types/DeletedVote.ts b/quotevote-backend/app/data/types/DeletedVote.ts index 4eba5029..f26fa8d2 100644 --- a/quotevote-backend/app/data/types/DeletedVote.ts +++ b/quotevote-backend/app/data/types/DeletedVote.ts @@ -1,5 +1,17 @@ -export const DeletedVote: string = `#graphql - type DeletedVote { - _id: String - } -`; +import { GraphQLObjectType, GraphQLString, type GraphQLFieldConfigMap } from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; + +interface DeletedIdPayload { + _id?: string; +} + +export const DeletedVoteType: GraphQLObjectType = + new GraphQLObjectType({ + name: 'DeletedVote', + description: 'Response payload for a soft-deleted Vote mutation.', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: GraphQLString }, + }), + }); + +export const DeletedVote = DeletedVoteType; diff --git a/quotevote-backend/app/data/types/Pagination.ts b/quotevote-backend/app/data/types/Pagination.ts index 347f603b..e77e1014 100644 --- a/quotevote-backend/app/data/types/Pagination.ts +++ b/quotevote-backend/app/data/types/Pagination.ts @@ -1,7 +1,16 @@ -export const Pagination: string = `#graphql - type Pagination { - total_count: Int - limit: Int - offset: Int - } -`; +import { GraphQLInt, GraphQLObjectType, type GraphQLFieldConfigMap } from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; +import type * as Common from '~/types/common'; + +export const PaginationType: GraphQLObjectType = + new GraphQLObjectType({ + name: 'Pagination', + description: 'Cursor / offset pagination metadata returned alongside list queries.', + fields: (): GraphQLFieldConfigMap => ({ + total_count: { type: GraphQLInt }, + limit: { type: GraphQLInt }, + offset: { type: GraphQLInt }, + }), + }); + +export const Pagination = PaginationType; diff --git a/quotevote-backend/app/data/types/Reaction.ts b/quotevote-backend/app/data/types/Reaction.ts index cb07143a..8c5d5874 100644 --- a/quotevote-backend/app/data/types/Reaction.ts +++ b/quotevote-backend/app/data/types/Reaction.ts @@ -1,10 +1,19 @@ -export const Reaction: string = `#graphql - type Reaction { - _id: String - created: String - userId: String - messageId: String - actionId: String - emoji: String - } -`; +import { GraphQLObjectType, GraphQLString, type GraphQLFieldConfigMap } from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; +import type * as Common from '~/types/common'; + +export const ReactionType: GraphQLObjectType = + new GraphQLObjectType({ + name: 'Reaction', + description: 'Emoji reaction on a message or action (vote/comment/etc).', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: GraphQLString }, + created: { type: GraphQLString }, + userId: { type: GraphQLString }, + messageId: { type: GraphQLString }, + actionId: { type: GraphQLString }, + emoji: { type: GraphQLString }, + }), + }); + +export const Reaction = ReactionType; diff --git a/quotevote-backend/app/data/types/UserInvite.ts b/quotevote-backend/app/data/types/UserInvite.ts index 8cf11ed0..f9d2a5a7 100644 --- a/quotevote-backend/app/data/types/UserInvite.ts +++ b/quotevote-backend/app/data/types/UserInvite.ts @@ -1,9 +1,27 @@ -export const UserInvite: string = `#graphql - type UserInvite { - _id: String! - email: String! - status: String - _userId: String - joined: Date - } -`; +import { + GraphQLNonNull, + GraphQLObjectType, + GraphQLString, + type GraphQLFieldConfigMap, +} from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; +import type * as Common from '~/types/common'; +import { DateScalar } from './scalars'; + +export const UserInviteType: GraphQLObjectType = + new GraphQLObjectType({ + name: 'UserInvite', + description: 'Outstanding invite record for a prospective user.', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: new GraphQLNonNull(GraphQLString) }, + email: { type: new GraphQLNonNull(GraphQLString) }, + status: { type: GraphQLString }, + _userId: { + type: GraphQLString, + resolve: (invite) => (invite as Common.UserInvite & { _userId?: string })._userId, + }, + joined: { type: DateScalar, resolve: (invite) => invite.created }, + }), + }); + +export const UserInvite = UserInviteType; diff --git a/quotevote-backend/app/data/types/UserReputation.ts b/quotevote-backend/app/data/types/UserReputation.ts index 54c97fd1..afa24e09 100644 --- a/quotevote-backend/app/data/types/UserReputation.ts +++ b/quotevote-backend/app/data/types/UserReputation.ts @@ -1,50 +1,133 @@ -export const UserReputation: string = `#graphql - type UserReputation { - _id: String! - _userId: String! - overallScore: Int! - inviteNetworkScore: Int! - conductScore: Int! - activityScore: Int! - metrics: ReputationMetrics! - history: [ReputationHistory!]! - lastCalculated: String! - createdAt: String! - updatedAt: String! - } +import { + GraphQLInt, + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, + GraphQLString, + type GraphQLFieldConfigMap, +} from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; +import type * as Common from '~/types/common'; - type ReputationMetrics { - totalInvitesSent: Int! - totalInvitesAccepted: Int! - totalInvitesDeclined: Int! - averageInviteeReputation: Int! - totalReportsReceived: Int! - totalReportsResolved: Int! - totalUpvotes: Int! - totalDownvotes: Int! - totalPosts: Int! - totalComments: Int! - } +export const ReputationMetricsType: GraphQLObjectType = + new GraphQLObjectType({ + name: 'ReputationMetrics', + fields: (): GraphQLFieldConfigMap => ({ + totalInvitesSent: { type: new GraphQLNonNull(GraphQLInt) }, + totalInvitesAccepted: { type: new GraphQLNonNull(GraphQLInt) }, + totalInvitesDeclined: { type: new GraphQLNonNull(GraphQLInt) }, + averageInviteeReputation: { type: new GraphQLNonNull(GraphQLInt) }, + totalReportsReceived: { type: new GraphQLNonNull(GraphQLInt) }, + totalReportsResolved: { type: new GraphQLNonNull(GraphQLInt) }, + totalUpvotes: { type: new GraphQLNonNull(GraphQLInt) }, + totalDownvotes: { type: new GraphQLNonNull(GraphQLInt) }, + totalPosts: { type: new GraphQLNonNull(GraphQLInt) }, + totalComments: { type: new GraphQLNonNull(GraphQLInt) }, + }), + }); - type ReputationHistory { - date: String! - overallScore: Int! - inviteNetworkScore: Int! - conductScore: Int! - activityScore: Int! - reason: String! - } +interface ReputationHistoryEntry { + date: string; + overallScore: number; + inviteNetworkScore: number; + conductScore: number; + activityScore: number; + reason: string; +} - type UserReport { - _id: String! - _reporterId: String! - _reportedUserId: String! - reason: String! - description: String - status: String! - severity: String! - adminNotes: String - createdAt: String! - updatedAt: String! - } -`; +export const ReputationHistoryType: GraphQLObjectType = + new GraphQLObjectType({ + name: 'ReputationHistory', + fields: (): GraphQLFieldConfigMap => ({ + date: { type: new GraphQLNonNull(GraphQLString) }, + overallScore: { type: new GraphQLNonNull(GraphQLInt) }, + inviteNetworkScore: { type: new GraphQLNonNull(GraphQLInt) }, + conductScore: { type: new GraphQLNonNull(GraphQLInt) }, + activityScore: { type: new GraphQLNonNull(GraphQLInt) }, + reason: { type: new GraphQLNonNull(GraphQLString) }, + }), + }); + +export const UserReputationType: GraphQLObjectType = + new GraphQLObjectType({ + name: 'UserReputation', + description: 'Aggregated reputation score for a user across invite/conduct/activity axes.', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: new GraphQLNonNull(GraphQLString) }, + _userId: { + type: new GraphQLNonNull(GraphQLString), + resolve: (rep) => rep.userId ?? '', + }, + overallScore: { type: new GraphQLNonNull(GraphQLInt) }, + inviteNetworkScore: { type: new GraphQLNonNull(GraphQLInt) }, + conductScore: { type: new GraphQLNonNull(GraphQLInt) }, + activityScore: { type: new GraphQLNonNull(GraphQLInt) }, + metrics: { type: new GraphQLNonNull(ReputationMetricsType) }, + history: { + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(ReputationHistoryType))), + resolve: (rep) => + (rep as Common.Reputation & { history?: ReputationHistoryEntry[] }).history ?? [], + }, + lastCalculated: { + type: new GraphQLNonNull(GraphQLString), + resolve: (rep) => + rep.lastCalculated instanceof Date + ? rep.lastCalculated.toISOString() + : String(rep.lastCalculated), + }, + createdAt: { + type: new GraphQLNonNull(GraphQLString), + resolve: (rep) => { + const v = (rep as Common.Reputation & { createdAt?: Date | string }).createdAt; + return v instanceof Date ? v.toISOString() : (v ?? ''); + }, + }, + updatedAt: { + type: new GraphQLNonNull(GraphQLString), + resolve: (rep) => { + const v = (rep as Common.Reputation & { updatedAt?: Date | string }).updatedAt; + return v instanceof Date ? v.toISOString() : (v ?? ''); + }, + }, + }), + }); + +export const UserReportType: GraphQLObjectType = + new GraphQLObjectType({ + name: 'UserReport', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: new GraphQLNonNull(GraphQLString) }, + _reporterId: { + type: new GraphQLNonNull(GraphQLString), + resolve: (r) => r.reporterId, + }, + _reportedUserId: { + type: new GraphQLNonNull(GraphQLString), + resolve: (r) => r.reportedUserId, + }, + reason: { type: new GraphQLNonNull(GraphQLString) }, + description: { type: GraphQLString }, + status: { + type: new GraphQLNonNull(GraphQLString), + resolve: (r) => r.status ?? 'pending', + }, + severity: { + type: new GraphQLNonNull(GraphQLString), + resolve: (r) => r.severity ?? 'low', + }, + adminNotes: { type: GraphQLString }, + createdAt: { + type: new GraphQLNonNull(GraphQLString), + resolve: (r) => (r.created instanceof Date ? r.created.toISOString() : String(r.created)), + }, + updatedAt: { + type: new GraphQLNonNull(GraphQLString), + resolve: (r) => { + const v = (r as Common.UserReport & { updatedAt?: Date | string }).updatedAt; + return v instanceof Date ? v.toISOString() : (v ?? ''); + }, + }, + }), + }); + +export const UserReputation = UserReputationType; From c0731cd241e8305a745fbc857a9494b727d36495 Mon Sep 17 00:00:00 2001 From: shubham-01-star Date: Wed, 22 Apr 2026 21:38:11 +0530 Subject: [PATCH 6/7] feat(graphql): convert entity and relationship typedefs to programmatic form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrite every cross-referenced typedef as a strongly-typed GraphQLObjectType. Circular references across files (User<->Post, Post<->Comment/Vote/Quote/MessageRoom, Activity<->Post/Vote/Quote/Comment/User, etc.) are handled via the fields: () => ({...}) thunk so ES-module load order is irrelevant. Covered: - User, Vote, Quote, Comment, Message, Group, ChatRoom - MessageRoom (+ PostDetails sub-type) - Post, Posts - Notification, Activity, Activities - Presence (+ PresenceUpdate, HeartbeatResponse) - Roster (+ BuddyWithPresence, DeletedRoster) - TypingIndicator (+ TypingResponse) All types are bound to Common.* source interfaces from ~/types/common, so adding a field that diverges from the Prisma-backed domain shape is a TypeScript error — not a runtime surprise. Legacy SDL field names (_id, admin, _followingId, _wallet, enable_voting, etc.) are preserved for frontend compatibility; Prisma @map() continues to bridge to the renamed storage fields. Co-Authored-By: Claude Opus 4.7 --- .../app/data/types/Activities.ts | 28 ++++- quotevote-backend/app/data/types/Activity.ts | 61 ++++++--- quotevote-backend/app/data/types/ChatRooms.ts | 44 +++++-- quotevote-backend/app/data/types/Comment.ts | 49 +++++--- quotevote-backend/app/data/types/Group.ts | 65 +++++++--- quotevote-backend/app/data/types/Message.ts | 61 ++++++--- .../app/data/types/MessageRoom.ts | 86 +++++++++---- .../app/data/types/Notification.ts | 43 +++++-- quotevote-backend/app/data/types/Post.ts | 116 +++++++++++++----- quotevote-backend/app/data/types/Posts.ts | 28 ++++- quotevote-backend/app/data/types/Presence.ts | 98 ++++++++++----- quotevote-backend/app/data/types/Quote.ts | 59 ++++++--- quotevote-backend/app/data/types/Roster.ts | 105 +++++++++++----- .../app/data/types/TypingIndicator.ts | 58 ++++++--- quotevote-backend/app/data/types/User.ts | 103 ++++++++++++---- quotevote-backend/app/data/types/Vote.ts | 52 +++++--- 16 files changed, 772 insertions(+), 284 deletions(-) diff --git a/quotevote-backend/app/data/types/Activities.ts b/quotevote-backend/app/data/types/Activities.ts index 919ebc6a..cf2d9668 100644 --- a/quotevote-backend/app/data/types/Activities.ts +++ b/quotevote-backend/app/data/types/Activities.ts @@ -1,6 +1,22 @@ -export const Activities: string = `#graphql - type Activities { - entities: [Activity] - pagination: Pagination - } -`; +import { GraphQLList, GraphQLObjectType, type GraphQLFieldConfigMap } from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; +import type * as Common from '~/types/common'; +import { ActivityType } from './Activity'; +import { PaginationType } from './Pagination'; + +export const ActivitiesType: GraphQLObjectType< + Common.PaginatedResult, + GraphQLContext +> = new GraphQLObjectType, GraphQLContext>({ + name: 'Activities', + description: 'Paginated list of activity feed entries.', + fields: (): GraphQLFieldConfigMap, GraphQLContext> => ({ + entities: { + type: new GraphQLList(ActivityType), + resolve: (r) => r.entities ?? [], + }, + pagination: { type: PaginationType }, + }), +}); + +export const Activities = ActivitiesType; diff --git a/quotevote-backend/app/data/types/Activity.ts b/quotevote-backend/app/data/types/Activity.ts index 4f9ebb17..53671725 100644 --- a/quotevote-backend/app/data/types/Activity.ts +++ b/quotevote-backend/app/data/types/Activity.ts @@ -1,18 +1,43 @@ -export const Activity: string = `#graphql - type Activity { - _id: String - created: Date - activityType: String - postId: String - post: Post - voteId: String - vote: Vote - quoteId: String - quote: Quote - commentId: String - comment: Comment - content: String - userId: String - user: User - } -`; +import { GraphQLObjectType, GraphQLString, type GraphQLFieldConfigMap } from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; +import type * as Common from '~/types/common'; +import { DateScalar } from './scalars'; +import { UserType } from './User'; +import { PostType } from './Post'; +import { VoteType } from './Vote'; +import { QuoteType } from './Quote'; +import { CommentType } from './Comment'; + +interface ActivityShape extends Common.Activity { + post?: Common.Post; + vote?: Common.Vote; + quote?: Common.Quote; + comment?: Common.Comment; + user?: Common.User; +} + +export const ActivityType: GraphQLObjectType = new GraphQLObjectType< + ActivityShape, + GraphQLContext +>({ + name: 'Activity', + description: 'Activity feed entry — a user action on a post/vote/quote/comment.', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: GraphQLString }, + created: { type: DateScalar }, + activityType: { type: GraphQLString }, + postId: { type: GraphQLString }, + post: { type: PostType }, + voteId: { type: GraphQLString }, + vote: { type: VoteType }, + quoteId: { type: GraphQLString }, + quote: { type: QuoteType }, + commentId: { type: GraphQLString }, + comment: { type: CommentType }, + content: { type: GraphQLString }, + userId: { type: GraphQLString }, + user: { type: UserType }, + }), +}); + +export const Activity = ActivityType; diff --git a/quotevote-backend/app/data/types/ChatRooms.ts b/quotevote-backend/app/data/types/ChatRooms.ts index e5f154c2..d2c67d20 100644 --- a/quotevote-backend/app/data/types/ChatRooms.ts +++ b/quotevote-backend/app/data/types/ChatRooms.ts @@ -1,10 +1,34 @@ -export const ChatRoom: string = `#graphql - type ChatRoom { - _id: ID! - users: JSON - messageType: String - created: Date - unreadMessages: Int - user: User - } -`; +import { + GraphQLID, + GraphQLInt, + GraphQLNonNull, + GraphQLObjectType, + GraphQLString, + type GraphQLFieldConfigMap, +} from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; +import type * as Common from '~/types/common'; +import { DateScalar, JSONScalar } from './scalars'; +import { UserType } from './User'; + +interface ChatRoomShape extends Common.MessageRoom { + user?: Common.User; +} + +export const ChatRoomType: GraphQLObjectType = new GraphQLObjectType< + ChatRoomShape, + GraphQLContext +>({ + name: 'ChatRoom', + description: 'Lightweight chat-room list-view projection of a MessageRoom.', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: new GraphQLNonNull(GraphQLID) }, + users: { type: JSONScalar, resolve: (r) => r.users ?? [] }, + messageType: { type: GraphQLString }, + created: { type: DateScalar }, + unreadMessages: { type: GraphQLInt }, + user: { type: UserType }, + }), +}); + +export const ChatRoom = ChatRoomType; diff --git a/quotevote-backend/app/data/types/Comment.ts b/quotevote-backend/app/data/types/Comment.ts index 91d64e03..8c0605fb 100644 --- a/quotevote-backend/app/data/types/Comment.ts +++ b/quotevote-backend/app/data/types/Comment.ts @@ -1,15 +1,34 @@ -export const Comment: string = `#graphql - type Comment { - _id: String - created: Date - content: String - userId: String - startWordIndex: Int - endWordIndex: Int - postId: String - url: String - reaction: String - deleted: Boolean - user: User - } -`; +import { + GraphQLBoolean, + GraphQLInt, + GraphQLObjectType, + GraphQLString, + type GraphQLFieldConfigMap, +} from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; +import type * as Common from '~/types/common'; +import { DateScalar } from './scalars'; +import { UserType } from './User'; + +export const CommentType: GraphQLObjectType = new GraphQLObjectType< + Common.Comment, + GraphQLContext +>({ + name: 'Comment', + description: 'Comment attached to a post, aligned with Prisma Comment.', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: GraphQLString }, + created: { type: DateScalar }, + content: { type: GraphQLString }, + userId: { type: GraphQLString }, + startWordIndex: { type: GraphQLInt }, + endWordIndex: { type: GraphQLInt }, + postId: { type: GraphQLString }, + url: { type: GraphQLString }, + reaction: { type: GraphQLString }, + deleted: { type: GraphQLBoolean }, + user: { type: UserType }, + }), +}); + +export const Comment = CommentType; diff --git a/quotevote-backend/app/data/types/Group.ts b/quotevote-backend/app/data/types/Group.ts index 783f1c20..b13b43fa 100644 --- a/quotevote-backend/app/data/types/Group.ts +++ b/quotevote-backend/app/data/types/Group.ts @@ -1,14 +1,51 @@ -export const Group: string = `#graphql - type Group { - _id: String! - creatorId: String! - created: String! - title: String! - description: String! - url: String! - privacy: String! - allowedUserIds: [String!] - adminIds: [String!] - pendingUsers: [User] - } -`; +import { + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, + GraphQLString, + type GraphQLFieldConfigMap, +} from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; +import type * as Common from '~/types/common'; +import { UserType } from './User'; + +interface GroupShape extends Common.Group { + pendingUsers?: Common.User[]; +} + +export const GroupType: GraphQLObjectType = new GraphQLObjectType< + GroupShape, + GraphQLContext +>({ + name: 'Group', + description: 'Group / community, aligned with Prisma Group.', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: new GraphQLNonNull(GraphQLString) }, + creatorId: { type: new GraphQLNonNull(GraphQLString) }, + created: { + type: new GraphQLNonNull(GraphQLString), + resolve: (g) => (g.created instanceof Date ? g.created.toISOString() : String(g.created)), + }, + title: { type: new GraphQLNonNull(GraphQLString) }, + description: { + type: new GraphQLNonNull(GraphQLString), + resolve: (g) => g.description ?? '', + }, + url: { type: new GraphQLNonNull(GraphQLString), resolve: (g) => g.url ?? '' }, + privacy: { type: new GraphQLNonNull(GraphQLString) }, + allowedUserIds: { + type: new GraphQLList(new GraphQLNonNull(GraphQLString)), + resolve: (g) => g.allowedUserIds ?? [], + }, + adminIds: { + type: new GraphQLList(new GraphQLNonNull(GraphQLString)), + resolve: (g) => g.adminIds ?? [], + }, + pendingUsers: { + type: new GraphQLList(UserType), + resolve: (g) => g.pendingUsers ?? [], + }, + }), +}); + +export const Group = GroupType; diff --git a/quotevote-backend/app/data/types/Message.ts b/quotevote-backend/app/data/types/Message.ts index a63d0daf..5b2c7e01 100644 --- a/quotevote-backend/app/data/types/Message.ts +++ b/quotevote-backend/app/data/types/Message.ts @@ -1,17 +1,44 @@ -export const Message: string = `#graphql - type Message { - _id: ID! - messageRoomId: String - userAvatar: String! - userName: String - userId: String - title: String - text: String - created: Date - type: String - mutation_type: String - deleted: Boolean - user: User - readBy: JSON - } -`; +import { + GraphQLBoolean, + GraphQLID, + GraphQLNonNull, + GraphQLObjectType, + GraphQLString, + type GraphQLFieldConfigMap, +} from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; +import type * as Common from '~/types/common'; +import { DateScalar, JSONScalar } from './scalars'; +import { UserType } from './User'; + +interface MessageShape extends Common.Message { + userAvatar?: string; +} + +export const MessageType: GraphQLObjectType = new GraphQLObjectType< + MessageShape, + GraphQLContext +>({ + name: 'Message', + description: 'Chat / direct-message entry.', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: new GraphQLNonNull(GraphQLID) }, + messageRoomId: { type: GraphQLString }, + userAvatar: { + type: new GraphQLNonNull(GraphQLString), + resolve: (m) => m.userAvatar ?? '', + }, + userName: { type: GraphQLString }, + userId: { type: GraphQLString }, + title: { type: GraphQLString }, + text: { type: GraphQLString }, + created: { type: DateScalar }, + type: { type: GraphQLString }, + mutation_type: { type: GraphQLString }, + deleted: { type: GraphQLBoolean }, + user: { type: UserType }, + readBy: { type: JSONScalar, resolve: (m) => m.readBy ?? [] }, + }), +}); + +export const Message = MessageType; diff --git a/quotevote-backend/app/data/types/MessageRoom.ts b/quotevote-backend/app/data/types/MessageRoom.ts index e60e18c0..5f69ff20 100644 --- a/quotevote-backend/app/data/types/MessageRoom.ts +++ b/quotevote-backend/app/data/types/MessageRoom.ts @@ -1,24 +1,64 @@ -export const MessageRoom: string = `#graphql - type MessageRoom { - _id: ID! - users: JSON - messageType: String - created: Date - lastActivity: Date - lastMessageTime: Date - title: String - avatar: JSON - unreadMessages: Int - postId: String - messages: [Message] - postDetails: PostDetails - } +import { + GraphQLID, + GraphQLInt, + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, + GraphQLString, + type GraphQLFieldConfigMap, +} from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; +import type * as Common from '~/types/common'; +import { DateScalar, JSONScalar } from './scalars'; +import { MessageType } from './Message'; - type PostDetails { - _id: ID - title: String - text: String - userId: ID - url: String - } -`; +interface PostDetailsShape { + _id?: string; + title?: string; + text?: string; + userId?: string; + url?: string; +} + +export const PostDetailsType: GraphQLObjectType = + new GraphQLObjectType({ + name: 'PostDetails', + description: 'Inlined Post snapshot displayed with the MessageRoom that wraps it.', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: GraphQLID }, + title: { type: GraphQLString }, + text: { type: GraphQLString }, + userId: { type: GraphQLID }, + url: { type: GraphQLString }, + }), + }); + +interface MessageRoomShape extends Common.MessageRoom { + messages?: Common.Message[]; + postDetails?: PostDetailsShape; +} + +export const MessageRoomType: GraphQLObjectType = + new GraphQLObjectType({ + name: 'MessageRoom', + description: 'Chat room (direct or group), aligned with Prisma MessageRoom.', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: new GraphQLNonNull(GraphQLID) }, + users: { type: JSONScalar, resolve: (r) => r.users ?? [] }, + messageType: { type: GraphQLString }, + created: { type: DateScalar }, + lastActivity: { type: DateScalar }, + lastMessageTime: { type: DateScalar }, + title: { type: GraphQLString }, + avatar: { type: JSONScalar }, + unreadMessages: { type: GraphQLInt }, + postId: { type: GraphQLString }, + messages: { + type: new GraphQLList(MessageType), + resolve: (r) => r.messages ?? [], + }, + postDetails: { type: PostDetailsType }, + }), + }); + +export const MessageRoom = MessageRoomType; diff --git a/quotevote-backend/app/data/types/Notification.ts b/quotevote-backend/app/data/types/Notification.ts index a57eae1b..bf350d4e 100644 --- a/quotevote-backend/app/data/types/Notification.ts +++ b/quotevote-backend/app/data/types/Notification.ts @@ -1,13 +1,30 @@ -export const Notification: string = `#graphql - type Notification { - _id: String - userId: String - userIdBy: String - userBy: User - post: Post - notificationType: String - label: String - status: String - created: Date - } -`; +import { GraphQLObjectType, GraphQLString, type GraphQLFieldConfigMap } from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; +import type * as Common from '~/types/common'; +import { DateScalar } from './scalars'; +import { UserType } from './User'; +import { PostType } from './Post'; + +interface NotificationShape extends Common.Notification { + userBy?: Common.User; + post?: Common.Post; +} + +export const NotificationType: GraphQLObjectType = + new GraphQLObjectType({ + name: 'Notification', + description: 'User-facing notification (follow / upvote / comment / etc).', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: GraphQLString }, + userId: { type: GraphQLString }, + userIdBy: { type: GraphQLString }, + userBy: { type: UserType }, + post: { type: PostType }, + notificationType: { type: GraphQLString }, + label: { type: GraphQLString }, + status: { type: GraphQLString }, + created: { type: DateScalar }, + }), + }); + +export const Notification = NotificationType; diff --git a/quotevote-backend/app/data/types/Post.ts b/quotevote-backend/app/data/types/Post.ts index 2180ddaa..607d7005 100644 --- a/quotevote-backend/app/data/types/Post.ts +++ b/quotevote-backend/app/data/types/Post.ts @@ -1,29 +1,87 @@ -export const Post: string = `#graphql - type Post { - _id: String - userId: String - created: Date - groupId: String - title: String - text: String - citationUrl: String - url: String - deleted: Boolean - upvotes: Int - downvotes: Int - reportedBy: [String] - approvedBy: [String] - rejectedBy: [String] - votedBy: [String] - bookmarkedBy: [String] - dayPoints: Int - pointTimestamp: String - featuredSlot: Int - enable_voting: Boolean - creator: User - comments: [Comment] - votes: [Vote] - quotes: [Quote] - messageRoom: MessageRoom - } -`; +import { + GraphQLBoolean, + GraphQLInt, + GraphQLList, + GraphQLObjectType, + GraphQLString, + type GraphQLFieldConfigMap, +} from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; +import type * as Common from '~/types/common'; +import { DateScalar } from './scalars'; +import { UserType } from './User'; +import { CommentType } from './Comment'; +import { VoteType } from './Vote'; +import { QuoteType } from './Quote'; +import { MessageRoomType } from './MessageRoom'; + +interface PostShape extends Common.Post { + creator?: Common.User; + comments?: Common.Comment[]; + votes?: Common.Vote[]; + quotes?: Common.Quote[]; + messageRoom?: Common.MessageRoom; + dayPoints?: number; + pointTimestamp?: string; +} + +export const PostType: GraphQLObjectType = new GraphQLObjectType< + PostShape, + GraphQLContext +>({ + name: 'Post', + description: 'A user post, aligned with Prisma Post / Mongoose Post model.', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: GraphQLString }, + userId: { type: GraphQLString }, + created: { type: DateScalar }, + groupId: { type: GraphQLString }, + title: { type: GraphQLString }, + text: { type: GraphQLString }, + citationUrl: { type: GraphQLString }, + url: { type: GraphQLString }, + deleted: { type: GraphQLBoolean }, + upvotes: { type: GraphQLInt }, + downvotes: { type: GraphQLInt }, + reportedBy: { + type: new GraphQLList(GraphQLString), + resolve: (p) => p.reportedBy ?? [], + }, + approvedBy: { + type: new GraphQLList(GraphQLString), + resolve: (p) => p.approvedBy ?? [], + }, + rejectedBy: { + type: new GraphQLList(GraphQLString), + resolve: (p) => p.rejectedBy ?? [], + }, + votedBy: { + type: new GraphQLList(GraphQLString), + resolve: (p) => p.votedBy ?? [], + }, + bookmarkedBy: { + type: new GraphQLList(GraphQLString), + resolve: (p) => p.bookmarkedBy ?? [], + }, + dayPoints: { type: GraphQLInt }, + pointTimestamp: { type: GraphQLString }, + featuredSlot: { type: GraphQLInt }, + enable_voting: { type: GraphQLBoolean }, + creator: { type: UserType }, + comments: { + type: new GraphQLList(CommentType), + resolve: (p) => p.comments ?? [], + }, + votes: { + type: new GraphQLList(VoteType), + resolve: (p) => p.votes ?? [], + }, + quotes: { + type: new GraphQLList(QuoteType), + resolve: (p) => p.quotes ?? [], + }, + messageRoom: { type: MessageRoomType }, + }), +}); + +export const Post = PostType; diff --git a/quotevote-backend/app/data/types/Posts.ts b/quotevote-backend/app/data/types/Posts.ts index 9641ff04..b4dec534 100644 --- a/quotevote-backend/app/data/types/Posts.ts +++ b/quotevote-backend/app/data/types/Posts.ts @@ -1,6 +1,22 @@ -export const Posts: string = `#graphql - type Posts { - entities: [Post] - pagination: Pagination - } -`; +import { GraphQLList, GraphQLObjectType, type GraphQLFieldConfigMap } from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; +import type * as Common from '~/types/common'; +import { PostType } from './Post'; +import { PaginationType } from './Pagination'; + +export const PostsType: GraphQLObjectType< + Common.PaginatedResult, + GraphQLContext +> = new GraphQLObjectType, GraphQLContext>({ + name: 'Posts', + description: 'Paginated list of posts.', + fields: (): GraphQLFieldConfigMap, GraphQLContext> => ({ + entities: { + type: new GraphQLList(PostType), + resolve: (r) => r.entities ?? [], + }, + pagination: { type: PaginationType }, + }), +}); + +export const Posts = PostsType; diff --git a/quotevote-backend/app/data/types/Presence.ts b/quotevote-backend/app/data/types/Presence.ts index ab70e04e..4ab0ab27 100644 --- a/quotevote-backend/app/data/types/Presence.ts +++ b/quotevote-backend/app/data/types/Presence.ts @@ -1,35 +1,69 @@ -export const Presence: string = `#graphql - # Presence status enum - enum PresenceStatus { - online - away - dnd - invisible - offline - } +import { + GraphQLBoolean, + GraphQLNonNull, + GraphQLObjectType, + GraphQLString, + type GraphQLFieldConfigMap, +} from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; +import type * as Common from '~/types/common'; +import { DateScalar } from './scalars'; +import { PresenceStatusEnum } from './enums'; +import { UserType } from './User'; - # User presence type - type Presence { - _id: String! - userId: String! - status: PresenceStatus! - statusMessage: String - lastHeartbeat: Date! - lastSeen: Date - user: User - } +interface PresenceShape extends Common.Presence { + user?: Common.User; +} - # Presence update for subscriptions - type PresenceUpdate { - userId: String! - status: PresenceStatus! - statusMessage: String - lastSeen: Date - } +export const PresenceType: GraphQLObjectType = new GraphQLObjectType< + PresenceShape, + GraphQLContext +>({ + name: 'Presence', + description: 'User presence status (online / away / dnd / offline / invisible).', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: new GraphQLNonNull(GraphQLString) }, + userId: { type: new GraphQLNonNull(GraphQLString) }, + status: { type: new GraphQLNonNull(PresenceStatusEnum) }, + statusMessage: { type: GraphQLString }, + lastHeartbeat: { type: new GraphQLNonNull(DateScalar) }, + lastSeen: { type: DateScalar }, + user: { type: UserType }, + }), +}); - # Heartbeat response - type HeartbeatResponse { - success: Boolean! - timestamp: Date! - } -`; +interface PresenceUpdateShape { + userId: string; + status: Common.PresenceStatus; + statusMessage?: string; + lastSeen?: Date | string | number; +} + +export const PresenceUpdateType: GraphQLObjectType = + new GraphQLObjectType({ + name: 'PresenceUpdate', + description: 'Presence change payload delivered over subscriptions.', + fields: (): GraphQLFieldConfigMap => ({ + userId: { type: new GraphQLNonNull(GraphQLString) }, + status: { type: new GraphQLNonNull(PresenceStatusEnum) }, + statusMessage: { type: GraphQLString }, + lastSeen: { type: DateScalar }, + }), + }); + +interface HeartbeatResponseShape { + success: boolean; + timestamp: Date | string | number; +} + +export const HeartbeatResponseType: GraphQLObjectType = + new GraphQLObjectType({ + name: 'HeartbeatResponse', + description: 'Response from a presence heartbeat mutation.', + fields: (): GraphQLFieldConfigMap => ({ + success: { type: new GraphQLNonNull(GraphQLBoolean) }, + timestamp: { type: new GraphQLNonNull(DateScalar) }, + }), + }); + +export const Presence = PresenceType; diff --git a/quotevote-backend/app/data/types/Quote.ts b/quotevote-backend/app/data/types/Quote.ts index 3e3a9d10..f4c69532 100644 --- a/quotevote-backend/app/data/types/Quote.ts +++ b/quotevote-backend/app/data/types/Quote.ts @@ -1,14 +1,45 @@ -export const Quote: string = `#graphql - type Quote { - _id: String - created: Date - postId: Int - quote: String - quoted: String - quoter: String - startWordIndex: Int - endWordIndex: Int - deleted: Boolean - user: User - } -`; +import { + GraphQLBoolean, + GraphQLInt, + GraphQLObjectType, + GraphQLString, + type GraphQLFieldConfigMap, +} from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; +import type * as Common from '~/types/common'; +import { DateScalar } from './scalars'; +import { UserType } from './User'; + +interface QuoteShape extends Common.Quote { + quoted?: string; + quoter?: string; + deleted?: boolean; +} + +export const QuoteType: GraphQLObjectType = new GraphQLObjectType< + QuoteShape, + GraphQLContext +>({ + name: 'Quote', + description: 'A quoted excerpt of a post.', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: GraphQLString }, + created: { type: DateScalar }, + postId: { + type: GraphQLInt, + resolve: (q) => { + const n = Number(q.postId); + return Number.isFinite(n) ? n : null; + }, + }, + quote: { type: GraphQLString }, + quoted: { type: GraphQLString }, + quoter: { type: GraphQLString }, + startWordIndex: { type: GraphQLInt }, + endWordIndex: { type: GraphQLInt }, + deleted: { type: GraphQLBoolean }, + user: { type: UserType }, + }), +}); + +export const Quote = QuoteType; diff --git a/quotevote-backend/app/data/types/Roster.ts b/quotevote-backend/app/data/types/Roster.ts index ea2409f1..0ceaf329 100644 --- a/quotevote-backend/app/data/types/Roster.ts +++ b/quotevote-backend/app/data/types/Roster.ts @@ -1,34 +1,77 @@ -export const Roster: string = `#graphql - # Roster status enum - enum RosterStatus { - pending - accepted - blocked - } +import { + GraphQLBoolean, + GraphQLNonNull, + GraphQLObjectType, + GraphQLString, + type GraphQLFieldConfigMap, +} from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; +import type * as Common from '~/types/common'; +import { DateScalar } from './scalars'; +import { RosterStatusEnum } from './enums'; +import { UserType } from './User'; +import { PresenceType } from './Presence'; - # Roster entry (buddy relationship) - type Roster { - _id: String! - userId: String! - buddyId: String! - status: RosterStatus! - initiatedBy: String! - buddy: User - presence: Presence - created: Date! - updated: Date! - } +interface RosterShape extends Common.Roster { + buddy?: Common.User; + presence?: Common.Presence; +} - # Buddy with presence information - type BuddyWithPresence { - user: User! - presence: Presence - roster: Roster! - } +export const RosterType: GraphQLObjectType = new GraphQLObjectType< + RosterShape, + GraphQLContext +>({ + name: 'Roster', + description: 'Buddy-list entry linking two users with a relationship status.', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: new GraphQLNonNull(GraphQLString) }, + userId: { type: new GraphQLNonNull(GraphQLString) }, + buddyId: { type: new GraphQLNonNull(GraphQLString) }, + status: { type: new GraphQLNonNull(RosterStatusEnum) }, + initiatedBy: { + type: new GraphQLNonNull(GraphQLString), + resolve: (r) => r.initiatedBy ?? r.userId, + }, + buddy: { type: UserType }, + presence: { type: PresenceType }, + created: { type: new GraphQLNonNull(DateScalar) }, + updated: { + type: new GraphQLNonNull(DateScalar), + resolve: (r) => r.updated ?? r.created, + }, + }), +}); - # Deleted roster response - type DeletedRoster { - _id: String! - success: Boolean! - } -`; +interface BuddyWithPresenceShape { + user: Common.User; + presence?: Common.Presence; + roster: Common.Roster; +} + +export const BuddyWithPresenceType: GraphQLObjectType = + new GraphQLObjectType({ + name: 'BuddyWithPresence', + description: 'Buddy user joined with their current presence + roster entry.', + fields: (): GraphQLFieldConfigMap => ({ + user: { type: new GraphQLNonNull(UserType) }, + presence: { type: PresenceType }, + roster: { type: new GraphQLNonNull(RosterType) }, + }), + }); + +interface DeletedRosterShape { + _id: string; + success: boolean; +} + +export const DeletedRosterType: GraphQLObjectType = + new GraphQLObjectType({ + name: 'DeletedRoster', + description: 'Response payload for a removed roster entry.', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: new GraphQLNonNull(GraphQLString) }, + success: { type: new GraphQLNonNull(GraphQLBoolean) }, + }), + }); + +export const Roster = RosterType; diff --git a/quotevote-backend/app/data/types/TypingIndicator.ts b/quotevote-backend/app/data/types/TypingIndicator.ts index 899c7603..1fbdcb05 100644 --- a/quotevote-backend/app/data/types/TypingIndicator.ts +++ b/quotevote-backend/app/data/types/TypingIndicator.ts @@ -1,15 +1,43 @@ -export const TypingIndicator: string = `#graphql - # Typing indicator type - type TypingIndicator { - messageRoomId: String! - userId: String! - user: User - isTyping: Boolean! - timestamp: Date! - } - - # Typing mutation response - type TypingResponse { - success: Boolean! - } -`; +import { + GraphQLBoolean, + GraphQLNonNull, + GraphQLObjectType, + GraphQLString, + type GraphQLFieldConfigMap, +} from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; +import type * as Common from '~/types/common'; +import { DateScalar } from './scalars'; +import { UserType } from './User'; + +interface TypingIndicatorShape extends Common.Typing { + user?: Common.User; +} + +export const TypingIndicatorType: GraphQLObjectType = + new GraphQLObjectType({ + name: 'TypingIndicator', + description: 'Live typing indicator broadcast within a message room.', + fields: (): GraphQLFieldConfigMap => ({ + messageRoomId: { type: new GraphQLNonNull(GraphQLString) }, + userId: { type: new GraphQLNonNull(GraphQLString) }, + user: { type: UserType }, + isTyping: { type: new GraphQLNonNull(GraphQLBoolean) }, + timestamp: { type: new GraphQLNonNull(DateScalar) }, + }), + }); + +interface TypingResponseShape { + success: boolean; +} + +export const TypingResponseType: GraphQLObjectType = + new GraphQLObjectType({ + name: 'TypingResponse', + description: 'Response payload for updateTyping mutation.', + fields: (): GraphQLFieldConfigMap => ({ + success: { type: new GraphQLNonNull(GraphQLBoolean) }, + }), + }); + +export const TypingIndicator = TypingIndicatorType; diff --git a/quotevote-backend/app/data/types/User.ts b/quotevote-backend/app/data/types/User.ts index a77cc6f3..eda32166 100644 --- a/quotevote-backend/app/data/types/User.ts +++ b/quotevote-backend/app/data/types/User.ts @@ -1,26 +1,77 @@ -export const User: string = `#graphql - type User { - _id: String! - userId: ID - joined: String - username: String - name: String - email: String - tokens: Int - _wallet: String - avatar: JSON - _followersId: [String] - _followingId: [String] - _votesId: Int - favorited: Int - admin: Boolean - upvotes: Int - downvotes: Int - contributorBadge: Boolean - reputation: UserReputation - botReports: Int - accountStatus: String - lastBotReportDate: String - themePreference: String - } -`; +import { + GraphQLBoolean, + GraphQLID, + GraphQLInt, + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, + GraphQLString, + type GraphQLFieldConfigMap, +} from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; +import type * as Common from '~/types/common'; +import { JSONScalar } from './scalars'; +import { UserReputationType } from './UserReputation'; + +export const UserType: GraphQLObjectType = new GraphQLObjectType< + Common.User, + GraphQLContext +>({ + name: 'User', + description: 'Registered user account, aligned with Prisma User / Mongoose User model.', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: new GraphQLNonNull(GraphQLString) }, + userId: { type: GraphQLID, resolve: (u) => u._id }, + joined: { + type: GraphQLString, + resolve: (u) => (u.joined instanceof Date ? u.joined.toISOString() : u.joined), + }, + username: { type: GraphQLString }, + name: { type: GraphQLString }, + email: { type: GraphQLString }, + tokens: { type: GraphQLInt }, + _wallet: { type: GraphQLString, resolve: (u) => u._wallet }, + avatar: { type: JSONScalar }, + _followersId: { + type: new GraphQLList(GraphQLString), + resolve: (u) => u._followersId ?? [], + }, + _followingId: { + type: new GraphQLList(GraphQLString), + resolve: (u) => u._followingId ?? [], + }, + _votesId: { + type: GraphQLInt, + resolve: (u) => { + const raw = u._votesId; + if (raw == null) return null; + const n = Number(raw); + return Number.isFinite(n) ? n : null; + }, + }, + favorited: { + type: GraphQLInt, + resolve: (u) => (Array.isArray(u.favorited) ? u.favorited.length : 0), + }, + admin: { type: GraphQLBoolean }, + upvotes: { type: GraphQLInt }, + downvotes: { type: GraphQLInt }, + contributorBadge: { type: GraphQLBoolean }, + reputation: { type: UserReputationType }, + botReports: { type: GraphQLInt }, + accountStatus: { type: GraphQLString }, + lastBotReportDate: { + type: GraphQLString, + resolve: (u) => + u.lastBotReportDate instanceof Date + ? u.lastBotReportDate.toISOString() + : (u.lastBotReportDate ?? null), + }, + themePreference: { + type: GraphQLString, + resolve: (u) => (u as Common.User & { themePreference?: string }).themePreference ?? null, + }, + }), +}); + +export const User = UserType; diff --git a/quotevote-backend/app/data/types/Vote.ts b/quotevote-backend/app/data/types/Vote.ts index 78dcabd4..2826a074 100644 --- a/quotevote-backend/app/data/types/Vote.ts +++ b/quotevote-backend/app/data/types/Vote.ts @@ -1,15 +1,37 @@ -export const Vote: string = `#graphql - type Vote { - _id: String - created: Date - postId: String - userId: String - type: String - tags: String - startWordIndex: Int - endWordIndex: Int - deleted: Boolean - user: User - content: String - } -`; +import { + GraphQLBoolean, + GraphQLInt, + GraphQLObjectType, + GraphQLString, + type GraphQLFieldConfigMap, +} from 'graphql'; +import type { GraphQLContext } from '~/types/graphql'; +import type * as Common from '~/types/common'; +import { DateScalar } from './scalars'; +import { UserType } from './User'; + +export const VoteType: GraphQLObjectType = new GraphQLObjectType< + Common.Vote, + GraphQLContext +>({ + name: 'Vote', + description: 'User vote (up/down) on a post, aligned with Prisma Vote.', + fields: (): GraphQLFieldConfigMap => ({ + _id: { type: GraphQLString }, + created: { type: DateScalar }, + postId: { type: GraphQLString }, + userId: { type: GraphQLString }, + type: { type: GraphQLString }, + tags: { + type: GraphQLString, + resolve: (v) => (Array.isArray(v.tags) ? v.tags.join(',') : (v.tags ?? null)), + }, + startWordIndex: { type: GraphQLInt }, + endWordIndex: { type: GraphQLInt }, + deleted: { type: GraphQLBoolean }, + content: { type: GraphQLString }, + user: { type: UserType }, + }), +}); + +export const Vote = VoteType; From aaa89d9964cf96a916c9f7a22217a54d5f141ac2 Mon Sep 17 00:00:00 2001 From: shubham-01-star Date: Wed, 22 Apr 2026 21:40:38 +0530 Subject: [PATCH 7/7] feat(graphql): compose domain schema into Apollo server + add smoke test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce app/data/types/index.ts as the barrel exporting every programmatic GraphQL type plus aggregated helpers: - domainTypes: readonly array of all 25 legacy types + sub-types, scalars, enums - domainTypeDefs: printed SDL aggregating every domain type via printType() Wire domainTypeDefs into app/server.ts so the running Apollo Server's typeDefs now contain the full domain schema alongside the existing Query/Mutation/Solid SDL. Removes the now-duplicate inline 'scalar JSON' since JSONScalar from the new types module provides it. Add __tests__/unit/graphql-types.test.ts — a 28-test smoke suite that: 1. Asserts every one of the 25 legacy typedef names is a programmatic GraphQLObjectType with resolvable fields (no circular-import errors) 2. Composes a GraphQLSchema from domainTypes and asserts every expected type name (plus 13 sub-types, scalars, enums) appears in schema.getTypeMap() 3. Round-trips domainTypeDefs through buildSchema + printSchema to verify the printed SDL parses back to a valid schema Satisfies ticket 7.28 acceptance criteria: 'GraphQL schema construction works with the new TypeScript modules' + 'introspect the schema to confirm type structure'. Co-Authored-By: Claude Opus 4.7 --- .../__tests__/unit/graphql-types.test.ts | 171 ++++++++++++++++++ quotevote-backend/app/data/types/index.ts | 115 ++++++++++++ quotevote-backend/app/server.ts | 7 +- 3 files changed, 290 insertions(+), 3 deletions(-) create mode 100644 quotevote-backend/__tests__/unit/graphql-types.test.ts diff --git a/quotevote-backend/__tests__/unit/graphql-types.test.ts b/quotevote-backend/__tests__/unit/graphql-types.test.ts new file mode 100644 index 00000000..13208a0f --- /dev/null +++ b/quotevote-backend/__tests__/unit/graphql-types.test.ts @@ -0,0 +1,171 @@ +/** + * Schema-construction smoke test for the GraphQL domain type system. + * + * Proves that: + * 1. All 25 legacy type-definition modules construct valid + * `GraphQLObjectType` instances (no thunk / circular-ref errors). + * 2. A GraphQLSchema composed from `domainTypes` resolves without errors + * and introspection exposes every expected type name. + * 3. The printed SDL from `domainTypeDefs` parses back to a valid schema + * (ticket criterion: "GraphQL schema construction works with the new + * TypeScript modules"). + */ + +import { buildSchema, GraphQLObjectType, GraphQLSchema, GraphQLString, printSchema } from 'graphql'; +import { + domainTypeDefs, + domainTypes, + // Named imports — each *Type comes from its own file. + ActivityType, + ActivitiesType, + ChatRoomType, + CommentType, + DeletedCommentType, + DeletedMessageType, + DeletedPostType, + DeletedQuoteType, + DeletedVoteType, + GroupType, + MessageType, + MessageRoomType, + NotificationType, + PaginationType, + PostType, + PostsType, + PresenceType, + QuoteType, + ReactionType, + RosterType, + TypingIndicatorType, + UserType, + UserInviteType, + UserReputationType, + VoteType, +} from '~/data/types'; + +const LEGACY_TYPE_NAMES = [ + 'Activity', + 'Activities', + 'ChatRoom', + 'Comment', + 'DeletedComment', + 'DeletedMessage', + 'DeletedPost', + 'DeletedQuote', + 'DeletedVote', + 'Group', + 'Message', + 'MessageRoom', + 'Notification', + 'Pagination', + 'Post', + 'Posts', + 'Presence', + 'Quote', + 'Reaction', + 'Roster', + 'TypingIndicator', + 'User', + 'UserInvite', + 'UserReputation', + 'Vote', +] as const; + +describe('GraphQL domain typedefs (7.28 migration)', () => { + describe('per-file programmatic types', () => { + const perFileCases: ReadonlyArray<[string, GraphQLObjectType]> = [ + ['Activity', ActivityType], + ['Activities', ActivitiesType], + ['ChatRoom', ChatRoomType], + ['Comment', CommentType], + ['DeletedComment', DeletedCommentType], + ['DeletedMessage', DeletedMessageType], + ['DeletedPost', DeletedPostType], + ['DeletedQuote', DeletedQuoteType], + ['DeletedVote', DeletedVoteType], + ['Group', GroupType], + ['Message', MessageType], + ['MessageRoom', MessageRoomType], + ['Notification', NotificationType], + ['Pagination', PaginationType], + ['Post', PostType], + ['Posts', PostsType], + ['Presence', PresenceType], + ['Quote', QuoteType], + ['Reaction', ReactionType], + ['Roster', RosterType], + ['TypingIndicator', TypingIndicatorType], + ['User', UserType], + ['UserInvite', UserInviteType], + ['UserReputation', UserReputationType], + ['Vote', VoteType], + ]; + + it.each(perFileCases)( + 'exports %s as a programmatic GraphQLObjectType with resolvable fields', + (name, type) => { + expect(type).toBeInstanceOf(GraphQLObjectType); + expect(type.name).toBe(name); + // Thunk resolution — throws if a circular import left the thunk seeing undefined. + expect(() => type.getFields()).not.toThrow(); + expect(Object.keys(type.getFields()).length).toBeGreaterThan(0); + } + ); + + it('covers all 25 legacy typedef names', () => { + expect(perFileCases).toHaveLength(LEGACY_TYPE_NAMES.length); + const coveredNames = perFileCases.map(([n]) => n).sort(); + expect(coveredNames).toEqual([...LEGACY_TYPE_NAMES].sort()); + }); + }); + + describe('schema composition', () => { + it('builds a GraphQLSchema that includes every domain type', () => { + const QueryRoot = new GraphQLObjectType({ + name: 'Query', + fields: { _placeholder: { type: GraphQLString } }, + }); + + const schema = new GraphQLSchema({ + query: QueryRoot, + types: [...domainTypes], + }); + + const typeMap = schema.getTypeMap(); + for (const expected of LEGACY_TYPE_NAMES) { + expect(typeMap).toHaveProperty(expected); + } + // Sub-types emitted alongside their parent files are also present. + for (const extra of [ + 'PostDetails', + 'ReputationMetrics', + 'ReputationHistory', + 'UserReport', + 'PresenceUpdate', + 'HeartbeatResponse', + 'BuddyWithPresence', + 'DeletedRoster', + 'TypingResponse', + 'PresenceStatus', + 'RosterStatus', + 'JSON', + 'Date', + ]) { + expect(typeMap).toHaveProperty(extra); + } + }); + + it('emits non-empty printable SDL that parses back to a valid schema', () => { + expect(typeof domainTypeDefs).toBe('string'); + expect(domainTypeDefs.length).toBeGreaterThan(0); + for (const name of LEGACY_TYPE_NAMES) { + expect(domainTypeDefs).toMatch(new RegExp(`\\btype ${name}\\b|\\benum ${name}\\b`)); + } + + // Round-trip: print → re-parse with buildSchema should yield a valid schema. + const roundTripped = buildSchema(`${domainTypeDefs}\n\ntype Query { _ping: String }`); + expect(printSchema(roundTripped)).toContain('type User'); + expect(printSchema(roundTripped)).toContain('type Post'); + }); + }); +}); diff --git a/quotevote-backend/app/data/types/index.ts b/quotevote-backend/app/data/types/index.ts index 6f9400e8..9f5527de 100644 --- a/quotevote-backend/app/data/types/index.ts +++ b/quotevote-backend/app/data/types/index.ts @@ -1,3 +1,58 @@ +/** + * GraphQL domain type definitions — TypeScript programmatic form. + * + * Each `*Type` is a `GraphQLObjectType` bound to a + * shared `~/types/common` interface (which itself tracks the Prisma model shape). + * This gives compile-time verification that GraphQL fields match the domain type + * system end-to-end. + * + * `domainTypes` — array of all 25 declared object types + their sub-types. + * `domainTypeDefs` — printed SDL string for all domain types (enums, scalars, + * objects), ready to concatenate with any Query/Mutation + * SDL when composing the Apollo schema. + */ + +import type { GraphQLNamedType } from 'graphql'; +import { printType } from 'graphql'; + +// Scalars & enums +import { DateScalar, JSONScalar } from './scalars'; +import { PresenceStatusEnum, RosterStatusEnum } from './enums'; + +// Core domain object types — one per legacy typedef file +import { ActivityType } from './Activity'; +import { ActivitiesType } from './Activities'; +import { PaginationType } from './Pagination'; +import { CommentType } from './Comment'; +import { ChatRoomType } from './ChatRooms'; +import { GroupType } from './Group'; +import { MessageType } from './Message'; +import { MessageRoomType, PostDetailsType } from './MessageRoom'; +import { NotificationType } from './Notification'; +import { PostType } from './Post'; +import { PostsType } from './Posts'; +import { QuoteType } from './Quote'; +import { ReactionType } from './Reaction'; +import { UserType } from './User'; +import { UserInviteType } from './UserInvite'; +import { + ReputationHistoryType, + ReputationMetricsType, + UserReportType, + UserReputationType, +} from './UserReputation'; +import { VoteType } from './Vote'; +import { DeletedPostType } from './DeletedPost'; +import { DeletedQuoteType } from './DeletedQuote'; +import { DeletedCommentType } from './DeletedComment'; +import { DeletedVoteType } from './DeletedVote'; +import { DeletedMessageType } from './DeletedMessage'; +import { HeartbeatResponseType, PresenceType, PresenceUpdateType } from './Presence'; +import { BuddyWithPresenceType, DeletedRosterType, RosterType } from './Roster'; +import { TypingIndicatorType, TypingResponseType } from './TypingIndicator'; + +export * from './scalars'; +export * from './enums'; export * from './Activity'; export * from './Activities'; export * from './Pagination'; @@ -23,3 +78,63 @@ export * from './DeletedMessage'; export * from './Presence'; export * from './Roster'; export * from './TypingIndicator'; + +/** + * All programmatic GraphQL types composing the domain schema. + * Keep this ordering stable — it defines the order types appear in the printed SDL. + */ +export const domainTypes: readonly GraphQLNamedType[] = [ + // Scalars + DateScalar, + JSONScalar, + // Enums + PresenceStatusEnum, + RosterStatusEnum, + // Pagination / envelopes + PaginationType, + PostsType, + ActivitiesType, + // Core entities + UserType, + UserInviteType, + UserReputationType, + ReputationMetricsType, + ReputationHistoryType, + UserReportType, + PostType, + CommentType, + VoteType, + QuoteType, + GroupType, + NotificationType, + ActivityType, + ReactionType, + // Chat / messaging + ChatRoomType, + MessageType, + MessageRoomType, + PostDetailsType, + // Soft-delete payloads + DeletedPostType, + DeletedQuoteType, + DeletedCommentType, + DeletedVoteType, + DeletedMessageType, + // Presence / roster / typing + PresenceType, + PresenceUpdateType, + HeartbeatResponseType, + RosterType, + BuddyWithPresenceType, + DeletedRosterType, + TypingIndicatorType, + TypingResponseType, +]; + +/** + * Printed SDL for all domain types. Composable with any Query/Mutation SDL. + * + * Example: + * const typeDefs = `${domainTypeDefs}\n\ntype Query { ... }`; + */ +export const domainTypeDefs: string = domainTypes.map((t) => printType(t)).join('\n\n'); diff --git a/quotevote-backend/app/server.ts b/quotevote-backend/app/server.ts index 451b4fd0..22eb5344 100644 --- a/quotevote-backend/app/server.ts +++ b/quotevote-backend/app/server.ts @@ -7,6 +7,7 @@ import mongoose from 'mongoose'; import dotenv from 'dotenv'; import { GraphQLError } from 'graphql'; import { solidResolvers } from './data/resolvers/solidResolvers'; +import { domainTypeDefs } from './data/types'; import type { GraphQLContext, PubSub } from './types/graphql'; import { requireAuth } from './data/utils/requireAuth'; import { pubsub } from './data/utils/pubsub'; @@ -44,6 +45,8 @@ async function startServer() { // 2. Apollo Server Setup (v4/v5 Syntax) const server = new ApolloServer({ typeDefs: ` + ${domainTypeDefs} + type Query { hello: String status: String @@ -73,8 +76,6 @@ async function startServer() { issuer: String message: String } - - scalar JSON type PortableState { version: String @@ -99,7 +100,7 @@ async function startServer() { status: () => 'Active', }, }, - solidResolvers + solidResolvers, ], });