diff --git a/.gitignore b/.gitignore index 1dcef2d..d88da03 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ node_modules -.env \ No newline at end of file +.env +.vercel +AGENTS.md +CLAUDE.md +.next \ No newline at end of file diff --git a/index.js b/index.js deleted file mode 100644 index d681572..0000000 --- a/index.js +++ /dev/null @@ -1,99 +0,0 @@ -const { ApolloServer } = require('@apollo/server'); -const { startStandaloneServer } = require('@apollo/server/standalone'); -const { Pool } = require('pg'); -require('dotenv').config(); - -// 1. Inisialisasi Pool PostgreSQL -const pool = new Pool({ - connectionString: process.env.DATABASE_URL, - ssl: { - rejectUnauthorized: false, - }, -}); - -// 2. Definisi Schema GraphQL (typeDefs) -const typeDefs = `#graphql - type Category { - id: ID! - name: String! - description: String - products: [Product!]! - } - - type Product { - id: ID! - title: String! - price: Float! - category_id: Int - created_by: ID - category: Category - } - - type Query { - categories: [Category!]! - category(id: ID!): Category - products: [Product!]! - product(id: ID!): Product - } -`; - -// 3. Definisi Resolver (Query + Nested Resolvers) -const resolvers = { - Query: { - categories: async () => { - const result = await pool.query('SELECT * FROM categories ORDER BY id ASC'); - return result.rows; - }, - category: async (_, { id }) => { - const result = await pool.query('SELECT * FROM categories WHERE id = $1', [id]); - return result.rows[0] || null; - }, - products: async () => { - // price di-cast ke float agar cocok dengan tipe Float di GraphQL - const result = await pool.query('SELECT id, title, price::float, category_id, created_by FROM products ORDER BY id ASC'); - return result.rows; - }, - product: async (_, { id }) => { - const result = await pool.query('SELECT id, title, price::float, category_id, created_by FROM products WHERE id = $1', [id]); - return result.rows[0] || null; - }, - }, - - // Resolver Relasi Category -> Products (One-to-Many) - Category: { - products: async (parent) => { - const result = await pool.query( - 'SELECT id, title, price::float, category_id, created_by FROM products WHERE category_id = $1 ORDER BY id ASC', - [parent.id] - ); - return result.rows; - }, - }, - - // Resolver Relasi Product -> Category (Many-to-One) - Product: { - category: async (parent) => { - if (!parent.category_id) return null; - const result = await pool.query('SELECT * FROM categories WHERE id = $1', [parent.category_id]); - return result.rows[0] || null; - }, - }, -}; - -// 4. Inisialisasi Apollo Server -const server = new ApolloServer({ - typeDefs, - resolvers, - introspection: true, // Wajib aktif untuk Apollo Sandbox publik -}); - -// 5. Jalankan Server -async function startServer() { - const port = process.env.PORT || 4000; - const { url } = await startStandaloneServer(server, { - listen: { port: Number(port) }, - }); - console.log(`Server GraphQL siap di: ${url}`); -} - -startServer(); \ No newline at end of file diff --git a/pages/api/graphql.js b/pages/api/graphql.js index 6b23c85..a301bdd 100644 --- a/pages/api/graphql.js +++ b/pages/api/graphql.js @@ -2,14 +2,16 @@ const { ApolloServer } = require('@apollo/server'); const { startServerAndCreateNextHandler } = require('@as-integrations/next'); const { Pool } = require('pg'); +// 1. Inisialisasi Pool PostgreSQL untuk Serverless const pool = new Pool({ connectionString: process.env.DATABASE_URL, ssl: { rejectUnauthorized: false, }, - max: 1, // Menjaga batas koneksi pada lingkungan serverless + max: 1, // Batasi 1 koneksi per instance serverless }); +// 2. Definisi Schema GraphQL (typeDefs) const typeDefs = `#graphql type Category { id: ID! @@ -35,6 +37,7 @@ const typeDefs = `#graphql } `; +// 3. Definisi Resolver (Query + Nested Resolvers) const resolvers = { Query: { categories: async () => { @@ -46,14 +49,21 @@ const resolvers = { return result.rows[0] || null; }, products: async () => { - const result = await pool.query('SELECT id, title, price::float, category_id, created_by FROM products ORDER BY id ASC'); + const result = await pool.query( + 'SELECT id, title, price::float, category_id, created_by FROM products ORDER BY id ASC' + ); return result.rows; }, product: async (_, { id }) => { - const result = await pool.query('SELECT id, title, price::float, category_id, created_by FROM products WHERE id = $1', [id]); + const result = await pool.query( + 'SELECT id, title, price::float, category_id, created_by FROM products WHERE id = $1', + [id] + ); return result.rows[0] || null; }, }, + + // Resolver Relasi Category -> Products (One-to-Many) Category: { products: async (parent) => { const result = await pool.query( @@ -63,6 +73,8 @@ const resolvers = { return result.rows; }, }, + + // Resolver Relasi Product -> Category (Many-to-One) Product: { category: async (parent) => { if (!parent.category_id) return null; @@ -72,25 +84,40 @@ const resolvers = { }, }; +// 4. Inisialisasi Apollo Server const server = new ApolloServer({ typeDefs, resolvers, introspection: true, + csrfPrevention: false, // Mematikan proteksi CSRF agar request dari browser Sandbox tidak dicegat }); const apolloHandler = startServerAndCreateNextHandler(server); -// Wrapper untuk menangani CORS Apollo Studio Sandbox +// 5. Handler CORS untuk Apollo Studio dan Embedded Sandbox module.exports = async function handler(req, res) { - res.setHeader('Access-Control-Allow-Credentials', 'true'); - res.setHeader('Access-Control-Allow-Origin', 'https://studio.apollographql.com'); - res.setHeader('Access-Control-Allow-Methods', 'GET,OPTIONS,POST'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + const allowedOrigins = new Set([ + 'https://studio.apollographql.com', + 'https://embeddable-sandbox.cdn.apollographql.com', + ]); + const origin = req.headers.origin; + if (allowedOrigins.has(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Vary', 'Origin'); + } + + res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS'); + res.setHeader( + 'Access-Control-Allow-Headers', + 'Content-Type, Authorization, apollo-require-preflight, x-apollo-operation-name' + ); + + // Respons langsung untuk preflight OPTIONS request if (req.method === 'OPTIONS') { res.status(200).end(); return; } return apolloHandler(req, res); -}; \ No newline at end of file +}; diff --git a/pages/index.js b/pages/index.js new file mode 100644 index 0000000..e1d27c1 --- /dev/null +++ b/pages/index.js @@ -0,0 +1,56 @@ +const React = require('react'); +const Head = require('next/head'); + +function Home() { + const sandboxRef = React.useRef(null); + + React.useEffect(() => { + const script = document.createElement('script'); + script.src = + 'https://embeddable-sandbox.cdn.apollographql.com/_latest/embeddable-sandbox.umd.production.min.js'; + script.async = true; + + script.onload = () => { + sandboxRef.current = new window.EmbeddedSandbox({ + target: '#embedded-sandbox', + initialEndpoint: `${window.location.origin}/api/graphql`, + initialState: { + document: `query TestCategoriesWithProducts { + categories { + id + name + products { + id + title + price + } + } +}`, + }, + }); + }; + + document.body.appendChild(script); + + return () => { + sandboxRef.current?.dispose?.(); + script.remove(); + }; + }, []); + + return React.createElement( + React.Fragment, + null, + React.createElement( + Head, + null, + React.createElement('title', null, 'GraphQL Sandbox - Lab 04') + ), + React.createElement('div', { + id: 'embedded-sandbox', + style: { width: '100vw', height: '100vh' }, + }) + ); +} + +module.exports = Home;