Skip to content
Open
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
151 changes: 136 additions & 15 deletions src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,12 @@ builder.objectType(LanguageRef, {
rtl: t.boolean({
resolve: (language) => language.rtl === 1,
}),
countries: t.field({
type: [CountryRef],
resolve: (language) => Object.entries(countries)
.filter(([, country]) => Array.isArray(country.languages) && country.languages.includes(language.code))
.map(([code, country]) => ({...country, code}))
}),
}),
});

Expand Down Expand Up @@ -205,18 +211,24 @@ const CountryFilterInput = builder.inputType("CountryFilterInput", {
name: t.field({ type: StringQueryOperatorInput }),
currency: t.field({ type: StringQueryOperatorInput }),
continent: t.field({ type: StringQueryOperatorInput }),
language: t.field({ type: StringQueryOperatorInput }),
}),
});

const ContinentFilterInput = builder.inputType("ContinentFilterInput", {
fields: (t) => ({
code: t.field({ type: StringQueryOperatorInput }),
// A continent matches if it has at least one country satisfying this filter
country: t.field({ type: CountryFilterInput }),
// A continent matches if it has at least one language (spoken within it) satisfying this filter
language: t.field({ type: LanguageFilterInput }),
}),
});

const LanguageFilterInput = builder.inputType("LanguageFilterInput", {
fields: (t) => ({
code: t.field({ type: StringQueryOperatorInput }),
country: t.field({ type: StringQueryOperatorInput }),
}),
});

Expand All @@ -240,6 +252,87 @@ const isValidLanguageCode = (
code: string | number
): code is keyof typeof languages => code in languages;



export type StringOps = {
eq?: string;
ne?: string;
in?: string[];
nin?: string[];
regex?: string;
};

// true if the array `values` satisfies the StringOps (ANY-match semantics)
const anyMatches = (values: string[], ops?: StringOps) => {
if (!ops) return true;

const has = (v: string) => values.includes(v);

if (ops.eq !== undefined && !has(ops.eq)) return false;
if (ops.in && !ops.in.some(has)) return false;

if (ops.regex) {
const re = new RegExp(ops.regex);
if (!values.some((v) => re.test(v))) return false;
}

if (ops.ne !== undefined && has(ops.ne)) return false;
if (ops.nin && ops.nin.some(has)) return false;

return true;
};

// Given a language code, return a list of country codes where it's spoken
const countryCodesForLanguage = (langCode: string) =>
Object.entries(countries)
.filter(([, c]) => Array.isArray(c.languages) && c.languages.includes(langCode))
.map(([code]) => code);

// Countries within a continent (as Country objects with `code`)
const countriesInContinent = (continentCode: string) =>
Object.entries(countries)
.filter(([, c]) => c.continent === continentCode)
.map(([code, c]) => ({ ...c, code }));

// Unique language codes used within a continent
const languageCodesInContinent = (continentCode: string) => {
const set = new Set<string>();
countriesInContinent(continentCode).forEach((c) => {
if (Array.isArray(c.languages)) c.languages.forEach((lc) => set.add(lc));
});
return Array.from(set);
};

// Language objects used within a continent (with `code`)
const languagesInContinent = (continentCode: string) =>
languageCodesInContinent(continentCode).map((code) => ({
...(languages[code as keyof typeof languages] as Language),
code,
}));

// Build a predicate to test a Country against CountryFilterInput (incl. language ops)
const makeCountryPredicate = (
filter?: { language?: StringOps } & Record<string, unknown>
) => {
if (!filter || Object.keys(filter).length === 0) return () => true;
const { language, ...rest } = filter;
const base = sift(JSON.parse(JSON.stringify(rest)), { operations });
return (c: (Country & { code: string })) =>
base(c) && anyMatches(Array.isArray(c.languages) ? c.languages : [], language);
};

// Build a predicate to test a Language against LanguageFilterInput (incl. country ops)
const makeLanguagePredicate = (
filter?: { country?: StringOps } & Record<string, unknown>
) => {
if (!filter || Object.keys(filter).length === 0) return () => true;
const { country, ...rest } = filter;
const base = sift(JSON.parse(JSON.stringify(rest)), { operations });
return (l: (Language & { code: string })) =>
base(l) && anyMatches(countryCodesForLanguage(l.code), country);
};


builder.queryType({
fields: (t) => ({
continents: t.field({
Expand All @@ -250,12 +343,37 @@ builder.queryType({
defaultValue: {},
}),
},
resolve: (_, { filter }) =>
Object.entries(continents)
resolve: (_, { filter }) => {
const { country: countryFilter, language: languageFilter, ...rest } =
(filter ?? {}) as {
country?: { language?: StringOps } & Record<string, unknown>;
language?: { country?: StringOps } & Record<string, unknown>;
};

// base continent-level filter (currently only `code`)
const base = sift(JSON.parse(JSON.stringify(rest)), { operations });

const countryPred = makeCountryPredicate(countryFilter);
const languagePred = makeLanguagePredicate(languageFilter);
return Object.entries(continents)
.map(([code, name]) => new Continent(code, name))
// need to parse and stringify because of some null prototype
// see https://stackoverflow.com/q/53983315/8190832
.filter(sift(JSON.parse(JSON.stringify(filter)), { operations })),
.filter((cont) => base(cont))
.filter((cont) => {
// If country filter provided, continent must have at least one matching country
const okByCountry = countryFilter
? countriesInContinent(cont.code).some(countryPred)
: true;

// If language filter provided, continent must have at least one matching language
const okByLanguage = languageFilter
? languagesInContinent(cont.code).some(languagePred)
: true;

return okByCountry && okByLanguage;
});
}
}),
continent: t.field({
type: Continent,
Expand All @@ -277,12 +395,13 @@ builder.queryType({
}),
},
resolve: (_, { filter }) => {
const { language, ...rest } = (filter ?? {}) as { language?: StringOps };
const base = sift(JSON.parse(JSON.stringify(rest)), { operations });

return Object.entries(countries)
.map(([code, country]) => ({
...country,
code,
}))
.filter(sift(JSON.parse(JSON.stringify(filter)), { operations }));
.map(([code, country]) => ({ ...country, code }))
.filter((c) => base(c))
.filter((c) => anyMatches(Array.isArray(c.languages) ? c.languages : [], language));
},
}),
country: t.field({
Expand All @@ -307,13 +426,15 @@ builder.queryType({
defaultValue: {},
}),
},
resolve: (_, { filter }) =>
Object.entries(languages)
.map(([code, language]) => ({
...language,
code,
}))
.filter(sift(JSON.parse(JSON.stringify(filter)), { operations })),
resolve: (_, { filter }) => {
const { country, ...rest } = (filter ?? {}) as { country?: StringOps };
const base = sift(JSON.parse(JSON.stringify(rest)), { operations });

return Object.entries(languages)
.map(([code, language]) => ({ ...language, code }))
.filter((l) => base(l))
.filter((l) => anyMatches(countryCodesForLanguage(l.code), country));
},
}),
language: t.field({
type: LanguageRef,
Expand Down