community: add graphql benchmark

GITHUB_PR_NUMBER: 9401
GITHUB_PR_URL: https://github.com/hasura/graphql-engine/pull/9401

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7672
Co-authored-by: arjunyel <11153289+arjunyel@users.noreply.github.com>
Co-authored-by: Praveen Durairaju <14110316+praveenweb@users.noreply.github.com>
GitOrigin-RevId: 9b0e9a3dd4cee1ef0a76fb45dee3d5e76d404512
This commit is contained in:
hasura-bot 2023-02-27 18:43:27 +05:30
parent 0b72259c1f
commit aaf20b971f
53 changed files with 48241 additions and 0 deletions

View File

@ -0,0 +1,15 @@
# GraphQL Benchmarking
Uses the [Chinook sample database](https://github.com/lerocha/chinook-database). Tested on macOS Ventura 13.1.
## Instructions
1. Checkout [https://github.com/hasura/graphql-bench](https://github.com/hasura/graphql-bench) and build the docker container with `make build_local_docker_image`
1. Run `docker compose -f docker-compose.hasura.yml up -d` to bootstrap Postgres with Chinook.
1. After a few minutes check the Hasura docker logs to see if Hasura is running, which implies the database is now bootstrapped. Run `docker compose -f docker-compose.hasura.yml down`
1. Run the benchmarks `sh benchmark.sh`
1. Open the results on the GraphQL bench website [https://hasura.github.io/graphql-bench/app/web-app/](https://hasura.github.io/graphql-bench/app/web-app/)

View File

@ -0,0 +1,31 @@
#!/bin/sh
docker compose -f docker-compose.hasura.yml up -d --build
echo "Running Hasura Benchmark"
sleep 5
docker run --net=host -v "$PWD":/app/tmp -it \
graphql-bench-local query \
--config="./tmp/config.query.hasura.yaml" \
--outfile="./tmp/report.hasura.json"
docker compose -f docker-compose.hasura.yml down
echo "Hasura Benchmark done"
docker compose -f docker-compose.node.yml up -d --build
echo "Running Nodejs Benchmark"
sleep 5
docker run --net=host -v "$PWD":/app/tmp -it \
graphql-bench-local query \
--config="./tmp/config.query.node.yaml" \
--outfile="./tmp/report.nodejs.json"
docker compose -f docker-compose.node.yml down
echo "Node.js Benchmark done"

View File

@ -0,0 +1,64 @@
url: "http://host.docker.internal:8080/v1/graphql"
# url: https://benchmark-hasura-rso3d2ja7a-uc.a.run.app/v1/graphql
headers:
content-type: application/json
# "Debug" mode enables request and response logging for Autocannon and K6
# This lets you see what is happening and confirm proper behavior.
# This should be disabled for genuine benchmarks, and only used for debugging/visibility.
debug: false
queries:
# Name: Unique name for the query
- name: GetAllArtistsAlbumsAndTracks
# Tools: List of benchmarking tools to run: ['autocannon', 'k6', 'wrk2']
tools: [k6]
execution_strategy: REQUESTS_PER_SECOND
rps: 2000
duration: 10s
connections: 50
query: |
query GetAllArtistsAlbumsTracks_Genres {
Artist {
ArtistId
Name
Albums {
AlbumId
Title
Tracks {
TrackId
Name
Composer
Genre {
GenreId
Name
}
}
}
}
}
- name: AlbumByPK
tools: [k6]
execution_strategy: FIXED_REQUEST_NUMBER
requests: 10000
query: |
query AlbumByPK {
Album_by_pk(AlbumId: 1) {
AlbumId
Title
}
}
- name: AlbumByPKMultiStage
tools: [k6]
execution_strategy: MULTI_STAGE
initial_rps: 0
stages:
- duration: 5s
target: 100
- duration: 5s
target: 1000
query: |
query AlbumByPK {
Album_by_pk(AlbumId: 1) {
AlbumId
Title
}
}

View File

@ -0,0 +1,64 @@
url: "http://host.docker.internal:8080/v1/graphql"
# url: https://benchmark-node-rso3d2ja7a-ul.a.run.app/v1/graphql
headers:
content-type: application/json
# "Debug" mode enables request and response logging for Autocannon and K6
# This lets you see what is happening and confirm proper behavior.
# This should be disabled for genuine benchmarks, and only used for debugging/visibility.
debug: false
queries:
# Name: Unique name for the query
- name: GetAllArtistsAlbumsAndTracks
# Tools: List of benchmarking tools to run: ['autocannon', 'k6', 'wrk2']
tools: [k6]
execution_strategy: REQUESTS_PER_SECOND
rps: 2000
duration: 10s
connections: 10
query: |
query GetAllArtistsAlbumsTracks_Genres {
Artist {
ArtistId
Name
Albums {
AlbumId
Title
Tracks {
TrackId
Name
Composer
Genre {
GenreId
Name
}
}
}
}
}
- name: AlbumByPK
tools: [k6]
execution_strategy: FIXED_REQUEST_NUMBER
requests: 10000
query: |
query AlbumByPK {
Album_by_pk(AlbumId: 1) {
AlbumId
Title
}
}
- name: AlbumByPKMultiStage
tools: [k6]
execution_strategy: MULTI_STAGE
initial_rps: 0
stages:
- duration: 5s
target: 100
- duration: 5s
target: 1000
query: |
query AlbumByPK {
Album_by_pk(AlbumId: 1) {
AlbumId
Title
}
}

View File

@ -0,0 +1,36 @@
services:
postgres:
image: postgres:15
restart: always
volumes:
- db_data:/var/lib/postgresql/data
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: postgrespassword
graphql-engine:
image: hasura/graphql-engine:v2.18.0.cli-migrations-v3
volumes:
- ./hasura/migrations:/hasura-migrations
- ./hasura/metadata:/hasura-metadata
ports:
- "8080:8080"
depends_on:
- "postgres"
restart: always
environment:
## postgres database to store Hasura metadata
HASURA_GRAPHQL_METADATA_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres
## this env var can be used to add the above postgres database to Hasura as a data source. this can be removed/updated based on your needs
PG_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres
## enable the console served by server
HASURA_GRAPHQL_ENABLE_CONSOLE: "true" # set to "false" to disable console
## enable debugging mode. It is recommended to disable this in production
HASURA_GRAPHQL_DEV_MODE: "true"
HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
## uncomment next line to run console offline (i.e load console assets from server instead of CDN)
# HASURA_GRAPHQL_CONSOLE_ASSETS_DIR: /srv/console-assets
## uncomment next line to set an admin secret
# HASURA_GRAPHQL_ADMIN_SECRET: myadminsecretkey
volumes:
db_data:

View File

@ -0,0 +1,18 @@
services:
postgres:
image: postgres:15
restart: always
volumes:
- db_data:/var/lib/postgresql/data
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: postgrespassword
node:
build: ./nodejs
ports:
- "8080:8080"
depends_on:
- "postgres"
volumes:
db_data:

View File

@ -0,0 +1,6 @@
version: 3
endpoint: http://localhost:8080
metadata_directory: metadata
actions:
kind: synchronous
handler_webhook_baseurl: http://localhost:3000

View File

@ -0,0 +1,6 @@
actions: []
custom_types:
enums: []
input_objects: []
objects: []
scalars: []

View File

@ -0,0 +1,9 @@
- name: default
kind: postgres
configuration:
connection_info:
database_url:
from_env: PG_DATABASE_URL
isolation_level: read-committed
use_prepared_statements: false
tables: "!include default/tables/tables.yaml"

View File

@ -0,0 +1,15 @@
table:
name: Album
schema: public
object_relationships:
- name: Artist
using:
foreign_key_constraint_on: ArtistId
array_relationships:
- name: Tracks
using:
foreign_key_constraint_on:
column: AlbumId
table:
name: Track
schema: public

View File

@ -0,0 +1,11 @@
table:
name: Artist
schema: public
array_relationships:
- name: Albums
using:
foreign_key_constraint_on:
column: ArtistId
table:
name: Album
schema: public

View File

@ -0,0 +1,15 @@
table:
name: Customer
schema: public
object_relationships:
- name: Employee
using:
foreign_key_constraint_on: SupportRepId
array_relationships:
- name: Invoices
using:
foreign_key_constraint_on:
column: CustomerId
table:
name: Invoice
schema: public

View File

@ -0,0 +1,22 @@
table:
name: Employee
schema: public
object_relationships:
- name: Employee
using:
foreign_key_constraint_on: ReportsTo
array_relationships:
- name: Customers
using:
foreign_key_constraint_on:
column: SupportRepId
table:
name: Customer
schema: public
- name: Employees
using:
foreign_key_constraint_on:
column: ReportsTo
table:
name: Employee
schema: public

View File

@ -0,0 +1,11 @@
table:
name: Genre
schema: public
array_relationships:
- name: Tracks
using:
foreign_key_constraint_on:
column: GenreId
table:
name: Track
schema: public

View File

@ -0,0 +1,15 @@
table:
name: Invoice
schema: public
object_relationships:
- name: Customer
using:
foreign_key_constraint_on: CustomerId
array_relationships:
- name: InvoiceLines
using:
foreign_key_constraint_on:
column: InvoiceId
table:
name: InvoiceLine
schema: public

View File

@ -0,0 +1,10 @@
table:
name: InvoiceLine
schema: public
object_relationships:
- name: Invoice
using:
foreign_key_constraint_on: InvoiceId
- name: Track
using:
foreign_key_constraint_on: TrackId

View File

@ -0,0 +1,11 @@
table:
name: MediaType
schema: public
array_relationships:
- name: Tracks
using:
foreign_key_constraint_on:
column: MediaTypeId
table:
name: Track
schema: public

View File

@ -0,0 +1,11 @@
table:
name: Playlist
schema: public
array_relationships:
- name: PlaylistTracks
using:
foreign_key_constraint_on:
column: PlaylistId
table:
name: PlaylistTrack
schema: public

View File

@ -0,0 +1,10 @@
table:
name: PlaylistTrack
schema: public
object_relationships:
- name: Playlist
using:
foreign_key_constraint_on: PlaylistId
- name: Track
using:
foreign_key_constraint_on: TrackId

View File

@ -0,0 +1,28 @@
table:
name: Track
schema: public
object_relationships:
- name: Album
using:
foreign_key_constraint_on: AlbumId
- name: Genre
using:
foreign_key_constraint_on: GenreId
- name: MediaType
using:
foreign_key_constraint_on: MediaTypeId
array_relationships:
- name: InvoiceLines
using:
foreign_key_constraint_on:
column: TrackId
table:
name: InvoiceLine
schema: public
- name: PlaylistTracks
using:
foreign_key_constraint_on:
column: TrackId
table:
name: PlaylistTrack
schema: public

View File

@ -0,0 +1,11 @@
- "!include public_Album.yaml"
- "!include public_Artist.yaml"
- "!include public_Customer.yaml"
- "!include public_Employee.yaml"
- "!include public_Genre.yaml"
- "!include public_Invoice.yaml"
- "!include public_InvoiceLine.yaml"
- "!include public_MediaType.yaml"
- "!include public_Playlist.yaml"
- "!include public_PlaylistTrack.yaml"
- "!include public_Track.yaml"

View File

@ -0,0 +1 @@
disabled_for_roles: []

View File

@ -0,0 +1 @@
version: 3

View File

@ -0,0 +1,193 @@
/*******************************************************************************
Chinook Database - Version 1.4
Script: Chinook_PostgreSql.sql
Description: Creates and populates the Chinook database.
DB Server: PostgreSql
Author: Luis Rocha
License: http://www.codeplex.com/ChinookDatabase/license
********************************************************************************/
/*******************************************************************************
Create Tables
********************************************************************************/
CREATE TABLE "Album"
(
"AlbumId" INT NOT NULL,
"Title" VARCHAR(160) NOT NULL,
"ArtistId" INT NOT NULL,
CONSTRAINT "PK_Album" PRIMARY KEY ("AlbumId")
);
CREATE TABLE "Artist"
(
"ArtistId" INT NOT NULL,
"Name" VARCHAR(120),
CONSTRAINT "PK_Artist" PRIMARY KEY ("ArtistId")
);
CREATE TABLE "Customer"
(
"CustomerId" INT NOT NULL,
"FirstName" VARCHAR(40) NOT NULL,
"LastName" VARCHAR(20) NOT NULL,
"Company" VARCHAR(80),
"Address" VARCHAR(70),
"City" VARCHAR(40),
"State" VARCHAR(40),
"Country" VARCHAR(40),
"PostalCode" VARCHAR(10),
"Phone" VARCHAR(24),
"Fax" VARCHAR(24),
"Email" VARCHAR(60) NOT NULL,
"SupportRepId" INT,
CONSTRAINT "PK_Customer" PRIMARY KEY ("CustomerId")
);
CREATE TABLE "Employee"
(
"EmployeeId" INT NOT NULL,
"LastName" VARCHAR(20) NOT NULL,
"FirstName" VARCHAR(20) NOT NULL,
"Title" VARCHAR(30),
"ReportsTo" INT,
"BirthDate" TIMESTAMP,
"HireDate" TIMESTAMP,
"Address" VARCHAR(70),
"City" VARCHAR(40),
"State" VARCHAR(40),
"Country" VARCHAR(40),
"PostalCode" VARCHAR(10),
"Phone" VARCHAR(24),
"Fax" VARCHAR(24),
"Email" VARCHAR(60),
CONSTRAINT "PK_Employee" PRIMARY KEY ("EmployeeId")
);
CREATE TABLE "Genre"
(
"GenreId" INT NOT NULL,
"Name" VARCHAR(120),
CONSTRAINT "PK_Genre" PRIMARY KEY ("GenreId")
);
CREATE TABLE "Invoice"
(
"InvoiceId" INT NOT NULL,
"CustomerId" INT NOT NULL,
"InvoiceDate" TIMESTAMP NOT NULL,
"BillingAddress" VARCHAR(70),
"BillingCity" VARCHAR(40),
"BillingState" VARCHAR(40),
"BillingCountry" VARCHAR(40),
"BillingPostalCode" VARCHAR(10),
"Total" NUMERIC(10,2) NOT NULL,
CONSTRAINT "PK_Invoice" PRIMARY KEY ("InvoiceId")
);
CREATE TABLE "InvoiceLine"
(
"InvoiceLineId" INT NOT NULL,
"InvoiceId" INT NOT NULL,
"TrackId" INT NOT NULL,
"UnitPrice" NUMERIC(10,2) NOT NULL,
"Quantity" INT NOT NULL,
CONSTRAINT "PK_InvoiceLine" PRIMARY KEY ("InvoiceLineId")
);
CREATE TABLE "MediaType"
(
"MediaTypeId" INT NOT NULL,
"Name" VARCHAR(120),
CONSTRAINT "PK_MediaType" PRIMARY KEY ("MediaTypeId")
);
CREATE TABLE "Playlist"
(
"PlaylistId" INT NOT NULL,
"Name" VARCHAR(120),
CONSTRAINT "PK_Playlist" PRIMARY KEY ("PlaylistId")
);
CREATE TABLE "PlaylistTrack"
(
"PlaylistId" INT NOT NULL,
"TrackId" INT NOT NULL,
CONSTRAINT "PK_PlaylistTrack" PRIMARY KEY ("PlaylistId", "TrackId")
);
CREATE TABLE "Track"
(
"TrackId" INT NOT NULL,
"Name" VARCHAR(200) NOT NULL,
"AlbumId" INT,
"MediaTypeId" INT NOT NULL,
"GenreId" INT,
"Composer" VARCHAR(220),
"Milliseconds" INT NOT NULL,
"Bytes" INT,
"UnitPrice" NUMERIC(10,2) NOT NULL,
CONSTRAINT "PK_Track" PRIMARY KEY ("TrackId")
);
/*******************************************************************************
Create Primary Key Unique Indexes
********************************************************************************/
/*******************************************************************************
Create Foreign Keys
********************************************************************************/
ALTER TABLE "Album" ADD CONSTRAINT "FK_AlbumArtistId"
FOREIGN KEY ("ArtistId") REFERENCES "Artist" ("ArtistId") ON DELETE NO ACTION ON UPDATE NO ACTION;
CREATE INDEX "IFK_AlbumArtistId" ON "Album" ("ArtistId");
ALTER TABLE "Customer" ADD CONSTRAINT "FK_CustomerSupportRepId"
FOREIGN KEY ("SupportRepId") REFERENCES "Employee" ("EmployeeId") ON DELETE NO ACTION ON UPDATE NO ACTION;
CREATE INDEX "IFK_CustomerSupportRepId" ON "Customer" ("SupportRepId");
ALTER TABLE "Employee" ADD CONSTRAINT "FK_EmployeeReportsTo"
FOREIGN KEY ("ReportsTo") REFERENCES "Employee" ("EmployeeId") ON DELETE NO ACTION ON UPDATE NO ACTION;
CREATE INDEX "IFK_EmployeeReportsTo" ON "Employee" ("ReportsTo");
ALTER TABLE "Invoice" ADD CONSTRAINT "FK_InvoiceCustomerId"
FOREIGN KEY ("CustomerId") REFERENCES "Customer" ("CustomerId") ON DELETE NO ACTION ON UPDATE NO ACTION;
CREATE INDEX "IFK_InvoiceCustomerId" ON "Invoice" ("CustomerId");
ALTER TABLE "InvoiceLine" ADD CONSTRAINT "FK_InvoiceLineInvoiceId"
FOREIGN KEY ("InvoiceId") REFERENCES "Invoice" ("InvoiceId") ON DELETE NO ACTION ON UPDATE NO ACTION;
CREATE INDEX "IFK_InvoiceLineInvoiceId" ON "InvoiceLine" ("InvoiceId");
ALTER TABLE "InvoiceLine" ADD CONSTRAINT "FK_InvoiceLineTrackId"
FOREIGN KEY ("TrackId") REFERENCES "Track" ("TrackId") ON DELETE NO ACTION ON UPDATE NO ACTION;
CREATE INDEX "IFK_InvoiceLineTrackId" ON "InvoiceLine" ("TrackId");
ALTER TABLE "PlaylistTrack" ADD CONSTRAINT "FK_PlaylistTrackPlaylistId"
FOREIGN KEY ("PlaylistId") REFERENCES "Playlist" ("PlaylistId") ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE "PlaylistTrack" ADD CONSTRAINT "FK_PlaylistTrackTrackId"
FOREIGN KEY ("TrackId") REFERENCES "Track" ("TrackId") ON DELETE NO ACTION ON UPDATE NO ACTION;
CREATE INDEX "IFK_PlaylistTrackTrackId" ON "PlaylistTrack" ("TrackId");
ALTER TABLE "Track" ADD CONSTRAINT "FK_TrackAlbumId"
FOREIGN KEY ("AlbumId") REFERENCES "Album" ("AlbumId") ON DELETE NO ACTION ON UPDATE NO ACTION;
CREATE INDEX "IFK_TrackAlbumId" ON "Track" ("AlbumId");
ALTER TABLE "Track" ADD CONSTRAINT "FK_TrackGenreId"
FOREIGN KEY ("GenreId") REFERENCES "Genre" ("GenreId") ON DELETE NO ACTION ON UPDATE NO ACTION;
CREATE INDEX "IFK_TrackGenreId" ON "Track" ("GenreId");
ALTER TABLE "Track" ADD CONSTRAINT "FK_TrackMediaTypeId"
FOREIGN KEY ("MediaTypeId") REFERENCES "MediaType" ("MediaTypeId") ON DELETE NO ACTION ON UPDATE NO ACTION;
CREATE INDEX "IFK_TrackMediaTypeId" ON "Track" ("MediaTypeId");

View File

@ -0,0 +1 @@
node_modules/

View File

@ -0,0 +1,4 @@
node_modules/
dist/
report.hasura.json
report.node.json

View File

@ -0,0 +1,19 @@
# https://www.andreadiotallevi.com/blog/how-to-create-a-production-image-for-a-node-typescript-app-using-docker-multi-stage-builds
FROM node:18-alpine AS builder
WORKDIR /app
COPY . .
RUN npm ci
RUN npx tsc
FROM node:18-alpine
ENV NODE_ENV=production
RUN apk add --no-cache tini
WORKDIR /app
COPY package.json .
COPY package-lock.json .
RUN npm ci --production
COPY --from=builder ./app/dist ./dist
COPY --from=builder ./app/src/schema.graphql ./dist
EXPOSE 3000
ENTRYPOINT [ "/sbin/tini", "--", "node", "dist/main.js" ]

View File

@ -0,0 +1,14 @@
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
overwrite: true,
schema: "./src/schema.graphql",
generates: {
"src/generated/graphql.ts": {
plugins: ["typescript", "typescript-resolvers"]
}
}
};
export default config;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
{
"devDependencies": {
"@graphql-codegen/cli": "^3.0.0",
"@graphql-codegen/typescript": "^3.0.0",
"@graphql-codegen/typescript-resolvers": "^3.0.0",
"@tsconfig/node18-strictest": "^1.0.0",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
},
"dependencies": {
"@envelop/dataloader": "^4.0.4",
"@escape.tech/graphql-armor": "^1.7.0",
"dataloader": "^2.2.1",
"graphql": "^16.6.0",
"graphql-yoga": "^3.5.1",
"postgres": "^3.3.3"
},
"scripts": {
"codegen": "graphql-codegen --config codegen.ts",
"start": "ts-node ./src/main.ts"
}
}

View File

@ -0,0 +1,24 @@
import sql from "./db";
import type { Album, Artist, Track } from "./generated/graphql";
import { keyByArray } from "./utils";
export async function genericBatchFunction<T extends Album | Artist | Track>(
keys: Readonly<number[]> | Readonly<string[]>,
{ name, id }:
| { name: "Album"; id: "AlbumId" }
| { name: "Album"; id: "ArtistId" }
| { name: "Artist"; id: "ArtistId" }
| { name: "Track"; id: "TrackId" }
| { name: "Track"; id: "AlbumId" }
| { name: "Genre"; id: "GenreId" },
resultsAreArray = false,
) {
// console.log(name, id, keys)
const results = await sql<T[]>`SELECT * FROM ${sql(
name
)} WHERE ${sql(id)} in ${sql(keys)}`;
const resultsMap = keyByArray<T>(results, id as any);
return keys.map((key: typeof keys[number]) => resultsAreArray ? resultsMap[key] : resultsMap[key]![0]);
}

View File

@ -0,0 +1,5 @@
import postgres from 'postgres'
const sql = postgres(process.env["PG_DATABASE_URL"] || 'postgres://postgres:postgrespassword@postgres:5432/postgres', { ssl: 'require' });
export default sql;

View File

@ -0,0 +1,230 @@
import type { GraphQLResolveInfo } from 'graphql';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type RequireFields<T, K extends keyof T> = Omit<T, K> & { [P in K]-?: NonNullable<T[P]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
};
export type Album = {
__typename?: 'Album';
AlbumId: Scalars['Int'];
Artist?: Maybe<Artist>;
ArtistId: Scalars['Int'];
Title: Scalars['String'];
Tracks?: Maybe<Array<Track>>;
};
export type Artist = {
__typename?: 'Artist';
Albums?: Maybe<Array<Album>>;
ArtistId: Scalars['Int'];
Name?: Maybe<Scalars['String']>;
};
export type Genre = {
__typename?: 'Genre';
GenreId: Scalars['Int'];
Name: Scalars['String'];
};
export type Query = {
__typename?: 'Query';
Album: Array<Album>;
Album_by_pk?: Maybe<Album>;
Artist: Array<Artist>;
Artist_by_pk?: Maybe<Artist>;
Genre: Array<Genre>;
Genre_by_pk?: Maybe<Genre>;
Track: Array<Track>;
Track_by_pk?: Maybe<Track>;
};
export type QueryAlbum_By_PkArgs = {
AlbumId: Scalars['Int'];
};
export type QueryArtist_By_PkArgs = {
ArtistId: Scalars['Int'];
};
export type QueryGenre_By_PkArgs = {
GenreId: Scalars['Int'];
};
export type QueryTrack_By_PkArgs = {
TrackId: Scalars['Int'];
};
export type Track = {
__typename?: 'Track';
Album?: Maybe<Album>;
AlbumId?: Maybe<Scalars['Int']>;
Bytes?: Maybe<Scalars['Int']>;
Composer?: Maybe<Scalars['String']>;
Genre?: Maybe<Genre>;
GenreId: Scalars['Int'];
MediaTypeId: Scalars['Int'];
Milliseconds: Scalars['Int'];
Name: Scalars['String'];
TrackId: Scalars['Int'];
};
export type ResolverTypeWrapper<T> = Promise<T> | T;
export type ResolverWithResolve<TResult, TParent, TContext, TArgs> = {
resolve: ResolverFn<TResult, TParent, TContext, TArgs>;
};
export type Resolver<TResult, TParent = {}, TContext = {}, TArgs = {}> = ResolverFn<TResult, TParent, TContext, TArgs> | ResolverWithResolve<TResult, TParent, TContext, TArgs>;
export type ResolverFn<TResult, TParent, TContext, TArgs> = (
parent: TParent,
args: TArgs,
context: TContext,
info: GraphQLResolveInfo
) => Promise<TResult> | TResult;
export type SubscriptionSubscribeFn<TResult, TParent, TContext, TArgs> = (
parent: TParent,
args: TArgs,
context: TContext,
info: GraphQLResolveInfo
) => AsyncIterable<TResult> | Promise<AsyncIterable<TResult>>;
export type SubscriptionResolveFn<TResult, TParent, TContext, TArgs> = (
parent: TParent,
args: TArgs,
context: TContext,
info: GraphQLResolveInfo
) => TResult | Promise<TResult>;
export interface SubscriptionSubscriberObject<TResult, TKey extends string, TParent, TContext, TArgs> {
subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>;
resolve?: SubscriptionResolveFn<TResult, { [key in TKey]: TResult }, TContext, TArgs>;
}
export interface SubscriptionResolverObject<TResult, TParent, TContext, TArgs> {
subscribe: SubscriptionSubscribeFn<any, TParent, TContext, TArgs>;
resolve: SubscriptionResolveFn<TResult, any, TContext, TArgs>;
}
export type SubscriptionObject<TResult, TKey extends string, TParent, TContext, TArgs> =
| SubscriptionSubscriberObject<TResult, TKey, TParent, TContext, TArgs>
| SubscriptionResolverObject<TResult, TParent, TContext, TArgs>;
export type SubscriptionResolver<TResult, TKey extends string, TParent = {}, TContext = {}, TArgs = {}> =
| ((...args: any[]) => SubscriptionObject<TResult, TKey, TParent, TContext, TArgs>)
| SubscriptionObject<TResult, TKey, TParent, TContext, TArgs>;
export type TypeResolveFn<TTypes, TParent = {}, TContext = {}> = (
parent: TParent,
context: TContext,
info: GraphQLResolveInfo
) => Maybe<TTypes> | Promise<Maybe<TTypes>>;
export type IsTypeOfResolverFn<T = {}, TContext = {}> = (obj: T, context: TContext, info: GraphQLResolveInfo) => boolean | Promise<boolean>;
export type NextResolverFn<T> = () => Promise<T>;
export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs = {}> = (
next: NextResolverFn<TResult>,
parent: TParent,
args: TArgs,
context: TContext,
info: GraphQLResolveInfo
) => TResult | Promise<TResult>;
/** Mapping between all available schema types and the resolvers types */
export type ResolversTypes = {
Album: ResolverTypeWrapper<Album>;
Artist: ResolverTypeWrapper<Artist>;
Boolean: ResolverTypeWrapper<Scalars['Boolean']>;
Genre: ResolverTypeWrapper<Genre>;
Int: ResolverTypeWrapper<Scalars['Int']>;
Query: ResolverTypeWrapper<{}>;
String: ResolverTypeWrapper<Scalars['String']>;
Track: ResolverTypeWrapper<Track>;
};
/** Mapping between all available schema types and the resolvers parents */
export type ResolversParentTypes = {
Album: Album;
Artist: Artist;
Boolean: Scalars['Boolean'];
Genre: Genre;
Int: Scalars['Int'];
Query: {};
String: Scalars['String'];
Track: Track;
};
export type AlbumResolvers<ContextType = any, ParentType extends ResolversParentTypes['Album'] = ResolversParentTypes['Album']> = {
AlbumId?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
Artist?: Resolver<Maybe<ResolversTypes['Artist']>, ParentType, ContextType>;
ArtistId?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
Title?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
Tracks?: Resolver<Maybe<Array<ResolversTypes['Track']>>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type ArtistResolvers<ContextType = any, ParentType extends ResolversParentTypes['Artist'] = ResolversParentTypes['Artist']> = {
Albums?: Resolver<Maybe<Array<ResolversTypes['Album']>>, ParentType, ContextType>;
ArtistId?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
Name?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type GenreResolvers<ContextType = any, ParentType extends ResolversParentTypes['Genre'] = ResolversParentTypes['Genre']> = {
GenreId?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
Name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type QueryResolvers<ContextType = any, ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query']> = {
Album?: Resolver<Array<ResolversTypes['Album']>, ParentType, ContextType>;
Album_by_pk?: Resolver<Maybe<ResolversTypes['Album']>, ParentType, ContextType, RequireFields<QueryAlbum_By_PkArgs, 'AlbumId'>>;
Artist?: Resolver<Array<ResolversTypes['Artist']>, ParentType, ContextType>;
Artist_by_pk?: Resolver<Maybe<ResolversTypes['Artist']>, ParentType, ContextType, RequireFields<QueryArtist_By_PkArgs, 'ArtistId'>>;
Genre?: Resolver<Array<ResolversTypes['Genre']>, ParentType, ContextType>;
Genre_by_pk?: Resolver<Maybe<ResolversTypes['Genre']>, ParentType, ContextType, RequireFields<QueryGenre_By_PkArgs, 'GenreId'>>;
Track?: Resolver<Array<ResolversTypes['Track']>, ParentType, ContextType>;
Track_by_pk?: Resolver<Maybe<ResolversTypes['Track']>, ParentType, ContextType, RequireFields<QueryTrack_By_PkArgs, 'TrackId'>>;
};
export type TrackResolvers<ContextType = any, ParentType extends ResolversParentTypes['Track'] = ResolversParentTypes['Track']> = {
Album?: Resolver<Maybe<ResolversTypes['Album']>, ParentType, ContextType>;
AlbumId?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
Bytes?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
Composer?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
Genre?: Resolver<Maybe<ResolversTypes['Genre']>, ParentType, ContextType>;
GenreId?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
MediaTypeId?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
Milliseconds?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
Name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
TrackId?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type Resolvers<ContextType = any> = {
Album?: AlbumResolvers<ContextType>;
Artist?: ArtistResolvers<ContextType>;
Genre?: GenreResolvers<ContextType>;
Query?: QueryResolvers<ContextType>;
Track?: TrackResolvers<ContextType>;
};

View File

@ -0,0 +1,223 @@
import { readFileSync } from "node:fs";
import { createServer } from "node:http";
import { join } from "node:path";
import { createSchema, createYoga } from "graphql-yoga";
import { GraphQLError } from "graphql";
import DataLoader from "dataloader";
import { useDataLoader } from "@envelop/dataloader";
import { EnvelopArmorPlugin } from "@escape.tech/graphql-armor";
import type {
Album,
Artist,
Genre,
Resolvers,
Track,
} from "./generated/graphql";
import sql from "./db";
import { genericBatchFunction } from "./dataloader";
import { keyByArray } from "./utils";
type Context = {
getAlbumsById: DataLoader<string, Album>;
getAllAlbums: DataLoader<string, Album[]>;
getAlbumsByArtistId: DataLoader<string, Album[]>;
getArtistsById: DataLoader<string, Artist>;
getAllArtists: DataLoader<string, Artist[]>;
getTracksById: DataLoader<string, Track>;
getTracksByAlbumId: DataLoader<string, Track[]>;
getAllTracks: DataLoader<string, Track[]>;
getGenreById: DataLoader<string, Genre>;
getAllGenres: DataLoader<string, Genre[]>;
loggedInArtistId?: number;
};
const typeDefs = readFileSync(join(__dirname, "schema.graphql"), "utf8");
const resolvers: Resolvers = {
Query: {
Album_by_pk: async (_parent, args, context: Context, _info) => {
return context.getAlbumsById.load(args.AlbumId.toString());
},
Album: async (_parent, _args, context: Context, _info) => {
const albums = await context.getAllAlbums.load("1");
for (const album of albums) {
context.getAlbumsById.prime(album.AlbumId.toString(), album);
}
const albumsByArtistId = keyByArray(albums, "ArtistId");
for (const [ArtistId, albums] of Object.entries(albumsByArtistId)) {
context.getAlbumsByArtistId.prime(ArtistId, albums);
}
return albums;
},
Artist_by_pk: async (_parent, args, context: Context, _info) => {
return context.getArtistsById.load(args.ArtistId.toString());
},
Artist: async (_parent, _args, context: Context, _info) => {
if (context.loggedInArtistId) {
}
const artists = await context.getAllArtists.load("1");
if (!artists) {
throw new GraphQLError(`Albums not found.`);
}
for (const artist of artists) {
context.getArtistsById.prime(artist.ArtistId.toString(), artist);
}
return artists;
},
Track_by_pk: async (_parent, args, context: Context, _info) => {
return context.getTracksById.load(args.TrackId.toString());
},
Track: async (_parent, _args, context: Context, _info) => {
const tracks = await context.getAllTracks.load("1");
for (const track of tracks) {
context.getTracksById.prime(track.TrackId.toString(), track);
}
const tracksByAlbumId = keyByArray(tracks, "AlbumId");
for (const [AlbumId, tracks] of Object.entries(tracksByAlbumId)) {
context.getTracksByAlbumId.prime(AlbumId, tracks);
}
return tracks;
},
Genre_by_pk: async (_parent, args, context: Context, _info) => {
return context.getGenreById.load(args.GenreId.toString());
},
Genre: async (_parent, _args, context: Context, _info) => {
const genres = await context.getAllGenres.load("1");
if (!genres) {
throw new GraphQLError(`Albums not found.`);
}
for (const genre of genres) {
context.getGenreById.prime(genre.GenreId.toString(), genre);
}
return genres;
},
},
Album: {
async Artist(parent, _args, context: Context, _info) {
return context.getArtistsById.load(parent.ArtistId.toString());
},
async Tracks(parent, _args, context: Context, _info) {
const tracks = await context.getTracksByAlbumId.load(
parent.AlbumId.toString()
);
for (const track of tracks) {
context.getTracksById.prime(track.TrackId.toString(), track);
}
return tracks;
},
},
Artist: {
async Albums(parent, _args, context: Context, _info) {
const albums = await context.getAlbumsByArtistId.load(
parent.ArtistId.toString()
);
if (Array.isArray(albums)) {
for (const album of albums) {
context.getAlbumsById.prime(album.AlbumId.toString(), album);
}
}
return albums || [];
},
},
Track: {
async Album(parent, _args, context: Context, _info) {
return context.getAlbumsById.load(parent.AlbumId!.toString());
},
async Genre(parent, _args, context: Context, _info) {
return context.getGenreById.load(parent.GenreId!.toString());
},
},
};
export const schema = createSchema({
typeDefs,
resolvers,
});
const server = createServer(
// @ts-ignore
createYoga({
graphqlEndpoint: "/v1/graphql",
// @ts-ignore
schema,
plugins: [
EnvelopArmorPlugin(),
useDataLoader("getAlbumsById", (_context: Context) => {
return new DataLoader((keys: Readonly<string[]>) =>
genericBatchFunction(keys, { name: "Album", id: "AlbumId" })
);
}),
useDataLoader("getAllAlbums", (_context: Context) => {
return new DataLoader(async (keys: Readonly<string[]>) => {
const albums = await sql`SELECT * FROM ${sql("Album")}`;
return keys.map((_key) => albums);
});
}),
useDataLoader(
"getAlbumsByArtistId",
(_context: Context) =>
new DataLoader((keys: Readonly<string[]>) =>
genericBatchFunction(keys, { name: "Album", id: "ArtistId" }, true)
)
),
useDataLoader(
"getArtistsById",
(_context: Context) =>
new DataLoader((keys: Readonly<string[]>) =>
genericBatchFunction(keys, { name: "Artist", id: "ArtistId" })
)
),
useDataLoader("getAllArtists", (_context: Context) => {
return new DataLoader(async (keys: Readonly<string[]>) => {
const artists = await sql`SELECT * FROM ${sql("Artist")}`;
return keys.map((_key) => artists);
});
}),
useDataLoader(
"getTracksById",
(_context: Context) =>
new DataLoader((keys: Readonly<string[]>) =>
genericBatchFunction(keys, { name: "Track", id: "TrackId" })
)
),
useDataLoader(
"getTracksByAlbumId",
(_context: Context) =>
new DataLoader((keys: Readonly<string[]>) =>
genericBatchFunction(keys, { name: "Track", id: "AlbumId" }, true)
)
),
useDataLoader(
"getAllTracks",
(_context: Context) =>
new DataLoader(async (keys: Readonly<string[]>) => {
const tracks = await sql`SELECT * FROM ${sql("Track")}`;
return keys.map((_key) => tracks);
})
),
useDataLoader(
"getGenreById",
(_context: Context) =>
new DataLoader((keys: Readonly<string[]>) =>
genericBatchFunction(keys, { name: "Genre", id: "GenreId" })
)
),
useDataLoader(
"getAllGenres",
(_context: Context) =>
new DataLoader(async (keys: Readonly<string[]>) => {
const genres = await sql`SELECT * FROM ${sql("Genre")}`;
return keys.map((_key) => genres);
})
),
],
})
);
server.listen(process.env["PORT"] || 8080, () => {
console.info(
`Server is running on http://localhost:${process.env["PORT"] || 8080
}/v1/graphql`
);
});

View File

@ -0,0 +1,46 @@
schema {
query: Query
}
type Query {
Album_by_pk(AlbumId: Int!): Album
Album: [Album!]!
Artist_by_pk(ArtistId: Int!): Artist
Artist: [Artist!]!
Track_by_pk(TrackId: Int!): Track
Track: [Track!]!
Genre_by_pk(GenreId: Int!): Genre
Genre: [Genre!]!
}
type Album {
AlbumId: Int!
Artist: Artist
ArtistId: Int!
Title: String!
Tracks: [Track!]
}
type Artist {
ArtistId: Int!
Name: String
Albums: [Album!]
}
type Track {
Album: Album
AlbumId: Int
Bytes: Int
Composer: String
TrackId: Int!
MediaTypeId: Int!
Milliseconds: Int!
Name: String!
GenreId: Int!
Genre: Genre
}
type Genre {
GenreId: Int!
Name: String!
}

View File

@ -0,0 +1,9 @@
export function keyByArray<T>(input: T[], id: keyof T) {
return input.reduce((acc, result) => {
const existing = acc[result[id] as string | number];
acc[result[id as keyof T] as string | number] = existing
? [...existing, result]
: [result];
return acc;
}, {} as Record<string | number, T[]>);
}

View File

@ -0,0 +1,7 @@
{
"extends": "@tsconfig/node18-strictest/tsconfig.json",
"include": ["src"],
"compilerOptions": {
"outDir": "dist"
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff