@gnar-engine/cli 1.0.4 → 1.0.6

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 (153) hide show
  1. package/bootstrap/deploy.localdev.yml +44 -3
  2. package/bootstrap/secrets.localdev.yml +20 -5
  3. package/bootstrap/services/control/src/config.js +4 -0
  4. package/bootstrap/services/notification/Dockerfile +2 -2
  5. package/bootstrap/services/notification/package.json +14 -32
  6. package/bootstrap/services/notification/src/app.js +50 -48
  7. package/bootstrap/services/notification/src/commands/notification.handler.js +96 -0
  8. package/bootstrap/services/notification/src/config.js +55 -12
  9. package/bootstrap/services/notification/src/controllers/http.controller.js +87 -0
  10. package/bootstrap/services/notification/src/controllers/message.controller.js +39 -70
  11. package/bootstrap/services/notification/src/db/migrations/01-init.js +50 -0
  12. package/bootstrap/services/notification/src/db/migrations/02-notification-service-init.js +23 -0
  13. package/bootstrap/services/notification/src/policies/notification.policy.js +49 -0
  14. package/bootstrap/services/notification/src/schema/notification.schema.js +17 -0
  15. package/bootstrap/services/notification/src/services/notification.service.js +32 -0
  16. package/bootstrap/services/page/Dockerfile +23 -0
  17. package/bootstrap/services/page/package.json +16 -0
  18. package/bootstrap/services/page/src/app.js +50 -0
  19. package/bootstrap/services/page/src/commands/block.handler.js +94 -0
  20. package/bootstrap/services/page/src/commands/page.handler.js +167 -0
  21. package/bootstrap/services/page/src/config.js +62 -0
  22. package/bootstrap/services/page/src/controllers/block.http.controller.js +87 -0
  23. package/bootstrap/services/page/src/controllers/message.controller.js +51 -0
  24. package/bootstrap/services/page/src/controllers/page.http.controller.js +89 -0
  25. package/bootstrap/services/page/src/policies/block.policy.js +50 -0
  26. package/bootstrap/services/page/src/policies/page.policy.js +49 -0
  27. package/bootstrap/services/page/src/schema/page.schema.js +139 -0
  28. package/bootstrap/services/page/src/services/block.service.js +83 -0
  29. package/bootstrap/services/page/src/services/page.service.js +83 -0
  30. package/bootstrap/services/portal/Dockerfile +20 -0
  31. package/bootstrap/services/portal/README.md +73 -0
  32. package/bootstrap/services/portal/index.html +13 -0
  33. package/bootstrap/services/portal/nginx.conf +5 -0
  34. package/bootstrap/services/portal/package.json +33 -0
  35. package/bootstrap/services/portal/public/vite.svg +1 -0
  36. package/bootstrap/services/portal/react-router.config.js +7 -0
  37. package/bootstrap/services/portal/src/App.jsx +16 -0
  38. package/bootstrap/services/portal/src/assets/gnar-engine-white-logo.svg +9 -0
  39. package/bootstrap/services/portal/src/assets/icon-agent.svg +6 -0
  40. package/bootstrap/services/portal/src/assets/icon-cog.svg +4 -0
  41. package/bootstrap/services/portal/src/assets/icon-delete.svg +3 -0
  42. package/bootstrap/services/portal/src/assets/icon-home.svg +3 -0
  43. package/bootstrap/services/portal/src/assets/icon-padlock.svg +3 -0
  44. package/bootstrap/services/portal/src/assets/icon-page.svg +6 -0
  45. package/bootstrap/services/portal/src/assets/icon-reports.svg +3 -0
  46. package/bootstrap/services/portal/src/assets/icon-user.svg +3 -0
  47. package/bootstrap/services/portal/src/assets/icon-users.svg +3 -0
  48. package/bootstrap/services/portal/src/assets/login-green-rad-back-1.jpg +0 -0
  49. package/bootstrap/services/portal/src/assets/react.svg +1 -0
  50. package/bootstrap/services/portal/src/components/CrudList/CrudList.jsx +85 -0
  51. package/bootstrap/services/portal/src/components/CrudList/CrudList.less +59 -0
  52. package/bootstrap/services/portal/src/components/CustomSelect/CustomSelect.jsx +81 -0
  53. package/bootstrap/services/portal/src/components/LoginForm/LoginForm.jsx +58 -0
  54. package/bootstrap/services/portal/src/components/PageBlockSwitch/PageBlockSwitch.jsx +129 -0
  55. package/bootstrap/services/portal/src/components/Sidebar/Sidebar.jsx +33 -0
  56. package/bootstrap/services/portal/src/components/Sidebar/Sidebar.less +37 -0
  57. package/bootstrap/services/portal/src/components/Topbar/Topbar.jsx +19 -0
  58. package/bootstrap/services/portal/src/components/Topbar/Topbar.less +22 -0
  59. package/bootstrap/services/portal/src/components/UserInfo/UserInfo.jsx +33 -0
  60. package/bootstrap/services/portal/src/components/UserInfo/UserInfo.less +21 -0
  61. package/bootstrap/services/portal/src/css/style.css +711 -0
  62. package/bootstrap/services/portal/src/data/pages.data.js +10 -0
  63. package/bootstrap/services/portal/src/elements/CustomSelect/CustomSelect.jsx +65 -0
  64. package/bootstrap/services/portal/src/elements/CustomSelect/CustomSelect.less +102 -0
  65. package/bootstrap/services/portal/src/elements/ImageInput/ImageInput.jsx +115 -0
  66. package/bootstrap/services/portal/src/elements/ImageInput/ImageInput.less +43 -0
  67. package/bootstrap/services/portal/src/elements/ImageMultiInput/ImageMultiInput.jsx +124 -0
  68. package/bootstrap/services/portal/src/elements/ImageMultiInput/ImageMultiInput.less +0 -0
  69. package/bootstrap/services/portal/src/elements/Repeater/Repeater.jsx +52 -0
  70. package/bootstrap/services/portal/src/elements/Repeater/Repeater.less +70 -0
  71. package/bootstrap/services/portal/src/elements/RichTextInput/RichTextInput.jsx +18 -0
  72. package/bootstrap/services/portal/src/elements/RichTextInput/RichTextInput.less +37 -0
  73. package/bootstrap/services/portal/src/elements/SaveButton/SaveButton.jsx +45 -0
  74. package/bootstrap/services/portal/src/elements/SelectRepeater/SelectRepeater.jsx +63 -0
  75. package/bootstrap/services/portal/src/elements/SelectRepeater/SelectRepeater.less +23 -0
  76. package/bootstrap/services/portal/src/elements/TextInput/TextInput.jsx +17 -0
  77. package/bootstrap/services/portal/src/layouts/Card/Card.jsx +15 -0
  78. package/bootstrap/services/portal/src/layouts/PortalLayout/PortalLayout.jsx +29 -0
  79. package/bootstrap/services/portal/src/layouts/PortalLayout/PortalLayout.less +49 -0
  80. package/bootstrap/services/portal/src/main.jsx +51 -0
  81. package/bootstrap/services/portal/src/pages/BlockSinglePage/BlockSinglePage.jsx +277 -0
  82. package/bootstrap/services/portal/src/pages/BlocksPage/BlocksPage.jsx +23 -0
  83. package/bootstrap/services/portal/src/pages/DashboardPage/DashboardPage.jsx +11 -0
  84. package/bootstrap/services/portal/src/pages/DashboardPage/DashboardPage.less +0 -0
  85. package/bootstrap/services/portal/src/pages/LoginPage/LoginPage.jsx +21 -0
  86. package/bootstrap/services/portal/src/pages/LoginPage/LoginPage.less +51 -0
  87. package/bootstrap/services/portal/src/pages/PageSinglePage/PageSinglePage.jsx +338 -0
  88. package/bootstrap/services/portal/src/pages/PagesPage/PagesPage.jsx +23 -0
  89. package/bootstrap/services/portal/src/pages/UserSinglePage/UserSinglePage.jsx +9 -0
  90. package/bootstrap/services/portal/src/pages/UserSinglePage/UserSinglePage.less +0 -0
  91. package/bootstrap/services/portal/src/pages/UsersPage/UsersPage.jsx +25 -0
  92. package/bootstrap/services/portal/src/pages/UsersPage/UsersPage.less +0 -0
  93. package/bootstrap/services/portal/src/services/block.js +28 -0
  94. package/bootstrap/services/portal/src/services/client.js +70 -0
  95. package/bootstrap/services/portal/src/services/gravatar.js +14 -0
  96. package/bootstrap/services/portal/src/services/page.js +28 -0
  97. package/bootstrap/services/portal/src/services/storage.js +62 -0
  98. package/bootstrap/services/portal/src/services/user.js +41 -0
  99. package/bootstrap/services/portal/src/slices/authSlice.js +101 -0
  100. package/bootstrap/services/portal/src/store/configureStore.js +10 -0
  101. package/bootstrap/services/portal/src/style/cards.less +57 -0
  102. package/bootstrap/services/portal/src/style/global.less +204 -0
  103. package/bootstrap/services/portal/src/style/icons.less +21 -0
  104. package/bootstrap/services/portal/src/style/inputs.less +52 -0
  105. package/bootstrap/services/portal/src/style/main.less +28 -0
  106. package/bootstrap/services/portal/src/utils/utils.js +9 -0
  107. package/bootstrap/services/portal/vite.config.js +12 -0
  108. package/bootstrap/services/user/src/app.js +6 -1
  109. package/bootstrap/services/user/src/commands/user.handler.js +35 -21
  110. package/bootstrap/services/user/src/config.js +5 -1
  111. package/bootstrap/services/user/src/policies/user.policy.js +3 -1
  112. package/bootstrap/services/user/src/tests/commands/user.test.js +31 -0
  113. package/install-from-clone.sh +30 -0
  114. package/package.json +1 -1
  115. package/src/cli.js +2 -0
  116. package/src/config.js +8 -0
  117. package/src/dev/commands.js +11 -3
  118. package/src/dev/dev.service.js +164 -64
  119. package/src/helpers/helpers.js +24 -0
  120. package/src/profiles/command.js +41 -0
  121. package/src/profiles/profiles.client.js +23 -0
  122. package/src/provisioner/Dockerfile +27 -0
  123. package/src/provisioner/package.json +19 -0
  124. package/src/provisioner/src/app.js +56 -0
  125. package/src/provisioner/src/services/mongodb.js +58 -0
  126. package/src/provisioner/src/services/mysql.js +51 -0
  127. package/src/provisioner/src/services/secrets.js +84 -0
  128. package/src/scaffolder/commands.js +58 -2
  129. package/src/scaffolder/scaffolder.handler.js +164 -72
  130. package/templates/entity/src/commands/{{entityName}}.handler.js.hbs +94 -0
  131. package/templates/entity/src/controllers/{{entityName}}.http.controller.js.hbs +87 -0
  132. package/templates/entity/src/mysql.db/migrations/03-{{entityName}}-entity-init.js.hbs +23 -0
  133. package/templates/entity/src/policies/{{entityName}}.policy.js.hbs +49 -0
  134. package/templates/entity/src/schema/{{entityName}}.schema.js.hbs +17 -0
  135. package/templates/entity/src/services/mongodb.{{entityName}}.service.js.hbs +70 -0
  136. package/templates/entity/src/services/mysql.{{entityName}}.service.js.hbs +27 -0
  137. package/templates/service/src/app.js.hbs +12 -1
  138. package/templates/service/src/commands/{{serviceName}}.handler.js.hbs +1 -1
  139. package/templates/service/src/mongodb.config.js.hbs +5 -1
  140. package/templates/service/src/mysql.config.js.hbs +4 -0
  141. package/bootstrap/services/notification/Dockerfile.prod +0 -37
  142. package/bootstrap/services/notification/README.md +0 -3
  143. package/bootstrap/services/notification/src/commands/command-bus.js +0 -20
  144. package/bootstrap/services/notification/src/commands/handlers/control.handler.js +0 -18
  145. package/bootstrap/services/notification/src/commands/handlers/notification.handler.js +0 -157
  146. package/bootstrap/services/notification/src/services/logger.service.js +0 -16
  147. package/bootstrap/services/notification/src/services/ses.service.js +0 -23
  148. package/bootstrap/services/notification/src/templates/admin-order-recieved.hbs +0 -136
  149. package/bootstrap/services/notification/src/templates/admin-subscription-failed.hbs +0 -87
  150. package/bootstrap/services/notification/src/templates/customer-order-recieved.hbs +0 -132
  151. package/bootstrap/services/notification/src/templates/customer-subscription-failed.hbs +0 -77
  152. package/bootstrap/services/user/src/tests/user.test.js +0 -126
  153. /package/bootstrap/services/{notification/src/tests/notification.test.js → portal/src/components/CustomSelect/CustomSelect.less} +0 -0
