diff --git a/.gitignore b/.gitignore index 3fddcf3..5e7c3d6 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ next-env.d.ts robots.txt sitemap.xml sitemap-*.xml + +/src/data/db/dm/ diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..7f5a121 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,16 @@ +import type { Config } from "drizzle-kit"; + +import "dotenv/config"; + +if (!process.env.NEXT_SECRET_URL_PSCALE) { + throw new Error("NEXT_SECRET_URL_PSCALE is missing"); +} + +export default { + schema: "./src/data/db/schema.ts", + out: "./src/data/db/dm", + driver: "mysql2", + dbCredentials: { + connectionString: process.env.NEXT_SECRET_URL_PSCALE + } +} satisfies Config; diff --git a/package.json b/package.json index 7cca761..4a3d472 100644 --- a/package.json +++ b/package.json @@ -20,18 +20,17 @@ "scripts": { "build": "next build", "canary": "pnpm add react@canary react-dom@canary next@canary", - "db:introspect": "drizzle-kit introspect:mysql", - "db:push": "drizzle-kit push:mysql", - "db:studio": "drizzle-kit studio", + "db:introspect": "drizzle-kit introspect:mysql --config drizzle.config.ts", + "db:push": "drizzle-kit push:mysql --config drizzle.config.ts", + "db:studio": "drizzle-kit studio --config drizzle.config.ts", "dev": "next dev", "edge": "pnpm up --latest && pnpm canary", "email:dev": "email dev --dir src/islands/emails -p 3001", "format": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache", "format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache", - "postinstall": "drizzle-kit generate:mysql", + "postinstall": "drizzle-kit generate:mysql --config drizzle.config.ts", "lint": "next lint", "lint:fix": "next lint --fix", - "prepare": "husky install", "shadcn": "pnpm dlx shadcn-ui@latest add", "start": "next start", "stripe:listen": "stripe listen --forward-to localhost:3000/api/webhooks/stripe --latest", @@ -41,6 +40,7 @@ }, "dependencies": { "@hookform/resolvers": "^3.2.0", + "@planetscale/database": "^1.10.0", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dropdown-menu": "^2.0.5", @@ -59,6 +59,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "cmdk": "^0.2.0", + "dotenv": "^16.3.1", "drizzle-orm": "^0.28.2", "ky": "^0.33.3", "lucia": "^2.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ba8681..e940fe5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ dependencies: '@hookform/resolvers': specifier: ^3.2.0 version: 3.2.0(react-hook-form@7.45.4) + '@planetscale/database': + specifier: ^1.10.0 + version: 1.10.0 '@radix-ui/react-checkbox': specifier: ^1.0.4 version: 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.3.0-canary-0fb5b61ac-20230814)(react@18.3.0-canary-0fb5b61ac-20230814) @@ -62,9 +65,12 @@ dependencies: cmdk: specifier: ^0.2.0 version: 0.2.0(@types/react@18.2.20)(react-dom@18.3.0-canary-0fb5b61ac-20230814)(react@18.3.0-canary-0fb5b61ac-20230814) + dotenv: + specifier: ^16.3.1 + version: 16.3.1 drizzle-orm: specifier: ^0.28.2 - version: 0.28.2(postgres@3.3.5) + version: 0.28.2(@planetscale/database@1.10.0)(postgres@3.3.5) ky: specifier: ^0.33.3 version: 0.33.3 @@ -1529,6 +1535,11 @@ packages: tslib: 2.6.1 dev: true + /@planetscale/database@1.10.0: + resolution: {integrity: sha512-XMfNRjIPgGTga6g1YpGr7E21CcnHZcHZdyhRUIiZ/AlpD+ts65UF2B3wKjcu7MKMynmmcOGs6R9kAT6D1OTlZQ==} + engines: {node: '>=16'} + dev: false + /@radix-ui/number@1.0.1: resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} dependencies: @@ -3707,6 +3718,11 @@ packages: is-obj: 2.0.0 dev: true + /dotenv@16.3.1: + resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} + engines: {node: '>=12'} + dev: false + /dreamopt@0.8.0: resolution: {integrity: sha512-vyJTp8+mC+G+5dfgsY+r3ckxlz+QMX40VjPQsZc5gxVAxLmi64TBoVkP54A/pRAXMXsbu2GMMBrZPxNv23waMg==} engines: {node: '>=0.4.0'} @@ -3734,7 +3750,7 @@ packages: - supports-color dev: true - /drizzle-orm@0.28.2(postgres@3.3.5): + /drizzle-orm@0.28.2(@planetscale/database@1.10.0)(postgres@3.3.5): resolution: {integrity: sha512-QRyuzvpJr7GE6LpvZ/sg2nAKNg2if1uGGkgFTiXn4auuYId//vVJe6HBsDTktfKfcaDGzIYos+/f+PS5EkBmrg==} peerDependencies: '@aws-sdk/client-rds-data': '>=3' @@ -3796,6 +3812,7 @@ packages: sqlite3: optional: true dependencies: + '@planetscale/database': 1.10.0 postgres: 3.3.5 dev: false diff --git a/src/data/db/db.ts b/src/data/db/db.ts new file mode 100644 index 0000000..9e005bd --- /dev/null +++ b/src/data/db/db.ts @@ -0,0 +1,18 @@ +import { connect } from "@planetscale/database"; +import { migrate } from "drizzle-orm/mysql2/migrator"; +import { drizzle } from "drizzle-orm/planetscale-serverless"; + +// create the connection +const connection = connect({ + host: process.env["DATABASE_HOST"], + username: process.env["DATABASE_USERNAME"], + password: process.env["DATABASE_PASSWORD"] +}); + +export const db = drizzle(connection); + +// syncs the migrations folder to PlanetScale +process.env.NODE_ENV === "development" && + migrate(db as any, { migrationsFolder: "./migrations" }) + .then((res) => res) + .catch((err) => console.log("Migration error in db.ts:", err)); diff --git a/src/data/db/schema.ts b/src/data/db/schema.ts new file mode 100644 index 0000000..4e5ee66 --- /dev/null +++ b/src/data/db/schema.ts @@ -0,0 +1,96 @@ +import { InferModel } from "drizzle-orm"; +import { + boolean, + decimal, + int, + json, + mysqlTable, + serial, + text, + uniqueIndex, + varchar +} from "drizzle-orm/mysql-core"; + +export const stores = mysqlTable( + "stores", + { + id: serial("id").primaryKey(), + name: varchar("store_name", { length: 40 }), + industry: text("industry"), + description: text("description"), + slug: varchar("slug", { length: 50 }) + }, + (table) => { + return { + storeNameIndex: uniqueIndex("store_name_index").on(table.name), + storeSlugIndex: uniqueIndex("store_slug_index").on(table.slug) + }; + } +); +export type Store = InferModel; + +export const products = mysqlTable("products", { + id: serial("id").primaryKey(), + name: text("name"), + price: decimal("price", { precision: 10, scale: 2 }).default("0"), + description: text("description"), + inventory: decimal("inventory").default("0"), + images: json("images"), + storeId: int("store_id") +}); +export type Product = InferModel; + +export const carts = mysqlTable("carts", { + id: serial("id").primaryKey(), + items: json("items"), + paymentIntentId: text("payment_intent_id"), + clientSecret: text("client_secret"), + isClosed: boolean("is_closed").default(false) +}); +export type Cart = InferModel; + +export const payments = mysqlTable("payments", { + id: serial("id").primaryKey(), + storeId: int("store_id"), + stripeAccountId: text("stripe_account_id"), + stripeAccountCreatedAt: int("stripe_account_created_at"), + stripeAccountExpiresAt: int("stripe_account_expires_at"), + details_submitted: boolean("details_submitted").default(false) +}); +export type Payment = InferModel; + +export const orders = mysqlTable( + "orders", + { + id: serial("id").primaryKey(), + prettyOrderId: int("pretty_order_id"), + storeId: int("store_id"), + items: json("items"), + total: decimal("total", { precision: 10, scale: 2 }).default("0"), + stripePaymentIntentId: varchar("stripe_payment_intent_id", { length: 256 }), // text field is valid for uniqueIndex in MySQL + stripePaymentIntentStatus: text("stripe_payment_intent_status"), + name: text("name"), + email: text("email"), + createdAt: int("created_at"), + addressId: int("address") + }, + (table) => { + return { + stripePaymentIntentIdIndex: uniqueIndex( + "stripe_payment_intent_id_index" + ).on(table.stripePaymentIntentId) + }; + } +); +export type Order = InferModel; + +export const addresses = mysqlTable("addresses", { + id: serial("id").primaryKey(), + line1: text("line1"), + line2: text("line2"), + city: text("city"), + state: text("state"), + postal_code: text("postal_code"), + country: text("country") +}); +export type Address = InferModel;