@gnar-engine/cli 1.0.0 → 1.0.1

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 (230) hide show
  1. package/assets/gnar-engine-logo-white.svg +9 -0
  2. package/bootstrap/deploy.localdev.yml +51 -0
  3. package/bootstrap/secrets.localdev.yml +27 -0
  4. package/bootstrap/services/agent/Dockerfile +23 -0
  5. package/bootstrap/services/agent/notes.md +28 -0
  6. package/bootstrap/services/agent/package.json +16 -0
  7. package/bootstrap/services/agent/src/app.js +52 -0
  8. package/bootstrap/services/agent/src/commands/agent.handler.js +104 -0
  9. package/bootstrap/services/agent/src/config.js +52 -0
  10. package/bootstrap/services/agent/src/controllers/http.controller.js +44 -0
  11. package/bootstrap/services/agent/src/controllers/message.controller.js +51 -0
  12. package/bootstrap/services/agent/src/db/migrations/01-init.js +50 -0
  13. package/bootstrap/services/agent/src/db/migrations/02-agent-service-init.js +36 -0
  14. package/bootstrap/services/agent/src/policies/agent.policy.js +13 -0
  15. package/bootstrap/services/agent/src/schema/Agent.schema.js +17 -0
  16. package/bootstrap/services/agent/src/services/agent.service.js +259 -0
  17. package/bootstrap/services/agent/src/services/chatgpt.service.js +46 -0
  18. package/bootstrap/services/agent/src/services/manifest.service.js +21 -0
  19. package/bootstrap/services/control/Dockerfile +23 -0
  20. package/bootstrap/services/control/Dockerfile.prod +37 -0
  21. package/bootstrap/services/control/README.md +25 -0
  22. package/bootstrap/services/control/package.json +16 -0
  23. package/bootstrap/services/control/src/app.js +45 -0
  24. package/bootstrap/services/control/src/commands/control.handler.js +231 -0
  25. package/bootstrap/services/control/src/commands/service.handler.js +81 -0
  26. package/bootstrap/services/control/src/commands/task.handler.js +247 -0
  27. package/bootstrap/services/control/src/config.js +55 -0
  28. package/bootstrap/services/control/src/controllers/http.controller.js +228 -0
  29. package/bootstrap/services/control/src/controllers/message.controller.js +40 -0
  30. package/bootstrap/services/control/src/db/migrations/01-init.js +50 -0
  31. package/bootstrap/services/control/src/db/migrations/02-control-service-init.js +60 -0
  32. package/bootstrap/services/control/src/db/migrations/03-alter-tasks.js +29 -0
  33. package/bootstrap/services/control/src/policies/task.policy.js +53 -0
  34. package/bootstrap/services/control/src/schema/control.schema.js +42 -0
  35. package/bootstrap/services/control/src/services/registry.service.js +83 -0
  36. package/bootstrap/services/control/src/services/reset.service.js +28 -0
  37. package/bootstrap/services/control/src/services/task.service.js +153 -0
  38. package/bootstrap/services/control/src/tests/control.test.js +50 -0
  39. package/bootstrap/services/notification/Dockerfile +23 -0
  40. package/bootstrap/services/notification/Dockerfile.prod +37 -0
  41. package/bootstrap/services/notification/README.md +3 -0
  42. package/bootstrap/services/notification/package.json +34 -0
  43. package/bootstrap/services/notification/src/app.js +51 -0
  44. package/bootstrap/services/notification/src/commands/command-bus.js +20 -0
  45. package/bootstrap/services/notification/src/commands/handlers/control.handler.js +18 -0
  46. package/bootstrap/services/notification/src/commands/handlers/notification.handler.js +157 -0
  47. package/bootstrap/services/notification/src/config.js +15 -0
  48. package/bootstrap/services/notification/src/controllers/message.controller.js +82 -0
  49. package/bootstrap/services/notification/src/services/logger.service.js +16 -0
  50. package/bootstrap/services/notification/src/services/ses.service.js +23 -0
  51. package/bootstrap/services/notification/src/templates/admin-order-recieved.hbs +136 -0
  52. package/bootstrap/services/notification/src/templates/admin-subscription-failed.hbs +87 -0
  53. package/bootstrap/services/notification/src/templates/customer-order-recieved.hbs +132 -0
  54. package/bootstrap/services/notification/src/templates/customer-subscription-failed.hbs +77 -0
  55. package/bootstrap/services/notification/src/tests/notification.test.js +0 -0
  56. package/bootstrap/services/portal/Dockerfile +23 -0
  57. package/bootstrap/services/portal/Dockerfile.remote +40 -0
  58. package/bootstrap/services/portal/README.md +22 -0
  59. package/bootstrap/services/portal/nginx.conf +12 -0
  60. package/bootstrap/services/portal/package.json +59 -0
  61. package/bootstrap/services/portal/public/favicon.ico +0 -0
  62. package/bootstrap/services/portal/public/gnar-white.png +0 -0
  63. package/bootstrap/services/portal/public/gnarengine-logo-black.png +0 -0
  64. package/bootstrap/services/portal/public/index.html +43 -0
  65. package/bootstrap/services/portal/public/logo192.png +0 -0
  66. package/bootstrap/services/portal/public/logo512.png +0 -0
  67. package/bootstrap/services/portal/public/manifest.json +25 -0
  68. package/bootstrap/services/portal/public/robots.txt +3 -0
  69. package/bootstrap/services/portal/src/App.js +56 -0
  70. package/bootstrap/services/portal/src/assets/Logo_Anchord_Black.svg +1 -0
  71. package/bootstrap/services/portal/src/assets/Logo_Anchord_Black_Green.svg +1 -0
  72. package/bootstrap/services/portal/src/assets/Logo_Anchord_White_Green.svg +1 -0
  73. package/bootstrap/services/portal/src/assets/activity.svg +3 -0
  74. package/bootstrap/services/portal/src/assets/arrow.svg +3 -0
  75. package/bootstrap/services/portal/src/assets/bin-white.svg +3 -0
  76. package/bootstrap/services/portal/src/assets/bin.svg +3 -0
  77. package/bootstrap/services/portal/src/assets/check.svg +3 -0
  78. package/bootstrap/services/portal/src/assets/chevron.svg +3 -0
  79. package/bootstrap/services/portal/src/assets/contact.svg +3 -0
  80. package/bootstrap/services/portal/src/assets/dots-vertical.svg +5 -0
  81. package/bootstrap/services/portal/src/assets/eye-off.svg +3 -0
  82. package/bootstrap/services/portal/src/assets/eye.svg +4 -0
  83. package/bootstrap/services/portal/src/assets/gnar-engine-black.svg +47 -0
  84. package/bootstrap/services/portal/src/assets/gnar-engine-white.svg +47 -0
  85. package/bootstrap/services/portal/src/assets/gnar_engine.svg +3 -0
  86. package/bootstrap/services/portal/src/assets/gnarengine-logo-black.png +0 -0
  87. package/bootstrap/services/portal/src/assets/home.svg +3 -0
  88. package/bootstrap/services/portal/src/assets/link.svg +3 -0
  89. package/bootstrap/services/portal/src/assets/lock.svg +3 -0
  90. package/bootstrap/services/portal/src/assets/package.svg +4 -0
  91. package/bootstrap/services/portal/src/assets/raffle.svg +3 -0
  92. package/bootstrap/services/portal/src/assets/settings.svg +4 -0
  93. package/bootstrap/services/portal/src/assets/shopping-bag.svg +3 -0
  94. package/bootstrap/services/portal/src/assets/user-black.svg +3 -0
  95. package/bootstrap/services/portal/src/assets/user.svg +3 -0
  96. package/bootstrap/services/portal/src/assets/users.svg +3 -0
  97. package/bootstrap/services/portal/src/assets/wallet.svg +3 -0
  98. package/bootstrap/services/portal/src/css/style.css +1007 -0
  99. package/bootstrap/services/portal/src/data/data.js +70 -0
  100. package/bootstrap/services/portal/src/features/attributeFormRow/AttributeFormRow.jsx +32 -0
  101. package/bootstrap/services/portal/src/features/billingShipping/BillingShipping.jsx +160 -0
  102. package/bootstrap/services/portal/src/features/crud/crudEdit.less +230 -0
  103. package/bootstrap/services/portal/src/features/crud/crudList.less +134 -0
  104. package/bootstrap/services/portal/src/features/crud/crudPage.less +31 -0
  105. package/bootstrap/services/portal/src/features/crudContact/CrudContactList.jsx +108 -0
  106. package/bootstrap/services/portal/src/features/crudContact/CrudContactSingle.jsx +243 -0
  107. package/bootstrap/services/portal/src/features/crudOrder/CrudOrderList.jsx +109 -0
  108. package/bootstrap/services/portal/src/features/crudOrder/CrudOrderSingle.jsx +315 -0
  109. package/bootstrap/services/portal/src/features/crudProducts/CrudProductList.jsx +104 -0
  110. package/bootstrap/services/portal/src/features/crudProducts/CrudProductSingle.jsx +388 -0
  111. package/bootstrap/services/portal/src/features/crudRaffles/CrudRafflesList.jsx +104 -0
  112. package/bootstrap/services/portal/src/features/crudRaffles/CrudRafflesSingle.jsx +208 -0
  113. package/bootstrap/services/portal/src/features/crudSubscription/CrudSubscriptionList.jsx +110 -0
  114. package/bootstrap/services/portal/src/features/crudSubscription/CrudSubscriptionSingle.jsx +261 -0
  115. package/bootstrap/services/portal/src/features/crudUser/CrudUserList.jsx +107 -0
  116. package/bootstrap/services/portal/src/features/crudUser/CrudUserSingle.jsx +402 -0
  117. package/bootstrap/services/portal/src/features/inventoryFormRow/InventoryFormRow.jsx +30 -0
  118. package/bootstrap/services/portal/src/features/lineItems/LineItems.jsx +113 -0
  119. package/bootstrap/services/portal/src/features/loginForm/LoginForm.jsx +56 -0
  120. package/bootstrap/services/portal/src/features/loginForm/loginForm.less +56 -0
  121. package/bootstrap/services/portal/src/features/notes/Notes.jsx +18 -0
  122. package/bootstrap/services/portal/src/features/passwordReset/PasswordResetForm.jsx +96 -0
  123. package/bootstrap/services/portal/src/features/passwordReset/PasswordResetRequestForm.jsx +74 -0
  124. package/bootstrap/services/portal/src/features/priceFormRow/PriceFormRow.jsx +102 -0
  125. package/bootstrap/services/portal/src/features/priceFormRow/priceFormRow.less +24 -0
  126. package/bootstrap/services/portal/src/features/raffleEntriesList/RaffleEntriesList.jsx +99 -0
  127. package/bootstrap/services/portal/src/features/raffleProductFormRow/RaffleProductFormRow.jsx +46 -0
  128. package/bootstrap/services/portal/src/features/sidebar/Sidebar.jsx +64 -0
  129. package/bootstrap/services/portal/src/features/sidebar/sidebar.less +49 -0
  130. package/bootstrap/services/portal/src/features/skus/Skus.jsx +109 -0
  131. package/bootstrap/services/portal/src/features/subscriptionSchedule/SubscriptionSchedule.jsx +44 -0
  132. package/bootstrap/services/portal/src/features/taxonomyFormRow/TaxonomyFormRow.jsx +32 -0
  133. package/bootstrap/services/portal/src/features/user/User.jsx +54 -0
  134. package/bootstrap/services/portal/src/features/user/user.less +57 -0
  135. package/bootstrap/services/portal/src/includes/utilities.js +259 -0
  136. package/bootstrap/services/portal/src/index.js +14 -0
  137. package/bootstrap/services/portal/src/layouts/CrudLayout.jsx +50 -0
  138. package/bootstrap/services/portal/src/layouts/LoginLayout.jsx +17 -0
  139. package/bootstrap/services/portal/src/layouts/PortalLayout.jsx +48 -0
  140. package/bootstrap/services/portal/src/layouts/loginLayout.less +33 -0
  141. package/bootstrap/services/portal/src/layouts/portalLayout.less +67 -0
  142. package/bootstrap/services/portal/src/pages/contacts/Contacts.jsx +199 -0
  143. package/bootstrap/services/portal/src/pages/dashboard/Dashboard.jsx +17 -0
  144. package/bootstrap/services/portal/src/pages/integrations/Integrations.jsx +10 -0
  145. package/bootstrap/services/portal/src/pages/login/Login.jsx +15 -0
  146. package/bootstrap/services/portal/src/pages/login/login.less +10 -0
  147. package/bootstrap/services/portal/src/pages/orders/Orders.jsx +199 -0
  148. package/bootstrap/services/portal/src/pages/passwordReset/PasswordResetPage.jsx +15 -0
  149. package/bootstrap/services/portal/src/pages/passwordResetRequest/PasswordResetRequestPage.jsx +15 -0
  150. package/bootstrap/services/portal/src/pages/payments/Payments.jsx +10 -0
  151. package/bootstrap/services/portal/src/pages/portal/Portal.jsx +43 -0
  152. package/bootstrap/services/portal/src/pages/products/Products.jsx +212 -0
  153. package/bootstrap/services/portal/src/pages/raffleEntries/RaffleEntries.jsx +124 -0
  154. package/bootstrap/services/portal/src/pages/raffles/Raffles.jsx +186 -0
  155. package/bootstrap/services/portal/src/pages/reports/Reports.jsx +10 -0
  156. package/bootstrap/services/portal/src/pages/settings/Settings.jsx +10 -0
  157. package/bootstrap/services/portal/src/pages/subscriptions/Subscriptions.jsx +199 -0
  158. package/bootstrap/services/portal/src/pages/users/Users.jsx +193 -0
  159. package/bootstrap/services/portal/src/pages/users/users.less +25 -0
  160. package/bootstrap/services/portal/src/slices/authSlice.js +71 -0
  161. package/bootstrap/services/portal/src/store/configureStore.js +12 -0
  162. package/bootstrap/services/portal/src/styles/global.less +159 -0
  163. package/bootstrap/services/portal/src/styles/inputs.less +157 -0
  164. package/bootstrap/services/portal/src/styles/main.less +26 -0
  165. package/bootstrap/services/portal/src/ui/collapsible/Collapsible.jsx +97 -0
  166. package/bootstrap/services/portal/src/ui/collapsible/collapsible.less +23 -0
  167. package/bootstrap/services/portal/src/ui/customCheckbox/CustomCheckbox.jsx +17 -0
  168. package/bootstrap/services/portal/src/ui/customCheckbox/customCheckbox.less +42 -0
  169. package/bootstrap/services/portal/src/ui/customMultiSelect/CustomMultiSelect.jsx +63 -0
  170. package/bootstrap/services/portal/src/ui/customMultiSelect/CustomMultiSelectPeriod.jsx +63 -0
  171. package/bootstrap/services/portal/src/ui/customSelect/CustomSelect.jsx +63 -0
  172. package/bootstrap/services/portal/src/ui/customSelect/customSelect.less +92 -0
  173. package/bootstrap/services/portal/src/ui/goBack/GoBack.jsx +19 -0
  174. package/bootstrap/services/portal/src/ui/loader/Loader.jsx +12 -0
  175. package/bootstrap/services/portal/src/ui/pagination/Pagination.jsx +23 -0
  176. package/bootstrap/services/portal/src/ui/repeater/Repeater.jsx +29 -0
  177. package/bootstrap/services/portal/src/ui/saveButton/SaveButton.jsx +69 -0
  178. package/bootstrap/services/portal/src/ui/saveButton/saveButton.less +0 -0
  179. package/bootstrap/services/rabbit-mq/Dockerfile.prod +9 -0
  180. package/bootstrap/services/user/Dockerfile +23 -0
  181. package/bootstrap/services/user/Dockerfile.prod +37 -0
  182. package/bootstrap/services/user/README.md +8 -0
  183. package/bootstrap/services/user/package.json +16 -0
  184. package/bootstrap/services/user/src/app.js +45 -0
  185. package/bootstrap/services/user/src/commands/session.handler.js +23 -0
  186. package/bootstrap/services/user/src/commands/user.handler.js +286 -0
  187. package/bootstrap/services/user/src/config.js +73 -0
  188. package/bootstrap/services/user/src/controllers/http.controller.js +156 -0
  189. package/bootstrap/services/user/src/controllers/message.controller.js +51 -0
  190. package/bootstrap/services/user/src/db/migrations/01-init.js +50 -0
  191. package/bootstrap/services/user/src/db/migrations/02-user-service-init.js +63 -0
  192. package/bootstrap/services/user/src/db/migrations/03-unauth-sessions.js +43 -0
  193. package/bootstrap/services/user/src/db/seeders/development/01-root-user.js +29 -0
  194. package/bootstrap/services/user/src/db/seeders/development/02-portal-admin-user.js +27 -0
  195. package/bootstrap/services/user/src/db/seeders/production/01-root-user.js +29 -0
  196. package/bootstrap/services/user/src/policies/user.policy.js +81 -0
  197. package/bootstrap/services/user/src/schema/user.schema.js +69 -0
  198. package/bootstrap/services/user/src/services/authentication.service.js +127 -0
  199. package/bootstrap/services/user/src/services/session.service.js +58 -0
  200. package/bootstrap/services/user/src/services/user.service.js +130 -0
  201. package/bootstrap/services/user/src/tests/user.test.js +126 -0
  202. package/package.json +3 -7
  203. package/src/agent/agent.client.js +28 -0
  204. package/src/agent/commands.js +48 -0
  205. package/src/cli.js +30 -0
  206. package/src/config.js +8 -0
  207. package/src/control/commands.js +156 -0
  208. package/src/control/control.client.js +127 -0
  209. package/src/dev/commands.js +71 -0
  210. package/src/dev/dev.service.js +320 -0
  211. package/src/engine/infra.js +142 -0
  212. package/src/helpers/helpers.js +63 -0
  213. package/src/profiles/command.js +170 -0
  214. package/src/profiles/profiles.client.js +101 -0
  215. package/src/scaffolder/commands.js +123 -0
  216. package/src/scaffolder/scaffolder.handler.js +252 -0
  217. package/src/services/client.js +174 -0
  218. package/templates/service/Dockerfile.hbs +20 -0
  219. package/templates/service/app.js.hbs +38 -0
  220. package/templates/service/commands/{{serviceName}}.handler.js.hbs +97 -0
  221. package/templates/service/config.js.hbs +48 -0
  222. package/templates/service/controllers/http.controller.js.hbs +87 -0
  223. package/templates/service/controllers/message.controller.js.hbs +51 -0
  224. package/templates/service/db/migrations/01-init.js.hbs +50 -0
  225. package/templates/service/db/migrations/02-{{lowerCase serviceName}}-service-init.js.hbs +23 -0
  226. package/templates/service/package.json.hbs +18 -0
  227. package/templates/service/policies/{{serviceName}}.policy.js.hbs +49 -0
  228. package/templates/service/schema/{{serviceName}}.schema.js.hbs +14 -0
  229. package/templates/service/services/{{serviceName}}.service.js.hbs +32 -0
  230. package/dist/cli.js +0 -18
