tercul-backend/internal/adapters/graphql/schema.graphqls
google-labs-jules[bot] caf07df08d feat(analytics): Enhance analytics capabilities
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.
2025-09-07 19:26:51 +00:00

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
}