Skip to content
Open
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
17 changes: 17 additions & 0 deletions _api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export { gql } from "apollo-server-micro";
// [Modules]
import modulesGuides from "_api/modules/guides";
import modulesCards from "@api/modules/cards";
// [Context]
import { createContext, GraphQLContext } from "@api/infra/graphql/context";
import { SiteLocale } from "@api/gql_types";

const customScalars = [UUIDDefinition];

Expand Down Expand Up @@ -67,6 +70,20 @@ const serverSchema = {
// 5 minutes (in milliseconds)
ttl: 300_000,
}),
context: ({ req }): GraphQLContext => {
// Extrair locale do header ou usar default
const localeHeader = req?.headers?.["x-locale"] as string | undefined;
let locale = SiteLocale.PtBr;

if (localeHeader) {
const normalizedLocale = localeHeader.toUpperCase().replace("-", "_");
if (normalizedLocale === "EN_US") locale = SiteLocale.EnUs;
else if (normalizedLocale === "ES") locale = SiteLocale.Es;
else if (normalizedLocale === "PT_BR") locale = SiteLocale.PtBr;
}

return createContext(locale);
},
};

export const apolloServer = new ApolloServer(serverSchema);
118 changes: 118 additions & 0 deletions _api/infra/dataloaders/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import DataLoader from "dataloader";
import _ from "lodash";
import { guidesRepository } from "@api/modules/guides/repository";
import { cardsRepository } from "@api/modules/cards/repository";
import {
Card,
Guide,
GuideExpertise,
GuideCollaboration,
SiteLocale,
} from "@api/gql_types";
import { gqlInput } from "@api/infra/graphql/gqlInput";

export interface DataLoaders {
guidesLoader: DataLoader<SiteLocale, Guide[]>;
cardsBySlugLoader: DataLoader<string, Card | undefined>;
expertisesByCardSlugLoader: DataLoader<string, GuideExpertise[]>;
collaborationsByCardSlugLoader: DataLoader<string, GuideCollaboration[]>;
}

export function createDataLoaders(locale: SiteLocale): DataLoaders {
// Cache de guides por locale (usado internamente pelos outros loaders)
let cachedGuides: Guide[] | null = null;

const getGuides = async (): Promise<Guide[]> => {
if (cachedGuides) return cachedGuides;
cachedGuides = await guidesRepository().getAll({
input: gqlInput({ locale }),
});
return cachedGuides;
};

// Loader para carregar todos os guides de uma vez por locale
const guidesLoader = new DataLoader<SiteLocale, Guide[]>(
async (locales) => {
// Batch load guides - na prática teremos apenas 1 locale por request
const results = await Promise.all(
locales.map(async (loc) => {
return guidesRepository().getAll({
input: gqlInput({ locale: loc }),
});
})
);
return results;
},
{ cache: true }
);

// Loader para carregar cards por slug
const cardsBySlugLoader = new DataLoader<string, Card | undefined>(
async (slugs) => {
// Batch load: carregar todos os cards de uma vez
const allCards = await cardsRepository().getAll({
input: gqlInput({ locale }),
});

// Criar mapa de slug -> card para lookup O(1)
const cardMap = new Map<string, Card>();
allCards.forEach((card) => {
if (card.slug) {
cardMap.set(card.slug, card);
}
});

// Retornar na ordem dos slugs solicitados
return slugs.map((slug) => cardMap.get(slug));
},
{ cache: true }
);

// Loader para expertises filtradas por card slug
const expertisesByCardSlugLoader = new DataLoader<string, GuideExpertise[]>(
async (cardSlugs) => {
const guides = await getGuides();

// Extrair todas as expertises de todos os guides
const allExpertises = guides.flatMap((guide) => guide.expertises || []);

// Para cada slug, filtrar expertises que contêm esse card
return cardSlugs.map((slug) => {
return _.filter(allExpertises, (expertise) => {
return _.some(expertise?.cards, { item: { slug } });
}) as GuideExpertise[];
});
},
{ cache: true }
);

// Loader para collaborations filtradas por card slug
const collaborationsByCardSlugLoader = new DataLoader<
string,
GuideCollaboration[]
>(
async (cardSlugs) => {
const guides = await getGuides();

// Extrair todas as collaborations de todos os guides
const allCollaborations = guides.flatMap(
(guide) => guide.collaborations || []
);

// Para cada slug, filtrar collaborations que contêm esse card
return cardSlugs.map((slug) => {
return _.filter(allCollaborations, (collaboration) => {
return _.some(collaboration?.cards, { item: { slug } });
}) as GuideCollaboration[];
});
},
{ cache: true }
);

return {
guidesLoader,
cardsBySlugLoader,
expertisesByCardSlugLoader,
collaborationsByCardSlugLoader,
};
}
16 changes: 16 additions & 0 deletions _api/infra/graphql/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { SiteLocale } from "@api/gql_types";
import { createDataLoaders, DataLoaders } from "@api/infra/dataloaders";

