@reldens/cms 0.26.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/installer.js CHANGED
@@ -1,497 +1,500 @@
1
- /**
2
- *
3
- * Reldens - CMS - Installer
4
- *
5
- */
6
-
7
- const { FileHandler, Encryptor } = require('@reldens/server-utils');
8
- const { DriversMap, DriversClassMap, EntitiesGenerator, PrismaSchemaGenerator } = require('@reldens/storage');
9
- const { EntitiesLoader } = require('./entities-loader');
10
- const { Logger, sc } = require('@reldens/utils');
11
-
12
- class Installer
13
- {
14
-
15
- constructor(props)
16
- {
17
- this.app = sc.get(props, 'app', false);
18
- this.appServer = sc.get(props, 'appServer', false);
19
- this.appServerFactory = sc.get(props, 'appServerFactory', false);
20
- this.renderEngine = sc.get(props, 'renderEngine', false);
21
- this.projectRoot = sc.get(props, 'projectRoot', './');
22
- this.projectTemplatesPath = FileHandler.joinPaths(this.projectRoot, 'templates');
23
- this.projectPublicPath = FileHandler.joinPaths(this.projectRoot, 'public');
24
- this.projectPublicAssetsPath = FileHandler.joinPaths(this.projectPublicPath, 'assets');
25
- this.projectCssPath = FileHandler.joinPaths(this.projectPublicPath, 'css');
26
- this.projectJsPath = FileHandler.joinPaths(this.projectPublicPath, 'js');
27
- this.installLockPath = FileHandler.joinPaths(this.projectRoot, 'install.lock');
28
- this.envFilePath = FileHandler.joinPaths(this.projectRoot, '.env');
29
- this.modulePath = FileHandler.joinPaths(__dirname, '..');
30
- this.installerPath = FileHandler.joinPaths(this.modulePath, 'install');
31
- this.migrationsPath = FileHandler.joinPaths(this.modulePath, 'migrations');
32
- this.defaultTemplatesPath = FileHandler.joinPaths(this.modulePath, 'templates');
33
- this.moduleAdminPath = FileHandler.joinPaths(this.modulePath, 'admin');
34
- this.moduleAdminAssetsPath = FileHandler.joinPaths(this.moduleAdminPath, 'assets');
35
- this.moduleAdminTemplatesPath = FileHandler.joinPaths(this.moduleAdminPath, 'templates')
36
- this.indexTemplatePath = FileHandler.joinPaths(this.defaultTemplatesPath, 'index.js.dist');
37
- this.postInstallCallback = sc.get(props, 'postInstallCallback', false);
38
- this.prismaClient = sc.get(props, 'prismaClient', false);
39
- this.entitiesLoader = new EntitiesLoader({projectRoot: this.projectRoot});
40
- }
41
-
42
- isInstalled()
43
- {
44
- return FileHandler.exists(this.installLockPath);
45
- }
46
-
47
- async prepareSetup(app, appServer, appServerFactory, renderEngine)
48
- {
49
- if(!app){
50
- Logger.error('Missing app on prepareSetup for Installer.');
51
- return false;
52
- }
53
- if(!appServer){
54
- Logger.error('Missing appServer on prepareSetup for Installer.');
55
- return false;
56
- }
57
- if(!appServerFactory){
58
- Logger.error('Missing appServerFactory on prepareSetup for Installer.');
59
- return false;
60
- }
61
- if(!renderEngine){
62
- Logger.error('Missing renderEngine on prepareSetup for Installer.');
63
- return false;
64
- }
65
- this.app = app;
66
- this.appServer = appServer;
67
- this.appServerFactory = appServerFactory;
68
- this.renderEngine = renderEngine;
69
- app.use('/install-assets', appServerFactory.applicationFramework.static(this.installerPath, {index: false}));
70
- app.use(appServerFactory.session({
71
- secret: Encryptor.generateSecretKey(),
72
- resave: true,
73
- saveUninitialized: true
74
- }));
75
- app.use(async (req, res, next) => {
76
- return await this.executeForEveryRequest(req, res, next);
77
- });
78
- app.post('/install', async (req, res) => {
79
- return await this.executeInstallProcess(req, res);
80
- });
81
- return true;
82
- }
83
-
84
- async executeForEveryRequest(req, res, next)
85
- {
86
- if(this.isInstalled()){
87
- return next();
88
- }
89
- let urlPath = req._parsedUrl.pathname;
90
- if('' === urlPath || '/' === urlPath){
91
- let installerIndexPath = FileHandler.joinPaths(this.installerPath, 'index.html');
92
- if(!FileHandler.exists(installerIndexPath)){
93
- return res.status(500).send('Installer template not found.');
94
- }
95
- let content = FileHandler.readFile(installerIndexPath);
96
- let contentParams = req.session?.templateVariables || this.fetchDefaults();
97
- let errorParam = req.query?.error;
98
- if(errorParam){
99
- contentParams.errorMessage = this.getErrorMessage(errorParam);
100
- }
101
- return res.send(this.renderEngine.render(content, contentParams));
102
- }
103
- if('/install' !== urlPath){
104
- return res.redirect('/');
105
- }
106
- next();
107
- }
108
-
109
- getErrorMessage(errorCode)
110
- {
111
- let errorMessages = {
112
- 'invalid-driver': 'Invalid storage driver selected.',
113
- 'connection-failed': 'Database connection failed. Please check your credentials.',
114
- 'raw-query-not-found': 'Query method not found in driver.',
115
- 'sql-file-not-found': 'SQL installation file not found.',
116
- 'sql-cms-tables-creation-failed': 'Failed to create CMS tables.',
117
- 'sql-user-auth-creation-failed': 'Failed to create user authentication tables.',
118
- 'sql-default-user-error': 'Failed to create default user.',
119
- 'sql-default-homepage-error': 'Failed to create default homepage.',
120
- 'installation-entities-generation-failed': 'Failed to generate entities.',
121
- 'installation-entities-callback-failed': 'Failed to process entities for callback.',
122
- 'configuration-error': 'Configuration error while completing installation.',
123
- 'already-installed': 'The application is already installed.'
124
- };
125
- return sc.get(errorMessages, errorCode, 'An unknown error occurred during installation.');
126
- }
127
-
128
- async executeInstallProcess(req, res)
129
- {
130
- if(this.isInstalled()){
131
- return res.redirect('/?redirect=already-installed');
132
- }
133
- let templateVariables = req.body;
134
- req.session.templateVariables = templateVariables;
135
- let selectedDriver = templateVariables['db-storage-driver'];
136
- let driverClass = DriversMap[selectedDriver];
137
- if(!driverClass){
138
- Logger.error('Invalid storage driver: ' + selectedDriver);
139
- return res.redirect('/?error=invalid-driver');
140
- }
141
- let dbConfig = {
142
- client: templateVariables['db-client'],
143
- config: {
144
- host: templateVariables['db-host'],
145
- port: Number(templateVariables['db-port']),
146
- database: templateVariables['db-name'],
147
- user: templateVariables['db-username'],
148
- password: templateVariables['db-password'],
149
- multipleStatements: true
150
- },
151
- debug: false
152
- };
153
- if('prisma' === selectedDriver && this.prismaClient){
154
- dbConfig.prismaClient = this.prismaClient;
155
- }
156
- let dbDriver = new driverClass(dbConfig);
157
- if(!await dbDriver.connect()){
158
- Logger.error('Connection failed');
159
- return res.redirect('/?error=connection-failed');
160
- }
161
- if(!sc.isObjectFunction(dbDriver, 'rawQuery')){
162
- Logger.error('Method "rawQuery" not found.');
163
- return res.redirect('/?error=raw-query-not-found');
164
- }
165
- let executeFiles = {
166
- 'install-cms-tables': 'install.sql',
167
- 'install-user-auth': 'users-authentication.sql',
168
- 'install-default-user': 'default-user.sql',
169
- 'install-default-homepage': 'default-homepage.sql',
170
- 'install-default-blocks': 'default-blocks.sql',
171
- 'install-entity-access': 'default-entity-access.sql',
172
- 'install-dynamic-forms': 'default-forms.sql'
173
- };
174
- for(let checkboxName of Object.keys(executeFiles)){
175
- let fileName = executeFiles[checkboxName];
176
- let redirectError = await this.executeQueryFile(
177
- sc.get(templateVariables, checkboxName, 'off'),
178
- fileName,
179
- dbDriver
180
- );
181
- if('' !== redirectError){
182
- return res.redirect(redirectError);
183
- }
184
- }
185
- let entitiesGenerationResult = await this.generateEntities(dbDriver, false, true);
186
- if(!entitiesGenerationResult){
187
- Logger.error('Entities generation error.');
188
- return res.redirect('/?error=installation-entities-generation-failed');
189
- }
190
- Logger.info('Generated entities.');
191
- try {
192
- let mappedVariablesForConfig = this.mapVariablesForConfig(templateVariables);
193
- await this.createEnvFile(this.mapVariablesForTemplate(mappedVariablesForConfig));
194
- await this.prepareProjectDirectories();
195
- await this.copyAdminDirectory();
196
- await this.createIndexJsFile(templateVariables);
197
- if(sc.isFunction(this.postInstallCallback)){
198
- if(this.appServer && sc.isFunction(this.appServer.close)){
199
- await this.appServer.close();
200
- }
201
- Logger.debug('Running postInstallCallback.');
202
- let callbackResult = await this.postInstallCallback({
203
- loadedEntities: this.entitiesLoader.loadEntities(selectedDriver),
204
- mappedVariablesForConfig
205
- });
206
- if(false === callbackResult){
207
- Logger.error('Post-install callback failed.');
208
- return res.redirect('/?error=installation-entities-callback-failed');
209
- }
210
- }
211
- await this.createLockFile();
212
- Logger.info('Installation successful!');
213
- let successContent = 'Installation successful! Run "node ." to start your CMS.';
214
- let successFileContent = FileHandler.readFile(FileHandler.joinPaths(this.installerPath, 'success.html'));
215
- if(successFileContent){
216
- successContent = this.renderEngine.render(
217
- successFileContent,
218
- {adminPath: templateVariables['app-admin-path']},
219
- );
220
- }
221
- return res.send(successContent);
222
- } catch (error) {
223
- Logger.critical('Configuration error: '+error.message);
224
- return res.redirect('/?error=installation-error');
225
- }
226
- }
227
-
228
- async executeQueryFile(isMarked, fileName, dbDriver)
229
- {
230
- if('on' !== isMarked){
231
- return '';
232
- }
233
- let sqlFileContent = FileHandler.readFile(FileHandler.joinPaths(this.migrationsPath, fileName));
234
- if(!sqlFileContent){
235
- Logger.error('SQL file "'+fileName+'" not found.');
236
- return '/?error=sql-file-not-found&file-name='+fileName;
237
- }
238
- let queryExecutionResult = await dbDriver.rawQuery(sqlFileContent);
239
- if(!queryExecutionResult){
240
- Logger.error('SQL file "'+fileName+'" raw execution failed.');
241
- return '/?error=sql-file-execution-error&file-name='+fileName;
242
- }
243
- Logger.info('SQL file "'+fileName+'" raw execution successfully.');
244
- return '';
245
- }
246
-
247
- async generateEntities(server, isOverride = false, isInstallationMode = false, isDryPrisma = false)
248
- {
249
- let driverType = sc.get(DriversClassMap, server.constructor.name, '');
250
- Logger.debug('Driver type detected: '+driverType+', Server constructor: '+server.constructor.name);
251
- if('prisma' === driverType && !isInstallationMode && !isDryPrisma){
252
- Logger.info('Running prisma introspect "npx prisma db pull"...');
253
- let dbConfig = this.extractDbConfigFromServer(server);
254
- Logger.debug('Extracted DB config:', dbConfig);
255
- if(dbConfig){
256
- let generatedPrismaSchema = await this.generatePrismaSchema(dbConfig);
257
- if(!generatedPrismaSchema){
258
- Logger.error('Prisma schema generation failed.');
259
- return false;
260
- }
261
- Logger.info('Generated Prisma schema for entities generation.');
262
- }
263
- }
264
- if('prisma' === driverType && isDryPrisma){
265
- Logger.info('Skipping Prisma schema generation due to --dry-prisma flag.');
266
- }
267
- let generatorConfig = {
268
- server,
269
- projectPath: this.projectRoot,
270
- isOverride
271
- };
272
- if('prisma' === driverType && this.prismaClient){
273
- generatorConfig.prismaClient = this.prismaClient;
274
- }
275
- let generator = new EntitiesGenerator(generatorConfig);
276
- let success = await generator.generate();
277
- if(!success){
278
- Logger.error('Entities generation failed.');
279
- return false;
280
- }
281
- return true;
282
- }
283
-
284
- extractDbConfigFromServer(server)
285
- {
286
- let config = sc.get(server, 'config');
287
- if(!config){
288
- Logger.warning('Could not extract database config from server.');
289
- return false;
290
- }
291
- let dbConfig = {
292
- client: sc.get(server, 'client', 'mysql'),
293
- config: {
294
- host: sc.get(config, 'host', 'localhost'),
295
- port: sc.get(config, 'port', 3306),
296
- database: sc.get(config, 'database', ''),
297
- user: sc.get(config, 'user', ''),
298
- password: sc.get(config, 'password', ''),
299
- multipleStatements: true
300
- },
301
- debug: false
302
- };
303
- Logger.debug('Extracted DB config structure:', {
304
- client: dbConfig.client,
305
- host: dbConfig.config.host,
306
- database: dbConfig.config.database
307
- });
308
- return dbConfig;
309
- }
310
-
311
- async generatePrismaSchema(connectionData, useDataProxy = false)
312
- {
313
- if(!connectionData){
314
- Logger.error('Missing "connectionData" to generate Prisma Schema.');
315
- return false;
316
- }
317
- let generator = new PrismaSchemaGenerator({
318
- ...connectionData,
319
- dataProxy: useDataProxy,
320
- clientOutputPath: FileHandler.joinPaths(this.projectRoot, 'prisma', 'client'),
321
- prismaSchemaPath: FileHandler.joinPaths(this.projectRoot, 'prisma')
322
- });
323
- let success = await generator.generate();
324
- if(!success){
325
- Logger.error('Prisma schema generation failed.');
326
- return false;
327
- }
328
- return true;
329
- }
330
-
331
- async createEnvFile(templateVariables)
332
- {
333
- let envTemplatePath = FileHandler.joinPaths(this.defaultTemplatesPath, '.env.dist');
334
- let envTemplateContent = FileHandler.readFile(envTemplatePath);
335
- if(!envTemplateContent){
336
- Logger.error('Template ".env.dist" not found: '+envTemplatePath);
337
- return false;
338
- }
339
- return FileHandler.writeFile(this.envFilePath, this.renderEngine.render(envTemplateContent, templateVariables));
340
- }
341
-
342
- mapVariablesForTemplate(configVariables)
343
- {
344
- return {
345
- host: configVariables.host,
346
- port: configVariables.port,
347
- adminPath: configVariables.adminPath,
348
- adminSecret: configVariables.adminSecret,
349
- dbClient: configVariables.database.client,
350
- dbHost: configVariables.database.host,
351
- dbPort: configVariables.database.port,
352
- dbName: configVariables.database.name,
353
- dbUser: configVariables.database.user,
354
- dbPassword: configVariables.database.password,
355
- dbDriver: configVariables.database.driver
356
- };
357
- }
358
-
359
- mapVariablesForConfig(templateVariables)
360
- {
361
- return {
362
- host: sc.get(templateVariables, 'app-host', 'http://localhost'),
363
- port: Number(sc.get(templateVariables, 'app-port', 8080)),
364
- adminPath: sc.get(templateVariables, 'app-admin-path', '/reldens-admin'),
365
- adminSecret: sc.get(templateVariables, 'app-admin-secret', Encryptor.generateSecretKey()),
366
- database: {
367
- client: sc.get(templateVariables, 'db-client', 'mysql'),
368
- host: sc.get(templateVariables, 'db-host', 'localhost'),
369
- port: Number(sc.get(templateVariables, 'db-port', 3306)),
370
- name: sc.get(templateVariables, 'db-name', 'reldens_cms'),
371
- user: sc.get(templateVariables, 'db-username', ''),
372
- password: sc.get(templateVariables, 'db-password', ''),
373
- driver: sc.get(templateVariables, 'db-storage-driver', 'prisma')
374
- }
375
- };
376
- }
377
-
378
- async createIndexJsFile(templateVariables)
379
- {
380
- if(!FileHandler.exists(this.indexTemplatePath)){
381
- Logger.error('Index.js template not found: ' + this.indexTemplatePath);
382
- return false;
383
- }
384
- let indexTemplate = FileHandler.readFile(this.indexTemplatePath);
385
- let driverKey = templateVariables['db-storage-driver'];
386
- let templateParams = {driverKey};
387
- if('prisma' === driverKey){
388
- let prismaClientPath = FileHandler.joinPaths(this.projectRoot, 'prisma', 'client');
389
- templateParams.prismaClientImports = 'const { PrismaClient } = require(\'' + prismaClientPath + '\');';
390
- templateParams.prismaClientParam = ',\n prismaClient: new PrismaClient()';
391
- }
392
- if('prisma' !== driverKey){
393
- templateParams.prismaClientImports = '';
394
- templateParams.prismaClientParam = '';
395
- }
396
- let indexContent = this.renderEngine.render(indexTemplate, templateParams);
397
- let indexFilePath = FileHandler.joinPaths(this.projectRoot, 'index.js');
398
- if(FileHandler.exists(indexFilePath)){
399
- Logger.info('Index.js file already exists, the CMS installer will not override the existent one.');
400
- return true;
401
- }
402
- return FileHandler.writeFile(indexFilePath, indexContent);
403
- }
404
-
405
- async createLockFile()
406
- {
407
- return FileHandler.writeFile(this.installLockPath, 'Installation completed on '+new Date().toISOString());
408
- }
409
-
410
- async copyAdminDirectory()
411
- {
412
- let projectAdminPath = FileHandler.joinPaths(this.projectRoot, 'admin');
413
- if(FileHandler.exists(projectAdminPath)){
414
- Logger.info('Admin folder already exists in project root.');
415
- return true;
416
- }
417
- if(!FileHandler.exists(this.moduleAdminPath)){
418
- Logger.error('Admin folder not found in module path: '+this.moduleAdminPath);
419
- return false;
420
- }
421
- let projectAdminTemplates = FileHandler.joinPaths(projectAdminPath, 'templates');
422
- FileHandler.copyFolderSync(this.moduleAdminTemplatesPath, projectAdminTemplates);
423
- FileHandler.copyFolderSync(this.moduleAdminAssetsPath, this.projectPublicAssetsPath);
424
- FileHandler.copyFile(
425
- FileHandler.joinPaths(this.moduleAdminPath, 'reldens-admin-client.css'),
426
- FileHandler.joinPaths(this.projectCssPath, 'reldens-admin-client.css'),
427
- );
428
- FileHandler.copyFile(
429
- FileHandler.joinPaths(this.moduleAdminPath, 'reldens-admin-client.js'),
430
- FileHandler.joinPaths(this.projectJsPath, 'reldens-admin-client.js'),
431
- );
432
- Logger.info('Admin folder copied to project root.');
433
- return true;
434
- }
435
-
436
- async prepareProjectDirectories()
437
- {
438
- FileHandler.createFolder(this.projectTemplatesPath);
439
- FileHandler.createFolder(FileHandler.joinPaths(this.projectTemplatesPath, 'layouts'));
440
- FileHandler.createFolder(this.projectPublicPath);
441
- FileHandler.createFolder(this.projectPublicAssetsPath);
442
- FileHandler.createFolder(this.projectCssPath);
443
- FileHandler.createFolder(this.projectJsPath);
444
- let baseFiles = [
445
- 'page.html',
446
- '404.html',
447
- 'browserconfig.xml',
448
- 'favicon.ico',
449
- 'site.webmanifest'
450
- ];
451
- for(let fileName of baseFiles){
452
- FileHandler.copyFile(
453
- FileHandler.joinPaths(this.defaultTemplatesPath, fileName),
454
- FileHandler.joinPaths(this.projectTemplatesPath, fileName)
455
- );
456
- }
457
- FileHandler.copyFile(
458
- FileHandler.joinPaths(this.defaultTemplatesPath, 'layouts', 'default.html'),
459
- FileHandler.joinPaths(this.projectTemplatesPath, 'layouts', 'default.html')
460
- );
461
- FileHandler.copyFile(
462
- FileHandler.joinPaths(this.defaultTemplatesPath, 'css', 'styles.css'),
463
- FileHandler.joinPaths(this.projectCssPath, 'styles.css')
464
- );
465
- FileHandler.copyFile(
466
- FileHandler.joinPaths(this.defaultTemplatesPath, 'js', 'scripts.js'),
467
- FileHandler.joinPaths(this.projectJsPath, 'scripts.js')
468
- );
469
- FileHandler.copyFolderSync(
470
- FileHandler.joinPaths(this.defaultTemplatesPath, 'partials'),
471
- FileHandler.joinPaths(this.projectTemplatesPath, 'partials')
472
- );
473
- FileHandler.copyFolderSync(
474
- FileHandler.joinPaths(this.defaultTemplatesPath, 'domains'),
475
- FileHandler.joinPaths(this.projectTemplatesPath, 'domains')
476
- );
477
- return true;
478
- }
479
-
480
- fetchDefaults()
481
- {
482
- return {
483
- 'app-host': process.env.RELDENS_APP_HOST || 'http://localhost',
484
- 'app-port': process.env.RELDENS_APP_PORT || '8080',
485
- 'app-admin-path': process.env.RELDENS_ADMIN_ROUTE_PATH || '/reldens-admin',
486
- 'db-storage-driver': process.env.RELDENS_STORAGE_DRIVER || 'prisma',
487
- 'db-client': process.env.RELDENS_DB_CLIENT || 'mysql',
488
- 'db-host': process.env.RELDENS_DB_HOST || 'localhost',
489
- 'db-port': process.env.RELDENS_DB_PORT || '3306',
490
- 'db-name': process.env.RELDENS_DB_NAME || 'reldens_cms',
491
- 'db-username': process.env.RELDENS_DB_USER || '',
492
- 'db-password': process.env.RELDENS_DB_PASSWORD || ''
493
- };
494
- }
495
- }
496
-
497
- module.exports.Installer = Installer;
1
+ /**
2
+ *
3
+ * Reldens - CMS - Installer
4
+ *
5
+ */
6
+
7
+ const { FileHandler, Encryptor } = require('@reldens/server-utils');
8
+ const { DriversMap, DriversClassMap, EntitiesGenerator, PrismaSchemaGenerator } = require('@reldens/storage');
9
+ const { EntitiesLoader } = require('./entities-loader');
10
+ const { Logger, sc } = require('@reldens/utils');
11
+
12
+ class Installer
13
+ {
14
+
15
+ constructor(props)
16
+ {
17
+ this.app = sc.get(props, 'app', false);
18
+ this.appServer = sc.get(props, 'appServer', false);
19
+ this.appServerFactory = sc.get(props, 'appServerFactory', false);
20
+ this.renderEngine = sc.get(props, 'renderEngine', false);
21
+ this.projectRoot = sc.get(props, 'projectRoot', './');
22
+ this.projectTemplatesPath = FileHandler.joinPaths(this.projectRoot, 'templates');
23
+ this.projectPublicPath = FileHandler.joinPaths(this.projectRoot, 'public');
24
+ this.projectPublicAssetsPath = FileHandler.joinPaths(this.projectPublicPath, 'assets');
25
+ this.projectCssPath = FileHandler.joinPaths(this.projectPublicPath, 'css');
26
+ this.projectJsPath = FileHandler.joinPaths(this.projectPublicPath, 'js');
27
+ this.installLockPath = FileHandler.joinPaths(this.projectRoot, 'install.lock');
28
+ this.envFilePath = FileHandler.joinPaths(this.projectRoot, '.env');
29
+ this.modulePath = FileHandler.joinPaths(__dirname, '..');
30
+ this.installerPath = FileHandler.joinPaths(this.modulePath, 'install');
31
+ this.migrationsPath = FileHandler.joinPaths(this.modulePath, 'migrations');
32
+ this.defaultTemplatesPath = FileHandler.joinPaths(this.modulePath, 'templates');
33
+ this.moduleAdminPath = FileHandler.joinPaths(this.modulePath, 'admin');
34
+ this.moduleAdminAssetsPath = FileHandler.joinPaths(this.moduleAdminPath, 'assets');
35
+ this.moduleAdminTemplatesPath = FileHandler.joinPaths(this.moduleAdminPath, 'templates')
36
+ this.indexTemplatePath = FileHandler.joinPaths(this.defaultTemplatesPath, 'index.js.dist');
37
+ this.postInstallCallback = sc.get(props, 'postInstallCallback', false);
38
+ this.prismaClient = sc.get(props, 'prismaClient', false);
39
+ this.entitiesLoader = new EntitiesLoader({projectRoot: this.projectRoot});
40
+ }
41
+
42
+ isInstalled()
43
+ {
44
+ return FileHandler.exists(this.installLockPath);
45
+ }
46
+
47
+ async prepareSetup(app, appServer, appServerFactory, renderEngine)
48
+ {
49
+ if(!app){
50
+ Logger.error('Missing app on prepareSetup for Installer.');
51
+ return false;
52
+ }
53
+ if(!appServer){
54
+ Logger.error('Missing appServer on prepareSetup for Installer.');
55
+ return false;
56
+ }
57
+ if(!appServerFactory){
58
+ Logger.error('Missing appServerFactory on prepareSetup for Installer.');
59
+ return false;
60
+ }
61
+ if(!renderEngine){
62
+ Logger.error('Missing renderEngine on prepareSetup for Installer.');
63
+ return false;
64
+ }
65
+ this.app = app;
66
+ this.appServer = appServer;
67
+ this.appServerFactory = appServerFactory;
68
+ this.renderEngine = renderEngine;
69
+ app.use('/install-assets', appServerFactory.applicationFramework.static(this.installerPath, {index: false}));
70
+ app.use(appServerFactory.session({
71
+ secret: Encryptor.generateSecretKey(),
72
+ resave: true,
73
+ saveUninitialized: true
74
+ }));
75
+ app.use(async (req, res, next) => {
76
+ return await this.executeForEveryRequest(req, res, next);
77
+ });
78
+ app.post('/install', async (req, res) => {
79
+ return await this.executeInstallProcess(req, res);
80
+ });
81
+ return true;
82
+ }
83
+
84
+ async executeForEveryRequest(req, res, next)
85
+ {
86
+ if(this.isInstalled()){
87
+ return next();
88
+ }
89
+ let urlPath = req._parsedUrl.pathname;
90
+ if('' === urlPath || '/' === urlPath){
91
+ let installerIndexPath = FileHandler.joinPaths(this.installerPath, 'index.html');
92
+ if(!FileHandler.exists(installerIndexPath)){
93
+ return res.status(500).send('Installer template not found.');
94
+ }
95
+ let content = FileHandler.readFile(installerIndexPath);
96
+ let contentParams = req.session?.templateVariables || this.fetchDefaults();
97
+ let errorParam = req.query?.error;
98
+ if(errorParam){
99
+ contentParams.errorMessage = this.getErrorMessage(errorParam);
100
+ }
101
+ return res.send(this.renderEngine.render(content, contentParams));
102
+ }
103
+ if('/install' !== urlPath){
104
+ return res.redirect('/');
105
+ }
106
+ next();
107
+ }
108
+
109
+ getErrorMessage(errorCode)
110
+ {
111
+ let errorMessages = {
112
+ 'invalid-driver': 'Invalid storage driver selected.',
113
+ 'connection-failed': 'Database connection failed. Please check your credentials.',
114
+ 'raw-query-not-found': 'Query method not found in driver.',
115
+ 'sql-file-not-found': 'SQL installation file not found.',
116
+ 'sql-cms-tables-creation-failed': 'Failed to create CMS tables.',
117
+ 'sql-user-auth-creation-failed': 'Failed to create user authentication tables.',
118
+ 'sql-default-user-error': 'Failed to create default user.',
119
+ 'sql-default-homepage-error': 'Failed to create default homepage.',
120
+ 'installation-entities-generation-failed': 'Failed to generate entities.',
121
+ 'installation-entities-callback-failed': 'Failed to process entities for callback.',
122
+ 'configuration-error': 'Configuration error while completing installation.',
123
+ 'already-installed': 'The application is already installed.'
124
+ };
125
+ return sc.get(errorMessages, errorCode, 'An unknown error occurred during installation.');
126
+ }
127
+
128
+ async executeInstallProcess(req, res)
129
+ {
130
+ if(this.isInstalled()){
131
+ return res.redirect('/?redirect=already-installed');
132
+ }
133
+ let templateVariables = req.body;
134
+ req.session.templateVariables = templateVariables;
135
+ let selectedDriver = templateVariables['db-storage-driver'];
136
+ let driverClass = DriversMap[selectedDriver];
137
+ if(!driverClass){
138
+ Logger.error('Invalid storage driver: ' + selectedDriver);
139
+ return res.redirect('/?error=invalid-driver');
140
+ }
141
+ let dbConfig = {
142
+ client: templateVariables['db-client'],
143
+ config: {
144
+ host: templateVariables['db-host'],
145
+ port: Number(templateVariables['db-port']),
146
+ database: templateVariables['db-name'],
147
+ user: templateVariables['db-username'],
148
+ password: templateVariables['db-password'],
149
+ multipleStatements: true
150
+ },
151
+ debug: false
152
+ };
153
+ if('prisma' === selectedDriver && this.prismaClient){
154
+ dbConfig.prismaClient = this.prismaClient;
155
+ }
156
+ let dbDriver = new driverClass(dbConfig);
157
+ if(!await dbDriver.connect()){
158
+ Logger.error('Connection failed');
159
+ return res.redirect('/?error=connection-failed');
160
+ }
161
+ if(!sc.isObjectFunction(dbDriver, 'rawQuery')){
162
+ Logger.error('Method "rawQuery" not found.');
163
+ return res.redirect('/?error=raw-query-not-found');
164
+ }
165
+ let executeFiles = {
166
+ 'install-cms-tables': 'install.sql',
167
+ 'install-user-auth': 'users-authentication.sql',
168
+ 'install-default-user': 'default-user.sql',
169
+ 'install-default-homepage': 'default-homepage.sql',
170
+ 'install-default-blocks': 'default-blocks.sql',
171
+ 'install-entity-access': 'default-entity-access.sql',
172
+ 'install-dynamic-forms': 'default-forms.sql'
173
+ };
174
+ for(let checkboxName of Object.keys(executeFiles)){
175
+ let fileName = executeFiles[checkboxName];
176
+ let redirectError = await this.executeQueryFile(
177
+ sc.get(templateVariables, checkboxName, 'off'),
178
+ fileName,
179
+ dbDriver
180
+ );
181
+ if('' !== redirectError){
182
+ return res.redirect(redirectError);
183
+ }
184
+ }
185
+ let entitiesGenerationResult = await this.generateEntities(dbDriver, false, true);
186
+ if(!entitiesGenerationResult){
187
+ Logger.error('Entities generation error.');
188
+ return res.redirect('/?error=installation-entities-generation-failed');
189
+ }
190
+ Logger.info('Generated entities.');
191
+ try {
192
+ let mappedVariablesForConfig = this.mapVariablesForConfig(templateVariables);
193
+ await this.createEnvFile(this.mapVariablesForTemplate(mappedVariablesForConfig));
194
+ await this.prepareProjectDirectories();
195
+ await this.copyAdminDirectory();
196
+ await this.createIndexJsFile(templateVariables);
197
+ if(sc.isFunction(this.postInstallCallback)){
198
+ if(this.appServer && sc.isFunction(this.appServer.close)){
199
+ await this.appServer.close();
200
+ }
201
+ Logger.debug('Running postInstallCallback.');
202
+ let callbackResult = await this.postInstallCallback({
203
+ loadedEntities: this.entitiesLoader.loadEntities(selectedDriver),
204
+ mappedVariablesForConfig
205
+ });
206
+ if(false === callbackResult){
207
+ Logger.error('Post-install callback failed.');
208
+ return res.redirect('/?error=installation-entities-callback-failed');
209
+ }
210
+ }
211
+ await this.createLockFile();
212
+ Logger.info('Installation successful!');
213
+ let successContent = 'Installation successful! Run "node ." to start your CMS.';
214
+ let successFileContent = FileHandler.readFile(FileHandler.joinPaths(this.installerPath, 'success.html'));
215
+ if(successFileContent){
216
+ successContent = this.renderEngine.render(
217
+ successFileContent,
218
+ {adminPath: templateVariables['app-admin-path']},
219
+ );
220
+ }
221
+ return res.send(successContent);
222
+ } catch (error) {
223
+ Logger.critical('Configuration error: '+error.message);
224
+ return res.redirect('/?error=installation-error');
225
+ }
226
+ }
227
+
228
+ async executeQueryFile(isMarked, fileName, dbDriver)
229
+ {
230
+ if('on' !== isMarked){
231
+ return '';
232
+ }
233
+ let sqlFileContent = FileHandler.readFile(FileHandler.joinPaths(this.migrationsPath, fileName));
234
+ if(!sqlFileContent){
235
+ Logger.error('SQL file "'+fileName+'" not found.');
236
+ return '/?error=sql-file-not-found&file-name='+fileName;
237
+ }
238
+ let queryExecutionResult = await dbDriver.rawQuery(sqlFileContent);
239
+ if(!queryExecutionResult){
240
+ Logger.error('SQL file "'+fileName+'" raw execution failed.');
241
+ return '/?error=sql-file-execution-error&file-name='+fileName;
242
+ }
243
+ Logger.info('SQL file "'+fileName+'" raw execution successfully.');
244
+ return '';
245
+ }
246
+
247
+ async generateEntities(server, isOverride = false, isInstallationMode = false, isDryPrisma = false)
248
+ {
249
+ let driverType = sc.get(DriversClassMap, server.constructor.name, '');
250
+ Logger.debug('Driver type detected: '+driverType+', Server constructor: '+server.constructor.name);
251
+ if('prisma' === driverType && !isInstallationMode && !isDryPrisma){
252
+ Logger.info('Running prisma introspect "npx prisma db pull"...');
253
+ let dbConfig = this.extractDbConfigFromServer(server);
254
+ Logger.debug('Extracted DB config:', dbConfig);
255
+ if(dbConfig){
256
+ let generatedPrismaSchema = await this.generatePrismaSchema(dbConfig);
257
+ if(!generatedPrismaSchema){
258
+ Logger.error('Prisma schema generation failed.');
259
+ return false;
260
+ }
261
+ Logger.info('Generated Prisma schema for entities generation.');
262
+ }
263
+ }
264
+ if('prisma' === driverType && isDryPrisma){
265
+ Logger.info('Skipping Prisma schema generation due to --dry-prisma flag.');
266
+ }
267
+ let generatorConfig = {
268
+ server,
269
+ projectPath: this.projectRoot,
270
+ isOverride
271
+ };
272
+ if('prisma' === driverType && this.prismaClient){
273
+ generatorConfig.prismaClient = this.prismaClient;
274
+ }
275
+ let generator = new EntitiesGenerator(generatorConfig);
276
+ let success = await generator.generate();
277
+ if(!success){
278
+ Logger.error('Entities generation failed.');
279
+ return false;
280
+ }
281
+ return true;
282
+ }
283
+
284
+ extractDbConfigFromServer(server)
285
+ {
286
+ let config = sc.get(server, 'config');
287
+ if(!config){
288
+ Logger.warning('Could not extract database config from server.');
289
+ return false;
290
+ }
291
+ let dbConfig = {
292
+ client: sc.get(server, 'client', 'mysql'),
293
+ config: {
294
+ host: sc.get(config, 'host', 'localhost'),
295
+ port: sc.get(config, 'port', 3306),
296
+ database: sc.get(config, 'database', ''),
297
+ user: sc.get(config, 'user', ''),
298
+ password: sc.get(config, 'password', ''),
299
+ multipleStatements: true
300
+ },
301
+ debug: false
302
+ };
303
+ Logger.debug('Extracted DB config structure:', {
304
+ client: dbConfig.client,
305
+ host: dbConfig.config.host,
306
+ database: dbConfig.config.database
307
+ });
308
+ return dbConfig;
309
+ }
310
+
311
+ async generatePrismaSchema(connectionData, useDataProxy = false)
312
+ {
313
+ if(!connectionData){
314
+ Logger.error('Missing "connectionData" to generate Prisma Schema.');
315
+ return false;
316
+ }
317
+ let generator = new PrismaSchemaGenerator({
318
+ ...connectionData,
319
+ dataProxy: useDataProxy,
320
+ clientOutputPath: FileHandler.joinPaths(this.projectRoot, 'prisma', 'client'),
321
+ prismaSchemaPath: FileHandler.joinPaths(this.projectRoot, 'prisma')
322
+ });
323
+ let success = await generator.generate();
324
+ if(!success){
325
+ Logger.error('Prisma schema generation failed.');
326
+ return false;
327
+ }
328
+ return true;
329
+ }
330
+
331
+ async createEnvFile(templateVariables)
332
+ {
333
+ let envTemplatePath = FileHandler.joinPaths(this.defaultTemplatesPath, '.env.dist');
334
+ let envTemplateContent = FileHandler.readFile(envTemplatePath);
335
+ if(!envTemplateContent){
336
+ Logger.error('Template ".env.dist" not found: '+envTemplatePath);
337
+ return false;
338
+ }
339
+ return FileHandler.writeFile(this.envFilePath, this.renderEngine.render(envTemplateContent, templateVariables));
340
+ }
341
+
342
+ mapVariablesForTemplate(configVariables)
343
+ {
344
+ return {
345
+ host: configVariables.host,
346
+ port: configVariables.port,
347
+ publicUrl: configVariables.publicUrl,
348
+ adminPath: configVariables.adminPath,
349
+ adminSecret: configVariables.adminSecret,
350
+ dbClient: configVariables.database.client,
351
+ dbHost: configVariables.database.host,
352
+ dbPort: configVariables.database.port,
353
+ dbName: configVariables.database.name,
354
+ dbUser: configVariables.database.user,
355
+ dbPassword: configVariables.database.password,
356
+ dbDriver: configVariables.database.driver
357
+ };
358
+ }
359
+
360
+ mapVariablesForConfig(templateVariables)
361
+ {
362
+ return {
363
+ host: sc.get(templateVariables, 'app-host', 'http://localhost'),
364
+ port: Number(sc.get(templateVariables, 'app-port', 8080)),
365
+ publicUrl: sc.get(templateVariables, 'app-public-url', ''),
366
+ adminPath: sc.get(templateVariables, 'app-admin-path', '/reldens-admin'),
367
+ adminSecret: sc.get(templateVariables, 'app-admin-secret', Encryptor.generateSecretKey()),
368
+ database: {
369
+ client: sc.get(templateVariables, 'db-client', 'mysql'),
370
+ host: sc.get(templateVariables, 'db-host', 'localhost'),
371
+ port: Number(sc.get(templateVariables, 'db-port', 3306)),
372
+ name: sc.get(templateVariables, 'db-name', 'reldens_cms'),
373
+ user: sc.get(templateVariables, 'db-username', ''),
374
+ password: sc.get(templateVariables, 'db-password', ''),
375
+ driver: sc.get(templateVariables, 'db-storage-driver', 'prisma')
376
+ }
377
+ };
378
+ }
379
+
380
+ async createIndexJsFile(templateVariables)
381
+ {
382
+ if(!FileHandler.exists(this.indexTemplatePath)){
383
+ Logger.error('Index.js template not found: ' + this.indexTemplatePath);
384
+ return false;
385
+ }
386
+ let indexTemplate = FileHandler.readFile(this.indexTemplatePath);
387
+ let driverKey = templateVariables['db-storage-driver'];
388
+ let templateParams = {driverKey};
389
+ if('prisma' === driverKey){
390
+ let prismaClientPath = FileHandler.joinPaths(this.projectRoot, 'prisma', 'client');
391
+ templateParams.prismaClientImports = 'const { PrismaClient } = require(\'' + prismaClientPath + '\');';
392
+ templateParams.prismaClientParam = ',\n prismaClient: new PrismaClient()';
393
+ }
394
+ if('prisma' !== driverKey){
395
+ templateParams.prismaClientImports = '';
396
+ templateParams.prismaClientParam = '';
397
+ }
398
+ let indexContent = this.renderEngine.render(indexTemplate, templateParams);
399
+ let indexFilePath = FileHandler.joinPaths(this.projectRoot, 'index.js');
400
+ if(FileHandler.exists(indexFilePath)){
401
+ Logger.info('Index.js file already exists, the CMS installer will not override the existent one.');
402
+ return true;
403
+ }
404
+ return FileHandler.writeFile(indexFilePath, indexContent);
405
+ }
406
+
407
+ async createLockFile()
408
+ {
409
+ return FileHandler.writeFile(this.installLockPath, 'Installation completed on '+new Date().toISOString());
410
+ }
411
+
412
+ async copyAdminDirectory()
413
+ {
414
+ let projectAdminPath = FileHandler.joinPaths(this.projectRoot, 'admin');
415
+ if(FileHandler.exists(projectAdminPath)){
416
+ Logger.info('Admin folder already exists in project root.');
417
+ return true;
418
+ }
419
+ if(!FileHandler.exists(this.moduleAdminPath)){
420
+ Logger.error('Admin folder not found in module path: '+this.moduleAdminPath);
421
+ return false;
422
+ }
423
+ let projectAdminTemplates = FileHandler.joinPaths(projectAdminPath, 'templates');
424
+ FileHandler.copyFolderSync(this.moduleAdminTemplatesPath, projectAdminTemplates);
425
+ FileHandler.copyFolderSync(this.moduleAdminAssetsPath, this.projectPublicAssetsPath);
426
+ FileHandler.copyFile(
427
+ FileHandler.joinPaths(this.moduleAdminPath, 'reldens-admin-client.css'),
428
+ FileHandler.joinPaths(this.projectCssPath, 'reldens-admin-client.css'),
429
+ );
430
+ FileHandler.copyFile(
431
+ FileHandler.joinPaths(this.moduleAdminPath, 'reldens-admin-client.js'),
432
+ FileHandler.joinPaths(this.projectJsPath, 'reldens-admin-client.js'),
433
+ );
434
+ Logger.info('Admin folder copied to project root.');
435
+ return true;
436
+ }
437
+
438
+ async prepareProjectDirectories()
439
+ {
440
+ FileHandler.createFolder(this.projectTemplatesPath);
441
+ FileHandler.createFolder(FileHandler.joinPaths(this.projectTemplatesPath, 'layouts'));
442
+ FileHandler.createFolder(this.projectPublicPath);
443
+ FileHandler.createFolder(this.projectPublicAssetsPath);
444
+ FileHandler.createFolder(this.projectCssPath);
445
+ FileHandler.createFolder(this.projectJsPath);
446
+ let baseFiles = [
447
+ 'page.html',
448
+ '404.html',
449
+ 'browserconfig.xml',
450
+ 'favicon.ico',
451
+ 'site.webmanifest'
452
+ ];
453
+ for(let fileName of baseFiles){
454
+ FileHandler.copyFile(
455
+ FileHandler.joinPaths(this.defaultTemplatesPath, fileName),
456
+ FileHandler.joinPaths(this.projectTemplatesPath, fileName)
457
+ );
458
+ }
459
+ FileHandler.copyFile(
460
+ FileHandler.joinPaths(this.defaultTemplatesPath, 'layouts', 'default.html'),
461
+ FileHandler.joinPaths(this.projectTemplatesPath, 'layouts', 'default.html')
462
+ );
463
+ FileHandler.copyFile(
464
+ FileHandler.joinPaths(this.defaultTemplatesPath, 'css', 'styles.css'),
465
+ FileHandler.joinPaths(this.projectCssPath, 'styles.css')
466
+ );
467
+ FileHandler.copyFile(
468
+ FileHandler.joinPaths(this.defaultTemplatesPath, 'js', 'scripts.js'),
469
+ FileHandler.joinPaths(this.projectJsPath, 'scripts.js')
470
+ );
471
+ FileHandler.copyFolderSync(
472
+ FileHandler.joinPaths(this.defaultTemplatesPath, 'partials'),
473
+ FileHandler.joinPaths(this.projectTemplatesPath, 'partials')
474
+ );
475
+ FileHandler.copyFolderSync(
476
+ FileHandler.joinPaths(this.defaultTemplatesPath, 'domains'),
477
+ FileHandler.joinPaths(this.projectTemplatesPath, 'domains')
478
+ );
479
+ return true;
480
+ }
481
+
482
+ fetchDefaults()
483
+ {
484
+ return {
485
+ 'app-host': process.env.RELDENS_APP_HOST || 'http://localhost',
486
+ 'app-port': process.env.RELDENS_APP_PORT || '8080',
487
+ 'app-public-url': process.env.RELDENS_PUBLIC_URL || '',
488
+ 'app-admin-path': process.env.RELDENS_ADMIN_ROUTE_PATH || '/reldens-admin',
489
+ 'db-storage-driver': process.env.RELDENS_STORAGE_DRIVER || 'prisma',
490
+ 'db-client': process.env.RELDENS_DB_CLIENT || 'mysql',
491
+ 'db-host': process.env.RELDENS_DB_HOST || 'localhost',
492
+ 'db-port': process.env.RELDENS_DB_PORT || '3306',
493
+ 'db-name': process.env.RELDENS_DB_NAME || 'reldens_cms',
494
+ 'db-username': process.env.RELDENS_DB_USER || '',
495
+ 'db-password': process.env.RELDENS_DB_PASSWORD || ''
496
+ };
497
+ }
498
+ }
499
+
500
+ module.exports.Installer = Installer;