@veloxts/cli 0.6.31 → 0.6.52
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/CHANGELOG.md +252 -0
- package/dist/cli.js +2 -0
- package/dist/commands/schedule.d.ts +14 -0
- package/dist/commands/schedule.js +324 -0
- package/dist/generators/generators/event.d.ts +35 -0
- package/dist/generators/generators/event.js +99 -0
- package/dist/generators/generators/index.d.ts +5 -0
- package/dist/generators/generators/index.js +15 -0
- package/dist/generators/generators/job.d.ts +36 -0
- package/dist/generators/generators/job.js +98 -0
- package/dist/generators/generators/mail.d.ts +36 -0
- package/dist/generators/generators/mail.js +90 -0
- package/dist/generators/generators/storage.d.ts +35 -0
- package/dist/generators/generators/storage.js +104 -0
- package/dist/generators/generators/task.d.ts +36 -0
- package/dist/generators/generators/task.js +99 -0
- package/dist/generators/templates/event.d.ts +21 -0
- package/dist/generators/templates/event.js +410 -0
- package/dist/generators/templates/job.d.ts +23 -0
- package/dist/generators/templates/job.js +352 -0
- package/dist/generators/templates/mail.d.ts +21 -0
- package/dist/generators/templates/mail.js +411 -0
- package/dist/generators/templates/storage.d.ts +23 -0
- package/dist/generators/templates/storage.js +556 -0
- package/dist/generators/templates/task.d.ts +33 -0
- package/dist/generators/templates/task.js +189 -0
- package/package.json +8 -6
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Template
|
|
3
|
+
*
|
|
4
|
+
* Generates storage configuration and upload handler files for VeloxTS applications.
|
|
5
|
+
*/
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Path Helpers
|
|
8
|
+
// ============================================================================
|
|
9
|
+
/**
|
|
10
|
+
* Get the path for a storage configuration file
|
|
11
|
+
*/
|
|
12
|
+
export function getStoragePath(entityName, _project, options) {
|
|
13
|
+
if (options.upload) {
|
|
14
|
+
return `src/handlers/${entityName.toLowerCase()}-upload.ts`;
|
|
15
|
+
}
|
|
16
|
+
return `src/config/storage/${entityName.toLowerCase()}.ts`;
|
|
17
|
+
}
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Templates
|
|
20
|
+
// ============================================================================
|
|
21
|
+
/**
|
|
22
|
+
* Generate local filesystem storage configuration
|
|
23
|
+
*/
|
|
24
|
+
function generateLocalStorage(ctx) {
|
|
25
|
+
const { entity } = ctx;
|
|
26
|
+
return `/**
|
|
27
|
+
* ${entity.pascal} Storage Configuration (Local Filesystem)
|
|
28
|
+
*
|
|
29
|
+
* Local file storage configuration for ${entity.humanReadable}.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { createLocalStorageDriver } from '@veloxts/storage';
|
|
33
|
+
import { join } from 'node:path';
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Configuration
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* ${entity.pascal} local storage driver
|
|
41
|
+
*
|
|
42
|
+
* Stores files in the local filesystem with organized directory structure.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* \`\`\`typescript
|
|
46
|
+
* import { ${entity.camel}Storage } from '@/config/storage/${entity.kebab}';
|
|
47
|
+
*
|
|
48
|
+
* // Store a file
|
|
49
|
+
* const filePath = await ${entity.camel}Storage.put('avatar.jpg', buffer);
|
|
50
|
+
*
|
|
51
|
+
* // Retrieve a file
|
|
52
|
+
* const fileBuffer = await ${entity.camel}Storage.get('avatar.jpg');
|
|
53
|
+
*
|
|
54
|
+
* // Delete a file
|
|
55
|
+
* await ${entity.camel}Storage.delete('avatar.jpg');
|
|
56
|
+
*
|
|
57
|
+
* // Check if file exists
|
|
58
|
+
* const exists = await ${entity.camel}Storage.exists('avatar.jpg');
|
|
59
|
+
* \`\`\`
|
|
60
|
+
*/
|
|
61
|
+
export const ${entity.camel}Storage = createLocalStorageDriver({
|
|
62
|
+
// Root directory for stored files
|
|
63
|
+
root: join(process.cwd(), 'storage', '${entity.kebab}'),
|
|
64
|
+
|
|
65
|
+
// Base URL for serving files (optional)
|
|
66
|
+
baseUrl: process.env.STORAGE_BASE_URL ?? 'http://localhost:3030/storage/${entity.kebab}',
|
|
67
|
+
|
|
68
|
+
// Disk visibility
|
|
69
|
+
visibility: 'private', // 'public' | 'private'
|
|
70
|
+
|
|
71
|
+
// File permissions (Unix-style, e.g., 0o644 for rw-r--r--)
|
|
72
|
+
permissions: {
|
|
73
|
+
file: 0o644,
|
|
74
|
+
directory: 0o755,
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// Optional: Organize files in subdirectories by date
|
|
78
|
+
pathGenerator: (filename: string) => {
|
|
79
|
+
const date = new Date();
|
|
80
|
+
const year = date.getFullYear();
|
|
81
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
82
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
83
|
+
return \`\${year}/\${month}/\${day}/\${filename}\`;
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// Helper Functions
|
|
89
|
+
// ============================================================================
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Generate a unique filename with timestamp
|
|
93
|
+
*/
|
|
94
|
+
export function generateUniqueFilename(originalFilename: string): string {
|
|
95
|
+
const timestamp = Date.now();
|
|
96
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
97
|
+
const ext = originalFilename.split('.').pop();
|
|
98
|
+
const basename = originalFilename.replace(/\\.[^/.]+$/, '');
|
|
99
|
+
const sanitized = basename.replace(/[^a-z0-9]/gi, '-').toLowerCase();
|
|
100
|
+
|
|
101
|
+
return \`\${sanitized}-\${timestamp}-\${random}.\${ext}\`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Validate file type and size
|
|
106
|
+
*/
|
|
107
|
+
export function validate${entity.pascal}File(file: { mimetype: string; size: number }): {
|
|
108
|
+
valid: boolean;
|
|
109
|
+
error?: string;
|
|
110
|
+
} {
|
|
111
|
+
// Allowed MIME types
|
|
112
|
+
const allowedTypes = [
|
|
113
|
+
'image/jpeg',
|
|
114
|
+
'image/png',
|
|
115
|
+
'image/gif',
|
|
116
|
+
'image/webp',
|
|
117
|
+
'application/pdf',
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
// Max file size (10MB)
|
|
121
|
+
const maxSize = 10 * 1024 * 1024;
|
|
122
|
+
|
|
123
|
+
if (!allowedTypes.includes(file.mimetype)) {
|
|
124
|
+
return {
|
|
125
|
+
valid: false,
|
|
126
|
+
error: \`Invalid file type. Allowed: \${allowedTypes.join(', ')}\`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (file.size > maxSize) {
|
|
131
|
+
return {
|
|
132
|
+
valid: false,
|
|
133
|
+
error: \`File too large. Max size: \${maxSize / (1024 * 1024)}MB\`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { valid: true };
|
|
138
|
+
}
|
|
139
|
+
`;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Generate S3-compatible storage configuration
|
|
143
|
+
*/
|
|
144
|
+
function generateS3Storage(ctx) {
|
|
145
|
+
const { entity } = ctx;
|
|
146
|
+
return `/**
|
|
147
|
+
* ${entity.pascal} Storage Configuration (S3/R2/MinIO)
|
|
148
|
+
*
|
|
149
|
+
* S3-compatible object storage configuration for ${entity.humanReadable}.
|
|
150
|
+
*/
|
|
151
|
+
|
|
152
|
+
import { createS3StorageDriver } from '@veloxts/storage';
|
|
153
|
+
|
|
154
|
+
// ============================================================================
|
|
155
|
+
// Configuration
|
|
156
|
+
// ============================================================================
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* ${entity.pascal} S3 storage driver
|
|
160
|
+
*
|
|
161
|
+
* Stores files in S3-compatible object storage (AWS S3, Cloudflare R2, MinIO).
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* \`\`\`typescript
|
|
165
|
+
* import { ${entity.camel}Storage } from '@/config/storage/${entity.kebab}';
|
|
166
|
+
*
|
|
167
|
+
* // Store a file
|
|
168
|
+
* const fileUrl = await ${entity.camel}Storage.put('avatar.jpg', buffer, {
|
|
169
|
+
* metadata: { userId: '123' },
|
|
170
|
+
* contentType: 'image/jpeg',
|
|
171
|
+
* });
|
|
172
|
+
*
|
|
173
|
+
* // Retrieve a file
|
|
174
|
+
* const fileBuffer = await ${entity.camel}Storage.get('avatar.jpg');
|
|
175
|
+
*
|
|
176
|
+
* // Get a signed URL (temporary access to private file)
|
|
177
|
+
* const signedUrl = await ${entity.camel}Storage.getSignedUrl('avatar.jpg', { expiresIn: 3600 });
|
|
178
|
+
*
|
|
179
|
+
* // Delete a file
|
|
180
|
+
* await ${entity.camel}Storage.delete('avatar.jpg');
|
|
181
|
+
* \`\`\`
|
|
182
|
+
*/
|
|
183
|
+
export const ${entity.camel}Storage = createS3StorageDriver({
|
|
184
|
+
// S3 credentials
|
|
185
|
+
credentials: {
|
|
186
|
+
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
|
|
187
|
+
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
// Bucket configuration
|
|
191
|
+
bucket: process.env.S3_BUCKET ?? '${entity.kebab}-files',
|
|
192
|
+
region: process.env.S3_REGION ?? 'us-east-1',
|
|
193
|
+
|
|
194
|
+
// Optional: Custom endpoint for R2/MinIO
|
|
195
|
+
endpoint: process.env.S3_ENDPOINT, // e.g., 'https://[account-id].r2.cloudflarestorage.com'
|
|
196
|
+
|
|
197
|
+
// Public URL base (for public files)
|
|
198
|
+
baseUrl: process.env.S3_PUBLIC_URL, // e.g., 'https://cdn.example.com'
|
|
199
|
+
|
|
200
|
+
// Default ACL for uploaded files
|
|
201
|
+
acl: 'private', // 'public-read' | 'private' | 'authenticated-read'
|
|
202
|
+
|
|
203
|
+
// Optional: Path prefix for all files
|
|
204
|
+
pathPrefix: '${entity.kebab}/',
|
|
205
|
+
|
|
206
|
+
// Cache control headers
|
|
207
|
+
cacheControl: 'public, max-age=31536000', // 1 year for immutable files
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ============================================================================
|
|
211
|
+
// Helper Functions
|
|
212
|
+
// ============================================================================
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Generate a unique S3 key with organized path structure
|
|
216
|
+
*/
|
|
217
|
+
export function generateS3Key(originalFilename: string, options?: { userId?: string }): string {
|
|
218
|
+
const timestamp = Date.now();
|
|
219
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
220
|
+
const ext = originalFilename.split('.').pop();
|
|
221
|
+
const basename = originalFilename.replace(/\\.[^/.]+$/, '');
|
|
222
|
+
const sanitized = basename.replace(/[^a-z0-9]/gi, '-').toLowerCase();
|
|
223
|
+
|
|
224
|
+
const date = new Date();
|
|
225
|
+
const year = date.getFullYear();
|
|
226
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
227
|
+
|
|
228
|
+
const userPath = options?.userId ? \`users/\${options.userId}/\` : '';
|
|
229
|
+
return \`\${userPath}\${year}/\${month}/\${sanitized}-\${timestamp}-\${random}.\${ext}\`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Validate file for S3 upload
|
|
234
|
+
*/
|
|
235
|
+
export function validate${entity.pascal}File(file: { mimetype: string; size: number }): {
|
|
236
|
+
valid: boolean;
|
|
237
|
+
error?: string;
|
|
238
|
+
} {
|
|
239
|
+
// Allowed MIME types
|
|
240
|
+
const allowedTypes = [
|
|
241
|
+
'image/jpeg',
|
|
242
|
+
'image/png',
|
|
243
|
+
'image/gif',
|
|
244
|
+
'image/webp',
|
|
245
|
+
'image/svg+xml',
|
|
246
|
+
'application/pdf',
|
|
247
|
+
'video/mp4',
|
|
248
|
+
'video/webm',
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
// Max file size (50MB for S3)
|
|
252
|
+
const maxSize = 50 * 1024 * 1024;
|
|
253
|
+
|
|
254
|
+
if (!allowedTypes.includes(file.mimetype)) {
|
|
255
|
+
return {
|
|
256
|
+
valid: false,
|
|
257
|
+
error: \`Invalid file type. Allowed: \${allowedTypes.join(', ')}\`,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (file.size > maxSize) {
|
|
262
|
+
return {
|
|
263
|
+
valid: false,
|
|
264
|
+
error: \`File too large. Max size: \${maxSize / (1024 * 1024)}MB\`,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { valid: true };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Get content type from file extension
|
|
273
|
+
*/
|
|
274
|
+
export function getContentType(filename: string): string {
|
|
275
|
+
const ext = filename.split('.').pop()?.toLowerCase();
|
|
276
|
+
const mimeTypes: Record<string, string> = {
|
|
277
|
+
jpg: 'image/jpeg',
|
|
278
|
+
jpeg: 'image/jpeg',
|
|
279
|
+
png: 'image/png',
|
|
280
|
+
gif: 'image/gif',
|
|
281
|
+
webp: 'image/webp',
|
|
282
|
+
svg: 'image/svg+xml',
|
|
283
|
+
pdf: 'application/pdf',
|
|
284
|
+
mp4: 'video/mp4',
|
|
285
|
+
webm: 'video/webm',
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
return mimeTypes[ext ?? ''] ?? 'application/octet-stream';
|
|
289
|
+
}
|
|
290
|
+
`;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Generate file upload handler with storage integration
|
|
294
|
+
*/
|
|
295
|
+
function generateUploadHandler(ctx) {
|
|
296
|
+
const { entity } = ctx;
|
|
297
|
+
return `/**
|
|
298
|
+
* ${entity.pascal} Upload Handler
|
|
299
|
+
*
|
|
300
|
+
* File upload endpoint with storage integration for ${entity.humanReadable}.
|
|
301
|
+
*/
|
|
302
|
+
|
|
303
|
+
import type { BaseContext } from '@veloxts/core';
|
|
304
|
+
import { procedure } from '@veloxts/router';
|
|
305
|
+
import { ${entity.camel}Storage, generateUniqueFilename, validate${entity.pascal}File } from '@/config/storage/${entity.kebab}';
|
|
306
|
+
import { z } from 'zod';
|
|
307
|
+
|
|
308
|
+
// ============================================================================
|
|
309
|
+
// Schema
|
|
310
|
+
// ============================================================================
|
|
311
|
+
|
|
312
|
+
const Upload${entity.pascal}Schema = z.object({
|
|
313
|
+
// File metadata will be provided via multipart form data
|
|
314
|
+
// Fastify @fastify/multipart plugin handles file parsing
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const Upload${entity.pascal}ResponseSchema = z.object({
|
|
318
|
+
success: z.boolean(),
|
|
319
|
+
fileUrl: z.string().url(),
|
|
320
|
+
filename: z.string(),
|
|
321
|
+
size: z.number(),
|
|
322
|
+
mimetype: z.string(),
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
export type Upload${entity.pascal}Response = z.infer<typeof Upload${entity.pascal}ResponseSchema>;
|
|
326
|
+
|
|
327
|
+
// ============================================================================
|
|
328
|
+
// Upload Handler
|
|
329
|
+
// ============================================================================
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Upload ${entity.humanReadable} file
|
|
333
|
+
*
|
|
334
|
+
* Handles multipart file uploads with validation and storage.
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* \`\`\`typescript
|
|
338
|
+
* // Client-side (with fetch)
|
|
339
|
+
* const formData = new FormData();
|
|
340
|
+
* formData.append('file', fileInput.files[0]);
|
|
341
|
+
*
|
|
342
|
+
* const response = await fetch('/api/${entity.kebab}/upload', {
|
|
343
|
+
* method: 'POST',
|
|
344
|
+
* body: formData,
|
|
345
|
+
* });
|
|
346
|
+
*
|
|
347
|
+
* const result = await response.json();
|
|
348
|
+
* console.log('File uploaded:', result.fileUrl);
|
|
349
|
+
* \`\`\`
|
|
350
|
+
*/
|
|
351
|
+
export const upload${entity.pascal} = procedure
|
|
352
|
+
.input(Upload${entity.pascal}Schema)
|
|
353
|
+
.output(Upload${entity.pascal}ResponseSchema)
|
|
354
|
+
.mutation(async ({ ctx }: { ctx: BaseContext }) => {
|
|
355
|
+
// Get multipart data from request
|
|
356
|
+
const data = await ctx.request.file();
|
|
357
|
+
|
|
358
|
+
if (!data) {
|
|
359
|
+
throw new Error('No file uploaded');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Validate file
|
|
363
|
+
const validation = validate${entity.pascal}File({
|
|
364
|
+
mimetype: data.mimetype,
|
|
365
|
+
size: data.file.bytesRead,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
if (!validation.valid) {
|
|
369
|
+
throw new Error(validation.error);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Generate unique filename
|
|
373
|
+
const filename = generateUniqueFilename(data.filename);
|
|
374
|
+
|
|
375
|
+
// Convert stream to buffer
|
|
376
|
+
const buffer = await data.toBuffer();
|
|
377
|
+
|
|
378
|
+
// Store file
|
|
379
|
+
const filePath = await ${entity.camel}Storage.put(filename, buffer);
|
|
380
|
+
|
|
381
|
+
// Get public URL (if storage is configured for public access)
|
|
382
|
+
const fileUrl = await ${entity.camel}Storage.url(filePath);
|
|
383
|
+
|
|
384
|
+
// TODO: Save file metadata to database
|
|
385
|
+
// Example:
|
|
386
|
+
// await ctx.db.${entity.camel}File.create({
|
|
387
|
+
// data: {
|
|
388
|
+
// filename,
|
|
389
|
+
// path: filePath,
|
|
390
|
+
// url: fileUrl,
|
|
391
|
+
// mimetype: data.mimetype,
|
|
392
|
+
// size: buffer.length,
|
|
393
|
+
// userId: ctx.user?.id,
|
|
394
|
+
// },
|
|
395
|
+
// });
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
success: true,
|
|
399
|
+
fileUrl,
|
|
400
|
+
filename,
|
|
401
|
+
size: buffer.length,
|
|
402
|
+
mimetype: data.mimetype,
|
|
403
|
+
};
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// ============================================================================
|
|
407
|
+
// Delete Handler
|
|
408
|
+
// ============================================================================
|
|
409
|
+
|
|
410
|
+
const Delete${entity.pascal}Schema = z.object({
|
|
411
|
+
filename: z.string(),
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Delete ${entity.humanReadable} file
|
|
416
|
+
*/
|
|
417
|
+
export const delete${entity.pascal} = procedure
|
|
418
|
+
.input(Delete${entity.pascal}Schema)
|
|
419
|
+
.output(z.object({ success: z.boolean() }))
|
|
420
|
+
.mutation(async ({ input }) => {
|
|
421
|
+
// TODO: Verify user owns this file or has permission to delete
|
|
422
|
+
// const file = await ctx.db.${entity.camel}File.findUnique({
|
|
423
|
+
// where: { filename: input.filename },
|
|
424
|
+
// });
|
|
425
|
+
//
|
|
426
|
+
// if (!file || file.userId !== ctx.user?.id) {
|
|
427
|
+
// throw new Error('File not found or unauthorized');
|
|
428
|
+
// }
|
|
429
|
+
|
|
430
|
+
await ${entity.camel}Storage.delete(input.filename);
|
|
431
|
+
|
|
432
|
+
// TODO: Delete from database
|
|
433
|
+
// await ctx.db.${entity.camel}File.delete({
|
|
434
|
+
// where: { filename: input.filename },
|
|
435
|
+
// });
|
|
436
|
+
|
|
437
|
+
return { success: true };
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// ============================================================================
|
|
441
|
+
// Download Handler
|
|
442
|
+
// ============================================================================
|
|
443
|
+
|
|
444
|
+
const Download${entity.pascal}Schema = z.object({
|
|
445
|
+
filename: z.string(),
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Download ${entity.humanReadable} file
|
|
450
|
+
*
|
|
451
|
+
* Returns a temporary signed URL for secure file access.
|
|
452
|
+
*/
|
|
453
|
+
export const download${entity.pascal} = procedure
|
|
454
|
+
.input(Download${entity.pascal}Schema)
|
|
455
|
+
.output(z.object({ downloadUrl: z.string().url() }))
|
|
456
|
+
.query(async ({ input }) => {
|
|
457
|
+
// TODO: Verify user has access to this file
|
|
458
|
+
// const file = await ctx.db.${entity.camel}File.findUnique({
|
|
459
|
+
// where: { filename: input.filename },
|
|
460
|
+
// });
|
|
461
|
+
//
|
|
462
|
+
// if (!file) {
|
|
463
|
+
// throw new Error('File not found');
|
|
464
|
+
// }
|
|
465
|
+
|
|
466
|
+
// Check if file exists
|
|
467
|
+
const exists = await ${entity.camel}Storage.exists(input.filename);
|
|
468
|
+
if (!exists) {
|
|
469
|
+
throw new Error('File not found in storage');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Generate signed URL (expires in 1 hour)
|
|
473
|
+
const downloadUrl = await ${entity.camel}Storage.getSignedUrl(input.filename, {
|
|
474
|
+
expiresIn: 3600,
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
return { downloadUrl };
|
|
478
|
+
});
|
|
479
|
+
`;
|
|
480
|
+
}
|
|
481
|
+
// ============================================================================
|
|
482
|
+
// Main Template
|
|
483
|
+
// ============================================================================
|
|
484
|
+
/**
|
|
485
|
+
* Storage template function
|
|
486
|
+
*/
|
|
487
|
+
export const storageTemplate = (ctx) => {
|
|
488
|
+
if (ctx.options.upload) {
|
|
489
|
+
return generateUploadHandler(ctx);
|
|
490
|
+
}
|
|
491
|
+
if (ctx.options.s3) {
|
|
492
|
+
return generateS3Storage(ctx);
|
|
493
|
+
}
|
|
494
|
+
// Default to local storage
|
|
495
|
+
return generateLocalStorage(ctx);
|
|
496
|
+
};
|
|
497
|
+
// ============================================================================
|
|
498
|
+
// Post-generation Instructions
|
|
499
|
+
// ============================================================================
|
|
500
|
+
export function getStorageInstructions(entityName, options) {
|
|
501
|
+
const lines = [];
|
|
502
|
+
if (options.upload) {
|
|
503
|
+
lines.push(`Your ${entityName} upload handler has been created.`, '', 'Next steps:');
|
|
504
|
+
lines.push(' 1. Register @fastify/multipart plugin in your app:');
|
|
505
|
+
lines.push('');
|
|
506
|
+
lines.push(" import multipart from '@fastify/multipart';");
|
|
507
|
+
lines.push(' await app.register(multipart);');
|
|
508
|
+
lines.push('');
|
|
509
|
+
lines.push(' 2. Create the storage configuration file:');
|
|
510
|
+
lines.push(` velox make storage ${entityName} --local # or --s3`);
|
|
511
|
+
lines.push('');
|
|
512
|
+
lines.push(' 3. Register upload procedures in your router');
|
|
513
|
+
lines.push(' 4. Optional: Create a Prisma model for file metadata');
|
|
514
|
+
}
|
|
515
|
+
else if (options.s3) {
|
|
516
|
+
lines.push(`Your ${entityName} S3 storage configuration has been created.`, '', 'Next steps:');
|
|
517
|
+
lines.push(' 1. Add environment variables to .env:');
|
|
518
|
+
lines.push('');
|
|
519
|
+
lines.push(' S3_ACCESS_KEY_ID=your-access-key');
|
|
520
|
+
lines.push(' S3_SECRET_ACCESS_KEY=your-secret-key');
|
|
521
|
+
lines.push(` S3_BUCKET=${entityName}-files`);
|
|
522
|
+
lines.push(' S3_REGION=us-east-1');
|
|
523
|
+
lines.push(' # Optional for R2/MinIO:');
|
|
524
|
+
lines.push(' # S3_ENDPOINT=https://[account-id].r2.cloudflarestorage.com');
|
|
525
|
+
lines.push(' # S3_PUBLIC_URL=https://cdn.example.com');
|
|
526
|
+
lines.push('');
|
|
527
|
+
lines.push(' 2. Install AWS SDK dependencies:');
|
|
528
|
+
lines.push('');
|
|
529
|
+
lines.push(' pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner');
|
|
530
|
+
lines.push('');
|
|
531
|
+
lines.push(' 3. Import and use the storage driver in your procedures');
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
lines.push(`Your ${entityName} local storage configuration has been created.`, '', 'Next steps:');
|
|
535
|
+
lines.push(' 1. Ensure the storage directory exists:');
|
|
536
|
+
lines.push('');
|
|
537
|
+
lines.push(` mkdir -p storage/${entityName.toLowerCase()}`);
|
|
538
|
+
lines.push('');
|
|
539
|
+
lines.push(' 2. Add storage directory to .gitignore:');
|
|
540
|
+
lines.push('');
|
|
541
|
+
lines.push(' echo "storage/" >> .gitignore');
|
|
542
|
+
lines.push('');
|
|
543
|
+
lines.push(' 3. Configure file serving in your app (for public access):');
|
|
544
|
+
lines.push('');
|
|
545
|
+
lines.push(" import fastifyStatic from '@fastify/static';");
|
|
546
|
+
lines.push(" import { join } from 'node:path';");
|
|
547
|
+
lines.push('');
|
|
548
|
+
lines.push(' await app.register(fastifyStatic, {');
|
|
549
|
+
lines.push(" root: join(process.cwd(), 'storage'),");
|
|
550
|
+
lines.push(" prefix: '/storage/',");
|
|
551
|
+
lines.push(' });');
|
|
552
|
+
lines.push('');
|
|
553
|
+
lines.push(' 4. Import and use the storage driver in your procedures');
|
|
554
|
+
}
|
|
555
|
+
return lines.join('\n');
|
|
556
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Template
|
|
3
|
+
*
|
|
4
|
+
* Template for generating scheduled task files.
|
|
5
|
+
*/
|
|
6
|
+
import type { ProjectContext, TemplateContext } from '../types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Options specific to task generation
|
|
9
|
+
*/
|
|
10
|
+
export interface TaskOptions {
|
|
11
|
+
/** Generate task with callback handlers */
|
|
12
|
+
callbacks: boolean;
|
|
13
|
+
/** Generate task with constraints */
|
|
14
|
+
constraints: boolean;
|
|
15
|
+
/** Generate task with overlap prevention */
|
|
16
|
+
noOverlap: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Get the output path for a task file
|
|
20
|
+
*/
|
|
21
|
+
export declare function getTaskPath(entityName: string, _project?: ProjectContext): string;
|
|
22
|
+
/**
|
|
23
|
+
* Generate a scheduled task file
|
|
24
|
+
*/
|
|
25
|
+
export declare function taskTemplate(context: TemplateContext<TaskOptions>): string;
|
|
26
|
+
/**
|
|
27
|
+
* Generate a schedule file that imports and exports all tasks
|
|
28
|
+
*/
|
|
29
|
+
export declare function scheduleFileTemplate(taskNames: string[]): string;
|
|
30
|
+
/**
|
|
31
|
+
* Get instructions to display after generating a task
|
|
32
|
+
*/
|
|
33
|
+
export declare function getTaskInstructions(entityName: string, options: TaskOptions): string;
|