@currentjs/gen 0.2.1 → 0.3.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/README.md +256 -8
  3. package/dist/cli.js +26 -0
  4. package/dist/commands/commit.js +4 -0
  5. package/dist/commands/createApp.js +7 -0
  6. package/dist/commands/diff.js +4 -0
  7. package/dist/commands/generateAll.js +159 -29
  8. package/dist/commands/migrateCommit.d.ts +1 -0
  9. package/dist/commands/migrateCommit.js +201 -0
  10. package/dist/generators/controllerGenerator.d.ts +7 -0
  11. package/dist/generators/controllerGenerator.js +60 -29
  12. package/dist/generators/domainModelGenerator.d.ts +8 -0
  13. package/dist/generators/domainModelGenerator.js +71 -4
  14. package/dist/generators/serviceGenerator.d.ts +17 -1
  15. package/dist/generators/serviceGenerator.js +139 -12
  16. package/dist/generators/storeGenerator.d.ts +9 -0
  17. package/dist/generators/storeGenerator.js +148 -8
  18. package/dist/generators/templateGenerator.d.ts +19 -0
  19. package/dist/generators/templateGenerator.js +216 -11
  20. package/dist/generators/templates/appTemplates.d.ts +8 -7
  21. package/dist/generators/templates/appTemplates.js +11 -1572
  22. package/dist/generators/templates/data/appTsTemplate +39 -0
  23. package/dist/generators/templates/data/appYamlTemplate +4 -0
  24. package/dist/generators/templates/data/cursorRulesTemplate +671 -0
  25. package/dist/generators/templates/data/errorTemplate +28 -0
  26. package/dist/generators/templates/data/frontendScriptTemplate +739 -0
  27. package/dist/generators/templates/data/mainViewTemplate +16 -0
  28. package/dist/generators/templates/data/translationsTemplate +68 -0
  29. package/dist/generators/templates/data/tsConfigTemplate +19 -0
  30. package/dist/generators/templates/viewTemplates.d.ts +10 -1
  31. package/dist/generators/templates/viewTemplates.js +138 -6
  32. package/dist/generators/validationGenerator.d.ts +5 -0
  33. package/dist/generators/validationGenerator.js +51 -0
  34. package/dist/utils/cliUtils.d.ts +6 -0
  35. package/dist/utils/cliUtils.js +24 -0
  36. package/dist/utils/constants.d.ts +3 -0
  37. package/dist/utils/constants.js +5 -2
  38. package/dist/utils/migrationUtils.d.ts +49 -0
  39. package/dist/utils/migrationUtils.js +291 -0
  40. package/howto.md +158 -66
  41. package/package.json +3 -2
@@ -35,8 +35,8 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.DEFAULT_FILES = exports.DEFAULT_DIRECTORIES = exports.cursorRulesTemplate = exports.translationsTemplate = exports.frontendScriptTemplate = exports.errorTemplate = exports.mainViewTemplate = exports.appTsTemplate = exports.appYamlTemplate = exports.tsconfigTemplate = exports.packageJsonTemplate = void 0;
37
37
  // Application-level templates for project scaffolding
38
+ const fs = __importStar(require("fs"));
38
39
  const path = __importStar(require("path"));
