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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions quotevote-backend/__tests__/unit/graphql-types.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
22 changes: 22 additions & 0 deletions quotevote-backend/app/data/types/Activities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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<Common.Activity>,
GraphQLContext
> = new GraphQLObjectType<Common.PaginatedResult<Common.Activity>, GraphQLContext>({
name: 'Activities',
description: 'Paginated list of activity feed entries.',
fields: (): GraphQLFieldConfigMap<Common.PaginatedResult<Common.Activity>, GraphQLContext> => ({
entities: {
type: new GraphQLList(ActivityType),
resolve: (r) => r.entities ?? [],
},
pagination: { type: PaginationType },
}),
});

export const Activities = ActivitiesType;
43 changes: 43 additions & 0 deletions quotevote-backend/app/data/types/Activity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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<ActivityShape, GraphQLContext> = new GraphQLObjectType<
ActivityShape,
GraphQLContext
>({
name: 'Activity',
description: 'Activity feed entry — a user action on a post/vote/quote/comment.',
fields: (): GraphQLFieldConfigMap<ActivityShape, GraphQLContext> => ({
_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;
34 changes: 34 additions & 0 deletions quotevote-backend/app/data/types/ChatRooms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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<ChatRoomShape, GraphQLContext> = new GraphQLObjectType<
ChatRoomShape,
GraphQLContext
>({
name: 'ChatRoom',
description: 'Lightweight chat-room list-view projection of a MessageRoom.',
fields: (): GraphQLFieldConfigMap<ChatRoomShape, GraphQLContext> => ({
_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;
34 changes: 34 additions & 0 deletions quotevote-backend/app/data/types/Comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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<Common.Comment, GraphQLContext> = new GraphQLObjectType<
Common.Comment,
GraphQLContext
>({
name: 'Comment',
description: 'Comment attached to a post, aligned with Prisma Comment.',
fields: (): GraphQLFieldConfigMap<Common.Comment, GraphQLContext> => ({
_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;
17 changes: 17 additions & 0 deletions quotevote-backend/app/data/types/DeletedComment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { GraphQLObjectType, GraphQLString, type GraphQLFieldConfigMap } from 'graphql';
import type { GraphQLContext } from '~/types/graphql';

interface DeletedIdPayload {
_id?: string;
}

export const DeletedCommentType: GraphQLObjectType<DeletedIdPayload, GraphQLContext> =
new GraphQLObjectType<DeletedIdPayload, GraphQLContext>({
name: 'DeletedComment',
description: 'Response payload for a soft-deleted Comment mutation.',
fields: (): GraphQLFieldConfigMap<DeletedIdPayload, GraphQLContext> => ({
_id: { type: GraphQLString },
}),
});

export const DeletedComment = DeletedCommentType;
17 changes: 17 additions & 0 deletions quotevote-backend/app/data/types/DeletedMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { GraphQLObjectType, GraphQLString, type GraphQLFieldConfigMap } from 'graphql';
import type { GraphQLContext } from '~/types/graphql';

interface DeletedIdPayload {
_id?: string;
}

export const DeletedMessageType: GraphQLObjectType<DeletedIdPayload, GraphQLContext> =
new GraphQLObjectType<DeletedIdPayload, GraphQLContext>({
name: 'DeletedMessage',
description: 'Response payload for a soft-deleted Message mutation.',
fields: (): GraphQLFieldConfigMap<DeletedIdPayload, GraphQLContext> => ({
_id: { type: GraphQLString },
}),
});

export const DeletedMessage = DeletedMessageType;
17 changes: 17 additions & 0 deletions quotevote-backend/app/data/types/DeletedPost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { GraphQLObjectType, GraphQLString, type GraphQLFieldConfigMap } from 'graphql';
import type { GraphQLContext } from '~/types/graphql';

interface DeletedIdPayload {
_id?: string;
}

export const DeletedPostType: GraphQLObjectType<DeletedIdPayload, GraphQLContext> =
new GraphQLObjectType<DeletedIdPayload, GraphQLContext>({
name: 'DeletedPost',
description: 'Response payload for a soft-deleted Post mutation.',
fields: (): GraphQLFieldConfigMap<DeletedIdPayload, GraphQLContext> => ({
_id: { type: GraphQLString },
}),
});

export const DeletedPost = DeletedPostType;
17 changes: 17 additions & 0 deletions quotevote-backend/app/data/types/DeletedQuote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { GraphQLObjectType, GraphQLString, type GraphQLFieldConfigMap } from 'graphql';
import type { GraphQLContext } from '~/types/graphql';

interface DeletedIdPayload {
_id?: string;
}

export const DeletedQuoteType: GraphQLObjectType<DeletedIdPayload, GraphQLContext> =
new GraphQLObjectType<DeletedIdPayload, GraphQLContext>({
name: 'DeletedQuote',
description: 'Response payload for a soft-deleted Quote mutation.',
fields: (): GraphQLFieldConfigMap<DeletedIdPayload, GraphQLContext> => ({
_id: { type: GraphQLString },
}),
});

export const DeletedQuote = DeletedQuoteType;
17 changes: 17 additions & 0 deletions quotevote-backend/app/data/types/DeletedVote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { GraphQLObjectType, GraphQLString, type GraphQLFieldConfigMap } from 'graphql';
import type { GraphQLContext } from '~/types/graphql';

interface DeletedIdPayload {
_id?: string;
}

export const DeletedVoteType: GraphQLObjectType<DeletedIdPayload, GraphQLContext> =
new GraphQLObjectType<DeletedIdPayload, GraphQLContext>({
name: 'DeletedVote',
description: 'Response payload for a soft-deleted Vote mutation.',
fields: (): GraphQLFieldConfigMap<DeletedIdPayload, GraphQLContext> => ({
_id: { type: GraphQLString },
}),
});

export const DeletedVote = DeletedVoteType;
Loading
Loading