Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
152 changes: 152 additions & 0 deletions GERMAN_IDIOM_FILTERING.md
Original file line number Diff line number Diff line change
@@ -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"
132 changes: 131 additions & 1 deletion lib/server/dataProvider/idiomDataProvider.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -477,10 +478,11 @@ export class IdiomDataProvider {
});
}

async queryIdioms(args: QueryIdiomsArgs, idiomExpandOptions: IdiomExpandOptions): Promise<Paged<Idiom>> {
async queryIdioms(args: QueryIdiomsArgsWithGermanFilter, idiomExpandOptions: IdiomExpandOptions): Promise<Paged<Idiom>> {
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;
Expand All @@ -496,6 +498,83 @@ export class IdiomDataProvider {
findFilter = { languageKey: { $eq: locale } };
}

// Apply German-specific filtering
if (germanFilter && locale === "de") {
const germanFilters: FilterQuery<DbIdiom>[] = [];

// 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' };
Expand Down Expand Up @@ -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<DbIdiom> | 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<DbIdiom> | 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 }
};
}


}
24 changes: 24 additions & 0 deletions lib/server/model/germanIdiomTypes.ts
Original file line number Diff line number Diff line change
@@ -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;
}
10 changes: 9 additions & 1 deletion lib/server/resolvers/idiomResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand Down
Loading