Skip to content

Commit e7665ce

Browse files
Merge pull request #60 from Ditectrev/feature/AzureCosmosDB
feat: Azure CosmosDB as a fallback from repos
2 parents e13e84e + 79ee998 commit e7665ce

File tree

11 files changed

+263
-52
lines changed

11 files changed

+263
-52
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
AZURE_COSMOSDB_DATABASE=
2+
AZURE_COSMOSDB_ENDPOINT=
3+
AZURE_COSMOSDB_KEY=

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,12 @@ next-env.d.ts
4646
**/public/sw.js
4747
**/public/worker-*.js
4848
**/public/sw.js.map
49+
50+
bundles
51+
.env
52+
53+
# service worker
54+
sw.js
55+
sw.js.map
56+
workbox-*.js
57+
workbox-*.js.map

app/api/graphql/route.ts

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
// import { CosmosContainer } from "@azure-fundamentals/src/graphql/cosmos-client";
2-
// import { QuestionsDataSource, LocalQuestionsDataSource } from "@azure-fundamentals/src/graphql/questionsDataSource";
1+
import {
2+
CombinedQuestionsDataSource,
3+
RepoQuestionsDataSource,
4+
} from "@azure-fundamentals/lib/graphql/questionsDataSource";
35
import { ApolloServer, BaseContext } from "@apollo/server";
46
import { startServerAndCreateNextHandler } from "@as-integrations/next";
57
import typeDefs from "@azure-fundamentals/lib/graphql/schemas";
68
import resolvers from "@azure-fundamentals/lib/graphql/resolvers";
7-
//import { RepoQuestionsDataSource } from "@azure-fundamentals/lib/graphql/questionsDataSource";
8-
//import { FetchQuestions } from "@azure-fundamentals/lib/graphql/repoQuestions";
9+
import { fetchQuestions } from "@azure-fundamentals/lib/graphql/repoQuestions";
910

1011
interface ContextValue {
1112
dataSources: {
@@ -19,22 +20,57 @@ const server = new ApolloServer<ContextValue>({
1920
introspection: process.env.NODE_ENV !== "production",
2021
});
2122

22-
//const questions = await FetchQuestions();
23-
24-
const handler = startServerAndCreateNextHandler(
25-
server,
26-
/*{
23+
const handler = startServerAndCreateNextHandler(server, {
2724
context: async () => {
28-
return {
29-
dataSources: {
30-
// questionsDB: process.env.AZURE_COSMOSDB_ENDPOINT
31-
// ? QuestionsDataSource(CosmosContainer())
32-
// : LocalQuestionsDataSource(questions),
33-
questionsDB: RepoQuestionsDataSource(questions),
34-
},
35-
};
25+
if (process.env.AZURE_COSMOSDB_ENDPOINT) {
26+
return {
27+
dataSources: {
28+
questionsDB: CombinedQuestionsDataSource(),
29+
},
30+
};
31+
} else {
32+
// Fallback to GitHub-only data source
33+
return {
34+
dataSources: {
35+
questionsDB: {
36+
getQuestion: async (id: string, link: string) => {
37+
const questions = await fetchQuestions(link);
38+
return questions?.find((q: any) => q.id === id);
39+
},
40+
getQuestions: async (link: string) => {
41+
const questions = await fetchQuestions(link);
42+
return { count: questions?.length || 0 };
43+
},
44+
getRandomQuestions: async (range: number, link: string) => {
45+
const questions = await fetchQuestions(link);
46+
const shuffled = questions?.sort(() => 0.5 - Math.random());
47+
return shuffled?.slice(0, range) || [];
48+
},
49+
},
50+
},
51+
};
52+
}
3653
},
37-
}*/
38-
);
54+
});
55+
56+
// Wrap the handler to handle errors
57+
const wrappedHandler = async (req: Request) => {
58+
try {
59+
return await handler(req);
60+
} catch (error) {
61+
console.error("GraphQL Error:", error);
62+
return new Response(
63+
JSON.stringify({
64+
errors: [{ message: "Internal server error" }],
65+
}),
66+
{
67+
status: 500,
68+
headers: {
69+
"Content-Type": "application/json",
70+
},
71+
},
72+
);
73+
}
74+
};
3975

40-
export { handler as GET, handler as POST };
76+
export { wrappedHandler as GET, wrappedHandler as POST };

app/layout.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import { type ReactNode } from "react";
2-
import { type Metadata } from "next";
2+
import { type Metadata, type Viewport } from "next";
33
import TopNav from "@azure-fundamentals/components/TopNav";
44
import Footer from "@azure-fundamentals/components/Footer";
55
import ApolloProvider from "@azure-fundamentals/components/ApolloProvider";
66
import Cookie from "@azure-fundamentals/components/Cookie";
77
import "styles/globals.css";
88