39
- const constants_1 = require("../../utils/constants");
40
40
  const packageJsonTemplate = (appName) => JSON.stringify({
41
41
  name: appName || 'my-app',
42
42
  version: '0.1.0',
@@ -62,1583 +62,22 @@ const packageJsonTemplate = (appName) => JSON.stringify({
62
62
  type: 'module'
63
63
  }, null, 2);
64
64
  exports.packageJsonTemplate = packageJsonTemplate;
65
- exports.tsconfigTemplate = `{
66
- "compilerOptions": {
67
- "module": "es2020",
68
- "target": "es2020",
69
- "sourceMap": false,
70
- "experimentalDecorators": true,
71
- "emitDecoratorMetadata": true,
72
- "baseUrl": "./src",
73
- "outDir": "./build",
74
- "types": ["node"],
75
- "moduleResolution": "Node",
76
- "declaration": true,
77
- "declarationMap": false
78
- },
79
- "exclude": [
80
- "node_modules"
81
- ],
82
- "type": "module"
83
- }`;
84
- exports.appYamlTemplate = `
85
- providers:
86
- mysql: '@currentjs/provider-mysql'
87
- database: mysql
88
- modules: []`;
89
- exports.appTsTemplate = `import { createWebServer } from '@currentjs/router';
90
- import { createTemplateEngine } from '@currentjs/templating';
91
- import * as path from 'path';
92
-
93
- /** DO NOT CHANGE THESE LINES **/
94
- ${constants_1.GENERATOR_MARKERS.CONTROLLERS_START}
95
- ${constants_1.GENERATOR_MARKERS.CONTROLLERS_END}
96
- /** END OF DO NOT CHANGE THESE LINES **/
97
-
98
- export async function main() {
99
- await Promise.all(Object.values(providers).map(provider => provider.init()));
100
- const webDir = path.join(process.cwd(), 'web');
101
- const renderEngine = createTemplateEngine({ directories: [process.cwd()] });
102
- const app = createWebServer({ controllers, webDir }, {
103
- port: 3000,
104
- renderer: (template, data, layout) => {
105
- try {
106
- return layout ? renderEngine.renderWithLayout(layout, template, data) : renderEngine.render(template, data);
107
- } catch (e) {
108
- return String(e instanceof Error ? e.message : e);
109
- }
110
- },
111
- errorTemplate: 'error'
112
- });
113
- await app.listen();
114
- console.log('Server started on http://localhost:3000');
115
-
116
- const handleTermination = async () => {
117
- try { await app.close(); } catch {}
118
- const shutdowns = Object.values(providers).map(provider => provider.shutdown ?? (() => {}));
119
- await Promise.all(shutdowns);
120
- process.exit(0);
121
- };
122
-
123
- process.on('SIGINT', handleTermination);
124
- process.on('SIGTERM', handleTermination);
125
- }
126
-
127
- void main();
128
- `;
129
- exports.mainViewTemplate = `<!-- @template name="main_view" -->
130
- <!doctype html>
131
- <html lang="en">
132
- <head>
133
- <meta charset="UTF-8">
134
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
135
- <title>Your App</title>
136
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
137
- <script src="/app.js"></script>
138
- </head>
139
- <body>
140
- <div class="container-fluid">
141
- <div id="main">{{ content }}</div>
142
- </div>
143
- </body>
144
- </html>
145
- `;
146
- exports.errorTemplate = `<!-- @template name="error" -->
147
- <!doctype html>
148
- <html lang="en">
149
- <head>
150
- <meta charset="UTF-8">
151
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
152
- <title>Error - Your App</title>
153
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
154
- <script src="/app.js"></script>
155
- </head>
156
- <body>
157
- <div class="container-fluid">
158
- <div class="row justify-content-center">
159
- <div class="col-md-6">
160
- <div class="text-center mt-5">
161
- <div class="display-1 text-danger fw-bold mb-3">{{ statusCode }}</div>
162
- <h2 class="mb-3">Oops! Something went wrong</h2>
163
- <p class="text-muted mb-4">{{ error }}</p>
164
- <div class="d-flex gap-2 justify-content-center">
165
- <button class="btn btn-primary" onclick="window.history.back()">Go Back</button>
166
- <a href="/" class="btn btn-outline-secondary">Home Page</a>
167
- </div>
168
- </div>
169
- </div>
170
- </div>
171
- </div>
172
- </body>
173
- </html>
174
- `;
175
- exports.frontendScriptTemplate = `/**
176
- * Common Frontend Functions for Generated Apps
177
- * This script provides utilities for UI feedback, navigation, form handling, and SPA-like behavior
178
- */
179
-
180
- // Global configuration
181
- window.AppConfig = {
182
- toastDuration: 3000,
183
- modalDuration: 1200,
184
- animationDuration: 300,
185
- debounceDelay: 300,
186
- translations: {},
187
- currentLang: null
188
- };
189
-
190
- // ===== TRANSLATION FUNCTIONS =====
191
-
192
- /**
193
- * Get current language
194
- * Priority: localStorage -> navigator.language -> 'en'
195
- * @returns {string} Current language code
196
- */
197
- function getCurrentLanguage() {
198
- if (window.AppConfig.currentLang) {
199
- return window.AppConfig.currentLang;
200
- }
201
-
202
- // 1. Check localStorage
203
- const storedLang = localStorage.getItem('lang');
204
- if (storedLang) {
205
- window.AppConfig.currentLang = storedLang;
206
- return storedLang;
207
- }
208
-
209
- // 2. Check browser language (Accept-Language equivalent)
210
- const browserLang = navigator.language || navigator.languages?.[0];
211
- if (browserLang) {
212
- // Extract language code (e.g., 'en-US' -> 'en')
213
- const langCode = browserLang.split('-')[0];
214
- window.AppConfig.currentLang = langCode;
215
- return langCode;
216
- }
217
-
218
- // 3. Default fallback
219
- window.AppConfig.currentLang = 'en';
220
- return 'en';
221
- }
222
-
223
- /**
224
- * Translate string to current language
225
- * @param {string} str - String in default language to translate
226
- * @returns {string} Translated string or original if translation not found
227
- */
228
- function t(str) {
229
- if (!str || typeof str !== 'string') return str;
230
-
231
- const currentLang = getCurrentLanguage();
232
-
233
- // If current language is the default or no translations loaded, return original
234
- if (currentLang === 'en' || !window.AppConfig.translations || !window.AppConfig.translations[currentLang]) {
235
- return str;
236
- }
237
-
238
- const translation = window.AppConfig.translations[currentLang][str];
239
- return translation || str;
240
- }
241
-
242
- /**
243
- * Set current language and save to localStorage
244
- * @param {string} langKey - Language code (e.g., 'en', 'ru', 'pl')
245
- */
246
- function setLang(langKey) {
247
- if (!langKey || typeof langKey !== 'string') return;
248
-
249
- window.AppConfig.currentLang = langKey;
250
- localStorage.setItem('lang', langKey);
251
-
252
- // Optionally reload page to apply translations
253
- // Uncomment the next line if you want automatic page reload on language change
254
- // window.location.reload();
255
- }
256
-
257
- /**
258
- * Load translations from JSON file
259
- * @param {string} url - URL to translations JSON file (default: '/translations.json')
260
- */
261
- function loadTranslations(url = '/translations.json') {
262
- fetch(url)
263
- .then(response => {
264
- if (!response.ok) {
265
- console.warn('Translations file not found:', url);
266
- return {};
267
- }
268
- return response.json();
269
- })
270
- .then(translations => {
271
- window.AppConfig.translations = translations || {};
272
- })
273
- .catch(error => {
274
- console.warn('Failed to load translations:', error);
275
- window.AppConfig.translations = {};
276
- });
277
- }
278
-
279
- // ===== UI FEEDBACK & NOTIFICATIONS =====
280
-
281
- /**
282
- * Show a toast notification
283
- * @param {string} message - The message to display
284
- * @param {string} type - 'success', 'error', 'info', 'warning'
285
- */
286
- function showToast(message, type = 'info') {
287
- // Translate the message
288
- message = t(message);
289
- const toast = document.createElement('div');
290
- toast.className = 'app-toast app-toast-' + type;
291
- toast.textContent = message;
292
- toast.style.cssText = \`
293
- position: fixed;
294
- top: 20px;
295
- right: 20px;
296
- padding: 12px 24px;
297
- border-radius: 4px;
298
- color: white;
299
- font-weight: 500;
300
- z-index: 10000;
301
- max-width: 300px;
302
- word-wrap: break-word;
303
- transition: all \${window.AppConfig.animationDuration}ms ease;
304
- transform: translateX(100%);
305
- opacity: 0;
306
- \`;
307
-
308
- // Type-specific styling
309
- const colors = {
310
- success: '#10b981',
311
- error: '#ef4444',
312
- warning: '#f59e0b',
313
- info: '#3b82f6'
314
- };
315
- toast.style.backgroundColor = colors[type] || colors.info;
316
-
317
- document.body.appendChild(toast);
318
-
319
- // Animate in
320
- setTimeout(() => {
321
- toast.style.transform = 'translateX(0)';
322
- toast.style.opacity = '1';
323
- }, 10);
324
-
325
- // Auto remove
326
- setTimeout(() => {
327
- toast.style.transform = 'translateX(100%)';
328
- toast.style.opacity = '0';
329
- setTimeout(() => {
330
- if (toast.parentNode) {
331
- toast.parentNode.removeChild(toast);
332
- }
333
- }, window.AppConfig.animationDuration);
334
- }, window.AppConfig.toastDuration);
335
- }
336
-
337
- /**
338
- * Display inline message in specific container
339
- * @param {string} elementId - ID of the target element
340
- * @param {string} message - The message to display
341
- * @param {string} type - 'success', 'error', 'info', 'warning'
342
- */
343
- function showMessage(elementId, message, type = 'info') {
344
- const element = getElementSafely('#' + elementId);
345
- if (!element) return;
346
-
347
- // Translate the message
348
- message = t(message);
349
- element.textContent = message;
350
- element.className = 'app-message app-message-' + type;
351
- element.style.cssText = \`
352
- padding: 8px 12px;
353
- border-radius: 4px;
354
- margin: 8px 0;
355
- font-size: 14px;
356
- display: block;
357
- \`;
358
-
359
- // Type-specific styling
360
- const styles = {
361
- success: 'background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0;',
362
- error: 'background: #fee2e2; color: #991b1b; border: 1px solid #fca5a5;',
363
- warning: 'background: #fef3c7; color: #92400e; border: 1px solid #fcd34d;',
364
- info: 'background: #dbeafe; color: #1e40af; border: 1px solid #93c5fd;'
365
- };
366
- element.style.cssText += styles[type] || styles.info;
367
- }
368
-
369
- /**
370
- * Show modal dialog
371
- * @param {string} modalId - ID of the modal element
372
- * @param {string} message - The message to display
373
- * @param {string} type - 'success', 'error', 'info', 'warning'
374
- */
375
- function showModal(modalId, message, type = 'info') {
376
- let modal = getElementSafely('#' + modalId);
377
-
378
- if (!modal) {
379
- // Create modal if it doesn't exist
380
- modal = document.createElement('dialog');
381
- modal.id = modalId;
382
- modal.innerHTML = \`
383
- <div class="modal-content">
384
- <div class="modal-header">
385
- <button class="modal-close" onclick="this.closest('dialog').close()">&times;</button>
386
- </div>
387
- <div class="modal-body"></div>
388
- </div>
389
- \`;
390
- modal.style.cssText = \`
391
- border: none;
392
- border-radius: 8px;
393
- padding: 0;
394
- max-width: 400px;
395
- box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
396
- \`;
397
- document.body.appendChild(modal);
398
- }
399
-
400
- const content = modal.querySelector('.modal-body');
401
- if (content) {
402
- // Translate the message
403
- message = t(message);
404
- content.textContent = message;
405
- content.className = 'modal-body modal-' + type;
406
- content.style.cssText = 'padding: 20px; text-align: center;';
407
- }
408
-
409
- if (modal.showModal) {
410
- modal.showModal();
411
- setTimeout(() => {
412
- try { modal.close(); } catch(e) {}
413
- }, window.AppConfig.modalDuration);
414
- }
415
- }
416
-
417
- // ===== NAVIGATION & PAGE ACTIONS =====
418
-
419
- /**
420
- * Enhanced history.back() with fallback
421
- */
422
- function navigateBack() {
423
- if (window.history.length > 1) {
424
- window.history.back();
425
- } else {
426
- window.location.href = '/';
427
- }
428
- }
429
-
430
- /**
431
- * Safe redirect with validation
432
- * @param {string} url - The URL to redirect to
433
- */
434
- function redirectTo(url) {
435
- if (!url || typeof url !== 'string') return;
436
- // Basic URL validation
437
- if (url.startsWith('/') || url.startsWith('http')) {
438
- window.location.href = url;
439
- }
440
- }
441
-
442
- /**
443
- * Page reload with loading indication
444
- */
445
- function reloadPage() {
446
- showToast('Reloading page...', 'info');
447
- setTimeout(() => {
448
- window.location.reload();
449
- }, 500);
450
- }
451
-
452
- /**
453
- * Refresh specific page sections by reloading their content
454
- * @param {string} selector - CSS selector for the section to refresh
455
- */
456
- function refreshSection(selector) {
457
- const element = getElementSafely(selector);
458
- if (!element) return;
459
-
460
- // If element has data-refresh-url, use that to reload content
461
- const refreshUrl = element.getAttribute('data-refresh-url');
462
- if (refreshUrl) {
463
- fetch(refreshUrl)
464
- .then(response => response.text())
465
- .then(html => {
466
- element.innerHTML = html;
467
- })
468
- .catch(error => {
469
- console.error('Failed to refresh section:', error);
470
- showToast('Failed to refresh content', 'error');
471
- });
472
- }
473
- }
474
-
475
- // ===== FORM & CONTENT MANAGEMENT =====
476
-
477
- /**
478
- * Safe element removal with animation
479
- * @param {string} selector - CSS selector for element to remove
480
- */
481
- function removeElement(selector) {
482
- const element = getElementSafely(selector);
483
- if (!element) return;
484
-
485
- element.style.transition = \`opacity \${window.AppConfig.animationDuration}ms ease\`;
486
- element.style.opacity = '0';
487
-
488
- setTimeout(() => {
489
- if (element.parentNode) {
490
- element.parentNode.removeChild(element);
491
- }
492
- }, window.AppConfig.animationDuration);
493
- }
494
-
495
- /**
496
- * Update content in element
497
- * @param {string} selector - CSS selector for target element
498
- * @param {string} content - New content
499
- * @param {string} mode - 'replace', 'append', 'prepend'
500
- */
501
- function updateContent(selector, content, mode = 'replace') {
502
- const element = getElementSafely(selector);
503
- if (!element) return;
504
-
505
- switch (mode) {
506
- case 'append':
507
- element.innerHTML += content;
508
- break;
509
- case 'prepend':
510
- element.innerHTML = content + element.innerHTML;
511
- break;
512
- case 'replace':
513
- default:
514
- element.innerHTML = content;
515
- break;
516
- }
517
- }
518
-
519
- /**
520
- * Reset form fields and validation states
521
- * @param {string} formSelector - CSS selector for the form
522
- */
523
- function clearForm(formSelector) {
524
- const form = getElementSafely(formSelector);
525
- if (!form) return;
526
-
527
- if (form.reset) {
528
- form.reset();
529
- }
530
-
531
- // Clear validation messages
532
- const messages = form.querySelectorAll('.app-message');
533
- messages.forEach(msg => msg.remove());
534
- }
535
-
536
- // ===== CUSTOM SPA INTEGRATION =====
537
-
538
- /**
539
- * Centralized success handler that executes strategy array
540
- * @param {object} response - The response object
541
- * @param {string[]} strategy - Array of strategy actions
542
- * @param {object} options - Additional options (basePath, entityName, etc.)
543
- */
544
- function handleFormSuccess(response, strategy = ['back', 'toast'], options = {}) {
545
- const { basePath, entityName = 'Item' } = options;
546
-
547
- strategy.forEach(action => {
548
- switch (action) {
549
- case 'toast':
550
- showToast(\`\${entityName} saved successfully\`, 'success');
551
- break;
552
- case 'message':
553
- if (options.messageId) {
554
- showMessage(options.messageId, \`\${entityName} saved successfully\`, 'success');
555
- }
556
- break;
557
- case 'modal':
558
- if (options.modalId) {
559
- showModal(options.modalId, \`\${entityName} saved successfully\`, 'success');
560
- }
561
- break;
562
- case 'remove':
563
- // If targetSelector is specified, remove that element instead of the form
564
- if (options.targetSelector) {
565
- removeElement(options.targetSelector);
566
- } else if (options.formSelector) {
567
- removeElement(options.formSelector);
568
- }
569
- break;
570
- case 'redirect':
571
- if (basePath) {
572
- redirectTo(basePath);
573
- }
574
- break;
575
- case 'back':
576
- navigateBack();
577
- break;
578
- case 'reload':
579
- case 'refresh':
580
- reloadPage();
581
- break;
582
- }
583
- });
584
- }
585
-
586
- /**
587
- * Handle internal link navigation via fetch
588
- * @param {string} url - The URL to navigate to
589
- * @param {Element} targetElement - Element to update with new content (default: #main)
590
- */
591
- function navigateToPage(url, targetElement = null) {
592
- const target = targetElement || document.querySelector('#main');
593
- if (!target) return;
594
-
595
- showLoading('#main');
596
-
597
- fetch(url, {
598
- headers: {
599
- 'Accept': 'text/html',
600
- 'X-Partial-Content': 'true'
601
- }
602
- })
603
- .then(response => {
604
- if (!response.ok) {
605
- throw new Error(\`HTTP \${response.status}: \${response.statusText}\`);
606
- }
607
- return response.text();
608
- })
609
- .then(html => {
610
- target.innerHTML = html;
611
- // Update browser history
612
- window.history.pushState({}, '', url);
613
- // Re-initialize event listeners for new content
614
- initializeEventListeners();
615
- })
616
- .catch(error => {
617
- console.error('Navigation failed:', error);
618
- showToast('Failed to load page', 'error');
619
- // Fallback to normal navigation
620
- window.location.href = url;
621
- })
622
- .finally(() => {
623
- hideLoading('#main');
624
- });
625
- }
626
-
627
- /**
628
- * Convert form value to appropriate type based on field type
629
- * @param {string} value - Raw form value
630
- * @param {string} fieldType - Field type (number, boolean, etc.)
631
- * @returns {any} Converted value
632
- */
633
- function convertFieldValue(value, fieldType) {
634
- if (!value || value === '') {
635
- return null;
636
- }
637
-
638
- switch (fieldType.toLowerCase()) {
639
- case 'number':
640
- case 'int':
641
- case 'integer':
642
- const intVal = parseInt(value, 10);
643
- return isNaN(intVal) ? null : intVal;
644
-
645
- case 'float':
646
- case 'decimal':
647
- const floatVal = parseFloat(value);
648
- return isNaN(floatVal) ? null : floatVal;
649
-
650
- case 'boolean':
651
- case 'bool':
652
- if (value === 'true') return true;
653
- if (value === 'false') return false;
654
- return Boolean(value);
655
-
656
- case 'enum':
657
- case 'string':
658
- case 'text':
659
- default:
660
- return value;
661
- }
662
- }
663
-
664
- /**
665
- * Handle form submission via fetch with JSON data
666
- * @param {HTMLFormElement} form - The form element
667
- * @param {string[]} strategy - Strategy actions to execute on success
668
- * @param {object} options - Additional options
669
- */
670
- function submitForm(form, strategy = ['back', 'toast'], options = {}) {
671
- const formData = new FormData(form);
672
- const jsonData = {};
673
-
674
- // Get field types from form data attribute
675
- const fieldTypesAttr = form.getAttribute('data-field-types');
676
- const fieldTypes = fieldTypesAttr ? JSON.parse(fieldTypesAttr) : {};
677
-
678
- // Convert FormData to JSON with proper typing
679
- for (const [key, value] of formData.entries()) {
680
- const fieldType = fieldTypes[key] || 'string';
681
- const convertedValue = convertFieldValue(value, fieldType);
682
-
683
- if (jsonData[key]) {
684
- // Handle multiple values (e.g., checkboxes)
685
- if (Array.isArray(jsonData[key])) {
686
- jsonData[key].push(convertedValue);
687
- } else {
688
- jsonData[key] = [jsonData[key], convertedValue];
689
- }
690
- } else {
691
- jsonData[key] = convertedValue;
692
- }
693
- }
694
-
695
- const url = form.getAttribute('data-action') || form.action;
696
- const method = (form.getAttribute('data-method') || form.method || 'POST').toUpperCase();
697
-
698
- showLoading(form);
699
-
700
- fetch(url, {
701
- method,
702
- headers: {
703
- 'Content-Type': 'application/json',
704
- 'Accept': 'application/json'
705
- },
706
- body: JSON.stringify(jsonData)
707
- })
708
- .then(response => {
709
- if (!response.ok) {
710
- throw new Error(\`HTTP \${response.status}: \${response.statusText}\`);
711
- }
712
- return response.json();
713
- })
714
- .then(data => {
715
- handleFormSuccess(data, strategy, options);
716
- })
717
- .catch(error => {
718
- console.error('Form submission failed:', error);
719
- handleFormError({ message: error.message || 'Form submission failed' }, options);
720
- })
721
- .finally(() => {
722
- hideLoading(form);
723
- });
724
- }
725
-
726
- /**
727
- * Standardized error handling for forms
728
- * @param {object} response - The error response
729
- * @param {object} options - Options including target elements
730
- */
731
- function handleFormError(response, options = {}) {
732
- const message = response.message || 'An error occurred';
733
-
734
- if (options.messageId) {
735
- showMessage(options.messageId, message, 'error');
736
- } else {
737
- showToast(message, 'error');
738
- }
739
- }
740
-
741
- /**
742
- * Add client-side validation helpers
743
- * @param {string} formSelector - CSS selector for the form
744
- */
745
- function setupFormValidation(formSelector) {
746
- const form = getElementSafely(formSelector);
747
- if (!form) return;
748
-
749
- // Add basic required field validation
750
- const requiredFields = form.querySelectorAll('[required]');
751
- requiredFields.forEach(field => {
752
- field.addEventListener('blur', function() {
753
- if (!this.value.trim()) {
754
- showMessage(this.id + '-error', 'This field is required', 'error');
755
- } else {
756
- const errorEl = getElementSafely('#' + this.id + '-error');
757
- if (errorEl) errorEl.textContent = '';
758
- }
759
- });
760
- });
761
- }
762
-
763
- // ===== UTILITY FUNCTIONS =====
764
-
765
- /**
766
- * Safe element selection with error handling
767
- * @param {string} selector - CSS selector
768
- * @returns {Element|null}
769
- */
770
- function getElementSafely(selector) {
771
- try {
772
- return document.querySelector(selector);
773
- } catch (e) {
774
- console.warn('Invalid selector:', selector);
775
- return null;
776
- }
777
- }
778
-
779
- /**
780
- * Debounce utility for search/input handlers
781
- * @param {Function} fn - Function to debounce
782
- * @param {number} delay - Delay in milliseconds
783
- * @returns {Function}
784
- */
785
- function debounce(fn, delay = window.AppConfig.debounceDelay) {
786
- let timeoutId;
787
- return function(...args) {
788
- clearTimeout(timeoutId);
789
- timeoutId = setTimeout(() => fn.apply(this, args), delay);
790
- };
791
- }
792
-
793
- /**
794
- * Show loading state for target element
795
- * @param {string} target - CSS selector for target element
796
- */
797
- function showLoading(target) {
798
- const element = getElementSafely(target);
799
- if (!element) return;
800
-
801
- element.style.position = 'relative';
802
- element.style.pointerEvents = 'none';
803
- element.style.opacity = '0.6';
804
-
805
- const loader = document.createElement('div');
806
- loader.className = 'app-loader';
807
- loader.style.cssText = \`
808
- position: absolute;
809
- top: 50%;
810
- left: 50%;
811
- transform: translate(-50%, -50%);
812
- width: 20px;
813
- height: 20px;
814
- border: 2px solid #f3f3f3;
815
- border-top: 2px solid #3498db;
816
- border-radius: 50%;
817
- animation: spin 1s linear infinite;
818
- z-index: 1000;
819
- \`;
820
-
821
- element.appendChild(loader);
822
-
823
- // Add CSS animation if not exists
824
- if (!document.querySelector('#app-loader-styles')) {
825
- const style = document.createElement('style');
826
- style.id = 'app-loader-styles';
827
- style.textContent = \`
828
- @keyframes spin {
829
- 0% { transform: translate(-50%, -50%) rotate(0deg); }
830
- 100% { transform: translate(-50%, -50%) rotate(360deg); }
831
- }
832
- \`;
833
- document.head.appendChild(style);
834
- }
835
- }
836
-
837
- /**
838
- * Hide loading state for target element
839
- * @param {string} target - CSS selector for target element
840
- */
841
- function hideLoading(target) {
842
- const element = getElementSafely(target);
843
- if (!element) return;
844
-
845
- element.style.pointerEvents = '';
846
- element.style.opacity = '';
847
-
848
- const loader = element.querySelector('.app-loader');
849
- if (loader) {
850
- loader.remove();
851
- }
852
- }
853
-
854
- // ===== INITIALIZATION =====
855
-
856
- /**
857
- * Initialize event listeners for links and forms
858
- */
859
- function initializeEventListeners() {
860
- // Handle internal links
861
- document.querySelectorAll('a[href]').forEach(link => {
862
- const href = link.getAttribute('href');
863
-
864
- // Skip external links, anchors, and special protocols
865
- if (!href ||
866
- href.startsWith('http://') ||
867
- href.startsWith('https://') ||
868
- href.startsWith('mailto:') ||
869
- href.startsWith('tel:') ||
870
- href.startsWith('#') ||
871
- href.startsWith('javascript:')) {
872
- return;
873
- }
874
-
875
- // Remove any existing event listeners and add new one
876
- link.removeEventListener('click', handleLinkClick);
877
- link.addEventListener('click', handleLinkClick);
878
- });
879
-
880
- // Handle forms with strategy
881
- document.querySelectorAll('form[data-strategy]').forEach(form => {
882
- // Remove any existing event listeners and add new one
883
- form.removeEventListener('submit', handleFormSubmit);
884
- form.addEventListener('submit', handleFormSubmit);
885
- });
886
- }
887
-
888
- /**
889
- * Handle link click for internal navigation
890
- * @param {Event} event - Click event
891
- */
892
- function handleLinkClick(event) {
893
- event.preventDefault();
894
- const href = event.currentTarget.getAttribute('href');
895
- if (href) {
896
- navigateToPage(href);
897
- }
898
- }
899
-
900
- /**
901
- * Handle form submission
902
- * @param {Event} event - Submit event
903
- */
904
- function handleFormSubmit(event) {
905
- event.preventDefault();
906
- const form = event.target;
907
-
908
- // Check for confirmation message
909
- const confirmMessage = form.getAttribute('data-confirm-message');
910
- if (confirmMessage) {
911
- const confirmed = confirm(t(confirmMessage));
912
- if (!confirmed) {
913
- return; // User cancelled
914
- }
915
- }
916
-
917
- const strategyAttr = form.getAttribute('data-strategy');
918
- const strategy = strategyAttr ? JSON.parse(strategyAttr) : ['back', 'toast'];
919
-
920
- // Extract options from form data attributes
921
- const options = {
922
- basePath: form.getAttribute('data-base-path') || '',
923
- entityName: form.getAttribute('data-entity-name') || 'Item',
924
- messageId: form.getAttribute('data-message-id'),
925
- modalId: form.getAttribute('data-modal-id'),
926
- formSelector: \`form[data-template="\${form.getAttribute('data-template')}"]\`,
927
- targetSelector: form.getAttribute('data-target-selector')
928
- };
929
-
930
- submitForm(form, strategy, options);
931
- }
932
-
933
- // Set up event handlers on page load
934
- document.addEventListener('DOMContentLoaded', function() {
935
- // Initialize event listeners
936
- initializeEventListeners();
937
-
938
- // Handle browser back/forward buttons
939
- window.addEventListener('popstate', function(event) {
940
- // Reload the page content for the current URL
941
- navigateToPage(window.location.pathname);
942
- });
943
-
944
- // Load translations on page load
945
- loadTranslations();
946
- });
947
-
948
- // Expose functions globally
949
- window.App = {
950
- // Translation functions
951
- t,
952
- setLang,
953
- getCurrentLanguage,
954
- loadTranslations,
955
- showToast,
956
- showMessage,
957
- showModal,
958
- navigateBack,
959
- redirectTo,
960
- reloadPage,
961
- refreshSection,
962
- removeElement,
963
- updateContent,
964
- clearForm,
965
- handleFormSuccess,
966
- handleFormError,
967
- setupFormValidation,
968
- getElementSafely,
969
- debounce,
970
- showLoading,
971
- hideLoading,
972
- // New SPA functions
973
- navigateToPage,
974
- submitForm,
975
- initializeEventListeners,
976
- // Type conversion
977
- convertFieldValue
978
- };
979
- `;
980
- exports.translationsTemplate = `{
981
- "ru": {
982
- "error": "ошибка",
983
- "success": "успешно",
984
- "Reloading page...": "Перезагрузка страницы...",
985
- "Request failed. Please try again.": "Запрос не выполнен. Попробуйте еще раз.",
986
- "This field is required": "Это поле обязательно",
987
- "An error occurred": "Произошла ошибка",
988
- "saved successfully": "успешно сохранено",
989
- "updated successfully": "успешно обновлено",
990
- "deleted successfully": "успешно удалено"
991
- },
992
- "pl": {
993
- "error": "błąd",
994
- "success": "sukces",
995
- "Reloading page...": "Przeładowywanie strony...",
996
- "Request failed. Please try again.": "Żądanie nie powiodło się. Spróbuj ponownie.",
997
- "This field is required": "To pole jest wymagane",
998
- "An error occurred": "Wystąpił błąd",
999
- "saved successfully": "zapisano pomyślnie",
1000
- "updated successfully": "zaktualizowano pomyślnie",
1001
- "deleted successfully": "usunięto pomyślnie"
1002
- },
1003
- "es": {
1004
- "error": "error",
1005
- "success": "éxito",
1006
- "Reloading page...": "Recargando página...",
1007
- "Request failed. Please try again.": "La solicitud falló. Por favor, inténtelo de nuevo.",
1008
- "This field is required": "Este campo es obligatorio",
1009
- "An error occurred": "Ha ocurrido un error",
1010
- "saved successfully": "guardado exitosamente",
1011
- "updated successfully": "actualizado exitosamente",
1012
- "deleted successfully": "eliminado exitosamente"
1013
- },
1014
- "de": {
1015
- "error": "Fehler",
1016
- "success": "Erfolg",
1017
- "Reloading page...": "Seite wird neu geladen...",
1018
- "Request failed. Please try again.": "Anfrage fehlgeschlagen. Bitte versuchen Sie es erneut.",
1019
- "This field is required": "Dieses Feld ist erforderlich",
1020
- "An error occurred": "Ein Fehler ist aufgetreten",
1021
- "saved successfully": "erfolgreich gespeichert",
1022
- "updated successfully": "erfolgreich aktualisiert",
1023
- "deleted successfully": "erfolgreich gelöscht"
1024
- },
1025
- "pt": {
1026
- "error": "erro",
1027
- "success": "sucesso",
1028
- "Reloading page...": "Recarregando página...",
1029
- "Request failed. Please try again.": "A solicitação falhou. Por favor, tente novamente.",
1030
- "This field is required": "Este campo é obrigatório",
1031
- "An error occurred": "Ocorreu um erro",
1032
- "saved successfully": "salvo com sucesso",
1033
- "updated successfully": "atualizado com sucesso",
1034
- "deleted successfully": "excluído com sucesso"
1035
- },
1036
- "zh": {
1037
- "error": "错误",
1038
- "success": "成功",
1039
- "Reloading page...": "正在重新加载页面...",
1040
- "Request failed. Please try again.": "请求失败。请再试一次。",
1041
- "This field is required": "此字段为必填项",
1042
- "An error occurred": "发生错误",
1043
- "saved successfully": "保存成功",
1044
- "updated successfully": "更新成功",
1045
- "deleted successfully": "删除成功"
1046
- }
1047
- }`;
1048
- exports.cursorRulesTemplate = `# CurrentJS Framework Rules
1049
-
1050
- ## Architecture Overview
1051
- This is a CurrentJS framework application using clean architecture principles with the following layers:
1052
- - **Controllers**: Handle HTTP requests/responses and route handling
1053
- - **Services**: Contain business logic and orchestrate operations
1054
- - **Stores**: Provide data access layer and database operations
1055
- - **Domain Entities**: Core business models
1056
- - **Views**: HTML templates for server-side rendering
1057
-
1058
- ## Commands
1059
-
1060
- \`\`\`bash
1061
- current create module Modulename # Creates "Modulename" with default structure and yaml file
1062
- \`\`\`
1063
- \`\`\`bash
1064
- current generate Modulename # Generates all TypeScript files based on the module's yaml
1065
- current generate # Does the same as above, but for all modules
1066
- \`\`\`
1067
- \`\`\`bash
1068
- current commit [files...] # Commits all changes in the code, so they won't be overwritten after regeneration
1069
- current diff [module] # Show differences between generated and current code
1070
- \`\`\`
1071
-
1072
- ## The flow
1073
- 1. Create an empty app (\`current create app\`) – this step is already done.
1074
- 2. Create a new module: \`current create module Name\`
1075
- 3. In the module's yaml file, define module's:
1076
- - model(s)
1077
- - routes & actions
1078
- - permissions
1079
- 4. Generate TypeScript files: \`current generate Name\`
1080
- 5. If required, make changes in the:
1081
- - model (i.e. some specific business rules or validations)
1082
- - views
1083
- 6. Commit those changes: \`current commit\`
1084
-
1085
- --- If needed more than CRUD: ---
1086
-
1087
- 7. Define action in the service by creating a method
1088
- 8. Describe this action in the module's yaml. Additionaly, you may define routes and permissions.
1089
- 9. \`current generate Modulename\`
1090
- 10. commit changes: \`current commit\`
1091
-
1092
- ## Configuration Files
1093
-
1094
- ### Application Configuration (app.yaml)
1095
-
1096
- **Do not modify this file**
1097
-
1098
- ### Module Configuration (modulename.yaml)
1099
-
1100
- **The most work must be done in these files**
1101
-
1102
- **Complete Module Example:**
1103
- \`\`\`yaml
1104
- models:
1105
- - name: Post # Entity name (capitalized)
1106
- fields:
1107
- - name: title # Field name
1108
- type: string # Field type: string, number, boolean, datetime
1109
- required: true # Validation requirement
1110
- - name: content
1111
- type: string
1112
- required: true
1113
- - name: authorId
1114
- type: number
1115
- required: true
1116
- - name: publishedAt
1117
- type: datetime
1118
- required: false
1119
- - name: status
1120
- type: string
1121
- required: true
1122
-
1123
- api: # REST API configuration
1124
- prefix: /api/posts # Base URL for API endpoints
1125
- model: Post # Optional: which model this API serves (defaults to first model)
1126
- endpoints:
1127
- - method: GET # HTTP method
1128
- path: / # Relative path (becomes /api/posts/)
1129
- action: list # Action name (references actions section)
1130
- - method: GET
1131
- path: /:id # Path parameter
1132
- action: get
1133
- - method: POST
1134
- path: /
1135
- action: create
1136
- - method: PUT
1137
- path: /:id
1138
- action: update
1139
- - method: DELETE
1140
- path: /:id
1141
- action: delete
1142
- - method: POST # Custom endpoint
1143
- path: /:id/publish
1144
- action: publish
1145
- model: Post # Optional: override model for specific endpoint
1146
-
1147
- routes: # Web interface configuration
1148
- prefix: /posts # Base URL for web pages
1149
- model: Post # Optional: which model this route serves (defaults to first model)
1150
- strategy: [toast, back] # Default success strategies for forms
1151
- endpoints:
1152
- - path: / # List page
1153
- action: list
1154
- view: postList # Template name
1155
- - path: /:id # Detail page
1156
- action: get
1157
- view: postDetail
1158
- - path: /create # Create form page
1159
- action: empty # No data loading action
1160
- view: postCreate
1161
- - path: /:id/edit # Edit form page
1162
- action: get # Load existing data
1163
- view: postUpdate
1164
- model: Post # Optional: override model for specific endpoint
1165
-
1166
- actions: # Business logic mapping
1167
- list:
1168
- handlers: [Post:default:list] # Use built-in list handler
1169
- get:
1170
- handlers: [Post:default:get] # Use built-in get handler
1171
- create:
1172
- handlers: [Post:default:create] # Built-in create
1173
- update:
1174
- handlers: [Post:default:update]
1175
- delete:
1176
- handlers: [ # Chain multiple handlers
1177
- Post:checkCanDelete, # Custom business logic
1178
- Post:default:delete
1179
- ]
1180
- publish: # Custom action
1181
- handlers: [
1182
- Post:default:get, # Fetch entity
1183
- Post:validateForPublish, # Custom validation
1184
- Post:updatePublishStatus # Custom update logic
1185
- ]
1186
-
1187
- permissions: # Role-based access control
1188
- - role: all
1189
- actions: [list, get] # Anyone (including anonymous)
1190
- - role: authenticated
1191
- actions: [create] # Must be logged in
1192
- - role: owner
1193
- actions: [update, publish] # Entity owner permissions
1194
- - role: admin
1195
- actions: [update, delete, publish] # Admin role permissions
1196
- - role: editor
1197
- actions: [publish] # Editor role permissions
1198
- \`\`\`
1199
-
1200
- **Make sure no \`ID\`/\`owner id\`/\`is deleted\` fields are present in the model definition, since it's added automatically**
1201
-
1202
- **Field Types:**
1203
- - \`string\` - Text data
1204
- - \`number\` - Numeric data (integer or float)
1205
- - \`boolean\` - True/false values
1206
- - \`datetime\` - Date and time values
1207
-
1208
- **🔄 Handler vs Action Architecture:**
1209
- - **Handler**: Creates a separate service method (one handler = one service method)
1210
- - **Action**: Virtual controller concept that calls handler methods step-by-step
1211
-
1212
- **Built-in Action Handlers:**
1213
- - \`ModelName:default:list\` - Creates service method with pagination parameters
1214
- - \`ModelName:default:get\` - Creates service method named \`get\` with ID parameter
1215
- - \`ModelName:default:create\` - Creates service method with DTO parameter
1216
- - \`ModelName:default:update\` - Creates service method with ID and DTO parameters
1217
- - \`ModelName:default:delete\` - Creates service method with ID parameter
1218
-
1219
- **Custom Action Handlers:**
1220
- - \`ModelName:customMethodName\` - Creates service method that accepts \`result, context\` parameters
1221
- - \`result\`: Result from previous handler (or \`null\` if it's the first handler)
1222
- - \`context\`: The request context object
1223
- - Each handler generates a separate method in the service
1224
- - User can customize the implementation after generation
1225
-
1226
- **🔗 Multiple Handlers per Action:**
1227
- When an action has multiple handlers, each handler generates a separate service method, and the controller action calls them sequentially. The action returns the result from the last handler.
1228
-
1229
- **Parameter Passing Rules:**
1230
- - **Default handlers** (\`:default:\`): Receive standard parameters (id, pagination, DTO, etc.)
1231
- - **Custom handlers**: Receive \`(result, context)\` where:
1232
- - \`result\`: Result from previous handler, or \`null\` if it's the first handler
1233
- - \`context\`: Request context object
1234
-
1235
- **Handler Format Examples:**
1236
- - \`Post:default:list\` - Creates Post service method \`list(page, limit)\`
1237
- - \`Post:default:get\` - Creates Post service method \`get(id)\`
1238
- - \`Post:validateContent\` - Creates Post service method \`validateContent(result, context)\`
1239
- - \`Comment:notifySubscribers\` - Creates Comment service method \`notifySubscribers(result, context)\`
1240
-
1241
- **Strategy Options (for forms):**
1242
- - \`toast\` - Success toast notification
1243
- - \`back\` - Navigate back in browser history
1244
- - \`message\` - Inline success message
1245
- - \`modal\` - Modal success dialog
1246
- - \`redirect\` - Redirect to specific URL
1247
- - \`refresh\` - Reload current page
1248
-
1249
- **Permission Roles:**
1250
- - \`all\` - Anyone (including anonymous users)
1251
- - \`authenticated\` - Any logged-in user
1252
- - \`owner\` - User who created the entity
1253
- - \`admin\`, \`editor\`, \`user\` - Custom roles from JWT token
1254
- - Multiple roles can be specified for each action
1255
-
1256
- **Generated Files from Configuration:**
1257
- - Domain entity class (one per model)
1258
- - Service class (one per model)
1259
- - API controller with REST endpoints (one per model)
1260
- - Web controller with page rendering (one per model)
1261
- - Store class with database operations (one per model)
1262
- - HTML templates for all views
1263
- - TypeScript interfaces and DTOs
1264
-
1265
- **Multi-Model Support:**
1266
- - Each model gets its own service, controller, and store
1267
- - Use \`model\` parameter in \`api\` and \`routes\` to specify which model to use (defaults to first model)
1268
- - Use \`model\` parameter on individual endpoints to override model for specific endpoints
1269
- - Action handlers use \`modelname:action\` format to specify which model's service method to call
1270
- - Controllers and services are generated per model, not per module
1271
-
1272
- ## Module Structure
1273
- \`\`\`
1274
- src/modules/ModuleName/
1275
- ├── application/
1276
- │ ├── services/ModuleService.ts # Business logic
1277
- │ └── validation/ModuleValidation.ts # DTOs and validation
1278
- ├── domain/
1279
- │ └── entities/Module.ts # Domain model
1280
- ├── infrastructure/
1281
- │ ├── controllers/
1282
- │ │ ├── ModuleApiController.ts # REST API endpoints
1283
- │ │ └── ModuleWebController.ts # Web page controllers
1284
- │ ├── interfaces/StoreInterface.ts # Data access interface
1285
- │ └── stores/ModuleStore.ts # Data access implementation
1286
- ├── views/
1287
- │ ├── modulelist.html # List view template
1288
- │ ├── moduledetail.html # Detail view template
1289
- │ ├── modulecreate.html # Create form template
1290
- │ └── moduleupdate.html # Update form template
1291
- └── module.yaml # Module configuration
1292
- \`\`\`
1293
-
1294
- ## Best Practices
1295
-
1296
- - Use Domain Driven Design and Clean Architecture (kind of).
1297
- - Prefer declarative configuration over imperative programming: when possible, change yamls instead of writing the code. Write the code only when it's really neccessary.
1298
- - CRUD operation are autogenerated (first in module's yaml, then in the generated code).
1299
- - If some custom action is needed, then it has to be defined in the **Service** then just put this action to the module's yaml. You may also define new methods in the *Store* and its interface (if needed).
1300
- - Business rules must be defined only in models. That also applies to business rules validations (in contrast to *just* validations: e.g. if field exists and is of needed type – then validators are in use)
1301
-
1302
- ## Core Package APIs (For Generated Code)
1303
-
1304
- ### @currentjs/router - Controller Decorators & Context
1305
-
1306
- **Decorators (in controllers):**
1307
- \`\`\`typescript
1308
- @Controller('/api/posts') // Base path for controller
1309
- @Get('/') // GET endpoint
1310
- @Get('/:id') // GET with path parameter
1311
- @Post('/') // POST endpoint
1312
- @Put('/:id') // PUT endpoint
1313
- @Delete('/:id') // DELETE endpoint
1314
- @Render('template', 'layout') // For web controllers
1315
- \`\`\`
1316
-
1317
- **Context Object (in route handlers):**
1318
- \`\`\`typescript
1319
- interface IContext {
1320
- request: {
1321
- url: string;
1322
- path: string;
1323
- method: string;
1324
- parameters: Record<string, string | number>; // Path params + query params
1325
- body: any; // Parsed JSON or raw string
1326
- headers: Record<string, string | string[]>;
1327
- user?: AuthenticatedUser; // Parsed JWT user if authenticated
1328
- };
1329
- response: Record<string, any>;
1330
- }
1331
-
1332
- // Usage examples
1333
- const id = parseInt(ctx.request.parameters.id as string);
1334
- const page = parseInt(ctx.request.parameters.page as string) || 1;
1335
- const payload = ctx.request.body;
1336
- const user = ctx.request.user; // If authenticated
1337
- \`\`\`
1338
-
1339
- **Authentication Support:**
1340
- - JWT tokens parsed automatically from \`Authorization: Bearer <token>\` header
1341
- - User object available at \`ctx.request.user\` with \`id\`, \`email\`, \`role\` fields
1342
- - No manual setup required in generated code
1343
-
1344
- ### @currentjs/templating - Template Syntax
1345
-
1346
- **Variables & Data Access:**
1347
- \`\`\`html
1348
- {{ variableName }}
1349
- {{ object.property }}
1350
- {{ $root.arrayData }} <!-- Access root data -->
1351
- {{ $index }} <!-- Loop index -->
1352
- \`\`\`
1353
-
1354
- **Control Structures:**
1355
- \`\`\`html
1356
- <!-- Loops -->
1357
- <tbody x-for="$root" x-row="item">
1358
- <tr>
1359
- <td>{{ item.name }}</td>
1360
- <td>{{ $index }}</td>
1361
- </tr>
1362
- </tbody>
1363
-
1364
- <!-- Conditionals -->
1365
- <div x-if="user.isAdmin">Admin content</div>
1366
- <span x-if="errors.name">{{ errors.name }}</span>
1367
-
1368
- <!-- Template includes -->
1369
- <userCard name="{{ user.name }}" role="admin" />
1370
- \`\`\`
1371
-
1372
- **Layout Integration:**
1373
- - Templates use \`<!-- @template name="templateName" -->\` header
1374
- - Main layout gets \`{{ content }}\` variable
1375
- - Forms use \`{{ formData.field || '' }}\` for default values
1376
-
1377
- ### @currentjs/provider-mysql - Database Operations
1378
-
1379
- **Query Execution (in stores):**
1380
- \`\`\`typescript
1381
- // Named parameters (preferred)
1382
- const result = await this.db.query(
1383
- 'SELECT * FROM users WHERE status = :status AND age > :minAge',
1384
- { status: 'active', minAge: 18 }
1385
- );
1386
-
1387
- // Result handling
1388
- if (result.success && result.data.length > 0) {
1389
- return result.data.map(row => this.rowToModel(row));
1390
- }
1391
- \`\`\`
1392
-
1393
- **Common Query Patterns:**
1394
- \`\`\`typescript
1395
- // Insert with auto-generated fields
1396
- const row = { ...data, created_at: new Date(), updated_at: new Date() };
1397
- const result = await this.db.query(
1398
- \`INSERT INTO table_name (\${fields.join(', ')}) VALUES (\${placeholders})\`,
1399
- row
1400
- );
1401
- const newId = result.insertId;
1402
-
1403
- // Update with validation
1404
- const query = \`UPDATE table_name SET \${updateFields.join(', ')}, updated_at = :updated_at WHERE id = :id\`;
1405
- await this.db.query(query, { ...updateData, updated_at: new Date(), id });
1406
-
1407
- // Soft delete
1408
- await this.db.query(
1409
- 'UPDATE table_name SET deleted_at = :deleted_at WHERE id = :id',
1410
- { deleted_at: new Date(), id }
1411
- );
1412
- \`\`\`
1413
-
1414
- **Error Handling:**
1415
- \`\`\`typescript
1416
- try {
1417
- const result = await this.db.query(query, params);
1418
- } catch (error) {
1419
- if (error instanceof MySQLConnectionError) {
1420
- throw new Error(\`Database connection error: \${error.message}\`);
1421
- } else if (error instanceof MySQLQueryError) {
1422
- throw new Error(\`Query error: \${error.message}\`);
1423
- }
1424
- throw error;
1425
- }
1426
- \`\`\`
1427
-
1428
- ## Frontend System (web/app.js)
1429
-
1430
- ### Translation System
1431
-
1432
- **Basic Usage:**
1433
- \`\`\`javascript
1434
- // Translate strings
1435
- t('Hello World') // Returns translated version or original
1436
- t('Save changes')
1437
-
1438
- // Set language
1439
- setLang('pl') // Switch to Polish
1440
- setLang('en') // Switch to English
1441
- getCurrentLanguage() // Get current language code
1442
- \`\`\`
1443
-
1444
- **Translation File (web/translations.json):**
1445
- \`\`\`json
1446
- {
1447
- "pl": {
1448
- "Hello World": "Witaj Świecie",
1449
- "Save changes": "Zapisz zmiany",
1450
- "Delete": "Usuń"
1451
- },
1452
- "ru": {
1453
- "Hello World": "Привет мир",
1454
- "Save changes": "Сохранить изменения"
1455
- }
1456
- }
1457
- \`\`\`
1458
-
1459
- ### UI Feedback & Notifications
1460
-
1461
- **Toast Notifications:**
1462
- \`\`\`javascript
1463
- showToast('Success message', 'success') // Green toast
1464
- showToast('Error occurred', 'error') // Red toast
1465
- showToast('Information', 'info') // Blue toast
1466
- showToast('Warning', 'warning') // Yellow toast
1467
- \`\`\`
1468
-
1469
- **Inline Messages:**
1470
- \`\`\`javascript
1471
- showMessage('messageId', 'Success!', 'success')
1472
- showMessage('errorContainer', 'Validation failed', 'error')
1473
- \`\`\`
1474
-
1475
- **Modal Dialogs:**
1476
- \`\`\`javascript
1477
- showModal('confirmModal', 'Item saved successfully', 'success')
1478
- showModal('errorModal', 'Operation failed', 'error')
1479
- \`\`\`
1480
-
1481
- ### Navigation & Page Actions
1482
-
1483
- **Navigation Functions:**
1484
- \`\`\`javascript
1485
- navigateBack() // Go back in history or to home
1486
- redirectTo('/posts') // Safe redirect with validation
1487
- reloadPage() // Reload with loading indicator
1488
- refreshSection('#content') // Refresh specific section
1489
-
1490
- // SPA-style navigation
1491
- navigateToPage('/posts/123') // Loads via AJAX, updates #main
1492
- \`\`\`
1493
-
1494
- **Content Management:**
1495
- \`\`\`javascript
1496
- updateContent('#results', newHtml, 'replace') // Replace content
1497
- updateContent('#list', itemHtml, 'append') // Add to end
1498
- updateContent('#list', itemHtml, 'prepend') // Add to beginning
1499
- removeElement('#item-123') // Animate and remove
1500
- \`\`\`
1501
-
1502
- ### Form Handling & Strategy System
1503
-
1504
- **Form Strategy Configuration:**
1505
- \`\`\`html
1506
- <!-- Form with strategy attributes -->
1507
- <form data-strategy='["toast", "back"]'
1508
- data-entity-name="Post"
1509
- data-field-types='{"age": "number", "active": "boolean"}'>
1510
- <input name="title" type="text" required>
1511
- <input name="age" type="number">
1512
- <input name="active" type="checkbox">
1513
- <button type="submit">Save</button>
1514
- </form>
1515
- \`\`\`
1516
-
1517
- **Available Strategies:**
1518
- - \`toast\` - Show success toast notification
1519
- - \`back\` - Navigate back using browser history
1520
- - \`message\` - Show inline message in specific element
1521
- - \`modal\` - Show modal dialog
1522
- - \`redirect\` - Redirect to specific URL
1523
- - \`refresh\` - Reload the page
1524
- - \`remove\` - Remove form element
1525
-
1526
- **Manual Form Submission:**
1527
- \`\`\`javascript
1528
- const form = document.querySelector('#myForm');
1529
- submitForm(form, ['toast', 'back'], {
1530
- entityName: 'Post',
1531
- basePath: '/posts',
1532
- messageId: 'form-message'
1533
- });
1534
- \`\`\`
1535
-
1536
- **Success Handling:**
1537
- \`\`\`javascript
1538
- handleFormSuccess(response, ['toast', 'back'], {
1539
- entityName: 'Post',
1540
- basePath: '/posts',
1541
- messageId: 'success-msg',
1542
- modalId: 'success-modal'
1543
- });
1544
- \`\`\`
1545
-
1546
- ### Form Validation & Type Conversion
1547
-
1548
- **Client-side Validation Setup:**
1549
- \`\`\`javascript
1550
- setupFormValidation('#createForm'); // Adds required field validation
1551
- \`\`\`
1552
-
1553
- **Field Type Conversion:**
1554
- \`\`\`javascript
1555
- // Automatic conversion based on data-field-types
1556
- convertFieldValue('123', 'number') // Returns 123 (number)
1557
- convertFieldValue('true', 'boolean') // Returns true (boolean)
1558
- convertFieldValue('text', 'string') // Returns 'text' (string)
1559
- \`\`\`
1560
-
1561
- ### Loading States & Utilities
1562
-
1563
- **Loading Indicators:**
1564
- \`\`\`javascript
1565
- showLoading('#form') // Show spinner on form
1566
- hideLoading('#form') // Hide spinner
1567
- showLoading('#main') // Show spinner on main content
1568
- \`\`\`
1569
-
1570
- **Utility Functions:**
1571
- \`\`\`javascript
1572
- debounce(searchFunction, 300) // Debounce for search inputs
1573
- getElementSafely('#selector') // Safe element selection
1574
- clearForm('#myForm') // Reset form and clear validation
1575
- \`\`\`
1576
-
1577
- ### Event Handling & SPA Integration
1578
-
1579
- **Automatic Link Handling:**
1580
- - Internal links automatically use AJAX navigation
1581
- - External links work normally
1582
- - Links with \`data-external\` skip AJAX handling
1583
-
1584
- **Automatic Form Handling:**
1585
- - Forms with \`data-strategy\` use AJAX submission
1586
- - Regular forms work normally
1587
- - Automatic JSON conversion from FormData
1588
-
1589
- **Custom Event Listeners:**
1590
- \`\`\`javascript
1591
- // Re-initialize after dynamic content loading
1592
- initializeEventListeners();
1593
-
1594
- // Handle specific link navigation
1595
- document.querySelector('#myLink').addEventListener('click', (e) => {
1596
- e.preventDefault();
1597
- navigateToPage('/custom/path');
1598
- });
1599
- \`\`\`
1600
-
1601
- ### Global App Object
1602
-
1603
- **Accessing Functions:**
1604
- \`\`\`javascript
1605
- // All functions available under window.App
1606
- App.showToast('Message', 'success');
1607
- App.navigateBack();
1608
- App.t('Translate this');
1609
- App.setLang('pl');
1610
- App.showLoading('#content');
1611
- \`\`\`
1612
-
1613
- ### Template Data Binding
1614
- \`\`\`html
1615
- <!-- List with pagination -->
1616
- <tbody x-for="modules" x-row="module">
1617
- <tr>
1618
- <td>{{ module.name }}</td>
1619
- <td><a href="/module/{{ module.id }}">View</a></td>
1620
- </tr>
1621
- </tbody>
1622
-
1623
- <!-- Form with validation errors -->
1624
- <div x-if="errors.name" class="text-danger">{{ errors.name }}</div>
1625
- <input type="text" name="name" value="{{ formData.name || '' }}" class="form-control">
1626
-
1627
- <!-- Form with strategy attributes -->
1628
- <form data-strategy='["toast", "back"]'
1629
- data-entity-name="Module"
1630
- data-field-types='{"count": "number", "active": "boolean"}'>
1631
- <!-- form fields -->
1632
- </form>
1633
- \`\`\`
1634
- `;
65
+ exports.tsconfigTemplate = fs.readFileSync(path.join(__dirname, 'data', 'tsConfigTemplate'), 'utf8');
66
+ exports.appYamlTemplate = fs.readFileSync(path.join(__dirname, 'data', 'appYamlTemplate'), 'utf8');
67
+ exports.appTsTemplate = fs.readFileSync(path.join(__dirname, 'data', 'appTsTemplate'), 'utf8');
68
+ exports.mainViewTemplate = fs.readFileSync(path.join(__dirname, 'data', 'mainViewTemplate'), 'utf8');
69
+ exports.errorTemplate = fs.readFileSync(path.join(__dirname, 'data', 'errorTemplate'), 'utf8');
70
+ exports.frontendScriptTemplate = fs.readFileSync(path.join(__dirname, 'data', 'frontendScriptTemplate'), 'utf8');
71
+ exports.translationsTemplate = fs.readFileSync(path.join(__dirname, 'data', 'translationsTemplate'), 'utf8');
72
+ exports.cursorRulesTemplate = fs.readFileSync(path.join(__dirname, 'data', 'cursorRulesTemplate'), 'utf8');
1635
73
  // Directory structure constants
1636
74
  exports.DEFAULT_DIRECTORIES = {
1637
75
  SRC: 'src',
1638
76
  DIST: 'build',
1639
77
  WEB: 'web',
1640
78
  TEMPLATES: path.join('src', 'common', 'ui', 'templates'),
1641
- SERVICES: path.join('src', 'common', 'services')
79
+ SERVICES: path.join('src', 'common', 'services'),
80
+ MIGRATIONS: 'migrations'
1642
81
  };
1643
82
  // File names constants
1644
83
  exports.DEFAULT_FILES = {