create-atsdc-stack 1.0.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/.claude/settings.local.json +9 -0
- package/CONTRIBUTING.md +342 -0
- package/INSTALLATION.md +359 -0
- package/LICENSE +201 -0
- package/README.md +405 -0
- package/app/.astro/settings.json +5 -0
- package/app/.astro/types.d.ts +1 -0
- package/app/.env.example +17 -0
- package/app/README.md +251 -0
- package/app/astro.config.mjs +83 -0
- package/app/drizzle.config.ts +16 -0
- package/app/package.json +52 -0
- package/app/public/manifest.webmanifest +36 -0
- package/app/src/components/Card.astro +36 -0
- package/app/src/db/initialize.ts +107 -0
- package/app/src/db/schema.ts +72 -0
- package/app/src/db/validations.ts +158 -0
- package/app/src/env.d.ts +1 -0
- package/app/src/layouts/Layout.astro +63 -0
- package/app/src/lib/config.ts +36 -0
- package/app/src/lib/content-converter.ts +141 -0
- package/app/src/lib/dom-utils.ts +230 -0
- package/app/src/lib/exa-search.ts +269 -0
- package/app/src/pages/api/chat.ts +91 -0
- package/app/src/pages/api/posts.ts +350 -0
- package/app/src/pages/index.astro +87 -0
- package/app/src/styles/components/button.scss +152 -0
- package/app/src/styles/components/card.scss +180 -0
- package/app/src/styles/components/form.scss +240 -0
- package/app/src/styles/global.scss +141 -0
- package/app/src/styles/pages/index.scss +80 -0
- package/app/src/styles/reset.scss +83 -0
- package/app/src/styles/variables/globals.scss +96 -0
- package/app/src/styles/variables/mixins.scss +238 -0
- package/app/tsconfig.json +45 -0
- package/bin/cli.js +1138 -0
- package/package.json +37 -0
package/app/README.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# ATSDC Stack Application
|
|
2
|
+
|
|
3
|
+
This is the main Astro application for the ATSDC Stack.
|
|
4
|
+
|
|
5
|
+
## 🚀 Quick Start
|
|
6
|
+
|
|
7
|
+
### Prerequisites
|
|
8
|
+
|
|
9
|
+
- Node.js >= 18.0.0
|
|
10
|
+
- PostgreSQL database (Vercel Postgres, Neon, or local)
|
|
11
|
+
- API keys for Clerk, OpenAI, and optionally Exa
|
|
12
|
+
|
|
13
|
+
### Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Install dependencies
|
|
17
|
+
npm install
|
|
18
|
+
|
|
19
|
+
# Copy environment template
|
|
20
|
+
cp .env.example .env
|
|
21
|
+
|
|
22
|
+
# Configure your .env file with your credentials
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Environment Variables
|
|
26
|
+
|
|
27
|
+
Create a `.env` file with the following variables:
|
|
28
|
+
|
|
29
|
+
```env
|
|
30
|
+
# Database
|
|
31
|
+
DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
|
|
32
|
+
|
|
33
|
+
# Clerk Authentication
|
|
34
|
+
PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_..."
|
|
35
|
+
CLERK_SECRET_KEY="sk_test_..."
|
|
36
|
+
|
|
37
|
+
# OpenAI (for Vercel AI SDK)
|
|
38
|
+
OPENAI_API_KEY="sk-..."
|
|
39
|
+
|
|
40
|
+
# Exa Search (optional)
|
|
41
|
+
EXA_API_KEY="..."
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Database Setup
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Push schema to database
|
|
48
|
+
npm run db:push
|
|
49
|
+
|
|
50
|
+
# Or generate migrations
|
|
51
|
+
npm run db:generate
|
|
52
|
+
npm run db:migrate
|
|
53
|
+
|
|
54
|
+
# Open Drizzle Studio (database GUI)
|
|
55
|
+
npm run db:studio
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Development
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Start dev server
|
|
62
|
+
npm run dev
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Visit `http://localhost:4321`
|
|
66
|
+
|
|
67
|
+
## 📝 Available Scripts
|
|
68
|
+
|
|
69
|
+
- `npm run dev` - Start development server
|
|
70
|
+
- `npm run build` - Build for production
|
|
71
|
+
- `npm run preview` - Preview production build
|
|
72
|
+
- `npm run astro` - Run Astro CLI commands
|
|
73
|
+
- `npm run db:generate` - Generate database migrations
|
|
74
|
+
- `npm run db:migrate` - Run database migrations
|
|
75
|
+
- `npm run db:push` - Push schema changes to database
|
|
76
|
+
- `npm run db:studio` - Open Drizzle Studio
|
|
77
|
+
|
|
78
|
+
## 📁 Project Structure
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
src/
|
|
82
|
+
├── components/ # Reusable Astro components
|
|
83
|
+
├── db/ # Database schema and client
|
|
84
|
+
│ ├── initialize.ts # Database initialization
|
|
85
|
+
│ ├── schema.ts # Drizzle ORM schemas
|
|
86
|
+
│ └── validations.ts # Zod validation schemas
|
|
87
|
+
├── layouts/ # Page layouts
|
|
88
|
+
│ └── Layout.astro
|
|
89
|
+
├── lib/ # Utility libraries
|
|
90
|
+
│ ├── config.ts # App configuration
|
|
91
|
+
│ ├── content-converter.ts # Markdown/HTML conversion
|
|
92
|
+
│ ├── dom-utils.ts # DOM manipulation
|
|
93
|
+
│ └── exa-search.ts # AI-powered search
|
|
94
|
+
├── pages/ # Routes and pages
|
|
95
|
+
│ ├── api/ # API endpoints
|
|
96
|
+
│ │ ├── chat.ts # AI chat endpoint
|
|
97
|
+
│ │ └── posts.ts # Posts CRUD
|
|
98
|
+
│ └── index.astro # Home page
|
|
99
|
+
└── styles/ # SCSS stylesheets
|
|
100
|
+
├── variables/ # SCSS variables and mixins
|
|
101
|
+
├── components/ # Component styles
|
|
102
|
+
├── pages/ # Page styles
|
|
103
|
+
├── reset.scss # CSS reset
|
|
104
|
+
└── global.scss # Global styles
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## 🎨 SCSS Architecture
|
|
108
|
+
|
|
109
|
+
This app uses a strict SCSS architecture:
|
|
110
|
+
|
|
111
|
+
- **No inline `<style>` tags** in `.astro` files
|
|
112
|
+
- **All styles in external SCSS files** for better maintainability
|
|
113
|
+
- **Data attributes for modifiers** (preferred over BEM)
|
|
114
|
+
- **Semantic class names** (no utility classes)
|
|
115
|
+
|
|
116
|
+
Example:
|
|
117
|
+
|
|
118
|
+
```astro
|
|
119
|
+
---
|
|
120
|
+
import '@/styles/components/button.scss';
|
|
121
|
+
---
|
|
122
|
+
<button class="btn" data-variant="primary" data-size="lg">
|
|
123
|
+
Click Me
|
|
124
|
+
</button>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## 🗄️ Database
|
|
128
|
+
|
|
129
|
+
### Schema Definition
|
|
130
|
+
|
|
131
|
+
Define your database schema in `src/db/schema.ts` using Drizzle ORM:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
export const posts = pgTable('posts', {
|
|
135
|
+
id: varchar('id', { length: 21 })
|
|
136
|
+
.primaryKey()
|
|
137
|
+
.$defaultFn(() => nanoid()),
|
|
138
|
+
title: varchar('title', { length: 255 }).notNull(),
|
|
139
|
+
content: text('content').notNull(),
|
|
140
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Validation
|
|
145
|
+
|
|
146
|
+
Define Zod schemas in `src/db/validations.ts`:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
export const createPostSchema = z.object({
|
|
150
|
+
title: z.string().min(1).max(255),
|
|
151
|
+
content: z.string().min(1),
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## 🔐 Authentication
|
|
156
|
+
|
|
157
|
+
Authentication is handled by Clerk. Configure in `astro.config.mjs`:
|
|
158
|
+
|
|
159
|
+
```javascript
|
|
160
|
+
clerk({
|
|
161
|
+
afterSignInUrl: '/',
|
|
162
|
+
afterSignUpUrl: '/',
|
|
163
|
+
})
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## 🤖 AI Features
|
|
167
|
+
|
|
168
|
+
### Vercel AI SDK
|
|
169
|
+
|
|
170
|
+
Chat endpoint example in `src/pages/api/chat.ts`:
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import { OpenAI } from 'ai';
|
|
174
|
+
|
|
175
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
176
|
+
// AI chat implementation
|
|
177
|
+
};
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Exa Search
|
|
181
|
+
|
|
182
|
+
AI-powered search utilities in `src/lib/exa-search.ts`.
|
|
183
|
+
|
|
184
|
+
## 📱 Progressive Web App
|
|
185
|
+
|
|
186
|
+
This app includes PWA support with offline capabilities:
|
|
187
|
+
|
|
188
|
+
- Service worker auto-generated
|
|
189
|
+
- Installable on mobile/desktop
|
|
190
|
+
- Offline caching configured in `astro.config.mjs`
|
|
191
|
+
|
|
192
|
+
## 🚀 Deployment
|
|
193
|
+
|
|
194
|
+
### Vercel (Recommended)
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# Install Vercel CLI
|
|
198
|
+
npm i -g vercel
|
|
199
|
+
|
|
200
|
+
# Deploy
|
|
201
|
+
vercel
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Make sure to set these environment variables in your Vercel project settings:
|
|
205
|
+
|
|
206
|
+
- `DATABASE_URL`
|
|
207
|
+
- `PUBLIC_CLERK_PUBLISHABLE_KEY`
|
|
208
|
+
- `CLERK_SECRET_KEY`
|
|
209
|
+
- `OPENAI_API_KEY`
|
|
210
|
+
- `EXA_API_KEY` (optional)
|
|
211
|
+
|
|
212
|
+
## 📚 Documentation
|
|
213
|
+
|
|
214
|
+
- [Astro Documentation](https://docs.astro.build)
|
|
215
|
+
- [Drizzle ORM](https://orm.drizzle.team)
|
|
216
|
+
- [Clerk](https://clerk.com/docs)
|
|
217
|
+
- [Vercel AI SDK](https://sdk.vercel.ai/docs)
|
|
218
|
+
- [Zod](https://zod.dev)
|
|
219
|
+
- [Exa Search](https://docs.exa.ai)
|
|
220
|
+
|
|
221
|
+
## 🛠️ Utilities
|
|
222
|
+
|
|
223
|
+
### Content Conversion
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
import { htmlToMarkdown, markdownToHtml } from '@/lib/content-converter';
|
|
227
|
+
|
|
228
|
+
const markdown = htmlToMarkdown('<h1>Hello</h1>');
|
|
229
|
+
const html = markdownToHtml('# Hello');
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### DOM Manipulation
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { extractText, findLinks } from '@/lib/dom-utils';
|
|
236
|
+
|
|
237
|
+
const text = extractText(htmlString);
|
|
238
|
+
const links = findLinks(htmlString);
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### AI Search
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
import { searchWithExa } from '@/lib/exa-search';
|
|
245
|
+
|
|
246
|
+
const results = await searchWithExa('your query');
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## 📄 License
|
|
250
|
+
|
|
251
|
+
MIT
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { defineConfig } from 'astro/config';
|
|
2
|
+
import react from '@astrojs/react';
|
|
3
|
+
import vercel from '@astrojs/vercel';
|
|
4
|
+
import clerk from '@clerk/astro';
|
|
5
|
+
import { VitePWA } from 'vite-plugin-pwa';
|
|
6
|
+
|
|
7
|
+
// https://astro.build/config
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
output: 'server',
|
|
10
|
+
adapter: vercel({
|
|
11
|
+
imageService: true,
|
|
12
|
+
}),
|
|
13
|
+
image: {
|
|
14
|
+
service: {
|
|
15
|
+
entrypoint: 'astro/assets/services/noop',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
integrations: [
|
|
19
|
+
react(),
|
|
20
|
+
clerk({
|
|
21
|
+
afterSignInUrl: '/',
|
|
22
|
+
afterSignUpUrl: '/',
|
|
23
|
+
}),
|
|
24
|
+
],
|
|
25
|
+
vite: {
|
|
26
|
+
plugins: [
|
|
27
|
+
VitePWA({
|
|
28
|
+
registerType: 'autoUpdate',
|
|
29
|
+
includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'mask-icon.svg'],
|
|
30
|
+
manifest: {
|
|
31
|
+
name: 'ATSDC Stack App',
|
|
32
|
+
short_name: 'ATSDC',
|
|
33
|
+
description: 'Progressive Web App built with the ATSDC Stack',
|
|
34
|
+
theme_color: '#ffffff',
|
|
35
|
+
background_color: '#ffffff',
|
|
36
|
+
display: 'standalone',
|
|
37
|
+
icons: [
|
|
38
|
+
{
|
|
39
|
+
src: 'pwa-192x192.png',
|
|
40
|
+
sizes: '192x192',
|
|
41
|
+
type: 'image/png',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
src: 'pwa-512x512.png',
|
|
45
|
+
sizes: '512x512',
|
|
46
|
+
type: 'image/png',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
src: 'pwa-512x512.png',
|
|
50
|
+
sizes: '512x512',
|
|
51
|
+
type: 'image/png',
|
|
52
|
+
purpose: 'any maskable',
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
workbox: {
|
|
57
|
+
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
|
|
58
|
+
runtimeCaching: [
|
|
59
|
+
{
|
|
60
|
+
urlPattern: /^https:\/\/api\./i,
|
|
61
|
+
handler: 'NetworkFirst',
|
|
62
|
+
options: {
|
|
63
|
+
cacheName: 'api-cache',
|
|
64
|
+
expiration: {
|
|
65
|
+
maxEntries: 50,
|
|
66
|
+
maxAgeSeconds: 60 * 60 * 24, // 24 hours
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
],
|
|
74
|
+
css: {
|
|
75
|
+
preprocessorOptions: {
|
|
76
|
+
scss: {
|
|
77
|
+
api: 'modern-compiler',
|
|
78
|
+
additionalData: `@use "@/styles/variables/globals.scss" as *;`,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Config } from 'drizzle-kit';
|
|
2
|
+
|
|
3
|
+
if (!process.env.DATABASE_URL) {
|
|
4
|
+
throw new Error('DATABASE_URL environment variable is required');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
schema: './src/db/schema.ts',
|
|
9
|
+
out: './drizzle',
|
|
10
|
+
dialect: 'postgresql',
|
|
11
|
+
dbCredentials: {
|
|
12
|
+
url: process.env.DATABASE_URL,
|
|
13
|
+
},
|
|
14
|
+
verbose: true,
|
|
15
|
+
strict: true,
|
|
16
|
+
} satisfies Config;
|
package/app/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "atsdc-app",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ATSDC Stack - Astro application",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "astro dev",
|
|
8
|
+
"build": "astro check && astro build",
|
|
9
|
+
"preview": "astro preview",
|
|
10
|
+
"astro": "astro",
|
|
11
|
+
"db:generate": "drizzle-kit generate",
|
|
12
|
+
"db:migrate": "drizzle-kit migrate",
|
|
13
|
+
"db:push": "drizzle-kit push",
|
|
14
|
+
"db:studio": "drizzle-kit studio"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@astrojs/check": "^0.9.6",
|
|
18
|
+
"@astrojs/react": "^4.4.2",
|
|
19
|
+
"@astrojs/vercel": "^9.0.2",
|
|
20
|
+
"@clerk/astro": "^1.4.3",
|
|
21
|
+
"@clerk/clerk-react": "^5.17.3",
|
|
22
|
+
"@rocicorp/zero": "^0.2.0",
|
|
23
|
+
"@vercel/postgres": "^0.10.0",
|
|
24
|
+
"ai": "^5.0.0",
|
|
25
|
+
"astro": "^5.16.6",
|
|
26
|
+
"cheerio": "^1.0.0",
|
|
27
|
+
"drizzle-orm": "^0.36.4",
|
|
28
|
+
"exa-js": "^1.1.1",
|
|
29
|
+
"marked": "^14.1.3",
|
|
30
|
+
"nanoid": "^5.0.9",
|
|
31
|
+
"open-props": "^1.7.17",
|
|
32
|
+
"react": "^18.3.1",
|
|
33
|
+
"react-dom": "^18.3.1",
|
|
34
|
+
"sass": "^1.82.0",
|
|
35
|
+
"turndown": "^7.2.0",
|
|
36
|
+
"typescript": "^5.7.2",
|
|
37
|
+
"zod": "^3.24.1"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^22.10.5",
|
|
41
|
+
"@types/react": "^18.3.18",
|
|
42
|
+
"@types/react-dom": "^18.3.5",
|
|
43
|
+
"@types/turndown": "^5.0.5",
|
|
44
|
+
"@vite-pwa/assets-generator": "^0.2.6",
|
|
45
|
+
"drizzle-kit": "^0.29.1",
|
|
46
|
+
"vercel": "^39.2.0",
|
|
47
|
+
"vite-plugin-pwa": "^0.21.2"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ATSDC Stack App",
|
|
3
|
+
"short_name": "ATSDC",
|
|
4
|
+
"description": "Progressive Web App built with the ATSDC Stack",
|
|
5
|
+
"start_url": "/",
|
|
6
|
+
"display": "standalone",
|
|
7
|
+
"background_color": "#ffffff",
|
|
8
|
+
"theme_color": "#4f46e5",
|
|
9
|
+
"orientation": "portrait-primary",
|
|
10
|
+
"icons": [
|
|
11
|
+
{
|
|
12
|
+
"src": "/pwa-192x192.png",
|
|
13
|
+
"sizes": "192x192",
|
|
14
|
+
"type": "image/png",
|
|
15
|
+
"purpose": "any"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"src": "/pwa-512x512.png",
|
|
19
|
+
"sizes": "512x512",
|
|
20
|
+
"type": "image/png",
|
|
21
|
+
"purpose": "any"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"src": "/pwa-512x512.png",
|
|
25
|
+
"sizes": "512x512",
|
|
26
|
+
"type": "image/png",
|
|
27
|
+
"purpose": "maskable"
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"categories": [
|
|
31
|
+
"productivity",
|
|
32
|
+
"utilities"
|
|
33
|
+
],
|
|
34
|
+
"screenshots": [],
|
|
35
|
+
"shortcuts": []
|
|
36
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Card Component
|
|
4
|
+
* Demonstrates proper SCSS architecture:
|
|
5
|
+
* - No <style> tag in this file
|
|
6
|
+
* - Styles imported from external SCSS file
|
|
7
|
+
* - Reusable component with props
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
title?: string;
|
|
12
|
+
href?: string;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { title, href, class: className = '' } = Astro.props;
|
|
17
|
+
|
|
18
|
+
// Import component-specific styles
|
|
19
|
+
import '@/styles/components/card.scss';
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
{href ? (
|
|
23
|
+
<a href={href} class={`card-component ${className}`}>
|
|
24
|
+
{title && <h3 class="card-component__title">{title}</h3>}
|
|
25
|
+
<div class="card-component__content">
|
|
26
|
+
<slot />
|
|
27
|
+
</div>
|
|
28
|
+
</a>
|
|
29
|
+
) : (
|
|
30
|
+
<div class={`card-component ${className}`}>
|
|
31
|
+
{title && <h3 class="card-component__title">{title}</h3>}
|
|
32
|
+
<div class="card-component__content">
|
|
33
|
+
<slot />
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
)}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { sql } from '@vercel/postgres';
|
|
2
|
+
import { drizzle } from 'drizzle-orm/vercel-postgres';
|
|
3
|
+
import * as schema from './schema';
|
|
4
|
+
import { posts, comments } from './schema';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Database client configuration
|
|
8
|
+
* Uses Vercel Postgres for production-ready connection pooling
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Create Drizzle instance with schema
|
|
12
|
+
export const db = drizzle(sql, { schema });
|
|
13
|
+
|
|
14
|
+
// Export schema for convenience
|
|
15
|
+
export { schema };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Database initialization utilities
|
|
19
|
+
* Handles table creation, migrations, and initial setup
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if the database tables exist by attempting a simple query
|
|
24
|
+
*/
|
|
25
|
+
export async function checkTablesExist(): Promise<boolean> {
|
|
26
|
+
try {
|
|
27
|
+
await db.select().from(posts).limit(1);
|
|
28
|
+
return true;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error('Error checking if tables exist:', error);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Initialize the database by creating tables if they don't exist
|
|
37
|
+
* Note: In production, use Drizzle migrations instead
|
|
38
|
+
*/
|
|
39
|
+
export async function initializeDatabase(): Promise<void> {
|
|
40
|
+
try {
|
|
41
|
+
const tablesExist = await checkTablesExist();
|
|
42
|
+
|
|
43
|
+
if (!tablesExist) {
|
|
44
|
+
console.log('Database tables do not exist. Please run migrations using:');
|
|
45
|
+
console.log(' npm run db:push');
|
|
46
|
+
console.log(' or');
|
|
47
|
+
console.log(' npm run db:migrate');
|
|
48
|
+
throw new Error('Database tables not initialized');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log('Database tables verified successfully');
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error('Database initialization failed:', error);
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Verify database connection
|
|
60
|
+
*/
|
|
61
|
+
export async function verifyConnection(): Promise<boolean> {
|
|
62
|
+
try {
|
|
63
|
+
await db.select().from(posts).limit(1);
|
|
64
|
+
return true;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('Database connection failed:', error);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get database health status
|
|
73
|
+
*/
|
|
74
|
+
export async function getDatabaseHealth(): Promise<{
|
|
75
|
+
connected: boolean;
|
|
76
|
+
tablesExist: boolean;
|
|
77
|
+
timestamp: Date;
|
|
78
|
+
}> {
|
|
79
|
+
const connected = await verifyConnection();
|
|
80
|
+
const tablesExist = connected ? await checkTablesExist() : false;
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
connected,
|
|
84
|
+
tablesExist,
|
|
85
|
+
timestamp: new Date(),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Seed initial data (optional - for development)
|
|
91
|
+
*/
|
|
92
|
+
export async function seedDatabase(): Promise<void> {
|
|
93
|
+
try {
|
|
94
|
+
// Check if data already exists
|
|
95
|
+
const existingPosts = await db.select().from(posts).limit(1);
|
|
96
|
+
|
|
97
|
+
if (existingPosts.length > 0) {
|
|
98
|
+
console.log('Database already contains data, skipping seed');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log('Database seeding completed');
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error('Database seeding failed:', error);
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { pgTable, text, timestamp, boolean, varchar } from 'drizzle-orm/pg-core';
|
|
2
|
+
import { nanoid } from 'nanoid';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Posts table schema
|
|
6
|
+
* Uses NanoID for primary keys for better URL-safe unique identifiers
|
|
7
|
+
*/
|
|
8
|
+
export const posts = pgTable('posts', {
|
|
9
|
+
// Primary key using NanoID (21 characters, URL-safe)
|
|
10
|
+
id: varchar('id', { length: 21 })
|
|
11
|
+
.primaryKey()
|
|
12
|
+
.$defaultFn(() => nanoid()),
|
|
13
|
+
|
|
14
|
+
// Post content fields
|
|
15
|
+
title: varchar('title', { length: 255 }).notNull(),
|
|
16
|
+
slug: varchar('slug', { length: 255 }).notNull().unique(),
|
|
17
|
+
content: text('content').notNull(),
|
|
18
|
+
excerpt: text('excerpt'),
|
|
19
|
+
|
|
20
|
+
// Author information (Clerk user ID)
|
|
21
|
+
authorId: varchar('author_id', { length: 255 }).notNull(),
|
|
22
|
+
authorName: varchar('author_name', { length: 255 }),
|
|
23
|
+
|
|
24
|
+
// Post metadata
|
|
25
|
+
published: boolean('published').default(false).notNull(),
|
|
26
|
+
featured: boolean('featured').default(false).notNull(),
|
|
27
|
+
|
|
28
|
+
// SEO fields
|
|
29
|
+
metaTitle: varchar('meta_title', { length: 255 }),
|
|
30
|
+
metaDescription: text('meta_description'),
|
|
31
|
+
|
|
32
|
+
// Timestamps
|
|
33
|
+
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
34
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
35
|
+
publishedAt: timestamp('published_at', { withTimezone: true }),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Comments table schema
|
|
40
|
+
* Demonstrates relationship with posts using NanoID
|
|
41
|
+
*/
|
|
42
|
+
export const comments = pgTable('comments', {
|
|
43
|
+
id: varchar('id', { length: 21 })
|
|
44
|
+
.primaryKey()
|
|
45
|
+
.$defaultFn(() => nanoid()),
|
|
46
|
+
|
|
47
|
+
// Foreign key to posts table
|
|
48
|
+
postId: varchar('post_id', { length: 21 })
|
|
49
|
+
.notNull()
|
|
50
|
+
.references(() => posts.id, { onDelete: 'cascade' }),
|
|
51
|
+
|
|
52
|
+
// Comment content
|
|
53
|
+
content: text('content').notNull(),
|
|
54
|
+
|
|
55
|
+
// Author information (Clerk user ID)
|
|
56
|
+
authorId: varchar('author_id', { length: 255 }).notNull(),
|
|
57
|
+
authorName: varchar('author_name', { length: 255 }),
|
|
58
|
+
|
|
59
|
+
// Moderation
|
|
60
|
+
approved: boolean('approved').default(false).notNull(),
|
|
61
|
+
flagged: boolean('flagged').default(false).notNull(),
|
|
62
|
+
|
|
63
|
+
// Timestamps
|
|
64
|
+
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
65
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Type exports for use in application
|
|
69
|
+
export type Post = typeof posts.$inferSelect;
|
|
70
|
+
export type NewPost = typeof posts.$inferInsert;
|
|
71
|
+
export type Comment = typeof comments.$inferSelect;
|
|
72
|
+
export type NewComment = typeof comments.$inferInsert;
|