export interface GraphQLContext {
loaders: DataLoaders;
locale: SiteLocale;
}

export function createContext(
locale: SiteLocale = SiteLocale.PtBr
): GraphQLContext {
return {
loaders: createDataLoaders(locale),
locale,
};
}
48 changes: 43 additions & 5 deletions _api/infra/storage/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,48 @@
const db = new Map();
interface CacheEntry<T> {
value: T;
expiresAt: number;
}

const DEFAULT_TTL_MS = 5 * 60 * 1000; // 5 minutes

const db = new Map<string, CacheEntry<unknown>>();

export const storage = {
async get<T>(key: string): Promise<T> {
return db.get(key);
async get<T>(key: string): Promise<T | undefined> {
const entry = db.get(key) as CacheEntry<T> | undefined;
if (!entry) return undefined;

if (Date.now() > entry.expiresAt) {
db.delete(key);
return undefined;
}

return entry.value;
},
async set<T>(key: string, value: T): Promise<void> {
db.set(key, value);

async set<T>(
key: string,
value: T,
ttlMs: number = DEFAULT_TTL_MS
): Promise<void> {
db.set(key, {
value,
expiresAt: Date.now() + ttlMs,
});
},

cleanup(): void {
const now = Date.now();
const keysToDelete: string[] = [];
db.forEach((entry, key) => {
if (now > entry.expiresAt) {
keysToDelete.push(key);
}
});
keysToDelete.forEach((key) => db.delete(key));
},

clear(): void {
db.clear();
},
};
33 changes: 8 additions & 25 deletions _api/modules/cards/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { gql } from "apollo-server-micro";
import { Resolvers, SiteLocale } from "@api/gql_types";
import { cardsRepository } from "@api/modules/cards/repository";
import { gqlInput } from "@api/infra/graphql/gqlInput";
import { guidesRepository } from "../guides/repository";
import _ from "lodash";
import { GraphQLContext } from "@api/infra/graphql/context";

const typeDefs = gql`
# Types
Expand Down Expand Up @@ -72,31 +71,15 @@ const typeDefs = gql`
}
`;

const resolvers: Resolvers = {
const resolvers: Resolvers<GraphQLContext> = {
Card: {
async expertises(parent) {
const guides = await guidesRepository().getAll({ input: {} });
const expertises = guides
.map((guide) => {
return guide.expertises;
})
.flatMap((expertise) => expertise);

return _.filter(expertises, (expertise) => {
return _.some(expertise.cards, { item: { slug: parent.slug } });
});
async expertises(parent, _args, context) {
if (!parent.slug) return [];
return context.loaders.expertisesByCardSlugLoader.load(parent.slug);
},
async collaborations(parent) {
const guides = await guidesRepository().getAll({ input: {} });
const collaborations = guides
.map((guide) => {
return guide.collaborations;
})
.flatMap((collaboration) => collaboration);

return _.filter(collaborations, (collaboration) => {
return _.some(collaboration.cards, { item: { slug: parent.slug } });
});
async collaborations(parent, _args, context) {
if (!parent.slug) return [];
return context.loaders.collaborationsByCardSlugLoader.load(parent.slug);
},
},
Query: {
Expand Down
22 changes: 9 additions & 13 deletions _api/modules/guides/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { gql } from "apollo-server-micro";
import { Resolvers } from "@api/gql_types";
import { guidesRepository } from "@api/modules/guides/repository";
import { gqlInput } from "@api/infra/graphql/gqlInput";
import { cardsRepository } from "../cards/repository";
import { GraphQLContext } from "@api/infra/graphql/context";

const typeDefs = gql`
type GuideCard {
Expand Down Expand Up @@ -61,7 +61,7 @@ const typeDefs = gql`
}
`;

const resolvers: Resolvers = {
const resolvers: Resolvers<GraphQLContext> = {
Query: {
async guides(_, { input, locale }) {
const guides = await guidesRepository().getAll({
Expand All @@ -84,29 +84,25 @@ const resolvers: Resolvers = {
},
},
GuideExpertise: {
async guide(parent) {
const guides = await guidesRepository().getAll({ input: {} });
async guide(parent, _args, context) {
const guides = await context.loaders.guidesLoader.load(context.locale);
return _.find(guides, (guide) => {
return _.some(guide.expertises, { name: parent.name });
});
},
},
GuideCollaboration: {
async guide(parent) {
const guides = await guidesRepository().getAll({ input: {} });
async guide(parent, _args, context) {
const guides = await context.loaders.guidesLoader.load(context.locale);
return _.find(guides, (guide) => {
return _.some(guide.collaborations, { name: parent.name });
});
},
},
GuideCard: {
async item(parent, _, __, args) {
return cardsRepository().getBySlug({
input: gqlInput({
slug: parent.item.slug,
locale: args.variableValues.locale,
}),
});
async item(parent, _args, context) {
if (!parent.item?.slug) return null;
return context.loaders.cardsBySlugLoader.load(parent.item.slug);
},
},
Mutation: {},
Expand Down
Loading