diff --git a/.gitignore b/.gitignore index e813bed..e3e38ba 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* lib/.eslintcache + +# Test files +test-german-filter.js +test-german-filter.ts diff --git a/GERMAN_IDIOM_FILTERING.md b/GERMAN_IDIOM_FILTERING.md new file mode 100644 index 0000000..46e2810 --- /dev/null +++ b/GERMAN_IDIOM_FILTERING.md @@ -0,0 +1,152 @@ +# German Idiom Filtering Documentation + +## Overview +Enhanced GraphQL endpoint for filtering German idioms with advanced parameters. + +## New Query Parameters + +The existing `idioms` query now supports an additional `germanFilter` parameter when querying German idioms (locale: "de"). + +### GraphQL Schema Addition + +```graphql +type Query { + idioms( + cursor: String, + filter: String, + locale: String, + limit: Int, + germanFilter: GermanIdiomFilter + ): IdiomConnection! +} + +input GermanIdiomFilter { + # Filter by specific German regions + regions: [String!] + + # Filter by difficulty level for German learners + difficulty: DifficultyLevel + + # Filter by specific German-related tags + tags: [String!] + + # Filter idioms that have literal translations + hasLiteralTranslation: Boolean + + # Filter idioms that have phonetic transliterations + hasTransliteration: Boolean +} + +enum DifficultyLevel { + BEGINNER + INTERMEDIATE + ADVANCED +} +``` + +## Usage Examples + +### Basic German Idiom Query +```graphql +query { + idioms(locale: "de", limit: 10) { + edges { + node { + id + title + description + tags + literalTranslation + } + } + } +} +``` + +### Filter by German Regions +```graphql +query { + idioms( + locale: "de", + germanFilter: { + regions: ["bavarian", "austrian"] + } + ) { + edges { + node { + id + title + description + language { + languageName + } + } + } + } +} +``` + +### Filter by Difficulty Level +```graphql +query { + idioms( + locale: "de", + germanFilter: { + difficulty: BEGINNER + } + ) { + edges { + node { + id + title + description + tags + } + } + } +} +``` + + +### Advanced Filtering +```graphql +query { + idioms( + locale: "de", + germanFilter: { + regions: ["swiss"], + difficulty: INTERMEDIATE, + tags: ["common", "business"], + hasLiteralTranslation: true + } + ) { + edges { + node { + id + title + description + literalTranslation + tags + language { + countries { + countryKey + countryName + } + } + } + } + } +} +``` + +## Supported Region Values +- `"northern"` - Northern Germany (DE) +- `"southern"` - Southern Germany (DE) +- `"bavarian"` - Bavaria region (DE) +- `"austrian"` - Austria (AT) +- `"swiss"` - Switzerland (CH) + +## Difficulty Level Mapping +- `BEGINNER`: Idioms tagged with "beginner", "easy", "simple", "basic" +- `INTERMEDIATE`: Idioms tagged with "intermediate", "medium", "common" +- `ADVANCED`: Idioms tagged with "advanced", "difficult", "complex", "archaic", "literary" \ No newline at end of file diff --git a/lib/server/dataProvider/idiomDataProvider.ts b/lib/server/dataProvider/idiomDataProvider.ts index b23dcfb..82323a5 100644 --- a/lib/server/dataProvider/idiomDataProvider.ts +++ b/lib/server/dataProvider/idiomDataProvider.ts @@ -1,5 +1,6 @@ import { Db, Collection, ObjectID, FilterQuery } from 'mongodb' import { Idiom, IdiomCreateInput, IdiomUpdateInput, QueryIdiomsArgs, IdiomOperationResult, OperationStatus, QueryIdiomArgs } from '../_graphql/types'; +import { QueryIdiomsArgsWithGermanFilter, GermanIdiomFilter, DifficultyLevel } from '../model/germanIdiomTypes'; import { Languages, LanguageModel } from './languages' import { UserModel, IdiomExpandOptions, MinimalIdiom } from '../model/types'; import { DbIdiom, mapDbIdiom, DbIdiomChangeProposal, IdiomProposalType, Paged, DbEquivalent, EquivalentSource, DbEquivalentClosureStatus } from './mapping'; @@ -477,10 +478,11 @@ export class IdiomDataProvider { }); } - async queryIdioms(args: QueryIdiomsArgs, idiomExpandOptions: IdiomExpandOptions): Promise> { + async queryIdioms(args: QueryIdiomsArgsWithGermanFilter, idiomExpandOptions: IdiomExpandOptions): Promise> { const filter = args && args.filter ? args.filter : undefined; const limit = args && args.limit ? args.limit : 50; const locale = args && args.locale ? args.locale : "en"; + const germanFilter = args && args.germanFilter ? args.germanFilter : undefined; let skip = args && args.cursor && Number.parseInt(args.cursor); if (isNaN(skip)) { skip = 0; @@ -496,6 +498,83 @@ export class IdiomDataProvider { findFilter = { languageKey: { $eq: locale } }; } + // Apply German-specific filtering + if (germanFilter && locale === "de") { + const germanFilters: FilterQuery[] = []; + + // Filter by German regions (stored in countryKeys) + if (germanFilter.regions && germanFilter.regions.length > 0) { + const regionFilter = this.buildGermanRegionFilter(germanFilter.regions); + if (regionFilter) { + germanFilters.push(regionFilter); + } + } + + // Filter by difficulty level (inferred from tags) + if (germanFilter.difficulty) { + const difficultyFilter = this.buildDifficultyFilter(germanFilter.difficulty); + if (difficultyFilter) { + germanFilters.push(difficultyFilter); + } + } + + // Filter by specific tags + if (germanFilter.tags && germanFilter.tags.length > 0) { + germanFilters.push({ + tags: { $in: germanFilter.tags } + }); + } + + // Filter by presence of literal translation + if (germanFilter.hasLiteralTranslation !== undefined) { + if (germanFilter.hasLiteralTranslation) { + germanFilters.push({ + literalTranslation: { $exists: true, $ne: null, $ne: "" } + }); + } else { + germanFilters.push({ + $or: [ + { literalTranslation: { $exists: false } }, + { literalTranslation: null }, + { literalTranslation: "" } + ] + }); + } + } + + // Filter by presence of transliteration + if (germanFilter.hasTransliteration !== undefined) { + if (germanFilter.hasTransliteration) { + germanFilters.push({ + transliteration: { $exists: true, $ne: null, $ne: "" } + }); + } else { + germanFilters.push({ + $or: [ + { transliteration: { $exists: false } }, + { transliteration: null }, + { transliteration: "" } + ] + }); + } + } + + + + // Combine German filters + if (germanFilters.length > 0) { + const combinedGermanFilter = germanFilters.length === 1 + ? germanFilters[0] + : { $and: germanFilters }; + + if (findFilter) { + findFilter = { $and: [findFilter, combinedGermanFilter] }; + } else { + findFilter = combinedGermanFilter; + } + } + } + if (filter) { const filterRegex = escapeRegex(filter); const filterRegexObj = { $regex: filterRegex, $options: 'i' }; @@ -712,4 +791,55 @@ export class IdiomDataProvider { private isUserProvisional(currentUser: UserModel) { return !currentUser.hasEditPermission(); } + + /** + * Build MongoDB filter for German regions based on country codes + */ + private buildGermanRegionFilter(regions: string[]): FilterQuery | null { + const regionCountryMap: { [key: string]: string[] } = { + "northern": ["DE"], // Northern Germany + "southern": ["DE"], // Southern Germany + "bavarian": ["DE"], // Bavaria region + "austrian": ["AT"], // Austria + "swiss": ["CH"] // Switzerland + }; + + const countryCodes: string[] = []; + for (const region of regions) { + const codes = regionCountryMap[region.toLowerCase()]; + if (codes) { + countryCodes.push(...codes); + } + } + + if (countryCodes.length === 0) { + return null; + } + + return { + countryKeys: { $in: countryCodes } + }; + } + + /** + * Build MongoDB filter for difficulty level based on tags + */ + private buildDifficultyFilter(difficulty: DifficultyLevel): FilterQuery | null { + const difficultyTagMap: { [key: string]: string[] } = { + [DifficultyLevel.BEGINNER]: ["beginner", "easy", "simple", "basic"], + [DifficultyLevel.INTERMEDIATE]: ["intermediate", "medium", "common"], + [DifficultyLevel.ADVANCED]: ["advanced", "difficult", "complex", "archaic", "literary"] + }; + + const tags = difficultyTagMap[difficulty]; + if (!tags || tags.length === 0) { + return null; + } + + return { + tags: { $in: tags } + }; + } + + } \ No newline at end of file diff --git a/lib/server/model/germanIdiomTypes.ts b/lib/server/model/germanIdiomTypes.ts new file mode 100644 index 0000000..628e346 --- /dev/null +++ b/lib/server/model/germanIdiomTypes.ts @@ -0,0 +1,24 @@ +// Manual type definitions for German idiom filtering +// This will be replaced by auto-generated types once the build process is fixed + +export interface GermanIdiomFilter { + regions?: string[]; + difficulty?: DifficultyLevel; + tags?: string[]; + hasLiteralTranslation?: boolean; + hasTransliteration?: boolean; +} + +export enum DifficultyLevel { + BEGINNER = 'BEGINNER', + INTERMEDIATE = 'INTERMEDIATE', + ADVANCED = 'ADVANCED' +} + +export interface QueryIdiomsArgsWithGermanFilter { + cursor?: string | null; + filter?: string | null; + locale?: string | null; + limit?: number | null; + germanFilter?: GermanIdiomFilter | null; +} \ No newline at end of file diff --git a/lib/server/resolvers/idiomResolver.ts b/lib/server/resolvers/idiomResolver.ts index 629790d..482ced3 100644 --- a/lib/server/resolvers/idiomResolver.ts +++ b/lib/server/resolvers/idiomResolver.ts @@ -4,6 +4,7 @@ import { MutationAddEquivalentArgs, MutationRemoveEquivalentArgs, QueryIdiomArgs, QueryIdiomsArgs, IdiomConnection, PageInfo, IdiomEdge, IdiomOperationResult, OperationStatus, MutationComputeEquivalentClosureArgs } from "../_graphql/types"; +import { QueryIdiomsArgsWithGermanFilter } from '../model/germanIdiomTypes'; import { GlobalContext, IdiomExpandOptions } from '../model/types'; import { GraphQLResolveInfo } from 'graphql'; import { traverse } from './traverser'; @@ -13,7 +14,14 @@ export default { Query: { idioms: async (parent, args: QueryIdiomsArgs, context: GlobalContext, info) => { const expandOptions: IdiomExpandOptions = getIdiomExpandOptions(info); - const response = await context.dataProviders.idiom.queryIdioms(args, expandOptions); + + // Convert args to include potential germanFilter + const argsWithGermanFilter: QueryIdiomsArgsWithGermanFilter = { + ...args, + germanFilter: (args as any).germanFilter || null + }; + + const response = await context.dataProviders.idiom.queryIdioms(argsWithGermanFilter, expandOptions); // This is really not right. Using skip/take is weak in two ways // 1. Performance isn't great since its paging whole query still diff --git a/lib/server/schema/idiom.ts b/lib/server/schema/idiom.ts index ffe2961..225a0f7 100644 --- a/lib/server/schema/idiom.ts +++ b/lib/server/schema/idiom.ts @@ -3,7 +3,7 @@ import { gql } from 'apollo-server-express'; export default gql` type Query { idiom(id: ID, slug: String): Idiom @cacheControl(maxAge: 1800) - idioms(cursor: String, filter: String, locale: String, limit: Int): IdiomConnection! @cacheControl(maxAge: 1800) + idioms(cursor: String, filter: String, locale: String, limit: Int, germanFilter: GermanIdiomFilter): IdiomConnection! @cacheControl(maxAge: 1800) } type Mutation { @@ -55,6 +55,29 @@ export default gql` endCursor: String! } + input GermanIdiomFilter { + # Filter by specific German regions (e.g., "northern", "southern", "bavarian", "austrian", "swiss") + regions: [String!] + + # Filter by difficulty level for German learners + difficulty: DifficultyLevel + + # Filter by specific German-related tags + tags: [String!] + + # Filter idioms that have literal translations + hasLiteralTranslation: Boolean + + # Filter idioms that have phonetic transliterations + hasTransliteration: Boolean + } + + enum DifficultyLevel { + BEGINNER + INTERMEDIATE + ADVANCED + } + input IdiomCreateInput { title: String! description: String