@gnar-engine/cli 1.0.5 → 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 (47) hide show
  1. package/bootstrap/deploy.localdev.yml +14 -0
  2. package/bootstrap/secrets.localdev.yml +7 -3
  3. package/bootstrap/services/notification/Dockerfile +2 -2
  4. package/bootstrap/services/notification/package.json +14 -32
  5. package/bootstrap/services/notification/src/app.js +50 -48
  6. package/bootstrap/services/notification/src/commands/notification.handler.js +96 -0
  7. package/bootstrap/services/notification/src/config.js +55 -12
  8. package/bootstrap/services/notification/src/controllers/http.controller.js +87 -0
  9. package/bootstrap/services/notification/src/controllers/message.controller.js +39 -70
  10. package/bootstrap/services/notification/src/db/migrations/01-init.js +50 -0
  11. package/bootstrap/services/notification/src/db/migrations/02-notification-service-init.js +23 -0
  12. package/bootstrap/services/notification/src/policies/notification.policy.js +49 -0
  13. package/bootstrap/services/notification/src/schema/notification.schema.js +17 -0
  14. package/bootstrap/services/notification/src/services/notification.service.js +32 -0
  15. package/bootstrap/services/portal/src/services/client.js +3 -0
  16. package/bootstrap/services/user/src/commands/user.handler.js +35 -18
  17. package/bootstrap/services/user/src/tests/commands/user.test.js +15 -6
  18. package/install-from-clone.sh +1 -1
  19. package/package.json +1 -1
  20. package/src/cli.js +0 -6
  21. package/src/config.js +8 -0
  22. package/src/dev/commands.js +2 -2
  23. package/src/dev/dev.service.js +19 -6
  24. package/src/helpers/helpers.js +24 -0
  25. package/src/profiles/command.js +41 -0
  26. package/src/profiles/profiles.client.js +23 -0
  27. package/src/scaffolder/commands.js +57 -1
  28. package/src/scaffolder/scaffolder.handler.js +127 -60
  29. package/templates/entity/src/commands/{{entityName}}.handler.js.hbs +94 -0
  30. package/templates/entity/src/controllers/{{entityName}}.http.controller.js.hbs +87 -0
  31. package/templates/entity/src/mysql.db/migrations/03-{{entityName}}-entity-init.js.hbs +23 -0
  32. package/templates/entity/src/policies/{{entityName}}.policy.js.hbs +49 -0
  33. package/templates/entity/src/schema/{{entityName}}.schema.js.hbs +17 -0
  34. package/templates/entity/src/services/mongodb.{{entityName}}.service.js.hbs +70 -0
  35. package/templates/entity/src/services/mysql.{{entityName}}.service.js.hbs +27 -0
  36. package/bootstrap/services/notification/Dockerfile.prod +0 -37
  37. package/bootstrap/services/notification/README.md +0 -3
  38. package/bootstrap/services/notification/src/commands/command-bus.js +0 -20
  39. package/bootstrap/services/notification/src/commands/handlers/control.handler.js +0 -18
  40. package/bootstrap/services/notification/src/commands/handlers/notification.handler.js +0 -157
  41. package/bootstrap/services/notification/src/services/logger.service.js +0 -16
  42. package/bootstrap/services/notification/src/services/ses.service.js +0 -23
  43. package/bootstrap/services/notification/src/templates/admin-order-recieved.hbs +0 -136
  44. package/bootstrap/services/notification/src/templates/admin-subscription-failed.hbs +0 -87
  45. package/bootstrap/services/notification/src/templates/customer-order-recieved.hbs +0 -132
  46. package/bootstrap/services/notification/src/templates/customer-subscription-failed.hbs +0 -77
  47. package/bootstrap/services/notification/src/tests/notification.test.js +0 -0
