@idealyst/cli 1.0.92 → 1.0.93
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/dist/template/packages/api/README.md +400 -164
- package/dist/template/packages/api/package.json +11 -1
- package/dist/template/packages/api/src/context.ts +35 -2
- package/dist/template/packages/api/src/graphql/builder.ts +75 -0
- package/dist/template/packages/api/src/graphql/generated.ts +64 -0
- package/dist/template/packages/api/src/graphql/index.ts +75 -0
- package/dist/template/packages/api/src/graphql/types/index.ts +44 -0
- package/dist/template/packages/api/src/graphql/types/test.ts +245 -0
- package/dist/template/packages/api/src/index.ts +20 -3
- package/dist/template/packages/api/src/lib/database.ts +1 -1
- package/dist/template/packages/api/src/routers/test.ts +140 -38
- package/dist/template/packages/api/src/server.ts +23 -5
- package/dist/template/packages/api/tsconfig.json +1 -0
- package/dist/template/packages/shared/package.json +6 -0
- package/dist/template/packages/shared/src/components/App.tsx +13 -2
- package/dist/template/packages/shared/src/components/HelloWorld.tsx +333 -106
- package/dist/template/packages/shared/src/graphql/client.ts +34 -0
- package/dist/template/packages/shared/src/index.ts +8 -0
- package/dist/template/packages/web/vite.config.ts +2 -2
- package/package.json +1 -1
- package/template/packages/api/README.md +400 -164
- package/template/packages/api/package.json +11 -1
- package/template/packages/api/src/context.ts +35 -2
- package/template/packages/api/src/graphql/builder.ts +75 -0
- package/template/packages/api/src/graphql/generated.ts +64 -0
- package/template/packages/api/src/graphql/index.ts +75 -0
- package/template/packages/api/src/graphql/types/index.ts +44 -0
- package/template/packages/api/src/graphql/types/test.ts +245 -0
- package/template/packages/api/src/index.ts +20 -3
- package/template/packages/api/src/lib/database.ts +1 -1
- package/template/packages/api/src/routers/test.ts +140 -38
- package/template/packages/api/src/server.ts +23 -5
- package/template/packages/api/tsconfig.json +1 -0
- package/template/packages/shared/package.json +6 -0
- package/template/packages/shared/src/components/App.tsx +13 -2
- package/template/packages/shared/src/components/HelloWorld.tsx +333 -106
- package/template/packages/shared/src/graphql/client.ts +34 -0
- package/template/packages/shared/src/index.ts +8 -0
- package/template/packages/web/vite.config.ts +2 -2
- package/dist/template/packages/api/src/lib/crud.ts +0 -150
- package/dist/template/packages/api/src/routers/user.example.ts +0 -83
- package/template/packages/api/src/lib/crud.ts +0 -150
- package/template/packages/api/src/routers/user.example.ts +0 -83
|
@@ -2,33 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
{{description}}
|
|
4
4
|
|
|
5
|
-
A
|
|
5
|
+
A dual-API backend with both **tRPC** (for internal TypeScript clients) and **GraphQL** (for public APIs and mobile apps).
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- **tRPC** - End-to-end```typescript
|
|
9
|
-
import { z } from 'zod';
|
|
10
|
-
import { router, publicProcedure } from '../trpc.js';
|
|
11
|
-
import { createCrudRouter } from '../lib/crud.js';
|
|
12
|
-
import { prisma } from '../lib/database.js';
|
|
7
|
+
## Tech Stack
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// Add custom procedures
|
|
20
|
-
getByEmail: publicProcedure
|
|
21
|
-
.input(z.object({ email: z.string().email() }))
|
|
22
|
-
.query(async ({ input }) => {
|
|
23
|
-
return await prisma.user.findUnique({
|
|
24
|
-
where: { email: input.email }
|
|
25
|
-
});
|
|
26
|
-
}),
|
|
27
|
-
});
|
|
28
|
-
```Zod** - TypeScript-first schema validation
|
|
9
|
+
- **tRPC** - End-to-end type-safe APIs for TypeScript clients
|
|
10
|
+
- **GraphQL + Pothos** - Code-first GraphQL schema with Prisma integration
|
|
11
|
+
- **GraphQL Yoga** - Modern, performant GraphQL server
|
|
12
|
+
- **Zod** - TypeScript-first schema validation
|
|
29
13
|
- **Express.js** - Web framework for Node.js
|
|
30
|
-
- **TypeScript** - Type-safe JavaScript
|
|
31
14
|
- **Prisma** - Next-generation ORM for database access
|
|
15
|
+
- **TypeScript** - Type-safe JavaScript
|
|
16
|
+
|
|
17
|
+
## API Architecture
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
┌─────────────────────────────────────────────────┐
|
|
21
|
+
│ Single Express Server │
|
|
22
|
+
│ (port 3000) │
|
|
23
|
+
├─────────────────────────────────────────────────┤
|
|
24
|
+
│ │
|
|
25
|
+
│ GET /health → Health check │
|
|
26
|
+
│ POST /trpc/* → tRPC procedures │
|
|
27
|
+
│ POST /graphql → GraphQL queries │
|
|
28
|
+
│ GET /graphql → GraphiQL Playground │
|
|
29
|
+
│ │
|
|
30
|
+
└─────────────────────────────────────────────────┘
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### When to Use Each
|
|
34
|
+
|
|
35
|
+
| Use Case | Recommended API |
|
|
36
|
+
|----------|----------------|
|
|
37
|
+
| Internal web app (TypeScript) | tRPC |
|
|
38
|
+
| Mobile apps (React Native, iOS, Android) | GraphQL |
|
|
39
|
+
| Third-party integrations | GraphQL |
|
|
40
|
+
| Admin dashboards | tRPC |
|
|
41
|
+
| Auth flows (login, register) | tRPC |
|
|
42
|
+
| Public data queries | GraphQL |
|
|
43
|
+
| File uploads | tRPC |
|
|
32
44
|
|
|
33
45
|
## Quick Start
|
|
34
46
|
|
|
@@ -44,14 +56,14 @@ export const userRouter = router({
|
|
|
44
56
|
|
|
45
57
|
3. **Setup database:**
|
|
46
58
|
```bash
|
|
47
|
-
# Generate Prisma client
|
|
48
|
-
yarn
|
|
49
|
-
|
|
59
|
+
# Generate Prisma client and Pothos types
|
|
60
|
+
yarn generate
|
|
61
|
+
|
|
50
62
|
# Run migrations
|
|
51
|
-
yarn
|
|
52
|
-
|
|
63
|
+
cd ../database && yarn db:migrate
|
|
64
|
+
|
|
53
65
|
# Seed database with sample data
|
|
54
|
-
yarn
|
|
66
|
+
yarn db:seed
|
|
55
67
|
```
|
|
56
68
|
|
|
57
69
|
4. **Start development server:**
|
|
@@ -59,216 +71,440 @@ export const userRouter = router({
|
|
|
59
71
|
yarn dev
|
|
60
72
|
```
|
|
61
73
|
|
|
62
|
-
The API will be available at
|
|
74
|
+
The API will be available at:
|
|
75
|
+
- **tRPC**: `http://localhost:3000/trpc`
|
|
76
|
+
- **GraphQL**: `http://localhost:3000/graphql`
|
|
77
|
+
- **GraphiQL Playground**: `http://localhost:3000/graphql` (in development)
|
|
78
|
+
|
|
79
|
+
## Test API - tRPC vs GraphQL Comparison
|
|
80
|
+
|
|
81
|
+
The template includes a `Test` model with equivalent implementations in both tRPC and GraphQL, demonstrating how the same functionality can be exposed through both APIs.
|
|
82
|
+
|
|
83
|
+
### API Equivalence Table
|
|
84
|
+
|
|
85
|
+
| Operation | tRPC | GraphQL |
|
|
86
|
+
|-----------|------|---------|
|
|
87
|
+
| Get all | `trpc.test.getAll({ skip, take })` | `query { tests(skip, take) { ... } }` |
|
|
88
|
+
| Get by ID | `trpc.test.getById({ id })` | `query { test(id) { ... } }` |
|
|
89
|
+
| Create | `trpc.test.create({ name, message })` | `mutation { createTest(input) { ... } }` |
|
|
90
|
+
| Update | `trpc.test.update({ id, data })` | `mutation { updateTest(id, input) { ... } }` |
|
|
91
|
+
| Delete | `trpc.test.delete({ id })` | `mutation { deleteTest(id) { ... } }` |
|
|
92
|
+
| Count | `trpc.test.count({})` | `query { testCount }` |
|
|
93
|
+
|
|
94
|
+
### tRPC Usage (TypeScript Client)
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { trpc } from './utils/trpc';
|
|
98
|
+
|
|
99
|
+
// Get all tests
|
|
100
|
+
const { data: tests } = trpc.test.getAll.useQuery({ take: 10 });
|
|
101
|
+
|
|
102
|
+
// Get test by ID
|
|
103
|
+
const { data: test } = trpc.test.getById.useQuery({ id: 'test-id' });
|
|
104
|
+
|
|
105
|
+
// Create a new test
|
|
106
|
+
const createTest = trpc.test.create.useMutation();
|
|
107
|
+
await createTest.mutateAsync({
|
|
108
|
+
name: 'My Test',
|
|
109
|
+
message: 'Hello World',
|
|
110
|
+
status: 'active',
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Update a test
|
|
114
|
+
const updateTest = trpc.test.update.useMutation();
|
|
115
|
+
await updateTest.mutateAsync({
|
|
116
|
+
id: 'test-id',
|
|
117
|
+
data: { name: 'Updated Name' },
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Delete a test
|
|
121
|
+
const deleteTest = trpc.test.delete.useMutation();
|
|
122
|
+
await deleteTest.mutateAsync({ id: 'test-id' });
|
|
123
|
+
|
|
124
|
+
// Get count
|
|
125
|
+
const { data: count } = trpc.test.count.useQuery({});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### GraphQL Usage
|
|
129
|
+
|
|
130
|
+
Visit `http://localhost:3000/graphql` to access the GraphiQL playground.
|
|
131
|
+
|
|
132
|
+
```graphql
|
|
133
|
+
# Get all tests (equivalent to trpc.test.getAll)
|
|
134
|
+
query GetTests {
|
|
135
|
+
tests(take: 10) {
|
|
136
|
+
id
|
|
137
|
+
name
|
|
138
|
+
message
|
|
139
|
+
status
|
|
140
|
+
createdAt
|
|
141
|
+
}
|
|
142
|
+
}
|
|
63
143
|
|
|
64
|
-
|
|
144
|
+
# Get test by ID (equivalent to trpc.test.getById)
|
|
145
|
+
query GetTest($id: String!) {
|
|
146
|
+
test(id: $id) {
|
|
147
|
+
id
|
|
148
|
+
name
|
|
149
|
+
message
|
|
150
|
+
status
|
|
151
|
+
}
|
|
152
|
+
}
|
|
65
153
|
|
|
66
|
-
|
|
154
|
+
# Create a new test (equivalent to trpc.test.create)
|
|
155
|
+
mutation CreateTest {
|
|
156
|
+
createTest(input: {
|
|
157
|
+
name: "My Test"
|
|
158
|
+
message: "Hello World"
|
|
159
|
+
status: "active"
|
|
160
|
+
}) {
|
|
161
|
+
id
|
|
162
|
+
name
|
|
163
|
+
message
|
|
164
|
+
}
|
|
165
|
+
}
|
|
67
166
|
|
|
68
|
-
|
|
167
|
+
# Update a test (equivalent to trpc.test.update)
|
|
168
|
+
mutation UpdateTest($id: String!) {
|
|
169
|
+
updateTest(id: $id, input: { name: "Updated Name" }) {
|
|
170
|
+
id
|
|
171
|
+
name
|
|
172
|
+
}
|
|
173
|
+
}
|
|
69
174
|
|
|
70
|
-
|
|
175
|
+
# Delete a test (equivalent to trpc.test.delete)
|
|
176
|
+
mutation DeleteTest($id: String!) {
|
|
177
|
+
deleteTest(id: $id) {
|
|
178
|
+
id
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# Get count (equivalent to trpc.test.count)
|
|
183
|
+
query GetTestCount {
|
|
184
|
+
testCount
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### GraphQL with React/Apollo Client
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { useQuery, useMutation, gql } from '@apollo/client';
|
|
192
|
+
|
|
193
|
+
const GET_TESTS = gql`
|
|
194
|
+
query GetTests($take: Int) {
|
|
195
|
+
tests(take: $take) {
|
|
196
|
+
id
|
|
197
|
+
name
|
|
198
|
+
message
|
|
199
|
+
status
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
`;
|
|
203
|
+
|
|
204
|
+
const CREATE_TEST = gql`
|
|
205
|
+
mutation CreateTest($input: CreateTestInput!) {
|
|
206
|
+
createTest(input: $input) {
|
|
207
|
+
id
|
|
208
|
+
name
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
`;
|
|
212
|
+
|
|
213
|
+
// In your component
|
|
214
|
+
function TestList() {
|
|
215
|
+
const { data, loading } = useQuery(GET_TESTS, { variables: { take: 10 } });
|
|
216
|
+
const [createTest] = useMutation(CREATE_TEST);
|
|
217
|
+
|
|
218
|
+
const handleCreate = async () => {
|
|
219
|
+
await createTest({
|
|
220
|
+
variables: {
|
|
221
|
+
input: { name: 'New Test', message: 'Hello!' },
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
return (/* ... */);
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Adding New Models
|
|
231
|
+
|
|
232
|
+
### Step 1: Define Prisma Model
|
|
233
|
+
|
|
234
|
+
Add to `packages/database/prisma/schema.prisma`:
|
|
71
235
|
|
|
72
236
|
```prisma
|
|
73
|
-
model
|
|
237
|
+
model Product {
|
|
74
238
|
id String @id @default(cuid())
|
|
75
|
-
email String @unique
|
|
76
239
|
name String
|
|
240
|
+
price Float
|
|
77
241
|
createdAt DateTime @default(now())
|
|
78
242
|
updatedAt DateTime @updatedAt
|
|
79
243
|
}
|
|
80
244
|
```
|
|
81
245
|
|
|
82
|
-
### 2
|
|
246
|
+
### Step 2: Create tRPC Router
|
|
83
247
|
|
|
84
|
-
Create `src/routers/
|
|
248
|
+
Create `src/routers/product.ts`:
|
|
85
249
|
|
|
86
250
|
```typescript
|
|
87
251
|
import { z } from 'zod';
|
|
88
252
|
import { createCrudRouter } from '../lib/crud.js';
|
|
89
253
|
|
|
90
|
-
const
|
|
91
|
-
email: z.string().email(),
|
|
254
|
+
const createProductSchema = z.object({
|
|
92
255
|
name: z.string().min(1),
|
|
256
|
+
price: z.number().positive(),
|
|
93
257
|
});
|
|
94
258
|
|
|
95
|
-
const
|
|
96
|
-
email: z.string().email().optional(),
|
|
97
|
-
name: z.string().min(1).optional(),
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
export const userRouter = createCrudRouter(
|
|
101
|
-
'user',
|
|
102
|
-
createUserSchema,
|
|
103
|
-
updateUserSchema
|
|
104
|
-
);
|
|
259
|
+
export const productRouter = createCrudRouter('Product', createProductSchema);
|
|
105
260
|
```
|
|
106
261
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
Update `src/router/index.ts`:
|
|
262
|
+
Add to `src/router/index.ts`:
|
|
110
263
|
|
|
111
264
|
```typescript
|
|
112
|
-
import {
|
|
265
|
+
import { productRouter } from '../routers/product.js';
|
|
113
266
|
|
|
114
267
|
export const appRouter = router({
|
|
115
|
-
|
|
116
|
-
|
|
268
|
+
test: testRouter,
|
|
269
|
+
product: productRouter, // Add here
|
|
117
270
|
});
|
|
118
271
|
```
|
|
119
272
|
|
|
120
|
-
###
|
|
273
|
+
### Step 3: Create GraphQL Type
|
|
274
|
+
|
|
275
|
+
Create `src/graphql/types/product.ts`:
|
|
121
276
|
|
|
122
277
|
```typescript
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
278
|
+
import { builder } from '../builder.js';
|
|
279
|
+
import { prisma } from '../../lib/database.js';
|
|
280
|
+
|
|
281
|
+
builder.prismaObject('Product', {
|
|
282
|
+
fields: (t) => ({
|
|
283
|
+
id: t.exposeID('id'),
|
|
284
|
+
name: t.exposeString('name'),
|
|
285
|
+
price: t.exposeFloat('price'),
|
|
286
|
+
createdAt: t.expose('createdAt', { type: 'DateTime' }),
|
|
287
|
+
}),
|
|
131
288
|
});
|
|
132
289
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
290
|
+
builder.queryFields((t) => ({
|
|
291
|
+
products: t.prismaField({
|
|
292
|
+
type: ['Product'],
|
|
293
|
+
args: {
|
|
294
|
+
skip: t.arg.int(),
|
|
295
|
+
take: t.arg.int(),
|
|
296
|
+
},
|
|
297
|
+
resolve: async (query, _root, args) => {
|
|
298
|
+
return prisma.product.findMany({
|
|
299
|
+
...query,
|
|
300
|
+
skip: args.skip ?? undefined,
|
|
301
|
+
take: args.take ?? 10,
|
|
302
|
+
});
|
|
303
|
+
},
|
|
304
|
+
}),
|
|
305
|
+
}));
|
|
306
|
+
|
|
307
|
+
const CreateProductInput = builder.inputType('CreateProductInput', {
|
|
308
|
+
fields: (t) => ({
|
|
309
|
+
name: t.string({ required: true }),
|
|
310
|
+
price: t.float({ required: true }),
|
|
311
|
+
}),
|
|
138
312
|
});
|
|
139
313
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
314
|
+
builder.mutationFields((t) => ({
|
|
315
|
+
createProduct: t.prismaField({
|
|
316
|
+
type: 'Product',
|
|
317
|
+
args: {
|
|
318
|
+
input: t.arg({ type: CreateProductInput, required: true }),
|
|
319
|
+
},
|
|
320
|
+
resolve: async (query, _root, args) => {
|
|
321
|
+
return prisma.product.create({
|
|
322
|
+
...query,
|
|
323
|
+
data: args.input,
|
|
324
|
+
});
|
|
325
|
+
},
|
|
326
|
+
}),
|
|
327
|
+
}));
|
|
143
328
|
```
|
|
144
329
|
|
|
145
|
-
|
|
330
|
+
Register in `src/graphql/types/index.ts`:
|
|
146
331
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
- `getById({ id })` - Get single record
|
|
151
|
-
- `create(data)` - Create new record
|
|
152
|
-
- `update({ id, data })` - Update existing record
|
|
153
|
-
- `delete({ id })` - Delete record
|
|
154
|
-
- `count({ where? })` - Count records
|
|
332
|
+
```typescript
|
|
333
|
+
import './product.js';
|
|
334
|
+
```
|
|
155
335
|
|
|
156
|
-
|
|
336
|
+
### Step 4: Regenerate Types
|
|
157
337
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
- `yarn test` - Run tests
|
|
162
|
-
- `yarn lint` - Lint code
|
|
163
|
-
- `yarn type-check` - Check TypeScript types
|
|
338
|
+
```bash
|
|
339
|
+
yarn generate
|
|
340
|
+
```
|
|
164
341
|
|
|
165
342
|
## Project Structure
|
|
166
343
|
|
|
167
344
|
```
|
|
168
345
|
src/
|
|
346
|
+
├── graphql/
|
|
347
|
+
│ ├── builder.ts # Pothos schema builder setup
|
|
348
|
+
│ ├── generated.ts # Auto-generated Prisma types
|
|
349
|
+
│ ├── index.ts # GraphQL Yoga server
|
|
350
|
+
│ └── types/
|
|
351
|
+
│ ├── index.ts # Type registration
|
|
352
|
+
│ ├── test.ts # Test model (active)
|
|
353
|
+
│ ├── user.ts # User model (example)
|
|
354
|
+
│ ├── post.ts # Post model (example)
|
|
355
|
+
│ └── comment.ts # Comment model (example)
|
|
169
356
|
├── router/
|
|
170
|
-
│ └── index.ts # Main router
|
|
357
|
+
│ └── index.ts # Main tRPC router
|
|
171
358
|
├── routers/
|
|
172
|
-
│ ├── test.ts
|
|
173
|
-
│ └── user.example.ts
|
|
359
|
+
│ ├── test.ts # Test tRPC CRUD router
|
|
360
|
+
│ └── user.example.ts # User tRPC example
|
|
174
361
|
├── lib/
|
|
175
|
-
│ ├── crud.ts
|
|
176
|
-
│ └── database.ts
|
|
177
|
-
├── context.ts
|
|
178
|
-
├── trpc.ts
|
|
179
|
-
|
|
362
|
+
│ ├── crud.ts # tRPC CRUD generator
|
|
363
|
+
│ └── database.ts # Prisma client
|
|
364
|
+
├── context.ts # Shared context
|
|
365
|
+
├── trpc.ts # tRPC setup
|
|
366
|
+
├── index.ts # Exports
|
|
367
|
+
└── server.ts # Express server
|
|
180
368
|
```
|
|
181
369
|
|
|
182
|
-
##
|
|
183
|
-
|
|
184
|
-
### Custom Procedures
|
|
185
|
-
|
|
186
|
-
Extend generated routers with custom procedures:
|
|
187
|
-
|
|
188
|
-
```typescript
|
|
189
|
-
import { router, publicProcedure } from '../trpc.js';
|
|
190
|
-
import { createCrudRouter } from '../lib/crud.js';
|
|
191
|
-
|
|
192
|
-
const baseCrudRouter = createCrudRouter('user', createUserSchema);
|
|
193
|
-
|
|
194
|
-
export const userRouter = router({
|
|
195
|
-
...baseCrudRouter,
|
|
196
|
-
|
|
197
|
-
// Add custom procedures
|
|
198
|
-
getByEmail: publicProcedure
|
|
199
|
-
.input(z.object({ email: z.string().email() }))
|
|
200
|
-
.query(async ({ input }) => {
|
|
201
|
-
return await db.user.findUnique({
|
|
202
|
-
where: { email: input.email }
|
|
203
|
-
});
|
|
204
|
-
}),
|
|
205
|
-
});
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
### Authentication
|
|
209
|
-
|
|
210
|
-
For protected routes, you can add authentication middleware to the tRPC setup or create protected procedures:
|
|
211
|
-
|
|
212
|
-
```typescript
|
|
213
|
-
import { protectedProcedure } from '../trpc.js';
|
|
214
|
-
|
|
215
|
-
// Use protectedProcedure instead of publicProcedure in your CRUD router
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
## Development
|
|
219
|
-
|
|
220
|
-
1. **Add New Model**:
|
|
221
|
-
- Add to Prisma schema
|
|
222
|
-
- Run `yarn db:migrate`
|
|
223
|
-
- Create router with `createCrudRouter`
|
|
224
|
-
- Add to main router
|
|
225
|
-
|
|
226
|
-
2. **Test API**:
|
|
227
|
-
- Start development: `yarn dev`
|
|
228
|
-
- API available at `http://localhost:3000/trpc`
|
|
229
|
-
|
|
230
|
-
3. **Type Safety**:
|
|
231
|
-
- All operations are fully type-safe
|
|
232
|
-
- Frontend gets autocomplete and validation
|
|
233
|
-
- Schemas ensure data integrity
|
|
370
|
+
## Available Scripts
|
|
234
371
|
|
|
235
|
-
|
|
372
|
+
| Script | Description |
|
|
373
|
+
|--------|-------------|
|
|
374
|
+
| `yarn dev` | Start development server with hot reload |
|
|
375
|
+
| `yarn build` | Build production bundle |
|
|
376
|
+
| `yarn start` | Start production server |
|
|
377
|
+
| `yarn generate` | Generate Prisma client and Pothos types |
|
|
378
|
+
| `yarn test` | Run tests |
|
|
379
|
+
| `yarn lint` | Lint code |
|
|
380
|
+
| `yarn type-check` | Check TypeScript types |
|
|
236
381
|
|
|
237
382
|
## Environment Variables
|
|
238
383
|
|
|
239
|
-
Create a `.env` file with:
|
|
240
|
-
|
|
241
384
|
```env
|
|
242
385
|
# Database
|
|
243
|
-
DATABASE_URL="
|
|
386
|
+
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
|
|
244
387
|
|
|
245
388
|
# API
|
|
246
389
|
PORT=3000
|
|
247
390
|
NODE_ENV=development
|
|
248
|
-
|
|
249
|
-
# Add your environment variables here
|
|
391
|
+
CORS_ORIGIN=http://localhost:5173
|
|
250
392
|
```
|
|
251
393
|
|
|
252
394
|
## Testing
|
|
253
395
|
|
|
254
|
-
|
|
396
|
+
### Testing tRPC
|
|
255
397
|
|
|
256
398
|
```typescript
|
|
257
399
|
import { createContext } from '../src/context.js';
|
|
258
400
|
import { appRouter } from '../src/router/index.js';
|
|
259
401
|
|
|
260
|
-
describe('API
|
|
261
|
-
test('should get
|
|
402
|
+
describe('tRPC Test API', () => {
|
|
403
|
+
test('should get all tests', async () => {
|
|
262
404
|
const ctx = createContext({} as any);
|
|
263
405
|
const caller = appRouter.createCaller(ctx);
|
|
264
|
-
|
|
265
|
-
const tests = await caller.test.getAll({});
|
|
406
|
+
|
|
407
|
+
const tests = await caller.test.getAll({ take: 10 });
|
|
266
408
|
expect(tests).toBeDefined();
|
|
267
409
|
});
|
|
410
|
+
|
|
411
|
+
test('should create a test', async () => {
|
|
412
|
+
const ctx = createContext({} as any);
|
|
413
|
+
const caller = appRouter.createCaller(ctx);
|
|
414
|
+
|
|
415
|
+
const test = await caller.test.create({
|
|
416
|
+
name: 'Test Name',
|
|
417
|
+
message: 'Test Message',
|
|
418
|
+
});
|
|
419
|
+
expect(test.id).toBeDefined();
|
|
420
|
+
expect(test.name).toBe('Test Name');
|
|
421
|
+
});
|
|
268
422
|
});
|
|
269
423
|
```
|
|
270
424
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
425
|
+
### Testing GraphQL
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
import { schema } from '../src/graphql/index.js';
|
|
429
|
+
import { graphql } from 'graphql';
|
|
430
|
+
|
|
431
|
+
describe('GraphQL Test API', () => {
|
|
432
|
+
test('should query tests', async () => {
|
|
433
|
+
const query = `
|
|
434
|
+
query {
|
|
435
|
+
tests(take: 5) {
|
|
436
|
+
id
|
|
437
|
+
name
|
|
438
|
+
message
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
`;
|
|
442
|
+
|
|
443
|
+
const result = await graphql({ schema, source: query });
|
|
444
|
+
expect(result.errors).toBeUndefined();
|
|
445
|
+
expect(result.data?.tests).toBeDefined();
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test('should create a test', async () => {
|
|
449
|
+
const mutation = `
|
|
450
|
+
mutation {
|
|
451
|
+
createTest(input: {
|
|
452
|
+
name: "Test Name"
|
|
453
|
+
message: "Test Message"
|
|
454
|
+
}) {
|
|
455
|
+
id
|
|
456
|
+
name
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
`;
|
|
460
|
+
|
|
461
|
+
const result = await graphql({ schema, source: mutation });
|
|
462
|
+
expect(result.errors).toBeUndefined();
|
|
463
|
+
expect(result.data?.createTest.name).toBe('Test Name');
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## Advanced: Shared Service Layer
|
|
469
|
+
|
|
470
|
+
For complex business logic, use a shared service layer:
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
// src/services/test.service.ts
|
|
474
|
+
import { prisma } from '../lib/database.js';
|
|
475
|
+
|
|
476
|
+
export const testService = {
|
|
477
|
+
async getAll(options: { skip?: number; take?: number }) {
|
|
478
|
+
return prisma.test.findMany({
|
|
479
|
+
skip: options.skip,
|
|
480
|
+
take: options.take ?? 10,
|
|
481
|
+
orderBy: { createdAt: 'desc' },
|
|
482
|
+
});
|
|
483
|
+
},
|
|
484
|
+
|
|
485
|
+
async create(data: { name: string; message: string; status?: string }) {
|
|
486
|
+
// Add business logic, validation, etc.
|
|
487
|
+
return prisma.test.create({ data });
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
// Use in tRPC (src/routers/test.ts)
|
|
492
|
+
import { testService } from '../services/test.service.js';
|
|
493
|
+
|
|
494
|
+
export const testRouter = router({
|
|
495
|
+
getAll: publicProcedure
|
|
496
|
+
.input(z.object({ skip: z.number().optional(), take: z.number().optional() }))
|
|
497
|
+
.query(({ input }) => testService.getAll(input)),
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// Use in GraphQL (src/graphql/types/test.ts)
|
|
501
|
+
builder.queryFields((t) => ({
|
|
502
|
+
tests: t.field({
|
|
503
|
+
type: [TestType],
|
|
504
|
+
args: { skip: t.arg.int(), take: t.arg.int() },
|
|
505
|
+
resolve: (_, args) => testService.getAll(args),
|
|
506
|
+
}),
|
|
507
|
+
}));
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
This ensures consistent business logic across both APIs.
|
|
@@ -20,15 +20,23 @@
|
|
|
20
20
|
"test:coverage": "jest --coverage",
|
|
21
21
|
"lint": "eslint src --ext .ts,.tsx",
|
|
22
22
|
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
|
23
|
-
"type-check": "tsc --noEmit"
|
|
23
|
+
"type-check": "tsc --noEmit",
|
|
24
|
+
"generate": "prisma generate && yarn generate:pothos",
|
|
25
|
+
"generate:pothos": "tsx scripts/generate-pothos-types.ts"
|
|
24
26
|
},
|
|
25
27
|
"dependencies": {
|
|
26
28
|
"@{{workspaceScope}}/database": "workspace:*",
|
|
27
29
|
"@{{workspaceScope}}/shared": "workspace:*",
|
|
30
|
+
"@pothos/core": "^4.10.0",
|
|
31
|
+
"@pothos/plugin-prisma": "^4.12.0",
|
|
32
|
+
"@pothos/plugin-relay": "^4.6.2",
|
|
33
|
+
"@pothos/plugin-validation": "^4.2.0",
|
|
28
34
|
"@trpc/server": "^11.5.1",
|
|
29
35
|
"cors": "^2.8.5",
|
|
30
36
|
"dotenv": "^16.3.1",
|
|
31
37
|
"express": "^4.18.2",
|
|
38
|
+
"graphql": "^16.9.0",
|
|
39
|
+
"graphql-yoga": "^5.10.6",
|
|
32
40
|
"zod": "^3.22.4"
|
|
33
41
|
},
|
|
34
42
|
"devDependencies": {
|
|
@@ -47,6 +55,8 @@
|
|
|
47
55
|
"keywords": [
|
|
48
56
|
"api",
|
|
49
57
|
"trpc",
|
|
58
|
+
"graphql",
|
|
59
|
+
"pothos",
|
|
50
60
|
"zod",
|
|
51
61
|
"typescript",
|
|
52
62
|
"express"
|