diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index c1b3d38..14aa576 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/package.json b/package.json index cdd2562..6e10391 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/jest-e2e.json", "prepare": "husky install", - "gql-to-interfaces": "cd ./src && ts-node schema-to-typings" + "gql-to-interfaces": "cd ./src/scripts && ts-node schema-to-typings.ts" }, "dependencies": { "@apollo/server": "^4.10.4", diff --git a/src/app.module.ts b/src/app.module.ts index 3b0754a..8392e50 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,55 +1,19 @@ -import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { GraphQLModule } from '@nestjs/graphql'; -import joi from 'joi'; -import { DiscoverFilteringModule } from './discover-filtering/discover-filtering.module'; -import { DiscoverFormDataModule } from './discover-form-data/discover-form-data.module'; -import { FilteringOptionsModule } from './filtering-options/filtering-options.module'; -import { MovieModule } from './movie/movie.module'; -import { PersonModule } from './person/person.module'; -import { ShowModule } from './show/show.module'; +import { graphqlConfig } from './config/graphql.config'; +import { serviceConfig } from './config/service.config'; +import { DiscoverFilteringModule } from './modules/discover/filtering/discover-filtering.module'; +import { DiscoverFormDataModule } from './modules/discover/form-data/discover-form-data.module'; +import { FilteringOptionsModule } from './modules/discover/options/filtering-options.module'; +import { MovieModule } from './modules/movies/movie.module'; +import { PersonModule } from './modules/person/person.module'; +import { ShowModule } from './modules/shows/show.module'; @Module({ imports: [ - ConfigModule.forRoot({ - isGlobal: true, - validationSchema: joi.object({ - THE_OPEN_MOVIE_DATABASE_API_KEY: joi.string().required() - }) - }), - GraphQLModule.forRoot({ - driver: ApolloDriver, - playground: true, + serviceConfig, + graphqlConfig, - // Must start with ./src/ for some reason otherwise nestjs doesn't pick up the files grr - typePaths: [ - // Enums used by all the graphql schemas - './src/models/enum.graphql', - './src/models/Pagination.graphql', - - // Entertainment specific models, used for the Movie and Show schemas - './src/models/entertainment/BelongsToCollection.graphql', - './src/models/entertainment/Cast.graphql', - './src/models/entertainment/Company.graphql', - './src/models/entertainment/Crew.graphql', - './src/models/entertainment/Genre.graphql', - './src/models/entertainment/Keyword.graphql', - './src/models/entertainment/Recommendation.graphql', - './src/models/entertainment/Review.graphql', - './src/models/entertainment/Social.graphql', - './src/models/entertainment/Video.graphql', - './src/models/Discover.graphql', - - // Individual resource schemas - './src/models/Show.graphql', - './src/models/Movie.graphql', - './src/models/Person.graphql', - - './src/models/Query.graphql' - ] - }), ShowModule, MovieModule, PersonModule, diff --git a/src/common/README.md b/src/common/README.md new file mode 100644 index 0000000..d56cde1 --- /dev/null +++ b/src/common/README.md @@ -0,0 +1,36 @@ +# Common Directory + +The Common directory is dedicated to shared code that is specific to the application's business logic and domain models, but needs to be reused across multiple features. Unlike core utilities, items in Common are tied to the application's domain concepts. + +## Purpose + +- Houses shared business logic and domain-specific utilities +- Provides reusable components that implement business rules +- Contains shared types and interfaces related to domain models +- Centralizes common validation logic and business constraints + +## Examples of What Goes Here + +1. Shared business validation rules +2. Common domain model transformations +3. Business-specific utility functions +4. Shared domain interfaces and types +5. Common business calculations or formulas + +## When to Use Common + +Use the Common directory when: + +- The code implements business rules needed by multiple features +- You have domain-specific logic that's reused across different modules +- You need to share business validation or transformation logic +- Multiple features need access to the same domain-specific utilities + +## When Not to Use Common + +Don't use Common for: + +- Generic utility functions (use Core instead) +- Framework-specific code +- Infrastructure concerns +- Pure technical utilities with no business logic diff --git a/src/lib/index.ts b/src/common/lib/index.ts similarity index 100% rename from src/lib/index.ts rename to src/common/lib/index.ts diff --git a/src/lib/isNonNullable.ts b/src/common/lib/isNonNullable.ts similarity index 100% rename from src/lib/isNonNullable.ts rename to src/common/lib/isNonNullable.ts diff --git a/src/models/Pagination.ts b/src/common/models/Pagination.ts similarity index 100% rename from src/models/Pagination.ts rename to src/common/models/Pagination.ts diff --git a/src/types/Nullable.ts b/src/common/types/Nullable.ts similarity index 100% rename from src/types/Nullable.ts rename to src/common/types/Nullable.ts diff --git a/src/config/README.md b/src/config/README.md new file mode 100644 index 0000000..6fbabcc --- /dev/null +++ b/src/config/README.md @@ -0,0 +1,48 @@ +# Config Directory + +The Config directory contains application-wide configuration settings, environment variables, and setup code that defines how the application behaves in different environments. + +## Purpose + +- Centralizes all configuration-related code and settings +- Manages environment-specific variables and settings +- Defines application-wide setup and initialization +- Houses configuration interfaces and types +- Provides configuration factories and providers + +## What Goes Here + +1. Environment configuration files +2. Service configuration providers +3. Module configuration factories +4. GraphQL and API configurations +5. Type generation configurations +6. Path and file location configurations +7. Global application settings + +## When to Use Config + +Use the Config directory when: + +- Adding new application-wide settings +- Defining environment-specific configurations +- Setting up module or service configurations +- Managing external service connections +- Defining global type generation settings + +## When Not to Use Config + +Don't use Config for: + +- Feature-specific settings (belong in respective modules) +- Business logic or rules +- Utility functions +- Runtime data management +- Local component configurations + +## Current Configuration Files + +- `graphql.config.ts` - GraphQL module configuration +- `service.config.ts` - Core service settings +- `filePaths.ts` - GraphQL schema file path management +- `schemaToTypings.ts` - TypeScript definitions generation config diff --git a/src/config/filePaths.ts b/src/config/filePaths.ts new file mode 100644 index 0000000..f3f5d8f --- /dev/null +++ b/src/config/filePaths.ts @@ -0,0 +1,76 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { join } from 'path'; + +const GRAPHQL_BASE_PATH = 'graphql'; +/** + * Gets the GraphQL schema file paths in either absolute or relative format + * + * Two different path formats are needed for different use cases: + * + * 1. Absolute paths (starting with 'src/'): + * - Required by NestJS GraphQL module to properly load schema files + * - Used in app.module.ts via graphql.config.ts for runtime schema loading + * - The NestJS GraphQL module specifically requires paths to start with 'src/' + * to properly resolve and watch schema files during development + * - Example: 'src/graphql/models/Query.graphql' + * + * 2. Relative paths (from project root): + * - Required by GraphQLDefinitionsFactory for TypeScript interface generation + * - Used in schema-to-typings.ts which runs as a separate process outside NestJS + * - Since this runs as a separate script, it needs full filesystem paths relative + * to the project root to locate the schema files + * - Example: '/path/to/project/graphql/models/Query.graphql' + * + * This dual path handling ensures both: + * - Runtime schema loading works correctly in the NestJS application + * - Type generation script can find and process schema files from the command line + * + * @param pathType - Whether to return absolute or relative paths + * @returns Array of GraphQL schema file paths in the requested format + */ +export function getGraphQLPaths(pathType: 'absolute' | 'relative' = 'absolute') { + const graphqlPaths = [ + // Enums used by all the graphql schemas + 'models/Common/CommonEnums.graphql', + 'models/Common/CommonPagination.graphql', + + // Entertainment specific models + 'models/Entertainment/BelongsToCollection.graphql', + 'models/Entertainment/Cast.graphql', + 'models/Entertainment/Company.graphql', + 'models/Entertainment/Crew.graphql', + 'models/Entertainment/Genre.graphql', + 'models/Entertainment/Keyword.graphql', + 'models/Entertainment/Recommendation.graphql', + 'models/Entertainment/Review.graphql', + 'models/Entertainment/Social.graphql', + 'models/Entertainment/Video.graphql', + + // Discover specific models + 'models/Discover/RangeFilters.graphql', + 'models/Discover/FiltersInput.graphql', + 'models/Discover.graphql', + 'models/Discover/DiscoverResult.graphql', + 'models/Discover/FiltersFormData.graphql', + + // Person specific models + 'models/Person/CreditGroup.graphql', + 'models/Person/Credit.graphql', + 'models/Person/Person.graphql', + + // Individual resource schemas + 'models/Show/Show.graphql', + 'models/Movie/Movie.graphql', + 'models/Person/Person.graphql', + + 'models/Query.graphql' + ]; + + if (pathType === 'absolute') { + // For NestJS GraphQL module (absolute paths starting with src/) + return graphqlPaths.map((path) => `src/${GRAPHQL_BASE_PATH}/${path}`); + } + + // For GraphQLDefinitionsFactory (relative paths from project root) + return graphqlPaths.map((path) => join(process.cwd(), `../${GRAPHQL_BASE_PATH}/${path}`)); +} diff --git a/src/config/graphql.config.ts b/src/config/graphql.config.ts new file mode 100644 index 0000000..91d4f52 --- /dev/null +++ b/src/config/graphql.config.ts @@ -0,0 +1,12 @@ +import { ApolloDriverConfig, ApolloDriver } from '@nestjs/apollo'; +import { GraphQLModule } from '@nestjs/graphql'; + +import { getGraphQLPaths } from './filePaths'; + +export const graphqlConfig = GraphQLModule.forRoot({ + driver: ApolloDriver, + playground: true, + + // Must start with src/ for some reason otherwise nestjs doesn't pick up the files grr + typePaths: getGraphQLPaths('absolute') +}); diff --git a/src/config/graphql/generated/schema.ts b/src/config/graphql/generated/schema.ts new file mode 100644 index 0000000..2264ce5 --- /dev/null +++ b/src/config/graphql/generated/schema.ts @@ -0,0 +1,362 @@ +/* + * ------------------------------------------------------- + * THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) + * ------------------------------------------------------- + */ + +/* eslint-disable */ + +export enum ENTERTAINMENT_TYPES { + MOVIE = 'MOVIE', + TV = 'TV' +} + +export enum RESOURCE_TYPE { + TOP_RATED = 'TOP_RATED', + POPULAR = 'POPULAR', + NOW_PLAYING = 'NOW_PLAYING', + UPCOMING = 'UPCOMING', + AIRING_TODAY = 'AIRING_TODAY', + ON_THE_AIR = 'ON_THE_AIR' +} + +export interface DiscoverFormDataInput { + sort_by?: Nullable; + show_me?: Nullable; + with_watch_monetization_types?: Nullable[]>; + with_genres?: Nullable[]>; + certifications?: Nullable[]>; + with_release_types?: Nullable[]>; + release_date?: Nullable; + air_date?: Nullable; + with_original_language?: Nullable; + region?: Nullable; + vote_average?: Nullable; + with_runtime?: Nullable; + vote_count?: Nullable; + search_first_air_date?: Nullable; + restrict_services?: Nullable; + ott_region?: Nullable; + with_ott_providers?: Nullable[]>; +} + +export interface DateRangeFilterInput { + gte?: Nullable; + lte?: Nullable; +} + +export interface NumberRangeFilterInput { + gte?: Nullable; + lte?: Nullable; +} + +export interface VoteCountFilterInput { + gte?: Nullable; + lte?: Nullable; +} + +export interface PaginatedResult { + meta: PaginationMetaData; +} + +export interface DiscoverResult { + __typename?: 'DiscoverResult'; + adult?: Nullable; + backdropUrl?: Nullable; + posterUrl?: Nullable; + name?: Nullable; + homepage?: Nullable; + id: string; + originCountry?: Nullable[]>; + originalLanguage?: Nullable; + overview?: Nullable; + releaseDate?: Nullable; + popularity?: Nullable; + posterPath?: Nullable; + status?: Nullable; + tagline?: Nullable; + voteAverage?: Nullable; + voteCount?: Nullable; +} + +export interface PaginatedDiscoverResult extends PaginatedResult { + __typename?: 'PaginatedDiscoverResult'; + meta: PaginationMetaData; + results: DiscoverResult[]; +} + +export interface DiscoverFormData { + __typename?: 'DiscoverFormData'; + sort_by?: Nullable; + show_me?: Nullable; + with_watch_monetization_types?: Nullable[]>; + with_genres?: Nullable[]>; + certifications?: Nullable[]>; + with_release_types?: Nullable[]>; + release_date?: Nullable; + air_date?: Nullable; + with_original_language?: Nullable; + region?: Nullable; + vote_average?: Nullable; + with_runtime?: Nullable; + vote_count?: Nullable; + search_first_air_date?: Nullable; + restrict_services?: Nullable; + ott_region?: Nullable; + with_ott_providers?: Nullable[]>; +} + +export interface DateRangeFilter { + __typename?: 'DateRangeFilter'; + gte?: Nullable; + lte?: Nullable; +} + +export interface NumberRangeFilter { + __typename?: 'NumberRangeFilter'; + gte?: Nullable; + lte?: Nullable; +} + +export interface VoteCountFilter { + __typename?: 'VoteCountFilter'; + gte?: Nullable; + lte?: Nullable; +} + +export interface BelongsToCollection { + __typename?: 'BelongsToCollection'; + id?: Nullable; + name?: Nullable; + backgroundUrl?: Nullable; + posterUrl?: Nullable; +} + +export interface Cast { + __typename?: 'Cast'; + id?: Nullable; + character?: Nullable; + profileImageUrl?: Nullable; + gender?: Nullable; + episodeCount?: Nullable; +} + +export interface Company { + __typename?: 'Company'; + id?: Nullable; + logo?: Nullable; + name?: Nullable; +} + +export interface Crew { + __typename?: 'Crew'; + name?: Nullable; + roles?: Nullable; +} + +export interface Genre { + __typename?: 'Genre'; + id?: Nullable; + name?: Nullable; +} + +export interface Keyword { + __typename?: 'Keyword'; + id?: Nullable; + name?: Nullable; +} + +export interface Recommendation { + __typename?: 'Recommendation'; + name?: Nullable; + releaseDate?: Nullable; + backgroundUrl?: Nullable; + rating?: Nullable; +} + +export interface Author { + __typename?: 'Author'; + name?: Nullable; + username?: Nullable; + avatarUrl?: Nullable; + rating?: Nullable; +} + +export interface Review { + __typename?: 'Review'; + author?: Nullable; + isFeatured?: Nullable; + content?: Nullable; + createdOn?: Nullable; +} + +export interface Social { + __typename?: 'Social'; + id?: Nullable; + freebase_mid?: Nullable; + freebase_id?: Nullable; + imdb_id?: Nullable; + tvrage_id?: Nullable; + wikidata_id?: Nullable; + facebook_id?: Nullable; + instagram_id?: Nullable; + tiktok_id?: Nullable; + twitter_id?: Nullable; + youtube_id?: Nullable; +} + +export interface Video { + __typename?: 'Video'; + id?: Nullable; + name?: Nullable; + url?: Nullable; + type?: Nullable; + site?: Nullable; +} + +export interface Movie { + __typename?: 'Movie'; + id?: Nullable; + name?: Nullable; + overview?: Nullable; + backgroundUrl?: Nullable; + posterUrl?: Nullable; + genres?: Nullable[]>; + homepage?: Nullable; + originalLanguage?: Nullable; + productionCompanies?: Nullable[]>; + releaseDate?: Nullable; + voteAverage?: Nullable; + status?: Nullable; + review?: Nullable; + recommendations?: Nullable[]>; + keywords?: Nullable[]>; + social?: Nullable; + topBilledCast?: Nullable[]>; + featuredCrew?: Nullable[]>; + youtubeVideo?: Nullable