neon-prisma-starter 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/README.md +80 -0
- package/bin/index.js +403 -0
- package/package.json +31 -0
- package/templates/js/prisma/schema.prisma +27 -0
- package/templates/js/prisma/seed.js +62 -0
- package/templates/js/src/db.js +11 -0
- package/templates/js/src/index.js +101 -0
- package/templates/prisma/schema.prisma +27 -0
- package/templates/prisma/seed.js +62 -0
- package/templates/src/db.js +11 -0
- package/templates/src/index.js +101 -0
- package/templates/ts/prisma/schema.prisma +27 -0
- package/templates/ts/prisma/seed.ts +61 -0
- package/templates/ts/src/db.ts +9 -0
- package/templates/ts/src/index.ts +101 -0
- package/templates/ts/tsconfig.json +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# neon-prisma-starter
|
|
2
|
+
|
|
3
|
+
> Instantly scaffold a Node.js + Prisma + Neon (serverless Postgres) project with one command.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx neon-prisma-starter my-project
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or install globally:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g neon-prisma-starter
|
|
15
|
+
neon-prisma-starter my-project
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## What gets created
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
my-project/
|
|
22
|
+
├── prisma/
|
|
23
|
+
│ ├── schema.prisma ← Author + Book models ready to go
|
|
24
|
+
│ └── seed.js ← Example seed data
|
|
25
|
+
├── src/
|
|
26
|
+
│ ├── generated/prisma/ ← auto-generated (do not touch)
|
|
27
|
+
│ ├── db.js ← Prisma client with Neon adapter
|
|
28
|
+
│ └── index.js ← Express server with CRUD routes
|
|
29
|
+
├── .env ← your DATABASE_URL goes here
|
|
30
|
+
├── .gitignore
|
|
31
|
+
└── package.json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## After scaffolding
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cd my-project
|
|
38
|
+
|
|
39
|
+
# 1. Add your Neon DATABASE_URL to .env (if not provided during setup)
|
|
40
|
+
|
|
41
|
+
# 2. Create tables in Neon
|
|
42
|
+
npm run migrate
|
|
43
|
+
|
|
44
|
+
# 3. Seed example data
|
|
45
|
+
npm run seed
|
|
46
|
+
|
|
47
|
+
# 4. Start the server
|
|
48
|
+
npm run dev
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## API Endpoints
|
|
52
|
+
|
|
53
|
+
| Method | Route | Description |
|
|
54
|
+
|--------|----------------|--------------------------|
|
|
55
|
+
| GET | / | Health check |
|
|
56
|
+
| GET | /authors | List all authors + books |
|
|
57
|
+
| POST | /authors | Create a new author |
|
|
58
|
+
| GET | /authors/:id | Get author by ID |
|
|
59
|
+
| DELETE | /authors/:id | Delete author |
|
|
60
|
+
| GET | /books | List all books |
|
|
61
|
+
| POST | /books | Create a new book |
|
|
62
|
+
| DELETE | /books/:id | Delete a book |
|
|
63
|
+
|
|
64
|
+
## Scripts
|
|
65
|
+
|
|
66
|
+
| Command | Description |
|
|
67
|
+
|-------------------|--------------------------------------|
|
|
68
|
+
| `npm run dev` | Start server with nodemon |
|
|
69
|
+
| `npm run migrate` | Run Prisma migrations on Neon |
|
|
70
|
+
| `npm run seed` | Seed database with example data |
|
|
71
|
+
| `npm run studio` | Open Prisma Studio (visual DB UI) |
|
|
72
|
+
|
|
73
|
+
## Requirements
|
|
74
|
+
|
|
75
|
+
- Node.js 18+
|
|
76
|
+
- A [Neon](https://neon.tech) account (free tier works)
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const fs = require('fs-extra');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
const ora = require('ora');
|
|
10
|
+
const prompts = require('prompts');
|
|
11
|
+
|
|
12
|
+
const TEMPLATE_DIR = path.join(__dirname, '..', 'templates');
|
|
13
|
+
|
|
14
|
+
// ─── Package JSON builder ─────────────────────────────────────────────────────
|
|
15
|
+
function buildPackageJson(projectName, useTs) {
|
|
16
|
+
const base = {
|
|
17
|
+
name: projectName,
|
|
18
|
+
version: '1.0.0',
|
|
19
|
+
description: 'Node.js + Prisma + Neon project',
|
|
20
|
+
dependencies: {
|
|
21
|
+
'@prisma/adapter-neon': '^6.0.0',
|
|
22
|
+
'@prisma/client': '^6.0.0',
|
|
23
|
+
dotenv: '^16.0.0',
|
|
24
|
+
express: '^4.18.0',
|
|
25
|
+
prisma: '^6.0.0',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
if (useTs) {
|
|
30
|
+
return {
|
|
31
|
+
...base,
|
|
32
|
+
main: 'dist/index.js',
|
|
33
|
+
scripts: {
|
|
34
|
+
dev: 'ts-node src/index.ts',
|
|
35
|
+
build: 'tsc',
|
|
36
|
+
start: 'node dist/index.js',
|
|
37
|
+
migrate: 'prisma migrate dev',
|
|
38
|
+
seed: 'ts-node prisma/seed.ts',
|
|
39
|
+
studio: 'prisma studio',
|
|
40
|
+
postinstall: 'prisma generate',
|
|
41
|
+
},
|
|
42
|
+
devDependencies: {
|
|
43
|
+
'@types/express': '^4.17.0',
|
|
44
|
+
'@types/node': '^20.0.0',
|
|
45
|
+
'ts-node': '^10.9.0',
|
|
46
|
+
typescript: '^5.0.0',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
...base,
|
|
53
|
+
main: 'src/index.js',
|
|
54
|
+
scripts: {
|
|
55
|
+
dev: 'nodemon src/index.js',
|
|
56
|
+
start: 'node src/index.js',
|
|
57
|
+
migrate: 'prisma migrate dev',
|
|
58
|
+
seed: 'node prisma/seed.js',
|
|
59
|
+
studio: 'prisma studio',
|
|
60
|
+
postinstall: 'prisma generate',
|
|
61
|
+
},
|
|
62
|
+
devDependencies: {
|
|
63
|
+
nodemon: '^3.0.0',
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── tsconfig ─────────────────────────────────────────────────────────────────
|
|
69
|
+
const TSCONFIG = JSON.stringify({
|
|
70
|
+
compilerOptions: {
|
|
71
|
+
target: 'ES2020',
|
|
72
|
+
module: 'commonjs',
|
|
73
|
+
lib: ['ES2020'],
|
|
74
|
+
outDir: './dist',
|
|
75
|
+
rootDir: './src',
|
|
76
|
+
strict: true,
|
|
77
|
+
esModuleInterop: true,
|
|
78
|
+
skipLibCheck: true,
|
|
79
|
+
forceConsistentCasingInFileNames: true,
|
|
80
|
+
resolveJsonModule: true,
|
|
81
|
+
},
|
|
82
|
+
include: ['src/**/*'],
|
|
83
|
+
exclude: ['node_modules', 'dist'],
|
|
84
|
+
}, null, 2);
|
|
85
|
+
|
|
86
|
+
// ─── TypeScript templates ─────────────────────────────────────────────────────
|
|
87
|
+
const TS_DB = `import 'dotenv/config';
|
|
88
|
+
import { PrismaClient } from './generated/prisma';
|
|
89
|
+
import { PrismaNeon } from '@prisma/adapter-neon';
|
|
90
|
+
|
|
91
|
+
const adapter = new PrismaNeon({
|
|
92
|
+
connectionString: process.env.DATABASE_URL as string,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export const prisma = new PrismaClient({ adapter });
|
|
96
|
+
`;
|
|
97
|
+
|
|
98
|
+
const TS_INDEX = `import 'dotenv/config';
|
|
99
|
+
import express, { Request, Response } from 'express';
|
|
100
|
+
import { prisma } from './db';
|
|
101
|
+
|
|
102
|
+
const app = express();
|
|
103
|
+
app.use(express.json());
|
|
104
|
+
|
|
105
|
+
app.get('/', (_req: Request, res: Response) => {
|
|
106
|
+
res.json({ status: 'ok', message: 'neon-prisma-starter API is running 🚀' });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ─── Authors ──────────────────────────────────────────────────────────────────
|
|
110
|
+
app.get('/authors', async (_req: Request, res: Response) => {
|
|
111
|
+
try {
|
|
112
|
+
const authors = await prisma.author.findMany({
|
|
113
|
+
include: { books: true },
|
|
114
|
+
orderBy: { createdAt: 'desc' },
|
|
115
|
+
});
|
|
116
|
+
res.json(authors);
|
|
117
|
+
} catch (err: any) { res.status(500).json({ error: err.message }); }
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
app.get('/authors/:id', async (req: Request, res: Response) => {
|
|
121
|
+
try {
|
|
122
|
+
const author = await prisma.author.findUnique({
|
|
123
|
+
where: { id: Number(req.params.id) },
|
|
124
|
+
include: { books: true },
|
|
125
|
+
});
|
|
126
|
+
if (!author) return res.status(404).json({ error: 'Author not found' });
|
|
127
|
+
res.json(author);
|
|
128
|
+
} catch (err: any) { res.status(500).json({ error: err.message }); }
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
app.post('/authors', async (req: Request, res: Response) => {
|
|
132
|
+
try {
|
|
133
|
+
const { name, bio }: { name: string; bio?: string } = req.body;
|
|
134
|
+
if (!name) return res.status(400).json({ error: 'name is required' });
|
|
135
|
+
const author = await prisma.author.create({ data: { name, bio } });
|
|
136
|
+
res.status(201).json(author);
|
|
137
|
+
} catch (err: any) { res.status(500).json({ error: err.message }); }
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
app.delete('/authors/:id', async (req: Request, res: Response) => {
|
|
141
|
+
try {
|
|
142
|
+
await prisma.book.deleteMany({ where: { authorId: Number(req.params.id) } });
|
|
143
|
+
await prisma.author.delete({ where: { id: Number(req.params.id) } });
|
|
144
|
+
res.json({ message: 'Author deleted' });
|
|
145
|
+
} catch (err: any) { res.status(500).json({ error: err.message }); }
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ─── Books ────────────────────────────────────────────────────────────────────
|
|
149
|
+
app.get('/books', async (_req: Request, res: Response) => {
|
|
150
|
+
try {
|
|
151
|
+
const books = await prisma.book.findMany({
|
|
152
|
+
include: { author: true },
|
|
153
|
+
orderBy: { createdAt: 'desc' },
|
|
154
|
+
});
|
|
155
|
+
res.json(books);
|
|
156
|
+
} catch (err: any) { res.status(500).json({ error: err.message }); }
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
app.post('/books', async (req: Request, res: Response) => {
|
|
160
|
+
try {
|
|
161
|
+
const { title, genre, authorId }: { title: string; genre?: string; authorId: number } = req.body;
|
|
162
|
+
if (!title || !authorId) return res.status(400).json({ error: 'title and authorId are required' });
|
|
163
|
+
const book = await prisma.book.create({
|
|
164
|
+
data: { title, genre, authorId: Number(authorId) },
|
|
165
|
+
include: { author: true },
|
|
166
|
+
});
|
|
167
|
+
res.status(201).json(book);
|
|
168
|
+
} catch (err: any) { res.status(500).json({ error: err.message }); }
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
app.delete('/books/:id', async (req: Request, res: Response) => {
|
|
172
|
+
try {
|
|
173
|
+
await prisma.book.delete({ where: { id: Number(req.params.id) } });
|
|
174
|
+
res.json({ message: 'Book deleted' });
|
|
175
|
+
} catch (err: any) { res.status(500).json({ error: err.message }); }
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const PORT = process.env.PORT || 3000;
|
|
179
|
+
app.listen(PORT, () => {
|
|
180
|
+
console.log(\`\\n🚀 Server running at http://localhost:\${PORT}\\n\`);
|
|
181
|
+
});
|
|
182
|
+
`;
|
|
183
|
+
|
|
184
|
+
const TS_SEED = `import { prisma } from '../src/db';
|
|
185
|
+
|
|
186
|
+
async function seed() {
|
|
187
|
+
console.log('🌱 Seeding database...');
|
|
188
|
+
|
|
189
|
+
await prisma.book.deleteMany();
|
|
190
|
+
await prisma.author.deleteMany();
|
|
191
|
+
|
|
192
|
+
await prisma.author.create({
|
|
193
|
+
data: {
|
|
194
|
+
name: 'J.R.R. Tolkien',
|
|
195
|
+
bio: 'Creator of Middle-earth.',
|
|
196
|
+
books: {
|
|
197
|
+
create: [
|
|
198
|
+
{ title: 'The Hobbit', genre: 'Fantasy' },
|
|
199
|
+
{ title: 'The Fellowship of the Ring', genre: 'Fantasy' },
|
|
200
|
+
{ title: 'The Two Towers', genre: 'Fantasy' },
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await prisma.author.create({
|
|
207
|
+
data: {
|
|
208
|
+
name: 'George R.R. Martin',
|
|
209
|
+
bio: 'Author of A Song of Ice and Fire.',
|
|
210
|
+
books: {
|
|
211
|
+
create: [
|
|
212
|
+
{ title: 'A Game of Thrones', genre: 'Fantasy' },
|
|
213
|
+
{ title: 'A Clash of Kings', genre: 'Fantasy' },
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
console.log('✅ Database seeded!');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
seed()
|
|
223
|
+
.catch((err) => { console.error('❌ Seed failed:', err); process.exit(1); })
|
|
224
|
+
.finally(() => prisma.$disconnect());
|
|
225
|
+
`;
|
|
226
|
+
|
|
227
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
228
|
+
function run(cmd, cwd) {
|
|
229
|
+
execSync(cmd, { stdio: 'inherit', cwd });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function writeFile(filePath, content) {
|
|
233
|
+
fs.ensureDirSync(path.dirname(filePath));
|
|
234
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
238
|
+
async function main() {
|
|
239
|
+
console.log('\n');
|
|
240
|
+
console.log(chalk.bgGreen.black.bold(' neon-prisma-starter '));
|
|
241
|
+
console.log(chalk.gray(' Node.js + Prisma + Neon · Instant Project Setup\n'));
|
|
242
|
+
|
|
243
|
+
// 1. Project name
|
|
244
|
+
let projectName = process.argv[2];
|
|
245
|
+
if (!projectName) {
|
|
246
|
+
const res = await prompts({
|
|
247
|
+
type: 'text',
|
|
248
|
+
name: 'projectName',
|
|
249
|
+
message: 'Project name:',
|
|
250
|
+
initial: 'my-neon-app',
|
|
251
|
+
validate: v => v.trim().length > 0 || 'Project name cannot be empty',
|
|
252
|
+
});
|
|
253
|
+
projectName = res.projectName;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!projectName) {
|
|
257
|
+
console.log(chalk.red('\n✖ No project name provided. Exiting.\n'));
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
262
|
+
|
|
263
|
+
// 2. Overwrite check
|
|
264
|
+
if (fs.existsSync(targetDir)) {
|
|
265
|
+
const { overwrite } = await prompts({
|
|
266
|
+
type: 'confirm',
|
|
267
|
+
name: 'overwrite',
|
|
268
|
+
message: `Folder "${projectName}" already exists. Overwrite?`,
|
|
269
|
+
initial: false,
|
|
270
|
+
});
|
|
271
|
+
if (!overwrite) { console.log(chalk.yellow('\n⚠ Aborted.\n')); process.exit(0); }
|
|
272
|
+
fs.removeSync(targetDir);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 3. ── Language choice ──────────────────────────────────────────────────────
|
|
276
|
+
const { language } = await prompts({
|
|
277
|
+
type: 'select',
|
|
278
|
+
name: 'language',
|
|
279
|
+
message: 'Which language do you want to use?',
|
|
280
|
+
choices: [
|
|
281
|
+
{
|
|
282
|
+
title: chalk.blue('TypeScript') + chalk.gray(' (recommended)'),
|
|
283
|
+
description: 'Full type safety, tsconfig included, ts-node dev setup',
|
|
284
|
+
value: 'ts',
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
title: chalk.green('JavaScript'),
|
|
288
|
+
description: 'Plain JS with nodemon dev setup',
|
|
289
|
+
value: 'js',
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
initial: 0,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
if (!language) { console.log(chalk.yellow('\n⚠ Aborted.\n')); process.exit(0); }
|
|
296
|
+
const useTs = language === 'ts';
|
|
297
|
+
|
|
298
|
+
// 4. DATABASE_URL
|
|
299
|
+
const { dbUrl } = await prompts({
|
|
300
|
+
type: 'text',
|
|
301
|
+
name: 'dbUrl',
|
|
302
|
+
message: 'Paste your Neon DATABASE_URL (or press Enter to fill in later):',
|
|
303
|
+
initial: '',
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
console.log('');
|
|
307
|
+
|
|
308
|
+
// ─── Scaffold ────────────────────────────────────────────────────────────
|
|
309
|
+
const spinner = ora(`Scaffolding ${useTs ? 'TypeScript' : 'JavaScript'} project...`).start();
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
fs.ensureDirSync(targetDir);
|
|
313
|
+
|
|
314
|
+
// Shared: prisma schema
|
|
315
|
+
fs.copySync(
|
|
316
|
+
path.join(TEMPLATE_DIR, 'prisma', 'schema.prisma'),
|
|
317
|
+
path.join(targetDir, 'prisma', 'schema.prisma')
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
if (useTs) {
|
|
321
|
+
writeFile(path.join(targetDir, 'src', 'db.ts'), TS_DB);
|
|
322
|
+
writeFile(path.join(targetDir, 'src', 'index.ts'), TS_INDEX);
|
|
323
|
+
writeFile(path.join(targetDir, 'prisma', 'seed.ts'), TS_SEED);
|
|
324
|
+
writeFile(path.join(targetDir, 'tsconfig.json'), TSCONFIG);
|
|
325
|
+
} else {
|
|
326
|
+
fs.copySync(path.join(TEMPLATE_DIR, 'src', 'db.js'), path.join(targetDir, 'src', 'db.js'));
|
|
327
|
+
fs.copySync(path.join(TEMPLATE_DIR, 'src', 'index.js'), path.join(targetDir, 'src', 'index.js'));
|
|
328
|
+
fs.copySync(path.join(TEMPLATE_DIR, 'prisma', 'seed.js'), path.join(targetDir, 'prisma', 'seed.js'));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// .env
|
|
332
|
+
const envContent = dbUrl
|
|
333
|
+
? `DATABASE_URL="${dbUrl}"\n`
|
|
334
|
+
: `DATABASE_URL="postgresql://user:pass@ep-xxx.us-east-1.aws.neon.tech/neondb?sslmode=require"\n`;
|
|
335
|
+
writeFile(path.join(targetDir, '.env'), envContent);
|
|
336
|
+
writeFile(path.join(targetDir, '.env.example'),
|
|
337
|
+
`DATABASE_URL="postgresql://user:pass@ep-xxx.us-east-1.aws.neon.tech/neondb?sslmode=require"\n`);
|
|
338
|
+
|
|
339
|
+
// .gitignore
|
|
340
|
+
fs.copySync(path.join(TEMPLATE_DIR, '.gitignore'), path.join(targetDir, '.gitignore'));
|
|
341
|
+
|
|
342
|
+
// package.json
|
|
343
|
+
writeFile(
|
|
344
|
+
path.join(targetDir, 'package.json'),
|
|
345
|
+
JSON.stringify(buildPackageJson(projectName, useTs), null, 2)
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
spinner.succeed(chalk.green(`${useTs ? 'TypeScript' : 'JavaScript'} project scaffolded`));
|
|
349
|
+
} catch (err) {
|
|
350
|
+
spinner.fail('Failed to scaffold project');
|
|
351
|
+
console.error(err);
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ─── Install deps ─────────────────────────────────────────────────────────
|
|
356
|
+
const installSpinner = ora('Installing dependencies...').start();
|
|
357
|
+
try {
|
|
358
|
+
run('npm install', targetDir);
|
|
359
|
+
installSpinner.succeed(chalk.green('Dependencies installed'));
|
|
360
|
+
} catch {
|
|
361
|
+
installSpinner.fail('npm install failed — run it manually');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ─── Migrate ──────────────────────────────────────────────────────────────
|
|
365
|
+
if (dbUrl) {
|
|
366
|
+
const migrateSpinner = ora('Running Prisma migration...').start();
|
|
367
|
+
try {
|
|
368
|
+
run('npx prisma migrate dev --name init', targetDir);
|
|
369
|
+
migrateSpinner.succeed(chalk.green('Database migrated'));
|
|
370
|
+
} catch {
|
|
371
|
+
migrateSpinner.warn(chalk.yellow('Migration skipped — run: npm run migrate'));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ─── Done ─────────────────────────────────────────────────────────────────
|
|
376
|
+
const langBadge = useTs
|
|
377
|
+
? chalk.bgBlue.white.bold(' TypeScript ')
|
|
378
|
+
: chalk.bgGreen.black.bold(' JavaScript ');
|
|
379
|
+
|
|
380
|
+
console.log('\n' + chalk.bgGreen.black.bold(' ✅ Project ready! ') + ' ' + langBadge + '\n');
|
|
381
|
+
console.log(chalk.white.bold(` cd ${projectName}`));
|
|
382
|
+
console.log('');
|
|
383
|
+
|
|
384
|
+
if (!dbUrl) {
|
|
385
|
+
console.log(chalk.yellow(' ⚠ Add your Neon DATABASE_URL to .env, then:'));
|
|
386
|
+
console.log('');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
console.log(chalk.cyan(' npm run migrate') + chalk.gray(' → create tables in Neon'));
|
|
390
|
+
console.log(chalk.cyan(' npm run seed ') + chalk.gray(' → populate with example data'));
|
|
391
|
+
console.log(chalk.cyan(' npm run dev ') + chalk.gray(' → start server on :3000'));
|
|
392
|
+
if (useTs) {
|
|
393
|
+
console.log(chalk.cyan(' npm run build ') + chalk.gray(' → compile TypeScript → dist/'));
|
|
394
|
+
}
|
|
395
|
+
console.log('');
|
|
396
|
+
console.log(chalk.gray(' Endpoints: GET /authors POST /authors GET /books POST /books'));
|
|
397
|
+
console.log('');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
main().catch(err => {
|
|
401
|
+
console.error(chalk.red('\n✖ Something went wrong:\n'), err);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "neon-prisma-starter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI to scaffold a Node.js + Prisma + Neon project instantly",
|
|
5
|
+
"main": "bin/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"neon-prisma-starter": "bin/index.js",
|
|
8
|
+
"create-neon-app": "bin/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node bin/index.js test-project"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"neon",
|
|
15
|
+
"prisma",
|
|
16
|
+
"nodejs",
|
|
17
|
+
"express",
|
|
18
|
+
"postgresql",
|
|
19
|
+
"starter",
|
|
20
|
+
"cli",
|
|
21
|
+
"scaffold"
|
|
22
|
+
],
|
|
23
|
+
"author": "",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"chalk": "^4.1.2",
|
|
27
|
+
"fs-extra": "^11.2.0",
|
|
28
|
+
"ora": "^5.4.1",
|
|
29
|
+
"prompts": "^2.4.2"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
generator client {
|
|
2
|
+
provider = "prisma-client-js"
|
|
3
|
+
output = "../src/generated/prisma"
|
|
4
|
+
previewFeatures = ["driverAdapters"]
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
datasource db {
|
|
8
|
+
provider = "postgresql"
|
|
9
|
+
url = env("DATABASE_URL")
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
model Author {
|
|
13
|
+
id Int @id @default(autoincrement())
|
|
14
|
+
name String
|
|
15
|
+
bio String?
|
|
16
|
+
books Book[]
|
|
17
|
+
createdAt DateTime @default(now())
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
model Book {
|
|
21
|
+
id Int @id @default(autoincrement())
|
|
22
|
+
title String
|
|
23
|
+
genre String?
|
|
24
|
+
author Author @relation(fields: [authorId], references: [id])
|
|
25
|
+
authorId Int
|
|
26
|
+
createdAt DateTime @default(now())
|
|
27
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const { prisma } = require('../src/db');
|
|
2
|
+
|
|
3
|
+
async function seed() {
|
|
4
|
+
console.log('🌱 Seeding database...');
|
|
5
|
+
|
|
6
|
+
// Clean existing data
|
|
7
|
+
await prisma.book.deleteMany();
|
|
8
|
+
await prisma.author.deleteMany();
|
|
9
|
+
|
|
10
|
+
// Seed authors with books
|
|
11
|
+
await prisma.author.create({
|
|
12
|
+
data: {
|
|
13
|
+
name: 'J.R.R. Tolkien',
|
|
14
|
+
bio: 'Creator of Middle-earth and author of The Lord of the Rings.',
|
|
15
|
+
books: {
|
|
16
|
+
create: [
|
|
17
|
+
{ title: 'The Hobbit', genre: 'Fantasy' },
|
|
18
|
+
{ title: 'The Fellowship of the Ring', genre: 'Fantasy' },
|
|
19
|
+
{ title: 'The Two Towers', genre: 'Fantasy' },
|
|
20
|
+
{ title: 'The Return of the King', genre: 'Fantasy' },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await prisma.author.create({
|
|
27
|
+
data: {
|
|
28
|
+
name: 'George R.R. Martin',
|
|
29
|
+
bio: 'Author of the epic fantasy series A Song of Ice and Fire.',
|
|
30
|
+
books: {
|
|
31
|
+
create: [
|
|
32
|
+
{ title: 'A Game of Thrones', genre: 'Fantasy' },
|
|
33
|
+
{ title: 'A Clash of Kings', genre: 'Fantasy' },
|
|
34
|
+
{ title: 'A Storm of Swords', genre: 'Fantasy' },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await prisma.author.create({
|
|
41
|
+
data: {
|
|
42
|
+
name: 'Frank Herbert',
|
|
43
|
+
bio: 'Author of the Dune science fiction series.',
|
|
44
|
+
books: {
|
|
45
|
+
create: [
|
|
46
|
+
{ title: 'Dune', genre: 'Sci-Fi' },
|
|
47
|
+
{ title: 'Dune Messiah', genre: 'Sci-Fi' },
|
|
48
|
+
{ title: 'Children of Dune', genre: 'Sci-Fi' },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
console.log('✅ Database seeded successfully!');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
seed()
|
|
58
|
+
.catch(err => {
|
|
59
|
+
console.error('❌ Seed failed:', err);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
})
|
|
62
|
+
.finally(() => prisma.$disconnect());
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
require('dotenv').config();
|
|
2
|
+
const { PrismaClient } = require('./generated/prisma');
|
|
3
|
+
const { PrismaNeon } = require('@prisma/adapter-neon');
|
|
4
|
+
|
|
5
|
+
const adapter = new PrismaNeon({
|
|
6
|
+
connectionString: process.env.DATABASE_URL,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const prisma = new PrismaClient({ adapter });
|
|
10
|
+
|
|
11
|
+
module.exports = { prisma };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
require('dotenv').config();
|
|
2
|
+
const express = require('express');
|
|
3
|
+
const { prisma } = require('./db');
|
|
4
|
+
|
|
5
|
+
const app = express();
|
|
6
|
+
app.use(express.json());
|
|
7
|
+
|
|
8
|
+
// ─── Health ───────────────────────────────────────────────────────────────────
|
|
9
|
+
app.get('/', (req, res) => {
|
|
10
|
+
res.json({ status: 'ok', message: 'neon-prisma-starter API is running 🚀' });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// ─── Authors ──────────────────────────────────────────────────────────────────
|
|
14
|
+
app.get('/authors', async (req, res) => {
|
|
15
|
+
try {
|
|
16
|
+
const authors = await prisma.author.findMany({
|
|
17
|
+
include: { books: true },
|
|
18
|
+
orderBy: { createdAt: 'desc' },
|
|
19
|
+
});
|
|
20
|
+
res.json(authors);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
res.status(500).json({ error: err.message });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
app.get('/authors/:id', async (req, res) => {
|
|
27
|
+
try {
|
|
28
|
+
const author = await prisma.author.findUnique({
|
|
29
|
+
where: { id: Number(req.params.id) },
|
|
30
|
+
include: { books: true },
|
|
31
|
+
});
|
|
32
|
+
if (!author) return res.status(404).json({ error: 'Author not found' });
|
|
33
|
+
res.json(author);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
res.status(500).json({ error: err.message });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
app.post('/authors', async (req, res) => {
|
|
40
|
+
try {
|
|
41
|
+
const { name, bio } = req.body;
|
|
42
|
+
if (!name) return res.status(400).json({ error: 'name is required' });
|
|
43
|
+
const author = await prisma.author.create({ data: { name, bio } });
|
|
44
|
+
res.status(201).json(author);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
res.status(500).json({ error: err.message });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
app.delete('/authors/:id', async (req, res) => {
|
|
51
|
+
try {
|
|
52
|
+
await prisma.book.deleteMany({ where: { authorId: Number(req.params.id) } });
|
|
53
|
+
await prisma.author.delete({ where: { id: Number(req.params.id) } });
|
|
54
|
+
res.json({ message: 'Author deleted' });
|
|
55
|
+
} catch (err) {
|
|
56
|
+
res.status(500).json({ error: err.message });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ─── Books ────────────────────────────────────────────────────────────────────
|
|
61
|
+
app.get('/books', async (req, res) => {
|
|
62
|
+
try {
|
|
63
|
+
const books = await prisma.book.findMany({
|
|
64
|
+
include: { author: true },
|
|
65
|
+
orderBy: { createdAt: 'desc' },
|
|
66
|
+
});
|
|
67
|
+
res.json(books);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
res.status(500).json({ error: err.message });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
app.post('/books', async (req, res) => {
|
|
74
|
+
try {
|
|
75
|
+
const { title, genre, authorId } = req.body;
|
|
76
|
+
if (!title || !authorId) return res.status(400).json({ error: 'title and authorId are required' });
|
|
77
|
+
const book = await prisma.book.create({
|
|
78
|
+
data: { title, genre, authorId: Number(authorId) },
|
|
79
|
+
include: { author: true },
|
|
80
|
+
});
|
|
81
|
+
res.status(201).json(book);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
res.status(500).json({ error: err.message });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
app.delete('/books/:id', async (req, res) => {
|
|
88
|
+
try {
|
|
89
|
+
await prisma.book.delete({ where: { id: Number(req.params.id) } });
|
|
90
|
+
res.json({ message: 'Book deleted' });
|
|
91
|
+
} catch (err) {
|
|
92
|
+
res.status(500).json({ error: err.message });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ─── Start ────────────────────────────────────────────────────────────────────
|
|
97
|
+
const PORT = process.env.PORT || 3000;
|
|
98
|
+
app.listen(PORT, () => {
|
|
99
|
+
console.log(`\n🚀 Server running at http://localhost:${PORT}`);
|
|
100
|
+
console.log(`📦 Routes: GET /authors POST /authors GET /books POST /books\n`);
|
|
101
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
generator client {
|
|
2
|
+
provider = "prisma-client-js"
|
|
3
|
+
output = "../src/generated/prisma"
|
|
4
|
+
previewFeatures = ["driverAdapters"]
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
datasource db {
|
|
8
|
+
provider = "postgresql"
|
|
9
|
+
url = env("DATABASE_URL")
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
model Author {
|
|
13
|
+
id Int @id @default(autoincrement())
|
|
14
|
+
name String
|
|
15
|
+
bio String?
|
|
16
|
+
books Book[]
|
|
17
|
+
createdAt DateTime @default(now())
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
model Book {
|
|
21
|
+
id Int @id @default(autoincrement())
|
|
22
|
+
title String
|
|
23
|
+
genre String?
|
|
24
|
+
author Author @relation(fields: [authorId], references: [id])
|
|
25
|
+
authorId Int
|
|
26
|
+
createdAt DateTime @default(now())
|
|
27
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const { prisma } = require('../src/db');
|
|
2
|
+
|
|
3
|
+
async function seed() {
|
|
4
|
+
console.log('🌱 Seeding database...');
|
|
5
|
+
|
|
6
|
+
// Clean existing data
|
|
7
|
+
await prisma.book.deleteMany();
|
|
8
|
+
await prisma.author.deleteMany();
|
|
9
|
+
|
|
10
|
+
// Seed authors with books
|
|
11
|
+
await prisma.author.create({
|
|
12
|
+
data: {
|
|
13
|
+
name: 'J.R.R. Tolkien',
|
|
14
|
+
bio: 'Creator of Middle-earth and author of The Lord of the Rings.',
|
|
15
|
+
books: {
|
|
16
|
+
create: [
|
|
17
|
+
{ title: 'The Hobbit', genre: 'Fantasy' },
|
|
18
|
+
{ title: 'The Fellowship of the Ring', genre: 'Fantasy' },
|
|
19
|
+
{ title: 'The Two Towers', genre: 'Fantasy' },
|
|
20
|
+
{ title: 'The Return of the King', genre: 'Fantasy' },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await prisma.author.create({
|
|
27
|
+
data: {
|
|
28
|
+
name: 'George R.R. Martin',
|
|
29
|
+
bio: 'Author of the epic fantasy series A Song of Ice and Fire.',
|
|
30
|
+
books: {
|
|
31
|
+
create: [
|
|
32
|
+
{ title: 'A Game of Thrones', genre: 'Fantasy' },
|
|
33
|
+
{ title: 'A Clash of Kings', genre: 'Fantasy' },
|
|
34
|
+
{ title: 'A Storm of Swords', genre: 'Fantasy' },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await prisma.author.create({
|
|
41
|
+
data: {
|
|
42
|
+
name: 'Frank Herbert',
|
|
43
|
+
bio: 'Author of the Dune science fiction series.',
|
|
44
|
+
books: {
|
|
45
|
+
create: [
|
|
46
|
+
{ title: 'Dune', genre: 'Sci-Fi' },
|
|
47
|
+
{ title: 'Dune Messiah', genre: 'Sci-Fi' },
|
|
48
|
+
{ title: 'Children of Dune', genre: 'Sci-Fi' },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
console.log('✅ Database seeded successfully!');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
seed()
|
|
58
|
+
.catch(err => {
|
|
59
|
+
console.error('❌ Seed failed:', err);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
})
|
|
62
|
+
.finally(() => prisma.$disconnect());
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
require('dotenv').config();
|
|
2
|
+
const { PrismaClient } = require('./generated/prisma');
|
|
3
|
+
const { PrismaNeon } = require('@prisma/adapter-neon');
|
|
4
|
+
|
|
5
|
+
const adapter = new PrismaNeon({
|
|
6
|
+
connectionString: process.env.DATABASE_URL,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const prisma = new PrismaClient({ adapter });
|
|
10
|
+
|
|
11
|
+
module.exports = { prisma };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
require('dotenv').config();
|
|
2
|
+
const express = require('express');
|
|
3
|
+
const { prisma } = require('./db');
|
|
4
|
+
|
|
5
|
+
const app = express();
|
|
6
|
+
app.use(express.json());
|
|
7
|
+
|
|
8
|
+
// ─── Health ───────────────────────────────────────────────────────────────────
|
|
9
|
+
app.get('/', (req, res) => {
|
|
10
|
+
res.json({ status: 'ok', message: 'neon-prisma-starter API is running 🚀' });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// ─── Authors ──────────────────────────────────────────────────────────────────
|
|
14
|
+
app.get('/authors', async (req, res) => {
|
|
15
|
+
try {
|
|
16
|
+
const authors = await prisma.author.findMany({
|
|
17
|
+
include: { books: true },
|
|
18
|
+
orderBy: { createdAt: 'desc' },
|
|
19
|
+
});
|
|
20
|
+
res.json(authors);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
res.status(500).json({ error: err.message });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
app.get('/authors/:id', async (req, res) => {
|
|
27
|
+
try {
|
|
28
|
+
const author = await prisma.author.findUnique({
|
|
29
|
+
where: { id: Number(req.params.id) },
|
|
30
|
+
include: { books: true },
|
|
31
|
+
});
|
|
32
|
+
if (!author) return res.status(404).json({ error: 'Author not found' });
|
|
33
|
+
res.json(author);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
res.status(500).json({ error: err.message });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
app.post('/authors', async (req, res) => {
|
|
40
|
+
try {
|
|
41
|
+
const { name, bio } = req.body;
|
|
42
|
+
if (!name) return res.status(400).json({ error: 'name is required' });
|
|
43
|
+
const author = await prisma.author.create({ data: { name, bio } });
|
|
44
|
+
res.status(201).json(author);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
res.status(500).json({ error: err.message });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
app.delete('/authors/:id', async (req, res) => {
|
|
51
|
+
try {
|
|
52
|
+
await prisma.book.deleteMany({ where: { authorId: Number(req.params.id) } });
|
|
53
|
+
await prisma.author.delete({ where: { id: Number(req.params.id) } });
|
|
54
|
+
res.json({ message: 'Author deleted' });
|
|
55
|
+
} catch (err) {
|
|
56
|
+
res.status(500).json({ error: err.message });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ─── Books ────────────────────────────────────────────────────────────────────
|
|
61
|
+
app.get('/books', async (req, res) => {
|
|
62
|
+
try {
|
|
63
|
+
const books = await prisma.book.findMany({
|
|
64
|
+
include: { author: true },
|
|
65
|
+
orderBy: { createdAt: 'desc' },
|
|
66
|
+
});
|
|
67
|
+
res.json(books);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
res.status(500).json({ error: err.message });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
app.post('/books', async (req, res) => {
|
|
74
|
+
try {
|
|
75
|
+
const { title, genre, authorId } = req.body;
|
|
76
|
+
if (!title || !authorId) return res.status(400).json({ error: 'title and authorId are required' });
|
|
77
|
+
const book = await prisma.book.create({
|
|
78
|
+
data: { title, genre, authorId: Number(authorId) },
|
|
79
|
+
include: { author: true },
|
|
80
|
+
});
|
|
81
|
+
res.status(201).json(book);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
res.status(500).json({ error: err.message });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
app.delete('/books/:id', async (req, res) => {
|
|
88
|
+
try {
|
|
89
|
+
await prisma.book.delete({ where: { id: Number(req.params.id) } });
|
|
90
|
+
res.json({ message: 'Book deleted' });
|
|
91
|
+
} catch (err) {
|
|
92
|
+
res.status(500).json({ error: err.message });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ─── Start ────────────────────────────────────────────────────────────────────
|
|
97
|
+
const PORT = process.env.PORT || 3000;
|
|
98
|
+
app.listen(PORT, () => {
|
|
99
|
+
console.log(`\n🚀 Server running at http://localhost:${PORT}`);
|
|
100
|
+
console.log(`📦 Routes: GET /authors POST /authors GET /books POST /books\n`);
|
|
101
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
generator client {
|
|
2
|
+
provider = "prisma-client-js"
|
|
3
|
+
output = "../src/generated/prisma"
|
|
4
|
+
previewFeatures = ["driverAdapters"]
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
datasource db {
|
|
8
|
+
provider = "postgresql"
|
|
9
|
+
url = env("DATABASE_URL")
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
model Author {
|
|
13
|
+
id Int @id @default(autoincrement())
|
|
14
|
+
name String
|
|
15
|
+
bio String?
|
|
16
|
+
books Book[]
|
|
17
|
+
createdAt DateTime @default(now())
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
model Book {
|
|
21
|
+
id Int @id @default(autoincrement())
|
|
22
|
+
title String
|
|
23
|
+
genre String?
|
|
24
|
+
author Author @relation(fields: [authorId], references: [id])
|
|
25
|
+
authorId Int
|
|
26
|
+
createdAt DateTime @default(now())
|
|
27
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { prisma } from '../src/db';
|
|
2
|
+
|
|
3
|
+
async function seed(): Promise<void> {
|
|
4
|
+
console.log('🌱 Seeding database...');
|
|
5
|
+
|
|
6
|
+
// Clean existing data
|
|
7
|
+
await prisma.book.deleteMany();
|
|
8
|
+
await prisma.author.deleteMany();
|
|
9
|
+
|
|
10
|
+
await prisma.author.create({
|
|
11
|
+
data: {
|
|
12
|
+
name: 'J.R.R. Tolkien',
|
|
13
|
+
bio: 'Creator of Middle-earth and author of The Lord of the Rings.',
|
|
14
|
+
books: {
|
|
15
|
+
create: [
|
|
16
|
+
{ title: 'The Hobbit', genre: 'Fantasy' },
|
|
17
|
+
{ title: 'The Fellowship of the Ring', genre: 'Fantasy' },
|
|
18
|
+
{ title: 'The Two Towers', genre: 'Fantasy' },
|
|
19
|
+
{ title: 'The Return of the King', genre: 'Fantasy' },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
await prisma.author.create({
|
|
26
|
+
data: {
|
|
27
|
+
name: 'George R.R. Martin',
|
|
28
|
+
bio: 'Author of the epic fantasy series A Song of Ice and Fire.',
|
|
29
|
+
books: {
|
|
30
|
+
create: [
|
|
31
|
+
{ title: 'A Game of Thrones', genre: 'Fantasy' },
|
|
32
|
+
{ title: 'A Clash of Kings', genre: 'Fantasy' },
|
|
33
|
+
{ title: 'A Storm of Swords', genre: 'Fantasy' },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await prisma.author.create({
|
|
40
|
+
data: {
|
|
41
|
+
name: 'Frank Herbert',
|
|
42
|
+
bio: 'Author of the Dune science fiction series.',
|
|
43
|
+
books: {
|
|
44
|
+
create: [
|
|
45
|
+
{ title: 'Dune', genre: 'Sci-Fi' },
|
|
46
|
+
{ title: 'Dune Messiah', genre: 'Sci-Fi' },
|
|
47
|
+
{ title: 'Children of Dune', genre: 'Sci-Fi' },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
console.log('✅ Database seeded successfully!');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
seed()
|
|
57
|
+
.catch((err: Error) => {
|
|
58
|
+
console.error('❌ Seed failed:', err);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
})
|
|
61
|
+
.finally(() => prisma.$disconnect());
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { PrismaClient } from './generated/prisma';
|
|
3
|
+
import { PrismaNeon } from '@prisma/adapter-neon';
|
|
4
|
+
|
|
5
|
+
const adapter = new PrismaNeon({
|
|
6
|
+
connectionString: process.env.DATABASE_URL as string,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const prisma = new PrismaClient({ adapter });
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import express, { Request, Response } from 'express';
|
|
3
|
+
import { prisma } from './db';
|
|
4
|
+
|
|
5
|
+
const app = express();
|
|
6
|
+
app.use(express.json());
|
|
7
|
+
|
|
8
|
+
// ─── Health ───────────────────────────────────────────────────────────────────
|
|
9
|
+
app.get('/', (_req: Request, res: Response) => {
|
|
10
|
+
res.json({ status: 'ok', message: 'neon-prisma-starter API is running 🚀' });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// ─── Authors ──────────────────────────────────────────────────────────────────
|
|
14
|
+
app.get('/authors', async (_req: Request, res: Response) => {
|
|
15
|
+
try {
|
|
16
|
+
const authors = await prisma.author.findMany({
|
|
17
|
+
include: { books: true },
|
|
18
|
+
orderBy: { createdAt: 'desc' },
|
|
19
|
+
});
|
|
20
|
+
res.json(authors);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
res.status(500).json({ error: (err as Error).message });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
app.get('/authors/:id', async (req: Request, res: Response) => {
|
|
27
|
+
try {
|
|
28
|
+
const author = await prisma.author.findUnique({
|
|
29
|
+
where: { id: Number(req.params.id) },
|
|
30
|
+
include: { books: true },
|
|
31
|
+
});
|
|
32
|
+
if (!author) return res.status(404).json({ error: 'Author not found' });
|
|
33
|
+
res.json(author);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
res.status(500).json({ error: (err as Error).message });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
app.post('/authors', async (req: Request, res: Response) => {
|
|
40
|
+
try {
|
|
41
|
+
const { name, bio }: { name: string; bio?: string } = req.body;
|
|
42
|
+
if (!name) return res.status(400).json({ error: 'name is required' });
|
|
43
|
+
const author = await prisma.author.create({ data: { name, bio } });
|
|
44
|
+
res.status(201).json(author);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
res.status(500).json({ error: (err as Error).message });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
app.delete('/authors/:id', async (req: Request, res: Response) => {
|
|
51
|
+
try {
|
|
52
|
+
await prisma.book.deleteMany({ where: { authorId: Number(req.params.id) } });
|
|
53
|
+
await prisma.author.delete({ where: { id: Number(req.params.id) } });
|
|
54
|
+
res.json({ message: 'Author deleted' });
|
|
55
|
+
} catch (err) {
|
|
56
|
+
res.status(500).json({ error: (err as Error).message });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ─── Books ────────────────────────────────────────────────────────────────────
|
|
61
|
+
app.get('/books', async (_req: Request, res: Response) => {
|
|
62
|
+
try {
|
|
63
|
+
const books = await prisma.book.findMany({
|
|
64
|
+
include: { author: true },
|
|
65
|
+
orderBy: { createdAt: 'desc' },
|
|
66
|
+
});
|
|
67
|
+
res.json(books);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
res.status(500).json({ error: (err as Error).message });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
app.post('/books', async (req: Request, res: Response) => {
|
|
74
|
+
try {
|
|
75
|
+
const { title, genre, authorId }: { title: string; genre?: string; authorId: number } = req.body;
|
|
76
|
+
if (!title || !authorId) return res.status(400).json({ error: 'title and authorId are required' });
|
|
77
|
+
const book = await prisma.book.create({
|
|
78
|
+
data: { title, genre, authorId: Number(authorId) },
|
|
79
|
+
include: { author: true },
|
|
80
|
+
});
|
|
81
|
+
res.status(201).json(book);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
res.status(500).json({ error: (err as Error).message });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
app.delete('/books/:id', async (req: Request, res: Response) => {
|
|
88
|
+
try {
|
|
89
|
+
await prisma.book.delete({ where: { id: Number(req.params.id) } });
|
|
90
|
+
res.json({ message: 'Book deleted' });
|
|
91
|
+
} catch (err) {
|
|
92
|
+
res.status(500).json({ error: (err as Error).message });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ─── Start ────────────────────────────────────────────────────────────────────
|
|
97
|
+
const PORT = process.env.PORT || 3000;
|
|
98
|
+
app.listen(PORT, () => {
|
|
99
|
+
console.log(`\n🚀 Server running at http://localhost:${PORT}`);
|
|
100
|
+
console.log(`📦 Routes: GET /authors POST /authors GET /books POST /books\n`);
|
|
101
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist", "src/generated"]
|
|
19
|
+
}
|