├── .env.example ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── app ├── layout.tsx └── page.tsx ├── helpers └── geoLocation.ts ├── lib └── db.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages └── api │ ├── default.ts │ └── edge-swap-db.ts ├── prisma └── schema.prisma ├── public ├── download.jpeg ├── favicon.ico └── vercel.svg └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # Environment variables declared in this file are automatically made available to Prisma. 2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 3 | 4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 6 | 7 | US_EAST_1_USERNAME="" 8 | US_EAST_1_PASSWORD="" 9 | US_WEST_2_USERNAME="" 10 | US_WEST_2_PASSWORD="" 11 | EU_CENTRAL_1_USERNAME="" 12 | EU_CENTRAL_1_PASSWORD="" 13 | EU_WEST_1_PASSWORD="" 14 | EU_WEST_1_USERNAME="" 15 | EU_WEST_2_USERNAME="" 16 | EU_WEST_2_PASSWORD="" 17 | AP_NORTHEAST_1_PASSWORD="" 18 | AP_NORTHEAST_1_USERNAME="" 19 | AP_SOUTHEAST_2_PASSWORD="" 20 | AP_SOUTHEAST_2_USERNAME="" 21 | AP_SOUTHEAST_1_PASSWORD="" 22 | AP_SOUTHEAST_1_USERNAME="" 23 | AP_SOUTH_1_USERNAME="" 24 | AP_SOUTH_1_PASSWORD="" 25 | SA_EAST_1_USERNAME="" 26 | SA_EAST_1_PASSWORD="" -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .vscode -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .git 2 | .svn 3 | .hg 4 | node_modules 5 | .next 6 | public 7 | design 8 | contracts 9 | .github 10 | *.lock -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "printWidth": 100, 5 | "singleQuote": true, 6 | "tabWidth": 4, 7 | "trailingComma": "all", 8 | "endOfLine": "auto", 9 | "semi": true, 10 | "importOrder": ["", "^[./]"], 11 | "importOrderSortSpecifiers": true 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | export default async function RootLayout({ children }: { children: React.ReactNode }) { 2 | return ( 3 | 4 | 5 | Create Next App 6 | 7 | 8 | 9 | {children} 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { headers } from 'next/headers'; 2 | import { closestDbConnection } from 'lib/db'; 3 | 4 | async function getData() { 5 | const longitude = headers().get('x-vercel-ip-longitude') ?? '0'; 6 | const latitude = headers().get('x-vercel-ip-latitude') ?? '0'; 7 | 8 | const games = await closestDbConnection(longitude, latitude) 9 | .selectFrom('Game') 10 | .selectAll() 11 | .execute(); 12 | 13 | return games; 14 | } 15 | 16 | export default async function Page() { 17 | const games = await getData(); 18 | return ( 19 |
20 | {games.map((game) => ( 21 |
{JSON.stringify(game)}
22 | ))} 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /helpers/geoLocation.ts: -------------------------------------------------------------------------------- 1 | // Calculate the crows distance between two geo locations 2 | export function calcCrow(lat1: number, lon1: number, lat2: number, lon2: number): number { 3 | const R = 6371; // km 4 | const dLat = toRad(lat2 - lat1); 5 | const dLon = toRad(lon2 - lon1); 6 | const lat1Rad = toRad(lat1); 7 | const lat2Rad = toRad(lat2); 8 | 9 | const a = 10 | Math.sin(dLat / 2) * Math.sin(dLat / 2) + 11 | Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1Rad) * Math.cos(lat2Rad); 12 | const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); 13 | const d = R * c; 14 | return d; 15 | } 16 | 17 | // Value to radians 18 | function toRad(value: number): number { 19 | return (value * Math.PI) / 180; 20 | } 21 | -------------------------------------------------------------------------------- /lib/db.ts: -------------------------------------------------------------------------------- 1 | import { Game } from '@prisma/client/edge'; 2 | import { calcCrow } from 'helpers/geoLocation'; 3 | import { Kysely } from 'kysely'; 4 | import { PlanetScaleDialect } from 'kysely-planetscale'; 5 | 6 | interface DbGeoLocation { 7 | latitude: number; 8 | longitude: number; 9 | } 10 | interface DB { 11 | Game: Game; 12 | } 13 | interface ConfigWithGeoLocation { 14 | dbConnection: Kysely; 15 | geoLocation: DbGeoLocation; 16 | } 17 | 18 | const connect = (username: string, password: string): Kysely => { 19 | return new Kysely({ 20 | dialect: new PlanetScaleDialect({ 21 | host: 'aws.connect.psdb.cloud', 22 | username, 23 | password, 24 | }), 25 | }); 26 | }; 27 | 28 | export const usWest2 = connect( 29 | process.env.US_WEST_2_USERNAME as string, 30 | process.env.US_WEST_2_PASSWORD as string, 31 | ); 32 | 33 | export const usEast1 = connect( 34 | process.env.US_EAST_1_USERNAME as string, 35 | process.env.US_EAST_1_PASSWORD as string, 36 | ); 37 | 38 | export const euCentral1 = connect( 39 | process.env.EU_CENTRAL_1_USERNAME as string, 40 | process.env.EU_CENTRAL_1_PASSWORD as string, 41 | ); 42 | 43 | const euWest1 = connect( 44 | process.env.EU_WEST_1_USERNAME as string, 45 | process.env.EU_WEST_1_PASSWORD as string, 46 | ); 47 | 48 | const euWest2 = connect( 49 | process.env.EU_WEST_2_USERNAME as string, 50 | process.env.EU_WEST_2_PASSWORD as string, 51 | ); 52 | 53 | const apNorthEast1 = connect( 54 | process.env.AP_NORTHEAST_1_USERNAME as string, 55 | process.env.AP_NORTHEAST_1_PASSWORD as string, 56 | ); 57 | 58 | const apSouthEast1 = connect( 59 | process.env.AP_SOUTHEAST_1_USERNAME as string, 60 | process.env.AP_SOUTHEAST_1_PASSWORD as string, 61 | ); 62 | 63 | const apSouthEast2 = connect( 64 | process.env.AP_SOUTHEAST_2_USERNAME as string, 65 | process.env.AP_SOUTHEAST_2_PASSWORD as string, 66 | ); 67 | 68 | const apSouth1 = connect( 69 | process.env.AP_SOUTH_1_USERNAME as string, 70 | process.env.AP_SOUTH_1_PASSWORD as string, 71 | ); 72 | 73 | const saEast1 = connect( 74 | process.env.SA_EAST_1_USERNAME as string, 75 | process.env.SA_EAST_1_PASSWORD as string, 76 | ); 77 | 78 | const dbConnectionsWithGeoLocation: ConfigWithGeoLocation[] = [ 79 | { 80 | // Frankfurt 81 | dbConnection: euCentral1, 82 | geoLocation: { 83 | latitude: 50.110924, 84 | longitude: 8.682127, 85 | }, 86 | }, 87 | { 88 | // Dublin 89 | dbConnection: euWest1, 90 | geoLocation: { 91 | latitude: 53.35014, 92 | longitude: -6.266155, 93 | }, 94 | }, 95 | { 96 | // London 97 | dbConnection: euWest2, 98 | geoLocation: { 99 | latitude: 51.507359, 100 | longitude: -0.136439, 101 | }, 102 | }, 103 | { 104 | // Portland, Oregon 105 | dbConnection: usWest2, 106 | geoLocation: { 107 | latitude: 45.523064, 108 | longitude: -122.676483, 109 | }, 110 | }, 111 | { 112 | // Northern Virginia 113 | dbConnection: usEast1, 114 | geoLocation: { 115 | latitude: 37.926868, 116 | longitude: -78.024902, 117 | }, 118 | }, 119 | { 120 | // Tokyo 121 | dbConnection: apNorthEast1, 122 | geoLocation: { 123 | latitude: 35.6762, 124 | longitude: 139.6503, 125 | }, 126 | }, 127 | { 128 | // Singapore 129 | dbConnection: apSouthEast1, 130 | geoLocation: { 131 | longitude: 103.851959, 132 | latitude: 1.29027, 133 | }, 134 | }, 135 | { 136 | // Sydney 137 | dbConnection: apSouthEast2, 138 | geoLocation: { 139 | longitude: 151.2099, 140 | latitude: -33.865143, 141 | }, 142 | }, 143 | { 144 | // Mumbai 145 | dbConnection: apSouth1, 146 | geoLocation: { 147 | longitude: 72.877426, 148 | latitude: 19.07609, 149 | }, 150 | }, 151 | { 152 | // Sao Paulo 153 | dbConnection: saEast1, 154 | geoLocation: { 155 | longitude: -46.62529, 156 | latitude: -23.533773, 157 | }, 158 | }, 159 | ]; 160 | 161 | export const closestDbConnection = ( 162 | longitude: string | number, 163 | latitude: string | number, 164 | ): Kysely => { 165 | let closestConnection = usWest2; 166 | let closestDistance = Number.MAX_SAFE_INTEGER; 167 | 168 | dbConnectionsWithGeoLocation.forEach((config) => { 169 | const distanceBetweenLocationAndConfig = calcCrow( 170 | parseFloat(latitude.toString()), 171 | parseFloat(longitude.toString()), 172 | config.geoLocation.latitude, 173 | config.geoLocation.longitude, 174 | ); 175 | 176 | if (distanceBetweenLocationAndConfig < closestDistance) { 177 | closestConnection = config.dbConnection; 178 | closestDistance = distanceBetweenLocationAndConfig; 179 | } 180 | }); 181 | return closestConnection; 182 | }; 183 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | appDir: true, 5 | runtime: 'experimental-edge', 6 | }, 7 | }; 8 | 9 | module.exports = nextConfig; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "20-questions", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "prebuild": "prisma generate", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@planetscale/database": "^1.4.0", 14 | "@prisma/client": "^4.5.0", 15 | "@vercel/edge": "^0.0.5", 16 | "kysely": "^0.22.0", 17 | "kysely-planetscale": "^1.1.0", 18 | "next": "^13.0.1", 19 | "next-auth": "^4.15.0", 20 | "react": "18.2.0", 21 | "react-dom": "18.2.0" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "18.11.7", 25 | "@types/react": "18.0.23", 26 | "@types/react-dom": "18.0.7", 27 | "eslint": "8.26.0", 28 | "eslint-config-next": "13.0.0", 29 | "prettier": "2.7.1", 30 | "prisma": "^4.5.0", 31 | "typescript": "4.8.4" 32 | }, 33 | "overrides": { 34 | "next-auth": { 35 | "next": "13.0.0" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pages/api/default.ts: -------------------------------------------------------------------------------- 1 | import { usWest2 } from 'lib/db'; 2 | 3 | export const config = { 4 | runtime: 'experimental-edge', 5 | }; 6 | 7 | export default async function handler() { 8 | const games = await usWest2.selectFrom('Game').selectAll().execute(); 9 | 10 | return new Response(JSON.stringify({ games }), { 11 | status: 200, 12 | headers: { 13 | 'content-type': 'application/json;charset=UTF-8', 14 | 'access-control-allow-origin': '*', 15 | }, 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /pages/api/edge-swap-db.ts: -------------------------------------------------------------------------------- 1 | import { geolocation } from '@vercel/edge'; 2 | import { closestDbConnection } from 'lib/db'; 3 | 4 | export const config = { 5 | runtime: 'experimental-edge', 6 | }; 7 | 8 | export default async function handler(req: Request) { 9 | const { longitude, latitude } = geolocation(req); 10 | 11 | const games = await closestDbConnection(longitude ?? '0', latitude ?? '0') 12 | .selectFrom('Game') 13 | .selectAll() 14 | .execute(); 15 | 16 | return new Response(JSON.stringify({ games }), { 17 | status: 200, 18 | headers: { 19 | 'content-type': 'application/json;charset=UTF-8', 20 | 'access-control-allow-origin': '*', 21 | }, 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | previewFeatures = ["referentialIntegrity"] 4 | } 5 | 6 | datasource db { 7 | provider = "mysql" 8 | url = env("DATABASE_URL") 9 | relationMode = "prisma" 10 | } 11 | 12 | model Game { 13 | id String @id @default(cuid()) 14 | createdAt DateTime @default(now()) 15 | updatedAt DateTime @updatedAt 16 | score Float 17 | completed Boolean @default(false) 18 | } 19 | -------------------------------------------------------------------------------- /public/download.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidTParks/typesafe-db-access-next13/27b55c09feb4468ca077388b0526f2c511bb0f0a/public/download.jpeg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidTParks/typesafe-db-access-next13/27b55c09feb4468ca077388b0526f2c511bb0f0a/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "plugins": [ 19 | { 20 | "name": "next" 21 | } 22 | ] 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"], 26 | } 27 | --------------------------------------------------------------------------------