feat: add embedded Apollo Sandbox page

This commit is contained in:
MelvynFaith 2026-09-09 18:23:47 +07:00
parent 6edf45b5f4
commit 3b458298e0
4 changed files with 97 additions and 109 deletions

4
.gitignore vendored
View File

@ -1,2 +1,6 @@
node_modules
.env
.vercel
AGENTS.md
CLAUDE.md
.next

View File

@ -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();

View File

@ -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,21 +84,36 @@ 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;

56
pages/index.js Normal file
View File

@ -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;