@@ -0,0 +1,56 @@
1
+ import { mysqlService } from './services/mysql.js';
2
+ import { mongoService } from './services/mongodb.js';
3
+ import { secrets } from './services/secrets.js';
4
+
5
+ /**
6
+ * Initialise service
7
+ */
8
+ export const initService = async () => {
9
+
10
+ console.log('G n a r E n g i n e | Provisioner provisioning databases...');
11
+
12
+ let provisionerSecrets;
13
+
14
+ // get all secrets
15
+ try {
16
+ provisionerSecrets = JSON.parse(process.env.PROVISIONER_SECRETS);
17
+ } catch (error) {
18
+ console.error('Error parsing provisioner secrets', error);
19
+ return;
20
+ }
21
+
22
+ // collate databases to provision from secrets
23
+ const mysqlDatabases = secrets.collateMysqlDatabases(provisionerSecrets);
24
+ const mongoDatabases = secrets.collateMongoDatabases(provisionerSecrets);
25
+
26
+ // provision mysql databases
27
+ if (mysqlDatabases) {
28
+ for (const [key, value] of Object.entries(mysqlDatabases)) {
29
+ mysqlService.provisionDatabase({
30
+ host: value.host,
31
+ database: value.database,
32
+ user: value.user,
33
+ password: value.password,
34
+ rootPassword: provisionerSecrets.provision.MYSQL_ROOT_PASSWORD
35
+ });
36
+ }
37
+ } else {
38
+ console.log('No MySQL databases to provision.');
39
+ }
40
+
41
+ if (mongoDatabases) {
42
+ for (const [key, value] of Object.entries(mongoDatabases)) {
43
+ mongoService.provisionDatabase({
44
+ host: value.host,
45
+ database: value.database,
46
+ user: value.user,
47
+ password: value.password,
48
+ rootPassword: provisionerSecrets.provision.MONGO_ROOT_PASSWORD
49
+ })
50
+ }
51
+ } else {
52
+ console.log('No MongoDB databases to provision.');
53
+ }
54
+ }
55
+
56
+ initService();
@@ -0,0 +1,58 @@
1
+ import { MongoClient } from 'mongodb';
2
+
3
+ const retryInterval = 5000;
4
+ const maxRetries = 5;
5
+
6
+ let db;
7
+
8
+ export const mongoService = {
9
+
10
+ /**
11
+ * Provision database and users
12
+ *
13
+ * @param {Object} params - The parameters object
14
+ * @param {string} host - The database host
15
+ * @param {string} database - The database name
16
+ * @param {string} user - The database user
17
+ * @param {string} password - The database user password
18
+ * @param {string} rootPassword - The root user password
19
+ * @param {number} [port=27017] - The database port
20
+ */
21
+ provisionDatabase: async ({host, database, user, password, rootPassword, port = 27017}) => {
22
+
23
+ const connectionUrl = `mongodb://root:${rootPassword}@${host}:${port}/admin`;
24
+ let retries = 0;
25
+
26
+ while (retries < maxRetries) {
27
+ try {
28
+ const dbClient = await MongoClient.connect(connectionUrl);
29
+ db = dbClient.db(database);
30
+
31
+ const existingUsers = await db.command({ usersInfo: 1 });
32
+
33
+ if (!existingUsers.users.some(u => u.user === user)) {
34
+ await db.command({
35
+ createUser: user,
36
+ pwd: password,
37
+ roles: [{ role: "readWrite", db: database }]
38
+ });
39
+ }
40
+
41
+ console.log(`Successfully provisioned MongoDB database: ${database} and user: ${user}`);
42
+ await dbClient.close();
43
+ return;
44
+
45
+ } catch (error) {
46
+ console.error(`Failed provisioning Mongo database "${database}" for user "${user}" ": ${error.message}`);
47
+ retries++;
48
+
49
+ if (retries >= maxRetries) {
50
+ console.error(`Max retries reached. Could not provision database "${database}".`);
51
+ return;
52
+ }
53
+
54
+ await new Promise(resolve => setTimeout(resolve, retryInterval));
55
+ }
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,51 @@
1
+ import mysql from 'mysql2/promise';
2
+
3
+ const retryInterval = 5000;
4
+ const maxRetries = 5;
5
+
6
+ export const mysqlService = {
7
+
8
+ /**
9
+ * Provision database and users
10
+ *
11
+ * @param {Object} params - The parameters object
12
+ * @param {string} host - The database host
13
+ * @param {string} database - The database name
14
+ * @param {string} user - The database user
15
+ * @param {string} password - The database user password
16
+ * @param {string} rootPassword - The root user password
17
+ */
18
+ provisionDatabase: async ({host, database, user, password, rootPassword}) => {
19
+ let retries = 0;
20
+
21
+ while (retries < maxRetries) {
22
+ try {
23
+ const conn = await mysql.createConnection({
24
+ host: host || 'db-mysql',
25
+ user: 'root',
26
+ password: rootPassword
27
+ });
28
+
29
+ await conn.query(`CREATE DATABASE IF NOT EXISTS ${mysql.escapeId(database)};`);
30
+ await conn.query(`CREATE USER IF NOT EXISTS ${mysql.escape(user)}@'%' IDENTIFIED BY ${mysql.escape(password)};`);
31
+ await conn.query(`GRANT ALL PRIVILEGES ON ${mysql.escapeId(database)}.* TO ${mysql.escape(user)}@'%';`);
32
+ await conn.query(`FLUSH PRIVILEGES;`);
33
+
34
+ console.log(`Successfully provisioned MySQL database: ${database} and user: ${user}`);
35
+ await conn.end();
36
+ return;
37
+
38
+ } catch (error) {
39
+ console.error(`Failed provisioning MySQL database "${database}" for user "${user}" ": ${error.message}`);
40
+ retries++;
41
+
42
+ if (retries >= maxRetries) {
43
+ console.error(`Max retries reached. Could not provision database "${database}".`);
44
+ return;
45
+ }
46
+
47
+ await new Promise(resolve => setTimeout(resolve, retryInterval));
48
+ }
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,84 @@
1
+
2
+
3
+ export const secrets = {
4
+
5
+ /**
6
+ * Collate MySQL databases from provisioner secrets
7
+ *
8
+ * @param {Object} provisionerSecrets - The provisioner secrets object
9
+ * @returns {Object} - Collated MySQL databases
10
+ */
11
+ collateMysqlDatabases: (provisionerSecrets) => {
12
+
13
+ const mysqlDatabases = {};
14
+
15
+ for (const [serviceKey, service] of Object.entries(provisionerSecrets.services)) {
16
+ for (const key of Object.keys(service)) {
17
+ if (key.startsWith('MYSQL_')) {
18
+ mysqlDatabases[serviceKey] = true;
19
+ break;
20
+ }
21
+ }
22
+ }
23
+
24
+ for (const [key, value] of Object.entries(mysqlDatabases)) {
25
+ try {
26
+ mysqlDatabases[key] = {};
27
+ mysqlDatabases[key].host = provisionerSecrets.services[key].MYSQL_HOST;
28
+ mysqlDatabases[key].database = provisionerSecrets.services[key].MYSQL_DATABASE;
29
+ mysqlDatabases[key].user = provisionerSecrets.services[key].MYSQL_USER;
30
+ mysqlDatabases[key].password = provisionerSecrets.services[key].MYSQL_PASSWORD;
31
+ } catch (error) {
32
+ console.error(`Missing database credentials for ${key} service. Please include: MYSQL_DATABASE, MYSQL_USER, and MYSQL_PASSWORD.`);
33
+ return;
34
+ }
35
+ }
36
+
37
+ if (!provisionerSecrets.provision.MYSQL_ROOT_PASSWORD) {
38
+ console.error('Missing MYSQL_ROOT_PASSWORD in provisioner secrets. Cannot provision databases.');
39
+ return;
40
+ }
41
+
42
+ return mysqlDatabases;
43
+ },
44
+
45
+ /**
46
+ * Collate MongoDB databases from provisioner secrets
47
+ *
48
+ * @param {Object} provisionerSecrets - The provisioner secrets object
49
+ * @returns {Object} - Collated MongoDB databases
50
+ */
51
+ collateMongoDatabases: (provisionerSecrets) => {
52
+
53
+ const mongoDatabases = {};
54
+
55
+ for (const [serviceKey, service] of Object.entries(provisionerSecrets.services)) {
56
+ for (const key of Object.keys(service)) {
57
+ if (key.startsWith('MONGO_')) {
58
+ mongoDatabases[serviceKey] = true;
59
+ break;
60
+ }
61
+ }
62
+ }
63
+
64
+ for (const [key, value] of Object.entries(mongoDatabases)) {
65
+ try {
66
+ mongoDatabases[key] = {};
67
+ mongoDatabases[key].host = provisionerSecrets.services[key].MONGO_HOST;
68
+ mongoDatabases[key].database = provisionerSecrets.services[key].MONGO_DATABASE;
69
+ mongoDatabases[key].user = provisionerSecrets.services[key].MONGO_USER;
70
+ mongoDatabases[key].password = provisionerSecrets.services[key].MONGO_PASSWORD;
71
+ } catch (error) {
72
+ console.error(`Missing database credentials for ${key} service. Please include: MONGO_DATABASE, MONGO_USER, and MONGO_PASSWORD.`);
73
+ return;
74
+ }
75
+ }
76
+
77
+ if (!provisionerSecrets.provision.MONGO_ROOT_PASSWORD) {
78
+ console.error('Missing MONGO_ROOT_PASSWORD in provisioner secrets. Cannot provision databases.');
79
+ return;
80
+ }
81
+
82
+ return mongoDatabases;
83
+ }
84
+ }
@@ -1,6 +1,7 @@
1
1
  import inquirer from 'inquirer';
2
2
  import { profiles } from '../profiles/profiles.client.js';
3
3
  import { scaffolder } from './scaffolder.handler.js';
4
+ import { helpers } from '../helpers/helpers.js';
4
5
  import path from 'path';
5
6
 
6
7
  export const registerScaffolderCommands = (program) => {
@@ -33,11 +34,16 @@ export const registerScaffolderCommands = (program) => {
33
34
  }
34
35
  ]);
35
36
 
37
+ // validate absolute path, if it is not absolute, make it absolute
38
+ if (!path.isAbsolute(answers.projectDir)) {
39
+ answers.projectDir = path.join(process.cwd(), answers.projectDir);
40
+ }
41
+
36
42
  // create the project
37
43
  try {
38
44
  scaffolder.createNewProject({
39
45
  projectName: projectName,
40
- projectDir: answers.projectDir,
46
+ projectDir: path.join('/', answers.projectDir),
41
47
  rootAdminEmail: answers.rootAdminEmail
42
48
  });
43
49
  } catch (error) {
@@ -119,7 +125,7 @@ export const registerScaffolderCommands = (program) => {
119
125
  console.log('Creating new service in... ' + activeProfile.profile.PROJECT_DIR);
120
126
 
121
127
  scaffolder.createNewFrontEndService({
122
- serviceName: options.service,
128
+ serviceName: service,
123
129
  projectDir: activeProfile.profile.PROJECT_DIR
124
130
  });
125
131
  } catch (error) {
@@ -127,4 +133,54 @@ export const registerScaffolderCommands = (program) => {
127
133
  }
128
134
  }
129
135
  });
136
+
137
+ create
138
+ .command('entity <entity>')
139
+ .description('📦 Create a new entity in an existing service')
140
+ .option('--in-service <serviceName>', 'The service in which to add the entity')
141
+ .action(async (entity, options) => {
142
+ // validate
143
+ if (!entity) {
144
+ console.error('❌ Please specify an entity name using gnar create entity <entityName> --in-service <serviceName>');
145
+ }
146
+ if (!options.inService) {
147
+ console.error('❌ Please specify the service using --in-service <serviceName>');
148
+ }
149
+
150
+ let activeProfile;
151
+ try {
152
+ activeProfile = profiles.getActiveProfile();
153
+ } catch (error) {
154
+ console.error('❌ No active profile found. Please create or set one using `gnar profile create` or `gnar profile set-active <profileName>`');
155
+ return;
156
+ }
157
+
158
+ // create the entity
159
+ try {
160
+ // add trailing slash to project dir if missing
161
+ let projectDir = activeProfile.profile.PROJECT_DIR;
162
+
163
+ if (!activeProfile.profile.PROJECT_DIR.endsWith(path.sep)) {
164
+ projectDir += path.sep;
165
+ }
166
+
167
+ const dbType = await helpers.getDbTypeFromSecrets(options.inService, projectDir);
168
+ const serviceDir = path.join(projectDir, 'services', options.inService.toLowerCase());
169
+
170
+ console.log('Creating new entity in... ' + serviceDir);
171
+
172
+ scaffolder.createNewEntity({
173
+ entityName: entity,
174
+ inService: options.inService,
175
+ serviceDir: serviceDir,
176
+ database: dbType
177
+ });
178
+
179
+ console.log('Created entity ' + entity + ' in service ' + options.inService);
180
+ console.log('👉 Remember to add the new entities handler and controllers to your service\'s app.js');
181
+
182
+ } catch (error) {
183
+ console.error('❌ Error creating entity:', error.message);
184
+ }
185
+ });
130
186
  }
@@ -3,8 +3,11 @@ import fs from 'fs';
3
3
  import yaml from 'js-yaml';
4
4
  import { profiles } from '../profiles/profiles.client.js';
5
5
  import { helpers } from '../helpers/helpers.js';
6
+ import { directories } from '../config.js';
6
7
  import Handlebars from 'handlebars';
7
8
 
9
+
10
+
8
11
  /**
9
12
  * Gnar Engine Scaffolder
10
13
  */
@@ -32,69 +35,22 @@ export const scaffolder = {
32
35
  fs.mkdirSync(serviceDir, { recursive: true });
33
36
 
34
37
  // Get all files in the templates directory
35
- const templatesDir = path.join(import.meta.dirname, '../../templates/service');
36
38
  const templateFiles = scaffolder.getAllTemplateFiles({
37
- dir: templatesDir,
38
- baseDir: templatesDir
39
+ dir: directories.scaffolderServiceTemplates,
40
+ baseDir: directories.scaffolderServiceTemplates
39
41
  });
40
42
 
41
- // Register Handlebars helpers
42
- Object.entries(helpers).forEach(([name, fn]) => {
43
- Handlebars.registerHelper(name, fn);
44
- });
45
-
46
- // Write the files to the service directory
47
- templateFiles.forEach(file => {
48
- let sourcePath;
49
- let targetPath;
50
- const templateArgs = {
51
- serviceName,
52
- database
53
- };
54
-
55
- let fileRelativePath = file.relativePath;
56
-
57
- // Database specific
58
- if (fileRelativePath.includes('mongodb.')) {
59
- if (database !== 'mongodb') {
60
- return;
61
- } else {
62
- fileRelativePath = fileRelativePath.replace('mongodb.', '');
63
- }
64
- }
65
-
66
- if (fileRelativePath.includes('mysql.')) {
67
- if (database !== 'mysql') {
68
- return;
69
- } else {
70
- fileRelativePath = fileRelativePath.replace('mysql.', '');
71
- }
72
- }
43
+ // scaffold the hbs templates
44
+ const templateArgs = {
45
+ serviceName,
46
+ database
47
+ };
73
48
 
74
- switch (file.extension) {
75
- case '.hbs':
76
- // Compile the Handlebars template for content
77
- const templateContent = fs.readFileSync(file.fullPath, 'utf8');
78
- const compiledTemplate = Handlebars.compile(templateContent);
79
- const renderedContent = compiledTemplate(templateArgs);
80
-
81
- // Compile the Handlebars template for the filename (excluding .hbs)
82
- const filenameTemplate = Handlebars.compile(fileRelativePath.replace(/\.hbs$/, ''));
83
- const renderedFilename = filenameTemplate(templateArgs);
84
- targetPath = path.join(serviceDir, renderedFilename);
85
-
86
- // Ensure directory exists
87
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
88
- fs.writeFileSync(targetPath, renderedContent, 'utf8');
89
- break;
90
- default:
91
- // By default, copy the file to the service directory
92
- sourcePath = file.fullPath;
93
- targetPath = path.join(serviceDir, fileRelativePath);
94
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
95
- fs.copyFileSync(sourcePath, targetPath);
96
- break;
97
- }
49
+ scaffolder.scaffoldHandlebarTemplates({
50
+ templateFiles: templateFiles,
51
+ serviceDir: serviceDir,
52
+ database: database,
53
+ templateArgs: templateArgs
98
54
  });
99
55
 
100
56
  // Scaffold deploy.yml
@@ -136,6 +92,25 @@ export const scaffolder = {
136
92
 
137
93
  // Create the service directory
138
94
  fs.mkdirSync(serviceDir, { recursive: true });
95
+
96
+ // Add to deploy.yml
97
+ scaffolder.scaffoldServiceDeployYml({
98
+ deployPath: path.join(projectDir, 'deploy.localdev.yml'),
99
+ serviceName: serviceName,
100
+ database: null
101
+ });
102
+
103
+ // Scaffold secrets
104
+ scaffolder.scaffoldServiceSecrets({
105
+ secretsPath: path.join(projectDir, 'secrets.localdev.yml'),
106
+ serviceName: serviceName,
107
+ database: null
108
+ });
109
+
110
+ return {
111
+ message: `Front-end service "${serviceName}" created successfully at ${serviceDir}`,
112
+ servicePath: serviceDir
113
+ };
139
114
  },
140
115
 
141
116
  /**
@@ -152,8 +127,10 @@ export const scaffolder = {
152
127
 
153
128
  entries.forEach(entry => {
154
129
  const fullPath = path.join(dir, entry.name);
130
+ // if the full path contains src, the 'data' directory can be included
131
+ const includeDataDir = fullPath.includes(`${path.sep}src${path.sep}`) && fullPath.includes('data') ? true : false;
155
132
  if (entry.isDirectory()) {
156
- if (entry.name !== 'node_modules' && entry.name !== 'data') {
133
+ if (entry.name !== 'node_modules' && (entry.name !== 'data' || includeDataDir)) {
157
134
  if (!entry.name.startsWith('.') || entry.name == '.gnarengine') {
158
135
  scaffolder.getAllTemplateFiles({
159
136
  dir: fullPath,
@@ -284,13 +261,17 @@ export const scaffolder = {
284
261
  const parsedSecrets = yaml.load(rawSecrets);
285
262
 
286
263
  // generate random passwords
287
- Object.keys(parsedSecrets.services).forEach(serviceName => {
288
- Object.keys(parsedSecrets.services[serviceName]).forEach(key => {
289
- if (key.toLowerCase().includes('pass')) {
290
- parsedSecrets.services[serviceName][key] = helpers.generateRandomString(16);
291
- }
264
+ try {
265
+ Object.keys(parsedSecrets.services).forEach(serviceName => {
266
+ Object.keys(parsedSecrets.services[serviceName]).forEach(key => {
267
+ if (key.toLowerCase().includes('pass')) {
268
+ parsedSecrets.services[serviceName][key] = helpers.generateRandomString(16);
269
+ }
270
+ });
292
271
  });
293
- });
272
+ } catch (error) {
273
+ throw new Error('Error generating random passwords for project secrets: ' + error.message);
274
+ }
294
275
 
295
276
  // set random root api key
296
277
  const cliApiKey = helpers.generateRandomString(32);
@@ -328,15 +309,14 @@ export const scaffolder = {
328
309
  parsedSecrets.services[serviceName]['MYSQL_USER'] = serviceName + '_user';
329
310
  parsedSecrets.services[serviceName]['MYSQL_PASSWORD'] = helpers.generateRandomString(16);
330
311
  parsedSecrets.services[serviceName]['MYSQL_DATABASE'] = serviceName + '_db';
331
- parsedSecrets.services[serviceName]['MYSQL_HOST'] = serviceName + '-db';
332
- parsedSecrets.services[serviceName]['MYSQL_RANDOM_ROOT_PASSWORD'] = helpers.generateRandomString(16);
312
+ parsedSecrets.services[serviceName]['MYSQL_HOST'] = 'db-mysql';
333
313
  break;
334
314
  case 'mongodb':
335
315
  const mongoPassword = helpers.generateRandomString(16);
336
316
  const mongoRootPassword = helpers.generateRandomString(16);
337
317
  const mongoUser = serviceName + '_user';
338
318
  const mongoDatabase = serviceName + '_db';
339
- const mongoHost = serviceName + '-db';
319
+ const mongoHost = 'db-mongo';
340
320
  const mongoUrl = `mongodb://${mongoUser}:${mongoPassword}@${mongoHost}:27017/${mongoDatabase}`;
341
321
 
342
322
  parsedSecrets.services[serviceName]['MONGO_URL'] = mongoUrl;
@@ -347,8 +327,9 @@ export const scaffolder = {
347
327
  parsedSecrets.services[serviceName]['MONGO_HOST'] = mongoHost;
348
328
  break;
349
329
  default:
350
- throw new Error(`Unsupported database type in secret scaffolder: ${database}`);
351
- }
330
+ // no db
331
+ break;
332
+ }
352
333
 
353
334
  // save updated secrets file
354
335
  const newSecretsContent = yaml.dump(parsedSecrets);
@@ -411,7 +392,7 @@ export const scaffolder = {
411
392
  // add database service if required
412
393
  if (database) {
413
394
  serviceConfig.depends_on = [
414
- `${serviceName}-db`
395
+ `db-${database}`
415
396
  ]
416
397
  }
417
398
 
@@ -421,5 +402,116 @@ export const scaffolder = {
421
402
  // write deploy.yml file
422
403
  const deployYmlContent = yaml.dump(deploy);
423
404
  fs.writeFileSync(deployPath, deployYmlContent, 'utf8');
405
+ },
406
+
407
+ /**
408
+ * Create new entity in existing service
409
+ *
410
+ * @param {object} param
411
+ * @param {string} param.entityName - The name of the entity to create
412
+ * @param {string} param.inService - The service in which to add the entity
413
+ * @param {string} param.serviceDir - The service directory where the entity will be created
414
+ * @param {string} param.database - The database type (e.g., 'mysql', 'mongodb')
415
+ * @returns {object} - An object containing a success message
416
+ */
417
+ createNewEntity: ({ entityName, inService, serviceDir, database }) => {
418
+
419
+ entityName = entityName.toLowerCase();
420
+
421
+ // validate serviceDir exists
422
+ if (!fs.existsSync(serviceDir)) {
423
+ throw new Error(`Service directory "${serviceDir}" does not exist`);
424
+ }
425
+
426
+ // Get all files in the templates directory
427
+ const templateFiles = scaffolder.getAllTemplateFiles({
428
+ dir: directories.scaffolderEntityTemplates,
429
+ baseDir: directories.scaffolderEntityTemplates
430
+ });
431
+
432
+ // scaffold the hbs templates
433
+ const templateArgs = {
434
+ entityName: entityName,
435
+ serviceName: inService,
436
+ database: database
437
+ };
438
+
439
+ scaffolder.scaffoldHandlebarTemplates({
440
+ templateFiles: templateFiles,
441
+ serviceDir: serviceDir,
442
+ database: database,
443
+ templateArgs: templateArgs
444
+ });
445
+ },
446
+
447
+ /**
448
+ * Scaffold handlebar templates
449
+ *
450
+ * @param {object} param
451
+ * @param {string} param.templateFiles - The list of template files
452
+ * @param {string} param.serviceDir - The service directory
453
+ * @param {string} param.serviceName - The service name
454
+ * @param {string} param.database - The database type
455
+ * @param {object} param.templateArgs - The template arguments
456
+ */
457
+ scaffoldHandlebarTemplates: function ({ templateFiles, serviceDir, serviceName, database, templateArgs }) {
458
+ try {
459
+ // Register Handlebars helpers
460
+ Object.entries(helpers).forEach(([name, fn]) => {
461
+ Handlebars.registerHelper(name, fn);
462
+ });
463
+
464
+ // Write the files to the service directory
465
+ templateFiles.forEach(file => {
466
+ let sourcePath;
467
+ let targetPath;
468
+
469
+ let fileRelativePath = file.relativePath;
470
+
471
+ // Database specific
472
+ if (fileRelativePath.includes('mongodb.')) {
473
+ if (database !== 'mongodb') {
474
+ return;
475
+ } else {
476
+ fileRelativePath = fileRelativePath.replace('mongodb.', '');
477
+ }
478
+ }
479
+
480
+ if (fileRelativePath.includes('mysql.')) {
481
+ if (database !== 'mysql') {
482
+ return;
483
+ } else {
484
+ fileRelativePath = fileRelativePath.replace('mysql.', '');
485
+ }
486
+ }
487
+
488
+ switch (file.extension) {
489
+ case '.hbs':
490
+ // Compile the Handlebars template for content
491
+ const templateContent = fs.readFileSync(file.fullPath, 'utf8');
492
+ const compiledTemplate = Handlebars.compile(templateContent);
493
+ const renderedContent = compiledTemplate(templateArgs);
494
+
495
+ // Compile the Handlebars template for the filename (excluding .hbs)
496
+ const filenameTemplate = Handlebars.compile(fileRelativePath.replace(/\.hbs$/, ''));
497
+ const renderedFilename = filenameTemplate(templateArgs);
498
+ targetPath = path.join(serviceDir, renderedFilename);
499
+
500
+ // Ensure directory exists
501
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
502
+ fs.writeFileSync(targetPath, renderedContent, 'utf8');
503
+ break;
504
+ default:
505
+ // By default, copy the file to the service directory
506
+ sourcePath = file.fullPath;
507
+ targetPath = path.join(serviceDir, fileRelativePath);
508
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
509
+ fs.copyFileSync(sourcePath, targetPath);
510
+ break;
511
+ }
512
+ });
513
+ } catch (error) {
514
+ throw new Error('Error scaffolding Handlebars templates: ' + error.message);
515
+ }
424
516
  }
425
517
  }