@@ -0,0 +1,49 @@
1
+ import { config } from '../config.js';
2
+
3
+ export const authorise = {
4
+
5
+ /**
6
+ * Authorise get single notification
7
+ */
8
+ getSingle: async (request, reply) => {
9
+ if (!request.user || request.user.role !== 'service_admin') {
10
+ reply.code(403).send({error: 'not authorised'});
11
+ }
12
+ },
13
+
14
+ /**
15
+ * Authorise get many notifications
16
+ */
17
+ getMany: async (request, reply) => {
18
+ if (!request.user || request.user.role !== 'service_admin') {
19
+ reply.code(403).send({error: 'not authorised'});
20
+ }
21
+ },
22
+
23
+ /**
24
+ * Authorise create notifications
25
+ */
26
+ create: async (request, reply) => {
27
+ if (!request.user || request.user.role !== 'service_admin') {
28
+ reply.code(403).send({error: 'not authorised'});
29
+ }
30
+ },
31
+
32
+ /**
33
+ * Authorise update notification
34
+ */
35
+ update: async (request, reply) => {
36
+ if (!request.user || request.user.role !== 'service_admin') {
37
+ reply.code(403).send({error: 'not authorised'});
38
+ }
39
+ },
40
+
41
+ /**
42
+ * Authorise delete notification
43
+ */
44
+ delete: async (request, reply) => {
45
+ if (!request.user || request.user.role !== 'service_admin') {
46
+ reply.code(403).send({error: 'not authorised'});
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,17 @@
1
+ import { schema } from '@gnar-engine/core';
2
+ import { config } from '../config.js';
3
+
4
+ export const notificationSchema = {
5
+ schemaName: 'notificationService.notificationSchema',
6
+ schema: {
7
+ type: 'object',
8
+ properties: {
9
+ // Add your properties here
10
+
11
+ },
12
+ required: [],
13
+ additionalProperties: false
14
+ }
15
+ };
16
+
17
+ export const validateNotification = schema.compile(notificationSchema);
@@ -0,0 +1,32 @@
1
+ import { db } from '@gnar-engine/core';
2
+
3
+ export const notification = {
4
+ async getById({ id }) {
5
+ const [result] = await db.query('SELECT * FROM notifications WHERE id = ?', [id]);
6
+ return result || null;
7
+ },
8
+
9
+ async getByEmail({ email }) {
10
+ // Placeholder: implement if your service uses email
11
+ return null;
12
+ },
13
+
14
+ async getAll() {
15
+ return await db.query('SELECT * FROM notifications');
16
+ },
17
+
18
+ async create(data) {
19
+ const { insertId } = await db.query('INSERT INTO notifications (created_at, updated_at) VALUES (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)');
20
+ return await this.getById({ id: insertId });
21
+ },
22
+
23
+ async update({ id, ...data }) {
24
+ await db.query('UPDATE notifications SET updated_at = CURRENT_TIMESTAMP WHERE id = ?', [id]);
25
+ return await this.getById({ id });
26
+ },
27
+
28
+ async delete({ id }) {
29
+ await db.query('DELETE FROM notifications WHERE id = ?', [id]);
30
+ return true;
31
+ },
32
+ };
@@ -20,6 +20,9 @@ client.interceptors.request.use(
20
20
  config.headers['Authorization'] = `Bearer ${authToken}`;
21
21
  }
22
22
 
23
+ // add content-type application/json to config.headers
24
+ config.headers['Content-Type'] = 'application/json';
25
+
23
26
  return config;
24
27
  },
25
28
  (error) => {
@@ -121,31 +121,48 @@ commands.register('userService.getManyUsers', async ({}) => {
121
121
  * Creat users with random password
122
122
  *
123
123
  * @param {Object} params
124
- * @param {Object} params.user - New user data
124
+ * @param {Array} params.users - New user data
125
125
  */
126
- commands.register('userService.createUserWithRandomPassword', async ({user}) => {
126
+ commands.register('userService.createUserWithRandomPassword', async ({users}) => {
127
127
 
128
- // create random password
129
- const password = Math.random().toString(36);
130
- const userData = {
131
- ...user,
132
- password: password
133
- };
128
+ const validationErrors = [];
129
+ let createdNewUsers = [];
134
130
 
135
- logger.info('creating new user : ' + JSON.stringify(userData));
131
+ // validate user data
132
+ for (const newUserData of users) {
136
133
 
137
- // create user
138
- try {
139
- const newUsers = await createUsers({users: [userData]})
134
+ // create random password
135
+ const password = Math.random().toString(36);
136
+ newUserData.password = password;
140
137
 
141
- if (!newUsers || newUsers.length === 0) {
142
- throw new error.badRequest('User creation failed');
138
+ const { errors } = validateUser(newUserData);
139
+
140
+ if (errors?.length) {
141
+ validationErrors.push(errors);
142
+ continue;
143
+ }
144
+
145
+ if (!newUserData.role || newUserData.role !== 'service_admin') {
146
+ // ensure emails are unique
147
+ const existingUser = await user.getByEmail({email: newUserData.email});
148
+
149
+ if (existingUser) {
150
+ validationErrors.push(`User with email ${newUserData.email} already exists`);
151
+ }
143
152
  }
144
-
145
- return newUsers[0];
146
- } catch (error) {
147
- throw new error.badRequest('User creation failed: ' + error);
148
153
  }
154
+
155
+ if (validationErrors.length) {
156
+ throw new error.badRequest(`Invalid user data: ${validationErrors}`);
157
+ }
158
+
159
+ // add users
160
+ for (const newUserData of users) {
161
+ const newUser = await user.create(newUserData);
162
+ createdNewUsers.push(newUser);
163
+ }
164
+
165
+ return createdNewUsers;
149
166
  });
150
167
 
151
168
  /**
@@ -8,15 +8,24 @@ test.prep(async () => {
8
8
  })
9
9
 
10
10
  // Test create user command
11
- test.run('Create User Command', async () => {
12
- const users = await commands.execute('createUsers', [
11
+ test.run('Create user command', async () => {
12
+ const users = await commands.execute('createUsers', { users: [
13
13
  {
14
- email: 'test@gnar.co.uk'
14
+ email: 'test@gnar.co.uk',
15
+ password: 'p4ssw0rd987'
15
16
  }
16
- ]);
17
-
17
+ ]});
18
18
  test.assert(users.length === 1, 'User was not created successfully');
19
19
  test.assert(users[0].email === 'test@gnar.co.uk', 'User email does not match');
20
20
  });
21
21
 
22
-
22
+ // Test create user with random password command
23
+ test.run('Create user with random password command', async () => {
24
+ const users = await commands.execute('createUserWithRandomPassword', { users: [
25
+ {
26
+ email: 'test2@gnar.co.uk'
27
+ }
28
+ ]});
29
+ test.assert(users.length === 1, 'User was not created successfully');
30
+ test.assert(users[0].email === 'test3@gnar.co.uk', 'User email does not match');
31
+ });
@@ -13,7 +13,7 @@ cd "$CLI_DIR"
13
13
  npm install
14
14
 
15
15
  # Link CLI to custom global folder
16
- npm link --prefix "$TARGET_DIR"
16
+ npm link
17
17
 
18
18
  # Bin path for npm link
19
19
  BIN_PATH="$TARGET_DIR/bin"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gnar-engine/cli",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Gnar Engine Development Framework CLI: Project bootstrap, scaffolder & control plane.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -30,9 +30,3 @@ G n a r E n g i n e - A powerful, AI ready microservice framework for modern a
30
30
  // Parse CLI input
31
31
  program.parse(process.argv);
32
32
 
33
- // Consts
34
- export const directories = {
35
- scaffolderTemplates: path.join(import.meta.dirname, '../templates/service'),
36
- bootstrap: path.join(import.meta.dirname, '../bootstrap'),
37
- provisioner: path.join(import.meta.dirname, './provisioner')
38
- }
package/src/config.js CHANGED
@@ -1,3 +1,4 @@
1
+ import path from 'path';
1
2
 
2
3
  export const gnarEngineCliConfig = {
3
4
 
@@ -7,3 +8,10 @@ export const gnarEngineCliConfig = {
7
8
  corePath: '/usr/gnar_engine/app/node_modules/@gnar-engine/core'
8
9
 
9
10
  }
11
+
12
+ export const directories = {
13
+ scaffolderServiceTemplates: path.join(import.meta.dirname, '../templates/service'),
14
+ scaffolderEntityTemplates: path.join(import.meta.dirname, '../templates/entity'),
15
+ bootstrap: path.join(import.meta.dirname, '../bootstrap'),
16
+ provisioner: path.join(import.meta.dirname, './provisioner')
17
+ };
@@ -10,10 +10,10 @@ export function registerDevCommands(program) {
10
10
  devCmd
11
11
  .command('up')
12
12
  .description('🛠️ Up Development Containers')
13
- .option('-b, --build', 'Ruild without cache')
13
+ .option('-b, --build', 'Build without cache')
14
14
  .option('-d, --detach', 'Run containers in background')
15
15
  .option('-t --test', 'Run the tests with ephemeral databases')
16
- .option('--test-service <service>', 'Run the tests for the specified service with ephemeral databases')
16
+ .option('--test-service <service>', 'Run the tests for the specified service with ephemeral databases (e.g. --test-service user)')
17
17
  .addOption(new Option('--core-dev').hideHelp())
18
18
  .action(async (options) => {
19
19
  let response = {};
@@ -5,7 +5,7 @@ import fs from "fs/promises";
5
5
  import path from "path";
6
6
  import yaml from "js-yaml";
7
7
  import { gnarEngineCliConfig } from "../config.js";
8
- import { directories } from "../cli.js";
8
+ import { directories } from "../config.js";
9
9
 
10
10
  const docker = new Docker();
11
11
 
@@ -17,13 +17,13 @@ const docker = new Docker();
17
17
  * @param {object} options
18
18
  * @param {string} options.projectDir - The project directory
19
19
  * @param {boolean} [options.build=false] - Whether to re-build images
20
- * @param {boolean} [options.detached=false] - Whether to run containers in background
20
+ * @param {boolean} [options.detach=false] - Whether to run containers in background
21
21
  * @param {boolean} [options.coreDev=false] - Whether to run in core development mode (requires access to core source)
22
22
  * @param {boolean} [options.test=false] - Whether to run tests with ephemeral databases
23
23
  * @param {string} [options.testService=''] - The service to run tests for (only applicable if test=true)
24
24
  * @param {boolean} [options.removeOrphans=true] - Whether to remove orphaned containers
25
25
  */
26
- export async function up({ projectDir, build = false, detached = false, coreDev = false, test = false, testService = '', removeOrphans = true }) {
26
+ export async function up({ projectDir, build = false, detach = false, coreDev = false, test = false, testService = '', removeOrphans = true }) {
27
27
 
28
28
  // core dev
29
29
  if (coreDev) {
@@ -74,7 +74,7 @@ export async function up({ projectDir, build = false, detached = false, coreDev
74
74
  args.push("--build");
75
75
  }
76
76
 
77
- if (detached) {
77
+ if (detach) {
78
78
  args.push("-d");
79
79
  }
80
80
 
@@ -111,11 +111,11 @@ export async function up({ projectDir, build = false, detached = false, coreDev
111
111
  */
112
112
  export async function down({ projectDir, allContainers = false }) {
113
113
  // list all containers
114
- const containers = await docker.listContainers();
114
+ let containers = await docker.listContainers();
115
115
 
116
116
  // filter containers by image name
117
117
  if (!allContainers) {
118
- const containers = containers.filter(c => c.Image.includes("ge-dev"));
118
+ containers = containers.filter(c => c.Image.includes("ge-localdev"));
119
119
  }
120
120
 
121
121
  if (containers.length === 0) {
@@ -137,6 +137,18 @@ export async function down({ projectDir, allContainers = false }) {
137
137
  });
138
138
  })
139
139
  );
140
+
141
+ // remove each container
142
+ await Promise.all(
143
+ containers.map(c => {
144
+ const container = docker.getContainer(c.Id);
145
+ return container.remove({ force: true }).catch(err => {
146
+ console.error(`Failed to remove ${c.Names[0]}: ${err.message}`);
147
+ });
148
+ })
149
+ );
150
+
151
+ console.log('Containers stopped and removed.');
140
152
  }
141
153
 
142
154
  /**
@@ -235,6 +247,7 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
235
247
  if (secrets.services?.[svc.name]) {
236
248
  secrets.services[svc.name].NODE_ENV = 'test';
237
249
 
250
+ console.log(testService, svc.name);
238
251
  if (testService && svc.name === testService) {
239
252
  secrets.services[svc.name].RUN_TESTS = 'true';
240
253
  }
@@ -1,3 +1,6 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import yaml from 'js-yaml';
1
4
 
2
5
  /**
3
6
  * CLI helper functions
@@ -59,5 +62,26 @@ export const helpers = {
59
62
  result += chars.charAt(Math.floor(Math.random() * chars.length));
60
63
  }
61
64
  return result;
65
+ },
66
+
67
+ getDbTypeFromSecrets: async (serviceName, projectDir) => {
68
+ let dbType;
69
+ const secretsPath = path.join(projectDir, "secrets.localdev.yml");
70
+ const parsedSecrets = yaml.load(await fs.readFile(secretsPath, "utf8"));
71
+ const serviceSecrets = parsedSecrets.services[serviceName.toLowerCase()];
72
+
73
+ Object.keys(serviceSecrets).forEach(key => {
74
+ if (key.toLowerCase().includes('host')) {
75
+ const host = serviceSecrets[key].toLowerCase();
76
+
77
+ if (host.includes('mongo')) {
78
+ dbType = 'mongodb';
79
+ } else if (host.includes('mysql')) {
80
+ dbType = 'mysql';
81
+ }
82
+ }
83
+ });
84
+
85
+ return dbType;
62
86
  }
63
87
  }
@@ -166,5 +166,46 @@ export function registerProfileCommand(program) {
166
166
  });
167
167
  });
168
168
 
169
+ // delete profile
170
+ profile
171
+ .command('delete <profileName>')
172
+ .description('Delete an existing profile')
173
+ .action(async (profileName) => {
174
+ const config = profiles.getAllProfiles();
175
+ const activeProfileName = config.activeProfile;
176
+
177
+ if (activeProfileName === profileName) {
178
+ console.error(`Cannot delete active profile "${profileName}". Please set another profile as active first.`);
179
+ return;
180
+ }
181
+
182
+ if (!config.profiles[profileName]) {
183
+ console.error(`Profile "${profileName}" not found.`);
184
+ return;
185
+ }
186
+
187
+ try {
188
+ // confirm deletion with user
189
+ const confirmation = await inquirer.prompt([
190
+ {
191
+ type: 'confirm',
192
+ name: 'confirmDelete',
193
+ message: `Are you sure you want to delete profile "${profileName}"?`,
194
+ default: false,
195
+ },
196
+ ]);
197
+
198
+ if (!confirmation.confirmDelete) {
199
+ console.log('❌ Deletion cancelled.');
200
+ return;
201
+ }
202
+
203
+ profiles.deleteProfile({ profileName });
204
+ console.log(`✅ Profile "${profileName}" deleted successfully.`);
205
+ } catch (error) {
206
+ console.error(error.message);
207
+ }
208
+ });
209
+
169
210
  program.addCommand(profile);
170
211
  }
@@ -90,6 +90,29 @@ export const profiles = {
90
90
  this.saveProfiles(allProfiles);
91
91
  },
92
92
 
93
+ deleteProfile: function ({ profileName }) {
94
+ if (!profileName) {
95
+ throw new Error('Invalid profile name');
96
+ }
97
+
98
+ const allProfiles = this.getAllProfiles().profiles || {};
99
+
100
+ if (!allProfiles[profileName]) {
101
+ throw new Error(`Profile "${profileName}" not found`);
102
+ }
103
+
104
+ const activeProfileName = this.getActiveProfile()?.name;
105
+
106
+ if (activeProfileName === profileName) {
107
+ throw new Error(`Cannot delete active profile "${profileName}". Please set another profile as active first.`);
108
+ }
109
+
110
+ // Prompt user to confirm deletion in the console
111
+ delete allProfiles[profileName];
112
+
113
+ this.saveProfiles(allProfiles);
114
+ },
115
+
93
116
  saveProfiles: function (profilesObj) {
94
117
  const dir = path.dirname(this.configPath);
95
118
  if (!fs.existsSync(dir)) {
@@ -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) {
@@ -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
  }