@marcoappio/marco-config 2.0.507 → 2.0.509

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 (97) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/types/Zero.d.ts +0 -17
  4. package/dist/types/Zero.d.ts.map +1 -1
  5. package/dist/zero/index.d.ts +2916 -7769
  6. package/dist/zero/index.d.ts.map +1 -1
  7. package/dist/zero/index.js +6 -16
  8. package/dist/zero/{queries/getThreadList.d.ts → mutators.d.ts} +572 -169
  9. package/dist/zero/{queries/getThreads.d.ts.map → mutators.d.ts.map} +1 -1
  10. package/dist/zero/mutators.js +659 -0
  11. package/dist/zero/mutators.test.d.ts +2 -0
  12. package/dist/zero/mutators.test.d.ts.map +1 -0
  13. package/dist/zero/mutators.test.js +603 -0
  14. package/dist/zero/{queries/getThreads.d.ts → queries.d.ts} +481 -196
  15. package/dist/zero/{queries/getThread.d.ts.map → queries.d.ts.map} +1 -1
  16. package/dist/zero/queries.js +168 -0
  17. package/dist/zero/schema.d.ts +138 -133
  18. package/dist/zero/schema.d.ts.map +1 -1
  19. package/dist/zero/schema.js +1 -1
  20. package/package.json +3 -3
  21. package/dist/zero/mutatorSchemas/account.d.ts +0 -80
  22. package/dist/zero/mutatorSchemas/account.d.ts.map +0 -1
  23. package/dist/zero/mutatorSchemas/account.js +0 -66
  24. package/dist/zero/mutatorSchemas/draft.d.ts +0 -150
  25. package/dist/zero/mutatorSchemas/draft.d.ts.map +0 -1
  26. package/dist/zero/mutatorSchemas/draft.js +0 -104
  27. package/dist/zero/mutatorSchemas/index.d.ts +0 -323
  28. package/dist/zero/mutatorSchemas/index.d.ts.map +0 -1
  29. package/dist/zero/mutatorSchemas/index.js +0 -10
  30. package/dist/zero/mutatorSchemas/thread.d.ts +0 -71
  31. package/dist/zero/mutatorSchemas/thread.d.ts.map +0 -1
  32. package/dist/zero/mutatorSchemas/thread.js +0 -47
  33. package/dist/zero/mutatorSchemas/user.d.ts +0 -26
  34. package/dist/zero/mutatorSchemas/user.d.ts.map +0 -1
  35. package/dist/zero/mutatorSchemas/user.js +0 -27
  36. package/dist/zero/mutators/accountMutators/accountMutators.d.ts +0 -4
  37. package/dist/zero/mutators/accountMutators/accountMutators.d.ts.map +0 -1
  38. package/dist/zero/mutators/accountMutators/accountMutators.js +0 -103
  39. package/dist/zero/mutators/accountMutators/accountMutators.test.d.ts +0 -2
  40. package/dist/zero/mutators/accountMutators/accountMutators.test.d.ts.map +0 -1
  41. package/dist/zero/mutators/accountMutators/accountMutators.test.js +0 -372
  42. package/dist/zero/mutators/accountMutators/index.d.ts +0 -2
  43. package/dist/zero/mutators/accountMutators/index.d.ts.map +0 -1
  44. package/dist/zero/mutators/accountMutators/index.js +0 -1
  45. package/dist/zero/mutators/draftMutators/draftMutators.d.ts +0 -4
  46. package/dist/zero/mutators/draftMutators/draftMutators.d.ts.map +0 -1
  47. package/dist/zero/mutators/draftMutators/draftMutators.js +0 -142
  48. package/dist/zero/mutators/draftMutators/draftMutators.test.d.ts +0 -2
  49. package/dist/zero/mutators/draftMutators/draftMutators.test.d.ts.map +0 -1
  50. package/dist/zero/mutators/draftMutators/draftMutators.test.js +0 -416
  51. package/dist/zero/mutators/draftMutators/index.d.ts +0 -2
  52. package/dist/zero/mutators/draftMutators/index.d.ts.map +0 -1
  53. package/dist/zero/mutators/draftMutators/index.js +0 -1
  54. package/dist/zero/mutators/index.d.ts +0 -3
  55. package/dist/zero/mutators/index.d.ts.map +0 -1
  56. package/dist/zero/mutators/index.js +0 -1
  57. package/dist/zero/mutators/mutators.d.ts +0 -18
  58. package/dist/zero/mutators/mutators.d.ts.map +0 -1
  59. package/dist/zero/mutators/mutators.js +0 -39
  60. package/dist/zero/mutators/threadMutators/index.d.ts +0 -2
  61. package/dist/zero/mutators/threadMutators/index.d.ts.map +0 -1
  62. package/dist/zero/mutators/threadMutators/index.js +0 -1
  63. package/dist/zero/mutators/threadMutators/threadMutators.d.ts +0 -8
  64. package/dist/zero/mutators/threadMutators/threadMutators.d.ts.map +0 -1
  65. package/dist/zero/mutators/threadMutators/threadMutators.js +0 -257
  66. package/dist/zero/mutators/threadMutators/threadMutators.test.d.ts +0 -2
  67. package/dist/zero/mutators/threadMutators/threadMutators.test.d.ts.map +0 -1
  68. package/dist/zero/mutators/threadMutators/threadMutators.test.js +0 -755
  69. package/dist/zero/mutators/userMutators/index.d.ts +0 -2
  70. package/dist/zero/mutators/userMutators/index.d.ts.map +0 -1
  71. package/dist/zero/mutators/userMutators/index.js +0 -1
  72. package/dist/zero/mutators/userMutators/userMutators.d.ts +0 -4
  73. package/dist/zero/mutators/userMutators/userMutators.d.ts.map +0 -1
  74. package/dist/zero/mutators/userMutators/userMutators.js +0 -28
  75. package/dist/zero/mutators/userMutators/userMutators.test.d.ts +0 -2
  76. package/dist/zero/mutators/userMutators/userMutators.test.d.ts.map +0 -1
  77. package/dist/zero/mutators/userMutators/userMutators.test.js +0 -84
  78. package/dist/zero/queries/getAccounts.d.ts +0 -1060
  79. package/dist/zero/queries/getAccounts.d.ts.map +0 -1
  80. package/dist/zero/queries/getAccounts.js +0 -3
  81. package/dist/zero/queries/getContacts.d.ts +0 -1040
  82. package/dist/zero/queries/getContacts.d.ts.map +0 -1
  83. package/dist/zero/queries/getContacts.js +0 -37
  84. package/dist/zero/queries/getDrafts.d.ts +0 -1061
  85. package/dist/zero/queries/getDrafts.d.ts.map +0 -1
  86. package/dist/zero/queries/getDrafts.js +0 -24
  87. package/dist/zero/queries/getThread.d.ts +0 -1072
  88. package/dist/zero/queries/getThread.js +0 -14
  89. package/dist/zero/queries/getThreadList.d.ts.map +0 -1
  90. package/dist/zero/queries/getThreadList.js +0 -57
  91. package/dist/zero/queries/getThreads.js +0 -59
  92. package/dist/zero/queries/getUser.d.ts +0 -1074
  93. package/dist/zero/queries/getUser.d.ts.map +0 -1
  94. package/dist/zero/queries/getUser.js +0 -7
  95. package/dist/zero/queries/index.d.ts +0 -1031
  96. package/dist/zero/queries/index.d.ts.map +0 -1
  97. package/dist/zero/queries/index.js +0 -10
