Skip to content

Commit 4574419

Browse files
committed
vercel example
1 parent cac453f commit 4574419

36 files changed

+8307
-2
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Alternatively, you can use it as a library dependence in your Rust/JavaScript/Ty
1313
- [cloudflare-pages-d1](cloudflare-pages-D1) - A example of a cloudflare pages deployment with data storred in D1 (Cloudflare's SQLite compatible edge databse).
1414
- [flyio-sqlite-litefs](flyio-sqlite-litefs) - A example of a Fly.io with data storred in a SQLite database that is replicated between nodes using LiteFS.
1515
- [flyio-postgresql](flyio-postgresql) - A example of a Fly.io with data storred in a PostgreSql database.
16+
- [vercel-postgresql-neon](vercel-postgresql-neon) - A example of a Vercel deployment with data storred in a PostgreSql database hosted by Neon.
1617

1718

1819

flyio-postgresql/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Northwind Traders (by subZero)
2-
### Running on Fly.io + SQLite (LiteFS)
2+
### Running on Fly.io + PostgreSQL
33
This is a demo of the Northwind dataset, see it live at [northwind-postgresql.fly.dev](https://northwind-postgresql.fly.dev).
44
- Frontend is implemented in NextJS</li>
55
- Backend is implemented in Typescript and leverages subZero as a library to automatically expose a PostgREST compatible backend on top of the underlying database

flyio-postgresql/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "flyio-sqlite",
2+
"name": "flyio-postgresql",
33
"version": "0.1.0",
44
"private": true,
55
"scripts": {

vercel-postgresql-neon/.dockerignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Dockerfile
2+
.dockerignore
3+
node_modules
4+
npm-debug.log
5+
README.md
6+
.next
7+
.git
8+
.env.local
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DEV_DATABASE_URL=postgres://postgres:pass@localhost:5432/app

vercel-postgresql-neon/.gitignore

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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+
.state
39+
db.sqlite
40+
fly.toml

vercel-postgresql-neon/README.md

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Northwind Traders (by subZero)
2+
### Running on Vercel + PostgreSQL (Neon)
3+
This is a demo of the Northwind dataset, see it live at [northwind-postgresql.vercel.app/](https://northwind-postgresql.vercel.app/).
4+
- Frontend is implemented in NextJS</li>
5+
- Backend is implemented in Typescript and leverages subZero as a library to automatically expose a PostgREST compatible backend on top of the underlying database
6+
- Data is stored in a PostgreSQL database
7+
- Everything is deployed to [Vercel](https://vercel.com)
8+
9+
This dataset was sourced from [northwind-SQLite3](https://github.com/jpwhite3/northwind-SQLite3)
10+
11+
## Running locally
12+
- Clone the repo
13+
- Install dependencies (you will need to have SQLite installed on your machine)
14+
```bash
15+
yarn install
16+
```
17+
- Copy .env.local file
18+
```bash
19+
cp .env.local.example .env.local
20+
```
21+
- Run in dev mode
22+
```bash
23+
yarn dev
24+
```
25+
- Open the app in your browser
26+
```bash
27+
open http://localhost:3000
28+
```
29+
30+
## Deploying to Vercel
31+
32+
- Setup a PostgreSQL database on [Neon](https://neon.tech) and get a connection string
33+
- Provision the database
34+
```bash
35+
psql <db_connection_string> -f northwindtraders.sql
36+
```
37+
- Link the current directory to a Vercel project
38+
```bash
39+
vercel link
40+
```
41+
- Set the `DATABASE_URL` environment variable
42+
```bash
43+
vercel env add DATABASE_URL <db_connection_string>
44+
```
45+
- Deploy the project
46+
```bash
47+
vercel --prod
48+
```
49+
50+
51+
52+
53+
54+
55+
## Implementation details
56+
57+
Most of the files in this directory are your basic NextJS setup with some configuration for tailwindcss and typescript.
58+
The interesting files are:
59+
60+
- The file implementing [the backend](pages/api/[...path].ts)
61+
Most of the code deals with the configuration of the backend, and 99% of the functionality is withing these lines:
62+
```typescript
63+
// parse the Request object into and internal AST representation
64+
let subzeroRequest = await subzero.parse(publicSchema, `${urlPrefix}/`, role, request)
65+
// .....
66+
// generate the SQL query from the AST representation
67+
const { query, parameters } = subzero.fmt_main_query(subzeroRequest, queryEnv)
68+
// .....
69+
// execute the query
70+
const r = await db.query(query, parameters)
71+
```
72+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import Link from 'next/link';
2+
3+
import { useState, useEffect } from 'react'
4+
5+
type DashboardLayoutProps = {
6+
children: React.ReactNode,
7+
general: { name: string, href: string, icon: any }[],
8+
backoffice: { name: string, href: string, icon: any }[],
9+
};
10+
11+
function classNames(...classes:string[]) {
12+
return classes.filter(Boolean).join(' ')
13+
}
14+
15+
export default function Layout({ general, backoffice, children }: DashboardLayoutProps) {
16+
const [activeMenuItem, setActiveMenuItem] = useState('/')
17+
useEffect(() => setActiveMenuItem(window.location.pathname), [])
18+
return (
19+
<>
20+
<div>
21+
{/* Static sidebar for desktop */}
22+
<div className="hidden md:fixed md:inset-y-0 md:flex md:w-64 md:flex-col">
23+
<div className="flex min-h-0 flex-1 flex-col bg-gray-800">
24+
<div className="flex h-16 flex-shrink-0 items-center bg-gray-900 px-4 text-white">
25+
<b>Northwind</b>&nbsp;Traders
26+
</div>
27+
<div className="flex flex-1 flex-col overflow-y-auto">
28+
<div className='text-gray-300 text-xs pl-5 pt-5 uppercase'>General</div>
29+
<nav className="space-y-1 px-2 py-4">
30+
{general.map((item) => (
31+
<Link href={item.href} key={item.name}>
32+
<a
33+
onClick={() => setActiveMenuItem(item.href)}
34+
className={classNames(
35+
item.href === activeMenuItem ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white',
36+
'group flex items-center px-2 py-2 text-sm font-medium rounded-md'
37+
)}
38+
>
39+
<item.icon
40+
className={classNames(
41+
item.href === activeMenuItem ? 'text-gray-300' : 'text-gray-400 group-hover:text-gray-300',
42+
'mr-3 flex-shrink-0 h-6 w-6'
43+
)}
44+
aria-hidden="true"
45+
/>
46+
{item.name}
47+
</a>
48+
</Link>
49+
))}
50+
</nav>
51+
52+
<div className='text-gray-300 text-xs pl-5 pt-5 uppercase'>Backoffice</div>
53+
<nav className="flex-1 space-y-1 px-2 py-4">
54+
{backoffice.map((item) => (
55+
<Link href={item.href} key={item.name}>
56+
<a
57+
onClick={() => setActiveMenuItem(item.href)}
58+
className={classNames(
59+
item.href === activeMenuItem ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white',
60+
'group flex items-center px-2 py-2 text-sm font-medium rounded-md'
61+
)}
62+
>
63+
<item.icon
64+
className={classNames(
65+
item.href === activeMenuItem ? 'text-gray-300' : 'text-gray-400 group-hover:text-gray-300',
66+
'mr-3 flex-shrink-0 h-6 w-6'
67+
)}
68+
aria-hidden="true"
69+
/>
70+
{item.name}
71+
</a>
72+
</Link>
73+
))}
74+
</nav>
75+
</div>
76+
</div>
77+
</div>
78+
79+
<div className="flex flex-col md:pl-64">
80+
<main className="flex-1">
81+
{children}
82+
</main>
83+
</div>
84+
</div>
85+
</>
86+
)
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/20/solid'
2+
interface PaginationProps {
3+
first: number,
4+
last: number,
5+
total: number,
6+
itemsPerPage: number,
7+
setPage: (page: number) => void,
8+
}
9+
10+
export default function Pagination(props: PaginationProps) {
11+
const { first, last, total, itemsPerPage, setPage } = props
12+
const totalPages = Math.ceil(total / itemsPerPage)
13+
const currentPage = Math.floor(first / itemsPerPage) + 1
14+
15+
function setPageWrapped(e: React.MouseEvent, page: number) {
16+
e.preventDefault()
17+
if (page < 1) setPage(1)
18+
else if (page > totalPages) setPage(totalPages)
19+
else setPage(page)
20+
}
21+
return (
22+
<div className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6">
23+
<div className="flex flex-1 justify-between sm:hidden">
24+
<a
25+
href="#"
26+
className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
27+
onClick={(e) => setPageWrapped(e, currentPage - 1)}
28+
>
29+
Previous
30+
</a>
31+
<a
32+
href="#"
33+
className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
34+
onClick={(e) => setPageWrapped(e, currentPage + 1)}
35+
>
36+
Next
37+
</a>
38+
</div>
39+
<div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
40+
<div>
41+
<p className="text-sm text-gray-700">
42+
Showing <span className="font-medium">{first + 1}</span> to <span className="font-medium">{last + 1}</span> of{' '}
43+
<span className="font-medium">{total}</span> results
44+
</p>
45+
</div>
46+
<div>
47+
<nav className="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
48+
<a
49+
href="#"
50+
className="relative inline-flex items-center rounded-l-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20"
51+
onClick={(e) => setPageWrapped(e, currentPage - 1)}
52+
>
53+
<span className="sr-only">Previous</span>
54+
<ChevronLeftIcon className="h-5 w-5" aria-hidden="true" />
55+
</a>
56+
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
57+
<a key={page}
58+
href="#"
59+
// className="relative inline-flex items-center border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20"
60+
className={`relative inline-flex items-center border px-4 py-2 text-sm font-medium focus:z-20 ${currentPage === page ? 'z-10 bg-indigo-50 border-indigo-500 text-indigo-600' : 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50'}`}
61+
onClick={(e) => setPageWrapped(e, page)}
62+
>
63+
{page}
64+
</a>
65+
))}
66+
67+
{/* Current: "z-10 bg-indigo-50 border-indigo-500 text-indigo-600", Default: "bg-white border-gray-300 text-gray-500 hover:bg-gray-50" */}
68+
{/* <a
69+
href="#"
70+
aria-current="page"
71+
className="relative z-10 inline-flex items-center border border-indigo-500 bg-indigo-50 px-4 py-2 text-sm font-medium text-indigo-600 focus:z-20"
72+
>
73+
1
74+
</a> */}
75+
76+
<a
77+
href="#"
78+
className="relative inline-flex items-center rounded-r-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20"
79+
onClick={(e) => setPageWrapped(e, currentPage + 1)}
80+
>
81+
<span className="sr-only">Next</span>
82+
<ChevronRightIcon className="h-5 w-5" aria-hidden="true" />
83+
</a>
84+
</nav>
85+
</div>
86+
</div>
87+
</div>
88+
)
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import Pagination from '../components/pagination'
2+
interface sProps {
3+
idColumn: string
4+
avatarColumn?: string
5+
columns: { name: string, header:string, fn?: (item: any) => string | JSX.Element }[]
6+
rows: any
7+
limit: number
8+
first: number
9+
last: number
10+
total: number
11+
12+
setPage: (page: number) => void
13+
}
14+
15+
export default function Table(props: sProps) {
16+
17+
const { idColumn, avatarColumn, columns, rows, limit, first, last, total, setPage } = props
18+
19+
return (
20+
<>
21+
<table className="min-w-full divide-y divide-gray-300">
22+
<thead className="bg-gray-50">
23+
<tr>
24+
{avatarColumn && <th scope="col" className={`py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6` }></th>}
25+
{columns.map((column: any) => (
26+
<th key={column.name} scope="col" className={`py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6 ${column.th_style || ''}` }>
27+
{column.header}
28+
</th>
29+
))}
30+
</tr>
31+
</thead>
32+
<tbody className="divide-y divide-gray-200 bg-white">
33+
{rows.map((row:any) => (
34+
<tr key={row[idColumn]}>
35+
{avatarColumn && <td className={`w-full max-w-0 py-2 pl-4 pr-3 text-sm font-medium text-gray-900 sm:w-auto sm:max-w-none sm:pl-6`}>
36+
{/* eslint-disable-next-line @next/next/no-img-element */}
37+
<img className="h-6 w-6 rounded-full" src={`https://avatars.dicebear.com/v2/initials/${row[avatarColumn].replace(' ', '-')}.svg`} alt="" />
38+
</td>}
39+
{columns.map((column: any) => (
40+
<td key={column.name} className={`w-full max-w-0 py-2 pl-4 pr-3 text-sm font-medium text-gray-900 sm:w-auto sm:max-w-none sm:pl-6 ${column.td_style || ''}`}>
41+
{column.fn ? column.fn(row) : row[column.name]}
42+
</td>
43+
))}
44+
</tr>
45+
))}
46+
</tbody>
47+
</table>
48+
<Pagination first={first} last={last} total={total} itemsPerPage={limit} setPage={setPage} />
49+
</>
50+
)
51+
}
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
declare module '*.sql' {
2+
const content: string;
3+
export default content;
4+
}

0 commit comments

Comments
 (0)