diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..9102568 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,11 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "bradlc.vscode-tailwindcss", + "Prisma.prisma", + "GraphQL.vscode-graphql", + "ms-playwright.playwright", + "vitest.explorer" + ] +} \ No newline at end of file diff --git a/client/src/lib/apolloClient.ts b/client/src/lib/apolloClient.ts index f4dbc89..860cdd4 100644 --- a/client/src/lib/apolloClient.ts +++ b/client/src/lib/apolloClient.ts @@ -1,5 +1,6 @@ import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client'; + // adressen til graphql backend const httpLink = new HttpLink({ uri: import.meta.env.VITE_GRAPHQL_URL, diff --git a/server/.env.example b/server/.env.example index e69de29..6a0f1a1 100644 --- a/server/.env.example +++ b/server/.env.example @@ -0,0 +1,2 @@ +DATABASE_URL= +PORT= \ No newline at end of file diff --git a/server/package.json b/server/package.json index 4206cb3..3085a63 100644 --- a/server/package.json +++ b/server/package.json @@ -13,7 +13,9 @@ "author": "", "license": "ISC", "type": "commonjs", - "prisma": { "seed": "tsx prisma/seed.ts" }, + "prisma": { + "seed": "tsx prisma/seed.ts" + }, "devDependencies": { "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.19", @@ -29,6 +31,11 @@ "vitest": "^5.0.2" }, "dependencies": { - "@prisma/client": "^6.19.3" + "@apollo/server": "^5.5.1", + "@as-integrations/express5": "^1.1.2", + "@prisma/client": "^6.19.3", + "cors": "^2.8.6", + "express": "^5.2.1", + "graphql": "^16.14.2" } } diff --git a/server/prisma/seed.ts b/server/prisma/seed.ts index ff856cb..f967355 100644 --- a/server/prisma/seed.ts +++ b/server/prisma/seed.ts @@ -5,7 +5,9 @@ async function main() { const tagPasta = await prisma.tag.upsert({ where: { name: 'vegetar' }, update: {}, create: { name: 'vegetar'}}); const user = await prisma.user.create({ - data: { name: 'Test Testesen', email: 'testesen@eksempel.no' }, + where: { id: 'dev-user' }, + update: {}, + create: { id: 'dev-user', name: 'Test Bruker', email: 'test@example.com'} }); await prisma.recipe.create({ @@ -16,7 +18,7 @@ async function main() { servings: 1, category: 'middag', status: 'PUBLISHED', - authorId: user.id, + authorId: 'dev-user', tags: { connect: [{ id: tagPasta.id}]}, ingredients: { create: [ diff --git a/server/src/db/client.ts b/server/src/db/client.ts new file mode 100644 index 0000000..46938c5 --- /dev/null +++ b/server/src/db/client.ts @@ -0,0 +1,2 @@ +import { PrismaClient } from '@prisma/client'; +export const prisma = new PrismaClient(); \ No newline at end of file diff --git a/server/src/index.js b/server/src/index.js index aa219d8..5963be6 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -1,3 +1,21 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -//# sourceMappingURL=index.js.map \ No newline at end of file +import express from 'express'; +import cors from 'cors'; +import { ApolloServer } from '@apollo/server'; +import { expressMiddleware } from '@apollo/server/express4'; +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { recipeResolvers } from './schema/resolvers/recipe'; + +const typeDefs = readFileSync(join(__dirname, 'schema/typeDefs/recipe.graphql'), 'utf-8'); + +async function start() { + const app = express(); + const server = new ApolloServer({ typeDefs, resolvers: recipeResolvers }); + await server.start(); + + app.use('/graphql', cors(), express.json(), expressMiddleware(server)); + + app.listen(3001, () => console.log('Server kjører på http://localhost:3001/graphql')); +} + +start(); \ No newline at end of file diff --git a/server/src/index.ts b/server/src/index.ts index e69de29..ba68147 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -0,0 +1,21 @@ +import express from 'express'; +import cors from 'cors'; +import { ApolloServer } from '@apollo/server'; +import { expressMiddleware } from '@as-integrations/express5'; +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { recipeResolvers } from './schema/resolvers/recipe'; + +const typeDefs = readFileSync(join(__dirname, 'schema/typeDefs/recipe.graphql'), 'utf-8'); + +async function start() { + const app = express(); + const server = new ApolloServer({ typeDefs, resolvers: recipeResolvers }); + await server.start(); + + app.use('/graphql', cors(), express.json(), expressMiddleware(server)); + + app.listen(3001, () => console.log('Server kjører på http://localhost:3001/graphql')); +} + +start(); \ No newline at end of file diff --git a/server/src/schema/resolvers/recipe.ts b/server/src/schema/resolvers/recipe.ts new file mode 100644 index 0000000..e602933 --- /dev/null +++ b/server/src/schema/resolvers/recipe.ts @@ -0,0 +1,49 @@ +import { prisma } from '../../db/client'; + +export const recipeResolvers = { + Query: { + recipes: async (_: unknown, args: { search?: string; category?: string; page?: number; pageSize?: number }) => { + const { search, category, page = 1, pageSize = 20 } = args; + return prisma.recipe.findMany({ + where: { + status: 'PUBLISHED', + ...(search && { title: { contains: search, mode: 'insensitive' } }), + ...(category && { category }), + }, + include: { ingredients: true, steps: true, tags: true }, + skip: (page - 1) * pageSize, + take: pageSize, + orderBy: { createdAt: 'desc' }, + }); + }, + recipe: async (_: unknown, args: { id: string }) => { + return prisma.recipe.findUnique({ + where: { id: args.id }, + include: { ingredients: true, steps: true, tags: true }, + }); + }, + }, + Mutation: { + createRecipe: async (_: unknown, args: { input: any }) => { + const { ingredients, steps, tagNames, ...rest } = args.input; + return prisma.recipe.create({ + data: { + ...rest, + status: 'PUBLISHED', + authorId: 'MIDLERTIDIG-ID', // bytt ut når Person 2 har auth klart + ingredients: { create: ingredients }, + steps: { create: steps }, + tags: tagNames + ? { + connectOrCreate: tagNames.map((name: string) => ({ + where: { name }, + create: { name }, + })), + } + : undefined, + }, + include: { ingredients: true, steps: true, tags: true }, + }); + }, + }, +}; \ No newline at end of file diff --git a/server/src/schema/typeDefs/recipe.graphql b/server/src/schema/typeDefs/recipe.graphql new file mode 100644 index 0000000..0478e09 --- /dev/null +++ b/server/src/schema/typeDefs/recipe.graphql @@ -0,0 +1,66 @@ +type RecipeIngredient { + id: ID! + name: String! + amount: Float! + unit: String! + order: Int! +} + +type Step { + id: ID! + order: Int! + instruction: String! +} + +type Tag { + id: ID! + name: String! +} + +type Recipe { + id: ID! + title: String! + description: String! + prepTimeMinutes: Int! + servings: Int! + category: String! + imageUrl: String + status: String! + createdAt: String! + ingredients: [RecipeIngredient!]! + steps: [Step!]! + tags: [Tag!]! +} + +input RecipeIngredientInput { + name: String! + amount: Float! + unit: String! + order: Int! +} + +input StepInput { + order: Int! + instruction: String! +} + +input CreateRecipeInput { + title: String! + description: String! + prepTimeMinutes: Int! + servings: Int! + category: String! + imageUrl: String + ingredients: [RecipeIngredientInput!]! + steps: [StepInput!]! + tagNames: [String!] +} + +type Query { + recipes(search: String, category: String, page: Int, pageSize: Int): [Recipe!]! + recipe(id: ID!): Recipe +} + +type Mutation { + createRecipe(input: CreateRecipeInput!): Recipe! +} \ No newline at end of file