mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 05:11:34 +00:00
This commit introduces a comprehensive enhancement of the application's analytics features, addressing performance, data modeling, and feature set. The key changes include: - **Performance Improvement:** The analytics repository now uses a database "UPSERT" operation to increment counters, reducing two separate database calls (read and write) into a single, more efficient operation. - **New Metrics:** The `WorkStats` and `TranslationStats` models have been enriched with new, calculated metrics: - `ReadingTime`: An estimation of the time required to read the work or translation. - `Complexity`: A score representing the linguistic complexity of the text. - `Sentiment`: A score indicating the emotional tone of the text. - **Service Refactoring:** The analytics service has been refactored to support the new metrics. It now includes methods to calculate and update these scores, leveraging the existing linguistics package for text analysis. - **GraphQL API Expansion:** The new analytics fields (`readingTime`, `complexity`, `sentiment`) have been exposed through the GraphQL API by updating the `WorkStats` and `TranslationStats` types in the schema. - **Validation and Testing:** - GraphQL input validation has been centralized and improved by moving from ad-hoc checks to a consistent validation pattern in the GraphQL layer. - The test suite has been significantly improved with the addition of new tests for the analytics service and the data access layer, ensuring the correctness and robustness of the new features. This includes fixing several bugs that were discovered during the development process.
712 lines
12 KiB
GraphQL
712 lines
12 KiB
GraphQL
# GraphQL schema for Tercul literary platform
|
|
|
|
# Core types
|
|
type Work {
|
|
id: ID!
|
|
name: String!
|
|
language: String!
|
|
content: String
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
translations: [Translation!]
|
|
authors: [Author!]
|
|
tags: [Tag!]
|
|
categories: [Category!]
|
|
readabilityScore: ReadabilityScore
|
|
writingStyle: WritingStyle
|
|
emotions: [Emotion!]
|
|
topicClusters: [TopicCluster!]
|
|
moods: [Mood!]
|
|
concepts: [Concept!]
|
|
linguisticLayers: [LinguisticLayer!]
|
|
stats: WorkStats
|
|
textMetadata: TextMetadata
|
|
poeticAnalysis: PoeticAnalysis
|
|
copyright: Copyright
|
|
copyrightClaims: [CopyrightClaim!]
|
|
collections: [Collection!]
|
|
comments: [Comment!]
|
|
likes: [Like!]
|
|
bookmarks: [Bookmark!]
|
|
}
|
|
|
|
type Translation {
|
|
id: ID!
|
|
name: String!
|
|
language: String!
|
|
content: String
|
|
workId: ID!
|
|
work: Work!
|
|
translator: User
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
stats: TranslationStats
|
|
copyright: Copyright
|
|
copyrightClaims: [CopyrightClaim!]
|
|
comments: [Comment!]
|
|
likes: [Like!]
|
|
}
|
|
|
|
type Author {
|
|
id: ID!
|
|
name: String!
|
|
language: String!
|
|
biography: String
|
|
birthDate: String
|
|
deathDate: String
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
works: [Work!]
|
|
books: [Book!]
|
|
country: Country
|
|
city: City
|
|
place: Place
|
|
address: Address
|
|
copyrightClaims: [CopyrightClaim!]
|
|
copyright: Copyright
|
|
}
|
|
|
|
type User {
|
|
id: ID!
|
|
username: String!
|
|
email: String!
|
|
firstName: String
|
|
lastName: String
|
|
displayName: String
|
|
bio: String
|
|
avatarUrl: String
|
|
role: UserRole!
|
|
lastLoginAt: String
|
|
verified: Boolean!
|
|
active: Boolean!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
|
|
# Relationships
|
|
translations: [Translation!]
|
|
comments: [Comment!]
|
|
likes: [Like!]
|
|
bookmarks: [Bookmark!]
|
|
collections: [Collection!]
|
|
contributions: [Contribution!]
|
|
|
|
# Location
|
|
country: Country
|
|
city: City
|
|
address: Address
|
|
|
|
# Stats
|
|
stats: UserStats
|
|
}
|
|
|
|
type UserProfile {
|
|
id: ID!
|
|
userId: ID!
|
|
user: User!
|
|
phoneNumber: String
|
|
website: String
|
|
twitter: String
|
|
facebook: String
|
|
linkedIn: String
|
|
github: String
|
|
preferences: JSON
|
|
settings: JSON
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
}
|
|
|
|
enum UserRole {
|
|
READER
|
|
CONTRIBUTOR
|
|
REVIEWER
|
|
EDITOR
|
|
ADMIN
|
|
}
|
|
|
|
type Book {
|
|
id: ID!
|
|
name: String!
|
|
language: String!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
works: [Work!]
|
|
stats: BookStats
|
|
copyright: Copyright
|
|
copyrightClaims: [CopyrightClaim!]
|
|
}
|
|
|
|
type Collection {
|
|
id: ID!
|
|
name: String!
|
|
description: String
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
works: [Work!]
|
|
user: User
|
|
stats: CollectionStats
|
|
}
|
|
|
|
type Tag {
|
|
id: ID!
|
|
name: String!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
works: [Work!]
|
|
}
|
|
|
|
type Category {
|
|
id: ID!
|
|
name: String!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
works: [Work!]
|
|
}
|
|
|
|
type Comment {
|
|
id: ID!
|
|
text: String!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
user: User!
|
|
work: Work
|
|
translation: Translation
|
|
lineNumber: Int
|
|
parentComment: Comment
|
|
childComments: [Comment!]
|
|
likes: [Like!]
|
|
}
|
|
|
|
type Like {
|
|
id: ID!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
user: User!
|
|
work: Work
|
|
translation: Translation
|
|
comment: Comment
|
|
}
|
|
|
|
type Bookmark {
|
|
id: ID!
|
|
name: String
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
user: User!
|
|
work: Work!
|
|
}
|
|
|
|
type Contribution {
|
|
id: ID!
|
|
name: String!
|
|
status: ContributionStatus!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
user: User!
|
|
work: Work
|
|
translation: Translation
|
|
}
|
|
|
|
enum ContributionStatus {
|
|
DRAFT
|
|
SUBMITTED
|
|
UNDER_REVIEW
|
|
APPROVED
|
|
REJECTED
|
|
}
|
|
|
|
type ReadabilityScore {
|
|
id: ID!
|
|
score: Float!
|
|
language: String!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
work: Work
|
|
}
|
|
|
|
type WritingStyle {
|
|
id: ID!
|
|
name: String!
|
|
language: String!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
work: Work
|
|
}
|
|
|
|
type Emotion {
|
|
id: ID!
|
|
name: String!
|
|
language: String!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
user: User
|
|
work: Work
|
|
collection: Collection
|
|
}
|
|
|
|
type TopicCluster {
|
|
id: ID!
|
|
name: String!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
works: [Work!]
|
|
}
|
|
|
|
type Mood {
|
|
id: ID!
|
|
name: String!
|
|
language: String!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
works: [Work!]
|
|
}
|
|
|
|
type Concept {
|
|
id: ID!
|
|
name: String!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
works: [Work!]
|
|
words: [Word!]
|
|
}
|
|
|
|
type Word {
|
|
id: ID!
|
|
name: String!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
concept: Concept
|
|
works: [Work!]
|
|
}
|
|
|
|
type LinguisticLayer {
|
|
id: ID!
|
|
name: String!
|
|
language: String!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
works: [Work!]
|
|
}
|
|
|
|
type WorkStats {
|
|
id: ID!
|
|
views: Int
|
|
likes: Int
|
|
comments: Int
|
|
bookmarks: Int
|
|
shares: Int
|
|
translationCount: Int
|
|
readingTime: Int
|
|
complexity: Float
|
|
sentiment: Float
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
work: Work!
|
|
}
|
|
|
|
type TranslationStats {
|
|
id: ID!
|
|
views: Int
|
|
likes: Int
|
|
comments: Int
|
|
shares: Int
|
|
readingTime: Int
|
|
sentiment: Float
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
translation: Translation!
|
|
}
|
|
|
|
type UserStats {
|
|
id: ID!
|
|
activity: Int!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
user: User!
|
|
}
|
|
|
|
type BookStats {
|
|
id: ID!
|
|
sales: Int!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
book: Book!
|
|
}
|
|
|
|
type CollectionStats {
|
|
id: ID!
|
|
items: Int!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
collection: Collection!
|
|
}
|
|
|
|
type TextMetadata {
|
|
id: ID!
|
|
analysis: String!
|
|
language: String!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
work: Work!
|
|
}
|
|
|
|
type PoeticAnalysis {
|
|
id: ID!
|
|
structure: String!
|
|
language: String!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
work: Work!
|
|
}
|
|
|
|
type Copyright {
|
|
id: ID!
|
|
name: String!
|
|
language: String!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
workOwner: Author
|
|
works: [Work!]
|
|
translations: [Translation!]
|
|
books: [Book!]
|
|
sources: [Source!]
|
|
}
|
|
|
|
type CopyrightClaim {
|
|
id: ID!
|
|
details: String!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
work: Work
|
|
translation: Translation
|
|
book: Book
|
|
source: Source
|
|
author: Author
|
|
user: User
|
|
}
|
|
|
|
type Country {
|
|
id: ID!
|
|
name: String!
|
|
language: String!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
authors: [Author!]
|
|
users: [User!]
|
|
}
|
|
|
|
type City {
|
|
id: ID!
|
|
name: String!
|
|
language: String!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
country: Country
|
|
authors: [Author!]
|
|
users: [User!]
|
|
}
|
|
|
|
type Place {
|
|
id: ID!
|
|
name: String!
|
|
language: String!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
city: City
|
|
country: Country
|
|
authors: [Author!]
|
|
}
|
|
|
|
type Address {
|
|
id: ID!
|
|
street: String!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
city: City
|
|
country: Country
|
|
authors: [Author!]
|
|
users: [User!]
|
|
}
|
|
|
|
type Source {
|
|
id: ID!
|
|
name: String!
|
|
language: String!
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
copyright: Copyright
|
|
copyrightClaims: [CopyrightClaim!]
|
|
works: [Work!]
|
|
}
|
|
|
|
type Edge {
|
|
id: ID!
|
|
sourceTable: String!
|
|
sourceId: ID!
|
|
targetTable: String!
|
|
targetId: ID!
|
|
relation: String!
|
|
language: String
|
|
extra: JSON
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
}
|
|
|
|
scalar JSON
|
|
|
|
directive @binding(constraint: String!) on INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION
|
|
|
|
# Queries
|
|
type Query {
|
|
# Work queries
|
|
work(id: ID!): Work
|
|
works(
|
|
limit: Int
|
|
offset: Int
|
|
language: String
|
|
authorId: ID
|
|
categoryId: ID
|
|
tagId: ID
|
|
search: String
|
|
): [Work!]!
|
|
|
|
# Translation queries
|
|
translation(id: ID!): Translation
|
|
translations(
|
|
workId: ID!
|
|
language: String
|
|
limit: Int
|
|
offset: Int
|
|
): [Translation!]!
|
|
|
|
# Author queries
|
|
author(id: ID!): Author
|
|
authors(
|
|
limit: Int
|
|
offset: Int
|
|
search: String
|
|
countryId: ID
|
|
): [Author!]!
|
|
|
|
# User queries
|
|
user(id: ID!): User
|
|
userByEmail(email: String!): User
|
|
userByUsername(username: String!): User
|
|
users(
|
|
limit: Int
|
|
offset: Int
|
|
role: UserRole
|
|
): [User!]!
|
|
me: User
|
|
userProfile(userId: ID!): UserProfile
|
|
|
|
# Collection queries
|
|
collection(id: ID!): Collection
|
|
collections(
|
|
userId: ID
|
|
limit: Int
|
|
offset: Int
|
|
): [Collection!]!
|
|
|
|
# Tag queries
|
|
tag(id: ID!): Tag
|
|
tags(limit: Int, offset: Int): [Tag!]!
|
|
|
|
# Category queries
|
|
category(id: ID!): Category
|
|
categories(limit: Int, offset: Int): [Category!]!
|
|
|
|
# Comment queries
|
|
comment(id: ID!): Comment
|
|
comments(
|
|
workId: ID
|
|
translationId: ID
|
|
userId: ID
|
|
limit: Int
|
|
offset: Int
|
|
): [Comment!]!
|
|
|
|
# Search
|
|
search(
|
|
query: String!
|
|
limit: Int
|
|
offset: Int
|
|
filters: SearchFilters
|
|
): SearchResults!
|
|
}
|
|
|
|
input SearchFilters {
|
|
languages: [String!]
|
|
categories: [ID!]
|
|
tags: [ID!]
|
|
authors: [ID!]
|
|
dateFrom: String
|
|
dateTo: String
|
|
}
|
|
|
|
type SearchResults {
|
|
works: [Work!]!
|
|
translations: [Translation!]!
|
|
authors: [Author!]!
|
|
total: Int!
|
|
}
|
|
|
|
# Mutations
|
|
type Mutation {
|
|
# Authentication
|
|
register(input: RegisterInput!): AuthPayload!
|
|
login(input: LoginInput!): AuthPayload!
|
|
|
|
# Work mutations
|
|
createWork(input: WorkInput!): Work!
|
|
updateWork(id: ID!, input: WorkInput!): Work!
|
|
deleteWork(id: ID!): Boolean!
|
|
|
|
# Translation mutations
|
|
createTranslation(input: TranslationInput!): Translation!
|
|
updateTranslation(id: ID!, input: TranslationInput!): Translation!
|
|
deleteTranslation(id: ID!): Boolean!
|
|
|
|
# Author mutations
|
|
createAuthor(input: AuthorInput!): Author!
|
|
updateAuthor(id: ID!, input: AuthorInput!): Author!
|
|
deleteAuthor(id: ID!): Boolean!
|
|
|
|
# User mutations
|
|
updateUser(id: ID!, input: UserInput!): User!
|
|
deleteUser(id: ID!): Boolean!
|
|
|
|
# Collection mutations
|
|
createCollection(input: CollectionInput!): Collection!
|
|
updateCollection(id: ID!, input: CollectionInput!): Collection!
|
|
deleteCollection(id: ID!): Boolean!
|
|
addWorkToCollection(collectionId: ID!, workId: ID!): Collection!
|
|
removeWorkFromCollection(collectionId: ID!, workId: ID!): Collection!
|
|
|
|
# Comment mutations
|
|
createComment(input: CommentInput!): Comment!
|
|
updateComment(id: ID!, input: CommentInput!): Comment!
|
|
deleteComment(id: ID!): Boolean!
|
|
|
|
# Like mutations
|
|
createLike(input: LikeInput!): Like!
|
|
deleteLike(id: ID!): Boolean!
|
|
|
|
# Bookmark mutations
|
|
createBookmark(input: BookmarkInput!): Bookmark!
|
|
deleteBookmark(id: ID!): Boolean!
|
|
|
|
# Contribution mutations
|
|
createContribution(input: ContributionInput!): Contribution!
|
|
updateContribution(id: ID!, input: ContributionInput!): Contribution!
|
|
deleteContribution(id: ID!): Boolean!
|
|
reviewContribution(id: ID!, status: ContributionStatus!, feedback: String): Contribution!
|
|
|
|
# Additional authentication mutations
|
|
logout: Boolean!
|
|
refreshToken: AuthPayload!
|
|
forgotPassword(email: String!): Boolean!
|
|
resetPassword(token: String!, newPassword: String!): Boolean!
|
|
verifyEmail(token: String!): Boolean!
|
|
resendVerificationEmail(email: String!): Boolean!
|
|
|
|
# User profile mutations
|
|
updateProfile(input: UserInput!): User!
|
|
changePassword(currentPassword: String!, newPassword: String!): Boolean!
|
|
}
|
|
|
|
# Input types
|
|
input LoginInput {
|
|
email: String!
|
|
password: String!
|
|
}
|
|
|
|
input RegisterInput {
|
|
username: String!
|
|
email: String!
|
|
password: String!
|
|
firstName: String!
|
|
lastName: String!
|
|
}
|
|
|
|
type AuthPayload {
|
|
token: String!
|
|
user: User!
|
|
}
|
|
|
|
input WorkInput {
|
|
name: String! @binding(constraint: "required,length(3|255)")
|
|
language: String! @binding(constraint: "required,alpha,length(2|2)")
|
|
content: String
|
|
authorIds: [ID!]
|
|
tagIds: [ID!]
|
|
categoryIds: [ID!]
|
|
}
|
|
|
|
input TranslationInput {
|
|
name: String! @binding(constraint: "required,length(3|255)")
|
|
language: String! @binding(constraint: "required,alpha,length(2|2)")
|
|
content: String
|
|
workId: ID! @binding(constraint: "required")
|
|
}
|
|
|
|
input AuthorInput {
|
|
name: String! @binding(constraint: "required,length(3|255)")
|
|
language: String! @binding(constraint: "required,alpha,length(2|2)")
|
|
biography: String
|
|
birthDate: String
|
|
deathDate: String
|
|
countryId: ID
|
|
cityId: ID
|
|
placeId: ID
|
|
addressId: ID
|
|
}
|
|
|
|
input UserInput {
|
|
username: String
|
|
email: String
|
|
password: String
|
|
firstName: String
|
|
lastName: String
|
|
displayName: String
|
|
bio: String
|
|
avatarUrl: String
|
|
role: UserRole
|
|
verified: Boolean
|
|
active: Boolean
|
|
countryId: ID
|
|
cityId: ID
|
|
addressId: ID
|
|
}
|
|
|
|
input CollectionInput {
|
|
name: String!
|
|
description: String
|
|
workIds: [ID!]
|
|
}
|
|
|
|
input CommentInput {
|
|
text: String!
|
|
workId: ID
|
|
translationId: ID
|
|
lineNumber: Int
|
|
parentCommentId: ID
|
|
}
|
|
|
|
input LikeInput {
|
|
workId: ID
|
|
translationId: ID
|
|
commentId: ID
|
|
}
|
|
|
|
input BookmarkInput {
|
|
name: String
|
|
workId: ID!
|
|
}
|
|
|
|
input ContributionInput {
|
|
name: String!
|
|
workId: ID
|
|
translationId: ID
|
|
status: ContributionStatus
|
|
}
|