create-fullstack-boilerplate 2.2.2 → 3.0.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/index.js CHANGED
@@ -13,12 +13,11 @@
13
13
  return;
14
14
  }
15
15
 
16
- // ---- Your existing create-project code ----
17
16
  const path = require("path");
18
17
  const copyProject = require('./lib/copyProject');
19
- const { runInstall, installDBDriver, normalizePath } = require("./lib/utils"); // ADD normalizePath HERE
18
+ const { runInstall, installDBDriver, normalizePath } = require("./lib/utils");
20
19
  const setupMainDB = require("./lib/setupMainDB");
21
- const setupExtraDB = require("./lib/setupExtraDB");
20
+ const { setupExtraDB, handleTableCreation } = require("./lib/setupExtraDB");
22
21
  const testDBConnection = require("./lib/testDBConnection");
23
22
  const { mainPrompts, extraDBPrompts } = require("./lib/prompts");
24
23
 
@@ -29,7 +28,7 @@
29
28
  let templateDir;
30
29
 
31
30
  // Try __dirname first (works for local/dev)
32
- const localTemplate = normalizePath(path.resolve(__dirname, "template")); // NORMALIZE
31
+ const localTemplate = normalizePath(path.resolve(__dirname, "template"));
33
32
  console.log("šŸ” Checking local template:", localTemplate);
34
33
 
