backend-manager 5.0.91 → 5.0.93

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 (61) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/CLAUDE.md +14 -6
  3. package/README.md +6 -6
  4. package/TODO-MARKETING.md +3 -0
  5. package/TODO-PAYMENT-v2.md +71 -0
  6. package/TODO.md +7 -0
  7. package/package.json +3 -3
  8. package/src/cli/commands/{emulators.js → emulator.js} +15 -15
  9. package/src/cli/commands/index.js +1 -1
  10. package/src/cli/commands/setup-tests/{emulators-config.js → emulator-config.js} +4 -4
  11. package/src/cli/commands/setup-tests/index.js +2 -2
  12. package/src/cli/commands/setup-tests/project-id-consistency.js +1 -1
  13. package/src/cli/commands/test.js +16 -16
  14. package/src/cli/index.js +4 -4
  15. package/src/manager/events/auth/on-create.js +5 -158
  16. package/src/manager/events/firestore/payments-webhooks/analytics.js +4 -3
  17. package/src/manager/events/firestore/payments-webhooks/on-write.js +56 -6
  18. package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +3 -3
  19. package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-failed.js +1 -1
  20. package/src/manager/events/firestore/payments-webhooks/transitions/send-email.js +32 -28
  21. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +3 -3
  22. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +3 -3
  23. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +3 -3
  24. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +3 -3
  25. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +3 -3
  26. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +3 -3
  27. package/src/manager/functions/core/actions/api/admin/send-email.js +0 -131
  28. package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +2 -137
  29. package/src/manager/functions/core/actions/api/general/emails/general:download-app-link.js +2 -2
  30. package/src/manager/functions/core/actions/api/user/sign-up.js +1 -1
  31. package/src/manager/index.js +12 -0
  32. package/src/manager/libraries/email.js +523 -0
  33. package/src/manager/libraries/infer-contact.js +140 -0
  34. package/src/manager/libraries/prompts/infer-contact.md +43 -0
  35. package/src/manager/routes/admin/backup/post.js +4 -3
  36. package/src/manager/routes/admin/email/post.js +11 -428
  37. package/src/manager/routes/admin/hook/post.js +3 -2
  38. package/src/manager/routes/admin/notification/post.js +14 -12
  39. package/src/manager/routes/admin/post/post.js +5 -6
  40. package/src/manager/routes/admin/post/put.js +3 -2
  41. package/src/manager/routes/admin/stats/get.js +19 -10
  42. package/src/manager/routes/general/email/post.js +8 -21
  43. package/src/manager/routes/general/email/templates/download-app-link.js +2 -2
  44. package/src/manager/routes/marketing/contact/post.js +2 -100
  45. package/src/manager/routes/payments/intent/post.js +0 -2
  46. package/src/manager/routes/payments/intent/processors/test.js +9 -10
  47. package/src/manager/routes/user/oauth2/_helpers.js +3 -2
  48. package/src/manager/routes/user/oauth2/delete.js +3 -3
  49. package/src/manager/routes/user/oauth2/get.js +2 -2
  50. package/src/manager/routes/user/oauth2/post.js +9 -9
  51. package/src/manager/routes/user/sessions/delete.js +4 -3
  52. package/src/manager/routes/user/signup/post.js +254 -54
  53. package/src/manager/schemas/admin/email/post.js +13 -8
  54. package/src/test/run-tests.js +1 -1
  55. package/test/functions/admin/send-email.js +1 -88
  56. package/test/helpers/email.js +421 -0
  57. package/test/helpers/infer-contact.js +299 -0
  58. package/test/routes/admin/email.js +41 -90
  59. package/REFACTOR-BEM-API.md +0 -76
  60. package/REFACTOR-MIDDLEWARE.md +0 -62
  61. package/REFACTOR-PAYMENT.md +0 -66
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Test: libraries/infer-contact.js
3
+ * Unit tests for contact inference from email addresses
4
+ *
5
+ * Tests capitalize, inferContactFromEmail (regex), and inferContact (AI fallback).
6
+ * AI tests only run when TEST_EXTENDED_MODE is set.
7
+ */
8
+ const { inferContact, inferContactFromEmail, capitalize } = require('../../src/manager/libraries/infer-contact.js');
9
+
10
+ module.exports = {
11
+ description: 'Infer contact from email',
12
+ type: 'group',
13
+
14
+ tests: [
15
+ // ─── capitalize ───
16
+
17
+ {
18
+ name: 'capitalize-single-word',
19
+ async run({ assert }) {
20
+ assert.equal(capitalize('john'), 'John', 'Should capitalize first letter');
21
+ },
22
+ },
23
+
24
+ {
25
+ name: 'capitalize-multiple-words',
26
+ async run({ assert }) {
27
+ assert.equal(capitalize('john doe'), 'John Doe', 'Should capitalize each word');
28
+ },
29
+ },
30
+
31
+ {
32
+ name: 'capitalize-all-uppercase',
33
+ async run({ assert }) {
34
+ assert.equal(capitalize('JOHN'), 'John', 'Should lowercase after first letter');
35
+ },
36
+ },
37
+
38
+ {
39
+ name: 'capitalize-mixed-case',
40
+ async run({ assert }) {
41
+ assert.equal(capitalize('jOHN dOE'), 'John Doe', 'Should normalize mixed case');
42
+ },
43
+ },
44
+
45
+ {
46
+ name: 'capitalize-empty-string',
47
+ async run({ assert }) {
48
+ assert.equal(capitalize(''), '', 'Empty string should return empty');
49
+ },
50
+ },
51
+
52
+ {
53
+ name: 'capitalize-null',
54
+ async run({ assert }) {
55
+ assert.equal(capitalize(null), '', 'Null should return empty');
56
+ },
57
+ },
58
+
59
+ {
60
+ name: 'capitalize-undefined',
61
+ async run({ assert }) {
62
+ assert.equal(capitalize(undefined), '', 'Undefined should return empty');
63
+ },
64
+ },
65
+
66
+ // ─── inferContactFromEmail: name parsing ───
67
+
68
+ {
69
+ name: 'regex-first-dot-last',
70
+ async run({ assert }) {
71
+ const result = inferContactFromEmail('john.doe@gmail.com');
72
+
73
+ assert.equal(result.firstName, 'John', 'First name from local part before dot');
74
+ assert.equal(result.lastName, 'Doe', 'Last name from local part after dot');
75
+ assert.equal(result.method, 'regex', 'Method should be regex');
76
+ assert.equal(result.confidence, 0.5, 'Two-part name should have 0.5 confidence');
77
+ },
78
+ },
79
+
80
+ {
81
+ name: 'regex-first-underscore-last',
82
+ async run({ assert }) {
83
+ const result = inferContactFromEmail('jane_smith@yahoo.com');
84
+
85
+ assert.equal(result.firstName, 'Jane', 'First name from local part before underscore');
86
+ assert.equal(result.lastName, 'Smith', 'Last name from local part after underscore');
87
+ },
88
+ },
89
+
90
+ {
91
+ name: 'regex-first-hyphen-last',
92
+ async run({ assert }) {
93
+ const result = inferContactFromEmail('bob-jones@hotmail.com');
94
+
95
+ assert.equal(result.firstName, 'Bob', 'First name from local part before hyphen');
96
+ assert.equal(result.lastName, 'Jones', 'Last name from local part after hyphen');
97
+ },
98
+ },
99
+
100
+ {
101
+ name: 'regex-three-part-name',
102
+ async run({ assert }) {
103
+ const result = inferContactFromEmail('mary.jane.watson@example.com');
104
+
105
+ assert.equal(result.firstName, 'Mary', 'First name is first part');
106
+ assert.equal(result.lastName, 'Jane Watson', 'Last name joins remaining parts');
107
+ },
108
+ },
109
+
110
+ {
111
+ name: 'regex-single-word-local-part',
112
+ async run({ assert }) {
113
+ const result = inferContactFromEmail('admin@example.com');
114
+
115
+ assert.equal(result.firstName, 'Admin', 'Single word becomes first name');
116
+ assert.equal(result.lastName, '', 'No last name for single word');
117
+ assert.equal(result.confidence, 0.25, 'Single-part name should have 0.25 confidence');
118
+ },
119
+ },
120
+
121
+ {
122
+ name: 'regex-strips-trailing-numbers',
123
+ async run({ assert }) {
124
+ const result = inferContactFromEmail('john.doe42@gmail.com');
125
+
126
+ assert.equal(result.firstName, 'John', 'Trailing numbers stripped before parsing');
127
+ assert.equal(result.lastName, 'Doe', 'Name parsed correctly after stripping');
128
+ },
129
+ },
130
+
131
+ {
132
+ name: 'regex-only-numbers-after-name',
133
+ async run({ assert }) {
134
+ const result = inferContactFromEmail('user123@gmail.com');
135
+
136
+ assert.equal(result.firstName, 'User', 'Numbers stripped, name capitalized');
137
+ assert.equal(result.lastName, '', 'No last name');
138
+ },
139
+ },
140
+
141
+ // ─── inferContactFromEmail: company inference ───
142
+
143
+ {
144
+ name: 'regex-company-from-custom-domain',
145
+ async run({ assert }) {
146
+ const result = inferContactFromEmail('john@acme.com');
147
+
148
+ assert.equal(result.company, 'Acme', 'Company from custom domain');
149
+ },
150
+ },
151
+
152
+ {
153
+ name: 'regex-company-from-hyphenated-domain',
154
+ async run({ assert }) {
155
+ const result = inferContactFromEmail('john@my-company.com');
156
+
157
+ assert.equal(result.company, 'My Company', 'Hyphens replaced with spaces');
158
+ },
159
+ },
160
+
161
+ {
162
+ name: 'regex-company-from-underscored-domain',
163
+ async run({ assert }) {
164
+ const result = inferContactFromEmail('john@my_company.com');
165
+
166
+ assert.equal(result.company, 'My Company', 'Underscores replaced with spaces');
167
+ },
168
+ },
169
+
170
+ {
171
+ name: 'regex-no-company-from-gmail',
172
+ async run({ assert }) {
173
+ const result = inferContactFromEmail('john@gmail.com');
174
+
175
+ assert.equal(result.company, '', 'Gmail is generic, no company');
176
+ },
177
+ },
178
+
179
+ {
180
+ name: 'regex-no-company-from-yahoo',
181
+ async run({ assert }) {
182
+ const result = inferContactFromEmail('john@yahoo.com');
183
+
184
+ assert.equal(result.company, '', 'Yahoo is generic, no company');
185
+ },
186
+ },
187
+
188
+ {
189
+ name: 'regex-no-company-from-outlook',
190
+ async run({ assert }) {
191
+ const result = inferContactFromEmail('john@outlook.com');
192
+
193
+ assert.equal(result.company, '', 'Outlook is generic, no company');
194
+ },
195
+ },
196
+
197
+ {
198
+ name: 'regex-no-company-from-protonmail',
199
+ async run({ assert }) {
200
+ const result = inferContactFromEmail('john@protonmail.com');
201
+
202
+ assert.equal(result.company, '', 'Protonmail is generic, no company');
203
+ },
204
+ },
205
+
206
+ {
207
+ name: 'regex-no-company-from-icloud',
208
+ async run({ assert }) {
209
+ const result = inferContactFromEmail('john@icloud.com');
210
+
211
+ assert.equal(result.company, '', 'iCloud is generic, no company');
212
+ },
213
+ },
214
+
215
+ {
216
+ name: 'regex-generic-domain-case-insensitive',
217
+ async run({ assert }) {
218
+ const result = inferContactFromEmail('john@GMAIL.COM');
219
+
220
+ assert.equal(result.company, '', 'Generic domain check should be case-insensitive');
221
+ },
222
+ },
223
+
224
+ // ─── inferContactFromEmail: combined name + company ───
225
+
226
+ {
227
+ name: 'regex-full-result-custom-domain',
228
+ async run({ assert }) {
229
+ const result = inferContactFromEmail('sarah.connor@skynet.io');
230
+
231
+ assert.equal(result.firstName, 'Sarah', 'First name parsed');
232
+ assert.equal(result.lastName, 'Connor', 'Last name parsed');
233
+ assert.equal(result.company, 'Skynet', 'Company from domain');
234
+ assert.equal(result.method, 'regex', 'Method is regex');
235
+ assert.equal(result.confidence, 0.5, 'Confidence 0.5 for two-part name');
236
+ },
237
+ },
238
+
239
+ {
240
+ name: 'regex-single-name-custom-domain',
241
+ async run({ assert }) {
242
+ const result = inferContactFromEmail('info@acme.com');
243
+
244
+ assert.equal(result.firstName, 'Info', 'Single word capitalized');
245
+ assert.equal(result.lastName, '', 'No last name');
246
+ assert.equal(result.company, 'Acme', 'Company inferred');
247
+ assert.equal(result.confidence, 0.25, 'Lower confidence for single name');
248
+ },
249
+ },
250
+
251
+ // ─── inferContact: regex fallback (no OPENAI_API_KEY) ───
252
+
253
+ {
254
+ name: 'infer-contact-regex-fallback',
255
+ async run({ assert }) {
256
+ // Temporarily clear OPENAI_API_KEY to force regex path
257
+ const originalKey = process.env.OPENAI_API_KEY;
258
+ delete process.env.OPENAI_API_KEY;
259
+
260
+ try {
261
+ const result = await inferContact('alice.wonderland@example.com');
262
+
263
+ assert.equal(result.firstName, 'Alice', 'Regex fallback first name');
264
+ assert.equal(result.lastName, 'Wonderland', 'Regex fallback last name');
265
+ assert.equal(result.company, 'Example', 'Regex fallback company');
266
+ assert.equal(result.method, 'regex', 'Should use regex when no API key');
267
+ } finally {
268
+ // Restore
269
+ if (originalKey) {
270
+ process.env.OPENAI_API_KEY = originalKey;
271
+ }
272
+ }
273
+ },
274
+ },
275
+
276
+ // ─── inferContact: AI path (requires TEST_EXTENDED_MODE) ───
277
+
278
+ {
279
+ name: 'infer-contact-ai',
280
+ skip: !process.env.TEST_EXTENDED_MODE ? 'TEST_EXTENDED_MODE not set (skipping AI inference test)' : false,
281
+ timeout: 30000,
282
+
283
+ async run({ assert, Manager }) {
284
+ // This test requires a real OPENAI_API_KEY and running Manager
285
+ if (!process.env.OPENAI_API_KEY) {
286
+ return assert.fail('OPENAI_API_KEY not set');
287
+ }
288
+
289
+ const assistant = Manager.Assistant();
290
+ const result = await inferContact('john.smith@microsoft.com', assistant);
291
+
292
+ assert.ok(result, 'Should return a result');
293
+ assert.ok(result.firstName, 'Should infer a first name');
294
+ assert.equal(result.method, 'ai', 'Should use AI method');
295
+ assert.ok(typeof result.confidence === 'number', 'Confidence should be a number');
296
+ },
297
+ },
298
+ ],
299
+ };
@@ -1,14 +1,48 @@
1
1
  /**
2
2
  * Test: POST /admin/email
3
- * Tests the admin send email endpoint
4
- * Requires admin authentication and SendGrid API key configured
3
+ * Route-level tests: auth, permissions, HTTP status codes, basic send/queue
4
+ *
5
+ * Library-level tests (validation, dedup, recipients, features) are in test/helpers/email.js
5
6
  */
