kaddidlehopper 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +139 -0
- package/README.md +47 -0
- package/add-ons/ai/README.md +34 -0
- package/add-ons/ai/assets/_dot_env.local.append +13 -0
- package/add-ons/ai/assets/src/components/AIAssistant.tsx +149 -0
- package/add-ons/ai/assets/src/lib/ai-hook.ts +21 -0
- package/add-ons/ai/assets/src/lib/weather-tools.ts +30 -0
- package/add-ons/ai/assets/src/routes/api.chat.ts +94 -0
- package/add-ons/ai/assets/src/routes/chat.css +175 -0
- package/add-ons/ai/assets/src/routes/chat.tsx +141 -0
- package/add-ons/ai/info.json +27 -0
- package/add-ons/ai/package.json +17 -0
- package/add-ons/ai/small-logo.svg +8 -0
- package/dist/cli.js +251 -0
- package/dist/index.js +33 -0
- package/dist/types/cli.d.ts +8 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/types.d.ts +14 -0
- package/dist/types.js +1 -0
- package/examples/blog/README.md +60 -0
- package/examples/blog/assets/content/posts/beach.md +12 -0
- package/examples/blog/assets/content/posts/jungle.md.ejs +12 -0
- package/examples/blog/assets/content/posts/mountains.md.ejs +12 -0
- package/examples/blog/assets/content/posts/snorkeling.md.ejs +12 -0
- package/examples/blog/assets/content/posts/waterfall.md.ejs +12 -0
- package/examples/blog/assets/content-collections.ts +30 -0
- package/examples/blog/assets/public/beach.jpg +0 -0
- package/examples/blog/assets/public/jungle.jpg +0 -0
- package/examples/blog/assets/public/mountains.jpg +0 -0
- package/examples/blog/assets/public/snorkeling.jpg +0 -0
- package/examples/blog/assets/public/waterfall.jpg +0 -0
- package/examples/blog/assets/src/components/Header.tsx +52 -0
- package/examples/blog/assets/src/components/VacayAssistant.tsx +205 -0
- package/examples/blog/assets/src/components/blog-posts.tsx +78 -0
- package/examples/blog/assets/src/components/ui/card.tsx +92 -0
- package/examples/blog/assets/src/lib/blog-ai-hook.ts +25 -0
- package/examples/blog/assets/src/lib/blog-tools.ts +111 -0
- package/examples/blog/assets/src/lib/utils.ts +6 -0
- package/examples/blog/assets/src/routes/__root.tsx +57 -0
- package/examples/blog/assets/src/routes/api.blog-chat.ts +117 -0
- package/examples/blog/assets/src/routes/category.$category.tsx +19 -0
- package/examples/blog/assets/src/routes/index.tsx +19 -0
- package/examples/blog/assets/src/routes/posts.$slug.tsx +63 -0
- package/examples/blog/assets/src/styles.css +138 -0
- package/examples/blog/info.json +43 -0
- package/examples/blog/package.json +23 -0
- package/examples/events/README.md +110 -0
- package/examples/events/assets/content/speakers/andre-costa.md +22 -0
- package/examples/events/assets/content/speakers/hans-mueller.md.ejs +22 -0
- package/examples/events/assets/content/speakers/isabella-martinez.md.ejs +22 -0
- package/examples/events/assets/content/speakers/kenji-nakamura.md.ejs +22 -0
- package/examples/events/assets/content/speakers/marie-dubois.md.ejs +20 -0
- package/examples/events/assets/content/speakers/priya-sharma.md.ejs +22 -0
- package/examples/events/assets/content/talks/croissant-lamination-secrets.md +39 -0
- package/examples/events/assets/content/talks/french-macaron-mastery.md.ejs +39 -0
- package/examples/events/assets/content/talks/neapolitan-pizza-tradition-meets-innovation.md.ejs +39 -0
- package/examples/events/assets/content/talks/savory-breads-of-the-mediterranean.md.ejs +39 -0
- package/examples/events/assets/content/talks/sourdough-from-starter-to-masterpiece.md.ejs +36 -0
- package/examples/events/assets/content/talks/the-art-of-the-perfect-tart.md.ejs +32 -0
- package/examples/events/assets/content/talks/the-science-of-sugar.md.ejs +39 -0
- package/examples/events/assets/content/talks/umami-in-pastry-east-meets-west.md.ejs +39 -0
- package/examples/events/assets/content-collections.ts +56 -0
- package/examples/events/assets/public/background-1.jpg +0 -0
- package/examples/events/assets/public/background-2.jpg +0 -0
- package/examples/events/assets/public/background-3.jpg +0 -0
- package/examples/events/assets/public/background-4.jpg +0 -0
- package/examples/events/assets/public/conference-logo.png +0 -0
- package/examples/events/assets/public/favicon.ico +0 -0
- package/examples/events/assets/public/speakers/andre-costa.jpg +0 -0
- package/examples/events/assets/public/speakers/hans-mueller.jpg +0 -0
- package/examples/events/assets/public/speakers/isabella-martinez.jpg +0 -0
- package/examples/events/assets/public/speakers/kenji-nakamura.jpg +0 -0
- package/examples/events/assets/public/speakers/marie-dubois.jpg +0 -0
- package/examples/events/assets/public/speakers/priya-sharma.jpg +0 -0
- package/examples/events/assets/public/talks/croissant-lamination-secrets.jpg +0 -0
- package/examples/events/assets/public/talks/french-macaron-mastery.jpg +0 -0
- package/examples/events/assets/public/talks/neapolitan-pizza-tradition-meets-innovation.jpg +0 -0
- package/examples/events/assets/public/talks/savory-breads-of-the-mediterranean.jpg +0 -0
- package/examples/events/assets/public/talks/sourdough-from-starter-to-masterpiece.jpg +0 -0
- package/examples/events/assets/public/talks/the-art-of-the-perfect-tart.jpg +0 -0
- package/examples/events/assets/public/talks/the-science-of-sugar.jpg +0 -0
- package/examples/events/assets/public/talks/umami-in-pastry-east-meets-west.jpg +0 -0
- package/examples/events/assets/public/tanstack-circle-logo.png +0 -0
- package/examples/events/assets/public/tanstack-word-logo-white.svg +1 -0
- package/examples/events/assets/src/components/Header.tsx +59 -0
- package/examples/events/assets/src/components/HeaderNav.tsx +67 -0
- package/examples/events/assets/src/components/HeroCarousel.tsx +61 -0
- package/examples/events/assets/src/components/RemyAssistant.tsx +207 -0
- package/examples/events/assets/src/components/SpeakerCard.tsx +67 -0
- package/examples/events/assets/src/components/TalkCard.tsx +77 -0
- package/examples/events/assets/src/components/ui/card.tsx +92 -0
- package/examples/events/assets/src/lib/conference-ai-hook.ts +26 -0
- package/examples/events/assets/src/lib/conference-tools.ts +210 -0
- package/examples/events/assets/src/lib/model-selection.ts +1 -0
- package/examples/events/assets/src/lib/utils.ts +6 -0
- package/examples/events/assets/src/routes/__root.tsx +70 -0
- package/examples/events/assets/src/routes/api.remy-chat.ts +119 -0
- package/examples/events/assets/src/routes/index.tsx +192 -0
- package/examples/events/assets/src/routes/schedule.index.tsx +274 -0
- package/examples/events/assets/src/routes/speakers.$slug.tsx +122 -0
- package/examples/events/assets/src/routes/speakers.index.tsx +40 -0
- package/examples/events/assets/src/routes/talks.$slug.tsx +116 -0
- package/examples/events/assets/src/routes/talks.index.tsx +40 -0
- package/examples/events/assets/src/styles.css +182 -0
- package/examples/events/info.json +74 -0
- package/examples/events/package.json +23 -0
- package/examples/marketing/README.md +60 -0
- package/examples/marketing/assets/public/logo.png +0 -0
- package/examples/marketing/assets/public/motorcycle-adventure.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-cruiser.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-scooter.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-sport.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-supersport.jpg +0 -0
- package/examples/marketing/assets/src/components/Header.tsx +36 -0
- package/examples/marketing/assets/src/components/MotorcycleAIAssistant.tsx +162 -0
- package/examples/marketing/assets/src/components/MotorcycleRecommendation.tsx +53 -0
- package/examples/marketing/assets/src/data/motorcycles.ts.ejs +77 -0
- package/examples/marketing/assets/src/lib/motorcycle-ai-hook.ts +24 -0
- package/examples/marketing/assets/src/lib/motorcycle-tools.ts +42 -0
- package/examples/marketing/assets/src/routes/__root.tsx +57 -0
- package/examples/marketing/assets/src/routes/api.motorcycle-chat.ts +78 -0
- package/examples/marketing/assets/src/routes/index.tsx +72 -0
- package/examples/marketing/assets/src/routes/motorcycles/$motorcycleId.tsx +56 -0
- package/examples/marketing/assets/src/store/motorcycle-assistant.ts +3 -0
- package/examples/marketing/assets/src/styles.css +212 -0
- package/examples/marketing/info.json +38 -0
- package/examples/marketing/package.json +14 -0
- package/examples/resume/README.md +82 -0
- package/examples/resume/assets/content/education/code-school.md +17 -0
- package/examples/resume/assets/content/jobs/freelance.md.ejs +13 -0
- package/examples/resume/assets/content/jobs/initech-junior.md +20 -0
- package/examples/resume/assets/content/jobs/initech-lead.md.ejs +29 -0
- package/examples/resume/assets/content/jobs/initrode-senior.md.ejs +28 -0
- package/examples/resume/assets/content-collections.ts +36 -0
- package/examples/resume/assets/public/headshot-on-white.jpg +0 -0
- package/examples/resume/assets/src/components/Header.tsx +33 -0
- package/examples/resume/assets/src/components/ResumeAssistant.tsx +193 -0
- package/examples/resume/assets/src/components/ui/badge.tsx +46 -0
- package/examples/resume/assets/src/components/ui/card.tsx +92 -0
- package/examples/resume/assets/src/components/ui/checkbox.tsx +30 -0
- package/examples/resume/assets/src/components/ui/hover-card.tsx +44 -0
- package/examples/resume/assets/src/components/ui/separator.tsx +26 -0
- package/examples/resume/assets/src/lib/resume-ai-hook.ts +21 -0
- package/examples/resume/assets/src/lib/resume-tools.ts +165 -0
- package/examples/resume/assets/src/lib/utils.ts +6 -0
- package/examples/resume/assets/src/routes/api.resume-chat.ts +110 -0
- package/examples/resume/assets/src/routes/index.tsx +220 -0
- package/examples/resume/assets/src/styles.css +138 -0
- package/examples/resume/info.json +25 -0
- package/examples/resume/package.json +26 -0
- package/package.json +39 -0
- package/project/base/_dot_claude/skills/content-collections/SKILL.md +505 -0
- package/project/base/_dot_claude/skills/netlify-blobs/SKILL.md +410 -0
- package/project/base/_dot_claude/skills/netlify-db/SKILL.md +424 -0
- package/project/base/_dot_claude/skills/netlify-debugging/SKILL.md +419 -0
- package/project/base/_dot_claude/skills/netlify-forms/SKILL.md +243 -0
- package/project/base/_dot_claude/skills/netlify-functions/SKILL.md +372 -0
- package/project/base/_dot_claude/skills/tanstack-start-api-routes/SKILL.md +421 -0
- package/project/base/_dot_claude/skills/tanstack-start-loaders/SKILL.md +426 -0
- package/project/base/_dot_claude/skills/tanstack-start-project-setup/SKILL.md +493 -0
- package/project/base/_dot_claude/skills/tanstack-start-routes/SKILL.md +430 -0
- package/project/base/_dot_claude/skills/tanstack-start-server-functions/SKILL.md +445 -0
- package/project/base/_dot_claude/skills/tanstack-start-typesafe-routing/SKILL.md +494 -0
- package/project/base/_dot_gitignore +8 -0
- package/project/base/netlify.toml +7 -0
- package/project/base/package.json +33 -0
- package/project/base/public/favicon.ico +0 -0
- package/project/base/public/tanstack-circle-logo.png +0 -0
- package/project/base/public/tanstack-word-logo-white.svg +1 -0
- package/project/base/src/components/Header.tsx +17 -0
- package/project/base/src/components/HeaderNav.tsx.ejs +179 -0
- package/project/base/src/router.tsx +15 -0
- package/project/base/src/routes/__root.tsx +57 -0
- package/project/base/src/routes/index.tsx +48 -0
- package/project/base/src/styles.css +15 -0
- package/project/base/tsconfig.json +28 -0
- package/project/base/vite.config.ts.ejs +25 -0
- package/project/packages.json +22 -0
- package/scripts/check-outdated-packages.js +421 -0
- package/src/cli.ts +343 -0
- package/src/index.ts +49 -0
- package/src/types.ts +15 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: netlify-db
|
|
3
|
+
description: Use Netlify DB, a managed Postgres database powered by Neon. Use when you need relational database capabilities, SQL queries, or persistent structured data storage on Netlify.
|
|
4
|
+
license: Apache-2.0
|
|
5
|
+
metadata:
|
|
6
|
+
author: netlify
|
|
7
|
+
version: "1.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Netlify DB
|
|
11
|
+
|
|
12
|
+
Netlify DB provides instant Postgres database instances powered by Neon. It auto-connects to your functions with zero configuration.
|
|
13
|
+
|
|
14
|
+
## When to Use
|
|
15
|
+
|
|
16
|
+
- Relational data storage
|
|
17
|
+
- Complex queries with joins
|
|
18
|
+
- ACID transactions
|
|
19
|
+
- Structured data with schemas
|
|
20
|
+
- SQL-based data access
|
|
21
|
+
|
|
22
|
+
## Quick Setup
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Initialize Netlify DB for your project
|
|
26
|
+
netlify db:init
|
|
27
|
+
|
|
28
|
+
# This creates:
|
|
29
|
+
# - A Neon Postgres database
|
|
30
|
+
# - DATABASE_URL environment variable
|
|
31
|
+
# - drizzle.config.ts (if using Drizzle)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Using with Drizzle ORM (Recommended)
|
|
35
|
+
|
|
36
|
+
### Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install drizzle-orm @neondatabase/serverless
|
|
40
|
+
npm install -D drizzle-kit
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Schema Definition
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// src/db/schema.ts
|
|
47
|
+
import { pgTable, serial, text, timestamp, integer, boolean } from "drizzle-orm/pg-core";
|
|
48
|
+
|
|
49
|
+
export const users = pgTable("users", {
|
|
50
|
+
id: serial("id").primaryKey(),
|
|
51
|
+
email: text("email").notNull().unique(),
|
|
52
|
+
name: text("name").notNull(),
|
|
53
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const posts = pgTable("posts", {
|
|
57
|
+
id: serial("id").primaryKey(),
|
|
58
|
+
title: text("title").notNull(),
|
|
59
|
+
content: text("content"),
|
|
60
|
+
published: boolean("published").default(false),
|
|
61
|
+
authorId: integer("author_id").references(() => users.id),
|
|
62
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
63
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Drizzle Configuration
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// drizzle.config.ts
|
|
71
|
+
import { defineConfig } from "drizzle-kit";
|
|
72
|
+
|
|
73
|
+
export default defineConfig({
|
|
74
|
+
schema: "./src/db/schema.ts",
|
|
75
|
+
out: "./drizzle",
|
|
76
|
+
dialect: "postgresql",
|
|
77
|
+
dbCredentials: {
|
|
78
|
+
url: process.env.DATABASE_URL!,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Database Client
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// src/db/index.ts
|
|
87
|
+
import { drizzle } from "drizzle-orm/neon-http";
|
|
88
|
+
import { neon } from "@neondatabase/serverless";
|
|
89
|
+
import * as schema from "./schema";
|
|
90
|
+
|
|
91
|
+
const sql = neon(process.env.DATABASE_URL!);
|
|
92
|
+
export const db = drizzle(sql, { schema });
|
|
93
|
+
|
|
94
|
+
export { schema };
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Migrations
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# Generate migration from schema changes
|
|
101
|
+
npx drizzle-kit generate
|
|
102
|
+
|
|
103
|
+
# Push schema directly to database (development)
|
|
104
|
+
npx drizzle-kit push
|
|
105
|
+
|
|
106
|
+
# Run migrations
|
|
107
|
+
npx drizzle-kit migrate
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Basic Queries with Drizzle
|
|
111
|
+
|
|
112
|
+
### Create
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { db, schema } from "./db";
|
|
116
|
+
|
|
117
|
+
// Insert single record
|
|
118
|
+
const newUser = await db.insert(schema.users).values({
|
|
119
|
+
email: "alice@example.com",
|
|
120
|
+
name: "Alice",
|
|
121
|
+
}).returning();
|
|
122
|
+
|
|
123
|
+
// Insert multiple records
|
|
124
|
+
await db.insert(schema.posts).values([
|
|
125
|
+
{ title: "First Post", authorId: newUser[0].id },
|
|
126
|
+
{ title: "Second Post", authorId: newUser[0].id },
|
|
127
|
+
]);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Read
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { db, schema } from "./db";
|
|
134
|
+
import { eq, and, like, desc } from "drizzle-orm";
|
|
135
|
+
|
|
136
|
+
// Get all users
|
|
137
|
+
const allUsers = await db.select().from(schema.users);
|
|
138
|
+
|
|
139
|
+
// Get user by ID
|
|
140
|
+
const user = await db
|
|
141
|
+
.select()
|
|
142
|
+
.from(schema.users)
|
|
143
|
+
.where(eq(schema.users.id, 1));
|
|
144
|
+
|
|
145
|
+
// Get with conditions
|
|
146
|
+
const publishedPosts = await db
|
|
147
|
+
.select()
|
|
148
|
+
.from(schema.posts)
|
|
149
|
+
.where(
|
|
150
|
+
and(
|
|
151
|
+
eq(schema.posts.published, true),
|
|
152
|
+
like(schema.posts.title, "%Tutorial%")
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
.orderBy(desc(schema.posts.createdAt))
|
|
156
|
+
.limit(10);
|
|
157
|
+
|
|
158
|
+
// Join tables
|
|
159
|
+
const postsWithAuthors = await db
|
|
160
|
+
.select({
|
|
161
|
+
postTitle: schema.posts.title,
|
|
162
|
+
authorName: schema.users.name,
|
|
163
|
+
})
|
|
164
|
+
.from(schema.posts)
|
|
165
|
+
.leftJoin(schema.users, eq(schema.posts.authorId, schema.users.id));
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Update
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { db, schema } from "./db";
|
|
172
|
+
import { eq } from "drizzle-orm";
|
|
173
|
+
|
|
174
|
+
// Update single field
|
|
175
|
+
await db
|
|
176
|
+
.update(schema.posts)
|
|
177
|
+
.set({ published: true })
|
|
178
|
+
.where(eq(schema.posts.id, 1));
|
|
179
|
+
|
|
180
|
+
// Update multiple fields
|
|
181
|
+
await db
|
|
182
|
+
.update(schema.users)
|
|
183
|
+
.set({
|
|
184
|
+
name: "Alice Smith",
|
|
185
|
+
updatedAt: new Date(),
|
|
186
|
+
})
|
|
187
|
+
.where(eq(schema.users.email, "alice@example.com"));
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Delete
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import { db, schema } from "./db";
|
|
194
|
+
import { eq, lt } from "drizzle-orm";
|
|
195
|
+
|
|
196
|
+
// Delete single record
|
|
197
|
+
await db.delete(schema.posts).where(eq(schema.posts.id, 1));
|
|
198
|
+
|
|
199
|
+
// Delete with condition
|
|
200
|
+
await db
|
|
201
|
+
.delete(schema.posts)
|
|
202
|
+
.where(
|
|
203
|
+
and(
|
|
204
|
+
eq(schema.posts.published, false),
|
|
205
|
+
lt(schema.posts.createdAt, thirtyDaysAgo)
|
|
206
|
+
)
|
|
207
|
+
);
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Using in Netlify Functions
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// netlify/functions/users.ts
|
|
214
|
+
import type { Context } from "@netlify/functions";
|
|
215
|
+
import { db, schema } from "../../src/db";
|
|
216
|
+
import { eq } from "drizzle-orm";
|
|
217
|
+
|
|
218
|
+
export default async (request: Request, context: Context) => {
|
|
219
|
+
const url = new URL(request.url);
|
|
220
|
+
|
|
221
|
+
switch (request.method) {
|
|
222
|
+
case "GET": {
|
|
223
|
+
const id = url.searchParams.get("id");
|
|
224
|
+
|
|
225
|
+
if (id) {
|
|
226
|
+
const user = await db
|
|
227
|
+
.select()
|
|
228
|
+
.from(schema.users)
|
|
229
|
+
.where(eq(schema.users.id, parseInt(id)));
|
|
230
|
+
|
|
231
|
+
if (!user.length) {
|
|
232
|
+
return new Response("User not found", { status: 404 });
|
|
233
|
+
}
|
|
234
|
+
return Response.json(user[0]);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const users = await db.select().from(schema.users);
|
|
238
|
+
return Response.json(users);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
case "POST": {
|
|
242
|
+
const body = await request.json();
|
|
243
|
+
const newUser = await db
|
|
244
|
+
.insert(schema.users)
|
|
245
|
+
.values(body)
|
|
246
|
+
.returning();
|
|
247
|
+
|
|
248
|
+
return Response.json(newUser[0], { status: 201 });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
case "DELETE": {
|
|
252
|
+
const id = url.searchParams.get("id");
|
|
253
|
+
if (!id) {
|
|
254
|
+
return new Response("ID required", { status: 400 });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
await db.delete(schema.users).where(eq(schema.users.id, parseInt(id)));
|
|
258
|
+
return new Response(null, { status: 204 });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
default:
|
|
262
|
+
return new Response("Method not allowed", { status: 405 });
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Using Raw SQL (Without ORM)
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
import { neon } from "@neondatabase/serverless";
|
|
271
|
+
|
|
272
|
+
const sql = neon(process.env.DATABASE_URL!);
|
|
273
|
+
|
|
274
|
+
// Simple query
|
|
275
|
+
const users = await sql`SELECT * FROM users`;
|
|
276
|
+
|
|
277
|
+
// Parameterized query (safe from SQL injection)
|
|
278
|
+
const user = await sql`
|
|
279
|
+
SELECT * FROM users WHERE id = ${userId}
|
|
280
|
+
`;
|
|
281
|
+
|
|
282
|
+
// Insert
|
|
283
|
+
await sql`
|
|
284
|
+
INSERT INTO users (email, name)
|
|
285
|
+
VALUES (${email}, ${name})
|
|
286
|
+
`;
|
|
287
|
+
|
|
288
|
+
// Transaction
|
|
289
|
+
const result = await sql.transaction([
|
|
290
|
+
sql`INSERT INTO users (email, name) VALUES (${email}, ${name}) RETURNING id`,
|
|
291
|
+
sql`INSERT INTO profiles (user_id, bio) VALUES (${userId}, ${bio})`,
|
|
292
|
+
]);
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Transactions with Drizzle
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
import { db, schema } from "./db";
|
|
299
|
+
|
|
300
|
+
// Transaction ensures all operations succeed or none do
|
|
301
|
+
await db.transaction(async (tx) => {
|
|
302
|
+
const [user] = await tx
|
|
303
|
+
.insert(schema.users)
|
|
304
|
+
.values({ email: "bob@example.com", name: "Bob" })
|
|
305
|
+
.returning();
|
|
306
|
+
|
|
307
|
+
await tx.insert(schema.posts).values({
|
|
308
|
+
title: "Bob's First Post",
|
|
309
|
+
authorId: user.id,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// If any operation fails, all are rolled back
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Common Patterns
|
|
317
|
+
|
|
318
|
+
### Soft Deletes
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
// Schema with soft delete
|
|
322
|
+
export const posts = pgTable("posts", {
|
|
323
|
+
id: serial("id").primaryKey(),
|
|
324
|
+
title: text("title").notNull(),
|
|
325
|
+
deletedAt: timestamp("deleted_at"),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Query only non-deleted
|
|
329
|
+
const activePosts = await db
|
|
330
|
+
.select()
|
|
331
|
+
.from(schema.posts)
|
|
332
|
+
.where(isNull(schema.posts.deletedAt));
|
|
333
|
+
|
|
334
|
+
// Soft delete
|
|
335
|
+
await db
|
|
336
|
+
.update(schema.posts)
|
|
337
|
+
.set({ deletedAt: new Date() })
|
|
338
|
+
.where(eq(schema.posts.id, postId));
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Pagination
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
async function getPaginatedPosts(page: number, pageSize: number = 10) {
|
|
345
|
+
const offset = (page - 1) * pageSize;
|
|
346
|
+
|
|
347
|
+
const [posts, countResult] = await Promise.all([
|
|
348
|
+
db
|
|
349
|
+
.select()
|
|
350
|
+
.from(schema.posts)
|
|
351
|
+
.orderBy(desc(schema.posts.createdAt))
|
|
352
|
+
.limit(pageSize)
|
|
353
|
+
.offset(offset),
|
|
354
|
+
db
|
|
355
|
+
.select({ count: sql<number>`count(*)` })
|
|
356
|
+
.from(schema.posts),
|
|
357
|
+
]);
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
data: posts,
|
|
361
|
+
page,
|
|
362
|
+
pageSize,
|
|
363
|
+
total: countResult[0].count,
|
|
364
|
+
totalPages: Math.ceil(countResult[0].count / pageSize),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Full-Text Search
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
// Use PostgreSQL's full-text search
|
|
373
|
+
const results = await sql`
|
|
374
|
+
SELECT * FROM posts
|
|
375
|
+
WHERE to_tsvector('english', title || ' ' || content)
|
|
376
|
+
@@ plainto_tsquery('english', ${searchQuery})
|
|
377
|
+
ORDER BY ts_rank(
|
|
378
|
+
to_tsvector('english', title || ' ' || content),
|
|
379
|
+
plainto_tsquery('english', ${searchQuery})
|
|
380
|
+
) DESC
|
|
381
|
+
LIMIT 20
|
|
382
|
+
`;
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
## CLI Commands
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
# Initialize database
|
|
389
|
+
netlify db:init
|
|
390
|
+
|
|
391
|
+
# Open database in Neon console
|
|
392
|
+
netlify db:open
|
|
393
|
+
|
|
394
|
+
# Run migrations
|
|
395
|
+
netlify db:migrate
|
|
396
|
+
|
|
397
|
+
# Pull schema from existing database
|
|
398
|
+
netlify db:pull
|
|
399
|
+
|
|
400
|
+
# Push schema to database
|
|
401
|
+
netlify db:push
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Environment Variables
|
|
405
|
+
|
|
406
|
+
- `DATABASE_URL` - Automatically set by Netlify DB
|
|
407
|
+
- Format: `postgres://user:password@host/database?sslmode=require`
|
|
408
|
+
|
|
409
|
+
## Local Development
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
# Run with Netlify Dev (auto-connects to database)
|
|
413
|
+
netlify dev
|
|
414
|
+
|
|
415
|
+
# Or set DATABASE_URL in .env for local testing
|
|
416
|
+
DATABASE_URL=postgres://user:pass@localhost:5432/mydb
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## Limits
|
|
420
|
+
|
|
421
|
+
- Based on Neon's Postgres offering
|
|
422
|
+
- Connection pooling handled automatically
|
|
423
|
+
- Serverless-optimized (connections close between requests)
|
|
424
|
+
- See Neon documentation for specific limits
|