9+
export const viewport: Viewport = {
10+
themeColor: "#3f51b5",
11+
width: "device-width",
12+
initialScale: 1,
13+
};
14+
915
export const metadata: Metadata = {
1016
appleWebApp: {
1117
capable: true,
@@ -71,7 +77,6 @@ export const metadata: Metadata = {
7177
follow: true,
7278
index: true,
7379
},
74-
themeColor: "#3f51b5",
7580
title: {
7681
default: "🧪 Practice Exams Platform | Ditectrev",
7782
template: "🧪 Practice Exams Platform | Ditectrev",
@@ -90,10 +95,6 @@ export const metadata: Metadata = {
9095
site: "@ditectrev",
9196
title: "🧪 Practice Exams Platform | Ditectrev",
9297
},
93-
viewport: {
94-
initialScale: 1,
95-
width: "device-width",
96-
},
9798
};
9899

99100
type RootLayoutProps = {

lib/graphql/cosmos-client.tsx

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,36 @@
11
import { CosmosClient } from "@azure/cosmos";
22

3-
export const CosmosContainer = () => {
3+
export const getDatabase = () => {
44
const client = new CosmosClient({
55
endpoint: process.env.AZURE_COSMOSDB_ENDPOINT!,
66
key: process.env.AZURE_COSMOSDB_KEY!,
77
});
88

9-
const container = client
10-
.database(process.env.AZURE_COSMOSDB_DATABASE!)
11-
.container(process.env.AZURE_COSMOSDB_CONTAINER!);
9+
return client.database(process.env.AZURE_COSMOSDB_DATABASE!);
10+
};
11+
12+
export const getQuestionsContainer = async () => {
13+
const database = getDatabase();
1214

13-
return container;
15+
// Try to create container if it doesn't exist
16+
try {
17+
const { container } = await database.containers.createIfNotExists({
18+
id: "questions",
19+
partitionKey: {
20+
paths: ["/examId"],
21+
},
22+
});
23+
return container;
24+
} catch (error: any) {
25+
// If container creation fails, try to get the existing container
26+
if (error.code === 409) {
27+
console.log(
28+
`Container questions already exists, using existing container`,
29+
);
30+
return database.container("questions");
31+
} else {
32+
console.error("Error creating container:", error);
33+
throw error;
34+
}
35+
}
1436
};

lib/graphql/questionsDataSource.tsx

Lines changed: 151 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
/*import { Container } from "@azure/cosmos";
1+
import { Container } from "@azure/cosmos";
2+
import { fetchQuestions } from "./repoQuestions";
3+
import { getQuestionsContainer } from "./cosmos-client";
24

35
export const QuestionsDataSource = (container: Container) => {
46
return {
@@ -38,7 +40,7 @@ export const QuestionsDataSource = (container: Container) => {
3840
},
3941
};
4042
};
41-
*/
43+
4244
export const RepoQuestionsDataSource = (container: any) => {
4345
return {
4446
async getQuestion(id: string) {
@@ -65,3 +67,150 @@ export const RepoQuestionsDataSource = (container: any) => {
6567
},
6668
};
6769
};
70+
71+
// Helper function to extract exam ID from URL
72+
const extractExamId = (link: string): string => {
73+
const segments = link.split("/");
74+
return segments[segments.length - 3].replace(/-/g, "_").toLowerCase();
75+
};
76+
77+
export const CombinedQuestionsDataSource = () => {
78+
return {
79+
async getQuestion(id: string, link: string) {
80+
try {
81+
const examId = extractExamId(link);
82+
const questionsContainer = await getQuestionsContainer();
83+
84+
// Try Cosmos DB first (most efficient)
85+
const querySpec = {
86+
query: "SELECT * FROM c WHERE c.id = @id AND c.examId = @examId",
87+
parameters: [
88+
{ name: "@id", value: id },
89+
{ name: "@examId", value: examId },
90+
],
91+
};
92+
const { resources: items } = await questionsContainer.items
93+
.query(querySpec)
94+
.fetchAll();
95+
96+
if (items.length > 0) {
97+
return items[0];
98+
}
99+
100+
// Fallback to GitHub if not found in database
101+
const questions = await fetchQuestions(link);
102+
if (questions) {
103+
const question = questions.find((q: any) => q.id === id);
104+
if (question) {
105+
// Add examId to the question document and upload to database
106+
const questionWithExamId = {
107+
...question,
108+
examId: examId,
109+
};
110+
111+
try {
112+
await questionsContainer.items.upsert(questionWithExamId);
113+
} catch (err) {
114+
console.warn("Failed to upload question to Cosmos DB:", err);
115+
}
116+
return question;
117+
}
118+
}
119+
120+
return null;
121+
} catch (err) {
122+
throw new Error("Error fetching question: " + err);
123+
}
124+
},
125+
126+
async getQuestions(link: string) {
127+
try {
128+
const examId = extractExamId(link);
129+
const questionsContainer = await getQuestionsContainer();
130+
131+
// Try Cosmos DB first
132+
const querySpec = {
133+
query: "SELECT VALUE COUNT(c.id) FROM c WHERE c.examId = @examId",
134+
parameters: [{ name: "@examId", value: examId }],
135+
};
136+
const { resources: items } = await questionsContainer.items
137+
.query(querySpec)
138+
.fetchAll();
139+
140+
if (items[0] > 0) {
141+
return { count: items[0] };
142+
}
143+
144+
// Fallback to GitHub if no questions found in database
145+
const questions = await fetchQuestions(link);
146+
if (questions) {
147+
// Upload all questions to database (only if they don't exist)
148+
try {
149+
for (const question of questions) {
150+
const questionWithExamId = {
151+
...question,
152+
examId: examId,
153+
};
154+
await questionsContainer.items.upsert(questionWithExamId);
155+
}
156+
} catch (err) {
157+
console.warn("Failed to upload questions to Cosmos DB:", err);
158+
}
159+
return { count: questions.length };
160+
}
161+
162+
return { count: 0 };
163+
} catch (err) {
164+
throw new Error("Error fetching questions: " + err);
165+
}
166+
},
167+
168+
async getRandomQuestions(range: number, link: string) {
169+
try {
170+
const examId = extractExamId(link);
171+
const questionsContainer = await getQuestionsContainer();
172+
173+
// Try Cosmos DB first
174+
const querySpec = {
175+
query: "SELECT * FROM c WHERE c.examId = @examId",
176+
parameters: [{ name: "@examId", value: examId }],
177+
};
178+
const { resources: items } = await questionsContainer.items
179+
.query(querySpec)
180+
.fetchAll();
181+
182+
if (items.length > 0) {
183+
// Questions exist in database, return random selection
184+
const shuffled = [...items].sort(() => 0.5 - Math.random());
185+
return shuffled.slice(0, range);
186+
}
187+
188+
// Fallback to GitHub if no questions found in database
189+
const questions = await fetchQuestions(link);
190+
if (questions) {
191+
const shuffled = [...questions].sort(() => 0.5 - Math.random());
192+
const selected = shuffled.slice(0, range);
193+
194+
// Upload selected questions to database (only if they don't exist)
195+
try {
196+
for (const question of selected) {
197+
const questionWithExamId = {
198+
...question,
199+
examId: examId,
200+
};
201+
await questionsContainer.items.upsert(questionWithExamId);
202+
}
203+
} catch (err) {
204+
console.warn("Failed to upload questions to Cosmos DB:", err);
205+
}
206+
207+
return selected;
208+
}
209+
210+
return [];
211+
} catch (err) {
212+
throw new Error("Error fetching random questions: " + err);
213+
}
214+
},
215+
};
216+
};

lib/graphql/resolvers.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,21 @@ const resolvers = {
77
{ link }: { link: string },
88
{ dataSources }: { dataSources: any },
99
) => {
10-
const response = await fetchQuestions(link);
11-
return { count: response?.length };
10+
return dataSources.questionsDB.getQuestions(link);
1211
},
1312
questionById: async (
1413
_: unknown,
1514
{ id, link }: { id: string; link: string },
1615
{ dataSources }: { dataSources: any },
1716
) => {
18-
const response = await fetchQuestions(link);
19-
return response?.filter((el: any) => el.id === id)[0];
17+
return dataSources.questionsDB.getQuestion(id, link);
2018
},
2119
randomQuestions: async (
2220
_: unknown,
2321
{ range, link }: { range: number; link: string },
2422
{ dataSources }: { dataSources: any },
2523
) => {
26-
const response = await fetchQuestions(link);
27-
const shuffled = response?.sort(() => 0.5 - Math.random());
28-
return shuffled?.slice(0, range);
24+
return dataSources.questionsDB.getRandomQuestions(range, link);
2925
},
3026
},
3127
};

lib/graphql/schemas.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const typeDefs = gql`
1414
question: String
1515
options: [Option]
1616
images: [Images]
17+
examId: String
1718
}
1819
type Questions {
1920
count: Int

0 commit comments

Comments
 (0)