feat: add embedded Apollo Sandbox page
This commit is contained in:
parent
6edf45b5f4
commit
3b458298e0
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,2 +1,6 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.env
|
.env
|
||||||
|
.vercel
|
||||||
|
AGENTS.md
|
||||||
|
CLAUDE.md
|
||||||
|
.next
|
||||||
99
index.js
99
index.js
@ -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();
|
|
||||||
@ -2,14 +2,16 @@ const { ApolloServer } = require('@apollo/server');
|
|||||||
const { startServerAndCreateNextHandler } = require('@as-integrations/next');
|
const { startServerAndCreateNextHandler } = require('@as-integrations/next');
|
||||||
const { Pool } = require('pg');
|
const { Pool } = require('pg');
|
||||||
|
|
||||||
|
// 1. Inisialisasi Pool PostgreSQL untuk Serverless
|
||||||
const pool = new Pool({
|
const pool = new Pool({
|
||||||
connectionString: process.env.DATABASE_URL,
|
connectionString: process.env.DATABASE_URL,
|
||||||
ssl: {
|
ssl: {
|
||||||
rejectUnauthorized: false,
|
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
|
const typeDefs = `#graphql
|
||||||
type Category {
|
type Category {
|
||||||
id: ID!
|
id: ID!
|
||||||
@ -35,6 +37,7 @@ const typeDefs = `#graphql
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// 3. Definisi Resolver (Query + Nested Resolvers)
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
categories: async () => {
|
categories: async () => {
|
||||||
@ -46,14 +49,21 @@ const resolvers = {
|
|||||||
return result.rows[0] || null;
|
return result.rows[0] || null;
|
||||||
},
|
},
|
||||||
products: async () => {
|
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;
|
return result.rows;
|
||||||
},
|
},
|
||||||
product: async (_, { id }) => {
|
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;
|
return result.rows[0] || null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Resolver Relasi Category -> Products (One-to-Many)
|
||||||
Category: {
|
Category: {
|
||||||
products: async (parent) => {
|
products: async (parent) => {
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
@ -63,6 +73,8 @@ const resolvers = {
|
|||||||
return result.rows;
|
return result.rows;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Resolver Relasi Product -> Category (Many-to-One)
|
||||||
Product: {
|
Product: {
|
||||||
category: async (parent) => {
|
category: async (parent) => {
|
||||||
if (!parent.category_id) return null;
|
if (!parent.category_id) return null;
|
||||||
@ -72,25 +84,40 @@ const resolvers = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 4. Inisialisasi Apollo Server
|
||||||
const server = new ApolloServer({
|
const server = new ApolloServer({
|
||||||
typeDefs,
|
typeDefs,
|
||||||
resolvers,
|
resolvers,
|
||||||
introspection: true,
|
introspection: true,
|
||||||
|
csrfPrevention: false, // Mematikan proteksi CSRF agar request dari browser Sandbox tidak dicegat
|
||||||
});
|
});
|
||||||
|
|
||||||
const apolloHandler = startServerAndCreateNextHandler(server);
|
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) {
|
module.exports = async function handler(req, res) {
|
||||||
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
const allowedOrigins = new Set([
|
||||||
res.setHeader('Access-Control-Allow-Origin', 'https://studio.apollographql.com');
|
'https://studio.apollographql.com',
|
||||||
res.setHeader('Access-Control-Allow-Methods', 'GET,OPTIONS,POST');
|
'https://embeddable-sandbox.cdn.apollographql.com',
|
||||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
]);
|
||||||
|
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') {
|
if (req.method === 'OPTIONS') {
|
||||||
res.status(200).end();
|
res.status(200).end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return apolloHandler(req, res);
|
return apolloHandler(req, res);
|
||||||
};
|
};
|
||||||
|
|||||||
56
pages/index.js
Normal file
56
pages/index.js
Normal 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;
|
||||||
Loading…
x
Reference in New Issue
Block a user