From bbfb7a14e287b1d6d72b076396b033e271ce3c67 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Jul 2025 23:05:22 +0000 Subject: [PATCH 1/4] Initial plan From 6ddeaa850a90657f2fd8913327935167cf43e734 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Jul 2025 23:23:50 +0000 Subject: [PATCH 2/4] Add enhanced GraphQL filtering for German idioms Co-authored-by: mmanela <304410+mmanela@users.noreply.github.com> --- GERMAN_IDIOM_FILTERING.md | 182 +++++++++++++++++++ lib/server/dataProvider/idiomDataProvider.ts | 155 +++++++++++++++- lib/server/model/germanIdiomTypes.ts | 25 +++ lib/server/resolvers/idiomResolver.ts | 10 +- lib/server/schema/idiom.ts | 28 ++- test-german-filter.js | 162 +++++++++++++++++ test-german-filter.ts | 170 +++++++++++++++++ 7 files changed, 729 insertions(+), 3 deletions(-) create mode 100644 GERMAN_IDIOM_FILTERING.md create mode 100644 lib/server/model/germanIdiomTypes.ts create mode 100644 test-german-filter.js create mode 100644 test-german-filter.ts diff --git a/GERMAN_IDIOM_FILTERING.md b/GERMAN_IDIOM_FILTERING.md new file mode 100644 index 0000000..7bc74d6 --- /dev/null +++ b/GERMAN_IDIOM_FILTERING.md @@ -0,0 +1,182 @@ +# 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 + + # Filter by whether the idiom is commonly used in modern German + isModernUsage: 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 + } + } + } +} +``` + +### Filter for Modern Usage with Translations +```graphql +query { + idioms( + locale: "de", + germanFilter: { + isModernUsage: true, + hasLiteralTranslation: true + } + ) { + edges { + node { + id + title + description + literalTranslation + tags + } + } + } +} +``` + +### Advanced Filtering +```graphql +query { + idioms( + locale: "de", + germanFilter: { + regions: ["swiss"], + difficulty: INTERMEDIATE, + tags: ["common", "business"], + hasLiteralTranslation: true, + isModernUsage: 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" + +## Modern Usage Classification +- `isModernUsage: true`: Excludes idioms tagged as "archaic", "obsolete", "historical", "old-fashioned" +- `isModernUsage: false`: Includes only idioms specifically marked with archaic/historical tags \ No newline at end of file diff --git a/lib/server/dataProvider/idiomDataProvider.ts b/lib/server/dataProvider/idiomDataProvider.ts index b23dcfb..15a3c94 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,89 @@ 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: "" } + ] + }); + } + } + + // Filter by modern usage (inferred from tags) + if (germanFilter.isModernUsage !== undefined) { + const modernUsageFilter = this.buildModernUsageFilter(germanFilter.isModernUsage); + if (modernUsageFilter) { + germanFilters.push(modernUsageFilter); + } + } + + // 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 +797,72 @@ 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 } + }; + } + + /** + * Build MongoDB filter for modern usage based on tags + */ + private buildModernUsageFilter(isModernUsage: boolean): FilterQuery | null { + if (isModernUsage) { + // Include idioms that are modern or don't have archaic tags + return { + $and: [ + { tags: { $nin: ["archaic", "obsolete", "historical", "old-fashioned"] } } + ] + }; + } else { + // Include idioms that are specifically marked as archaic/historical + return { + tags: { $in: ["archaic", "obsolete", "historical", "old-fashioned"] } + }; + } + } } \ 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..69451f8 --- /dev/null +++ b/lib/server/model/germanIdiomTypes.ts @@ -0,0 +1,25 @@ +// 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; + isModernUsage?: 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..4275649 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,32 @@ 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 + + # Filter by whether the idiom is commonly used in modern German + isModernUsage: Boolean + } + + enum DifficultyLevel { + BEGINNER + INTERMEDIATE + ADVANCED + } + input IdiomCreateInput { title: String! description: String diff --git a/test-german-filter.js b/test-german-filter.js new file mode 100644 index 0000000..1bba1b4 --- /dev/null +++ b/test-german-filter.js @@ -0,0 +1,162 @@ +/** + * Manual validation test for German idiom filtering functionality (JavaScript version) + */ + +// Mock enum +const DifficultyLevel = { + BEGINNER: 'BEGINNER', + INTERMEDIATE: 'INTERMEDIATE', + ADVANCED: 'ADVANCED' +}; + +// Helper functions extracted from IdiomDataProvider for testing +function buildGermanRegionFilter(regions) { + const regionCountryMap = { + "northern": ["DE"], + "southern": ["DE"], + "bavarian": ["DE"], + "austrian": ["AT"], + "swiss": ["CH"] + }; + + const countryCodes = []; + 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 } + }; +} + +function buildDifficultyFilter(difficulty) { + const difficultyTagMap = { + [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 } + }; +} + +function buildModernUsageFilter(isModernUsage) { + if (isModernUsage) { + return { + $and: [ + { tags: { $nin: ["archaic", "obsolete", "historical", "old-fashioned"] } } + ] + }; + } else { + return { + tags: { $in: ["archaic", "obsolete", "historical", "old-fashioned"] } + }; + } +} + +// Test cases +console.log("Testing German Idiom Filtering Logic...\n"); + +// Test 1: Region filtering +console.log("1. Testing region filtering:"); +const regionFilter = buildGermanRegionFilter(["bavarian", "austrian"]); +console.log(" Input: ['bavarian', 'austrian']"); +console.log(" Output:", JSON.stringify(regionFilter, null, 2)); +console.log(); + +// Test 2: Difficulty filtering +console.log("2. Testing difficulty filtering:"); +const difficultyFilter = buildDifficultyFilter(DifficultyLevel.BEGINNER); +console.log(" Input: DifficultyLevel.BEGINNER"); +console.log(" Output:", JSON.stringify(difficultyFilter, null, 2)); +console.log(); + +// Test 3: Modern usage filtering +console.log("3. Testing modern usage filtering:"); +const modernFilter = buildModernUsageFilter(true); +console.log(" Input: isModernUsage = true"); +console.log(" Output:", JSON.stringify(modernFilter, null, 2)); +console.log(); + +const archaicFilter = buildModernUsageFilter(false); +console.log(" Input: isModernUsage = false"); +console.log(" Output:", JSON.stringify(archaicFilter, null, 2)); +console.log(); + +// Test 4: Complex filter combination +console.log("4. Testing complex filter combination:"); +const germanFilter = { + regions: ["swiss"], + difficulty: DifficultyLevel.INTERMEDIATE, + tags: ["common", "business"], + hasLiteralTranslation: true, + isModernUsage: true +}; + +console.log(" German Filter Input:", JSON.stringify(germanFilter, null, 2)); + +// Simulate the filter building logic +const filters = []; + +if (germanFilter.regions && germanFilter.regions.length > 0) { + const regionFilter = buildGermanRegionFilter(germanFilter.regions); + if (regionFilter) { + filters.push(regionFilter); + } +} + +if (germanFilter.difficulty) { + const difficultyFilter = buildDifficultyFilter(germanFilter.difficulty); + if (difficultyFilter) { + filters.push(difficultyFilter); + } +} + +if (germanFilter.tags && germanFilter.tags.length > 0) { + filters.push({ + tags: { $in: germanFilter.tags } + }); +} + +if (germanFilter.hasLiteralTranslation !== undefined) { + if (germanFilter.hasLiteralTranslation) { + filters.push({ + literalTranslation: { $exists: true, $ne: null, $ne: "" } + }); + } else { + filters.push({ + $or: [ + { literalTranslation: { $exists: false } }, + { literalTranslation: null }, + { literalTranslation: "" } + ] + }); + } +} + +if (germanFilter.isModernUsage !== undefined) { + const modernUsageFilter = buildModernUsageFilter(germanFilter.isModernUsage); + if (modernUsageFilter) { + filters.push(modernUsageFilter); + } +} + +const combinedFilter = filters.length === 1 ? filters[0] : { $and: filters }; +console.log(" Combined MongoDB Filter:", JSON.stringify(combinedFilter, null, 2)); + +console.log("\n✅ All tests completed successfully!"); +console.log("\nThe German idiom filtering logic is working correctly."); +console.log("Use the examples in GERMAN_IDIOM_FILTERING.md to test with real GraphQL queries."); \ No newline at end of file diff --git a/test-german-filter.ts b/test-german-filter.ts new file mode 100644 index 0000000..c5e6cca --- /dev/null +++ b/test-german-filter.ts @@ -0,0 +1,170 @@ +/** + * Manual validation test for German idiom filtering functionality + * This file demonstrates how the new German filtering features work + * Run this with: TS_NODE_PROJECT=server/tsconfig.json ts-node test-german-filter.ts + */ + +import { FilterQuery } from 'mongodb'; +import { GermanIdiomFilter, DifficultyLevel } from './lib/server/model/germanIdiomTypes'; + +// Mock DbIdiom interface for testing +interface DbIdiom { + title: string; + languageKey: string; + countryKeys?: string[]; + tags?: string[]; + literalTranslation?: string; + transliteration?: string; +} + +// Helper functions extracted from IdiomDataProvider for testing +function buildGermanRegionFilter(regions: string[]): FilterQuery | null { + const regionCountryMap: { [key: string]: string[] } = { + "northern": ["DE"], + "southern": ["DE"], + "bavarian": ["DE"], + "austrian": ["AT"], + "swiss": ["CH"] + }; + + 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 } + }; +} + +function 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 } + }; +} + +function buildModernUsageFilter(isModernUsage: boolean): FilterQuery | null { + if (isModernUsage) { + return { + $and: [ + { tags: { $nin: ["archaic", "obsolete", "historical", "old-fashioned"] } } + ] + }; + } else { + return { + tags: { $in: ["archaic", "obsolete", "historical", "old-fashioned"] } + }; + } +} + +// Test cases +console.log("Testing German Idiom Filtering Logic...\n"); + +// Test 1: Region filtering +console.log("1. Testing region filtering:"); +const regionFilter = buildGermanRegionFilter(["bavarian", "austrian"]); +console.log(" Input: ['bavarian', 'austrian']"); +console.log(" Output:", JSON.stringify(regionFilter, null, 2)); +console.log(); + +// Test 2: Difficulty filtering +console.log("2. Testing difficulty filtering:"); +const difficultyFilter = buildDifficultyFilter(DifficultyLevel.BEGINNER); +console.log(" Input: DifficultyLevel.BEGINNER"); +console.log(" Output:", JSON.stringify(difficultyFilter, null, 2)); +console.log(); + +// Test 3: Modern usage filtering +console.log("3. Testing modern usage filtering:"); +const modernFilter = buildModernUsageFilter(true); +console.log(" Input: isModernUsage = true"); +console.log(" Output:", JSON.stringify(modernFilter, null, 2)); +console.log(); + +const archaicFilter = buildModernUsageFilter(false); +console.log(" Input: isModernUsage = false"); +console.log(" Output:", JSON.stringify(archaicFilter, null, 2)); +console.log(); + +// Test 4: Complex filter combination +console.log("4. Testing complex filter combination:"); +const germanFilter: GermanIdiomFilter = { + regions: ["swiss"], + difficulty: DifficultyLevel.INTERMEDIATE, + tags: ["common", "business"], + hasLiteralTranslation: true, + isModernUsage: true +}; + +console.log(" German Filter Input:", JSON.stringify(germanFilter, null, 2)); + +// Simulate the filter building logic +const filters: FilterQuery[] = []; + +if (germanFilter.regions && germanFilter.regions.length > 0) { + const regionFilter = buildGermanRegionFilter(germanFilter.regions); + if (regionFilter) { + filters.push(regionFilter); + } +} + +if (germanFilter.difficulty) { + const difficultyFilter = buildDifficultyFilter(germanFilter.difficulty); + if (difficultyFilter) { + filters.push(difficultyFilter); + } +} + +if (germanFilter.tags && germanFilter.tags.length > 0) { + filters.push({ + tags: { $in: germanFilter.tags } + }); +} + +if (germanFilter.hasLiteralTranslation !== undefined) { + if (germanFilter.hasLiteralTranslation) { + filters.push({ + literalTranslation: { $exists: true, $ne: null, $ne: "" } + }); + } else { + filters.push({ + $or: [ + { literalTranslation: { $exists: false } }, + { literalTranslation: null }, + { literalTranslation: "" } + ] + }); + } +} + +if (germanFilter.isModernUsage !== undefined) { + const modernUsageFilter = buildModernUsageFilter(germanFilter.isModernUsage); + if (modernUsageFilter) { + filters.push(modernUsageFilter); + } +} + +const combinedFilter = filters.length === 1 ? filters[0] : { $and: filters }; +console.log(" Combined MongoDB Filter:", JSON.stringify(combinedFilter, null, 2)); + +console.log("\n✅ All tests completed successfully!"); +console.log("\nThe German idiom filtering logic is working correctly."); +console.log("Use the examples in GERMAN_IDIOM_FILTERING.md to test with real GraphQL queries."); \ No newline at end of file From 883e9d49a06d8b0eed4b43246608f8ef8a40abb9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Jul 2025 23:24:49 +0000 Subject: [PATCH 3/4] Clean up test files and update gitignore Co-authored-by: mmanela <304410+mmanela@users.noreply.github.com> --- .gitignore | 4 + test-german-filter.js | 162 ---------------------------------------- test-german-filter.ts | 170 ------------------------------------------ 3 files changed, 4 insertions(+), 332 deletions(-) delete mode 100644 test-german-filter.js delete mode 100644 test-german-filter.ts 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/test-german-filter.js b/test-german-filter.js deleted file mode 100644 index 1bba1b4..0000000 --- a/test-german-filter.js +++ /dev/null @@ -1,162 +0,0 @@ -/** - * Manual validation test for German idiom filtering functionality (JavaScript version) - */ - -// Mock enum -const DifficultyLevel = { - BEGINNER: 'BEGINNER', - INTERMEDIATE: 'INTERMEDIATE', - ADVANCED: 'ADVANCED' -}; - -// Helper functions extracted from IdiomDataProvider for testing -function buildGermanRegionFilter(regions) { - const regionCountryMap = { - "northern": ["DE"], - "southern": ["DE"], - "bavarian": ["DE"], - "austrian": ["AT"], - "swiss": ["CH"] - }; - - const countryCodes = []; - 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 } - }; -} - -function buildDifficultyFilter(difficulty) { - const difficultyTagMap = { - [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 } - }; -} - -function buildModernUsageFilter(isModernUsage) { - if (isModernUsage) { - return { - $and: [ - { tags: { $nin: ["archaic", "obsolete", "historical", "old-fashioned"] } } - ] - }; - } else { - return { - tags: { $in: ["archaic", "obsolete", "historical", "old-fashioned"] } - }; - } -} - -// Test cases -console.log("Testing German Idiom Filtering Logic...\n"); - -// Test 1: Region filtering -console.log("1. Testing region filtering:"); -const regionFilter = buildGermanRegionFilter(["bavarian", "austrian"]); -console.log(" Input: ['bavarian', 'austrian']"); -console.log(" Output:", JSON.stringify(regionFilter, null, 2)); -console.log(); - -// Test 2: Difficulty filtering -console.log("2. Testing difficulty filtering:"); -const difficultyFilter = buildDifficultyFilter(DifficultyLevel.BEGINNER); -console.log(" Input: DifficultyLevel.BEGINNER"); -console.log(" Output:", JSON.stringify(difficultyFilter, null, 2)); -console.log(); - -// Test 3: Modern usage filtering -console.log("3. Testing modern usage filtering:"); -const modernFilter = buildModernUsageFilter(true); -console.log(" Input: isModernUsage = true"); -console.log(" Output:", JSON.stringify(modernFilter, null, 2)); -console.log(); - -const archaicFilter = buildModernUsageFilter(false); -console.log(" Input: isModernUsage = false"); -console.log(" Output:", JSON.stringify(archaicFilter, null, 2)); -console.log(); - -// Test 4: Complex filter combination -console.log("4. Testing complex filter combination:"); -const germanFilter = { - regions: ["swiss"], - difficulty: DifficultyLevel.INTERMEDIATE, - tags: ["common", "business"], - hasLiteralTranslation: true, - isModernUsage: true -}; - -console.log(" German Filter Input:", JSON.stringify(germanFilter, null, 2)); - -// Simulate the filter building logic -const filters = []; - -if (germanFilter.regions && germanFilter.regions.length > 0) { - const regionFilter = buildGermanRegionFilter(germanFilter.regions); - if (regionFilter) { - filters.push(regionFilter); - } -} - -if (germanFilter.difficulty) { - const difficultyFilter = buildDifficultyFilter(germanFilter.difficulty); - if (difficultyFilter) { - filters.push(difficultyFilter); - } -} - -if (germanFilter.tags && germanFilter.tags.length > 0) { - filters.push({ - tags: { $in: germanFilter.tags } - }); -} - -if (germanFilter.hasLiteralTranslation !== undefined) { - if (germanFilter.hasLiteralTranslation) { - filters.push({ - literalTranslation: { $exists: true, $ne: null, $ne: "" } - }); - } else { - filters.push({ - $or: [ - { literalTranslation: { $exists: false } }, - { literalTranslation: null }, - { literalTranslation: "" } - ] - }); - } -} - -if (germanFilter.isModernUsage !== undefined) { - const modernUsageFilter = buildModernUsageFilter(germanFilter.isModernUsage); - if (modernUsageFilter) { - filters.push(modernUsageFilter); - } -} - -const combinedFilter = filters.length === 1 ? filters[0] : { $and: filters }; -console.log(" Combined MongoDB Filter:", JSON.stringify(combinedFilter, null, 2)); - -console.log("\n✅ All tests completed successfully!"); -console.log("\nThe German idiom filtering logic is working correctly."); -console.log("Use the examples in GERMAN_IDIOM_FILTERING.md to test with real GraphQL queries."); \ No newline at end of file diff --git a/test-german-filter.ts b/test-german-filter.ts deleted file mode 100644 index c5e6cca..0000000 --- a/test-german-filter.ts +++ /dev/null @@ -1,170 +0,0 @@ -/** - * Manual validation test for German idiom filtering functionality - * This file demonstrates how the new German filtering features work - * Run this with: TS_NODE_PROJECT=server/tsconfig.json ts-node test-german-filter.ts - */ - -import { FilterQuery } from 'mongodb'; -import { GermanIdiomFilter, DifficultyLevel } from './lib/server/model/germanIdiomTypes'; - -// Mock DbIdiom interface for testing -interface DbIdiom { - title: string; - languageKey: string; - countryKeys?: string[]; - tags?: string[]; - literalTranslation?: string; - transliteration?: string; -} - -// Helper functions extracted from IdiomDataProvider for testing -function buildGermanRegionFilter(regions: string[]): FilterQuery | null { - const regionCountryMap: { [key: string]: string[] } = { - "northern": ["DE"], - "southern": ["DE"], - "bavarian": ["DE"], - "austrian": ["AT"], - "swiss": ["CH"] - }; - - 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 } - }; -} - -function 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 } - }; -} - -function buildModernUsageFilter(isModernUsage: boolean): FilterQuery | null { - if (isModernUsage) { - return { - $and: [ - { tags: { $nin: ["archaic", "obsolete", "historical", "old-fashioned"] } } - ] - }; - } else { - return { - tags: { $in: ["archaic", "obsolete", "historical", "old-fashioned"] } - }; - } -} - -// Test cases -console.log("Testing German Idiom Filtering Logic...\n"); - -// Test 1: Region filtering -console.log("1. Testing region filtering:"); -const regionFilter = buildGermanRegionFilter(["bavarian", "austrian"]); -console.log(" Input: ['bavarian', 'austrian']"); -console.log(" Output:", JSON.stringify(regionFilter, null, 2)); -console.log(); - -// Test 2: Difficulty filtering -console.log("2. Testing difficulty filtering:"); -const difficultyFilter = buildDifficultyFilter(DifficultyLevel.BEGINNER); -console.log(" Input: DifficultyLevel.BEGINNER"); -console.log(" Output:", JSON.stringify(difficultyFilter, null, 2)); -console.log(); - -// Test 3: Modern usage filtering -console.log("3. Testing modern usage filtering:"); -const modernFilter = buildModernUsageFilter(true); -console.log(" Input: isModernUsage = true"); -console.log(" Output:", JSON.stringify(modernFilter, null, 2)); -console.log(); - -const archaicFilter = buildModernUsageFilter(false); -console.log(" Input: isModernUsage = false"); -console.log(" Output:", JSON.stringify(archaicFilter, null, 2)); -console.log(); - -// Test 4: Complex filter combination -console.log("4. Testing complex filter combination:"); -const germanFilter: GermanIdiomFilter = { - regions: ["swiss"], - difficulty: DifficultyLevel.INTERMEDIATE, - tags: ["common", "business"], - hasLiteralTranslation: true, - isModernUsage: true -}; - -console.log(" German Filter Input:", JSON.stringify(germanFilter, null, 2)); - -// Simulate the filter building logic -const filters: FilterQuery[] = []; - -if (germanFilter.regions && germanFilter.regions.length > 0) { - const regionFilter = buildGermanRegionFilter(germanFilter.regions); - if (regionFilter) { - filters.push(regionFilter); - } -} - -if (germanFilter.difficulty) { - const difficultyFilter = buildDifficultyFilter(germanFilter.difficulty); - if (difficultyFilter) { - filters.push(difficultyFilter); - } -} - -if (germanFilter.tags && germanFilter.tags.length > 0) { - filters.push({ - tags: { $in: germanFilter.tags } - }); -} - -if (germanFilter.hasLiteralTranslation !== undefined) { - if (germanFilter.hasLiteralTranslation) { - filters.push({ - literalTranslation: { $exists: true, $ne: null, $ne: "" } - }); - } else { - filters.push({ - $or: [ - { literalTranslation: { $exists: false } }, - { literalTranslation: null }, - { literalTranslation: "" } - ] - }); - } -} - -if (germanFilter.isModernUsage !== undefined) { - const modernUsageFilter = buildModernUsageFilter(germanFilter.isModernUsage); - if (modernUsageFilter) { - filters.push(modernUsageFilter); - } -} - -const combinedFilter = filters.length === 1 ? filters[0] : { $and: filters }; -console.log(" Combined MongoDB Filter:", JSON.stringify(combinedFilter, null, 2)); - -console.log("\n✅ All tests completed successfully!"); -console.log("\nThe German idiom filtering logic is working correctly."); -console.log("Use the examples in GERMAN_IDIOM_FILTERING.md to test with real GraphQL queries."); \ No newline at end of file From 3bdddbee401bf8a516b992ccb07647f722da481b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Jul 2025 23:36:10 +0000 Subject: [PATCH 4/4] Remove buildModernUsageFilter and isModernUsage functionality Co-authored-by: mmanela <304410+mmanela@users.noreply.github.com> --- GERMAN_IDIOM_FILTERING.md | 34 ++------------------ lib/server/dataProvider/idiomDataProvider.ts | 27 ++-------------- lib/server/model/germanIdiomTypes.ts | 1 - lib/server/schema/idiom.ts | 3 -- 4 files changed, 4 insertions(+), 61 deletions(-) diff --git a/GERMAN_IDIOM_FILTERING.md b/GERMAN_IDIOM_FILTERING.md index 7bc74d6..46e2810 100644 --- a/GERMAN_IDIOM_FILTERING.md +++ b/GERMAN_IDIOM_FILTERING.md @@ -35,9 +35,6 @@ input GermanIdiomFilter { # Filter idioms that have phonetic transliterations hasTransliteration: Boolean - - # Filter by whether the idiom is commonly used in modern German - isModernUsage: Boolean } enum DifficultyLevel { @@ -110,28 +107,6 @@ query { } ``` -### Filter for Modern Usage with Translations -```graphql -query { - idioms( - locale: "de", - germanFilter: { - isModernUsage: true, - hasLiteralTranslation: true - } - ) { - edges { - node { - id - title - description - literalTranslation - tags - } - } - } -} -``` ### Advanced Filtering ```graphql @@ -142,8 +117,7 @@ query { regions: ["swiss"], difficulty: INTERMEDIATE, tags: ["common", "business"], - hasLiteralTranslation: true, - isModernUsage: true + hasLiteralTranslation: true } ) { edges { @@ -175,8 +149,4 @@ query { ## 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" - -## Modern Usage Classification -- `isModernUsage: true`: Excludes idioms tagged as "archaic", "obsolete", "historical", "old-fashioned" -- `isModernUsage: false`: Includes only idioms specifically marked with archaic/historical tags \ No newline at end of file +- `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 15a3c94..82323a5 100644 --- a/lib/server/dataProvider/idiomDataProvider.ts +++ b/lib/server/dataProvider/idiomDataProvider.ts @@ -559,13 +559,7 @@ export class IdiomDataProvider { } } - // Filter by modern usage (inferred from tags) - if (germanFilter.isModernUsage !== undefined) { - const modernUsageFilter = this.buildModernUsageFilter(germanFilter.isModernUsage); - if (modernUsageFilter) { - germanFilters.push(modernUsageFilter); - } - } + // Combine German filters if (germanFilters.length > 0) { @@ -847,22 +841,5 @@ export class IdiomDataProvider { }; } - /** - * Build MongoDB filter for modern usage based on tags - */ - private buildModernUsageFilter(isModernUsage: boolean): FilterQuery | null { - if (isModernUsage) { - // Include idioms that are modern or don't have archaic tags - return { - $and: [ - { tags: { $nin: ["archaic", "obsolete", "historical", "old-fashioned"] } } - ] - }; - } else { - // Include idioms that are specifically marked as archaic/historical - return { - tags: { $in: ["archaic", "obsolete", "historical", "old-fashioned"] } - }; - } - } + } \ No newline at end of file diff --git a/lib/server/model/germanIdiomTypes.ts b/lib/server/model/germanIdiomTypes.ts index 69451f8..628e346 100644 --- a/lib/server/model/germanIdiomTypes.ts +++ b/lib/server/model/germanIdiomTypes.ts @@ -7,7 +7,6 @@ export interface GermanIdiomFilter { tags?: string[]; hasLiteralTranslation?: boolean; hasTransliteration?: boolean; - isModernUsage?: boolean; } export enum DifficultyLevel { diff --git a/lib/server/schema/idiom.ts b/lib/server/schema/idiom.ts index 4275649..225a0f7 100644 --- a/lib/server/schema/idiom.ts +++ b/lib/server/schema/idiom.ts @@ -70,9 +70,6 @@ export default gql` # Filter idioms that have phonetic transliterations hasTransliteration: Boolean - - # Filter by whether the idiom is commonly used in modern German - isModernUsage: Boolean } enum DifficultyLevel {