6
7
  module.exports = {
7
- description: 'Admin send email',
8
+ description: 'Admin send email (route)',
8
9
  type: 'group',
9
10
  skip: !process.env.TEST_EXTENDED_MODE ? 'TEST_EXTENDED_MODE env var not set (skipping email tests)' : false,
10
11
  tests: [
11
- // Test 1: Missing subject returns 400 error
12
+ // --- Auth / Permissions ---
13
+
14
+ {
15
+ name: 'unauthenticated-rejected',
16
+ auth: 'none',
17
+ timeout: 15000,
18
+
19
+ async run({ http, assert, config }) {
20
+ const response = await http.post('admin/email', {
21
+ subject: 'Test Email',
22
+ to: [{ email: `_test-receiver@${config.domain}` }],
23
+ });
24
+
25
+ assert.isError(response, 401, 'Send email should fail without authentication');
26
+ },
27
+ },
28
+
29
+ {
30
+ name: 'non-admin-rejected',
31
+ auth: 'basic',
32
+ timeout: 15000,
33
+
34
+ async run({ http, assert, config }) {
35
+ const response = await http.post('admin/email', {
36
+ subject: 'Test Email',
37
+ to: [{ email: `_test-receiver@${config.domain}` }],
38
+ });
39
+
40
+ assert.isError(response, 403, 'Send email should fail for non-admin user');
41
+ },
42
+ },
43
+
44
+ // --- Basic Validation ---
45
+
12
46
  {
13
47
  name: 'missing-subject-rejected',
14
48
  auth: 'admin',
@@ -18,14 +52,14 @@ module.exports = {
18
52
  const response = await http.post('admin/email', {
19
53
  to: [{ email: `_test-receiver@${config.domain}` }],
20
54
  copy: false,
21
- ensureUnique: false,
22
55
  });
23
56
 
24
57
  assert.isError(response, 400, 'Missing subject should return 400');
25
58
  },
26
59
  },
27
60
 
28
- // Test 2: Status 'sent' - Email sent successfully via SendGrid
61
+ // --- Happy Path ---
62
+
29
63
  {
30
64
  name: 'status-sent',
31
65
  auth: 'admin',
@@ -36,7 +70,6 @@ module.exports = {
36
70
  subject: 'BEM Test Email - Status Sent',
37
71
  to: [{ email: `_test-receiver@${config.domain}`, name: 'Test Receiver' }],
38
72
  copy: false,
39
- ensureUnique: false,
40
73
  data: {
41
74
  email: {
42
75
  subject: 'BEM Test Email - Status Sent',
@@ -47,11 +80,10 @@ module.exports = {
47
80
 
48
81
  assert.isSuccess(response, 'Admin should be able to send email');
49
82
  assert.hasProperty(response, 'data.status', 'Response should have status');
50
- assert.equal(response.data.status, 'sent', 'Status should be sent when ensureUnique is false');
83
+ assert.equal(response.data.status, 'sent', 'Status should be sent');
51
84
  },
52
85
  },
53
86
 
54
- // Test 3: Status 'queued' - Email scheduled beyond 71 hours
55
87
  {
56
88
  name: 'status-queued',
57
89
  auth: 'admin',
@@ -64,7 +96,6 @@ module.exports = {
64
96
  subject: 'BEM Test Email - Status Queued',
65
97
  to: [{ email: `_test-receiver@${config.domain}`, name: 'Test Receiver' }],
66
98
  copy: false,
67
- ensureUnique: false,
68
99
  sendAt: sendAt,
69
100
  data: {
70
101
  email: {
@@ -79,85 +110,5 @@ module.exports = {
79
110
  assert.equal(response.data.status, 'queued', 'Status should be queued when sendAt is beyond 71 hours');
80
111
  },
81
112
  },
82
-
83
- // Test 4: Status 'non-unique' - Duplicate email detected
84
- {
85
- name: 'status-non-unique',
86
- auth: 'admin',
87
- timeout: 120000,
88
-
89
- async run({ http, assert, config }) {
90
- const uniqueSubject = `BEM Test Email - Unique Check ${Date.now()}`;
91
-
92
- const response1Promise = http.post('admin/email', {
93
- subject: uniqueSubject,
94
- to: [{ email: `_test-receiver@${config.domain}` }],
95
- copy: false,
96
- ensureUnique: true,
97
- categories: ['bem-test-unique'],
98
- data: {
99
- email: {
100
- subject: uniqueSubject,
101
- body: 'Testing ensureUnique feature.',
102
- },
103
- },
104
- });
105
-
106
- const response2Promise = http.post('admin/email', {
107
- subject: uniqueSubject,
108
- to: [{ email: `_test-receiver@${config.domain}` }],
109
- copy: false,
110
- ensureUnique: true,
111
- categories: ['bem-test-unique'],
112
- data: {
113
- email: {
114
- subject: uniqueSubject,
115
- body: 'Testing ensureUnique feature.',
116
- },
117
- },
118
- });
119
-
120
- const [response1, response2] = await Promise.all([response1Promise, response2Promise]);
121
-
122
- assert.isSuccess(response1, 'First email should succeed');
123
- assert.isSuccess(response2, 'Second email should succeed');
124
-
125
- const statuses = [response1.data.status, response2.data.status].sort();
126
- assert.equal(statuses[0], 'non-unique', 'One email should have status non-unique');
127
- assert.equal(statuses[1], 'sent', 'One email should have status sent');
128
- },
129
- },
130
-
131
- // Test 5: Unauthenticated request fails
132
- {
133
- name: 'unauthenticated-rejected',
134
- auth: 'none',
135
- timeout: 15000,
136
-
137
- async run({ http, assert, config }) {
138
- const response = await http.post('admin/email', {
139
- subject: 'Test Email',
140
- to: [{ email: `_test-receiver@${config.domain}` }],
141
- });
142
-
143
- assert.isError(response, 401, 'Send email should fail without authentication');
144
- },
145
- },
146
-
147
- // Test 6: Non-admin user fails
148
- {
149
- name: 'non-admin-rejected',
150
- auth: 'basic',
151
- timeout: 15000,
152
-
153
- async run({ http, assert, config }) {
154
- const response = await http.post('admin/email', {
155
- subject: 'Test Email',
156
- to: [{ email: `_test-receiver@${config.domain}` }],
157
- });
158
-
159
- assert.isError(response, 403, 'Send email should fail for non-admin user');
160
- },
161
- },
162
113
  ],
163
114
  };
@@ -1,76 +0,0 @@
1
- We need to do a pretty significatn refactor of our BEM API now.
2
-
3
- Since that was orignally implemented, I built a much better process for hading incoming http requests. That is, the route/schema system found here:
4
- /Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/src/manager/helpers/assistant.js
5
- /Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/src/manager/helpers/middleware.js
6
- /Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/src/manager/helpers/settings.js
7
-
8
- You can see an example of it in our consuming project:
9
- /Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-backend/functions/index.js
10
- /Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-backend/functions/routes/example/index.js
11
- /Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-backend/functions/schemas/example/index.js
12
-
13
- We built a single unified bem_api function that handles all http requests in a single place, and then routes them, see here:
14
- /Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/src/manager/index.js
15
- .https.onRequest(async (req, res) => self._process((new (require(`${core}/actions/api.js`))()).init(self, { req: req, res: res, })));
16
-
17
- We should refactor this system to USE THE NEW ROUTE/SCHEMA SYSTEM rather than the old way of doing things. This will make it much easier to maintain and extend in the future.
18
-
19
- We can start with a simple one like:
20
- /Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/src/manager/functions/core/actions/api/general/generate-uuid.js
21
-
22
- and once we perfect that we can move on to the others.
23
-
24
- For each BEM refactor, we should create a route and a schema for the expected input. As you can see, BEM APIs epect a command and payload in the body, requiring it to be a POST operation. I would like to rebuild this system to be more proper, so that each BEM api can be GET, POST, etc as appropriate.
25
-
26
- Previously:
27
- request('/backend-manager', {
28
- method: 'POST',
29
- body: {
30
- command: 'generate-uuid',
31
- payload: { ... }
32
- }
33
- })
34
-
35
- but i think it owud be more intuitive if going forward we just had endpoints like:
36
- request('/backend-manager/general:uuid', {
37
- method: 'POST',
38
- body: { ... }
39
- })
40
- OR
41
- request('/backend-manager/general/uuid', {
42
- method: 'POST',
43
- body: { ... }
44
- })
45
-
46
- Im not sure how we can do this to be backwards compatible with existing BEM API consumers, but it does need to be backwards compatible.
47
-
48
- If you look at the firebase.json in the consuming project we can see that
49
- /Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-backend/firebase.json
50
- {
51
- "source": "/backend-manager",
52
- "function": "bm_api"
53
- },
54
-
55
- So maybe we could make it:
56
- {
57
- "source": "/backend-manager/**",
58
- "function": "bm_api"
59
- },
60
- and then parse the route inside the bem_api function to determine which route/schema to use, falling back to the old system if the route is just /backend-manager with a "command" in the body, and then use the new route/schema system if the path is /backend-manager/something?
61
-
62
- Either way, i think we need a minimal intermediary step where we determine which one to use based on the incoming request and then either just route to the old one or route to the new "middleware", "settings", route/schema system
63
-
64
- I would like each new route to have a great name clearly indicating its purpose, the method should be appropriate for the action (GET for fetches, POST for creates, etc) and the schema should be well defined for each route.
65
-
66
- Since we can build this new API system however we want, i also expect you to rewrite and refactor the BEM api endppints to be kickass, modern, and well designed.
67
-
68
- Also, certain VERBS should be removed from the actual file/function names since they are implied by the HTTP method. For example, instead of having a generate-uuid.js file, we could just have uuid.js since the POST method implies that we are generating/creating a new one, or insetad of add-marketing-contact we could just have marketing-contact.js since the POST method implies adding a new one and GET would imply fetching them.
69
-
70
- Next, some fucntions have a lot crammed isnide them that could use some separation. For example, in add-marketing-contact.js we have code for handling multiple email providers (SendGrid, Beehiiv) all jammed into a single file. I think we should refactor this to have a separate file for each provider in subfolder that handles the specifics of that provider, and then the main route file just calls those provider-specific files as needed. This will make it much easier to maintain and extend in the future as we add more providers.
71
-
72
- Also, sometimes there are two endpoints that should be combined,for example
73
- * /Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/src/manager/functions/core/actions/api/general/add-marketing-contact.js
74
- * /Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/src/manager/functions/core/actions/api/general/remove-marketing-contact.js
75
- These could be combined into a single marketing-contact.js file that handles both adding and removing based on the HTTP method (POST for add, DELETE for remove). This will make the API more RESTful and easier to understand.
76
-
@@ -1,62 +0,0 @@
1
- MIDDLEWARE REFACTOR
2
- We have a system where we handle incoming requests using a route/schema system found here:
3
- /Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/src/manager/helpers/assistant.js
4
- /Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/src/manager/helpers/middleware.js
5
- /Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/src/manager/helpers/settings.js
6
-
7
- You can see an example of it in our consuming project:
8
- /Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-backend/functions/index.js
9
- /Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-backend/functions/routes/example/index.js
10
- /Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-backend/functions/schemas/example/index.js
11
-
12
- I have some ideas iw as thinking about and id like to know your thoughts:
13
- * new design so that each route is modern JS that does a single export instead of exportting a class with an init() and a main() method.
14
- * schema system currently uses the user's plan when designing the schma like
15
- module.exports = function (assistant) {
16
- return {
17
- // DEFAULTS
18
- ['defaults']: {
19
- key: {
20
- types: ['string'],
21
- value: undefined,
22
- default: '',
23
- required: false,
24
- min: 0,
25
- max: 2048,
26
- },
27
- },
28
-
29
- // Premium plan
30
- ['premium']: {
31
- key: {
32
- types: ['string'],
33
- value: undefined,
34
- default: 'premium-default',
35
- required: false,
36
- min: 0,
37
- max: 4096,
38
- },
39
- }
40
- };
41
- }
42
-
43
- however it hink we should instead eliminate the toplevel plan/default system and code each plan changes into the individual keys like:
44
- const schema = {
45
- id: {
46
- types: ['string'],
47
- value: () => assistant.Manager.Utilities().randomId(),
48
- required: false,
49
- },
50
- feature: {
51
- types: ['string'],
52
- default: '',
53
- required: true,
54
- min: 1,
55
- max: 4,
56
- },
57
- };
58
-
59
- // Adjust schema based on plan
60
- if (assistant.user.plan === 'premium') {
61
- schema.feature.max = 8;
62
- }