35
34
  if (fs.existsSync(localTemplate)) {
@@ -39,7 +38,7 @@
39
38
  // Fallback for npx (temp folder)
40
39
  try {
41
40
  const pkg = require.resolve("create-fullstack-boilerplate/package.json");
42
- templateDir = normalizePath(path.resolve(path.dirname(pkg), "template")); // NORMALIZE
41
+ templateDir = normalizePath(path.resolve(path.dirname(pkg), "template"));
43
42
  console.log("āœ… Found npm template:", templateDir);
44
43
  } catch (err) {
45
44
  console.error("āŒ Could not resolve package:", err.message);
@@ -68,7 +67,7 @@
68
67
  console.log("\nšŸŽ›ļø Setting Up Full Stack Project...\n");
69
68
 
70
69
  const answers = await mainPrompts();
71
- const targetDir = normalizePath(path.join(process.cwd(), answers.projectName)); // NORMALIZE
70
+ const targetDir = normalizePath(path.join(process.cwd(), answers.projectName));
72
71
 
73
72
  console.log("Target directory:", targetDir);
74
73
 
@@ -84,23 +83,37 @@
84
83
 
85
84
  await setupMainDB(targetDir, answers.dbDialect);
86
85
  console.log("āž”ļø Installing backend dependencies...");
87
- await runInstall(normalizePath(path.join(targetDir, "Backend"))); // NORMALIZE
86
+ await runInstall(normalizePath(path.join(targetDir, "Backend")));
88
87
 
89
88
  console.log("āž”ļø Installing frontend dependencies...");
90
- await runInstall(normalizePath(path.join(targetDir, "Frontend"))); // NORMALIZE
89
+ await runInstall(normalizePath(path.join(targetDir, "Frontend")));
91
90
 
92
91
  console.log("āž”ļø Installing Your Db Dialect: ", answers.dbDialect);
93
- await installDBDriver(normalizePath(path.join(targetDir, "Backend")), answers.dbDialect); // NORMALIZE
94
-
92
+ await installDBDriver(normalizePath(path.join(targetDir, "Backend")), answers.dbDialect);
95
93
 
96
94
  if (answers.addExtraDB) {
97
95
  const extraDB = await extraDBPrompts();
98
96
  console.log(`\nšŸ” Testing connection for ${extraDB.dbKey}...`);
99
- await installDBDriver(normalizePath(path.join(targetDir, "Backend")), extraDB.dialect); // NORMALIZE
97
+ await installDBDriver(normalizePath(path.join(targetDir, "Backend")), extraDB.dialect);
100
98
  const ok = await testDBConnection(targetDir, extraDB);
101
99
 
102
100
  if (ok) {
101
+ // Setup the database configuration and first table
103
102
  await setupExtraDB(targetDir, extraDB);
103
+
104
+ const firstTable = {
105
+ tableName: extraDB.tableName,
106
+ attributes: extraDB.attributes
107
+ };
108
+
109
+ await handleTableCreation(
110
+ targetDir,
111
+ extraDB.dbKey,
112
+ firstTable,
113
+ extraDB.dialect,
114
+ true // isNewDB
115
+ );
116
+
104
117
  console.log(`āœ… Extra DB '${extraDB.dbKey}' successfully integrated.\n`);
105
118
  } else {
106
119
  console.log(`āš ļø Connection failed. Skipping extra DB setup.\n`);
package/lib/addDB.js CHANGED
@@ -1,9 +1,10 @@
1
1
  const path = require("path");
2
- const { extraDBPrompts } = require("./prompts");
3
- const testDBConnection = require("./testDBConnection");
4
- const setupExtraDB = require("./setupExtraDB");
5
2
  const fs = require("fs-extra");
6
-
3
+ const inquirer = require("inquirer");
4
+ const { tablePrompts } = require("./prompts");
5
+ const testDBConnection = require("./testDBConnection");
6
+ const { setupExtraDB, handleTableCreation } = require("./setupExtraDB");
7
+ const { installDBDriver } = require("./utils");
7
8
 
8
9
  module.exports = async function addDB() {
9
10
  console.log(`\nāž• Add New Database to Existing Project\n`);
@@ -31,28 +32,134 @@ module.exports = async function addDB() {
31
32
  }
32
33
 
33
34
  try {
34
- // Ask DB info
35
- const extraDB = await extraDBPrompts();
36
-
37
- // Check if DB with same key already exists
35
+ // Get list of existing databases
38
36
  const dbConfigsPath = path.join(dbDir, "dbConfigs.js");
37
+ let existingDBs = [];
38
+
39
39
  if (await fs.pathExists(dbConfigsPath)) {
40
40
  const dbConfigsContent = await fs.readFile(dbConfigsPath, "utf8");
41
- if (dbConfigsContent.includes(`key: "${extraDB.dbKey}"`)) {
42
- console.log(`\nāš ļø Database with key '${extraDB.dbKey}' already exists in the project.`);
43
- console.log(` Please use a different DB identifier.\n`);
44
- return;
41
+ const keyMatches = dbConfigsContent.match(/key:\s*"([^"]+)"/g);
42
+ if (keyMatches) {
43
+ existingDBs = keyMatches.map(match => match.match(/"([^"]+)"/)[1]);
45
44
  }
46
45
  }
47
46
 
48
- // Check if DB folder already exists
49
- const dbFolder = path.join(modelsDir, extraDB.dbKey);
50
- if (await fs.pathExists(dbFolder)) {
51
- console.log(`\nāš ļø Folder '${extraDB.dbKey}' already exists in Models directory.`);
52
- console.log(` Please use a different DB identifier.\n`);
47
+ // Show existing databases if any
48
+ if (existingDBs.length > 0) {
49
+ console.log(`\nšŸ“Š Existing databases in your project:`);
50
+ existingDBs.forEach(db => console.log(` - ${db}`));
51
+ console.log('');
52
+ }
53
+
54
+ // Ask what user wants to do
55
+ const { action } = await inquirer.prompt([
56
+ {
57
+ type: 'list',
58
+ name: 'action',
59
+ message: 'What would you like to do?',
60
+ choices: [
61
+ { name: 'Add a new database', value: 'new' },
62
+ ...(existingDBs.length > 0 ? [{ name: 'Add tables to an existing database', value: 'existing' }] : [])
63
+ ]
64
+ }
65
+ ]);
66
+
67
+ // If user wants to add to existing DB
68
+ if (action === 'existing') {
69
+ const { dbKey } = await inquirer.prompt([
70
+ {
71
+ type: 'list',
72
+ name: 'dbKey',
73
+ message: 'Select database to add tables to:',
74
+ choices: existingDBs
75
+ }
76
+ ]);
77
+
78
+ console.log(`\nāœ… Adding tables to '${dbKey}'...\n`);
79
+
80
+ await handleTableCreation(
81
+ targetDir,
82
+ dbKey,
83
+ null, // no first table
84
+ 'mysql', // default dialect
85
+ false // isNewDB = false
86
+ );
87
+
88
+ console.log(`āœ… Successfully updated ${dbKey}.\n`);
89
+ console.log(`šŸ“ Next steps:`);
90
+ console.log(` 1. Use the CREATE statements above to create tables in your database`);
91
+ console.log(` 2. Import and use in your routes: const { ${dbKey} } = require('./Models');\n`);
92
+
53
93
  return;
54
94
  }
55
95
 
96
+ // New DB flow
97
+ console.log(`\nšŸ“ Setting up a new database...\n`);
98
+
99
+ // Ask for DB identifier first
100
+ const { dbKey } = await inquirer.prompt([
101
+ {
102
+ name: 'dbKey',
103
+ message: 'DB Identifier (e.g., REPORTING_DB, myDB, test-db):',
104
+ validate: (input) => {
105
+ if (!input || input.trim().length === 0) {
106
+ return 'Cannot be empty';
107
+ }
108
+ if (/[\s@#$%^&*()+={}[\];:'",.<>?/\\|`~]/.test(input)) {
109
+ return 'Special characters and spaces not allowed';
110
+ }
111
+ return true;
112
+ }
113
+ }
114
+ ]);
115
+
116
+ // Collect DB credentials
117
+ const dbCredentials = await inquirer.prompt([
118
+ {
119
+ name: 'database',
120
+ message: 'DB Name:',
121
+ validate: (input) => input.trim().length > 0 ? true : 'Cannot be empty'
122
+ },
123
+ {
124
+ name: 'username',
125
+ message: 'DB Username:',
126
+ validate: (input) => input.trim().length > 0 ? true : 'Cannot be empty'
127
+ },
128
+ {
129
+ name: 'password',
130
+ message: 'DB Password:'
131
+ },
132
+ {
133
+ name: 'host',
134
+ message: 'DB Host:',
135
+ default: 'localhost'
136
+ },
137
+ {
138
+ name: 'port',
139
+ message: 'DB Port:',
140
+ default: '3306',
141
+ validate: (input) => {
142
+ const port = parseInt(input);
143
+ if (isNaN(port) || port < 1 || port > 65535) {
144
+ return 'Please enter a valid port number (1-65535)';
145
+ }
146
+ return true;
147
+ }
148
+ },
149
+ {
150
+ type: 'list',
151
+ name: 'dialect',
152
+ message: 'DB Dialect:',
153
+ choices: ['mysql', 'mariadb', 'postgres']
154
+ }
155
+ ]);
156
+
157
+ const extraDB = {
158
+ dbKey: dbKey,
159
+ ...dbCredentials
160
+ };
161
+
162
+ // Test connection
56
163
  console.log(`\nšŸ” Testing connection for ${extraDB.dbKey}...\n`);
57
164
  const ok = await testDBConnection(targetDir, extraDB);
58
165
 
@@ -61,11 +168,36 @@ module.exports = async function addDB() {
61
168
  return;
62
169
  }
63
170
 
171
+ // Install DB driver
172
+ await installDBDriver(path.join(targetDir, "Backend"), extraDB.dialect);
173
+
174
+ // Collect first table information
175
+ console.log(`\nšŸ“‹ Now let's create the first table for ${extraDB.dbKey}...\n`);
176
+ const firstTableConfig = await tablePrompts();
177
+
178
+ // Setup database with first table
179
+ extraDB.tableName = firstTableConfig.tableName;
180
+ extraDB.attributes = firstTableConfig.attributes;
64
181
  await setupExtraDB(targetDir, extraDB);
65
- console.log(`\nāœ… Successfully added ${extraDB.dbKey} to project.\n`);
182
+
183
+ // Handle additional tables
184
+ const firstTable = {
185
+ tableName: firstTableConfig.tableName,
186
+ attributes: firstTableConfig.attributes
187
+ };
188
+
189
+ await handleTableCreation(
190
+ targetDir,
191
+ extraDB.dbKey,
192
+ firstTable,
193
+ extraDB.dialect,
194
+ true // isNewDB
195
+ );
196
+
197
+ console.log(`āœ… Successfully added ${extraDB.dbKey} to project.\n`);
66
198
  console.log(`šŸ“ Next steps:`);
67
- console.log(` 1. Check backend/.env for database credentials`);
68
- console.log(` 2. Add more models in backend/Models/${extraDB.dbKey}/`);
199
+ console.log(` 1. Use the CREATE statements above to create tables in your database`);
200
+ console.log(` 2. Check backend/.env for database credentials`);
69
201
  console.log(` 3. Import and use in your routes: const { ${extraDB.dbKey} } = require('./Models');\n`);
70
202
 
71
203
  } catch (err) {
@@ -74,4 +206,4 @@ module.exports = async function addDB() {
74
206
  console.log(`\nStack trace:`, err.stack);
75
207
  }
76
208
  }
77
- };
209
+ };
@@ -0,0 +1,72 @@
1
+ const inquirer = require("inquirer");
2
+ const collectTableAttributes = require('./tableAttributePrompts')
3
+
4
+ const extraDBPrompts = async () => {
5
+ const basicInfo = await inquirer.prompt([
6
+ {
7
+ name: 'dbKey',
8
+ message: 'DB Identifier (e.g., REPORTING_DB, myDB, test-db):',
9
+ validate: (input) => {
10
+ if (!input || input.trim().length === 0) {
11
+ return 'Cannot be empty';
12
+ }
13
+ if (/[\s@#$%^&*()+={}[\];:'",.<>?/\\|`~]/.test(input)) {
14
+ return 'Special characters and spaces not allowed';
15
+ }
16
+ return true;
17
+ }
18
+ },
19
+ { name: 'database', message: 'DB Name:', validate: (input) => input.trim().length > 0 ? true : 'Cannot be empty' },
20
+ { name: 'username', message: 'DB Username:', validate: (input) => input.trim().length > 0 ? true : 'Cannot be empty' },
21
+ { name: 'password', message: 'DB Password:' },
22
+ { name: 'host', message: 'DB Host:', default: 'localhost' },
23
+ {
24
+ name: 'port',
25
+ message: 'DB Port:',
26
+ default: '3306',
27
+ validate: (input) => {
28
+ const port = parseInt(input);
29
+ if (isNaN(port) || port < 1 || port > 65535) {
30
+ return 'Please enter a valid port number (1-65535)';
31
+ }
32
+ return true;
33
+ }
34
+ },
35
+ {
36
+ type: 'list',
37
+ name: 'dialect',
38
+ message: 'DB Dialect:',
39
+ choices: ['mysql', 'mariadb', 'postgres']
40
+ },
41
+ {
42
+ name: 'tableName',
43
+ message: 'Model / Table Name:',
44
+ default: 'sample_table',
45
+ validate: (input) => {
46
+ if (!input || input.trim().length === 0) {
47
+ return 'Cannot be empty';
48
+ }
49
+ if (/\s/.test(input)) {
50
+ return 'Spaces not allowed - use underscores instead';
51
+ }
52
+ return true;
53
+ }
54
+
55
+ },
56
+ {
57
+ type: 'confirm',
58
+ name: 'defineAttributes',
59
+ message: 'Do you want to define table attributes now?',
60
+ default: false
61
+ }
62
+ ]);
63
+
64
+ // If user wants to define attributes, collect them
65
+ if (basicInfo.defineAttributes) {
66
+ basicInfo.attributes = await collectTableAttributes(basicInfo.dialect);
67
+ }
68
+
69
+ return basicInfo;
70
+ };
71
+
72
+ module.exports = extraDBPrompts;
@@ -0,0 +1,46 @@
1
+ const inquirer = require("inquirer");
2
+
3
+ const mainPrompts = () => {
4
+ return inquirer.prompt([
5
+ {
6
+ name: 'projectName',
7
+ message: 'Project name',
8
+ default: 'my-fullstack-app',
9
+ validate: (input) => {
10
+ if (!input || input.trim().length === 0) {
11
+ return 'Project name cannot be empty';
12
+ }
13
+ if (!/^[a-zA-Z0-9_-]+$/.test(input)) {
14
+ return 'Project name can only contain letters, numbers, hyphens, and underscores';
15
+ }
16
+ return true;
17
+ }
18
+ },
19
+ {
20
+ type: 'list',
21
+ name: 'dbDialect',
22
+ message: 'Main DB Dialect:',
23
+ choices: ['mysql', 'mariadb', 'postgres']
24
+ },
25
+ {
26
+ type: 'confirm',
27
+ name: 'addExtraDB',
28
+ message: 'Add an extra database connection?',
29
+ default: false
30
+ },
31
+ {
32
+ type: 'confirm',
33
+ name: 'initGit',
34
+ message: 'Initialize Git repository?',
35
+ default: false
36
+ },
37
+ {
38
+ name: 'remoteRepo',
39
+ message: 'Enter remote Git repository URL:',
40
+ when: (answers) => answers.initGit === true,
41
+ validate: (input) => input.length > 0 ? true : "This field cannot be empty."
42
+ }
43
+ ]);
44
+ };
45
+
46
+ module.exports = mainPrompts;
@@ -0,0 +1,37 @@
1
+ const inquirer = require("inquirer");
2
+
3
+ const routePrompts = () => {
4
+ return inquirer.prompt([
5
+ {
6
+ name: 'routeName',
7
+ message: 'Route name (e.g., users, products):',
8
+ validate: (input) => {
9
+ if (!input || input.trim().length === 0) {
10
+ return 'Route name cannot be empty';
11
+ }
12
+ return true;
13
+ }
14
+ },
15
+ {
16
+ name: 'routePath',
17
+ message: 'Route path (e.g., /users):',
18
+ default: (answers) => `/${answers.routeName}`,
19
+ validate: (input) => {
20
+ if (!input || input.trim().length === 0) {
21
+ return 'Route path cannot be empty';
22
+ }
23
+ if (!input.startsWith('/')) {
24
+ return 'Route path should start with /';
25
+ }
26
+ return true;
27
+ }
28
+ },
29
+ {
30
+ type: 'confirm',
31
+ name: 'needsAuth',
32
+ message: 'Does this route require authentication?',
33
+ default: true
34
+ }
35
+ ]);
36
+ };
37
+ module.exports = routePrompts;
@@ -0,0 +1,148 @@
1
+ const inquirer = require("inquirer");
2
+ const { getDataTypeChoices, needsLength } = require('../dataTypes');
3
+
4
+ const collectTableAttributes = async (dialect) => {
5
+ const attributes = [];
6
+
7
+ console.log('\nšŸ“‹ Define table attributes. Start with the primary key.\n');
8
+
9
+ // Primary key first
10
+ const pkAnswers = await inquirer.prompt([
11
+ {
12
+ name: 'name',
13
+ message: 'Primary key column name:',
14
+ default: 'id',
15
+ validate: (input) => {
16
+ if (!input || input.trim().length === 0) return 'Column name cannot be empty';
17
+ return true;
18
+ }
19
+ },
20
+ {
21
+ type: 'list',
22
+ name: 'type',
23
+ message: 'Primary key data type:',
24
+ choices: ['INTEGER', 'BIGINT', 'UUID', 'STRING'],
25
+ default: 'INTEGER'
26
+ },
27
+ {
28
+ type: 'confirm',
29
+ name: 'autoIncrement',
30
+ message: 'Auto-increment?',
31
+ default: true,
32
+ when: (answers) => answers.type === 'INTEGER' || answers.type === 'BIGINT'
33
+ }
34
+ ]);
35
+
36
+ attributes.push({
37
+ name: pkAnswers.name,
38
+ type: pkAnswers.type,
39
+ primaryKey: true,
40
+ autoIncrement: pkAnswers.autoIncrement || false,
41
+ allowNull: false
42
+ });
43
+
44
+ console.log('\nāœ… Primary key added. Now add other attributes.\n');
45
+
46
+ // Collect other attributes
47
+ let addMore = true;
48
+ while (addMore) {
49
+ const attrAnswers = await inquirer.prompt([
50
+ {
51
+ name: 'name',
52
+ message: 'Column name:',
53
+ validate: (input) => {
54
+ if (!input || input.trim().length === 0) {
55
+ return 'Cannot be empty';
56
+ }
57
+ if (/\s/.test(input)) {
58
+ return 'Spaces not allowed - use underscores instead';
59
+ }
60
+ if (attributes.find(a => a.name === input)) return 'Column name already exists';
61
+
62
+ return true;
63
+ }
64
+ },
65
+
66
+ {
67
+ type: 'list',
68
+ name: 'type',
69
+ message: 'Data type:',
70
+ choices: getDataTypeChoices(dialect)
71
+ }
72
+ ]);
73
+
74
+ // Ask for length if needed
75
+ if (needsLength(dialect, attrAnswers.type)) {
76
+ const lengthAnswer = await inquirer.prompt([
77
+ {
78
+ name: 'length',
79
+ message: `Length/Precision for ${attrAnswers.type}:`,
80
+ default: attrAnswers.type === 'STRING' ? '255' : '10,2',
81
+ validate: (input) => {
82
+ if (!input || input.trim().length === 0) return 'Length cannot be empty';
83
+ return true;
84
+ }
85
+ }
86
+ ]);
87
+ attrAnswers.length = lengthAnswer.length;
88
+ }
89
+
90
+ // Ask for constraints
91
+ const constraints = await inquirer.prompt([
92
+ {
93
+ type: 'confirm',
94
+ name: 'allowNull',
95
+ message: 'Allow NULL values?',
96
+ default: true
97
+ },
98
+ {
99
+ type: 'confirm',
100
+ name: 'unique',
101
+ message: 'Should this be unique?',
102
+ default: false
103
+ },
104
+ {
105
+ type: 'confirm',
106
+ name: 'hasDefault',
107
+ message: 'Set a default value?',
108
+ default: false
109
+ }
110
+ ]);
111
+
112
+ if (constraints.hasDefault) {
113
+ const defaultAnswer = await inquirer.prompt([
114
+ {
115
+ name: 'defaultValue',
116
+ message: 'Default value:',
117
+ validate: (input) => input !== undefined && input !== '' ? true : 'Provide a default value'
118
+ }
119
+ ]);
120
+ attrAnswers.defaultValue = defaultAnswer.defaultValue;
121
+ }
122
+
123
+ attributes.push({
124
+ name: attrAnswers.name,
125
+ type: attrAnswers.type,
126
+ length: attrAnswers.length,
127
+ allowNull: constraints.allowNull,
128
+ unique: constraints.unique,
129
+ defaultValue: attrAnswers.defaultValue
130
+ });
131
+
132
+ const continueAnswer = await inquirer.prompt([
133
+ {
134
+ type: 'confirm',
135
+ name: 'addMore',
136
+ message: 'Add another attribute?',
137
+ default: true
138
+ }
139
+ ]);
140
+
141
+ addMore = continueAnswer.addMore;
142
+ }
143
+
144
+ console.log(`\nāœ… Added ${attributes.length} attributes to the model.\n`);
145
+ return attributes;
146
+ }
147
+
148
+ module.exports = collectTableAttributes
@@ -0,0 +1,31 @@
1
+ const inquirer = require("inquirer");
2
+ const collectTableAttributes = require('./tableAttributePrompts')
3
+
4
+ const tablePrompts = async () => {
5
+ const tableInfo = await inquirer.prompt([
6
+ {
7
+ name: 'tableName',
8
+ message: 'Model / Table Name:',
9
+ validate: (input) => {
10
+ if (!input || input.trim().length === 0) {
11
+ return 'Table name cannot be empty';
12
+ }
13
+ return true;
14
+ }
15
+ },
16
+ {
17
+ type: 'confirm',
18
+ name: 'defineAttributes',
19
+ message: 'Do you want to define table attributes now?',
20
+ default: true
21
+ }
22
+ ]);
23
+
24
+ if (tableInfo.defineAttributes) {
25
+ tableInfo.attributes = await collectTableAttributes('mysql'); // You can pass dialect if needed
26
+ }
27
+
28
+ return tableInfo;
29
+ };
30
+
31
+ module.exports = tablePrompts
package/lib/prompts.js CHANGED
@@ -1,289 +1,13 @@
1
- const inquirer = require("inquirer");
2
- const { getDataTypeChoices, needsLength } = require('./dataTypes');
3
-
4
- module.exports.mainPrompts = () => {
5
- return inquirer.prompt([
6
- {
7
- name: 'projectName',
8
- message: 'Project name',
9
- default: 'my-fullstack-app',
10
- validate: (input) => {
11
- if (!input || input.trim().length === 0) {
12
- return 'Project name cannot be empty';
13
- }
14
- if (!/^[a-zA-Z0-9_-]+$/.test(input)) {
15
- return 'Project name can only contain letters, numbers, hyphens, and underscores';
16
- }
17
- return true;
18
- }
19
- },
20
- {
21
- type: 'list',
22
- name: 'dbDialect',
23
- message: 'Main DB Dialect:',
24
- choices: ['mysql', 'mariadb', 'postgres']
25
- },
26
- {
27
- type: 'confirm',
28
- name: 'addExtraDB',
29
- message: 'Add an extra database connection?',
30
- default: false
31
- },
32
- {
33
- type: 'confirm',
34
- name: 'initGit',
35
- message: 'Initialize Git repository?',
36
- default: false
37
- },
38
- {
39
- name: 'remoteRepo',
40
- message: 'Enter remote Git repository URL:',
41
- when: (answers) => answers.initGit === true,
42
- validate: (input) => input.length > 0 ? true : "This field cannot be empty."
43
- }
44
- ]);
45
- };
46
-
47
- module.exports.extraDBPrompts = async () => {
48
- const basicInfo = await inquirer.prompt([
49
- {
50
- name: 'dbKey',
51
- message: 'DB Identifier (e.g., REPORTING_DB):',
52
- validate: (input) => {
53
- if (!input || input.trim().length === 0) {
54
- return 'DB identifier cannot be empty';
55
- }
56
- if (!/^[A-Z][A-Z0-9_]*$/.test(input)) {
57
- return 'DB identifier should be UPPERCASE with underscores (e.g., MY_DB)';
58
- }
59
- return true;
60
- }
61
- },
62
- { name: 'database', message: 'DB Name:', validate: (input) => input.trim().length > 0 ? true : 'Cannot be empty' },
63
- { name: 'username', message: 'DB Username:', validate: (input) => input.trim().length > 0 ? true : 'Cannot be empty' },
64
- { name: 'password', message: 'DB Password:' },
65
- { name: 'host', message: 'DB Host:', default: 'localhost' },
66
- {
67
- name: 'port',
68
- message: 'DB Port:',
69
- default: '3306',
70
- validate: (input) => {
71
- const port = parseInt(input);
72
- if (isNaN(port) || port < 1 || port > 65535) {
73
- return 'Please enter a valid port number (1-65535)';
74
- }
75
- return true;
76
- }
77
- },
78
- {
79
- type: 'list',
80
- name: 'dialect',
81
- message: 'DB Dialect:',
82
- choices: ['mysql', 'mariadb', 'postgres']
83
- },
84
- {
85
- name: 'tableName',
86
- message: 'Model / Table Name:',
87
- default: 'sample_table',
88
- validate: (input) => {
89
- if (!input || input.trim().length === 0) {
90
- return 'Table name cannot be empty';
91
- }
92
- if (!/^[a-z][a-z0-9_]*$/.test(input)) {
93
- return 'Table name should be lowercase with underscores (e.g., user_profiles)';
94
- }
95
- return true;
96
- }
97
- },
98
- {
99
- type: 'confirm',
100
- name: 'defineAttributes',
101
- message: 'Do you want to define table attributes now?',
102
- default: false
103
- }
104
- ]);
105
-
106
- // If user wants to define attributes, collect them
107
- if (basicInfo.defineAttributes) {
108
- basicInfo.attributes = await collectTableAttributes(basicInfo.dialect);
109
- }
110
-
111
- return basicInfo;
112
- };
113
-
114
- async function collectTableAttributes(dialect) {
115
- const attributes = [];
116
-
117
- console.log('\nšŸ“‹ Define table attributes. Start with the primary key.\n');
118
-
119
- // Primary key first
120
- const pkAnswers = await inquirer.prompt([
121
- {
122
- name: 'name',
123
- message: 'Primary key column name:',
124
- default: 'id',
125
- validate: (input) => {
126
- if (!input || input.trim().length === 0) return 'Column name cannot be empty';
127
- if (!/^[a-z][a-z0-9_]*$/.test(input)) return 'Use lowercase with underscores';
128
- return true;
129
- }
130
- },
131
- {
132
- type: 'list',
133
- name: 'type',
134
- message: 'Primary key data type:',
135
- choices: ['INTEGER', 'BIGINT', 'UUID', 'STRING'],
136
- default: 'INTEGER'
137
- },
138
- {
139
- type: 'confirm',
140
- name: 'autoIncrement',
141
- message: 'Auto-increment?',
142
- default: true,
143
- when: (answers) => answers.type === 'INTEGER' || answers.type === 'BIGINT'
144
- }
145
- ]);
146
-
147
- attributes.push({
148
- name: pkAnswers.name,
149
- type: pkAnswers.type,
150
- primaryKey: true,
151
- autoIncrement: pkAnswers.autoIncrement || false,
152
- allowNull: false
153
- });
154
-
155
- console.log('\nāœ… Primary key added. Now add other attributes.\n');
156
-
157
- // Collect other attributes
158
- let addMore = true;
159
- while (addMore) {
160
- const attrAnswers = await inquirer.prompt([
161
- {
162
- name: 'name',
163
- message: 'Column name:',
164
- validate: (input) => {
165
- if (!input || input.trim().length === 0) return 'Column name cannot be empty';
166
- if (!/^[a-z][a-z0-9_]*$/.test(input)) return 'Use lowercase with underscores';
167
- if (attributes.find(a => a.name === input)) return 'Column name already exists';
168
- return true;
169
- }
170
- },
171
- {
172
- type: 'list',
173
- name: 'type',
174
- message: 'Data type:',
175
- choices: getDataTypeChoices(dialect)
176
- }
177
- ]);
178
-
179
- // Ask for length if needed
180
- if (needsLength(dialect, attrAnswers.type)) {
181
- const lengthAnswer = await inquirer.prompt([
182
- {
183
- name: 'length',
184
- message: `Length/Precision for ${attrAnswers.type}:`,
185
- default: attrAnswers.type === 'STRING' ? '255' : '10,2',
186
- validate: (input) => {
187
- if (!input || input.trim().length === 0) return 'Length cannot be empty';
188
- return true;
189
- }
190
- }
191
- ]);
192
- attrAnswers.length = lengthAnswer.length;
193
- }
194
-
195
- // Ask for constraints
196
- const constraints = await inquirer.prompt([
197
- {
198
- type: 'confirm',
199
- name: 'allowNull',
200
- message: 'Allow NULL values?',
201
- default: true
202
- },
203
- {
204
- type: 'confirm',
205
- name: 'unique',
206
- message: 'Should this be unique?',
207
- default: false
208
- },
209
- {
210
- type: 'confirm',
211
- name: 'hasDefault',
212
- message: 'Set a default value?',
213
- default: false
214
- }
215
- ]);
216
-
217
- if (constraints.hasDefault) {
218
- const defaultAnswer = await inquirer.prompt([
219
- {
220
- name: 'defaultValue',
221
- message: 'Default value:',
222
- validate: (input) => input !== undefined && input !== '' ? true : 'Provide a default value'
223
- }
224
- ]);
225
- attrAnswers.defaultValue = defaultAnswer.defaultValue;
226
- }
227
-
228
- attributes.push({
229
- name: attrAnswers.name,
230
- type: attrAnswers.type,
231
- length: attrAnswers.length,
232
- allowNull: constraints.allowNull,
233
- unique: constraints.unique,
234
- defaultValue: attrAnswers.defaultValue
235
- });
236
-
237
- const continueAnswer = await inquirer.prompt([
238
- {
239
- type: 'confirm',
240
- name: 'addMore',
241
- message: 'Add another attribute?',
242
- default: true
243
- }
244
- ]);
245
-
246
- addMore = continueAnswer.addMore;
247
- }
248
-
249
- console.log(`\nāœ… Added ${attributes.length} attributes to the model.\n`);
250
- return attributes;
251
- }
252
-
253
- module.exports.routePrompts = () => {
254
- return inquirer.prompt([
255
- {
256
- name: 'routeName',
257
- message: 'Route name (e.g., users, products):',
258
- validate: (input) => {
259
- if (!input || input.trim().length === 0) {
260
- return 'Route name cannot be empty';
261
- }
262
- if (!/^[a-z][a-z0-9_]*$/.test(input)) {
263
- return 'Route name should be lowercase with underscores (e.g., user_profiles)';
264
- }
265
- return true;
266
- }
267
- },
268
- {
269
- name: 'routePath',
270
- message: 'Route path (e.g., /users):',
271
- default: (answers) => `/${answers.routeName}`,
272
- validate: (input) => {
273
- if (!input || input.trim().length === 0) {
274
- return 'Route path cannot be empty';
275
- }
276
- if (!input.startsWith('/')) {
277
- return 'Route path should start with /';
278
- }
279
- return true;
280
- }
281
- },
282
- {
283
- type: 'confirm',
284
- name: 'needsAuth',
285
- message: 'Does this route require authentication?',
286
- default: true
287
- }
288
- ]);
289
- };
1
+ const mainPrompts = require("./inquirePrompts/mainPrompts");
2
+ const extraDBPrompts = require("./inquirePrompts/extraDBPrompts");
3
+ const routePrompts = require("./inquirePrompts/routePrompts");
4
+ const tablePrompts = require("./inquirePrompts/tablePrompts");
5
+ const tableAttributePrompts = require("./inquirePrompts/tableAttributePrompts");
6
+
7
+ module.exports = {
8
+ mainPrompts,
9
+ extraDBPrompts,
10
+ routePrompts,
11
+ tablePrompts,
12
+ tableAttributePrompts
13
+ };
@@ -1,8 +1,11 @@
1
1
  const path = require("path");
2
2
  const fs = require("fs-extra");
3
+ const { generateModelContent, addTableToDB } = require('./tableHandlers/dbTableAddition')
4
+ const handleTableCreation = require('./tableHandlers/handleTableCreation')
5
+ const generateCreateStatement = require('./tableHandlers/createStatementGenerator')
3
6
 
4
7
 
5
- module.exports = async function setupExtraDB(targetDir, dbConfig) {
8
+ async function setupExtraDB(targetDir, dbConfig) {
6
9
  const backendDir = path.join(targetDir, "Backend");
7
10
  const modelsDir = path.join(backendDir, "Models");
8
11
  const dbDir = path.join(backendDir, "DB");
@@ -96,77 +99,11 @@ $2`
96
99
  console.log(`āœ… Database '${dbConfig.dbKey}' configured successfully.`);
97
100
  };
98
101
 
99
- function generateModelContent(dbConfig) {
100
- const modelName = dbConfig.tableName.charAt(0).toUpperCase() + dbConfig.tableName.slice(1);
101
-
102
- // Generate model content based on whether attributes were provided
103
- if (dbConfig.attributes && dbConfig.attributes.length > 0) {
104
- const attributesStr = dbConfig.attributes.map(attr => {
105
- let attrDef = ` ${attr.name}: {\n type: DataTypes.${attr.type}`;
106
-
107
- if (attr.length) {
108
- attrDef += `(${attr.length})`;
109
- }
110
-
111
- if (attr.primaryKey) {
112
- attrDef += `,\n primaryKey: true,\n autoIncrement: true`;
113
- }
114
-
115
- if (attr.allowNull === false) {
116
- attrDef += `,\n allowNull: false`;
117
- }
118
-
119
- if (attr.unique) {
120
- attrDef += `,\n unique: true`;
121
- }
122
-
123
- if (attr.defaultValue !== undefined) {
124
- attrDef += `,\n defaultValue: ${JSON.stringify(attr.defaultValue)}`;
125
- }
126
-
127
- attrDef += `\n }`;
128
- return attrDef;
129
- }).join(',\n');
130
-
131
- return `'use strict';
132
- module.exports = (sequelize, DataTypes) => {
133
- const ${modelName} = sequelize.define('${modelName}', {
134
- ${attributesStr}
135
- }, {
136
- tableName: '${dbConfig.tableName}',
137
- timestamps: true
138
- });
139
-
140
- ${modelName}.associate = function(models) {
141
- // Define associations here
142
- // Example: ${modelName}.belongsTo(models.OtherModel, { foreignKey: 'otherId' });
143
- };
144
-
145
- return ${modelName};
146
- };
147
- `;
148
- } else {
149
- // Simple model with just ID
150
- return `'use strict';
151
- module.exports = (sequelize, DataTypes) => {
152
- const ${modelName} = sequelize.define('${modelName}', {
153
- id: {
154
- type: DataTypes.INTEGER,
155
- primaryKey: true,
156
- autoIncrement: true
157
- }
158
- }, {
159
- tableName: '${dbConfig.tableName}',
160
- timestamps: true
161
- });
162
-
163
- ${modelName}.associate = function(models) {
164
- // Define associations here
165
- // Example: ${modelName}.belongsTo(models.OtherModel, { foreignKey: 'otherId' });
166
- };
167
-
168
- return ${modelName};
169
- };
170
- `;
171
- }
172
- }
102
+
103
+ module.exports = {
104
+ setupExtraDB,
105
+ addTableToDB,
106
+ handleTableCreation,
107
+ generateCreateStatement,
108
+ generateModelContent
109
+ }
@@ -0,0 +1,90 @@
1
+ function generateCreateStatement(tableName, attributes, dialect = 'mysql') {
2
+ const modelName = tableName.charAt(0).toUpperCase() + tableName.slice(1);
3
+
4
+ if (!attributes || attributes.length === 0) {
5
+ return `CREATE TABLE ${tableName} (
6
+ id INT PRIMARY KEY AUTO_INCREMENT,
7
+ createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
8
+ updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
9
+ );`;
10
+ }
11
+
12
+ const columns = attributes.map(attr => {
13
+ let colDef = ` ${attr.name} `;
14
+
15
+ // Map Sequelize types to SQL types
16
+ switch (attr.type) {
17
+ case 'INTEGER':
18
+ colDef += 'INT';
19
+ break;
20
+ case 'BIGINT':
21
+ colDef += 'BIGINT';
22
+ break;
23
+ case 'STRING':
24
+ colDef += `VARCHAR(${attr.length || 255})`;
25
+ break;
26
+ case 'TEXT':
27
+ colDef += 'TEXT';
28
+ break;
29
+ case 'BOOLEAN':
30
+ colDef += 'BOOLEAN';
31
+ break;
32
+ case 'DATE':
33
+ colDef += 'DATE';
34
+ break;
35
+ case 'DATEONLY':
36
+ colDef += 'DATE';
37
+ break;
38
+ case 'DECIMAL':
39
+ colDef += `DECIMAL(${attr.length || '10,2'})`;
40
+ break;
41
+ case 'FLOAT':
42
+ colDef += 'FLOAT';
43
+ break;
44
+ case 'DOUBLE':
45
+ colDef += 'DOUBLE';
46
+ break;
47
+ case 'UUID':
48
+ colDef += 'CHAR(36)';
49
+ break;
50
+ case 'JSON':
51
+ colDef += 'JSON';
52
+ break;
53
+ default:
54
+ colDef += attr.type;
55
+ }
56
+
57
+ if (attr.primaryKey) {
58
+ colDef += ' PRIMARY KEY';
59
+ if (attr.autoIncrement) {
60
+ colDef += ' AUTO_INCREMENT';
61
+ }
62
+ }
63
+
64
+ if (attr.allowNull === false && !attr.primaryKey) {
65
+ colDef += ' NOT NULL';
66
+ }
67
+
68
+ if (attr.unique && !attr.primaryKey) {
69
+ colDef += ' UNIQUE';
70
+ }
71
+
72
+ if (attr.defaultValue !== undefined) {
73
+ if (typeof attr.defaultValue === 'string') {
74
+ colDef += ` DEFAULT '${attr.defaultValue}'`;
75
+ } else {
76
+ colDef += ` DEFAULT ${attr.defaultValue}`;
77
+ }
78
+ }
79
+
80
+ return colDef;
81
+ }).join(',\n');
82
+
83
+ return `CREATE TABLE ${tableName} (
84
+ ${columns},
85
+ createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
86
+ updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
87
+ );`;
88
+ }
89
+
90
+ module.exports = generateCreateStatement;
@@ -0,0 +1,98 @@
1
+ const path = require("path");
2
+ const fs = require("fs-extra");
3
+
4
+ function generateModelContent(dbConfig) {
5
+ const modelName = dbConfig.tableName.charAt(0).toUpperCase() + dbConfig.tableName.slice(1);
6
+
7
+ // Generate model content based on whether attributes were provided
8
+ if (dbConfig.attributes && dbConfig.attributes.length > 0) {
9
+ const attributesStr = dbConfig.attributes.map(attr => {
10
+ let attrDef = ` ${attr.name}: {\n type: DataTypes.${attr.type}`;
11
+
12
+ if (attr.length) {
13
+ attrDef += `(${attr.length})`;
14
+ }
15
+
16
+ if (attr.primaryKey) {
17
+ attrDef += `,\n primaryKey: true,\n autoIncrement: true`;
18
+ }
19
+
20
+ if (attr.allowNull === false) {
21
+ attrDef += `,\n allowNull: false`;
22
+ }
23
+
24
+ if (attr.unique) {
25
+ attrDef += `,\n unique: true`;
26
+ }
27
+
28
+ if (attr.defaultValue !== undefined) {
29
+ attrDef += `,\n defaultValue: ${JSON.stringify(attr.defaultValue)}`;
30
+ }
31
+
32
+ attrDef += `\n }`;
33
+ return attrDef;
34
+ }).join(',\n');
35
+
36
+ return `'use strict';
37
+ module.exports = (sequelize, DataTypes) => {
38
+ const ${modelName} = sequelize.define('${modelName}', {
39
+ ${attributesStr}
40
+ }, {
41
+ tableName: '${dbConfig.tableName}',
42
+ timestamps: true
43
+ });
44
+
45
+ ${modelName}.associate = function(models) {
46
+ // Define associations here
47
+ // Example: ${modelName}.belongsTo(models.OtherModel, { foreignKey: 'otherId' });
48
+ };
49
+
50
+ return ${modelName};
51
+ };
52
+ `;
53
+ } else {
54
+ // Simple model with just ID
55
+ return `'use strict';
56
+ module.exports = (sequelize, DataTypes) => {
57
+ const ${modelName} = sequelize.define('${modelName}', {
58
+ id: {
59
+ type: DataTypes.INTEGER,
60
+ primaryKey: true,
61
+ autoIncrement: true
62
+ }
63
+ }, {
64
+ tableName: '${dbConfig.tableName}',
65
+ timestamps: true
66
+ });
67
+
68
+ ${modelName}.associate = function(models) {
69
+ // Define associations here
70
+ // Example: ${modelName}.belongsTo(models.OtherModel, { foreignKey: 'otherId' });
71
+ };
72
+
73
+ return ${modelName};
74
+ };
75
+ `;
76
+ }
77
+ }
78
+
79
+ // New function to add a table to an existing DB
80
+ async function addTableToDB(targetDir, dbKey, tableConfig) {
81
+ const backendDir = path.join(targetDir, "Backend");
82
+ const modelsDir = path.join(backendDir, "Models");
83
+ const dbModelsFolder = path.join(modelsDir, dbKey);
84
+
85
+ // Create the model file
86
+ const modelContent = generateModelContent(tableConfig);
87
+ const modelFileName = `${tableConfig.tableName}.js`;
88
+ const modelPath = path.join(dbModelsFolder, modelFileName);
89
+
90
+ await fs.writeFile(modelPath, modelContent);
91
+ console.log(`āœ… Table model '${tableConfig.tableName}' created in ${dbKey}/`);
92
+ };
93
+
94
+
95
+ module.exports = {
96
+ addTableToDB,
97
+ generateModelContent
98
+ }
@@ -0,0 +1,77 @@
1
+ const {tablePrompts} = require('../prompts')
2
+ const { addTableToDB } = require('./dbTableAddition')
3
+ const generateCreateStatement = require('./createStatementGenerator')
4
+ const inquirer = require("inquirer");
5
+
6
+ async function handleTableCreation(
7
+ targetDir,
8
+ dbKey,
9
+ firstTable = null,
10
+ dialect = 'mysql',
11
+ isNewDB = true
12
+ ) {
13
+ const createdTables = [];
14
+
15
+ // If this is a new DB and we have a first table from initial setup
16
+ if (isNewDB && firstTable) {
17
+ createdTables.push(firstTable);
18
+ }
19
+
20
+ // Ask if user wants to add more tables (or start adding if no firstTable)
21
+ let addMoreTables = true;
22
+
23
+ while (addMoreTables) {
24
+ // If we don't have a first table yet, or we're adding additional tables
25
+ if (!firstTable || createdTables.length > 0) {
26
+ // Ask if they want to add a table
27
+ const { wantToAdd } = await inquirer.prompt([
28
+ {
29
+ type: "confirm",
30
+ name: "wantToAdd",
31
+ message: createdTables.length === 0
32
+ ? "Do you want to create a table for this database?"
33
+ : "Do you want to add another table to this database?",
34
+ default: createdTables.length === 0 ? true : false
35
+ }
36
+ ]);
37
+
38
+ if (!wantToAdd) {
39
+ addMoreTables = false;
40
+ break;
41
+ }
42
+
43
+ const tableConfig = await tablePrompts();
44
+
45
+ // For additional tables (not the first one), use addTableToDB
46
+ if (isNewDB && createdTables.length === 0) {
47
+ // First table in new DB - already created by setupExtraDB, just track it
48
+ createdTables[0] = tableConfig;
49
+ } else {
50
+ // Additional tables - need to create them
51
+ await addTableToDB(targetDir, dbKey, tableConfig);
52
+ createdTables.push(tableConfig);
53
+ }
54
+ } else {
55
+ // We already have the first table, just break to ask for more
56
+ addMoreTables = false;
57
+ }
58
+ }
59
+
60
+ // Print CREATE statements for all tables
61
+ if (createdTables.length > 0) {
62
+ console.log(`\nšŸ“‹ CREATE Statements for your tables:\n`);
63
+ console.log(`${'='.repeat(60)}\n`);
64
+
65
+ createdTables.forEach((table, index) => {
66
+ console.log(`-- Table ${index + 1}: ${table.tableName}`);
67
+ console.log(generateCreateStatement(table.tableName, table.attributes, dialect));
68
+ console.log('');
69
+ });
70
+
71
+ console.log(`${'='.repeat(60)}\n`);
72
+ }
73
+
74
+ return createdTables;
75
+ };
76
+
77
+ module.exports = handleTableCreation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-fullstack-boilerplate",
3
- "version": "2.2.2",
3
+ "version": "3.0.0",
4
4
  "description": "A powerful CLI tool to generate fully configured full-stack applications with React frontend and Express backend. Includes authentication middleware, database configuration, protected routes, route-based Axios instances, and encryption setup - all ready with a single command.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -2,9 +2,9 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/PMDLogo.png" />
5
+ <link rel="icon" type="image/svg+xml" href="/pp.jpg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>PMD - Reports</title>
7
+ <title>Full Stack Boilerplate</title>
8
8
  </head>
9
9
  <body>
10
10
  <div id="root"></div>
@@ -6,7 +6,7 @@ const SpinningLoader = ({ size = 10 }) => {
6
6
  return (
7
7
  <div className={`flex flex-col justify-center items-center gap-4 h-full w-full ${isDark ? 'bg-gray-900' : 'bg-white'}`}>
8
8
  <img
9
- src={'/PMDLogo.png'}
9
+ src={'/load.webp'}
10
10
  alt="Loading..."
11
11
  style={{ width: "auto", height: size }}
12
12
  className="animate-pulse"
@@ -69,7 +69,7 @@ const Login = () => {
69
69
  {/* Header */}
70
70
  <div className="px-6 pt-6 pb-4 text-center">
71
71
  <div className="relative mb-4">
72
- <img src={'/PMDLogo.png'} alt="PMD Logo" className='w-max h-14 mx-auto' />
72
+ <img src={'/pp.jpg'} alt="PMD Logo" className='w-max h-20 rounded-full shadow-md shadow-gray-400 mx-auto' />
73
73
  </div>
74
74
 
75
75
  <h1 className="text-2xl font-articulatcf-demibold text-gray-800 mb-1">Welcome Back</h1>