├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── bun.lockb ├── docker-compose.yml ├── drizzle.config.ts ├── drizzle ├── 0000_wealthy_zarda.sql ├── 0001_normal_chameleon.sql ├── 0002_parallel_zaran.sql ├── 0003_greedy_rattler.sql ├── 0004_dry_violations.sql ├── 0005_panoramic_valkyrie.sql ├── 0006_peaceful_mysterio.sql └── meta │ ├── 0000_snapshot.json │ ├── 0001_snapshot.json │ ├── 0002_snapshot.json │ ├── 0003_snapshot.json │ ├── 0004_snapshot.json │ ├── 0005_snapshot.json │ ├── 0006_snapshot.json │ └── _journal.json ├── package.json ├── src ├── db │ ├── connection.ts │ ├── migrate.ts │ ├── schema │ │ ├── auth-links.ts │ │ ├── index.ts │ │ ├── order-items.ts │ │ ├── orders.ts │ │ ├── products.ts │ │ ├── restaurantes.ts │ │ └── users.ts │ └── seed.ts ├── env.ts ├── http │ ├── auth.ts │ ├── errors │ │ └── unauthorized-error.ts │ ├── routes │ │ ├── approve-order.ts │ │ ├── authenticate-from-link.ts │ │ ├── cancel-order.ts │ │ ├── deliver-order.ts │ │ ├── dispatch-order.ts │ │ ├── get-daily-receipt-in-period.ts │ │ ├── get-day-orders-amount.ts │ │ ├── get-managed-restaurante.ts │ │ ├── get-month-canceled-orders-amount.ts │ │ ├── get-month-orders-amount.ts │ │ ├── get-month.receipt.ts │ │ ├── get-order-details.ts │ │ ├── get-orders.ts │ │ ├── get-popular-products.ts │ │ ├── get-profile.ts │ │ ├── register-restaurante.ts │ │ ├── send-auth-link.ts │ │ └── sign-out.ts │ └── server.ts └── lib │ └── mail.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | API_BASE_URL="http://localhost:3333" 2 | AUTH_REDIRECT_URL="http://localhost:5173" 3 | DATABASE_URL="postgresql://docker:docker@localhost:5432/pizza_shop" 4 | JWT_SECRET_KEY="jwt-secret-sample" 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@rocketseat/eslint-config/node", 4 | "plugin:drizzle/all" 5 | ], 6 | "plugins": ["drizzle"] 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pizza-shop-api 2 | 3 | To install dependencies: 4 | 5 | ```bash 6 | bun install 7 | ``` 8 | 9 | To run: 10 | 11 | ```bash 12 | bun run index.ts 13 | ``` 14 | 15 | This project was created using `bun init` in bun v1.0.23. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. 16 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocketseat-education/api-com-bun/0a063768d6819bda0552688b30f58365eb1a9c9e/bun.lockb -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | postgres: 5 | image: bitnami/postgresql:latest 6 | ports: 7 | - '5432:5432' 8 | environment: 9 | - POSTGRES_USER=docker 10 | - POSTGRES_PASSWORD=docker 11 | - POSTGRES_DB=pizza_shop 12 | volumes: 13 | - postgres_data:/bitnami/postgresql 14 | 15 | volumes: 16 | postgres_data: 17 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "drizzle-kit"; 2 | import { env } from "./src/env"; 3 | 4 | export default { 5 | schema: "./src/db/schema/index.ts", 6 | out: "./drizzle", 7 | driver: 'pg', 8 | dbCredentials: { 9 | connectionString: env.DATABASE_URL, 10 | } 11 | } satisfies Config; 12 | -------------------------------------------------------------------------------- /drizzle/0000_wealthy_zarda.sql: -------------------------------------------------------------------------------- 1 | DO $$ BEGIN 2 | CREATE TYPE "user_role" AS ENUM('manager', 'customer'); 3 | EXCEPTION 4 | WHEN duplicate_object THEN null; 5 | END $$; 6 | --> statement-breakpoint 7 | CREATE TABLE IF NOT EXISTS "users" ( 8 | "id" text PRIMARY KEY NOT NULL, 9 | "name" text NOT NULL, 10 | "email" text NOT NULL, 11 | "phone" text, 12 | "role" "user_role" DEFAULT 'customer' NOT NULL, 13 | "created_at" timestamp DEFAULT now() NOT NULL, 14 | "updated_at" timestamp DEFAULT now() NOT NULL, 15 | CONSTRAINT "users_email_unique" UNIQUE("email") 16 | ); 17 | -------------------------------------------------------------------------------- /drizzle/0001_normal_chameleon.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "restaurants" ( 2 | "id" text PRIMARY KEY NOT NULL, 3 | "name" text NOT NULL, 4 | "description" text, 5 | "created_at" timestamp DEFAULT now() NOT NULL, 6 | "updated_at" timestamp DEFAULT now() NOT NULL 7 | ); 8 | -------------------------------------------------------------------------------- /drizzle/0002_parallel_zaran.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "restaurants" ADD COLUMN "manager_id" text;--> statement-breakpoint 2 | DO $$ BEGIN 3 | ALTER TABLE "restaurants" ADD CONSTRAINT "restaurants_manager_id_users_id_fk" FOREIGN KEY ("manager_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action; 4 | EXCEPTION 5 | WHEN duplicate_object THEN null; 6 | END $$; 7 | -------------------------------------------------------------------------------- /drizzle/0003_greedy_rattler.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "restaurants" DROP CONSTRAINT "restaurants_manager_id_users_id_fk"; 2 | --> statement-breakpoint 3 | DO $$ BEGIN 4 | ALTER TABLE "restaurants" ADD CONSTRAINT "restaurants_manager_id_users_id_fk" FOREIGN KEY ("manager_id") REFERENCES "users"("id") ON DELETE set null ON UPDATE no action; 5 | EXCEPTION 6 | WHEN duplicate_object THEN null; 7 | END $$; 8 | -------------------------------------------------------------------------------- /drizzle/0004_dry_violations.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "auth_links" ( 2 | "id" text PRIMARY KEY NOT NULL, 3 | "code" text NOT NULL, 4 | "user_id" text NOT NULL, 5 | "created_at" timestamp DEFAULT now(), 6 | CONSTRAINT "auth_links_code_unique" UNIQUE("code") 7 | ); 8 | --> statement-breakpoint 9 | DO $$ BEGIN 10 | ALTER TABLE "auth_links" ADD CONSTRAINT "auth_links_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action; 11 | EXCEPTION 12 | WHEN duplicate_object THEN null; 13 | END $$; 14 | -------------------------------------------------------------------------------- /drizzle/0005_panoramic_valkyrie.sql: -------------------------------------------------------------------------------- 1 | DO $$ BEGIN 2 | CREATE TYPE "order_status" AS ENUM('pending', 'processing', 'delivering', 'delivered', 'canceled'); 3 | EXCEPTION 4 | WHEN duplicate_object THEN null; 5 | END $$; 6 | --> statement-breakpoint 7 | CREATE TABLE IF NOT EXISTS "orders" ( 8 | "id" text PRIMARY KEY NOT NULL, 9 | "customer_id" text, 10 | "restaurant_id" text NOT NULL, 11 | "status" "order_status" DEFAULT 'pending' NOT NULL, 12 | "total_in_cents" integer NOT NULL, 13 | "created_at" timestamp DEFAULT now() NOT NULL 14 | ); 15 | --> statement-breakpoint 16 | CREATE TABLE IF NOT EXISTS "products" ( 17 | "id" text PRIMARY KEY NOT NULL, 18 | "name" text NOT NULL, 19 | "description" text, 20 | "price_in_cents" integer NOT NULL, 21 | "restaurant_id" text NOT NULL, 22 | "created_at" timestamp DEFAULT now() NOT NULL, 23 | "updated_at" timestamp DEFAULT now() NOT NULL 24 | ); 25 | --> statement-breakpoint 26 | CREATE TABLE IF NOT EXISTS "order_items" ( 27 | "id" text PRIMARY KEY NOT NULL, 28 | "order_id" text NOT NULL, 29 | "product_id" text, 30 | "price_in_cents" integer NOT NULL, 31 | "quantity" integer NOT NULL 32 | ); 33 | --> statement-breakpoint 34 | DO $$ BEGIN 35 | ALTER TABLE "orders" ADD CONSTRAINT "orders_customer_id_users_id_fk" FOREIGN KEY ("customer_id") REFERENCES "users"("id") ON DELETE set null ON UPDATE no action; 36 | EXCEPTION 37 | WHEN duplicate_object THEN null; 38 | END $$; 39 | --> statement-breakpoint 40 | DO $$ BEGIN 41 | ALTER TABLE "orders" ADD CONSTRAINT "orders_restaurant_id_restaurants_id_fk" FOREIGN KEY ("restaurant_id") REFERENCES "restaurants"("id") ON DELETE cascade ON UPDATE no action; 42 | EXCEPTION 43 | WHEN duplicate_object THEN null; 44 | END $$; 45 | --> statement-breakpoint 46 | DO $$ BEGIN 47 | ALTER TABLE "products" ADD CONSTRAINT "products_restaurant_id_restaurants_id_fk" FOREIGN KEY ("restaurant_id") REFERENCES "restaurants"("id") ON DELETE cascade ON UPDATE no action; 48 | EXCEPTION 49 | WHEN duplicate_object THEN null; 50 | END $$; 51 | --> statement-breakpoint 52 | DO $$ BEGIN 53 | ALTER TABLE "order_items" ADD CONSTRAINT "order_items_order_id_orders_id_fk" FOREIGN KEY ("order_id") REFERENCES "orders"("id") ON DELETE cascade ON UPDATE no action; 54 | EXCEPTION 55 | WHEN duplicate_object THEN null; 56 | END $$; 57 | --> statement-breakpoint 58 | DO $$ BEGIN 59 | ALTER TABLE "order_items" ADD CONSTRAINT "order_items_product_id_products_id_fk" FOREIGN KEY ("product_id") REFERENCES "products"("id") ON DELETE set default ON UPDATE no action; 60 | EXCEPTION 61 | WHEN duplicate_object THEN null; 62 | END $$; 63 | -------------------------------------------------------------------------------- /drizzle/0006_peaceful_mysterio.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "auth_links" DROP CONSTRAINT "auth_links_user_id_users_id_fk"; 2 | --> statement-breakpoint 3 | DO $$ BEGIN 4 | ALTER TABLE "auth_links" ADD CONSTRAINT "auth_links_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action; 5 | EXCEPTION 6 | WHEN duplicate_object THEN null; 7 | END $$; 8 | -------------------------------------------------------------------------------- /drizzle/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "df169d09-ae91-4d67-ace1-6c7cd45880e1", 3 | "prevId": "00000000-0000-0000-0000-000000000000", 4 | "version": "5", 5 | "dialect": "pg", 6 | "tables": { 7 | "users": { 8 | "name": "users", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "text", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "name": { 18 | "name": "name", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "email": { 24 | "name": "email", 25 | "type": "text", 26 | "primaryKey": false, 27 | "notNull": true 28 | }, 29 | "phone": { 30 | "name": "phone", 31 | "type": "text", 32 | "primaryKey": false, 33 | "notNull": false 34 | }, 35 | "role": { 36 | "name": "role", 37 | "type": "user_role", 38 | "primaryKey": false, 39 | "notNull": true, 40 | "default": "'customer'" 41 | }, 42 | "created_at": { 43 | "name": "created_at", 44 | "type": "timestamp", 45 | "primaryKey": false, 46 | "notNull": true, 47 | "default": "now()" 48 | }, 49 | "updated_at": { 50 | "name": "updated_at", 51 | "type": "timestamp", 52 | "primaryKey": false, 53 | "notNull": true, 54 | "default": "now()" 55 | } 56 | }, 57 | "indexes": {}, 58 | "foreignKeys": {}, 59 | "compositePrimaryKeys": {}, 60 | "uniqueConstraints": { 61 | "users_email_unique": { 62 | "name": "users_email_unique", 63 | "nullsNotDistinct": false, 64 | "columns": [ 65 | "email" 66 | ] 67 | } 68 | } 69 | } 70 | }, 71 | "enums": { 72 | "user_role": { 73 | "name": "user_role", 74 | "values": { 75 | "manager": "manager", 76 | "customer": "customer" 77 | } 78 | } 79 | }, 80 | "schemas": {}, 81 | "_meta": { 82 | "columns": {}, 83 | "schemas": {}, 84 | "tables": {} 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /drizzle/meta/0001_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "a50609c6-df59-427f-81c4-5cf3c3b9e39e", 3 | "prevId": "df169d09-ae91-4d67-ace1-6c7cd45880e1", 4 | "version": "5", 5 | "dialect": "pg", 6 | "tables": { 7 | "users": { 8 | "name": "users", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "text", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "name": { 18 | "name": "name", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "email": { 24 | "name": "email", 25 | "type": "text", 26 | "primaryKey": false, 27 | "notNull": true 28 | }, 29 | "phone": { 30 | "name": "phone", 31 | "type": "text", 32 | "primaryKey": false, 33 | "notNull": false 34 | }, 35 | "role": { 36 | "name": "role", 37 | "type": "user_role", 38 | "primaryKey": false, 39 | "notNull": true, 40 | "default": "'customer'" 41 | }, 42 | "created_at": { 43 | "name": "created_at", 44 | "type": "timestamp", 45 | "primaryKey": false, 46 | "notNull": true, 47 | "default": "now()" 48 | }, 49 | "updated_at": { 50 | "name": "updated_at", 51 | "type": "timestamp", 52 | "primaryKey": false, 53 | "notNull": true, 54 | "default": "now()" 55 | } 56 | }, 57 | "indexes": {}, 58 | "foreignKeys": {}, 59 | "compositePrimaryKeys": {}, 60 | "uniqueConstraints": { 61 | "users_email_unique": { 62 | "name": "users_email_unique", 63 | "nullsNotDistinct": false, 64 | "columns": [ 65 | "email" 66 | ] 67 | } 68 | } 69 | }, 70 | "restaurants": { 71 | "name": "restaurants", 72 | "schema": "", 73 | "columns": { 74 | "id": { 75 | "name": "id", 76 | "type": "text", 77 | "primaryKey": true, 78 | "notNull": true 79 | }, 80 | "name": { 81 | "name": "name", 82 | "type": "text", 83 | "primaryKey": false, 84 | "notNull": true 85 | }, 86 | "description": { 87 | "name": "description", 88 | "type": "text", 89 | "primaryKey": false, 90 | "notNull": false 91 | }, 92 | "created_at": { 93 | "name": "created_at", 94 | "type": "timestamp", 95 | "primaryKey": false, 96 | "notNull": true, 97 | "default": "now()" 98 | }, 99 | "updated_at": { 100 | "name": "updated_at", 101 | "type": "timestamp", 102 | "primaryKey": false, 103 | "notNull": true, 104 | "default": "now()" 105 | } 106 | }, 107 | "indexes": {}, 108 | "foreignKeys": {}, 109 | "compositePrimaryKeys": {}, 110 | "uniqueConstraints": {} 111 | } 112 | }, 113 | "enums": { 114 | "user_role": { 115 | "name": "user_role", 116 | "values": { 117 | "manager": "manager", 118 | "customer": "customer" 119 | } 120 | } 121 | }, 122 | "schemas": {}, 123 | "_meta": { 124 | "columns": {}, 125 | "schemas": {}, 126 | "tables": {} 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /drizzle/meta/0002_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "d360e3af-6536-4631-97c1-62545dac8762", 3 | "prevId": "a50609c6-df59-427f-81c4-5cf3c3b9e39e", 4 | "version": "5", 5 | "dialect": "pg", 6 | "tables": { 7 | "users": { 8 | "name": "users", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "text", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "name": { 18 | "name": "name", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "email": { 24 | "name": "email", 25 | "type": "text", 26 | "primaryKey": false, 27 | "notNull": true 28 | }, 29 | "phone": { 30 | "name": "phone", 31 | "type": "text", 32 | "primaryKey": false, 33 | "notNull": false 34 | }, 35 | "role": { 36 | "name": "role", 37 | "type": "user_role", 38 | "primaryKey": false, 39 | "notNull": true, 40 | "default": "'customer'" 41 | }, 42 | "created_at": { 43 | "name": "created_at", 44 | "type": "timestamp", 45 | "primaryKey": false, 46 | "notNull": true, 47 | "default": "now()" 48 | }, 49 | "updated_at": { 50 | "name": "updated_at", 51 | "type": "timestamp", 52 | "primaryKey": false, 53 | "notNull": true, 54 | "default": "now()" 55 | } 56 | }, 57 | "indexes": {}, 58 | "foreignKeys": {}, 59 | "compositePrimaryKeys": {}, 60 | "uniqueConstraints": { 61 | "users_email_unique": { 62 | "name": "users_email_unique", 63 | "nullsNotDistinct": false, 64 | "columns": [ 65 | "email" 66 | ] 67 | } 68 | } 69 | }, 70 | "restaurants": { 71 | "name": "restaurants", 72 | "schema": "", 73 | "columns": { 74 | "id": { 75 | "name": "id", 76 | "type": "text", 77 | "primaryKey": true, 78 | "notNull": true 79 | }, 80 | "name": { 81 | "name": "name", 82 | "type": "text", 83 | "primaryKey": false, 84 | "notNull": true 85 | }, 86 | "description": { 87 | "name": "description", 88 | "type": "text", 89 | "primaryKey": false, 90 | "notNull": false 91 | }, 92 | "manager_id": { 93 | "name": "manager_id", 94 | "type": "text", 95 | "primaryKey": false, 96 | "notNull": false 97 | }, 98 | "created_at": { 99 | "name": "created_at", 100 | "type": "timestamp", 101 | "primaryKey": false, 102 | "notNull": true, 103 | "default": "now()" 104 | }, 105 | "updated_at": { 106 | "name": "updated_at", 107 | "type": "timestamp", 108 | "primaryKey": false, 109 | "notNull": true, 110 | "default": "now()" 111 | } 112 | }, 113 | "indexes": {}, 114 | "foreignKeys": { 115 | "restaurants_manager_id_users_id_fk": { 116 | "name": "restaurants_manager_id_users_id_fk", 117 | "tableFrom": "restaurants", 118 | "tableTo": "users", 119 | "columnsFrom": [ 120 | "manager_id" 121 | ], 122 | "columnsTo": [ 123 | "id" 124 | ], 125 | "onDelete": "no action", 126 | "onUpdate": "no action" 127 | } 128 | }, 129 | "compositePrimaryKeys": {}, 130 | "uniqueConstraints": {} 131 | } 132 | }, 133 | "enums": { 134 | "user_role": { 135 | "name": "user_role", 136 | "values": { 137 | "manager": "manager", 138 | "customer": "customer" 139 | } 140 | } 141 | }, 142 | "schemas": {}, 143 | "_meta": { 144 | "columns": {}, 145 | "schemas": {}, 146 | "tables": {} 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /drizzle/meta/0003_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "9579b41a-f679-4621-b1e9-fe225bf2f1ae", 3 | "prevId": "d360e3af-6536-4631-97c1-62545dac8762", 4 | "version": "5", 5 | "dialect": "pg", 6 | "tables": { 7 | "users": { 8 | "name": "users", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "text", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "name": { 18 | "name": "name", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "email": { 24 | "name": "email", 25 | "type": "text", 26 | "primaryKey": false, 27 | "notNull": true 28 | }, 29 | "phone": { 30 | "name": "phone", 31 | "type": "text", 32 | "primaryKey": false, 33 | "notNull": false 34 | }, 35 | "role": { 36 | "name": "role", 37 | "type": "user_role", 38 | "primaryKey": false, 39 | "notNull": true, 40 | "default": "'customer'" 41 | }, 42 | "created_at": { 43 | "name": "created_at", 44 | "type": "timestamp", 45 | "primaryKey": false, 46 | "notNull": true, 47 | "default": "now()" 48 | }, 49 | "updated_at": { 50 | "name": "updated_at", 51 | "type": "timestamp", 52 | "primaryKey": false, 53 | "notNull": true, 54 | "default": "now()" 55 | } 56 | }, 57 | "indexes": {}, 58 | "foreignKeys": {}, 59 | "compositePrimaryKeys": {}, 60 | "uniqueConstraints": { 61 | "users_email_unique": { 62 | "name": "users_email_unique", 63 | "nullsNotDistinct": false, 64 | "columns": [ 65 | "email" 66 | ] 67 | } 68 | } 69 | }, 70 | "restaurants": { 71 | "name": "restaurants", 72 | "schema": "", 73 | "columns": { 74 | "id": { 75 | "name": "id", 76 | "type": "text", 77 | "primaryKey": true, 78 | "notNull": true 79 | }, 80 | "name": { 81 | "name": "name", 82 | "type": "text", 83 | "primaryKey": false, 84 | "notNull": true 85 | }, 86 | "description": { 87 | "name": "description", 88 | "type": "text", 89 | "primaryKey": false, 90 | "notNull": false 91 | }, 92 | "manager_id": { 93 | "name": "manager_id", 94 | "type": "text", 95 | "primaryKey": false, 96 | "notNull": false 97 | }, 98 | "created_at": { 99 | "name": "created_at", 100 | "type": "timestamp", 101 | "primaryKey": false, 102 | "notNull": true, 103 | "default": "now()" 104 | }, 105 | "updated_at": { 106 | "name": "updated_at", 107 | "type": "timestamp", 108 | "primaryKey": false, 109 | "notNull": true, 110 | "default": "now()" 111 | } 112 | }, 113 | "indexes": {}, 114 | "foreignKeys": { 115 | "restaurants_manager_id_users_id_fk": { 116 | "name": "restaurants_manager_id_users_id_fk", 117 | "tableFrom": "restaurants", 118 | "tableTo": "users", 119 | "columnsFrom": [ 120 | "manager_id" 121 | ], 122 | "columnsTo": [ 123 | "id" 124 | ], 125 | "onDelete": "set null", 126 | "onUpdate": "no action" 127 | } 128 | }, 129 | "compositePrimaryKeys": {}, 130 | "uniqueConstraints": {} 131 | } 132 | }, 133 | "enums": { 134 | "user_role": { 135 | "name": "user_role", 136 | "values": { 137 | "manager": "manager", 138 | "customer": "customer" 139 | } 140 | } 141 | }, 142 | "schemas": {}, 143 | "_meta": { 144 | "columns": {}, 145 | "schemas": {}, 146 | "tables": {} 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /drizzle/meta/0004_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "65dc9811-0699-4a28-87a0-495c6cf7fb1a", 3 | "prevId": "9579b41a-f679-4621-b1e9-fe225bf2f1ae", 4 | "version": "5", 5 | "dialect": "pg", 6 | "tables": { 7 | "users": { 8 | "name": "users", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "text", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "name": { 18 | "name": "name", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "email": { 24 | "name": "email", 25 | "type": "text", 26 | "primaryKey": false, 27 | "notNull": true 28 | }, 29 | "phone": { 30 | "name": "phone", 31 | "type": "text", 32 | "primaryKey": false, 33 | "notNull": false 34 | }, 35 | "role": { 36 | "name": "role", 37 | "type": "user_role", 38 | "primaryKey": false, 39 | "notNull": true, 40 | "default": "'customer'" 41 | }, 42 | "created_at": { 43 | "name": "created_at", 44 | "type": "timestamp", 45 | "primaryKey": false, 46 | "notNull": true, 47 | "default": "now()" 48 | }, 49 | "updated_at": { 50 | "name": "updated_at", 51 | "type": "timestamp", 52 | "primaryKey": false, 53 | "notNull": true, 54 | "default": "now()" 55 | } 56 | }, 57 | "indexes": {}, 58 | "foreignKeys": {}, 59 | "compositePrimaryKeys": {}, 60 | "uniqueConstraints": { 61 | "users_email_unique": { 62 | "name": "users_email_unique", 63 | "nullsNotDistinct": false, 64 | "columns": [ 65 | "email" 66 | ] 67 | } 68 | } 69 | }, 70 | "restaurants": { 71 | "name": "restaurants", 72 | "schema": "", 73 | "columns": { 74 | "id": { 75 | "name": "id", 76 | "type": "text", 77 | "primaryKey": true, 78 | "notNull": true 79 | }, 80 | "name": { 81 | "name": "name", 82 | "type": "text", 83 | "primaryKey": false, 84 | "notNull": true 85 | }, 86 | "description": { 87 | "name": "description", 88 | "type": "text", 89 | "primaryKey": false, 90 | "notNull": false 91 | }, 92 | "manager_id": { 93 | "name": "manager_id", 94 | "type": "text", 95 | "primaryKey": false, 96 | "notNull": false 97 | }, 98 | "created_at": { 99 | "name": "created_at", 100 | "type": "timestamp", 101 | "primaryKey": false, 102 | "notNull": true, 103 | "default": "now()" 104 | }, 105 | "updated_at": { 106 | "name": "updated_at", 107 | "type": "timestamp", 108 | "primaryKey": false, 109 | "notNull": true, 110 | "default": "now()" 111 | } 112 | }, 113 | "indexes": {}, 114 | "foreignKeys": { 115 | "restaurants_manager_id_users_id_fk": { 116 | "name": "restaurants_manager_id_users_id_fk", 117 | "tableFrom": "restaurants", 118 | "tableTo": "users", 119 | "columnsFrom": [ 120 | "manager_id" 121 | ], 122 | "columnsTo": [ 123 | "id" 124 | ], 125 | "onDelete": "set null", 126 | "onUpdate": "no action" 127 | } 128 | }, 129 | "compositePrimaryKeys": {}, 130 | "uniqueConstraints": {} 131 | }, 132 | "auth_links": { 133 | "name": "auth_links", 134 | "schema": "", 135 | "columns": { 136 | "id": { 137 | "name": "id", 138 | "type": "text", 139 | "primaryKey": true, 140 | "notNull": true 141 | }, 142 | "code": { 143 | "name": "code", 144 | "type": "text", 145 | "primaryKey": false, 146 | "notNull": true 147 | }, 148 | "user_id": { 149 | "name": "user_id", 150 | "type": "text", 151 | "primaryKey": false, 152 | "notNull": true 153 | }, 154 | "created_at": { 155 | "name": "created_at", 156 | "type": "timestamp", 157 | "primaryKey": false, 158 | "notNull": false, 159 | "default": "now()" 160 | } 161 | }, 162 | "indexes": {}, 163 | "foreignKeys": { 164 | "auth_links_user_id_users_id_fk": { 165 | "name": "auth_links_user_id_users_id_fk", 166 | "tableFrom": "auth_links", 167 | "tableTo": "users", 168 | "columnsFrom": [ 169 | "user_id" 170 | ], 171 | "columnsTo": [ 172 | "id" 173 | ], 174 | "onDelete": "no action", 175 | "onUpdate": "no action" 176 | } 177 | }, 178 | "compositePrimaryKeys": {}, 179 | "uniqueConstraints": { 180 | "auth_links_code_unique": { 181 | "name": "auth_links_code_unique", 182 | "nullsNotDistinct": false, 183 | "columns": [ 184 | "code" 185 | ] 186 | } 187 | } 188 | } 189 | }, 190 | "enums": { 191 | "user_role": { 192 | "name": "user_role", 193 | "values": { 194 | "manager": "manager", 195 | "customer": "customer" 196 | } 197 | } 198 | }, 199 | "schemas": {}, 200 | "_meta": { 201 | "columns": {}, 202 | "schemas": {}, 203 | "tables": {} 204 | } 205 | } -------------------------------------------------------------------------------- /drizzle/meta/0005_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "3865595b-0f7a-4c74-86d9-89717a412ed3", 3 | "prevId": "65dc9811-0699-4a28-87a0-495c6cf7fb1a", 4 | "version": "5", 5 | "dialect": "pg", 6 | "tables": { 7 | "users": { 8 | "name": "users", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "text", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "name": { 18 | "name": "name", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "email": { 24 | "name": "email", 25 | "type": "text", 26 | "primaryKey": false, 27 | "notNull": true 28 | }, 29 | "phone": { 30 | "name": "phone", 31 | "type": "text", 32 | "primaryKey": false, 33 | "notNull": false 34 | }, 35 | "role": { 36 | "name": "role", 37 | "type": "user_role", 38 | "primaryKey": false, 39 | "notNull": true, 40 | "default": "'customer'" 41 | }, 42 | "created_at": { 43 | "name": "created_at", 44 | "type": "timestamp", 45 | "primaryKey": false, 46 | "notNull": true, 47 | "default": "now()" 48 | }, 49 | "updated_at": { 50 | "name": "updated_at", 51 | "type": "timestamp", 52 | "primaryKey": false, 53 | "notNull": true, 54 | "default": "now()" 55 | } 56 | }, 57 | "indexes": {}, 58 | "foreignKeys": {}, 59 | "compositePrimaryKeys": {}, 60 | "uniqueConstraints": { 61 | "users_email_unique": { 62 | "name": "users_email_unique", 63 | "nullsNotDistinct": false, 64 | "columns": [ 65 | "email" 66 | ] 67 | } 68 | } 69 | }, 70 | "restaurants": { 71 | "name": "restaurants", 72 | "schema": "", 73 | "columns": { 74 | "id": { 75 | "name": "id", 76 | "type": "text", 77 | "primaryKey": true, 78 | "notNull": true 79 | }, 80 | "name": { 81 | "name": "name", 82 | "type": "text", 83 | "primaryKey": false, 84 | "notNull": true 85 | }, 86 | "description": { 87 | "name": "description", 88 | "type": "text", 89 | "primaryKey": false, 90 | "notNull": false 91 | }, 92 | "manager_id": { 93 | "name": "manager_id", 94 | "type": "text", 95 | "primaryKey": false, 96 | "notNull": false 97 | }, 98 | "created_at": { 99 | "name": "created_at", 100 | "type": "timestamp", 101 | "primaryKey": false, 102 | "notNull": true, 103 | "default": "now()" 104 | }, 105 | "updated_at": { 106 | "name": "updated_at", 107 | "type": "timestamp", 108 | "primaryKey": false, 109 | "notNull": true, 110 | "default": "now()" 111 | } 112 | }, 113 | "indexes": {}, 114 | "foreignKeys": { 115 | "restaurants_manager_id_users_id_fk": { 116 | "name": "restaurants_manager_id_users_id_fk", 117 | "tableFrom": "restaurants", 118 | "tableTo": "users", 119 | "columnsFrom": [ 120 | "manager_id" 121 | ], 122 | "columnsTo": [ 123 | "id" 124 | ], 125 | "onDelete": "set null", 126 | "onUpdate": "no action" 127 | } 128 | }, 129 | "compositePrimaryKeys": {}, 130 | "uniqueConstraints": {} 131 | }, 132 | "auth_links": { 133 | "name": "auth_links", 134 | "schema": "", 135 | "columns": { 136 | "id": { 137 | "name": "id", 138 | "type": "text", 139 | "primaryKey": true, 140 | "notNull": true 141 | }, 142 | "code": { 143 | "name": "code", 144 | "type": "text", 145 | "primaryKey": false, 146 | "notNull": true 147 | }, 148 | "user_id": { 149 | "name": "user_id", 150 | "type": "text", 151 | "primaryKey": false, 152 | "notNull": true 153 | }, 154 | "created_at": { 155 | "name": "created_at", 156 | "type": "timestamp", 157 | "primaryKey": false, 158 | "notNull": false, 159 | "default": "now()" 160 | } 161 | }, 162 | "indexes": {}, 163 | "foreignKeys": { 164 | "auth_links_user_id_users_id_fk": { 165 | "name": "auth_links_user_id_users_id_fk", 166 | "tableFrom": "auth_links", 167 | "tableTo": "users", 168 | "columnsFrom": [ 169 | "user_id" 170 | ], 171 | "columnsTo": [ 172 | "id" 173 | ], 174 | "onDelete": "no action", 175 | "onUpdate": "no action" 176 | } 177 | }, 178 | "compositePrimaryKeys": {}, 179 | "uniqueConstraints": { 180 | "auth_links_code_unique": { 181 | "name": "auth_links_code_unique", 182 | "nullsNotDistinct": false, 183 | "columns": [ 184 | "code" 185 | ] 186 | } 187 | } 188 | }, 189 | "orders": { 190 | "name": "orders", 191 | "schema": "", 192 | "columns": { 193 | "id": { 194 | "name": "id", 195 | "type": "text", 196 | "primaryKey": true, 197 | "notNull": true 198 | }, 199 | "customer_id": { 200 | "name": "customer_id", 201 | "type": "text", 202 | "primaryKey": false, 203 | "notNull": false 204 | }, 205 | "restaurant_id": { 206 | "name": "restaurant_id", 207 | "type": "text", 208 | "primaryKey": false, 209 | "notNull": true 210 | }, 211 | "status": { 212 | "name": "status", 213 | "type": "order_status", 214 | "primaryKey": false, 215 | "notNull": true, 216 | "default": "'pending'" 217 | }, 218 | "total_in_cents": { 219 | "name": "total_in_cents", 220 | "type": "integer", 221 | "primaryKey": false, 222 | "notNull": true 223 | }, 224 | "created_at": { 225 | "name": "created_at", 226 | "type": "timestamp", 227 | "primaryKey": false, 228 | "notNull": true, 229 | "default": "now()" 230 | } 231 | }, 232 | "indexes": {}, 233 | "foreignKeys": { 234 | "orders_customer_id_users_id_fk": { 235 | "name": "orders_customer_id_users_id_fk", 236 | "tableFrom": "orders", 237 | "tableTo": "users", 238 | "columnsFrom": [ 239 | "customer_id" 240 | ], 241 | "columnsTo": [ 242 | "id" 243 | ], 244 | "onDelete": "set null", 245 | "onUpdate": "no action" 246 | }, 247 | "orders_restaurant_id_restaurants_id_fk": { 248 | "name": "orders_restaurant_id_restaurants_id_fk", 249 | "tableFrom": "orders", 250 | "tableTo": "restaurants", 251 | "columnsFrom": [ 252 | "restaurant_id" 253 | ], 254 | "columnsTo": [ 255 | "id" 256 | ], 257 | "onDelete": "cascade", 258 | "onUpdate": "no action" 259 | } 260 | }, 261 | "compositePrimaryKeys": {}, 262 | "uniqueConstraints": {} 263 | }, 264 | "products": { 265 | "name": "products", 266 | "schema": "", 267 | "columns": { 268 | "id": { 269 | "name": "id", 270 | "type": "text", 271 | "primaryKey": true, 272 | "notNull": true 273 | }, 274 | "name": { 275 | "name": "name", 276 | "type": "text", 277 | "primaryKey": false, 278 | "notNull": true 279 | }, 280 | "description": { 281 | "name": "description", 282 | "type": "text", 283 | "primaryKey": false, 284 | "notNull": false 285 | }, 286 | "price_in_cents": { 287 | "name": "price_in_cents", 288 | "type": "integer", 289 | "primaryKey": false, 290 | "notNull": true 291 | }, 292 | "restaurant_id": { 293 | "name": "restaurant_id", 294 | "type": "text", 295 | "primaryKey": false, 296 | "notNull": true 297 | }, 298 | "created_at": { 299 | "name": "created_at", 300 | "type": "timestamp", 301 | "primaryKey": false, 302 | "notNull": true, 303 | "default": "now()" 304 | }, 305 | "updated_at": { 306 | "name": "updated_at", 307 | "type": "timestamp", 308 | "primaryKey": false, 309 | "notNull": true, 310 | "default": "now()" 311 | } 312 | }, 313 | "indexes": {}, 314 | "foreignKeys": { 315 | "products_restaurant_id_restaurants_id_fk": { 316 | "name": "products_restaurant_id_restaurants_id_fk", 317 | "tableFrom": "products", 318 | "tableTo": "restaurants", 319 | "columnsFrom": [ 320 | "restaurant_id" 321 | ], 322 | "columnsTo": [ 323 | "id" 324 | ], 325 | "onDelete": "cascade", 326 | "onUpdate": "no action" 327 | } 328 | }, 329 | "compositePrimaryKeys": {}, 330 | "uniqueConstraints": {} 331 | }, 332 | "order_items": { 333 | "name": "order_items", 334 | "schema": "", 335 | "columns": { 336 | "id": { 337 | "name": "id", 338 | "type": "text", 339 | "primaryKey": true, 340 | "notNull": true 341 | }, 342 | "order_id": { 343 | "name": "order_id", 344 | "type": "text", 345 | "primaryKey": false, 346 | "notNull": true 347 | }, 348 | "product_id": { 349 | "name": "product_id", 350 | "type": "text", 351 | "primaryKey": false, 352 | "notNull": false 353 | }, 354 | "price_in_cents": { 355 | "name": "price_in_cents", 356 | "type": "integer", 357 | "primaryKey": false, 358 | "notNull": true 359 | }, 360 | "quantity": { 361 | "name": "quantity", 362 | "type": "integer", 363 | "primaryKey": false, 364 | "notNull": true 365 | } 366 | }, 367 | "indexes": {}, 368 | "foreignKeys": { 369 | "order_items_order_id_orders_id_fk": { 370 | "name": "order_items_order_id_orders_id_fk", 371 | "tableFrom": "order_items", 372 | "tableTo": "orders", 373 | "columnsFrom": [ 374 | "order_id" 375 | ], 376 | "columnsTo": [ 377 | "id" 378 | ], 379 | "onDelete": "cascade", 380 | "onUpdate": "no action" 381 | }, 382 | "order_items_product_id_products_id_fk": { 383 | "name": "order_items_product_id_products_id_fk", 384 | "tableFrom": "order_items", 385 | "tableTo": "products", 386 | "columnsFrom": [ 387 | "product_id" 388 | ], 389 | "columnsTo": [ 390 | "id" 391 | ], 392 | "onDelete": "set default", 393 | "onUpdate": "no action" 394 | } 395 | }, 396 | "compositePrimaryKeys": {}, 397 | "uniqueConstraints": {} 398 | } 399 | }, 400 | "enums": { 401 | "user_role": { 402 | "name": "user_role", 403 | "values": { 404 | "manager": "manager", 405 | "customer": "customer" 406 | } 407 | }, 408 | "order_status": { 409 | "name": "order_status", 410 | "values": { 411 | "pending": "pending", 412 | "processing": "processing", 413 | "delivering": "delivering", 414 | "delivered": "delivered", 415 | "canceled": "canceled" 416 | } 417 | } 418 | }, 419 | "schemas": {}, 420 | "_meta": { 421 | "columns": {}, 422 | "schemas": {}, 423 | "tables": {} 424 | } 425 | } -------------------------------------------------------------------------------- /drizzle/meta/0006_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "3c096bde-05bf-45e9-9754-852d45098032", 3 | "prevId": "3865595b-0f7a-4c74-86d9-89717a412ed3", 4 | "version": "5", 5 | "dialect": "pg", 6 | "tables": { 7 | "users": { 8 | "name": "users", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "text", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "name": { 18 | "name": "name", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "email": { 24 | "name": "email", 25 | "type": "text", 26 | "primaryKey": false, 27 | "notNull": true 28 | }, 29 | "phone": { 30 | "name": "phone", 31 | "type": "text", 32 | "primaryKey": false, 33 | "notNull": false 34 | }, 35 | "role": { 36 | "name": "role", 37 | "type": "user_role", 38 | "primaryKey": false, 39 | "notNull": true, 40 | "default": "'customer'" 41 | }, 42 | "created_at": { 43 | "name": "created_at", 44 | "type": "timestamp", 45 | "primaryKey": false, 46 | "notNull": true, 47 | "default": "now()" 48 | }, 49 | "updated_at": { 50 | "name": "updated_at", 51 | "type": "timestamp", 52 | "primaryKey": false, 53 | "notNull": true, 54 | "default": "now()" 55 | } 56 | }, 57 | "indexes": {}, 58 | "foreignKeys": {}, 59 | "compositePrimaryKeys": {}, 60 | "uniqueConstraints": { 61 | "users_email_unique": { 62 | "name": "users_email_unique", 63 | "nullsNotDistinct": false, 64 | "columns": [ 65 | "email" 66 | ] 67 | } 68 | } 69 | }, 70 | "restaurants": { 71 | "name": "restaurants", 72 | "schema": "", 73 | "columns": { 74 | "id": { 75 | "name": "id", 76 | "type": "text", 77 | "primaryKey": true, 78 | "notNull": true 79 | }, 80 | "name": { 81 | "name": "name", 82 | "type": "text", 83 | "primaryKey": false, 84 | "notNull": true 85 | }, 86 | "description": { 87 | "name": "description", 88 | "type": "text", 89 | "primaryKey": false, 90 | "notNull": false 91 | }, 92 | "manager_id": { 93 | "name": "manager_id", 94 | "type": "text", 95 | "primaryKey": false, 96 | "notNull": false 97 | }, 98 | "created_at": { 99 | "name": "created_at", 100 | "type": "timestamp", 101 | "primaryKey": false, 102 | "notNull": true, 103 | "default": "now()" 104 | }, 105 | "updated_at": { 106 | "name": "updated_at", 107 | "type": "timestamp", 108 | "primaryKey": false, 109 | "notNull": true, 110 | "default": "now()" 111 | } 112 | }, 113 | "indexes": {}, 114 | "foreignKeys": { 115 | "restaurants_manager_id_users_id_fk": { 116 | "name": "restaurants_manager_id_users_id_fk", 117 | "tableFrom": "restaurants", 118 | "tableTo": "users", 119 | "columnsFrom": [ 120 | "manager_id" 121 | ], 122 | "columnsTo": [ 123 | "id" 124 | ], 125 | "onDelete": "set null", 126 | "onUpdate": "no action" 127 | } 128 | }, 129 | "compositePrimaryKeys": {}, 130 | "uniqueConstraints": {} 131 | }, 132 | "auth_links": { 133 | "name": "auth_links", 134 | "schema": "", 135 | "columns": { 136 | "id": { 137 | "name": "id", 138 | "type": "text", 139 | "primaryKey": true, 140 | "notNull": true 141 | }, 142 | "code": { 143 | "name": "code", 144 | "type": "text", 145 | "primaryKey": false, 146 | "notNull": true 147 | }, 148 | "user_id": { 149 | "name": "user_id", 150 | "type": "text", 151 | "primaryKey": false, 152 | "notNull": true 153 | }, 154 | "created_at": { 155 | "name": "created_at", 156 | "type": "timestamp", 157 | "primaryKey": false, 158 | "notNull": false, 159 | "default": "now()" 160 | } 161 | }, 162 | "indexes": {}, 163 | "foreignKeys": { 164 | "auth_links_user_id_users_id_fk": { 165 | "name": "auth_links_user_id_users_id_fk", 166 | "tableFrom": "auth_links", 167 | "tableTo": "users", 168 | "columnsFrom": [ 169 | "user_id" 170 | ], 171 | "columnsTo": [ 172 | "id" 173 | ], 174 | "onDelete": "cascade", 175 | "onUpdate": "no action" 176 | } 177 | }, 178 | "compositePrimaryKeys": {}, 179 | "uniqueConstraints": { 180 | "auth_links_code_unique": { 181 | "name": "auth_links_code_unique", 182 | "nullsNotDistinct": false, 183 | "columns": [ 184 | "code" 185 | ] 186 | } 187 | } 188 | }, 189 | "orders": { 190 | "name": "orders", 191 | "schema": "", 192 | "columns": { 193 | "id": { 194 | "name": "id", 195 | "type": "text", 196 | "primaryKey": true, 197 | "notNull": true 198 | }, 199 | "customer_id": { 200 | "name": "customer_id", 201 | "type": "text", 202 | "primaryKey": false, 203 | "notNull": false 204 | }, 205 | "restaurant_id": { 206 | "name": "restaurant_id", 207 | "type": "text", 208 | "primaryKey": false, 209 | "notNull": true 210 | }, 211 | "status": { 212 | "name": "status", 213 | "type": "order_status", 214 | "primaryKey": false, 215 | "notNull": true, 216 | "default": "'pending'" 217 | }, 218 | "total_in_cents": { 219 | "name": "total_in_cents", 220 | "type": "integer", 221 | "primaryKey": false, 222 | "notNull": true 223 | }, 224 | "created_at": { 225 | "name": "created_at", 226 | "type": "timestamp", 227 | "primaryKey": false, 228 | "notNull": true, 229 | "default": "now()" 230 | } 231 | }, 232 | "indexes": {}, 233 | "foreignKeys": { 234 | "orders_customer_id_users_id_fk": { 235 | "name": "orders_customer_id_users_id_fk", 236 | "tableFrom": "orders", 237 | "tableTo": "users", 238 | "columnsFrom": [ 239 | "customer_id" 240 | ], 241 | "columnsTo": [ 242 | "id" 243 | ], 244 | "onDelete": "set null", 245 | "onUpdate": "no action" 246 | }, 247 | "orders_restaurant_id_restaurants_id_fk": { 248 | "name": "orders_restaurant_id_restaurants_id_fk", 249 | "tableFrom": "orders", 250 | "tableTo": "restaurants", 251 | "columnsFrom": [ 252 | "restaurant_id" 253 | ], 254 | "columnsTo": [ 255 | "id" 256 | ], 257 | "onDelete": "cascade", 258 | "onUpdate": "no action" 259 | } 260 | }, 261 | "compositePrimaryKeys": {}, 262 | "uniqueConstraints": {} 263 | }, 264 | "products": { 265 | "name": "products", 266 | "schema": "", 267 | "columns": { 268 | "id": { 269 | "name": "id", 270 | "type": "text", 271 | "primaryKey": true, 272 | "notNull": true 273 | }, 274 | "name": { 275 | "name": "name", 276 | "type": "text", 277 | "primaryKey": false, 278 | "notNull": true 279 | }, 280 | "description": { 281 | "name": "description", 282 | "type": "text", 283 | "primaryKey": false, 284 | "notNull": false 285 | }, 286 | "price_in_cents": { 287 | "name": "price_in_cents", 288 | "type": "integer", 289 | "primaryKey": false, 290 | "notNull": true 291 | }, 292 | "restaurant_id": { 293 | "name": "restaurant_id", 294 | "type": "text", 295 | "primaryKey": false, 296 | "notNull": true 297 | }, 298 | "created_at": { 299 | "name": "created_at", 300 | "type": "timestamp", 301 | "primaryKey": false, 302 | "notNull": true, 303 | "default": "now()" 304 | }, 305 | "updated_at": { 306 | "name": "updated_at", 307 | "type": "timestamp", 308 | "primaryKey": false, 309 | "notNull": true, 310 | "default": "now()" 311 | } 312 | }, 313 | "indexes": {}, 314 | "foreignKeys": { 315 | "products_restaurant_id_restaurants_id_fk": { 316 | "name": "products_restaurant_id_restaurants_id_fk", 317 | "tableFrom": "products", 318 | "tableTo": "restaurants", 319 | "columnsFrom": [ 320 | "restaurant_id" 321 | ], 322 | "columnsTo": [ 323 | "id" 324 | ], 325 | "onDelete": "cascade", 326 | "onUpdate": "no action" 327 | } 328 | }, 329 | "compositePrimaryKeys": {}, 330 | "uniqueConstraints": {} 331 | }, 332 | "order_items": { 333 | "name": "order_items", 334 | "schema": "", 335 | "columns": { 336 | "id": { 337 | "name": "id", 338 | "type": "text", 339 | "primaryKey": true, 340 | "notNull": true 341 | }, 342 | "order_id": { 343 | "name": "order_id", 344 | "type": "text", 345 | "primaryKey": false, 346 | "notNull": true 347 | }, 348 | "product_id": { 349 | "name": "product_id", 350 | "type": "text", 351 | "primaryKey": false, 352 | "notNull": false 353 | }, 354 | "price_in_cents": { 355 | "name": "price_in_cents", 356 | "type": "integer", 357 | "primaryKey": false, 358 | "notNull": true 359 | }, 360 | "quantity": { 361 | "name": "quantity", 362 | "type": "integer", 363 | "primaryKey": false, 364 | "notNull": true 365 | } 366 | }, 367 | "indexes": {}, 368 | "foreignKeys": { 369 | "order_items_order_id_orders_id_fk": { 370 | "name": "order_items_order_id_orders_id_fk", 371 | "tableFrom": "order_items", 372 | "tableTo": "orders", 373 | "columnsFrom": [ 374 | "order_id" 375 | ], 376 | "columnsTo": [ 377 | "id" 378 | ], 379 | "onDelete": "cascade", 380 | "onUpdate": "no action" 381 | }, 382 | "order_items_product_id_products_id_fk": { 383 | "name": "order_items_product_id_products_id_fk", 384 | "tableFrom": "order_items", 385 | "tableTo": "products", 386 | "columnsFrom": [ 387 | "product_id" 388 | ], 389 | "columnsTo": [ 390 | "id" 391 | ], 392 | "onDelete": "set default", 393 | "onUpdate": "no action" 394 | } 395 | }, 396 | "compositePrimaryKeys": {}, 397 | "uniqueConstraints": {} 398 | } 399 | }, 400 | "enums": { 401 | "user_role": { 402 | "name": "user_role", 403 | "values": { 404 | "manager": "manager", 405 | "customer": "customer" 406 | } 407 | }, 408 | "order_status": { 409 | "name": "order_status", 410 | "values": { 411 | "pending": "pending", 412 | "processing": "processing", 413 | "delivering": "delivering", 414 | "delivered": "delivered", 415 | "canceled": "canceled" 416 | } 417 | } 418 | }, 419 | "schemas": {}, 420 | "_meta": { 421 | "columns": {}, 422 | "schemas": {}, 423 | "tables": {} 424 | } 425 | } -------------------------------------------------------------------------------- /drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "dialect": "pg", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "5", 8 | "when": 1705608528613, 9 | "tag": "0000_wealthy_zarda", 10 | "breakpoints": true 11 | }, 12 | { 13 | "idx": 1, 14 | "version": "5", 15 | "when": 1705608640711, 16 | "tag": "0001_normal_chameleon", 17 | "breakpoints": true 18 | }, 19 | { 20 | "idx": 2, 21 | "version": "5", 22 | "when": 1705608754112, 23 | "tag": "0002_parallel_zaran", 24 | "breakpoints": true 25 | }, 26 | { 27 | "idx": 3, 28 | "version": "5", 29 | "when": 1705608802221, 30 | "tag": "0003_greedy_rattler", 31 | "breakpoints": true 32 | }, 33 | { 34 | "idx": 4, 35 | "version": "5", 36 | "when": 1708016259063, 37 | "tag": "0004_dry_violations", 38 | "breakpoints": true 39 | }, 40 | { 41 | "idx": 5, 42 | "version": "5", 43 | "when": 1708533840515, 44 | "tag": "0005_panoramic_valkyrie", 45 | "breakpoints": true 46 | }, 47 | { 48 | "idx": 6, 49 | "version": "5", 50 | "when": 1708535288348, 51 | "tag": "0006_peaceful_mysterio", 52 | "breakpoints": true 53 | } 54 | ] 55 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pizza-shop-api", 3 | "module": "index.ts", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "bun --watch src/http/server.ts", 7 | "build": "bun build src/http/server.ts", 8 | "start": "NODE_ENV=production bun src/http/server.ts", 9 | "test": "bun test", 10 | "generate": "drizzle-kit generate:pg", 11 | "studio": "drizzle-kit studio", 12 | "migrate": "bun ./src/db/migrate.ts", 13 | "seed": "bun ./src/db/seed.ts", 14 | "lint": "eslint --fix src --ext ts" 15 | }, 16 | "dependencies": { 17 | "@elysiajs/cookie": "^0.8.0", 18 | "@elysiajs/jwt": "^0.8.0", 19 | "@paralleldrive/cuid2": "^2.2.2", 20 | "chalk": "^5.3.0", 21 | "dayjs": "^1.11.10", 22 | "drizzle-orm": "^0.29.3", 23 | "drizzle-typebox": "^0.1.1", 24 | "elysia": "^0.8.9", 25 | "nodemailer": "^6.9.9", 26 | "postgres": "^3.4.3", 27 | "zod": "^3.22.4" 28 | }, 29 | "devDependencies": { 30 | "@faker-js/faker": "^8.3.1", 31 | "@rocketseat/eslint-config": "^2.1.0", 32 | "@types/bun": "latest", 33 | "@types/nodemailer": "^6.4.14", 34 | "drizzle-kit": "^0.20.13", 35 | "eslint": "^8.56.0", 36 | "eslint-plugin-drizzle": "^0.2.3", 37 | "pg": "^8.11.3", 38 | "typescript": "^5.3.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/db/connection.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from 'drizzle-orm/postgres-js' 2 | import postgres from 'postgres' 3 | import { env } from '../env' 4 | import * as schema from './schema' 5 | 6 | const connection = postgres(env.DATABASE_URL) 7 | 8 | export const db = drizzle(connection, { schema }) 9 | -------------------------------------------------------------------------------- /src/db/migrate.ts: -------------------------------------------------------------------------------- 1 | import postgres from 'postgres' 2 | import chalk from 'chalk' 3 | import { env } from '../env' 4 | 5 | import { drizzle } from 'drizzle-orm/postgres-js' 6 | import { migrate } from 'drizzle-orm/postgres-js/migrator' 7 | 8 | const connection = postgres(env.DATABASE_URL, { max: 1 }) 9 | const db = drizzle(connection) 10 | 11 | await migrate(db, { migrationsFolder: 'drizzle' }) 12 | 13 | console.log(chalk.greenBright('Migrations applied successfully!')) 14 | 15 | await connection.end() 16 | 17 | process.exit() 18 | -------------------------------------------------------------------------------- /src/db/schema/auth-links.ts: -------------------------------------------------------------------------------- 1 | import { pgTable, text, timestamp } from 'drizzle-orm/pg-core' 2 | import { createId } from '@paralleldrive/cuid2' 3 | import { users } from '.' 4 | 5 | export const authLinks = pgTable('auth_links', { 6 | id: text('id') 7 | .$defaultFn(() => createId()) 8 | .primaryKey(), 9 | code: text('code').notNull().unique(), 10 | userId: text('user_id') 11 | .references(() => users.id, { onDelete: 'cascade' }) 12 | .notNull(), 13 | createdAt: timestamp('created_at').defaultNow(), 14 | }) 15 | -------------------------------------------------------------------------------- /src/db/schema/index.ts: -------------------------------------------------------------------------------- 1 | export * from './users' 2 | export * from './restaurantes' 3 | export * from './auth-links' 4 | export * from './orders' 5 | export * from './products' 6 | export * from './order-items' 7 | -------------------------------------------------------------------------------- /src/db/schema/order-items.ts: -------------------------------------------------------------------------------- 1 | import { integer, pgTable, text } from 'drizzle-orm/pg-core' 2 | import { createId } from '@paralleldrive/cuid2' 3 | import { orders, products } from '.' 4 | import { relations } from 'drizzle-orm' 5 | 6 | export const orderItems = pgTable('order_items', { 7 | id: text('id') 8 | .$defaultFn(() => createId()) 9 | .primaryKey(), 10 | orderId: text('order_id') 11 | .notNull() 12 | .references(() => orders.id, { 13 | onDelete: 'cascade', 14 | }), 15 | productId: text('product_id').references(() => products.id, { 16 | onDelete: 'set default', 17 | }), 18 | priceInCents: integer('price_in_cents').notNull(), 19 | quantity: integer('quantity').notNull(), 20 | }) 21 | 22 | export const orderItemsRelations = relations(orderItems, ({ one }) => { 23 | return { 24 | order: one(orders, { 25 | fields: [orderItems.orderId], 26 | references: [orders.id], 27 | relationName: 'order_item_order', 28 | }), 29 | product: one(products, { 30 | fields: [orderItems.productId], 31 | references: [products.id], 32 | relationName: 'order_item_product', 33 | }), 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /src/db/schema/orders.ts: -------------------------------------------------------------------------------- 1 | import { integer, pgEnum, pgTable, text, timestamp } from 'drizzle-orm/pg-core' 2 | import { createId } from '@paralleldrive/cuid2' 3 | import { orderItems, restaurants, users } from '.' 4 | import { relations } from 'drizzle-orm' 5 | 6 | export const orderStatusEnum = pgEnum('order_status', [ 7 | 'pending', 8 | 'processing', 9 | 'delivering', 10 | 'delivered', 11 | 'canceled', 12 | ]) 13 | 14 | export const orders = pgTable('orders', { 15 | id: text('id') 16 | .$defaultFn(() => createId()) 17 | .primaryKey(), 18 | customerId: text('customer_id').references(() => users.id, { 19 | onDelete: 'set null', 20 | }), 21 | restaurantId: text('restaurant_id') 22 | .notNull() 23 | .references(() => restaurants.id, { 24 | onDelete: 'cascade', 25 | }), 26 | status: orderStatusEnum('status').default('pending').notNull(), 27 | totalInCents: integer('total_in_cents').notNull(), 28 | createdAt: timestamp('created_at').notNull().defaultNow(), 29 | }) 30 | 31 | export const ordersRelations = relations(orders, ({ one, many }) => { 32 | return { 33 | customer: one(users, { 34 | fields: [orders.customerId], 35 | references: [users.id], 36 | relationName: 'order_customer', 37 | }), 38 | restaurant: one(restaurants, { 39 | fields: [orders.restaurantId], 40 | references: [restaurants.id], 41 | relationName: 'order_restaurant', 42 | }), 43 | orderItems: many(orderItems), 44 | } 45 | }) 46 | -------------------------------------------------------------------------------- /src/db/schema/products.ts: -------------------------------------------------------------------------------- 1 | import { integer, pgTable, text, timestamp } from 'drizzle-orm/pg-core' 2 | import { createId } from '@paralleldrive/cuid2' 3 | import { orderItems, restaurants } from '.' 4 | import { relations } from 'drizzle-orm' 5 | 6 | export const products = pgTable('products', { 7 | id: text('id') 8 | .$defaultFn(() => createId()) 9 | .primaryKey(), 10 | name: text('name').notNull(), 11 | description: text('description'), 12 | priceInCents: integer('price_in_cents').notNull(), 13 | restaurantId: text('restaurant_id') 14 | .notNull() 15 | .references(() => restaurants.id, { 16 | onDelete: 'cascade', 17 | }), 18 | createdAt: timestamp('created_at').notNull().defaultNow(), 19 | updatedAt: timestamp('updated_at').notNull().defaultNow(), 20 | }) 21 | 22 | export const productsRelations = relations(products, ({ one, many }) => { 23 | return { 24 | restaurant: one(restaurants, { 25 | fields: [products.restaurantId], 26 | references: [restaurants.id], 27 | relationName: 'product_restaurant', 28 | }), 29 | orderItems: many(orderItems), 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /src/db/schema/restaurantes.ts: -------------------------------------------------------------------------------- 1 | import { pgTable, text, timestamp } from 'drizzle-orm/pg-core' 2 | import { createId } from '@paralleldrive/cuid2' 3 | import { users } from './users' 4 | import { relations } from 'drizzle-orm' 5 | import { orders, products } from '.' 6 | 7 | export const restaurants = pgTable('restaurants', { 8 | id: text('id') 9 | .$defaultFn(() => createId()) 10 | .primaryKey(), 11 | name: text('name').notNull(), 12 | description: text('description'), 13 | managerId: text('manager_id').references(() => users.id, { 14 | onDelete: 'set null', 15 | }), 16 | createdAt: timestamp('created_at').notNull().defaultNow(), 17 | updatedAt: timestamp('updated_at').notNull().defaultNow(), 18 | }) 19 | 20 | export const restaurantsRelations = relations(restaurants, ({ one, many }) => { 21 | return { 22 | manager: one(users, { 23 | fields: [restaurants.managerId], 24 | references: [users.id], 25 | relationName: 'restaurant_manager', 26 | }), 27 | orders: many(orders), 28 | products: many(products), 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /src/db/schema/users.ts: -------------------------------------------------------------------------------- 1 | import { pgEnum, pgTable, text, timestamp } from 'drizzle-orm/pg-core' 2 | import { createId } from '@paralleldrive/cuid2' 3 | import { relations } from 'drizzle-orm' 4 | import { orders, restaurants } from '.' 5 | 6 | export const userRoleEnum = pgEnum('user_role', ['manager', 'customer']) 7 | 8 | export const users = pgTable('users', { 9 | id: text('id') 10 | .$defaultFn(() => createId()) 11 | .primaryKey(), 12 | name: text('name').notNull(), 13 | email: text('email').notNull().unique(), 14 | phone: text('phone'), 15 | role: userRoleEnum('role').default('customer').notNull(), 16 | createdAt: timestamp('created_at').notNull().defaultNow(), 17 | updatedAt: timestamp('updated_at').notNull().defaultNow(), 18 | }) 19 | 20 | export const usersRelations = relations(users, ({ one, many }) => { 21 | return { 22 | managedRestaurant: one(restaurants, { 23 | fields: [users.id], 24 | references: [restaurants.managerId], 25 | relationName: 'managed_restaurant', 26 | }), 27 | orders: many(orders), 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /src/db/seed.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable drizzle/enforce-delete-with-where */ 2 | 3 | import { faker } from '@faker-js/faker' 4 | import { 5 | authLinks, 6 | orderItems, 7 | orders, 8 | products, 9 | restaurants, 10 | users, 11 | } from './schema' 12 | import { db } from './connection' 13 | import chalk from 'chalk' 14 | import { createId } from '@paralleldrive/cuid2' 15 | 16 | /** 17 | * Reset database 18 | */ 19 | await db.delete(users) 20 | await db.delete(restaurants) 21 | await db.delete(orderItems) 22 | await db.delete(orders) 23 | await db.delete(products) 24 | await db.delete(authLinks) 25 | 26 | console.log(chalk.yellowBright('✔️ Database reset!')) 27 | 28 | /** 29 | * Create customers 30 | */ 31 | const [customer1, customer2] = await db 32 | .insert(users) 33 | .values([ 34 | { 35 | name: faker.person.fullName(), 36 | email: faker.internet.email(), 37 | role: 'customer', 38 | }, 39 | { 40 | name: faker.person.fullName(), 41 | email: faker.internet.email(), 42 | role: 'customer', 43 | }, 44 | ]) 45 | .returning() 46 | 47 | console.log(chalk.yellowBright('✔️ Created customers!')) 48 | 49 | /** 50 | * Create manager 51 | */ 52 | const [manager] = await db 53 | .insert(users) 54 | .values([ 55 | { 56 | name: faker.person.fullName(), 57 | email: 'admin@admin.com', 58 | role: 'manager', 59 | }, 60 | ]) 61 | .returning({ 62 | id: users.id, 63 | }) 64 | 65 | console.log(chalk.yellowBright('✔️ Created manager!')) 66 | 67 | /** 68 | * Create restaurant 69 | */ 70 | const [restaurant] = await db 71 | .insert(restaurants) 72 | .values([ 73 | { 74 | name: faker.company.name(), 75 | description: faker.lorem.paragraph(), 76 | managerId: manager.id, 77 | }, 78 | ]) 79 | .returning() 80 | 81 | console.log(chalk.yellowBright('✔️ Created restaurant!')) 82 | 83 | function generateProduct() { 84 | return { 85 | name: faker.commerce.productName(), 86 | description: faker.commerce.productDescription(), 87 | restaurantId: restaurant.id, 88 | priceInCents: Number(faker.commerce.price({ min: 190, max: 490, dec: 0 })), 89 | } 90 | } 91 | 92 | /** 93 | * Create products 94 | */ 95 | 96 | const availableProducts = await db 97 | .insert(products) 98 | .values([ 99 | generateProduct(), 100 | generateProduct(), 101 | generateProduct(), 102 | generateProduct(), 103 | generateProduct(), 104 | generateProduct(), 105 | ]) 106 | .returning() 107 | 108 | console.log(chalk.yellowBright('✔️ Created products!')) 109 | 110 | /** 111 | * Create orders 112 | */ 113 | type OrderItemInsert = typeof orderItems.$inferInsert 114 | type OrderInsert = typeof orders.$inferInsert 115 | 116 | const orderItemsToInsert: OrderItemInsert[] = [] 117 | const ordersToInsert: OrderInsert[] = [] 118 | 119 | for (let i = 0; i < 200; i++) { 120 | const orderId = createId() 121 | 122 | const orderProducts = faker.helpers.arrayElements(availableProducts, { 123 | min: 1, 124 | max: 3, 125 | }) 126 | 127 | let totalInCents = 0 128 | 129 | orderProducts.forEach((orderProduct) => { 130 | const quantity = faker.number.int({ min: 1, max: 3 }) 131 | 132 | totalInCents += orderProduct.priceInCents * quantity 133 | 134 | orderItemsToInsert.push({ 135 | orderId, 136 | productId: orderProduct.id, 137 | priceInCents: orderProduct.priceInCents, 138 | quantity, 139 | }) 140 | }) 141 | 142 | ordersToInsert.push({ 143 | id: orderId, 144 | customerId: faker.helpers.arrayElement([customer1.id, customer2.id]), 145 | restaurantId: restaurant.id, 146 | totalInCents, 147 | status: faker.helpers.arrayElement([ 148 | 'pending', 149 | 'processing', 150 | 'delivering', 151 | 'delivered', 152 | 'canceled', 153 | ]), 154 | createdAt: faker.date.recent({ days: 40 }), 155 | }) 156 | } 157 | 158 | await db.insert(orders).values(ordersToInsert) 159 | await db.insert(orderItems).values(orderItemsToInsert) 160 | 161 | console.log(chalk.yellowBright('✔️ Created orders!')) 162 | 163 | console.log(chalk.greenBright('Database seeded successfully!')) 164 | 165 | process.exit(0) 166 | -------------------------------------------------------------------------------- /src/env.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | const envSchema = z.object({ 4 | API_BASE_URL: z.string().url().min(1), 5 | AUTH_REDIRECT_URL: z.string().url().min(1), 6 | DATABASE_URL: z.string().url().min(1), 7 | JWT_SECRET_KEY: z.string().min(1), 8 | }) 9 | 10 | export const env = envSchema.parse(process.env) 11 | -------------------------------------------------------------------------------- /src/http/auth.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t, type Static } from 'elysia' 2 | import jwt from '@elysiajs/jwt' 3 | import cookie from '@elysiajs/cookie' 4 | 5 | import { env } from '../env' 6 | import { UnauthorizedError } from './errors/unauthorized-error' 7 | 8 | const jwtPayload = t.Object({ 9 | sub: t.String(), 10 | restauranteId: t.Optional(t.String()), 11 | }) 12 | 13 | export const auth = new Elysia() 14 | .error({ 15 | UNAUTHORIZED: UnauthorizedError, 16 | }) 17 | .onError(({ error, code, set }) => { 18 | switch (code) { 19 | case 'UNAUTHORIZED': { 20 | set.status = 401 21 | return { 22 | code, 23 | message: error.message, 24 | } 25 | } 26 | } 27 | }) 28 | .use( 29 | jwt({ 30 | secret: env.JWT_SECRET_KEY, 31 | schema: jwtPayload, 32 | }), 33 | ) 34 | .use(cookie()) 35 | .derive(({ jwt, setCookie, removeCookie, cookie }) => { 36 | return { 37 | signUser: async (payload: Static) => { 38 | const token = await jwt.sign(payload) 39 | 40 | setCookie('auth', token, { 41 | httpOnly: true, 42 | maxAge: 60 * 60 * 24 * 7, // 7 days 43 | path: '/', 44 | }) 45 | }, 46 | 47 | signOut: () => { 48 | removeCookie('auth') 49 | }, 50 | 51 | getCurrentUser: async () => { 52 | const payload = await jwt.verify(cookie.auth) 53 | 54 | if (!payload) { 55 | throw new UnauthorizedError() 56 | } 57 | 58 | return { 59 | userId: payload.sub, 60 | restauranteId: payload.restauranteId, 61 | } 62 | }, 63 | } 64 | }) 65 | -------------------------------------------------------------------------------- /src/http/errors/unauthorized-error.ts: -------------------------------------------------------------------------------- 1 | export class UnauthorizedError extends Error { 2 | constructor() { 3 | super('Unauthorized.') 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/http/routes/approve-order.ts: -------------------------------------------------------------------------------- 1 | import Elysia, { t } from 'elysia' 2 | import { auth } from '../auth' 3 | import { UnauthorizedError } from '../errors/unauthorized-error' 4 | import { db } from '../../db/connection' 5 | import { orders } from '../../db/schema' 6 | import { eq } from 'drizzle-orm' 7 | 8 | export const approveOrder = new Elysia().use(auth).patch( 9 | '/orders/:orderId/approve', 10 | async ({ getCurrentUser, set, params }) => { 11 | const { orderId } = params 12 | const { restauranteId } = await getCurrentUser() 13 | 14 | if (!restauranteId) { 15 | throw new UnauthorizedError() 16 | } 17 | 18 | const order = await db.query.orders.findFirst({ 19 | where(fields, { eq, and }) { 20 | return and( 21 | eq(fields.id, orderId), 22 | eq(fields.restaurantId, restauranteId), 23 | ) 24 | }, 25 | }) 26 | 27 | if (!order) { 28 | set.status = 400 29 | 30 | return { message: 'Order not found.' } 31 | } 32 | 33 | if (order.status !== 'pending') { 34 | set.status = 400 35 | 36 | return { message: 'You can only approve pending orders.' } 37 | } 38 | 39 | await db 40 | .update(orders) 41 | .set({ status: 'processing' }) 42 | .where(eq(orders.id, orderId)) 43 | }, 44 | { 45 | params: t.Object({ 46 | orderId: t.String(), 47 | }), 48 | }, 49 | ) 50 | -------------------------------------------------------------------------------- /src/http/routes/authenticate-from-link.ts: -------------------------------------------------------------------------------- 1 | import Elysia, { t } from 'elysia' 2 | import { db } from '../../db/connection' 3 | import { auth } from '../auth' 4 | import dayjs from 'dayjs' 5 | import { authLinks } from '../../db/schema' 6 | import { eq } from 'drizzle-orm' 7 | 8 | export const authenticateFromLink = new Elysia().use(auth).get( 9 | '/auth-links/authenticate', 10 | async ({ query, signUser, set }) => { 11 | const { code, redirect } = query 12 | 13 | const authLinkFromCode = await db.query.authLinks.findFirst({ 14 | where(fields, { eq }) { 15 | return eq(fields.code, code) 16 | }, 17 | }) 18 | 19 | if (!authLinkFromCode) { 20 | throw new Error('Auth link not found.') 21 | } 22 | 23 | const daysSinceAuthLinkWasCreated = dayjs().diff( 24 | authLinkFromCode.createdAt, 25 | 'days', 26 | ) 27 | 28 | if (daysSinceAuthLinkWasCreated > 7) { 29 | throw new Error('Auth link expired, please generate a new one.') 30 | } 31 | 32 | const managedRestaurante = await db.query.restaurants.findFirst({ 33 | where(fields, { eq }) { 34 | return eq(fields.managerId, authLinkFromCode.userId) 35 | }, 36 | }) 37 | 38 | await signUser({ 39 | sub: authLinkFromCode.userId, 40 | restauranteId: managedRestaurante?.id, 41 | }) 42 | 43 | await db.delete(authLinks).where(eq(authLinks.code, code)) 44 | 45 | set.redirect = redirect 46 | }, 47 | { 48 | query: t.Object({ 49 | code: t.String(), 50 | redirect: t.String(), 51 | }), 52 | }, 53 | ) 54 | -------------------------------------------------------------------------------- /src/http/routes/cancel-order.ts: -------------------------------------------------------------------------------- 1 | import Elysia, { t } from 'elysia' 2 | import { auth } from '../auth' 3 | import { UnauthorizedError } from '../errors/unauthorized-error' 4 | import { db } from '../../db/connection' 5 | import { orders } from '../../db/schema' 6 | import { eq } from 'drizzle-orm' 7 | 8 | export const cancelOrder = new Elysia().use(auth).patch( 9 | '/orders/:orderId/cancel', 10 | async ({ getCurrentUser, set, params }) => { 11 | const { orderId } = params 12 | const { restauranteId } = await getCurrentUser() 13 | 14 | if (!restauranteId) { 15 | throw new UnauthorizedError() 16 | } 17 | 18 | const order = await db.query.orders.findFirst({ 19 | where(fields, { eq, and }) { 20 | return and( 21 | eq(fields.id, orderId), 22 | eq(fields.restaurantId, restauranteId), 23 | ) 24 | }, 25 | }) 26 | 27 | if (!order) { 28 | set.status = 400 29 | 30 | return { message: 'Order not found.' } 31 | } 32 | 33 | if (!['pending', 'processing'].includes(order.status)) { 34 | set.status = 400 35 | 36 | return { message: 'You cannot cancel orders after dispatch.' } 37 | } 38 | 39 | await db 40 | .update(orders) 41 | .set({ status: 'canceled' }) 42 | .where(eq(orders.id, orderId)) 43 | }, 44 | { 45 | params: t.Object({ 46 | orderId: t.String(), 47 | }), 48 | }, 49 | ) 50 | -------------------------------------------------------------------------------- /src/http/routes/deliver-order.ts: -------------------------------------------------------------------------------- 1 | import Elysia, { t } from 'elysia' 2 | import { auth } from '../auth' 3 | import { UnauthorizedError } from '../errors/unauthorized-error' 4 | import { db } from '../../db/connection' 5 | import { orders } from '../../db/schema' 6 | import { eq } from 'drizzle-orm' 7 | 8 | export const deliverOrder = new Elysia().use(auth).patch( 9 | '/orders/:orderId/deliver', 10 | async ({ getCurrentUser, set, params }) => { 11 | const { orderId } = params 12 | const { restauranteId } = await getCurrentUser() 13 | 14 | if (!restauranteId) { 15 | throw new UnauthorizedError() 16 | } 17 | 18 | const order = await db.query.orders.findFirst({ 19 | where(fields, { eq, and }) { 20 | return and( 21 | eq(fields.id, orderId), 22 | eq(fields.restaurantId, restauranteId), 23 | ) 24 | }, 25 | }) 26 | 27 | if (!order) { 28 | set.status = 400 29 | 30 | return { message: 'Order not found.' } 31 | } 32 | 33 | if (order.status !== 'delivering') { 34 | set.status = 400 35 | 36 | return { 37 | message: 38 | 'You cannot deliver orders that are not in "delivering" status.', 39 | } 40 | } 41 | 42 | await db 43 | .update(orders) 44 | .set({ status: 'delivered' }) 45 | .where(eq(orders.id, orderId)) 46 | }, 47 | { 48 | params: t.Object({ 49 | orderId: t.String(), 50 | }), 51 | }, 52 | ) 53 | -------------------------------------------------------------------------------- /src/http/routes/dispatch-order.ts: -------------------------------------------------------------------------------- 1 | import Elysia, { t } from 'elysia' 2 | import { auth } from '../auth' 3 | import { UnauthorizedError } from '../errors/unauthorized-error' 4 | import { db } from '../../db/connection' 5 | import { orders } from '../../db/schema' 6 | import { eq } from 'drizzle-orm' 7 | 8 | export const dispatchOrder = new Elysia().use(auth).patch( 9 | '/orders/:orderId/dispatch', 10 | async ({ getCurrentUser, set, params }) => { 11 | const { orderId } = params 12 | const { restauranteId } = await getCurrentUser() 13 | 14 | if (!restauranteId) { 15 | throw new UnauthorizedError() 16 | } 17 | 18 | const order = await db.query.orders.findFirst({ 19 | where(fields, { eq, and }) { 20 | return and( 21 | eq(fields.id, orderId), 22 | eq(fields.restaurantId, restauranteId), 23 | ) 24 | }, 25 | }) 26 | 27 | if (!order) { 28 | set.status = 400 29 | 30 | return { message: 'Order not found.' } 31 | } 32 | 33 | if (order.status !== 'processing') { 34 | set.status = 400 35 | 36 | return { 37 | message: 38 | 'You cannot dispatch orders that are not in "processing" status.', 39 | } 40 | } 41 | 42 | await db 43 | .update(orders) 44 | .set({ status: 'delivering' }) 45 | .where(eq(orders.id, orderId)) 46 | }, 47 | { 48 | params: t.Object({ 49 | orderId: t.String(), 50 | }), 51 | }, 52 | ) 53 | -------------------------------------------------------------------------------- /src/http/routes/get-daily-receipt-in-period.ts: -------------------------------------------------------------------------------- 1 | import Elysia, { t } from 'elysia' 2 | import { auth } from '../auth' 3 | import { UnauthorizedError } from '../errors/unauthorized-error' 4 | import dayjs from 'dayjs' 5 | import { db } from '../../db/connection' 6 | import { orders } from '../../db/schema' 7 | import { and, eq, gte, lte, sql, sum } from 'drizzle-orm' 8 | 9 | export const getDailyReceiptInPeriod = new Elysia().use(auth).get( 10 | '/metrics/daily-receipt-in-period', 11 | async ({ getCurrentUser, query, set }) => { 12 | const { restauranteId } = await getCurrentUser() 13 | 14 | if (!restauranteId) { 15 | throw new UnauthorizedError() 16 | } 17 | 18 | const { from, to } = query 19 | 20 | const startDate = from ? dayjs(from) : dayjs().subtract(7, 'days') 21 | const endDate = to ? dayjs(to) : from ? startDate.add(7, 'days') : dayjs() 22 | 23 | if (endDate.diff(startDate, 'days') > 7) { 24 | set.status = 400 25 | 26 | return { 27 | message: 'You cannot list receipt in a large period than 7 days.', 28 | } 29 | } 30 | 31 | const receiptPerDay = await db 32 | .select({ 33 | date: sql`TO_CHAR(${orders.createdAt}, 'DD/MM')`, 34 | receipt: sum(orders.totalInCents).mapWith(Number), 35 | }) 36 | .from(orders) 37 | .where( 38 | and( 39 | eq(orders.restaurantId, restauranteId), 40 | gte( 41 | orders.createdAt, 42 | startDate 43 | .startOf('day') 44 | .add(startDate.utcOffset(), 'minutes') 45 | .toDate(), 46 | ), 47 | lte( 48 | orders.createdAt, 49 | endDate.endOf('day').add(startDate.utcOffset(), 'minutes').toDate(), 50 | ), 51 | ), 52 | ) 53 | .groupBy(sql`TO_CHAR(${orders.createdAt}, 'DD/MM')`) 54 | 55 | const orderedReceiptPerDay = receiptPerDay.sort((a, b) => { 56 | const [dayA, monthA] = a.date.split('/').map(Number) 57 | const [dayB, monthB] = b.date.split('/').map(Number) 58 | 59 | if (monthA === monthB) { 60 | return dayA - dayB 61 | } else { 62 | const dateA = new Date(2024, monthA - 1) 63 | const dateB = new Date(2024, monthB - 1) 64 | 65 | return dateA.getTime() - dateB.getTime() 66 | } 67 | }) 68 | 69 | return orderedReceiptPerDay 70 | }, 71 | { 72 | query: t.Object({ 73 | from: t.Optional(t.String()), 74 | to: t.Optional(t.String()), 75 | }), 76 | }, 77 | ) 78 | -------------------------------------------------------------------------------- /src/http/routes/get-day-orders-amount.ts: -------------------------------------------------------------------------------- 1 | import Elysia from 'elysia' 2 | import { auth } from '../auth' 3 | import { UnauthorizedError } from '../errors/unauthorized-error' 4 | import dayjs from 'dayjs' 5 | import { db } from '../../db/connection' 6 | import { orders } from '../../db/schema' 7 | import { and, count, eq, gte, sql } from 'drizzle-orm' 8 | 9 | export const getDayOrdersAmount = new Elysia() 10 | .use(auth) 11 | .get('/metrics/day-orders-amount', async ({ getCurrentUser }) => { 12 | const { restauranteId } = await getCurrentUser() 13 | 14 | if (!restauranteId) { 15 | throw new UnauthorizedError() 16 | } 17 | 18 | const today = dayjs() 19 | const yesterday = today.subtract(1, 'day') 20 | const startOfYesterday = yesterday.startOf('day') 21 | 22 | const orderPerDay = await db 23 | .select({ 24 | dayWithMonthAndYear: sql`TO_CHAR(${orders.createdAt}, 'YYYY-MM-DD')`, 25 | amount: count(), 26 | }) 27 | .from(orders) 28 | .where( 29 | and( 30 | eq(orders.restaurantId, restauranteId), 31 | gte(orders.createdAt, startOfYesterday.toDate()), 32 | ), 33 | ) 34 | .groupBy(sql`TO_CHAR(${orders.createdAt}, 'YYYY-MM-DD')`) 35 | 36 | const todayWithMonthAndYear = today.format('YYYY-MM-DD') 37 | const yesterdayWithMonthAndYear = yesterday.format('YYYY-MM-DD') 38 | 39 | const todayOrdersAmount = orderPerDay.find((orderPerDay) => { 40 | return orderPerDay.dayWithMonthAndYear === todayWithMonthAndYear 41 | }) 42 | 43 | const yesterdayOrdersAmount = orderPerDay.find((orderPerDay) => { 44 | return orderPerDay.dayWithMonthAndYear === yesterdayWithMonthAndYear 45 | }) 46 | 47 | const diffFromYesterday = 48 | todayOrdersAmount && yesterdayOrdersAmount 49 | ? (todayOrdersAmount.amount * 100) / yesterdayOrdersAmount.amount 50 | : null 51 | 52 | return { 53 | amount: todayOrdersAmount?.amount || 0, 54 | diffFromLastMonth: diffFromYesterday 55 | ? Number((diffFromYesterday - 100).toFixed(2)) 56 | : 0, 57 | } 58 | }) 59 | -------------------------------------------------------------------------------- /src/http/routes/get-managed-restaurante.ts: -------------------------------------------------------------------------------- 1 | import Elysia from 'elysia' 2 | import { auth } from '../auth' 3 | import { db } from '../../db/connection' 4 | 5 | export const getManagedRestaurante = new Elysia() 6 | .use(auth) 7 | .get('/managed-restaurante', async ({ getCurrentUser }) => { 8 | const { restauranteId } = await getCurrentUser() 9 | 10 | if (!restauranteId) { 11 | throw new Error('User is not a manager.') 12 | } 13 | 14 | const managedRestaurante = await db.query.restaurants.findFirst({ 15 | where(fields, { eq }) { 16 | return eq(fields.id, restauranteId) 17 | }, 18 | }) 19 | 20 | if (!managedRestaurante) { 21 | throw new Error('Restaurante not found.') 22 | } 23 | 24 | return managedRestaurante 25 | }) 26 | -------------------------------------------------------------------------------- /src/http/routes/get-month-canceled-orders-amount.ts: -------------------------------------------------------------------------------- 1 | import Elysia from 'elysia' 2 | import { auth } from '../auth' 3 | import { UnauthorizedError } from '../errors/unauthorized-error' 4 | import dayjs from 'dayjs' 5 | import { db } from '../../db/connection' 6 | import { orders } from '../../db/schema' 7 | import { and, count, eq, gte, sql } from 'drizzle-orm' 8 | 9 | export const getMonthCanceledOrdersAmount = new Elysia() 10 | .use(auth) 11 | .get('/metrics/month-canceled-orders-amount', async ({ getCurrentUser }) => { 12 | const { restauranteId } = await getCurrentUser() 13 | 14 | if (!restauranteId) { 15 | throw new UnauthorizedError() 16 | } 17 | 18 | const today = dayjs() 19 | const lastMonth = today.subtract(1, 'month') 20 | const startOfLastMonth = lastMonth.startOf('month') 21 | 22 | const ordersPerMonth = await db 23 | .select({ 24 | monthWithYear: sql`TO_CHAR(${orders.createdAt}, 'YYYY-MM')`, 25 | amount: count(), 26 | }) 27 | .from(orders) 28 | .where( 29 | and( 30 | eq(orders.restaurantId, restauranteId), 31 | eq(orders.status, 'canceled'), 32 | gte(orders.createdAt, startOfLastMonth.toDate()), 33 | ), 34 | ) 35 | .groupBy(sql`TO_CHAR(${orders.createdAt}, 'YYYY-MM')`) 36 | 37 | const currentMonthWithYear = today.format('YYYY-MM') // 2024-02 38 | const lastMonthWithYear = lastMonth.format('YYYY-MM') // 2024-01 39 | 40 | const currentMonthOrdersAmount = ordersPerMonth.find((orderPerMonth) => { 41 | return orderPerMonth.monthWithYear === currentMonthWithYear 42 | }) 43 | 44 | const lastMonthOrdersAmount = ordersPerMonth.find((orderPerMonth) => { 45 | return orderPerMonth.monthWithYear === lastMonthWithYear 46 | }) 47 | 48 | const diffFromLastMonth = 49 | currentMonthOrdersAmount && lastMonthOrdersAmount 50 | ? (currentMonthOrdersAmount.amount * 100) / lastMonthOrdersAmount.amount 51 | : null 52 | 53 | return { 54 | amount: currentMonthOrdersAmount?.amount || 0, 55 | diffFromLastMonth: diffFromLastMonth 56 | ? Number((diffFromLastMonth - 100).toFixed(2)) 57 | : 0, 58 | } 59 | }) 60 | -------------------------------------------------------------------------------- /src/http/routes/get-month-orders-amount.ts: -------------------------------------------------------------------------------- 1 | import Elysia from 'elysia' 2 | import { auth } from '../auth' 3 | import { UnauthorizedError } from '../errors/unauthorized-error' 4 | import dayjs from 'dayjs' 5 | import { db } from '../../db/connection' 6 | import { orders } from '../../db/schema' 7 | import { and, count, eq, gte, sql } from 'drizzle-orm' 8 | 9 | export const getMonthOrdersAmount = new Elysia() 10 | .use(auth) 11 | .get('/metrics/month-orders-amount', async ({ getCurrentUser }) => { 12 | const { restauranteId } = await getCurrentUser() 13 | 14 | if (!restauranteId) { 15 | throw new UnauthorizedError() 16 | } 17 | 18 | const today = dayjs() 19 | const lastMonth = today.subtract(1, 'month') 20 | const startOfLastMonth = lastMonth.startOf('month') 21 | 22 | const ordersPerMonth = await db 23 | .select({ 24 | monthWithYear: sql`TO_CHAR(${orders.createdAt}, 'YYYY-MM')`, 25 | amount: count(), 26 | }) 27 | .from(orders) 28 | .where( 29 | and( 30 | eq(orders.restaurantId, restauranteId), 31 | gte(orders.createdAt, startOfLastMonth.toDate()), 32 | ), 33 | ) 34 | .groupBy(sql`TO_CHAR(${orders.createdAt}, 'YYYY-MM')`) 35 | 36 | const currentMonthWithYear = today.format('YYYY-MM') // 2024-02 37 | const lastMonthWithYear = lastMonth.format('YYYY-MM') // 2024-01 38 | 39 | const currentMonthOrdersAmount = ordersPerMonth.find((orderPerMonth) => { 40 | return orderPerMonth.monthWithYear === currentMonthWithYear 41 | }) 42 | 43 | const lastMonthOrdersAmount = ordersPerMonth.find((orderPerMonth) => { 44 | return orderPerMonth.monthWithYear === lastMonthWithYear 45 | }) 46 | 47 | const diffFromLastMonth = 48 | currentMonthOrdersAmount && lastMonthOrdersAmount 49 | ? (currentMonthOrdersAmount.amount * 100) / lastMonthOrdersAmount.amount 50 | : null 51 | 52 | return { 53 | amount: currentMonthOrdersAmount?.amount || 0, 54 | diffFromLastMonth: diffFromLastMonth 55 | ? Number((diffFromLastMonth - 100).toFixed(2)) 56 | : 0, 57 | } 58 | }) 59 | -------------------------------------------------------------------------------- /src/http/routes/get-month.receipt.ts: -------------------------------------------------------------------------------- 1 | import Elysia from 'elysia' 2 | import { auth } from '../auth' 3 | import { UnauthorizedError } from '../errors/unauthorized-error' 4 | import dayjs from 'dayjs' 5 | import { db } from '../../db/connection' 6 | import { orders } from '../../db/schema' 7 | import { and, eq, gte, sql, sum } from 'drizzle-orm' 8 | 9 | export const getMonthReceipt = new Elysia() 10 | .use(auth) 11 | .get('/metrics/month-receipt', async ({ getCurrentUser }) => { 12 | const { restauranteId } = await getCurrentUser() 13 | 14 | if (!restauranteId) { 15 | throw new UnauthorizedError() 16 | } 17 | 18 | const today = dayjs() 19 | const lastMonth = today.subtract(1, 'month') 20 | const startOfLastMonth = lastMonth.startOf('month') 21 | 22 | const monthsReceipts = await db 23 | .select({ 24 | monthWithYear: sql`TO_CHAR(${orders.createdAt}, 'YYYY-MM')`, 25 | receipt: sum(orders.totalInCents).mapWith(Number), 26 | }) 27 | .from(orders) 28 | .where( 29 | and( 30 | eq(orders.restaurantId, restauranteId), 31 | gte(orders.createdAt, startOfLastMonth.toDate()), 32 | ), 33 | ) 34 | .groupBy(sql`TO_CHAR(${orders.createdAt}, 'YYYY-MM')`) 35 | 36 | const currentMonthWithYear = today.format('YYYY-MM') // 2024-02 37 | const lastMonthWithYear = lastMonth.format('YYYY-MM') // 2024-01 38 | 39 | const currentMonthReceipt = monthsReceipts.find((monthReceipt) => { 40 | return monthReceipt.monthWithYear === currentMonthWithYear 41 | }) 42 | 43 | const lastMonthReceipt = monthsReceipts.find((monthReceipt) => { 44 | return monthReceipt.monthWithYear === lastMonthWithYear 45 | }) 46 | 47 | const diffFromLastMonth = 48 | currentMonthReceipt && lastMonthReceipt 49 | ? (currentMonthReceipt.receipt * 100) / lastMonthReceipt.receipt 50 | : null 51 | 52 | return { 53 | receipt: currentMonthReceipt?.receipt || 0, 54 | diffFromLastMonth: diffFromLastMonth 55 | ? Number((diffFromLastMonth - 100).toFixed(2)) 56 | : 0, 57 | } 58 | }) 59 | -------------------------------------------------------------------------------- /src/http/routes/get-order-details.ts: -------------------------------------------------------------------------------- 1 | import Elysia, { t } from 'elysia' 2 | import { auth } from '../auth' 3 | import { UnauthorizedError } from '../errors/unauthorized-error' 4 | import { db } from '../../db/connection' 5 | import { and } from 'drizzle-orm' 6 | 7 | export const getOrderDetails = new Elysia().use(auth).get( 8 | '/orders/:orderId', 9 | async ({ getCurrentUser, params, set }) => { 10 | const { orderId } = params 11 | const { restauranteId } = await getCurrentUser() 12 | 13 | if (!restauranteId) { 14 | throw new UnauthorizedError() 15 | } 16 | 17 | const order = await db.query.orders.findFirst({ 18 | columns: { 19 | id: true, 20 | status: true, 21 | totalInCents: true, 22 | createdAt: true, 23 | }, 24 | with: { 25 | customer: { 26 | columns: { 27 | name: true, 28 | phone: true, 29 | email: true, 30 | }, 31 | }, 32 | orderItems: { 33 | columns: { 34 | id: true, 35 | priceInCents: true, 36 | quantity: true, 37 | }, 38 | with: { 39 | product: { 40 | columns: { 41 | name: true, 42 | }, 43 | }, 44 | }, 45 | }, 46 | }, 47 | where(fields, { eq }) { 48 | return and( 49 | eq(fields.id, orderId), 50 | eq(fields.restaurantId, restauranteId), 51 | ) 52 | }, 53 | }) 54 | 55 | if (!order) { 56 | set.status = 400 57 | 58 | return { message: 'Order not found.' } 59 | } 60 | 61 | return order 62 | }, 63 | { 64 | params: t.Object({ 65 | orderId: t.String(), 66 | }), 67 | }, 68 | ) 69 | -------------------------------------------------------------------------------- /src/http/routes/get-orders.ts: -------------------------------------------------------------------------------- 1 | import Elysia, { t } from 'elysia' 2 | import { auth } from '../auth' 3 | import { db } from '../../db/connection' 4 | import { UnauthorizedError } from '../errors/unauthorized-error' 5 | import { createSelectSchema } from 'drizzle-typebox' 6 | import { orders, users } from '../../db/schema' 7 | import { and, count, desc, eq, ilike, sql } from 'drizzle-orm' 8 | 9 | export const getOrders = new Elysia().use(auth).get( 10 | '/orders', 11 | async ({ getCurrentUser, query }) => { 12 | const { restauranteId } = await getCurrentUser() 13 | const { customerName, orderId, status, pageIndex } = query 14 | 15 | if (!restauranteId) { 16 | throw new UnauthorizedError() 17 | } 18 | 19 | const baseQuery = db 20 | .select({ 21 | orderId: orders.id, 22 | createdAt: orders.createdAt, 23 | status: orders.status, 24 | total: orders.totalInCents, 25 | customerName: users.name, 26 | }) 27 | .from(orders) 28 | .innerJoin(users, eq(users.id, orders.customerId)) 29 | .where( 30 | and( 31 | eq(orders.restaurantId, restauranteId), 32 | orderId ? ilike(orders.id, `%${orderId}%`) : undefined, 33 | status ? eq(orders.id, status) : undefined, 34 | customerName ? ilike(users.name, `%${customerName}%`) : undefined, 35 | ), 36 | ) 37 | 38 | const [amountOfOrdersQuery, allOrders] = await Promise.all([ 39 | db.select({ count: count() }).from(baseQuery.as('baseQuery')), 40 | db 41 | .select() 42 | .from(baseQuery.as('baseQuery')) 43 | .offset(pageIndex * 10) 44 | .limit(10) 45 | .orderBy((fields) => { 46 | return [ 47 | sql`CASE ${fields.status} 48 | WHEN 'pending' THEN 1 49 | WHEN 'processing' THEN 2 50 | WHEN 'delivering' THEN 3 51 | WHEN 'delivered' THEN 4 52 | WHEN 'canceled' THEN 99 53 | END`, 54 | desc(fields.createdAt), 55 | ] 56 | }), 57 | ]) 58 | 59 | const amountOfOrders = amountOfOrdersQuery[0].count 60 | 61 | return { 62 | orders: allOrders, 63 | meta: { 64 | pageIndex, 65 | perPage: 10, 66 | totalCount: amountOfOrders, 67 | }, 68 | } 69 | }, 70 | { 71 | query: t.Object({ 72 | customerName: t.Optional(t.String()), 73 | orderId: t.Optional(t.String()), 74 | status: t.Optional(createSelectSchema(orders).properties.status), 75 | pageIndex: t.Numeric({ minimum: 0 }), 76 | }), 77 | }, 78 | ) 79 | -------------------------------------------------------------------------------- /src/http/routes/get-popular-products.ts: -------------------------------------------------------------------------------- 1 | import Elysia from 'elysia' 2 | import { auth } from '../auth' 3 | import { db } from '../../db/connection' 4 | import { UnauthorizedError } from '../errors/unauthorized-error' 5 | import { orderItems, orders, products } from '../../db/schema' 6 | import { desc, eq, sum } from 'drizzle-orm' 7 | 8 | export const getPopularProducts = new Elysia() 9 | .use(auth) 10 | .get('/metrics/popular-products', async ({ getCurrentUser }) => { 11 | const { restauranteId } = await getCurrentUser() 12 | 13 | if (!restauranteId) { 14 | throw new UnauthorizedError() 15 | } 16 | 17 | const popularProducts = await db 18 | .select({ 19 | product: products.name, 20 | amount: sum(orderItems.quantity).mapWith(Number), 21 | }) 22 | .from(orderItems) 23 | .leftJoin(orders, eq(orders.id, orderItems.orderId)) 24 | .leftJoin(products, eq(products.id, orderItems.productId)) 25 | .where(eq(orders.restaurantId, restauranteId)) 26 | .groupBy(products.name) 27 | .orderBy((fields) => { 28 | return desc(fields.amount) 29 | }) 30 | .limit(5) 31 | 32 | return popularProducts 33 | }) 34 | -------------------------------------------------------------------------------- /src/http/routes/get-profile.ts: -------------------------------------------------------------------------------- 1 | import Elysia from 'elysia' 2 | import { auth } from '../auth' 3 | import { db } from '../../db/connection' 4 | import { UnauthorizedError } from '../errors/unauthorized-error' 5 | 6 | export const getProfile = new Elysia() 7 | .use(auth) 8 | .get('/me', async ({ getCurrentUser }) => { 9 | const { userId, restauranteId } = await getCurrentUser() 10 | 11 | const user = await db.query.users.findFirst({ 12 | where(fields, { eq }) { 13 | return eq(fields.id, userId) 14 | }, 15 | }) 16 | 17 | if (!user) { 18 | throw new UnauthorizedError() 19 | } 20 | 21 | return user 22 | }) 23 | -------------------------------------------------------------------------------- /src/http/routes/register-restaurante.ts: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from 'elysia' 2 | import { db } from '../../db/connection' 3 | import { restaurants, users } from '../../db/schema' 4 | 5 | export const registerRestaurante = new Elysia().post( 6 | '/restaurantes', 7 | async ({ body, set }) => { 8 | const { restaurantName, managerName, email, phone } = body 9 | 10 | const [manager] = await db 11 | .insert(users) 12 | .values({ 13 | name: managerName, 14 | email, 15 | phone, 16 | role: 'manager', 17 | }) 18 | .returning({ 19 | id: users.id, 20 | }) 21 | 22 | await db.insert(restaurants).values({ 23 | name: restaurantName, 24 | managerId: manager.id, 25 | }) 26 | 27 | set.status = 204 28 | }, 29 | { 30 | body: t.Object({ 31 | restaurantName: t.String(), 32 | managerName: t.String(), 33 | phone: t.String(), 34 | email: t.String({ format: 'email' }), 35 | }), 36 | }, 37 | ) 38 | -------------------------------------------------------------------------------- /src/http/routes/send-auth-link.ts: -------------------------------------------------------------------------------- 1 | import Elysia, { t } from 'elysia' 2 | import nodemailer from 'nodemailer' 3 | import { db } from '../../db/connection' 4 | import { createId } from '@paralleldrive/cuid2' 5 | import { authLinks } from '../../db/schema' 6 | import { env } from '../../env' 7 | import { mail } from '../../lib/mail' 8 | 9 | export const sendAuthLink = new Elysia().post( 10 | '/authenticate', 11 | async ({ body }) => { 12 | const { email } = body 13 | 14 | const userFromEmail = await db.query.users.findFirst({ 15 | where(fields, { eq }) { 16 | return eq(fields.email, email) 17 | }, 18 | }) 19 | 20 | if (!userFromEmail) { 21 | throw new Error('User not found') 22 | } 23 | 24 | const authLinkCode = createId() 25 | 26 | await db.insert(authLinks).values({ 27 | userId: userFromEmail.id, 28 | code: authLinkCode, 29 | }) 30 | 31 | const authLink = new URL('/auth-links/authenticate', env.API_BASE_URL) 32 | 33 | authLink.searchParams.set('code', authLinkCode) 34 | authLink.searchParams.set('redirect', env.AUTH_REDIRECT_URL) 35 | 36 | const info = await mail.sendMail({ 37 | from: { 38 | name: 'Pizza Shop', 39 | address: 'hi@pizzashop.com', 40 | }, 41 | to: email, 42 | subject: 'Authenticate to Pizza Shop', 43 | text: `Use the following link to authenticate on Pizza Shop: ${authLink.toString()}`, 44 | }) 45 | 46 | console.log(nodemailer.getTestMessageUrl(info)) 47 | }, 48 | { 49 | body: t.Object({ 50 | email: t.String({ format: 'email' }), 51 | }), 52 | }, 53 | ) 54 | -------------------------------------------------------------------------------- /src/http/routes/sign-out.ts: -------------------------------------------------------------------------------- 1 | import Elysia from 'elysia' 2 | import { auth } from '../auth' 3 | 4 | export const signOut = new Elysia() 5 | .use(auth) 6 | .post('/sign-out', async ({ signOut: internalSignOut }) => { 7 | internalSignOut() 8 | }) 9 | -------------------------------------------------------------------------------- /src/http/server.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from 'elysia' 2 | 3 | import { registerRestaurante } from './routes/register-restaurante' 4 | import { sendAuthLink } from './routes/send-auth-link' 5 | import { authenticateFromLink } from './routes/authenticate-from-link' 6 | import { signOut } from './routes/sign-out' 7 | import { getProfile } from './routes/get-profile' 8 | import { getManagedRestaurante } from './routes/get-managed-restaurante' 9 | import { getOrderDetails } from './routes/get-order-details' 10 | import { approveOrder } from './routes/approve-order' 11 | import { cancelOrder } from './routes/cancel-order' 12 | import { deliverOrder } from './routes/deliver-order' 13 | import { dispatchOrder } from './routes/dispatch-order' 14 | import { getOrders } from './routes/get-orders' 15 | import { getMonthReceipt } from './routes/get-month.receipt' 16 | import { getDayOrdersAmount } from './routes/get-day-orders-amount' 17 | import { getMonthOrdersAmount } from './routes/get-month-orders-amount' 18 | import { getMonthCanceledOrdersAmount } from './routes/get-month-canceled-orders-amount' 19 | import { getPopularProducts } from './routes/get-popular-products' 20 | import { getDailyReceiptInPeriod } from './routes/get-daily-receipt-in-period' 21 | 22 | const app = new Elysia() 23 | .use(registerRestaurante) 24 | .use(sendAuthLink) 25 | .use(authenticateFromLink) 26 | .use(signOut) 27 | .use(getProfile) 28 | .use(getManagedRestaurante) 29 | .use(getOrderDetails) 30 | .use(approveOrder) 31 | .use(cancelOrder) 32 | .use(deliverOrder) 33 | .use(dispatchOrder) 34 | .use(getOrders) 35 | .use(getMonthReceipt) 36 | .use(getDayOrdersAmount) 37 | .use(getMonthOrdersAmount) 38 | .use(getMonthCanceledOrdersAmount) 39 | .use(getPopularProducts) 40 | .use(getDailyReceiptInPeriod) 41 | .onError(({ code, error, set }) => { 42 | switch (code) { 43 | case 'VALIDATION': { 44 | set.status = error.status 45 | 46 | return error.toResponse() 47 | } 48 | case 'NOT_FOUND': { 49 | return new Response(null, { status: 404 }) 50 | } 51 | default: { 52 | console.error(error) 53 | 54 | return new Response(null, { status: 500 }) 55 | } 56 | } 57 | }) 58 | 59 | app.listen(3333, () => { 60 | console.log('🔥 HTTP server running!') 61 | }) 62 | -------------------------------------------------------------------------------- /src/lib/mail.ts: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer' 2 | 3 | const account = await nodemailer.createTestAccount() 4 | 5 | export const mail = nodemailer.createTransport({ 6 | host: account.smtp.host, 7 | port: account.smtp.port, 8 | secure: account.smtp.secure, 9 | debug: true, 10 | auth: { 11 | user: account.user, 12 | pass: account.pass, 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleDetection": "force", 7 | "jsx": "react-jsx", 8 | "allowJs": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "verbatimModuleSyntax": true, 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "skipLibCheck": true, 18 | "strict": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "forceConsistentCasingInFileNames": true 21 | } 22 | } 23 | --------------------------------------------------------------------------------