Appearance
Google Spanner new
This example shows how to connect to a Google Spanner database on the cloud and run queries using GQL. See a full tutorial in Google Spanner tutorial.
ts
import Ogma, { RawGraph } from '@linkurious/ogma';
import Prism from 'prismjs';
import 'prismjs/themes/prism.css';
import './styles.css';
import { isGenre, isMovie, isPerson, NodeDataType } from './types';
import * as Queries from './queries';
import { APIClient as API } from './client';
import { addStyles } from './styles';
const ogma = new Ogma<NodeDataType>({
container: 'graph-container',
options: {
backgroundColor: 'transparent'
}
});
addStyles(ogma);
const client = new API();
const initialMovie = 'Chappie';
const graph = await client.getMovieByTitle(initialMovie);
setQuery(Queries.GET_MOVIE_BY_TITLE.replace('@title', `"${initialMovie}"`));
await ogma.setGraph(graph);
await ogma.layouts.force({ locate: true });
async function addGraphAndZoomIn(graph: RawGraph) {
if (graph.nodes.length === 0) return;
await ogma.addGraph(graph, { ignoreInvalid: true });
await ogma.layouts.force({ gpu: true });
await ogma.view.moveToBounds(
ogma
.getNodes(graph.nodes.map(n => n.id!))
.getBoundingBox()
.pad(50),
{ duration: 200 }
);
}
ogma.events.on('doubleclick', async ({ target }) => {
if (target && target.isNode) {
if (isMovie(target)) {
const movieTitle = target.getData('title');
setQuery(Queries.GET_MOVIE_BY_TITLE.replace('@title', `"${movieTitle}"`));
const graph = await client.getMovieByTitle(movieTitle);
await ogma.addGraph(graph, { ignoreInvalid: true });
await addGraphAndZoomIn(graph);
} else if (isPerson(target)) {
const actorName = target.getData('name');
setQuery(
Queries.GET_MOVIES_BY_ACTOR_NAME.replace('@actorName', `"${actorName}"`)
);
const graph = await client.getActorByName(actorName);
await addGraphAndZoomIn(graph);
} else if (isGenre(target)) {
const genreName = target.getData('name');
setQuery(
Queries.GET_MOVIES_BY_GENRE_NAME.replace('@genreName', `"${genreName}"`)
);
const graph = await client.getGenreByName(genreName);
await addGraphAndZoomIn(graph);
}
}
});
function setQuery(query: string) {
const codeBlock = document.querySelector<HTMLElement>('#query-text')!;
codeBlock.innerHTML = query;
Prism.highlightElement(codeBlock);
}
document.getElementById('layout')!.addEventListener('click', async () => {
await ogma.layouts.force({ gpu: true, locate: true });
});
html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<link
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/solid.min.css"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.19.0/themes/prism.min.css"
integrity="sha256-cuvic28gVvjQIo3Q4hnRpQSNB0aMw3C+kjkR0i+hrWg="
crossorigin="anonymous"
/>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.19.0/prism.min.js"
integrity="sha256-YZQM6/hLBZYkb01VYf17isoQM4qpaOP+aX96hhYrWhg="
crossorigin="anonymous"
></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.19.0/plugins/autoloader/prism-autoloader.min.js"
integrity="sha256-WIuEtgHNTdrDT2obGtHYz/emxxAj04sJBdMhRjDXd8I="
crossorigin="anonymous"
></script>
<link
href="https://cdn.jsdelivr.net/npm/lucide-static@0.483.0/font/lucide.css"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap"
rel="stylesheet"
/>
</head>
<body>
<div id="graph-container"></div>
<div class="toolbar">
<h3>Query</h3>
<pre><code class="language-sparql" id="query-text"></code></pre>
<div class="controls">
<button id="layout">Run Layout →</button>
<img
src="img/google-spanner-logo.svg"
alt="Google spanner"
class="logo"
/>
</div>
</div>
<div id="info">loading...</div>
<script type="module" src="index.ts"></script>
</body>
</html>
css
:root {
--font: "IBM Plex", monospace;
}
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font-family: var(--font);
font-size: 13px;
}
/* dotted background */
body {
background-color: #fff;
background-image: radial-gradient(
circle,
rgba(0, 0, 0, 0.1) 1px,
transparent 1px
);
background-size: 20px 20px;
}
#graph-container {
width: 100%;
height: 100%;
position: relative;
}
.toolbar {
position: absolute;
top: 12px;
right: 12px;
background-color: rgba(255, 255, 255, 0.8);
padding: 10px;
border-radius: 12px;
width: 400px;
background: #fff;
box-shadow: 0px 14px 5px 0px rgba(75, 73, 73, 0.01),
0px 8px 5px 0px rgba(75, 73, 73, 0.05),
0px 3px 3px 0px rgba(75, 73, 73, 0.09),
0px 1px 2px 0px rgba(75, 73, 73, 0.1);
}
.toolbar pre {
padding-top: 0;
margin-top: 0;
background-color: #f7f7f7;
max-height: 200px;
overflow-y: auto;
}
.toolbar .controls {
display: flex;
flex-direction: row;
gap: 10px;
justify-content: space-between;
padding-top: 12px;
}
.toolbar button {
border-radius: 8px;
padding: 8px 24px 8px 24px;
background-color: #0e64ef;
color: #fff;
font: var(--font);
border: none;
font-size: 12px;
}
.toolbar .logo {
align-self: flex-end;
}
json
{
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"@types/node": "^22.14.0",
"csv-parser": "^3.2.0"
},
"dependencies": {
"@google-cloud/spanner": "^7.19.1",
"@linkurious/ogma": "^5.2.3-PR-4014.6",
"dotenv": "^16.4.7",
"express": "^5.1.0",
"prismjs": "^1.19.0",
"uuid": "^11.1.0"
}
}
ts
import { EdgeId, NodeId, RawGraph } from '@linkurious/ogma';
export class APIClient {
private apiUrl = '/api'; // Replace with your API URL
getMovieByTitle(title: string) {
return fetch(`${this.apiUrl}/movie/${title}`)
.then(response => response.json())
.then(data => data.graph);
}
getActorByName(name: string) {
return fetch(`${this.apiUrl}/actor/${name}`)
.then(response => response.json())
.then(data => data.graph);
}
getGenreByName(name: string) {
return fetch(`${this.apiUrl}/genre/${name}`)
.then(response => response.json())
.then(data => data.graph);
}
getDirectorByName(name: string) {
return fetch(`${this.apiUrl}/director/${name}`)
.then(response => response.json())
.then(data => data.graph);
}
// <MOCK IMPLEMENTATIONS BELOW> ==============================================
private graph!: RawGraph;
// @ts-expect-error mock implementation
async getDirectorByName(name: string): Promise<any> {
await this.init();
const director = this.graph.nodes.find(node => node.data.name === name);
if (!director) return emptyGraph();
const directed = this.graph.edges.filter(
edge => edge.source == director.id
);
const movies = directed.map(edge =>
this.graph.nodes.find(node => node.id === edge.target)
);
return {
nodes: [director, ...movies],
edges: directed
};
}
// @ts-expect-error mock implementation
async getActorByName(name: string): Promise<any> {
await this.init();
const actor = this.graph.nodes.find(node => node.data.name === name);
if (!actor) return emptyGraph();
const actedIn = this.graph.edges.filter(edge => edge.source == actor.id);
const directed = this.graph.edges.filter(
edge => edge.source == actor.id && edge.data.type === 'Directed'
);
const directedMovies = actedIn.map(
edge => this.graph.nodes.find(node => node.id === edge.target)!
);
const movies = actedIn.map(
edge => this.graph.nodes.find(node => node.id === edge.target)!
);
const nodeSet = new Set<NodeId>();
const edgeSet = new Set<EdgeId>();
const nodes = [actor, ...movies, ...directedMovies].filter(node => {
if (nodeSet.has(node.id!)) return false;
nodeSet.add(node.id!);
return true;
});
const edges = [...actedIn, ...directed].filter(edge => {
if (edgeSet.has(edge.id!)) return false;
edgeSet.add(edge.id!);
return true;
});
return {
nodes,
edges
};
}
// @ts-expect-error mock implementation
async getGenreByName(name: string): Promise<any> {
await this.init();
const genre = this.graph.nodes.find(
node => node.data.type === 'Genre' && node.data.name === name
);
if (!genre) return emptyGraph();
const hasGenre = this.graph.edges.filter(
edge => edge.target === genre.id && edge.data.type === 'HasGenre'
);
const movies = hasGenre.map(
edge => this.graph.nodes.find(node => node.id === edge.source)!
);
const nodeSet = new Set<NodeId>();
const edgeSet = new Set<EdgeId>();
const nodes = [genre, ...movies].filter(node => {
if (nodeSet.has(node.id!)) return false;
nodeSet.add(node.id!);
return true;
});
const edges = hasGenre.filter(edge => {
if (edgeSet.has(edge.id!)) return false;
edgeSet.add(edge.id!);
return true;
});
return {
nodes,
edges
};
}
// @ts-expect-error mock implementation
async getMovieByTitle(title: string): Promise<any> {
await this.init();
const movie = this.graph.nodes.find(node => node.data.title === title);
if (!movie) return emptyGraph();
const actedIn = this.graph.edges.filter(
edge => edge.data.type === 'ActedIn' && edge.target === movie.id
);
const directed = this.graph.edges.filter(
edge => edge.data.type === 'Directed' && edge.target === movie.id
);
const hasGenre = this.graph.edges.filter(
edge => edge.source === movie.id && edge.data.type === 'HasGenre'
);
const actors = actedIn.map(
edge => this.graph.nodes.find(node => node.id === edge.source)!
);
const directors = directed.map(
edge => this.graph.nodes.find(node => node.id === edge.source)!
);
const genres = hasGenre.map(
edge =>
this.graph.nodes.find(
node => node.data.type === 'Genre' && node.id === edge.target
)!
);
const nodeSet = new Set<NodeId>();
const edgeSet = new Set<EdgeId>();
const nodes = [movie, ...actors, ...directors, ...genres].filter(node => {
if (nodeSet.has(node.id!)) return false;
nodeSet.add(node.id!);
return true;
});
const edges = [...actedIn, ...directed, ...hasGenre].filter(edge => {
if (edgeSet.has(edge.id!)) return false;
edgeSet.add(edge.id!);
return true;
});
return {
nodes,
edges
};
}
async init() {
if (this.graph) return;
const graph = await fetch(`files/movies.json`).then(res => res.json());
this.graph = graph;
}
}
function emptyGraph(): RawGraph {
return {
nodes: [],
edges: []
};
}
ts
export const PERSON = "Person";
export const MOVIE = "Movie";
export const GENRE = "Genre";
export const DIRECTED = "Directed";
export const ACTED_IN = "ActedIn";
export const HAS_GENRE = "HasGenre";
xml
<svg width="138" height="36" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="138" height="36" rx="8" fill="#F7F7F7"/><path d="M21.295 15.447c-.74-.434-1.462-.904-2.227-1.288-.604-.303-.843-.706-.807-1.385.053-.994.012-1.992.015-2.989a.74.74 0 0 1 .554-.738c.329-.084.684.048.832.351.075.155.108.345.11.52.01.902.009 1.805.001 2.707-.001.15.048.233.177.306.646.365 1.29.737 1.93 1.112.113.066.2.068.315 0 .647-.38 1.298-.753 1.95-1.122.115-.065.159-.138.158-.272-.006-.953-.006-1.906-.002-2.859a.752.752 0 0 1 .575-.746.733.733 0 0 1 .836.388c.059.126.087.277.088.417.007 1.112.003 2.224.004 3.336 0 .341-.141.586-.444.757-.81.458-1.612.93-2.42 1.391-.111.064-.166.136-.159.272.012.209.002.418-.017.644-.502-.256-.985-.53-1.47-.802Z" fill="#FBBD07"/><path d="M25.005 20.213c.521-.291 1.043-.582 1.563-.875.364-.206.726-.415 1.087-.626a.71.71 0 0 1 .76-.01c1.004.58 2.01 1.16 3.009 1.749.382.225.489.672.274 1.03-.217.363-.662.47-1.058.243-.806-.463-1.61-.93-2.41-1.403-.134-.079-.23-.085-.368-.004-.64.378-1.285.744-1.932 1.108-.113.064-.161.135-.16.27.006.757.006 1.515-.002 2.272 0 .127.043.192.149.253.806.463 1.608.933 2.413 1.398.216.124.38.285.431.538a.72.72 0 0 1-.273.75.689.689 0 0 1-.811.06c-.79-.448-1.574-.906-2.36-1.36-.193-.112-.38-.236-.579-.337-.343-.175-.476-.454-.473-.828.009-.91.002-1.819.014-2.727.002-.155-.05-.237-.179-.305a4.632 4.632 0 0 1-.54-.355c.476-.303.972-.559 1.445-.84Z" fill="#EA4436"/><path d="M19.843 21.485c-.008.193-.024.386-.024.58 0 .786-.002 1.573.01 2.36.005.383-.141.654-.481.847-.972.55-1.932 1.123-2.903 1.674-.497.282-1.05.006-1.123-.548-.045-.347.117-.596.412-.768.605-.35 1.21-.7 1.815-1.052.212-.123.42-.252.636-.368.1-.054.142-.115.14-.235-.007-.772-.002-1.544-.012-2.316a.295.295 0 0 0-.124-.213 90.976 90.976 0 0 0-2.026-1.16c-.066-.036-.195-.02-.267.021-.82.468-1.634.946-2.452 1.418-.324.187-.663.15-.917-.09-.247-.234-.318-.596-.144-.888a.931.931 0 0 1 .314-.315c.98-.58 1.968-1.145 2.952-1.719.285-.166.555-.146.836.017.804.467 1.613.925 2.417 1.393.112.065.193.08.308.003.198-.095.393-.246.588-.333.03.566.033 1.14.045 1.692Z" fill="#35A854"/><path d="M19.847 21.462c-.028-.545-.041-1.096-.056-1.67.438-.28.876-.544 1.322-.794.125-.07.16-.172.159-.312-.003-1.081.002-2.163.01-3.264.487.254.976.549 1.474.826.018.81.024 1.618.02 2.425 0 .147.041.23.172.306.68.39 1.375.821 2.063 1.23-.463.293-.967.554-1.451.845-.465-.251-.91-.517-1.361-.784-.104-.062-.178-.071-.289-.005-.679.403-1.364.795-2.063 1.197Z" fill="#4386F3"/><path d="M42.834 18.512h2.994v4.154c-.45.152-.912.266-1.389.34-.472.074-1.002.111-1.587.111-.891 0-1.643-.178-2.256-.533a3.443 3.443 0 0 1-1.389-1.518c-.316-.66-.475-1.443-.475-2.35 0-.886.174-1.66.522-2.32a3.708 3.708 0 0 1 1.518-1.535c.663-.367 1.46-.55 2.39-.55.477 0 .928.044 1.354.134.43.086.828.21 1.195.37l-.38.867a7.011 7.011 0 0 0-1.05-.352 4.698 4.698 0 0 0-1.178-.146c-.699 0-1.298.144-1.798.433a2.886 2.886 0 0 0-1.143 1.225c-.262.527-.392 1.152-.392 1.875 0 .715.115 1.338.345 1.869.23.527.59.937 1.078 1.23.489.293 1.117.44 1.887.44.387 0 .719-.022.996-.065.277-.046.53-.101.756-.164v-2.625h-1.998v-.89Zm10.459 1.265c0 .528-.068.998-.205 1.412a2.893 2.893 0 0 1-.592 1.05c-.258.284-.57.503-.937.655a3.243 3.243 0 0 1-1.237.223 3.07 3.07 0 0 1-1.183-.223 2.67 2.67 0 0 1-.926-.656 3.032 3.032 0 0 1-.604-1.049 4.381 4.381 0 0 1-.21-1.412c0-.703.119-1.3.357-1.793a2.546 2.546 0 0 1 1.02-1.13c.445-.262.974-.393 1.587-.393.586 0 1.098.13 1.535.393.442.261.784.64 1.026 1.136.246.492.369 1.088.369 1.787Zm-4.887 0c0 .516.069.963.205 1.342.137.379.348.672.633.879.285.207.653.31 1.102.31.445 0 .81-.103 1.095-.31.29-.207.502-.5.64-.879.136-.379.204-.826.204-1.342 0-.511-.068-.953-.205-1.324-.137-.375-.348-.664-.633-.867-.285-.203-.654-.305-1.107-.305-.668 0-1.158.221-1.47.662-.31.442-.464 1.053-.464 1.834Zm12.106 0c0 .528-.069.998-.205 1.412a2.892 2.892 0 0 1-.592 1.05c-.258.284-.57.503-.938.655a3.243 3.243 0 0 1-1.236.223 3.07 3.07 0 0 1-1.184-.223 2.672 2.672 0 0 1-.925-.656 3.033 3.033 0 0 1-.604-1.049 4.381 4.381 0 0 1-.21-1.412c0-.703.118-1.3.357-1.793a2.547 2.547 0 0 1 1.02-1.13c.445-.262.974-.393 1.587-.393.586 0 1.098.13 1.535.393.442.261.783.64 1.026 1.136.246.492.369 1.088.369 1.787Zm-4.887 0c0 .516.068.963.205 1.342.137.379.348.672.633.879.285.207.652.31 1.102.31.445 0 .81-.103 1.095-.31.29-.207.502-.5.639-.879.136-.379.205-.826.205-1.342 0-.511-.069-.953-.205-1.324-.137-.375-.348-.664-.633-.867-.285-.203-.654-.305-1.107-.305-.668 0-1.159.221-1.471.662-.309.442-.463 1.053-.463 1.834Zm8.361 6.106c-.843 0-1.494-.158-1.95-.475-.458-.312-.686-.752-.686-1.318 0-.402.127-.746.38-1.031.258-.286.614-.475 1.067-.569a1.168 1.168 0 0 1-.434-.351.875.875 0 0 1-.175-.54c0-.234.064-.439.193-.615.133-.18.334-.351.603-.515a1.754 1.754 0 0 1-.82-.692 2.095 2.095 0 0 1-.31-1.142c0-.461.095-.854.287-1.178.191-.328.468-.578.832-.75.363-.172.802-.258 1.318-.258.113 0 .227.006.34.018.117.008.228.021.334.04.105.016.197.036.275.06h2.203v.626l-1.183.147a2.078 2.078 0 0 1 .41 1.266c0 .64-.217 1.15-.65 1.529-.434.375-1.03.562-1.788.562-.18 0-.363-.015-.55-.047a1.424 1.424 0 0 0-.44.352.717.717 0 0 0-.146.445c0 .125.037.225.111.3.078.073.19.128.334.163.145.031.318.047.522.047h1.13c.7 0 1.235.146 1.606.44.375.293.562.72.562 1.283 0 .71-.289 1.256-.867 1.634-.578.38-1.414.569-2.508.569Zm.03-.762c.53 0 .97-.055 1.318-.164.352-.105.613-.26.785-.463.176-.2.264-.437.264-.715 0-.258-.059-.453-.176-.586-.117-.129-.289-.215-.516-.258a4.19 4.19 0 0 0-.832-.07h-1.113c-.289 0-.54.045-.756.135-.215.09-.38.223-.498.398-.113.176-.17.395-.17.657 0 .347.147.611.44.79.293.184.71.276 1.254.276Zm.252-5.133c.464 0 .814-.117 1.048-.351.235-.235.352-.576.352-1.026 0-.48-.12-.84-.358-1.078-.238-.242-.59-.363-1.054-.363-.446 0-.79.125-1.031.375-.239.246-.358.607-.358 1.084 0 .437.121.773.364 1.008.242.234.587.351 1.037.351ZM69.688 23h-.979v-9.117h.978V23Zm4.529-6.54c.547 0 1.015.122 1.406.364.39.242.69.582.896 1.02.208.433.311.941.311 1.523v.604h-4.436c.012.754.2 1.328.563 1.722.363.395.875.592 1.535.592.406 0 .766-.037 1.078-.111.313-.074.637-.184.973-.328v.855c-.324.145-.647.25-.967.317-.316.066-.691.1-1.125.1-.617 0-1.156-.126-1.617-.376a2.623 2.623 0 0 1-1.066-1.113c-.254-.488-.381-1.086-.381-1.793 0-.691.115-1.29.345-1.793.235-.508.563-.898.985-1.172.426-.273.926-.41 1.5-.41Zm-.012.798c-.52 0-.933.17-1.242.51-.309.34-.492.814-.55 1.423h3.398a2.745 2.745 0 0 0-.182-1.007 1.415 1.415 0 0 0-.522-.68c-.234-.164-.535-.246-.902-.246ZM86.58 20.72c0 .508-.127.941-.38 1.3-.25.356-.602.627-1.056.815-.453.187-.986.281-1.6.281-.323 0-.63-.015-.919-.047a6.247 6.247 0 0 1-.797-.134 3.382 3.382 0 0 1-.639-.217v-.955c.293.12.65.234 1.073.34.422.101.863.152 1.324.152.43 0 .793-.057 1.09-.17.297-.117.521-.283.674-.498.156-.219.234-.48.234-.785 0-.293-.065-.537-.193-.733-.13-.199-.344-.379-.645-.539a8.502 8.502 0 0 0-1.219-.521 6.56 6.56 0 0 1-.96-.422 2.996 2.996 0 0 1-.698-.527 1.987 1.987 0 0 1-.428-.68 2.582 2.582 0 0 1-.14-.885c0-.457.115-.848.346-1.172a2.2 2.2 0 0 1 .966-.75c.414-.176.889-.264 1.424-.264.457 0 .879.043 1.266.13.39.085.748.2 1.072.345l-.31.856a6.756 6.756 0 0 0-.985-.323 4.377 4.377 0 0 0-1.066-.128c-.367 0-.678.054-.932.164-.25.105-.441.255-.574.45a1.21 1.21 0 0 0-.2.698c0 .3.063.55.188.75.129.2.332.377.61.533.28.152.652.313 1.113.48.504.184.931.38 1.283.587.352.203.62.453.803.75.183.293.275.666.275 1.119Zm4.623-4.26c.8 0 1.44.275 1.916.826.477.55.715 1.379.715 2.485 0 .73-.11 1.343-.328 1.84-.219.495-.528.87-.926 1.124-.394.254-.861.381-1.4.381a2.72 2.72 0 0 1-.897-.135 2.114 2.114 0 0 1-.662-.363 2.369 2.369 0 0 1-.457-.498h-.07l.04.55c.02.216.03.403.03.563v2.637h-.978v-9.293h.802l.13.95h.046c.125-.196.277-.374.457-.534.18-.164.398-.293.656-.387.262-.097.57-.146.926-.146Zm-.17.82c-.445 0-.805.086-1.078.258-.27.172-.467.43-.592.773-.125.34-.191.768-.199 1.284v.187c0 .543.059 1.002.176 1.377.12.375.318.66.592.856.277.195.648.293 1.113.293.398 0 .73-.108.996-.323.266-.215.463-.513.592-.896.133-.387.199-.83.199-1.33 0-.758-.148-1.36-.445-1.805-.293-.45-.744-.674-1.354-.674Zm6.903-.808c.765 0 1.333.172 1.705.515.371.344.556.893.556 1.647V23h-.709l-.187-.95h-.047c-.18.235-.367.432-.563.593a2.091 2.091 0 0 1-.68.357 3.227 3.227 0 0 1-.937.117c-.39 0-.738-.068-1.043-.205a1.624 1.624 0 0 1-.715-.621c-.172-.277-.257-.629-.257-1.055 0-.64.254-1.133.761-1.476.508-.344 1.282-.532 2.32-.563l1.108-.047v-.392c0-.555-.12-.944-.357-1.166-.239-.223-.575-.334-1.008-.334-.336 0-.656.049-.961.146a6.58 6.58 0 0 0-.867.346l-.3-.738a5.35 5.35 0 0 1 .997-.381 4.35 4.35 0 0 1 1.184-.158Zm1.3 3.363-.978.041c-.801.031-1.365.162-1.694.393-.328.23-.492.556-.492.978 0 .367.112.639.334.814.223.176.518.264.885.264.57 0 1.037-.158 1.4-.474.364-.317.545-.791.545-1.424v-.592Zm6.006-3.375c.762 0 1.338.187 1.729.562.39.372.586.97.586 1.793V23h-.961v-4.12c0-.534-.123-.935-.369-1.2-.243-.266-.616-.399-1.12-.399-.711 0-1.211.201-1.5.604-.289.402-.433.986-.433 1.752V23h-.973v-6.422h.785l.147.926h.053a1.9 1.9 0 0 1 .521-.569 2.27 2.27 0 0 1 .709-.351c.262-.082.537-.123.826-.123Zm7.36 0c.761 0 1.337.187 1.728.562.391.372.586.97.586 1.793V23h-.961v-4.12c0-.534-.123-.935-.369-1.2-.242-.266-.615-.399-1.119-.399-.711 0-1.211.201-1.5.604-.289.402-.434.986-.434 1.752V23h-.972v-6.422h.785l.146.926h.053c.137-.227.31-.416.521-.569.211-.156.448-.273.709-.351a2.75 2.75 0 0 1 .827-.123Zm6.791 0c.546 0 1.015.121 1.406.363.39.242.689.582.896 1.02.207.433.311.941.311 1.523v.604h-4.436c.012.754.2 1.328.563 1.722.363.395.875.592 1.535.592a4.69 4.69 0 0 0 1.078-.111c.313-.074.637-.184.973-.328v.855a4.74 4.74 0 0 1-.967.317c-.316.066-.691.1-1.125.1-.617 0-1.156-.126-1.617-.376a2.625 2.625 0 0 1-1.067-1.113c-.254-.488-.381-1.086-.381-1.793 0-.691.116-1.29.346-1.793.235-.508.563-.898.985-1.172.425-.273.925-.41 1.5-.41Zm-.012.797c-.52 0-.934.17-1.242.51-.309.34-.493.814-.551 1.423h3.398a2.751 2.751 0 0 0-.181-1.007 1.416 1.416 0 0 0-.522-.68c-.234-.164-.535-.246-.902-.246Zm7.195-.797c.129 0 .264.008.404.023.141.012.266.03.375.053l-.123.902a3.033 3.033 0 0 0-.714-.088c-.258 0-.502.053-.733.159a1.73 1.73 0 0 0-.597.445 2.078 2.078 0 0 0-.405.697 2.65 2.65 0 0 0-.146.903V23h-.979v-6.422h.809l.105 1.184h.041c.133-.239.293-.455.481-.65.187-.2.404-.358.65-.475.25-.117.527-.176.832-.176Z" fill="#000407"/></svg>
ts
export const GET_MOVIE_BY_ID = `
GRAPH movieGraph
MATCH (m:Movies { id: @movieId })
OPTIONAL MATCH (director:People)-[:Directed]->(m)
OPTIONAL MATCH (actor:People)-[:ActedIn]->(m)
OPTIONAL MATCH (genre:Genres)-[:HasGenre]->(m)
RETURN
m.id AS movieId,
m.title AS title,
m.description AS description,
m.year AS year,
m.runtime AS runtime,
m.votes AS votes,
director.id AS directorId,
director.name AS directorName,
actor.id AS actorId,
actor.name AS actorName,
genre.id AS genreId,
genre.name AS genreName
`;
export const GET_MOVIE_BY_TITLE = `
GRAPH movieGraph
MATCH (m:Movies { title: @title })
OPTIONAL MATCH (director:People)-[directed:Directed]->(m)
OPTIONAL MATCH (actor:People)-[acted:ActedIn]->(m)
OPTIONAL MATCH (genre:Genres)-[hasGenre:HasGenre]->(m)
RETURN
m.id AS movieId,
m.title AS movieTitle,
m.description AS movieDescription,
m.year AS movieYear,
m.runtime AS movieRuntime,
m.votes AS movieVotes,
director.id AS directorId,
director.name AS directorName,
actor.id AS actorId,
actor.name AS actorName,
genre.id AS genreId,
genre.name AS genreName,
directed.id AS directedId,
acted.id AS actedInId,
hasGenre.id AS hasGenreId
ORDER BY m.year DESC
`;
export const GET_MOVIES_BY_DIRECTOR_NAME = `
GRAPH movieGraph
MATCH (director:People {name: @directorName})-[directed:Directed]->(movie:Movies)
RETURN
director.id AS directorId,
director.name AS directorName,
directed.id AS directedId,
movie.id AS movieId,
movie.title AS movieTitle,
movie.year AS movieYear,
movie.runtime AS movieRuntime,
movie.votes AS movieVotes,
movie.description AS movieDescription
ORDER BY movie.year DESC
`;
export const GET_MOVIES_BY_GENRE_NAME = `
GRAPH movieGraph
MATCH (genre:Genres {name: @genreName})-[hasGenre:HasGenre]->(movie:Movies)
RETURN
genre.id AS genreId,
genre.name AS genreName,
hasGenre.id AS hasGenreId,
movie.id AS movieId,
movie.title AS movieTitle,
movie.year AS movieYear,
movie.runtime AS movieRuntime,
movie.votes AS movieVotes,
movie.description AS movieDescription
ORDER BY movie.year DESC
`;
export const GET_MOVIES_BY_ACTOR_NAME = `
GRAPH movieGraph
MATCH (actor:People {name: @actorName})-[actedIn:ActedIn]->(movie:Movies)
OPTIONAL MATCH (director:People)-[directed:Directed]->(movie:Movies)
RETURN
actor.id AS actorId,
actor.name AS actorName,
actedIn.id AS actedInId,
movie.id AS movieId,
movie.title AS movieTitle,
movie.year AS movieYear,
movie.runtime AS movieRuntime,
movie.votes AS movieVotes,
movie.description AS movieDescription,
director.id AS directorId,
director.name AS directorName,
directed.id AS directedId
ORDER BY movie.year DESC
`;
ts
import express, { Request, Response } from "express";
import { Spanner } from "@google-cloud/spanner";
import dotenv from "dotenv";
import { getDatabase } from "./db";
import {
GET_MOVIE_BY_TITLE,
GET_MOVIES_BY_ACTOR_NAME,
GET_MOVIES_BY_DIRECTOR_NAME,
GET_MOVIES_BY_GENRE_NAME,
} from "./queries";
import { Parser } from "./parser";
import { Row } from "@google-cloud/spanner/build/src/partial-result-stream";
import { Rows } from "@google-cloud/spanner/build/src/transaction";
// Load environment variables from .env file
dotenv.config();
// Initialize Express app
const app = express();
const port = 3000;
const database = getDatabase();
const parser = new Parser();
const toGraph = (rows: Rows) => parser.parse(rows.map((r) => r.toJSON()));
// Route stubs
app.get("/movie/:title", async (req: Request, res: Response) => {
const { title } = req.params;
const [rows] = await database.run({
sql: GET_MOVIE_BY_TITLE,
params: { title },
});
// Placeholder for database query logic
res.json({
message: `Stub: Get movie with title "${title}"`,
graph: toGraph(rows),
});
});
app.get("/actor/:name", async (req: Request, res: Response) => {
const { name } = req.params;
const [rows] = await database.run({
sql: GET_MOVIES_BY_ACTOR_NAME,
params: { actorName: name },
});
// Placeholder for database query logic
res.json({
message: `Stub: Get actor with name "${name}"`,
graph: toGraph(rows),
});
});
app.get("/genre/:name", async (req: Request, res: Response) => {
const { name } = req.params;
const [rows] = await database.run({
sql: GET_MOVIES_BY_GENRE_NAME,
params: { genreName: name },
});
// Placeholder for database query logic
res.json({
message: `Stub: Get genre with name "${name}"`,
graph: toGraph(rows),
});
});
app.get("/director/:name", async (req: Request, res: Response) => {
const { name } = req.params;
const [rows] = await database.run({
sql: GET_MOVIES_BY_DIRECTOR_NAME,
params: { directorName: name },
});
// Placeholder for database query logic
res.json({
message: `Stub: Get director with name "${name}"`,
graph: toGraph(rows),
});
});
// Start the server
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
ts
import Ogma from "@linkurious/ogma";
import { isGenre, isMovie, isPerson, NodeDataType } from "./types";
const placeholder = document.createElement("span");
document.body.appendChild(placeholder);
placeholder.style.visibility = "hidden";
// helper routine to get the icon HEX code
export function getIconCode(className: string) {
placeholder.className = className;
const code = getComputedStyle(placeholder, ":before").content;
return code[1];
}
const font = "IBM Plex Sans";
const HIGHLIGHTED_STYLE = {
outerStroke: {
color: "#de425b",
},
text: {
backgroundColor: "#2d2e41",
color: "#fff",
margin: 6,
},
};
export function addStyles(ogma: Ogma<NodeDataType>) {
ogma.styles.addNodeRule(isMovie, {
color: "#FCBC05",
text: {
content: (node) => node.getData("title"),
font,
size: 16.5,
style: "bold",
minVisibleSize: 0,
},
radius: 8,
icon: {
content: getIconCode("icon-film"),
font: "Lucide",
scale: 0.5,
color: "#372b09",
},
});
ogma.styles.addNodeRule(isPerson, {
color: "#4386F3",
text: {
content: (node) => node.getData("name"),
font,
size: 11,
minVisibleSize: 10,
},
icon: {
content: getIconCode("icon-circle-user-round"),
font: "Lucide",
scale: 0.5,
color: "#fff",
},
});
ogma.styles.addNodeRule(isGenre, {
color: "#7843F3",
radius: 4,
text: {
content: (node) => node.getData("name"),
font,
size: 11,
minVisibleSize: 10,
},
icon: {
content: getIconCode("icon-tag"),
font: "Lucide",
scale: 0.5,
color: "#fff",
},
});
ogma.styles.setSelectedNodeAttributes(HIGHLIGHTED_STYLE);
ogma.styles.setHoveredNodeAttributes(HIGHLIGHTED_STYLE);
}
ts
import { Node, RawEdge, RawNode } from "@linkurious/ogma";
import {
ACTED_IN,
DIRECTED,
GENRE,
HAS_GENRE,
MOVIE,
PERSON,
} from "./constants";
// Types for our data model
export interface MovieRow {
Rank: number;
Title: string;
Genre: string;
Description: string;
Director: string;
Actors: string;
Year: number;
"Runtime (Minutes)": number;
Rating: number;
Votes: number;
"Revenue (Millions)": number | null;
}
export interface Movie {
id: string;
title: string;
description: string;
year: number;
runtime: number;
votes: number;
}
export interface Person {
id: string;
name: string;
}
export interface Genre {
id: string;
name: string;
}
export interface DirectedRelationship {
id: string;
personId: string;
movieId: string;
}
export interface ActedInRelationship {
id: string;
personId: string;
movieId: string;
}
export interface HasGenreRelationship {
id: string;
movieId: string;
genreId: string;
}
export enum NodeType {
Person = PERSON,
Movie = MOVIE,
Genre = GENRE,
}
export enum EdgeType {
Directed = DIRECTED,
ActedIn = ACTED_IN,
HasGenre = HAS_GENRE,
}
export const isMovie = (node: Node): node is Node<MovieData> => {
return node.getData("type") === MOVIE;
};
export const isPerson = (node: Node): node is Node<PersonData> => {
return node.getData("type") === PERSON;
};
export const isGenre = (node: Node): node is Node<GenreData> => {
return node.getData("type") === GENRE;
};
export type MovieData = Movie & { type: NodeType.Movie };
export type MovieNode = RawNode<MovieData>;
export type PersonData = Person & { type: NodeType.Person };
export type PersonNode = RawNode<PersonData>;
export type GenreData = Genre & { type: NodeType.Genre };
export type GenreNode = RawNode<GenreData>;
export type DirectedEdge = RawEdge<
DirectedRelationship & { type: EdgeType.Directed }
>;
export type ActedInEdge = RawEdge<
ActedInRelationship & { type: EdgeType.ActedIn }
>;
export type HasGenreEdge = RawEdge<
HasGenreRelationship & { type: EdgeType.HasGenre }
>;
export type GraphNode = MovieNode | PersonNode | GenreNode;
export type GraphEdge = DirectedEdge | ActedInEdge | HasGenreEdge;
export type NodeDataType = PersonData | MovieData | GenreData;
export type EdgeDataType = DirectedEdge | ActedInEdge | HasGenreEdge;
ts
import { defineConfig } from "vite";
export default defineConfig({
server: {
proxy: {
"/api": {
target: "http://localhost:3000", // Replace with your backend server URL
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
});