@@ -0,0 +1,603 @@
1
+ import { beforeEach, describe, expect, it, mock } from 'bun:test';
2
+ import { MutationError } from '../types';
3
+ import { mutators } from './mutators';
4
+ const createMockTx = () => ({
5
+ mutate: {
6
+ account: {
7
+ delete: mock(() => Promise.resolve()),
8
+ insert: mock(() => Promise.resolve()),
9
+ update: mock(() => Promise.resolve()),
10
+ },
11
+ accountAlias: {
12
+ delete: mock(() => Promise.resolve()),
13
+ insert: mock(() => Promise.resolve()),
14
+ update: mock(() => Promise.resolve()),
15
+ },
16
+ accountLabel: { update: mock(() => Promise.resolve()) },
17
+ draft: {
18
+ delete: mock(() => Promise.resolve()),
19
+ insert: mock(() => Promise.resolve()),
20
+ update: mock(() => Promise.resolve()),
21
+ },
22
+ draftAttachment: { delete: mock(() => Promise.resolve()), insert: mock(() => Promise.resolve()) },
23
+ thread: { delete: mock(() => Promise.resolve()), update: mock(() => Promise.resolve()) },
24
+ threadByLabel: { delete: mock(() => Promise.resolve()), insert: mock(() => Promise.resolve()) },
25
+ threadLabel: { delete: mock(() => Promise.resolve()), insert: mock(() => Promise.resolve()) },
26
+ user: { update: mock(() => Promise.resolve()) },
27
+ userPushNotificationToken: { delete: mock(() => Promise.resolve()), insert: mock(() => Promise.resolve()) },
28
+ },
29
+ run: mock(() => Promise.resolve(null)),
30
+ });
31
+ describe('mutators', () => {
32
+ let tx;
33
+ const ctx = { userId: 'test-user-id' };
34
+ beforeEach(() => {
35
+ tx = createMockTx();
36
+ });
37
+ describe('account', () => {
38
+ describe('createAccount', () => {
39
+ it('creates an account and primary alias', async () => {
40
+ const args = {
41
+ aliasId: 'alias-1',
42
+ color: '#ff0000',
43
+ emailAddress: 'test@example.com',
44
+ id: 'account-1',
45
+ };
46
+ await mutators.account.createAccount.fn({ args, ctx, tx: tx });
47
+ expect(tx.mutate.account.insert).toHaveBeenCalledWith({
48
+ color: '#ff0000',
49
+ displayName: null,
50
+ id: 'account-1',
51
+ imapConnectionStatus: 'AWAITING_CONNECTION',
52
+ mailProcessedCount: 0,
53
+ mailTotalCount: 0,
54
+ primaryAliasId: 'alias-1',
55
+ userId: 'test-user-id',
56
+ });
57
+ expect(tx.mutate.accountAlias.insert).toHaveBeenCalledWith({
58
+ accountId: 'account-1',
59
+ emailAddress: 'test@example.com',
60
+ id: 'alias-1',
61
+ isPrimary: true,
62
+ name: null,
63
+ });
64
+ });
65
+ });
66
+ describe('createAlias', () => {
67
+ it('creates a non-primary alias', async () => {
68
+ const args = {
69
+ accountId: 'account-1',
70
+ alias: {
71
+ emailAddress: 'alias@example.com',
72
+ id: 'alias-2',
73
+ name: 'My Alias',
74
+ },
75
+ };
76
+ await mutators.account.createAlias.fn({ args, ctx, tx: tx });
77
+ expect(tx.mutate.accountAlias.insert).toHaveBeenCalledWith({
78
+ accountId: 'account-1',
79
+ emailAddress: 'alias@example.com',
80
+ id: 'alias-2',
81
+ isPrimary: false,
82
+ name: 'My Alias',
83
+ });
84
+ });
85
+ it('creates an alias with null name', async () => {
86
+ const args = {
87
+ accountId: 'account-1',
88
+ alias: {
89
+ emailAddress: 'alias@example.com',
90
+ id: 'alias-2',
91
+ name: null,
92
+ },
93
+ };
94
+ await mutators.account.createAlias.fn({ args, ctx, tx: tx });
95
+ expect(tx.mutate.accountAlias.insert).toHaveBeenCalledWith({
96
+ accountId: 'account-1',
97
+ emailAddress: 'alias@example.com',
98
+ id: 'alias-2',
99
+ isPrimary: false,
100
+ name: null,
101
+ });
102
+ });
103
+ });
104
+ describe('deleteAccount', () => {
105
+ it('deletes an account', async () => {
106
+ const args = { id: 'account-1' };
107
+ await mutators.account.deleteAccount.fn({ args, ctx, tx: tx });
108
+ expect(tx.mutate.account.delete).toHaveBeenCalledWith({ id: 'account-1' });
109
+ });
110
+ });
111
+ describe('deleteAlias', () => {
112
+ it('deletes a non-primary alias', async () => {
113
+ tx.run = mock(() => Promise.resolve({ isPrimary: false }));
114
+ const args = { accountId: 'account-1', aliasId: 'alias-2' };
115
+ await mutators.account.deleteAlias.fn({ args, ctx, tx: tx });
116
+ expect(tx.mutate.accountAlias.delete).toHaveBeenCalledWith({ id: 'alias-2' });
117
+ expect(tx.mutate.accountAlias.update).not.toHaveBeenCalled();
118
+ });
119
+ it('promotes another alias when deleting primary', async () => {
120
+ tx.run = mock()
121
+ .mockResolvedValueOnce({ isPrimary: true })
122
+ .mockResolvedValueOnce([{ id: 'alias-3' }]);
123
+ const args = { accountId: 'account-1', aliasId: 'alias-1' };
124
+ await mutators.account.deleteAlias.fn({ args, ctx, tx: tx });
125
+ expect(tx.mutate.accountAlias.delete).toHaveBeenCalledWith({ id: 'alias-1' });
126
+ expect(tx.mutate.accountAlias.update).toHaveBeenCalledWith({ id: 'alias-3', isPrimary: true });
127
+ expect(tx.mutate.account.update).toHaveBeenCalledWith({ id: 'account-1', primaryAliasId: 'alias-3' });
128
+ });
129
+ it('sets primaryAliasId to null when no remaining aliases', async () => {
130
+ tx.run = mock().mockResolvedValueOnce({ isPrimary: true }).mockResolvedValueOnce([]);
131
+ const args = { accountId: 'account-1', aliasId: 'alias-1' };
132
+ await mutators.account.deleteAlias.fn({ args, ctx, tx: tx });
133
+ expect(tx.mutate.account.update).toHaveBeenCalledWith({ id: 'account-1', primaryAliasId: null });
134
+ });
135
+ });
136
+ describe('setAliasName', () => {
137
+ it('updates alias name', async () => {
138
+ const args = { accountId: 'account-1', aliasId: 'alias-1', displayName: 'New Name' };
139
+ await mutators.account.setAliasName.fn({ args, ctx, tx: tx });
140
+ expect(tx.mutate.accountAlias.update).toHaveBeenCalledWith({ id: 'alias-1', name: 'New Name' });
141
+ });
142
+ });
143
+ describe('setAliasPrimary', () => {
144
+ it('sets a new primary alias', async () => {
145
+ tx.run = mock(() => Promise.resolve([{ id: 'alias-1' }, { id: 'alias-2' }]));
146
+ const args = { accountId: 'account-1', aliasId: 'alias-2' };
147
+ await mutators.account.setAliasPrimary.fn({ args, ctx, tx: tx });
148
+ expect(tx.mutate.accountAlias.update).toHaveBeenCalledWith({ id: 'alias-1', isPrimary: false });
149
+ expect(tx.mutate.accountAlias.update).toHaveBeenCalledWith({ id: 'alias-2', isPrimary: true });
150
+ expect(tx.mutate.account.update).toHaveBeenCalledWith({ id: 'account-1', primaryAliasId: 'alias-2' });
151
+ });
152
+ });
153
+ describe('setConnectionConfigImapRaw', () => {
154
+ it('updates connection status to awaiting', async () => {
155
+ const args = {
156
+ connectionConfig: {
157
+ imapHost: 'imap.example.com',
158
+ imapPassword: 'pass',
159
+ imapPort: 993,
160
+ imapSocketType: 'SSL',
161
+ imapUser: 'user',
162
+ smtpHost: 'smtp.example.com',
163
+ smtpPassword: 'pass',
164
+ smtpPort: 465,
165
+ smtpSocketType: 'SSL',
166
+ smtpUser: 'user',
167
+ },
168
+ id: 'account-1',
169
+ };
170
+ await mutators.account.setConnectionConfigImapRaw.fn({ args, ctx, tx: tx });
171
+ expect(tx.mutate.account.update).toHaveBeenCalledWith({
172
+ id: 'account-1',
173
+ imapConnectionStatus: 'AWAITING_CONNECTION',
174
+ });
175
+ });
176
+ });
177
+ describe('setConnectionConfigOauth', () => {
178
+ it('updates connection status for oauth', async () => {
179
+ const args = {
180
+ connectionConfig: { code: 'auth-code', provider: 'GOOGLE', user: 'user@example.com' },
181
+ id: 'account-1',
182
+ };
183
+ await mutators.account.setConnectionConfigOauth.fn({ args, ctx, tx: tx });
184
+ expect(tx.mutate.account.update).toHaveBeenCalledWith({
185
+ id: 'account-1',
186
+ imapConnectionStatus: 'AWAITING_CONNECTION',
187
+ });
188
+ });
189
+ });
190
+ describe('setSettings', () => {
191
+ it('updates account settings', async () => {
192
+ const args = { color: '#00ff00', displayName: 'Work', id: 'account-1' };
193
+ await mutators.account.setSettings.fn({ args, ctx, tx: tx });
194
+ expect(tx.mutate.account.update).toHaveBeenCalledWith({
195
+ color: '#00ff00',
196
+ displayName: 'Work',
197
+ id: 'account-1',
198
+ });
199
+ });
200
+ });
201
+ });
202
+ describe('draft', () => {
203
+ describe('cancelSend', () => {
204
+ it('cancels a scheduled send', async () => {
205
+ tx.run = mock(() => Promise.resolve({ status: 'SEND_REQUESTED' }));
206
+ const args = { id: 'draft-1', updatedAt: 1234567890 };
207
+ await mutators.draft.cancelSend.fn({ args, ctx, tx: tx });
208
+ expect(tx.mutate.draft.update).toHaveBeenCalledWith({
209
+ id: 'draft-1',
210
+ scheduledFor: null,
211
+ status: 'DRAFT',
212
+ updatedAt: 1234567890,
213
+ });
214
+ });
215
+ it('throws if draft not found', async () => {
216
+ tx.run = mock(() => Promise.resolve(null));
217
+ const args = { id: 'draft-1', updatedAt: 1234567890 };
218
+ await expect(mutators.draft.cancelSend.fn({ args, ctx, tx: tx })).rejects.toThrow(MutationError.ENTITY_NOT_FOUND);
219
+ });
220
+ it('throws if already confirmed', async () => {
221
+ tx.run = mock(() => Promise.resolve({ status: 'SEND_CONFIRMED' }));
222
+ const args = { id: 'draft-1', updatedAt: 1234567890 };
223
+ await expect(mutators.draft.cancelSend.fn({ args, ctx, tx: tx })).rejects.toThrow(MutationError.ALREADY_APPLIED);
224
+ });
225
+ });
226
+ describe('createAttachment', () => {
227
+ it('creates an attachment and updates draft', async () => {
228
+ const args = {
229
+ attachment: {
230
+ fileName: 'file.pdf',
231
+ id: 'attachment-1',
232
+ mimeType: 'application/pdf',
233
+ status: 'PENDING',
234
+ totalSize: 1024,
235
+ },
236
+ id: 'draft-1',
237
+ updatedAt: 1234567890,
238
+ };
239
+ await mutators.draft.createAttachment.fn({ args, ctx, tx: tx });
240
+ expect(tx.mutate.draftAttachment.insert).toHaveBeenCalledWith({
241
+ draftId: 'draft-1',
242
+ fileName: 'file.pdf',
243
+ id: 'attachment-1',
244
+ mimeType: 'application/pdf',
245
+ status: 'PENDING',
246
+ totalSize: 1024,
247
+ });
248
+ expect(tx.mutate.draft.update).toHaveBeenCalledWith({
249
+ id: 'draft-1',
250
+ updatedAt: 1234567890,
251
+ });
252
+ });
253
+ });
254
+ describe('createDraft', () => {
255
+ it('creates a draft with attachments', async () => {
256
+ const args = {
257
+ accountId: 'account-1',
258
+ attachments: [
259
+ {
260
+ fileName: 'file.pdf',
261
+ id: 'att-1',
262
+ mimeType: 'application/pdf',
263
+ status: 'COMPLETE',
264
+ totalSize: 1024,
265
+ },
266
+ ],
267
+ body: { bcc: [], cc: ['cc@example.com'], content: 'Hello', subject: 'Test', to: ['to@example.com'] },
268
+ error: null,
269
+ from: 'from@example.com',
270
+ fromName: 'Sender',
271
+ id: 'draft-1',
272
+ referencedMessageId: null,
273
+ scheduledFor: null,
274
+ status: 'DRAFT',
275
+ type: 'NEW',
276
+ updatedAt: 1234567890,
277
+ };
278
+ await mutators.draft.createDraft.fn({ args, ctx, tx: tx });
279
+ expect(tx.mutate.draft.insert).toHaveBeenCalledWith({
280
+ accountId: 'account-1',
281
+ body: { bcc: [], cc: ['cc@example.com'], content: 'Hello', to: ['to@example.com'] },
282
+ error: null,
283
+ fromAliasId: null,
284
+ fromEmail: 'from@example.com',
285
+ fromName: 'Sender',
286
+ id: 'draft-1',
287
+ referencedMessageId: null,
288
+ scheduledFor: null,
289
+ status: 'DRAFT',
290
+ subject: 'Test',
291
+ type: 'NEW',
292
+ updatedAt: 1234567890,
293
+ userId: 'test-user-id',
294
+ });
295
+ expect(tx.mutate.draftAttachment.insert).toHaveBeenCalledWith({
296
+ draftId: 'draft-1',
297
+ fileName: 'file.pdf',
298
+ id: 'att-1',
299
+ mimeType: 'application/pdf',
300
+ status: 'COMPLETE',
301
+ totalSize: 1024,
302
+ });
303
+ });
304
+ });
305
+ describe('deleteAttachment', () => {
306
+ it('deletes an attachment and updates draft', async () => {
307
+ const args = { attachmentId: 'att-1', id: 'draft-1', updatedAt: 1234567890 };
308
+ await mutators.draft.deleteAttachment.fn({ args, ctx, tx: tx });
309
+ expect(tx.mutate.draftAttachment.delete).toHaveBeenCalledWith({ id: 'att-1' });
310
+ expect(tx.mutate.draft.update).toHaveBeenCalledWith({ id: 'draft-1', updatedAt: 1234567890 });
311
+ });
312
+ });
313
+ describe('deleteDraft', () => {
314
+ it('deletes a draft', async () => {
315
+ const args = { id: 'draft-1' };
316
+ await mutators.draft.deleteDraft.fn({ args, ctx, tx: tx });
317
+ expect(tx.mutate.draft.delete).toHaveBeenCalledWith({ id: 'draft-1' });
318
+ });
319
+ });
320
+ describe('scheduleSend', () => {
321
+ it('schedules an immediate send with undo delay', async () => {
322
+ const args = { id: 'draft-1', kind: 'IMMEDIATE', undoMs: 5000, updatedAt: 1000000 };
323
+ await mutators.draft.scheduleSend.fn({ args, ctx, tx: tx });
324
+ expect(tx.mutate.draft.update).toHaveBeenCalledWith({
325
+ id: 'draft-1',
326
+ scheduledFor: 1005000,
327
+ status: 'SEND_REQUESTED',
328
+ updatedAt: 1000000,
329
+ });
330
+ });
331
+ it('schedules a future send', async () => {
332
+ const args = { id: 'draft-1', kind: 'SCHEDULED', scheduledFor: 2000000, updatedAt: 1000000 };
333
+ await mutators.draft.scheduleSend.fn({ args, ctx, tx: tx });
334
+ expect(tx.mutate.draft.update).toHaveBeenCalledWith({
335
+ id: 'draft-1',
336
+ scheduledFor: 2000000,
337
+ status: 'SEND_REQUESTED',
338
+ updatedAt: 1000000,
339
+ });
340
+ });
341
+ });
342
+ describe('setContent', () => {
343
+ it('applies a content patch', async () => {
344
+ tx.run = mock(() => Promise.resolve({ body: { bcc: [], cc: [], content: 'Hello', to: [] } }));
345
+ const args = {
346
+ id: 'draft-1',
347
+ patch: [{ index: 5, type: 'INSERTION', value: ' World' }],
348
+ updatedAt: 1234567890,
349
+ };
350
+ await mutators.draft.setContent.fn({ args, ctx, tx: tx });
351
+ expect(tx.mutate.draft.update).toHaveBeenCalledWith({
352
+ body: { bcc: [], cc: [], content: 'Hello World', to: [] },
353
+ id: 'draft-1',
354
+ updatedAt: 1234567890,
355
+ });
356
+ });
357
+ it('throws if draft not found', async () => {
358
+ tx.run = mock(() => Promise.resolve(null));
359
+ const args = { id: 'draft-1', patch: [], updatedAt: 1234567890 };
360
+ await expect(mutators.draft.setContent.fn({ args, ctx, tx: tx })).rejects.toThrow(MutationError.ENTITY_NOT_FOUND);
361
+ });
362
+ });
363
+ describe('setEnvelope', () => {
364
+ it('updates envelope fields', async () => {
365
+ tx.run = mock(() => Promise.resolve({ body: { content: 'Hello' } }));
366
+ const args = {
367
+ envelope: { bcc: ['bcc@example.com'], cc: [], subject: 'New Subject', to: ['to@example.com'] },
368
+ id: 'draft-1',
369
+ updatedAt: 1234567890,
370
+ };
371
+ await mutators.draft.setEnvelope.fn({ args, ctx, tx: tx });
372
+ expect(tx.mutate.draft.update).toHaveBeenCalledWith({
373
+ body: { bcc: ['bcc@example.com'], cc: [], content: 'Hello', to: ['to@example.com'] },
374
+ id: 'draft-1',
375
+ subject: 'New Subject',
376
+ updatedAt: 1234567890,
377
+ });
378
+ });
379
+ });
380
+ describe('setFrom', () => {
381
+ it('updates from fields', async () => {
382
+ const args = {
383
+ accountId: 'account-2',
384
+ aliasId: 'alias-2',
385
+ from: 'new@example.com',
386
+ fromName: 'New Name',
387
+ id: 'draft-1',
388
+ updatedAt: 1234567890,
389
+ };
390
+ await mutators.draft.setFrom.fn({ args, ctx, tx: tx });
391
+ expect(tx.mutate.draft.update).toHaveBeenCalledWith({
392
+ accountId: 'account-2',
393
+ fromAliasId: 'alias-2',
394
+ fromEmail: 'new@example.com',
395
+ fromName: 'New Name',
396
+ id: 'draft-1',
397
+ updatedAt: 1234567890,
398
+ });
399
+ });
400
+ });
401
+ });
402
+ describe('thread', () => {
403
+ describe('addLabel', () => {
404
+ it('adds a label to threads', async () => {
405
+ tx.run = mock()
406
+ .mockResolvedValueOnce({ id: 'label-1', uidValidity: 1, unreadCount: 0 })
407
+ .mockResolvedValueOnce({
408
+ id: 'thread-1',
409
+ labelIdList: ' label-2 ',
410
+ latestMessageDate: 1234567890,
411
+ seen: true,
412
+ })
413
+ .mockResolvedValueOnce([{ id: 'message-1' }])
414
+ .mockResolvedValueOnce(null);
415
+ const args = {
416
+ accounts: { 'account-1': { threadIds: ['thread-1'] } },
417
+ labelPath: 'INBOX',
418
+ };
419
+ await mutators.thread.addLabel.fn({ args, ctx, tx: tx });
420
+ expect(tx.mutate.threadByLabel.insert).toHaveBeenCalledWith({
421
+ labelId: 'label-1',
422
+ latestMessageDate: 1234567890,
423
+ threadId: 'thread-1',
424
+ });
425
+ expect(tx.mutate.threadLabel.insert).toHaveBeenCalled();
426
+ expect(tx.mutate.thread.update).toHaveBeenCalledWith({
427
+ id: 'thread-1',
428
+ labelIdList: expect.stringContaining('label-1'),
429
+ });
430
+ });
431
+ it('throws if label not found', async () => {
432
+ tx.run = mock(() => Promise.resolve(null));
433
+ const args = {
434
+ accounts: { 'account-1': { threadIds: ['thread-1'] } },
435
+ labelPath: 'NONEXISTENT',
436
+ };
437
+ await expect(mutators.thread.addLabel.fn({ args, ctx, tx: tx })).rejects.toThrow(MutationError.ENTITY_NOT_FOUND);
438
+ });
439
+ });
440
+ describe('delete', () => {
441
+ it('deletes threads and updates unread counts', async () => {
442
+ tx.run = mock()
443
+ .mockResolvedValueOnce({ id: 'thread-1', labelIdList: ' label-1 ', seen: false })
444
+ .mockResolvedValueOnce([{ id: 'label-1', unreadCount: 5 }]);
445
+ const args = { accounts: { 'account-1': { threadIds: ['thread-1'] } } };
446
+ await mutators.thread.delete.fn({ args, ctx, tx: tx });
447
+ expect(tx.mutate.accountLabel.update).toHaveBeenCalledWith({ id: 'label-1', unreadCount: 4 });
448
+ expect(tx.mutate.thread.delete).toHaveBeenCalledWith({ id: 'thread-1' });
449
+ });
450
+ it('skips unread count update for seen threads', async () => {
451
+ tx.run = mock(() => Promise.resolve({ id: 'thread-1', labelIdList: '', seen: true }));
452
+ const args = { accounts: { 'account-1': { threadIds: ['thread-1'] } } };
453
+ await mutators.thread.delete.fn({ args, ctx, tx: tx });
454
+ expect(tx.mutate.accountLabel.update).not.toHaveBeenCalled();
455
+ expect(tx.mutate.thread.delete).toHaveBeenCalledWith({ id: 'thread-1' });
456
+ });
457
+ });
458
+ describe('removeLabel', () => {
459
+ it('removes a label from threads', async () => {
460
+ tx.run = mock()
461
+ .mockResolvedValueOnce({ id: 'label-1', unreadCount: 5 })
462
+ .mockResolvedValueOnce({ id: 'thread-1', labelIdList: ' label-1 label-2 ', seen: false })
463
+ .mockResolvedValueOnce([{ id: 'message-1' }]);
464
+ const args = {
465
+ accounts: { 'account-1': { threadIds: ['thread-1'] } },
466
+ labelPath: 'INBOX',
467
+ };
468
+ await mutators.thread.removeLabel.fn({ args, ctx, tx: tx });
469
+ expect(tx.mutate.threadLabel.delete).toHaveBeenCalledWith({
470
+ accountId: 'account-1',
471
+ labelId: 'label-1',
472
+ threadMessageId: 'message-1',
473
+ });
474
+ expect(tx.mutate.threadByLabel.delete).toHaveBeenCalledWith({
475
+ labelId: 'label-1',
476
+ threadId: 'thread-1',
477
+ });
478
+ expect(tx.mutate.accountLabel.update).toHaveBeenCalledWith({ id: 'label-1', unreadCount: 4 });
479
+ });
480
+ it('throws if thread does not have label', async () => {
481
+ tx.run = mock()
482
+ .mockResolvedValueOnce({ id: 'label-1' })
483
+ .mockResolvedValueOnce({ id: 'thread-1', labelIdList: ' label-2 ' })
484
+ .mockResolvedValueOnce([]);
485
+ const args = {
486
+ accounts: { 'account-1': { threadIds: ['thread-1'] } },
487
+ labelPath: 'INBOX',
488
+ };
489
+ await expect(mutators.thread.removeLabel.fn({ args, ctx, tx: tx })).rejects.toThrow(MutationError.ENTITY_NOT_FOUND);
490
+ });
491
+ });
492
+ describe('setFlagged', () => {
493
+ it('sets flagged status', async () => {
494
+ const args = { accounts: { 'account-1': { threadIds: ['thread-1', 'thread-2'] } }, flagged: true };
495
+ await mutators.thread.setFlagged.fn({ args, ctx, tx: tx });
496
+ expect(tx.mutate.thread.update).toHaveBeenCalledWith({ flagged: true, id: 'thread-1' });
497
+ expect(tx.mutate.thread.update).toHaveBeenCalledWith({ flagged: true, id: 'thread-2' });
498
+ });
499
+ });
500
+ describe('setSeen', () => {
501
+ it('marks threads as seen and updates label counts', async () => {
502
+ tx.run = mock()
503
+ .mockResolvedValueOnce([{ id: 'thread-1', labelIdList: ' label-1 ', seen: false }])
504
+ .mockResolvedValueOnce([{ id: 'label-1', unreadCount: 5 }]);
505
+ const args = { accounts: { 'account-1': { threadIds: ['thread-1'] } }, seen: true };
506
+ await mutators.thread.setSeen.fn({ args, ctx, tx: tx });
507
+ expect(tx.mutate.accountLabel.update).toHaveBeenCalledWith({ id: 'label-1', unreadCount: 4 });
508
+ expect(tx.mutate.thread.update).toHaveBeenCalledWith({ id: 'thread-1', seen: true });
509
+ });
510
+ it('marks threads as unseen and increments label counts', async () => {
511
+ tx.run = mock()
512
+ .mockResolvedValueOnce([{ id: 'thread-1', labelIdList: ' label-1 ', seen: true }])
513
+ .mockResolvedValueOnce([{ id: 'label-1', unreadCount: 5 }]);
514
+ const args = { accounts: { 'account-1': { threadIds: ['thread-1'] } }, seen: false };
515
+ await mutators.thread.setSeen.fn({ args, ctx, tx: tx });
516
+ expect(tx.mutate.accountLabel.update).toHaveBeenCalledWith({ id: 'label-1', unreadCount: 6 });
517
+ expect(tx.mutate.thread.update).toHaveBeenCalledWith({ id: 'thread-1', seen: false });
518
+ });
519
+ it('skips label update when thread seen status unchanged', async () => {
520
+ tx.run = mock(() => Promise.resolve([{ id: 'thread-1', labelIdList: ' label-1 ', seen: true }]));
521
+ const args = { accounts: { 'account-1': { threadIds: ['thread-1'] } }, seen: true };
522
+ await mutators.thread.setSeen.fn({ args, ctx, tx: tx });
523
+ expect(tx.mutate.accountLabel.update).not.toHaveBeenCalled();
524
+ });
525
+ });
526
+ const systemLabelTestCases = [
527
+ { mutatorName: 'setArchive', specialUse: 'ARCHIVE' },
528
+ { mutatorName: 'setInbox', specialUse: 'INBOX' },
529
+ { mutatorName: 'setSpam', specialUse: 'SPAM' },
530
+ { mutatorName: 'setTrash', specialUse: 'TRASH' },
531
+ ];
532
+ for (const { mutatorName, specialUse } of systemLabelTestCases) {
533
+ describe(mutatorName, () => {
534
+ it(`moves thread to ${specialUse} label`, async () => {
535
+ tx.run = mock()
536
+ .mockResolvedValueOnce({
537
+ accountId: 'account-1',
538
+ id: 'thread-1',
539
+ labelIdList: ' old-label ',
540
+ latestMessageDate: 1234567890,
541
+ seen: true,
542
+ })
543
+ .mockResolvedValueOnce({ id: 'new-label', uidValidity: 1 })
544
+ .mockResolvedValueOnce([{ id: 'message-1' }])
545
+ .mockResolvedValueOnce([{ id: 'old-label', unreadCount: 0 }]);
546
+ const args = { accounts: { 'account-1': { threadIds: ['thread-1'] } } };
547
+ await mutators.thread[mutatorName].fn({ args, ctx, tx: tx });
548
+ expect(tx.mutate.threadByLabel.delete).toHaveBeenCalledWith({ labelId: 'old-label', threadId: 'thread-1' });
549
+ expect(tx.mutate.threadByLabel.insert).toHaveBeenCalledWith({
550
+ labelId: 'new-label',
551
+ latestMessageDate: 1234567890,
552
+ threadId: 'thread-1',
553
+ });
554
+ expect(tx.mutate.thread.update).toHaveBeenCalledWith({
555
+ id: 'thread-1',
556
+ labelIdList: expect.stringContaining('new-label'),
557
+ });
558
+ });
559
+ });
560
+ }
561
+ });
562
+ describe('user', () => {
563
+ describe('deleteSettingsPushNotificationToken', () => {
564
+ it('deletes a push notification token', async () => {
565
+ const args = { id: 'token-1', token: 'abc123' };
566
+ await mutators.user.deleteSettingsPushNotificationToken.fn({ args, ctx, tx: tx });
567
+ expect(tx.mutate.userPushNotificationToken.delete).toHaveBeenCalledWith({ id: 'token-1' });
568
+ });
569
+ });
570
+ describe('setSettingsName', () => {
571
+ it('updates user name', async () => {
572
+ const args = { id: 'user-1', name: 'New Name' };
573
+ await mutators.user.setSettingsName.fn({ args, ctx, tx: tx });
574
+ expect(tx.mutate.user.update).toHaveBeenCalledWith({ id: 'user-1', name: 'New Name' });
575
+ });
576
+ });
577
+ describe('setSettingsPushNotificationToken', () => {
578
+ it('inserts a new push notification token', async () => {
579
+ tx.run = mock(() => Promise.resolve(null));
580
+ const args = {
581
+ id: 'user-1',
582
+ pushNotificationToken: { createdAt: 1234567890, id: 'token-1', token: 'abc123' },
583
+ };
584
+ await mutators.user.setSettingsPushNotificationToken.fn({ args, ctx, tx: tx });
585
+ expect(tx.mutate.userPushNotificationToken.insert).toHaveBeenCalledWith({
586
+ createdAt: 1234567890,
587
+ id: 'token-1',
588
+ token: 'abc123',
589
+ userId: 'user-1',
590
+ });
591
+ });
592
+ it('skips insert if token already exists', async () => {
593
+ tx.run = mock(() => Promise.resolve({ id: 'existing-token' }));
594
+ const args = {
595
+ id: 'user-1',
596
+ pushNotificationToken: { createdAt: 1234567890, id: 'token-1', token: 'abc123' },
597
+ };
598
+ await mutators.user.setSettingsPushNotificationToken.fn({ args, ctx, tx: tx });
599
+ expect(tx.mutate.userPushNotificationToken.insert).not.toHaveBeenCalled();
600
+ });
601
+ });
602
+ });
603
+ });