@@ -0,0 +1,153 @@
1
+ import { logger, utils, db } from '@gnar-engine/core';
2
+
3
+
4
+ /**
5
+ * Task Service
6
+ */
7
+ export const task = {
8
+
9
+ // schedule a task
10
+ scheduleTask: async ({name, payload, scheduled, recurringInterval, recurringIntervalCount, rescheduleCentrallyOnSuccess = false, rescheduleCentrallyOnFailure = false, handlerServiceName, handlerName, idempotencyKey = ''}) => {
11
+ try {
12
+ const [result] = await db.execute(
13
+ 'INSERT INTO `tasks` (`id`, `name`, `payload`, `status`, `scheduled`, `recurring_interval`, `recurring_interval_count`, `reschedule_centrally_on_success`, `reschedule_centrally_on_failure`, `handler_service_name`, `handler_name`, `idempotency_key`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
14
+ [utils.uuid(), name, JSON.stringify(payload), 'scheduled', scheduled, recurringInterval, recurringIntervalCount, rescheduleCentrallyOnSuccess, rescheduleCentrallyOnFailure, handlerServiceName, handlerName, idempotencyKey]
15
+ );
16
+
17
+ return result.insertId;
18
+ } catch (error) {
19
+ logger.error("Error scheduling task:" + error);
20
+ throw error;
21
+ }
22
+ },
23
+
24
+ // update task status
25
+ updateTaskStatus: async ({id, status}) => {
26
+ try {
27
+ const [result] = await db.execute(
28
+ 'UPDATE `tasks` SET `status` = ? WHERE `id` = ?',
29
+ [status, id]
30
+ );
31
+
32
+ return result.affectedRows;
33
+ } catch (error) {
34
+ logger.error("Error updating task status:" + error);
35
+ throw error;
36
+ }
37
+ },
38
+
39
+ // cancel tasks by associated resource id
40
+ cancelTasksByAssociatedResourceId: async ({associatedResourceId}) => {
41
+ try {
42
+ const [result] = await db.execute(
43
+ 'UPDATE `tasks` SET `status` = ? WHERE `associated_resource_id` = ?',
44
+ ['cancelled', associatedResourceId]
45
+ );
46
+
47
+ return result.affectedRows;
48
+ } catch (error) {
49
+ logger.error("Error cancelling tasks by associated resource id:" + error);
50
+ throw error;
51
+ }
52
+ },
53
+
54
+ // get task batch
55
+ getTaskBatch: async ({status}) => {
56
+ try {
57
+ const [result] = await db.execute(
58
+ 'SELECT * FROM `tasks` WHERE `status` = ? AND `scheduled` <= NOW()',
59
+ [status]
60
+ );
61
+
62
+ // map from snake case to camel case
63
+ const tasks = result.map(task => {
64
+ return {
65
+ id: task.id,
66
+ name: task.name,
67
+ payload: JSON.parse(task.payload),
68
+ status: task.status,
69
+ scheduled: task.scheduled,
70
+ recurringInterval: task.recurring_interval,
71
+ recurringIntervalCount: task.recurring_interval_count,
72
+ rescheduleCentrallyOnSuccess: task.reschedule_centrally_on_success,
73
+ rescheduleCentrallyOnFailure: task.reschedule_centrally_on_failure,
74
+ handlerServiceName: task.handler_service_name,
75
+ handlerName: task.handler_name,
76
+ idempotencyKey: task.idempotency_key
77
+ };
78
+ });
79
+
80
+ return tasks;
81
+ } catch (error) {
82
+ logger.error("Error getting task batch:" + error);
83
+ throw error;
84
+ }
85
+ },
86
+
87
+ // get tasks by status
88
+ getTasksByStatus: async ({status}) => {
89
+ try {
90
+ const [result] = await db.execute(
91
+ 'SELECT * FROM `tasks` WHERE `status` = ?',
92
+ [status]
93
+ );
94
+
95
+ // map from snake case to camel case
96
+ const tasks = result.map(task => {
97
+ return {
98
+ id: task.id,
99
+ name: task.name,
100
+ payload: JSON.parse(task.payload),
101
+ status: task.status,
102
+ scheduled: task.scheduled,
103
+ recurringInterval: task.recurring_interval,
104
+ recurringIntervalCount: task.recurring_interval_count,
105
+ rescheduleCentrallyOnSuccess: task.reschedule_centrally_on_success,
106
+ rescheduleCentrallyOnFailure: task.reschedule_centrally_on_failure,
107
+ handlerServiceName: task.handler_service_name,
108
+ handlerName: task.handler_name,
109
+ idempotencyKey: task.idempotency_key
110
+ };
111
+ });
112
+
113
+ return tasks;
114
+ } catch (error) {
115
+ logger.error("Error getting tasks by status in service:" + error);
116
+ throw error;
117
+ }
118
+ },
119
+
120
+ // ensure task is the only scheduled with this indempotency key
121
+ checkIndempotent: async ({idempotencyKey}) => {
122
+ try {
123
+ if (!idempotencyKey) {
124
+ return true;
125
+ }
126
+
127
+ const [result] = await db.execute(
128
+ 'SELECT * FROM `tasks` WHERE `idempotency_key` = ? AND `status` = ?',
129
+ [idempotencyKey, 'scheduled']
130
+ );
131
+
132
+ return result.length === 0;
133
+ } catch (error) {
134
+ logger.error("Error checking indempotency:" + error);
135
+ throw error;
136
+ }
137
+ },
138
+
139
+ // delete failed tasks
140
+ deleteFailedTasks: async () => {
141
+ try {
142
+ const [result] = await db.execute(
143
+ 'DELETE FROM `tasks` WHERE `status` = ?',
144
+ ['failed']
145
+ );
146
+
147
+ return result.affectedRows;
148
+ } catch (error) {
149
+ logger.error("Error deleting failed tasks:" + error);
150
+ throw error;
151
+ }
152
+ }
153
+ };
@@ -0,0 +1,50 @@
1
+ import request from 'supertest';
2
+ const url = 'http://localhost:4000';
3
+ const authUrl = 'http://localhost:4001';
4
+
5
+ describe('Products API CRUD', () => {
6
+
7
+ let authToken;
8
+ let productId;
9
+
10
+ beforeAll(async () => {
11
+ // check we are not in production mode
12
+ if (process.env.NODE_ENV === 'production') {
13
+ throw new Error('Do not run tests in production mode!');
14
+ }
15
+ });
16
+
17
+ // authenticate with email and password
18
+ it('POST /authenticate', async () => {
19
+ const response = await request(authUrl).post('/authenticate')
20
+ .set('Content-Type', 'application/json')
21
+ .send({
22
+ username: 'adam@gnar.co.uk',
23
+ password: 'password'
24
+ });
25
+ console.log(response.body);
26
+ expect(response.status).toBe(200);
27
+ expect(response.body).toHaveProperty('token');
28
+
29
+ authToken = response.body.token;
30
+ });
31
+
32
+ // schedule a task
33
+ it('should return 200 OK - POST /tasks/schedule', async () => {
34
+ const res = await request(url).post('/tasks/schedule')
35
+ .set('Content-Type', 'application/json')
36
+ .set('Authorization', 'Bearer ' + authToken)
37
+ .send({
38
+ name: 'test-task',
39
+ payload: { test: 'data' },
40
+ scheduled: new Date().addMonths(1),
41
+ recurringInterval: 'monthly',
42
+ recurringIntervalCount: 1,
43
+ handlerServiceName: 'userService',
44
+ handlerName: 'testHandler'
45
+ });
46
+ console.log(res.body);
47
+ expect(res.statusCode).toEqual(200);
48
+ });
49
+
50
+ });
@@ -0,0 +1,23 @@
1
+ # Dockerfile for Notification Service
2
+ FROM node:20-alpine
3
+
4
+ # Set the working directory
5
+ WORKDIR /usr/gnar_engine/app
6
+
7
+ # Define a global env var
8
+ ENV GLOBAL_SERVICE_BASE_DIR=/usr/gnar_engine/app/src/
9
+
10
+ # Copy package.json and package-lock.json
11
+ COPY ./services/notification/package*.json ./
12
+
13
+ # Install nodemon
14
+ RUN npm install -g nodemon
15
+
16
+ # Install app dependencies
17
+ RUN npm install
18
+
19
+ # Expose the port the service will run on
20
+ EXPOSE 4000
21
+
22
+ # Start the application
23
+ CMD ["nodemon", "--watch", "./gnar_engine", "./gnar_engine/app.js"]
@@ -0,0 +1,37 @@
1
+ # Stage 1: Builder
2
+ FROM node:20-alpine AS builder
3
+
4
+ WORKDIR /app
5
+
6
+ # Copy app source
7
+ COPY ./services/notification/src ./src
8
+ COPY ./Lib ./Lib
9
+
10
+ # Copy environment variables (do this later in build only if needed)
11
+ COPY ./.env.production .env
12
+
13
+ # Copy package files and install deps
14
+ COPY ./services/notification/package*.json ./
15
+ RUN npm install --omit=dev
16
+
17
+ # Stage 2: Runtime
18
+ FROM node:20-alpine
19
+
20
+ WORKDIR /app
21
+
22
+ # Copy built app from builder stage
23
+ COPY --from=builder /app /app
24
+
25
+ # Install system deps
26
+ RUN apk add --no-cache ca-certificates wget
27
+
28
+ # Install AWS DocumentDB CA certificates
29
+ RUN mkdir -p /usr/local/share/ca-certificates && \
30
+ wget https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem -O /usr/local/share/ca-certificates/aws-docdb.pem && \
31
+ update-ca-certificates
32
+
33
+ # Expose port
34
+ EXPOSE 4000
35
+
36
+ # Start app
37
+ CMD ["npm", "run", "start"]
@@ -0,0 +1,3 @@
1
+ # Gnar Engine Notification Service
2
+
3
+ - Email sending
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "gnar_engine_notification",
3
+ "version": "1.0.0",
4
+ "description": "Gnar Engine - Notification Service",
5
+ "type": "module",
6
+ "scripts": {
7
+ "start": "node ./src/app.js",
8
+ "start:dev": "nodemon --watch ./src ./src/app.js",
9
+ "test": "NODE_OPTIONS='--experimental-vm-modules' jest --watchAll --verbose"
10
+ },
11
+ "author": "Gnar Software Ltd.",
12
+ "license": "ISC",
13
+ "dependencies": {
14
+ "@aws-sdk/client-ses": "^3.806.0",
15
+ "ajv": "^8.17.1",
16
+ "ajv-formats": "^3.0.1",
17
+ "dotenv": "^16.4.7",
18
+ "nodemailer": "^7.0.3",
19
+ "path": "^0.12.7",
20
+ "pino": "^9.6.0",
21
+ "pino-pretty": "^13.0.0",
22
+ "uuid": "^11.1.0",
23
+ "handlebars": "^4.7.8"
24
+ },
25
+ "devDependencies": {
26
+ "@babel/preset-env": "^7.22.20",
27
+ "jest": "^29.7.0",
28
+ "nodemon": "^3.1.9",
29
+ "supertest": "^6.3.3"
30
+ },
31
+ "workspaces": [
32
+ "./Lib/*"
33
+ ]
34
+ }
@@ -0,0 +1,51 @@
1
+ import dotenv from 'dotenv';
2
+ import { logger } from './services/logger.service.js';
3
+ import { commandBus } from './commands/command-bus.js';
4
+ import { messageController } from './controllers/message.controller.js';
5
+ import { messageAwaitResponse } from '@gnar-engine/message-client';
6
+ import { internalHealthCheck } from './commands/handlers/control.handler.js';
7
+ import { sendNotification } from './commands/handlers/notification.handler.js';
8
+
9
+ dotenv.config({ path: '.env' });
10
+
11
+ process.on('unhandledRejection', (reason, promise) => {
12
+ console.error('🚨 Unhandled Rejection at:', promise, '\nReason:', reason);
13
+ process.exit(1);
14
+ });
15
+
16
+ process.on('uncaughtException', (err) => {
17
+ console.error('🚨 Uncaught Exception:', err);
18
+ process.exit(1);
19
+ });
20
+
21
+ /**
22
+ * @function startServer
23
+ * @description Initializes and starts the Fastify server.
24
+ */
25
+ const startService = async () => {
26
+
27
+ // Register commands
28
+ commandBus.register('internalHealthCheck', internalHealthCheck);
29
+ commandBus.register('sendNotification', sendNotification);
30
+
31
+ // Init controllers and error handlers
32
+ messageController.init();
33
+
34
+ // Register with control service
35
+ try {
36
+ await messageAwaitResponse('controlService', {
37
+ method: 'registerService',
38
+ data: {
39
+ service: {
40
+ name: 'notificationService'
41
+ }
42
+ }
43
+ });
44
+ } catch (error) {
45
+ logger.info('No response from the control service when registering as a service');
46
+ }
47
+
48
+ };
49
+
50
+ // Entry point
51
+ startService();
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Command bus
3
+ */
4
+ export const commandBus = {
5
+ handlers: new Map(),
6
+
7
+ register(commandName, handlerFunction) {
8
+ this.handlers.set(commandName, handlerFunction);
9
+ },
10
+
11
+ async execute(commandName, ...args) {
12
+ const handlerFunction = this.handlers.get(commandName);
13
+ if (!handlerFunction) {
14
+ console.log('handlers', this.handlers);
15
+ throw new Error(`Command ${commandName} not registered`);
16
+ }
17
+
18
+ return await handlerFunction(...args);
19
+ }
20
+ }
@@ -0,0 +1,18 @@
1
+
2
+ /**
3
+ * Run seeders
4
+ *
5
+ * @param {Object} params
6
+ * @param {string} params.seeder Name of single seeder to run (optional)
7
+ */
8
+ export const runSeeders = async ({seeder}) => {
9
+ // checkout service has no db
10
+ }
11
+
12
+ /**
13
+ * Internal health check (kills process if it fails)
14
+ */
15
+ export const internalHealthCheck = async () => {
16
+
17
+ // Nothing to check for checkout service
18
+ }
@@ -0,0 +1,157 @@
1
+ import { logger } from '../../services/logger.service.js';
2
+ import { emailSendingService, emailHeaderLogoUrl } from './../../config.js';
3
+ import { helpers } from '@gnar-engine/helpers';
4
+ import fs from 'fs';
5
+ import handlebars from 'handlebars';
6
+ import nodemailer from 'nodemailer';
7
+ import { getSesClient } from '../../services/ses.service.js';
8
+ import { SendEmailCommand } from '@aws-sdk/client-ses';
9
+
10
+
11
+ /**
12
+ * Send a notification
13
+ *
14
+ * @param {Object} params
15
+ * @param {string} params.templateName - Name of the template file (without extension)
16
+ * @param {string} params.to - Recipient email address
17
+ * @param {Object} params.params - Parameters to be passed to the template
18
+ * @param {string} params.subject - Subject of the email
19
+ */
20
+ export const sendNotification = async ({ templateName, to, params, subject }) => {
21
+ let source;
22
+ let template;
23
+
24
+ // get the requested template
25
+ try {
26
+ const workingDir = process.cwd();
27
+ const path = workingDir + '/src/templates/' + templateName + '.hbs';
28
+ source = fs.readFileSync(path, 'utf8');
29
+ } catch (error) {
30
+ logger.error('Error reading template file: ' + error.message);
31
+ throw new Error('Template not found');
32
+ }
33
+
34
+ // compile the template
35
+ try {
36
+ template = handlebars.compile(source);
37
+ } catch (error) {
38
+ logger.error('Error compiling template: ' + error.message);
39
+ throw new Error('Template compilation failed');
40
+ }
41
+
42
+ // append other params
43
+ params = prepareParams(params, templateName);
44
+
45
+ // prepare the template
46
+ const html = template(params);
47
+
48
+ // send the email
49
+ switch (emailSendingService) {
50
+ case 'SMTP':
51
+ await sendSmtpEmail({ to, subject, html });
52
+ break;
53
+
54
+ case 'SES':
55
+ await sendSesEmail({ to, subject, html });
56
+ break;
57
+
58
+ case 'Direct':
59
+ logger.error('Email sending service not implemented: ' + emailSendingService);
60
+ throw new Error('Email sending service not implemented');
61
+
62
+ default:
63
+ logger.error('Invalid email sending service: ' + emailSendingService);
64
+ throw new Error('Invalid email sending service');
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Send SMTP email
70
+ *
71
+ * @param {Object} params
72
+ * @param {string} params.to - Recipient email address
73
+ * @param {string} params.subject - Email subject
74
+ * @param {string} params.html - HTML content of the email
75
+ */
76
+ export const sendSmtpEmail = async ({ to, subject, html }) => {
77
+ try {
78
+ const transporter = nodemailer.createTransport({
79
+ host: process.env.SMTP_HOST,
80
+ port: parseInt(process.env.SMTP_PORT || '465'),
81
+ secure: true,
82
+ auth: {
83
+ user: process.env.SMTP_USER,
84
+ pass: process.env.SMTP_PASS
85
+ }
86
+ });
87
+
88
+ const mailOptions = {
89
+ from: `"Your App Name" <${process.env.SMTP_USER}>`,
90
+ to,
91
+ subject,
92
+ html
93
+ };
94
+
95
+ await transporter.sendMail(mailOptions);
96
+ } catch (error) {
97
+ logger.error('SMTP email send error: ' + error.message);
98
+ throw new Error('SMTP email failed to send');
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Send SES email
104
+ *
105
+ * @param {Object} params
106
+ * @param {string} params.to - Recipient email address
107
+ * @param {string} params.subject - Email subject
108
+ * @param {string} params.html - HTML content of the email
109
+ */
110
+ export const sendSesEmail = async ({ to, subject, html }) => {
111
+ try {
112
+ const sesClient = getSesClient();
113
+
114
+ const command = new SendEmailCommand({
115
+ Source: process.env.NOTIFICATION_SES_SOURCE_EMAIL,
116
+ Destination: {
117
+ ToAddresses: [to]
118
+ },
119
+ Message: {
120
+ Subject: {
121
+ Data: subject,
122
+ Charset: 'UTF-8'
123
+ },
124
+ Body: {
125
+ Html: {
126
+ Data: html,
127
+ Charset: 'UTF-8'
128
+ }
129
+ }
130
+ }
131
+ });
132
+
133
+ await sesClient.send(command);
134
+ } catch (error) {
135
+ logger.error('Error sending email with SES: ' + error.message);
136
+ throw new Error('SES email failed to send');
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Prepare parameters for the template
142
+ *
143
+ * @param {Object} params - Parameters to be passed to the template
144
+ * @param {string} templateName - Name of the template file (without extension)
145
+ * @returns {Object} - Prepared parameters
146
+ */
147
+ const prepareParams = (params, templateName) => {
148
+
149
+ // add shop logo
150
+ params.logoUrl = emailHeaderLogoUrl;
151
+
152
+ if (params.order?.currency) {
153
+ params.currencySymbol = helpers.ecommerce.getCurrencySymbol(params.order.currency);
154
+ }
155
+
156
+ return params
157
+ }
@@ -0,0 +1,15 @@
1
+
2
+ /**
3
+ * Email sending service
4
+ *
5
+ * - Values:
6
+ * - SES (not implemented yet)
7
+ * - SMTP
8
+ * - Direct (not implemented yet)
9
+ */
10
+ export const emailSendingService = 'SES';
11
+
12
+ /**
13
+ * Email header logo url
14
+ */
15
+ export const emailHeaderLogoUrl = 'https://admin.gnarengine.com/public/gnarengine-logo-black.png';
@@ -0,0 +1,82 @@
1
+ import { commandBus } from '../commands/command-bus.js';
2
+ import { logger } from '../services/logger.service.js';
3
+ import { initializeRabbitMQ } from '@gnar-engine/message-client';
4
+
5
+ // Configuration
6
+ const queueName = 'notificationServiceQueue';
7
+ const prefetch = 3;
8
+
9
+ export const messageController = {
10
+ handleMessage: async function (msg, channel) {
11
+ if (!msg) return;
12
+
13
+ const payload = JSON.parse(msg.content.toString());
14
+
15
+ if (!payload.method) {
16
+ return channel.sendToQueue(msg.properties.replyTo, Buffer.from(JSON.stringify({ error: 'Method not found' })), {
17
+ correlationId: msg.properties.correlationId,
18
+ });
19
+ }
20
+
21
+ switch (payload.method) {
22
+ case 'sendNotification':
23
+ try {
24
+ const { templateName, to, params, subject } = payload.data;
25
+ await commandBus.execute('sendNotification', { templateName, to, params, subject });
26
+ channel.sendToQueue(msg.properties.replyTo, Buffer.from(JSON.stringify({ status: 'ok' })), {
27
+ correlationId: msg.properties.correlationId,
28
+ });
29
+ } catch (error) {
30
+ logger.error("Error sending notification: " + error);
31
+ channel.sendToQueue(msg.properties.replyTo, Buffer.from(JSON.stringify({ error: 'Failed to send notification' })), {
32
+ correlationId: msg.properties.correlationId,
33
+ });
34
+ } finally {
35
+ channel.ack(msg);
36
+ }
37
+ break;
38
+
39
+ case 'healthCheck':
40
+ await this.handleHealthCheck(msg, channel);
41
+ break;
42
+
43
+ default:
44
+ await this.handleMethodNotFound(msg, channel);
45
+ }
46
+ },
47
+
48
+ // Handler for the health check method
49
+ async handleHealthCheck(msg, channel) {
50
+ try {
51
+ channel.sendToQueue(msg.properties.replyTo, Buffer.from(JSON.stringify({ status: 'ok' })), {
52
+ correlationId: msg.properties.correlationId,
53
+ });
54
+ } catch (error) {
55
+ logger.error("Error running health check:", error);
56
+ } finally {
57
+ channel.ack(msg);
58
+ }
59
+ },
60
+
61
+ // Handler for unknown methods
62
+ async handleMethodNotFound(msg, channel) {
63
+ try {
64
+ channel.sendToQueue(msg.properties.replyTo, Buffer.from(JSON.stringify({ error: 'Method not found' })), {
65
+ correlationId: msg.properties.correlationId,
66
+ });
67
+ } catch (error) {
68
+ logger.error("Error handling method not found:", error);
69
+ } finally {
70
+ channel.ack(msg);
71
+ }
72
+ },
73
+
74
+ // Initialize RabbitMQ and consumers
75
+ init: async function () {
76
+ await initializeRabbitMQ(
77
+ queueName,
78
+ prefetch,
79
+ this.handleMessage.bind(this)
80
+ );
81
+ },
82
+ }
@@ -0,0 +1,16 @@
1
+ import pino from 'pino';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+
5
+ const __dirname = path.dirname(new URL(import.meta.url).pathname);
6
+
7
+ // Create a logger instance
8
+ export const logger = pino({
9
+ level: process.env.LOG_MODE || 'info',
10
+ transport: {
11
+ target: 'pino-pretty', // Pretty print logs for the console
12
+ options: {
13
+ colorize: true, // Colorize the logs in the console
14
+ }
15
+ }
16
+ });