@idealyst/cli 1.0.45 → 1.0.48
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/generators/fullstack.js +61 -3
- package/dist/generators/fullstack.js.map +1 -1
- package/dist/generators/native.js +12 -0
- package/dist/generators/native.js.map +1 -1
- package/dist/generators/utils.js +64 -31
- package/dist/generators/utils.js.map +1 -1
- package/dist/templates/api/README.md +207 -130
- package/dist/templates/api/package.json +5 -5
- package/dist/templates/api/src/controllers/TestController.ts +0 -0
- package/dist/templates/api/src/index.ts +2 -7
- package/dist/templates/api/src/lib/crud.ts +150 -0
- package/dist/templates/api/src/lib/database.ts +23 -0
- package/dist/templates/api/src/router/index.ts +104 -71
- package/dist/templates/api/src/routers/test.ts +59 -0
- package/dist/templates/api/src/routers/user.example.ts +83 -0
- package/dist/templates/api/src/server.ts +1 -1
- package/dist/templates/api/tsconfig.json +0 -1
- package/dist/templates/database/README.md +115 -1
- package/dist/templates/database/package.json +2 -0
- package/dist/templates/database/prisma/seed.ts +37 -1
- package/dist/templates/database/schema.prisma +11 -1
- package/dist/templates/native/index.js +1 -1
- package/dist/templates/native/metro.config.js +1 -1
- package/dist/templates/native/package.json +4 -0
- package/dist/templates/native/src/App.tsx +16 -0
- package/dist/templates/native/src/utils/trpc.ts +7 -127
- package/dist/templates/native/tsconfig.json +0 -2
- package/dist/templates/shared/README.md +31 -5
- package/dist/templates/shared/__tests__/shared.test.ts +17 -5
- package/dist/templates/shared/package.json +14 -30
- package/dist/templates/shared/src/components/App.tsx +57 -0
- package/dist/templates/shared/src/components/HelloWorld.tsx +307 -0
- package/dist/templates/shared/src/components/index.ts +1 -392
- package/dist/templates/shared/src/index.ts +9 -57
- package/dist/templates/shared/src/trpc/client.ts +39 -0
- package/dist/templates/shared/tsconfig.json +4 -7
- package/dist/templates/web/README.md +65 -8
- package/dist/templates/web/package.json +3 -3
- package/dist/templates/web/src/App-with-trpc-and-shared.tsx +11 -299
- package/dist/templates/web/src/components/TestDemo.tsx +164 -0
- package/dist/templates/web/src/utils/trpc.ts +7 -93
- package/dist/templates/web/tsconfig.json +0 -1
- package/dist/templates/workspace/.devcontainer/devcontainer.json +4 -9
- package/dist/templates/workspace/.devcontainer/docker-compose.yml +1 -2
- package/dist/templates/workspace/.devcontainer/setup.sh +1 -1
- package/dist/templates/workspace/.env.example +1 -1
- package/dist/templates/workspace/Dockerfile +4 -4
- package/dist/templates/workspace/docker/nginx/prod.conf +2 -2
- package/dist/templates/workspace/docker/nginx.conf +1 -1
- package/dist/templates/workspace/docker/prometheus/prometheus.yml +1 -1
- package/dist/templates/workspace/docker-compose.yml +4 -5
- package/dist/templates/workspace/tsconfig.json +0 -1
- package/package.json +1 -1
- package/templates/api/README.md +207 -130
- package/templates/api/package.json +5 -5
- package/templates/api/src/controllers/TestController.ts +0 -0
- package/templates/api/src/index.ts +2 -7
- package/templates/api/src/lib/crud.ts +150 -0
- package/templates/api/src/lib/database.ts +23 -0
- package/templates/api/src/router/index.ts +104 -71
- package/templates/api/src/routers/test.ts +59 -0
- package/templates/api/src/routers/user.example.ts +83 -0
- package/templates/api/src/server.ts +1 -1
- package/templates/api/tsconfig.json +0 -1
- package/templates/database/README.md +115 -1
- package/templates/database/package.json +2 -0
- package/templates/database/prisma/seed.ts +37 -1
- package/templates/database/schema.prisma +11 -1
- package/templates/native/index.js +1 -1
- package/templates/native/metro.config.js +1 -1
- package/templates/native/package.json +4 -0
- package/templates/native/src/App.tsx +16 -0
- package/templates/native/src/utils/trpc.ts +7 -127
- package/templates/native/tsconfig.json +0 -2
- package/templates/shared/README.md +31 -5
- package/templates/shared/__tests__/shared.test.ts +17 -5
- package/templates/shared/package.json +14 -30
- package/templates/shared/src/components/App.tsx +57 -0
- package/templates/shared/src/components/HelloWorld.tsx +307 -0
- package/templates/shared/src/components/index.ts +1 -392
- package/templates/shared/src/index.ts +9 -57
- package/templates/shared/src/trpc/client.ts +39 -0
- package/templates/shared/tsconfig.json +4 -7
- package/templates/web/README.md +65 -8
- package/templates/web/package.json +3 -3
- package/templates/web/src/App-with-trpc-and-shared.tsx +11 -299
- package/templates/web/src/components/TestDemo.tsx +164 -0
- package/templates/web/src/utils/trpc.ts +7 -93
- package/templates/web/tsconfig.json +0 -1
- package/templates/workspace/.devcontainer/devcontainer.json +4 -9
- package/templates/workspace/.devcontainer/docker-compose.yml +1 -2
- package/templates/workspace/.devcontainer/setup.sh +1 -1
- package/templates/workspace/.env.example +1 -1
- package/templates/workspace/Dockerfile +4 -4
- package/templates/workspace/docker/nginx/prod.conf +2 -2
- package/templates/workspace/docker/nginx.conf +1 -1
- package/templates/workspace/docker/prometheus/prometheus.yml +1 -1
- package/templates/workspace/docker-compose.yml +4 -5
- package/templates/workspace/tsconfig.json +0 -1
- package/dist/templates/api/src/controllers/UserController.ts +0 -102
- package/dist/templates/api/src/lib/controller.ts +0 -90
- package/dist/templates/api/src/lib/middleware.ts +0 -170
- package/dist/templates/api/src/middleware/auth.ts +0 -75
- package/dist/templates/api/src/middleware/common.ts +0 -103
- package/dist/templates/database/.env.example +0 -1
- package/dist/templates/native/App.tsx +0 -23
- package/dist/templates/native/src/App-with-trpc-and-shared.tsx +0 -266
- package/dist/templates/shared/rollup.config.js +0 -43
- package/dist/templates/shared/src/types/index.ts +0 -148
- package/dist/templates/shared/src/utils/index.ts +0 -278
- package/templates/api/src/controllers/UserController.ts +0 -102
- package/templates/api/src/lib/controller.ts +0 -90
- package/templates/api/src/lib/middleware.ts +0 -170
- package/templates/api/src/middleware/auth.ts +0 -75
- package/templates/api/src/middleware/common.ts +0 -103
- package/templates/database/.env.example +0 -1
- package/templates/native/App.tsx +0 -23
- package/templates/native/src/App-with-trpc-and-shared.tsx +0 -266
- package/templates/shared/rollup.config.js +0 -43
- package/templates/shared/src/types/index.ts +0 -148
- package/templates/shared/src/utils/index.ts +0 -278
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
// Date and time utilities
|
|
2
|
-
export const formatDate = (date: Date | string): string => {
|
|
3
|
-
const d = typeof date === 'string' ? new Date(date) : date;
|
|
4
|
-
return d.toLocaleDateString('en-US', {
|
|
5
|
-
year: 'numeric',
|
|
6
|
-
month: 'long',
|
|
7
|
-
day: 'numeric'
|
|
8
|
-
});
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export const formatDateTime = (date: Date | string): string => {
|
|
12
|
-
const d = typeof date === 'string' ? new Date(date) : date;
|
|
13
|
-
return d.toLocaleString('en-US', {
|
|
14
|
-
year: 'numeric',
|
|
15
|
-
month: 'short',
|
|
16
|
-
day: 'numeric',
|
|
17
|
-
hour: '2-digit',
|
|
18
|
-
minute: '2-digit'
|
|
19
|
-
});
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export const formatRelativeTime = (date: Date | string): string => {
|
|
23
|
-
const d = typeof date === 'string' ? new Date(date) : date;
|
|
24
|
-
const now = new Date();
|
|
25
|
-
const diffInSeconds = Math.floor((now.getTime() - d.getTime()) / 1000);
|
|
26
|
-
|
|
27
|
-
if (diffInSeconds < 60) return 'just now';
|
|
28
|
-
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`;
|
|
29
|
-
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`;
|
|
30
|
-
if (diffInSeconds < 2592000) return `${Math.floor(diffInSeconds / 86400)} days ago`;
|
|
31
|
-
return formatDate(d);
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// String utilities
|
|
35
|
-
export const truncateText = (text: string, maxLength: number): string => {
|
|
36
|
-
if (text.length <= maxLength) return text;
|
|
37
|
-
return text.substring(0, maxLength).trim() + '...';
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export const capitalizeFirst = (text: string): string => {
|
|
41
|
-
if (!text) return text;
|
|
42
|
-
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export const slugify = (text: string): string => {
|
|
46
|
-
return text
|
|
47
|
-
.toLowerCase()
|
|
48
|
-
.replace(/[^\w\s-]/g, '') // Remove special chars
|
|
49
|
-
.replace(/[\s_-]+/g, '-') // Replace spaces and underscores with hyphens
|
|
50
|
-
.replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// Array utilities
|
|
54
|
-
export const paginate = <T>(array: T[], page: number, pageSize: number) => {
|
|
55
|
-
const startIndex = (page - 1) * pageSize;
|
|
56
|
-
const endIndex = startIndex + pageSize;
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
items: array.slice(startIndex, endIndex),
|
|
60
|
-
total: array.length,
|
|
61
|
-
page,
|
|
62
|
-
pageSize,
|
|
63
|
-
hasMore: endIndex < array.length,
|
|
64
|
-
totalPages: Math.ceil(array.length / pageSize)
|
|
65
|
-
};
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export const sortBy = <T>(array: T[], key: keyof T, order: 'asc' | 'desc' = 'asc'): T[] => {
|
|
69
|
-
return [...array].sort((a, b) => {
|
|
70
|
-
const aVal = a[key];
|
|
71
|
-
const bVal = b[key];
|
|
72
|
-
|
|
73
|
-
if (aVal < bVal) return order === 'asc' ? -1 : 1;
|
|
74
|
-
if (aVal > bVal) return order === 'asc' ? 1 : -1;
|
|
75
|
-
return 0;
|
|
76
|
-
});
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
// Object utilities
|
|
80
|
-
export const pick = <T extends object, K extends keyof T>(
|
|
81
|
-
obj: T,
|
|
82
|
-
keys: K[]
|
|
83
|
-
): Pick<T, K> => {
|
|
84
|
-
const result = {} as Pick<T, K>;
|
|
85
|
-
keys.forEach(key => {
|
|
86
|
-
if (key in obj) {
|
|
87
|
-
result[key] = obj[key];
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
return result;
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
export const omit = <T extends object, K extends keyof T>(
|
|
94
|
-
obj: T,
|
|
95
|
-
keys: K[]
|
|
96
|
-
): Omit<T, K> => {
|
|
97
|
-
const result = { ...obj };
|
|
98
|
-
keys.forEach(key => {
|
|
99
|
-
delete result[key];
|
|
100
|
-
});
|
|
101
|
-
return result as Omit<T, K>;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
// Validation utilities
|
|
105
|
-
export const isValidEmail = (email: string): boolean => {
|
|
106
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
107
|
-
return emailRegex.test(email);
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
export const isValidUrl = (url: string): boolean => {
|
|
111
|
-
try {
|
|
112
|
-
new URL(url);
|
|
113
|
-
return true;
|
|
114
|
-
} catch {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
// Local storage utilities (with fallbacks for environments without localStorage)
|
|
120
|
-
export const storage = {
|
|
121
|
-
get: (key: string): string | null => {
|
|
122
|
-
try {
|
|
123
|
-
if (typeof localStorage !== 'undefined') {
|
|
124
|
-
return localStorage.getItem(key);
|
|
125
|
-
}
|
|
126
|
-
} catch (error) {
|
|
127
|
-
console.warn('localStorage not available:', error);
|
|
128
|
-
}
|
|
129
|
-
return null;
|
|
130
|
-
},
|
|
131
|
-
|
|
132
|
-
set: (key: string, value: string): void => {
|
|
133
|
-
try {
|
|
134
|
-
if (typeof localStorage !== 'undefined') {
|
|
135
|
-
localStorage.setItem(key, value);
|
|
136
|
-
}
|
|
137
|
-
} catch (error) {
|
|
138
|
-
console.warn('localStorage not available:', error);
|
|
139
|
-
}
|
|
140
|
-
},
|
|
141
|
-
|
|
142
|
-
remove: (key: string): void => {
|
|
143
|
-
try {
|
|
144
|
-
if (typeof localStorage !== 'undefined') {
|
|
145
|
-
localStorage.removeItem(key);
|
|
146
|
-
}
|
|
147
|
-
} catch (error) {
|
|
148
|
-
console.warn('localStorage not available:', error);
|
|
149
|
-
}
|
|
150
|
-
},
|
|
151
|
-
|
|
152
|
-
getObject: <T>(key: string): T | null => {
|
|
153
|
-
const value = storage.get(key);
|
|
154
|
-
if (value) {
|
|
155
|
-
try {
|
|
156
|
-
return JSON.parse(value);
|
|
157
|
-
} catch (error) {
|
|
158
|
-
console.warn('Failed to parse stored object:', error);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return null;
|
|
162
|
-
},
|
|
163
|
-
|
|
164
|
-
setObject: <T>(key: string, value: T): void => {
|
|
165
|
-
try {
|
|
166
|
-
storage.set(key, JSON.stringify(value));
|
|
167
|
-
} catch (error) {
|
|
168
|
-
console.warn('Failed to stringify object for storage:', error);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
// Debounce utility for search and input handling
|
|
174
|
-
export const debounce = <T extends (...args: any[]) => any>(
|
|
175
|
-
func: T,
|
|
176
|
-
delay: number
|
|
177
|
-
): ((...args: Parameters<T>) => void) => {
|
|
178
|
-
let timeoutId: NodeJS.Timeout;
|
|
179
|
-
|
|
180
|
-
return (...args: Parameters<T>) => {
|
|
181
|
-
clearTimeout(timeoutId);
|
|
182
|
-
timeoutId = setTimeout(() => func(...args), delay);
|
|
183
|
-
};
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
// Theme utilities
|
|
187
|
-
export const getSystemTheme = (): 'light' | 'dark' => {
|
|
188
|
-
if (typeof window !== 'undefined' && window.matchMedia) {
|
|
189
|
-
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
190
|
-
}
|
|
191
|
-
return 'light';
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
// Error handling utilities
|
|
195
|
-
export const getErrorMessage = (error: unknown): string => {
|
|
196
|
-
if (error instanceof Error) {
|
|
197
|
-
return error.message;
|
|
198
|
-
}
|
|
199
|
-
if (typeof error === 'string') {
|
|
200
|
-
return error;
|
|
201
|
-
}
|
|
202
|
-
return 'An unexpected error occurred';
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
// Platform detection utilities
|
|
206
|
-
export const isWeb = (): boolean => {
|
|
207
|
-
return typeof window !== 'undefined';
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
export const isMobile = (): boolean => {
|
|
211
|
-
return typeof navigator !== 'undefined' && /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
// Constants for demo data
|
|
215
|
-
export const DEMO_USERS = [
|
|
216
|
-
{
|
|
217
|
-
id: '1',
|
|
218
|
-
name: 'Alice Johnson',
|
|
219
|
-
email: 'alice@example.com',
|
|
220
|
-
avatar: 'https://via.placeholder.com/150/4CAF50/white?text=AJ',
|
|
221
|
-
bio: 'Full-stack developer passionate about React and TypeScript',
|
|
222
|
-
location: 'San Francisco, CA',
|
|
223
|
-
website: 'https://alice-dev.com'
|
|
224
|
-
},
|
|
225
|
-
{
|
|
226
|
-
id: '2',
|
|
227
|
-
name: 'Bob Smith',
|
|
228
|
-
email: 'bob@example.com',
|
|
229
|
-
avatar: 'https://via.placeholder.com/150/2196F3/white?text=BS',
|
|
230
|
-
bio: 'UI/UX designer and frontend enthusiast',
|
|
231
|
-
location: 'New York, NY',
|
|
232
|
-
website: 'https://bobdesigns.co'
|
|
233
|
-
},
|
|
234
|
-
{
|
|
235
|
-
id: '3',
|
|
236
|
-
name: 'Carol Williams',
|
|
237
|
-
email: 'carol@example.com',
|
|
238
|
-
avatar: 'https://via.placeholder.com/150/FF9800/white?text=CW',
|
|
239
|
-
bio: 'Product manager with a love for user research',
|
|
240
|
-
location: 'Austin, TX'
|
|
241
|
-
}
|
|
242
|
-
];
|
|
243
|
-
|
|
244
|
-
export const DEMO_POSTS = [
|
|
245
|
-
{
|
|
246
|
-
id: '1',
|
|
247
|
-
title: 'Getting Started with React Native',
|
|
248
|
-
content: 'React Native is a powerful framework for building cross-platform mobile applications...',
|
|
249
|
-
excerpt: 'Learn the basics of React Native development',
|
|
250
|
-
authorId: '1',
|
|
251
|
-
tags: ['react-native', 'mobile', 'javascript'],
|
|
252
|
-
published: true,
|
|
253
|
-
views: 234,
|
|
254
|
-
likes: 15
|
|
255
|
-
},
|
|
256
|
-
{
|
|
257
|
-
id: '2',
|
|
258
|
-
title: 'Design Systems in Modern Web Development',
|
|
259
|
-
content: 'A design system is a complete set of standards intended to manage design at scale...',
|
|
260
|
-
excerpt: 'Building consistent user interfaces across applications',
|
|
261
|
-
authorId: '2',
|
|
262
|
-
tags: ['design-systems', 'ui-ux', 'frontend'],
|
|
263
|
-
published: true,
|
|
264
|
-
views: 189,
|
|
265
|
-
likes: 23
|
|
266
|
-
},
|
|
267
|
-
{
|
|
268
|
-
id: '3',
|
|
269
|
-
title: 'User Research Best Practices',
|
|
270
|
-
content: 'Understanding your users is crucial for building successful products...',
|
|
271
|
-
excerpt: 'How to conduct effective user research',
|
|
272
|
-
authorId: '3',
|
|
273
|
-
tags: ['user-research', 'product-management', 'ux'],
|
|
274
|
-
published: true,
|
|
275
|
-
views: 156,
|
|
276
|
-
likes: 31
|
|
277
|
-
}
|
|
278
|
-
];
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { BaseController, controllerToRouter } from '../lib/controller.js';
|
|
3
|
-
import { requireAuth, requireAdmin } from '../middleware/auth.js';
|
|
4
|
-
|
|
5
|
-
// Input schemas
|
|
6
|
-
const createUserSchema = z.object({
|
|
7
|
-
email: z.string().email(),
|
|
8
|
-
name: z.string().optional(),
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
const getUserSchema = z.object({
|
|
12
|
-
id: z.string(),
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
const updateUserSchema = z.object({
|
|
16
|
-
id: z.string(),
|
|
17
|
-
name: z.string().optional(),
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
// User controller class
|
|
21
|
-
export class UserController extends BaseController {
|
|
22
|
-
// Get all users (admin only)
|
|
23
|
-
getAll = this.createQueryWithMiddleware(
|
|
24
|
-
z.object({}),
|
|
25
|
-
[requireAuth, requireAdmin],
|
|
26
|
-
async (input, ctx) => {
|
|
27
|
-
// Simulate database query
|
|
28
|
-
return [
|
|
29
|
-
{ id: '1', email: 'user1@example.com', name: 'User 1' },
|
|
30
|
-
{ id: '2', email: 'user2@example.com', name: 'User 2' },
|
|
31
|
-
];
|
|
32
|
-
}
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
// Get user by ID (authenticated users)
|
|
36
|
-
getById = this.createQueryWithMiddleware(
|
|
37
|
-
getUserSchema,
|
|
38
|
-
[requireAuth],
|
|
39
|
-
async (input, ctx) => {
|
|
40
|
-
// In a real app, you'd query your database
|
|
41
|
-
const user = { id: input.id, email: 'user@example.com', name: 'John Doe' };
|
|
42
|
-
|
|
43
|
-
if (!user) {
|
|
44
|
-
throw new Error('User not found');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return user;
|
|
48
|
-
}
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
// Create user (public endpoint)
|
|
52
|
-
create = this.createMutation(
|
|
53
|
-
createUserSchema,
|
|
54
|
-
async (input, ctx) => {
|
|
55
|
-
// In a real app, you'd save to database
|
|
56
|
-
const newUser = {
|
|
57
|
-
id: Math.random().toString(36),
|
|
58
|
-
email: input.email,
|
|
59
|
-
name: input.name || null,
|
|
60
|
-
createdAt: new Date(),
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
return newUser;
|
|
64
|
-
}
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
// Update user (authenticated users)
|
|
68
|
-
update = this.createMutationWithMiddleware(
|
|
69
|
-
updateUserSchema,
|
|
70
|
-
[requireAuth],
|
|
71
|
-
async (input, ctx) => {
|
|
72
|
-
// In a real app, you'd update the database
|
|
73
|
-
const updatedUser = {
|
|
74
|
-
id: input.id,
|
|
75
|
-
email: 'user@example.com', // Would come from database
|
|
76
|
-
name: input.name || 'Updated Name',
|
|
77
|
-
updatedAt: new Date(),
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
return updatedUser;
|
|
81
|
-
}
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
// Delete user (admin only)
|
|
85
|
-
delete = this.createMutationWithMiddleware(
|
|
86
|
-
z.object({ id: z.string() }),
|
|
87
|
-
[requireAuth, requireAdmin],
|
|
88
|
-
async (input, ctx) => {
|
|
89
|
-
// In a real app, you'd delete from database
|
|
90
|
-
return { success: true, deletedId: input.id };
|
|
91
|
-
}
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Export router - this would be used in your main router
|
|
96
|
-
export const userRouter = controllerToRouter({
|
|
97
|
-
getAll: new UserController({} as any).getAll,
|
|
98
|
-
getById: new UserController({} as any).getById,
|
|
99
|
-
create: new UserController({} as any).create,
|
|
100
|
-
update: new UserController({} as any).update,
|
|
101
|
-
delete: new UserController({} as any).delete,
|
|
102
|
-
});
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import type { Context } from '../context.js';
|
|
3
|
-
import { publicProcedure } from '../trpc.js';
|
|
4
|
-
|
|
5
|
-
// Base controller class
|
|
6
|
-
export abstract class BaseController {
|
|
7
|
-
protected ctx: Context;
|
|
8
|
-
|
|
9
|
-
constructor(ctx: Context) {
|
|
10
|
-
this.ctx = ctx;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Helper method to create a query procedure
|
|
14
|
-
protected createQuery<TInput, TOutput>(
|
|
15
|
-
inputSchema: z.ZodSchema<TInput>,
|
|
16
|
-
handler: (input: TInput, ctx: Context) => Promise<TOutput> | TOutput
|
|
17
|
-
) {
|
|
18
|
-
return publicProcedure
|
|
19
|
-
.input(inputSchema)
|
|
20
|
-
.query(async ({ input, ctx }) => {
|
|
21
|
-
return handler(input, ctx);
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Helper method to create a mutation procedure
|
|
26
|
-
protected createMutation<TInput, TOutput>(
|
|
27
|
-
inputSchema: z.ZodSchema<TInput>,
|
|
28
|
-
handler: (input: TInput, ctx: Context) => Promise<TOutput> | TOutput
|
|
29
|
-
) {
|
|
30
|
-
return publicProcedure
|
|
31
|
-
.input(inputSchema)
|
|
32
|
-
.mutation(async ({ input, ctx }) => {
|
|
33
|
-
return handler(input, ctx);
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Helper method to create a query with middleware
|
|
38
|
-
protected createQueryWithMiddleware<TInput, TOutput>(
|
|
39
|
-
inputSchema: z.ZodSchema<TInput>,
|
|
40
|
-
middleware: MiddlewareFn[],
|
|
41
|
-
handler: (input: TInput, ctx: Context) => Promise<TOutput> | TOutput
|
|
42
|
-
) {
|
|
43
|
-
let procedure = publicProcedure.input(inputSchema);
|
|
44
|
-
|
|
45
|
-
// Apply middleware
|
|
46
|
-
for (const mw of middleware) {
|
|
47
|
-
procedure = procedure.use(mw);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return procedure.query(async ({ input, ctx }) => {
|
|
51
|
-
return handler(input, ctx);
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Helper method to create a mutation with middleware
|
|
56
|
-
protected createMutationWithMiddleware<TInput, TOutput>(
|
|
57
|
-
inputSchema: z.ZodSchema<TInput>,
|
|
58
|
-
middleware: MiddlewareFn[],
|
|
59
|
-
handler: (input: TInput, ctx: Context) => Promise<TOutput> | TOutput
|
|
60
|
-
) {
|
|
61
|
-
let procedure = publicProcedure.input(inputSchema);
|
|
62
|
-
|
|
63
|
-
// Apply middleware
|
|
64
|
-
for (const mw of middleware) {
|
|
65
|
-
procedure = procedure.use(mw);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return procedure.mutation(async ({ input, ctx }) => {
|
|
69
|
-
return handler(input, ctx);
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Middleware function type compatible with tRPC
|
|
75
|
-
export type MiddlewareFn = (opts: {
|
|
76
|
-
ctx: Context;
|
|
77
|
-
next: () => Promise<{ ctx: Context }>;
|
|
78
|
-
}) => Promise<{ ctx: Context }>;
|
|
79
|
-
|
|
80
|
-
// Controller method decorator type
|
|
81
|
-
export interface ControllerMethod {
|
|
82
|
-
[key: string]: ReturnType<typeof publicProcedure.query> | ReturnType<typeof publicProcedure.mutation>;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Helper function to convert controller to tRPC router object
|
|
86
|
-
export function controllerToRouter<T extends Record<string, any>>(
|
|
87
|
-
controllerMethods: T
|
|
88
|
-
): T {
|
|
89
|
-
return controllerMethods;
|
|
90
|
-
}
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import { TRPCError } from '@trpc/server';
|
|
2
|
-
import type { Context } from '../context.js';
|
|
3
|
-
|
|
4
|
-
// Middleware function type that works with tRPC
|
|
5
|
-
export type MiddlewareFunction = (opts: {
|
|
6
|
-
ctx: Context;
|
|
7
|
-
next: () => Promise<{ ctx: Context }>;
|
|
8
|
-
}) => Promise<{ ctx: Context }>;
|
|
9
|
-
|
|
10
|
-
// Authentication middleware
|
|
11
|
-
export const requireAuth: MiddlewareFunction = async ({ ctx, next }) => {
|
|
12
|
-
// Example: Check for authorization header
|
|
13
|
-
const authHeader = ctx.req.headers.authorization;
|
|
14
|
-
|
|
15
|
-
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
16
|
-
throw new TRPCError({
|
|
17
|
-
code: 'UNAUTHORIZED',
|
|
18
|
-
message: 'Missing or invalid authorization header',
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const token = authHeader.substring(7);
|
|
23
|
-
|
|
24
|
-
// Here you would validate the token (JWT, session, etc.)
|
|
25
|
-
// For this example, we'll just check if it's not empty
|
|
26
|
-
if (!token) {
|
|
27
|
-
throw new TRPCError({
|
|
28
|
-
code: 'UNAUTHORIZED',
|
|
29
|
-
message: 'Invalid token',
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Add user info to context (replace with your actual user lookup)
|
|
34
|
-
const user = {
|
|
35
|
-
id: 'user-id-from-token',
|
|
36
|
-
email: 'user@example.com',
|
|
37
|
-
// ... other user properties
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
return next({
|
|
41
|
-
ctx: {
|
|
42
|
-
...ctx,
|
|
43
|
-
user, // Add user to context
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// Role-based authorization middleware
|
|
49
|
-
export const requireRole = (role: string): MiddlewareFunction => {
|
|
50
|
-
return async ({ ctx, next }) => {
|
|
51
|
-
const user = (ctx as any).user;
|
|
52
|
-
|
|
53
|
-
if (!user) {
|
|
54
|
-
throw new TRPCError({
|
|
55
|
-
code: 'UNAUTHORIZED',
|
|
56
|
-
message: 'Authentication required',
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Check if user has required role
|
|
61
|
-
if (!user.roles?.includes(role)) {
|
|
62
|
-
throw new TRPCError({
|
|
63
|
-
code: 'FORBIDDEN',
|
|
64
|
-
message: `Role '${role}' required`,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return next();
|
|
69
|
-
};
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
// Logging middleware
|
|
73
|
-
export const logger: MiddlewareFunction = async ({ ctx, next }) => {
|
|
74
|
-
const start = Date.now();
|
|
75
|
-
|
|
76
|
-
console.log(`📝 ${ctx.req.method} ${ctx.req.url} - ${new Date().toISOString()}`);
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
const result = await next();
|
|
80
|
-
const duration = Date.now() - start;
|
|
81
|
-
console.log(`✅ Request completed in ${duration}ms`);
|
|
82
|
-
return result;
|
|
83
|
-
} catch (error) {
|
|
84
|
-
const duration = Date.now() - start;
|
|
85
|
-
console.log(`❌ Request failed in ${duration}ms:`, error);
|
|
86
|
-
throw error;
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// Rate limiting middleware (simple in-memory implementation)
|
|
91
|
-
const requestCounts = new Map<string, { count: number; resetTime: number }>();
|
|
92
|
-
|
|
93
|
-
export const rateLimit = (maxRequests: number, windowMs: number): MiddlewareFunction => {
|
|
94
|
-
return async ({ ctx, next }) => {
|
|
95
|
-
const clientId = ctx.req.ip || 'unknown';
|
|
96
|
-
const now = Date.now();
|
|
97
|
-
|
|
98
|
-
const clientData = requestCounts.get(clientId);
|
|
99
|
-
|
|
100
|
-
if (!clientData || now > clientData.resetTime) {
|
|
101
|
-
// Reset or initialize
|
|
102
|
-
requestCounts.set(clientId, {
|
|
103
|
-
count: 1,
|
|
104
|
-
resetTime: now + windowMs,
|
|
105
|
-
});
|
|
106
|
-
} else {
|
|
107
|
-
clientData.count++;
|
|
108
|
-
|
|
109
|
-
if (clientData.count > maxRequests) {
|
|
110
|
-
throw new TRPCError({
|
|
111
|
-
code: 'TOO_MANY_REQUESTS',
|
|
112
|
-
message: 'Rate limit exceeded',
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return next();
|
|
118
|
-
};
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
// Validation middleware factory
|
|
122
|
-
export const validateInput = <T>(schema: import('zod').ZodSchema<T>) => {
|
|
123
|
-
const middleware: MiddlewareFunction = async ({ ctx, next }) => {
|
|
124
|
-
return next();
|
|
125
|
-
};
|
|
126
|
-
return middleware;
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
// Error handling middleware
|
|
130
|
-
export const errorHandler: MiddlewareFunction = async ({ ctx, next }) => {
|
|
131
|
-
try {
|
|
132
|
-
return await next();
|
|
133
|
-
} catch (error) {
|
|
134
|
-
// Log the error
|
|
135
|
-
console.error('❌ API Error:', error);
|
|
136
|
-
|
|
137
|
-
// Re-throw tRPC errors as-is
|
|
138
|
-
if (error instanceof TRPCError) {
|
|
139
|
-
throw error;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Convert unknown errors to internal server error
|
|
143
|
-
throw new TRPCError({
|
|
144
|
-
code: 'INTERNAL_SERVER_ERROR',
|
|
145
|
-
message: 'An unexpected error occurred',
|
|
146
|
-
cause: error,
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
// Middleware composer utility
|
|
152
|
-
export const composeMiddleware = (...middlewares: MiddlewareFunction[]): MiddlewareFunction => {
|
|
153
|
-
return async ({ ctx, next }) => {
|
|
154
|
-
let index = 0;
|
|
155
|
-
|
|
156
|
-
const runMiddleware = async (currentCtx: Context): Promise<{ ctx: Context }> => {
|
|
157
|
-
if (index >= middlewares.length) {
|
|
158
|
-
return next();
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const middleware = middlewares[index++];
|
|
162
|
-
return middleware({
|
|
163
|
-
ctx: currentCtx,
|
|
164
|
-
next: () => runMiddleware(currentCtx),
|
|
165
|
-
});
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
return runMiddleware(ctx);
|
|
169
|
-
};
|
|
170
|
-
};
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { TRPCError } from '@trpc/server';
|
|
2
|
-
import type { Context } from '../context.js';
|
|
3
|
-
import type { MiddlewareFn } from '../lib/controller.js';
|
|
4
|
-
|
|
5
|
-
// Extended context with user
|
|
6
|
-
export interface AuthContext extends Context {
|
|
7
|
-
user: {
|
|
8
|
-
id: string;
|
|
9
|
-
email: string;
|
|
10
|
-
roles?: string[];
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// Authentication middleware
|
|
15
|
-
export const requireAuth: MiddlewareFn = async ({ ctx, next }) => {
|
|
16
|
-
const authHeader = ctx.req.headers.authorization;
|
|
17
|
-
|
|
18
|
-
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
19
|
-
throw new TRPCError({
|
|
20
|
-
code: 'UNAUTHORIZED',
|
|
21
|
-
message: 'Missing or invalid authorization header',
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const token = authHeader.substring(7);
|
|
26
|
-
|
|
27
|
-
// Here you would validate the token (JWT, session, etc.)
|
|
28
|
-
// For example purposes, we'll simulate token validation
|
|
29
|
-
if (!token || token === 'invalid') {
|
|
30
|
-
throw new TRPCError({
|
|
31
|
-
code: 'UNAUTHORIZED',
|
|
32
|
-
message: 'Invalid token',
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Mock user lookup - replace with your actual implementation
|
|
37
|
-
const user = {
|
|
38
|
-
id: 'user-123',
|
|
39
|
-
email: 'user@example.com',
|
|
40
|
-
roles: ['user'],
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
return next({
|
|
44
|
-
ctx: {
|
|
45
|
-
...ctx,
|
|
46
|
-
user,
|
|
47
|
-
} as AuthContext,
|
|
48
|
-
});
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
// Role-based authorization middleware factory
|
|
52
|
-
export const requireRole = (requiredRole: string): MiddlewareFn => {
|
|
53
|
-
return async ({ ctx, next }) => {
|
|
54
|
-
const authCtx = ctx as AuthContext;
|
|
55
|
-
|
|
56
|
-
if (!authCtx.user) {
|
|
57
|
-
throw new TRPCError({
|
|
58
|
-
code: 'UNAUTHORIZED',
|
|
59
|
-
message: 'Authentication required',
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (!authCtx.user.roles?.includes(requiredRole)) {
|
|
64
|
-
throw new TRPCError({
|
|
65
|
-
code: 'FORBIDDEN',
|
|
66
|
-
message: `Role '${requiredRole}' required`,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return next();
|
|
71
|
-
};
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
// Admin role middleware
|
|
75
|
-
export const requireAdmin = requireRole('admin');
|