@marcoappio/marco-config 2.0.528 → 2.0.529

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.
@@ -1,4 +1,5 @@
1
1
  import * as v from 'valibot';
2
+ import { marcoConstants } from '../constants';
2
3
  import { marcoSchemas } from '../schemas';
3
4
  import { socketTypeSchema } from '../schemas/emailAccount';
4
5
  import { DRAFT_ATTACHMENT_UPLOAD_STATUSES, DRAFT_STATUSES, DRAFT_TYPES } from '../types';
@@ -53,6 +54,11 @@ const connectionConfigImapRawSchema = v.object({
53
54
  smtpSocketType: socketTypeSchema,
54
55
  smtpUser: marcoSchemas.string.required(),
55
56
  });
57
+ const userViewSchema = v.object({
58
+ aliasEmails: v.pipe(v.array(marcoSchemas.string.email()), v.minLength(1), v.maxLength(marcoConstants.user.views.maxAliasEmailsPerView)),
59
+ id: marcoSchemas.string.required(),
60
+ name: marcoSchemas.string.required(),
61
+ });
56
62
  export const mutatorSchemas = {
57
63
  account: {
58
64
  createAccount: v.object({
@@ -178,10 +184,18 @@ export const mutatorSchemas = {
178
184
  setTrash: baseThreadSchema,
179
185
  },
180
186
  user: {
187
+ createView: v.object({
188
+ id: marcoSchemas.string.required(),
189
+ view: userViewSchema,
190
+ }),
181
191
  deleteSettingsPushNotificationToken: v.object({
182
192
  id: marcoSchemas.string.required(),
183
193
  token: marcoSchemas.string.required(),
184
194
  }),
195
+ deleteView: v.object({
196
+ id: marcoSchemas.string.required(),
197
+ viewId: marcoSchemas.string.required(),
198
+ }),
185
199
  setSettingsName: v.object({
186
200
  id: marcoSchemas.string.required(),
187
201
  name: marcoSchemas.string.nullable(),
@@ -190,5 +204,13 @@ export const mutatorSchemas = {
190
204
  id: marcoSchemas.string.required(),
191
205
  pushNotificationToken: userPushNotificationTokenSchema,
192
206
  }),
207
+ updateView: v.object({
208
+ id: marcoSchemas.string.required(),
209
+ updates: v.partial(v.object({
210
+ aliasEmails: v.pipe(v.array(marcoSchemas.string.email()), v.minLength(1), v.maxLength(marcoConstants.user.views.maxAliasEmailsPerView)),
211
+ name: marcoSchemas.string.required(),
212
+ })),
213
+ viewId: marcoSchemas.string.required(),
214
+ }),
193
215
  },
194
216
  };
@@ -406,6 +406,21 @@ export declare const mutators: import("@rocicorp/zero").MutatorRegistry<{
406
406
  }, Context, unknown>;
407
407
  };
408
408
  readonly user: {
409
+ readonly createView: import("@rocicorp/zero").MutatorDefinition<{
410
+ id: string;
411
+ view: {
412
+ aliasEmails: string[];
413
+ id: string;
414
+ name: string;
415
+ };
416
+ }, {
417
+ id: string;
418
+ view: {
419
+ aliasEmails: string[];
420
+ id: string;
421
+ name: string;
422
+ };
423
+ }, Context, unknown>;
409
424
  readonly deleteSettingsPushNotificationToken: import("@rocicorp/zero").MutatorDefinition<{
410
425
  id: string;
411
426
  token: string;
@@ -413,6 +428,13 @@ export declare const mutators: import("@rocicorp/zero").MutatorRegistry<{
413
428
  id: string;
414
429
  token: string;
415
430
  }, Context, unknown>;
431
+ readonly deleteView: import("@rocicorp/zero").MutatorDefinition<{
432
+ id: string;
433
+ viewId: string;
434
+ }, {
435
+ id: string;
436
+ viewId: string;
437
+ }, Context, unknown>;
416
438
  readonly setSettingsName: import("@rocicorp/zero").MutatorDefinition<{
417
439
  id: string;
418
440
  name: string | null;
@@ -435,6 +457,21 @@ export declare const mutators: import("@rocicorp/zero").MutatorRegistry<{
435
457
  token: string;
436
458
  };
437
459
  }, Context, unknown>;
460
+ readonly updateView: import("@rocicorp/zero").MutatorDefinition<{
461
+ id: string;
462
+ updates: {
463
+ aliasEmails?: string[] | undefined;
464
+ name?: string | undefined;
465
+ };
466
+ viewId: string;
467
+ }, {
468
+ id: string;
469
+ updates: {
470
+ aliasEmails?: string[] | undefined;
471
+ name?: string | undefined;
472
+ };
473
+ viewId: string;
474
+ }, Context, unknown>;
438
475
  };
439
476
  }, {
440
477
  tables: {
@@ -469,6 +506,17 @@ export declare const mutators: import("@rocicorp/zero").MutatorRegistry<{
469
506
  } & {
470
507
  serverName: string;
471
508
  };
509
+ readonly views: Omit<{
510
+ type: "json";
511
+ optional: false;
512
+ customType: {
513
+ id: string;
514
+ name: string;
515
+ aliasEmails: string[];
516
+ }[];
517
+ }, "optional"> & {
518
+ optional: true;
519
+ };
472
520
  };
473
521
  primaryKey: readonly [string, ...string[]];
474
522
  } & {
@@ -1207,31 +1255,31 @@ export declare const mutators: import("@rocicorp/zero").MutatorRegistry<{
1207
1255
  relationships: {
1208
1256
  readonly user: {
1209
1257
  accounts: [{
1210
- readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
1258
+ readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
1211
1259
  readonly destField: readonly ("id" | "userId" | "color" | "displayName" | "imapConnectionStatus" | "mailProcessedCount" | "mailTotalCount" | "primaryAliasId")[];
1212
1260
  readonly destSchema: "account";
1213
1261
  readonly cardinality: "many";
1214
1262
  }];
1215
1263
  contacts: [{
1216
- readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
1264
+ readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
1217
1265
  readonly destField: readonly ("id" | "name" | "userId" | "emailAddress")[];
1218
1266
  readonly destSchema: "contact";
1219
1267
  readonly cardinality: "many";
1220
1268
  }];
1221
1269
  drafts: [{
1222
- readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
1270
+ readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
1223
1271
  readonly destField: readonly ("type" | "status" | "id" | "userId" | "accountId" | "fromAliasId" | "fromEmail" | "fromName" | "referencedMessageId" | "scheduledFor" | "updatedAt" | "body" | "error" | "subject")[];
1224
1272
  readonly destSchema: "draft";
1225
1273
  readonly cardinality: "many";
1226
1274
  }];
1227
1275
  pushNotificationTokens: [{
1228
- readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
1276
+ readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
1229
1277
  readonly destField: readonly ("id" | "createdAt" | "token" | "userId")[];
1230
1278
  readonly destSchema: "userPushNotificationToken";
1231
1279
  readonly cardinality: "many";
1232
1280
  }];
1233
1281
  threads: [{
1234
- readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
1282
+ readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
1235
1283
  readonly destField: readonly ("id" | "userId" | "accountId" | "subject" | "flagged" | "hasAttachments" | "labelIdList" | "latestMessageDate" | "latestMessageId" | "messageCount" | "previewText" | "seen" | "senderEmail" | "senderName" | "words")[];
1236
1284
  readonly destSchema: "thread";
1237
1285
  readonly cardinality: "many";
@@ -1240,7 +1288,7 @@ export declare const mutators: import("@rocicorp/zero").MutatorRegistry<{
1240
1288
  readonly userPushNotificationToken: {
1241
1289
  user: [{
1242
1290
  readonly sourceField: readonly ("id" | "createdAt" | "token" | "userId")[];
1243
- readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
1291
+ readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
1244
1292
  readonly destSchema: "user";
1245
1293
  readonly cardinality: "one";
1246
1294
  }];
@@ -1248,7 +1296,7 @@ export declare const mutators: import("@rocicorp/zero").MutatorRegistry<{
1248
1296
  readonly contact: {
1249
1297
  user: [{
1250
1298
  readonly sourceField: readonly ("id" | "name" | "userId" | "emailAddress")[];
1251
- readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
1299
+ readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
1252
1300
  readonly destSchema: "user";
1253
1301
  readonly cardinality: "one";
1254
1302
  }];
@@ -1286,7 +1334,7 @@ export declare const mutators: import("@rocicorp/zero").MutatorRegistry<{
1286
1334
  }];
1287
1335
  user: [{
1288
1336
  readonly sourceField: readonly ("id" | "userId" | "color" | "displayName" | "imapConnectionStatus" | "mailProcessedCount" | "mailTotalCount" | "primaryAliasId")[];
1289
- readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
1337
+ readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
1290
1338
  readonly destSchema: "user";
1291
1339
  readonly cardinality: "one";
1292
1340
  }];
@@ -1333,7 +1381,7 @@ export declare const mutators: import("@rocicorp/zero").MutatorRegistry<{
1333
1381
  }];
1334
1382
  user: [{
1335
1383
  readonly sourceField: readonly ("type" | "status" | "id" | "userId" | "accountId" | "fromAliasId" | "fromEmail" | "fromName" | "referencedMessageId" | "scheduledFor" | "updatedAt" | "body" | "error" | "subject")[];
1336
- readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
1384
+ readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
1337
1385
  readonly destSchema: "user";
1338
1386
  readonly cardinality: "one";
1339
1387
  }];
@@ -1378,7 +1426,7 @@ export declare const mutators: import("@rocicorp/zero").MutatorRegistry<{
1378
1426
  }];
1379
1427
  user: [{
1380
1428
  readonly sourceField: readonly ("id" | "userId" | "accountId" | "subject" | "flagged" | "hasAttachments" | "labelIdList" | "latestMessageDate" | "latestMessageId" | "messageCount" | "previewText" | "seen" | "senderEmail" | "senderName" | "words")[];
1381
- readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
1429
+ readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
1382
1430
  readonly destSchema: "user";
1383
1431
  readonly cardinality: "one";
1384
1432
  }];
@@ -1 +1 @@
1
- {"version":3,"file":"mutators.d.ts","sourceRoot":"","sources":["../../src/zero/mutators.ts"],"names":[],"mappings":"AAQA,KAAK,OAAO,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAqGjC,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6fnB,CAAA"}
1
+ {"version":3,"file":"mutators.d.ts","sourceRoot":"","sources":["../../src/zero/mutators.ts"],"names":[],"mappings":"AASA,KAAK,OAAO,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAqGjC,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0iBnB,CAAA"}
@@ -1,4 +1,5 @@
1
1
  import { createBuilder, defineMutators, defineMutatorWithType } from '@rocicorp/zero';
2
+ import { marcoConstants } from '../constants';
2
3
  import { stringPatch } from '../utils';
3
4
  import { threadsUtils } from '../utils/threads';
4
5
  import { mutatorSchemas } from '../zero/mutatorSchemas';
@@ -487,11 +488,33 @@ export const mutators = defineMutators({
487
488
  }),
488
489
  },
489
490
  user: {
491
+ createView: defineMutator(mutatorSchemas.user.createView, async ({ tx, args }) => {
492
+ const user = await tx.run(zql.user.where('id', args.id).one());
493
+ if (user) {
494
+ const currentViews = user.views ?? [];
495
+ const existingView = currentViews.find(x => x.id === args.view.id);
496
+ if (!existingView && currentViews.length < marcoConstants.user.views.maxViews) {
497
+ await tx.mutate.user.update({
498
+ id: args.id,
499
+ views: [...currentViews, { ...args.view, aliasEmails: [...new Set(args.view.aliasEmails)] }],
500
+ });
501
+ }
502
+ }
503
+ }),
490
504
  deleteSettingsPushNotificationToken: defineMutator(mutatorSchemas.user.deleteSettingsPushNotificationToken, async ({ tx, args }) => {
491
505
  await tx.mutate.userPushNotificationToken.delete({
492
506
  id: args.id,
493
507
  });
494
508
  }),
509
+ deleteView: defineMutator(mutatorSchemas.user.deleteView, async ({ tx, args }) => {
510
+ const user = await tx.run(zql.user.where('id', args.id).one());
511
+ if (user) {
512
+ await tx.mutate.user.update({
513
+ id: args.id,
514
+ views: (user.views ?? []).filter(x => x.id !== args.viewId),
515
+ });
516
+ }
517
+ }),
495
518
  setSettingsName: defineMutator(mutatorSchemas.user.setSettingsName, async ({ tx, args }) => {
496
519
  await tx.mutate.user.update({
497
520
  id: args.id,
@@ -509,5 +532,20 @@ export const mutators = defineMutators({
509
532
  });
510
533
  }
511
534
  }),
535
+ updateView: defineMutator(mutatorSchemas.user.updateView, async ({ tx, args }) => {
536
+ const user = await tx.run(zql.user.where('id', args.id).one());
537
+ if (user) {
538
+ const viewExists = (user.views ?? []).some(x => x.id === args.viewId);
539
+ if (viewExists) {
540
+ const deduplicatedEmails = args.updates.aliasEmails ? [...new Set(args.updates.aliasEmails)] : undefined;
541
+ await tx.mutate.user.update({
542
+ id: args.id,
543
+ views: (user.views ?? []).map(x => x.id === args.viewId
544
+ ? { ...x, aliasEmails: deduplicatedEmails ?? x.aliasEmails, name: args.updates.name ?? x.name }
545
+ : x),
546
+ });
547
+ }
548
+ }
549
+ }),
512
550
  },
513
551
  });
@@ -1,4 +1,6 @@
1
1
  import { beforeEach, describe, expect, it, mock } from 'bun:test';
2
+ import * as v from 'valibot';
3
+ import { mutatorSchemas } from './mutatorSchemas';
2
4
  import { mutators } from './mutators';
3
5
  const createMockTx = () => ({
4
6
  mutate: {
@@ -605,5 +607,224 @@ describe('mutators', () => {
605
607
  expect(tx.mutate.userPushNotificationToken.insert).not.toHaveBeenCalled();
606
608
  });
607
609
  });
610
+ describe('createView', () => {
611
+ it('creates a new view', async () => {
612
+ tx.run = mock(() => Promise.resolve({ id: 'user-1', views: [] }));
613
+ const args = {
614
+ id: 'user-1',
615
+ view: { aliasEmails: ['a@example.com', 'b@example.com'], id: 'view-1', name: 'Work' },
616
+ };
617
+ await mutators.user.createView.fn({ args, ctx, tx: tx });
618
+ expect(tx.mutate.user.update).toHaveBeenCalledWith({
619
+ id: 'user-1',
620
+ views: [{ aliasEmails: ['a@example.com', 'b@example.com'], id: 'view-1', name: 'Work' }],
621
+ });
622
+ });
623
+ it('deduplicates aliasEmails', async () => {
624
+ tx.run = mock(() => Promise.resolve({ id: 'user-1', views: [] }));
625
+ const args = {
626
+ id: 'user-1',
627
+ view: { aliasEmails: ['a@example.com', 'b@example.com', 'a@example.com'], id: 'view-1', name: 'Work' },
628
+ };
629
+ await mutators.user.createView.fn({ args, ctx, tx: tx });
630
+ expect(tx.mutate.user.update).toHaveBeenCalledWith({
631
+ id: 'user-1',
632
+ views: [{ aliasEmails: ['a@example.com', 'b@example.com'], id: 'view-1', name: 'Work' }],
633
+ });
634
+ });
635
+ it('appends to existing views', async () => {
636
+ tx.run = mock(() => Promise.resolve({
637
+ id: 'user-1',
638
+ views: [{ aliasEmails: ['x@example.com'], id: 'view-0', name: 'Personal' }],
639
+ }));
640
+ const args = {
641
+ id: 'user-1',
642
+ view: { aliasEmails: ['a@example.com'], id: 'view-1', name: 'Work' },
643
+ };
644
+ await mutators.user.createView.fn({ args, ctx, tx: tx });
645
+ expect(tx.mutate.user.update).toHaveBeenCalledWith({
646
+ id: 'user-1',
647
+ views: [
648
+ { aliasEmails: ['x@example.com'], id: 'view-0', name: 'Personal' },
649
+ { aliasEmails: ['a@example.com'], id: 'view-1', name: 'Work' },
650
+ ],
651
+ });
652
+ });
653
+ it('does nothing if user not found', async () => {
654
+ tx.run = mock(() => Promise.resolve(null));
655
+ const args = {
656
+ id: 'user-1',
657
+ view: { aliasEmails: ['a@example.com'], id: 'view-1', name: 'Work' },
658
+ };
659
+ await mutators.user.createView.fn({ args, ctx, tx: tx });
660
+ expect(tx.mutate.user.update).not.toHaveBeenCalled();
661
+ });
662
+ it('does nothing if view with same id already exists', async () => {
663
+ tx.run = mock(() => Promise.resolve({
664
+ id: 'user-1',
665
+ views: [{ aliasEmails: ['x@example.com'], id: 'view-1', name: 'Existing' }],
666
+ }));
667
+ const args = {
668
+ id: 'user-1',
669
+ view: { aliasEmails: ['a@example.com'], id: 'view-1', name: 'Work' },
670
+ };
671
+ await mutators.user.createView.fn({ args, ctx, tx: tx });
672
+ expect(tx.mutate.user.update).not.toHaveBeenCalled();
673
+ });
674
+ it('does nothing if max views limit reached', async () => {
675
+ const existingViews = Array.from({ length: 25 }, (_, i) => ({
676
+ aliasEmails: [`email${i}@example.com`],
677
+ id: `view-${i}`,
678
+ name: `View ${i}`,
679
+ }));
680
+ tx.run = mock(() => Promise.resolve({ id: 'user-1', views: existingViews }));
681
+ const args = {
682
+ id: 'user-1',
683
+ view: { aliasEmails: ['new@example.com'], id: 'view-new', name: 'New View' },
684
+ };
685
+ await mutators.user.createView.fn({ args, ctx, tx: tx });
686
+ expect(tx.mutate.user.update).not.toHaveBeenCalled();
687
+ });
688
+ it('handles null views array', async () => {
689
+ tx.run = mock(() => Promise.resolve({ id: 'user-1', views: null }));
690
+ const args = {
691
+ id: 'user-1',
692
+ view: { aliasEmails: ['a@example.com'], id: 'view-1', name: 'Work' },
693
+ };
694
+ await mutators.user.createView.fn({ args, ctx, tx: tx });
695
+ expect(tx.mutate.user.update).toHaveBeenCalledWith({
696
+ id: 'user-1',
697
+ views: [{ aliasEmails: ['a@example.com'], id: 'view-1', name: 'Work' }],
698
+ });
699
+ });
700
+ });
701
+ describe('deleteView', () => {
702
+ it('deletes a view', async () => {
703
+ tx.run = mock(() => Promise.resolve({
704
+ id: 'user-1',
705
+ views: [
706
+ { aliasEmails: ['a@example.com'], id: 'view-1', name: 'Work' },
707
+ { aliasEmails: ['b@example.com'], id: 'view-2', name: 'Personal' },
708
+ ],
709
+ }));
710
+ const args = { id: 'user-1', viewId: 'view-1' };
711
+ await mutators.user.deleteView.fn({ args, ctx, tx: tx });
712
+ expect(tx.mutate.user.update).toHaveBeenCalledWith({
713
+ id: 'user-1',
714
+ views: [{ aliasEmails: ['b@example.com'], id: 'view-2', name: 'Personal' }],
715
+ });
716
+ });
717
+ it('does nothing if user not found', async () => {
718
+ tx.run = mock(() => Promise.resolve(null));
719
+ const args = { id: 'user-1', viewId: 'view-1' };
720
+ await mutators.user.deleteView.fn({ args, ctx, tx: tx });
721
+ expect(tx.mutate.user.update).not.toHaveBeenCalled();
722
+ });
723
+ it('handles null views array', async () => {
724
+ tx.run = mock(() => Promise.resolve({ id: 'user-1', views: null }));
725
+ const args = { id: 'user-1', viewId: 'view-1' };
726
+ await mutators.user.deleteView.fn({ args, ctx, tx: tx });
727
+ expect(tx.mutate.user.update).toHaveBeenCalledWith({
728
+ id: 'user-1',
729
+ views: [],
730
+ });
731
+ });
732
+ });
733
+ describe('updateView', () => {
734
+ it('updates view name', async () => {
735
+ tx.run = mock(() => Promise.resolve({
736
+ id: 'user-1',
737
+ views: [{ aliasEmails: ['a@example.com'], id: 'view-1', name: 'Work' }],
738
+ }));
739
+ const args = { id: 'user-1', updates: { name: 'Updated Work' }, viewId: 'view-1' };
740
+ await mutators.user.updateView.fn({ args, ctx, tx: tx });
741
+ expect(tx.mutate.user.update).toHaveBeenCalledWith({
742
+ id: 'user-1',
743
+ views: [{ aliasEmails: ['a@example.com'], id: 'view-1', name: 'Updated Work' }],
744
+ });
745
+ });
746
+ it('updates view aliasEmails', async () => {
747
+ tx.run = mock(() => Promise.resolve({
748
+ id: 'user-1',
749
+ views: [{ aliasEmails: ['a@example.com'], id: 'view-1', name: 'Work' }],
750
+ }));
751
+ const args = { id: 'user-1', updates: { aliasEmails: ['x@example.com', 'y@example.com'] }, viewId: 'view-1' };
752
+ await mutators.user.updateView.fn({ args, ctx, tx: tx });
753
+ expect(tx.mutate.user.update).toHaveBeenCalledWith({
754
+ id: 'user-1',
755
+ views: [{ aliasEmails: ['x@example.com', 'y@example.com'], id: 'view-1', name: 'Work' }],
756
+ });
757
+ });
758
+ it('deduplicates aliasEmails on update', async () => {
759
+ tx.run = mock(() => Promise.resolve({
760
+ id: 'user-1',
761
+ views: [{ aliasEmails: ['a@example.com'], id: 'view-1', name: 'Work' }],
762
+ }));
763
+ const args = {
764
+ id: 'user-1',
765
+ updates: { aliasEmails: ['x@example.com', 'y@example.com', 'x@example.com'] },
766
+ viewId: 'view-1',
767
+ };
768
+ await mutators.user.updateView.fn({ args, ctx, tx: tx });
769
+ expect(tx.mutate.user.update).toHaveBeenCalledWith({
770
+ id: 'user-1',
771
+ views: [{ aliasEmails: ['x@example.com', 'y@example.com'], id: 'view-1', name: 'Work' }],
772
+ });
773
+ });
774
+ it('updates both name and aliasEmails', async () => {
775
+ tx.run = mock(() => Promise.resolve({
776
+ id: 'user-1',
777
+ views: [{ aliasEmails: ['a@example.com'], id: 'view-1', name: 'Work' }],
778
+ }));
779
+ const args = {
780
+ id: 'user-1',
781
+ updates: { aliasEmails: ['x@example.com'], name: 'New Name' },
782
+ viewId: 'view-1',
783
+ };
784
+ await mutators.user.updateView.fn({ args, ctx, tx: tx });
785
+ expect(tx.mutate.user.update).toHaveBeenCalledWith({
786
+ id: 'user-1',
787
+ views: [{ aliasEmails: ['x@example.com'], id: 'view-1', name: 'New Name' }],
788
+ });
789
+ });
790
+ it('does nothing if user not found', async () => {
791
+ tx.run = mock(() => Promise.resolve(null));
792
+ const args = { id: 'user-1', updates: { name: 'Updated' }, viewId: 'view-1' };
793
+ await mutators.user.updateView.fn({ args, ctx, tx: tx });
794
+ expect(tx.mutate.user.update).not.toHaveBeenCalled();
795
+ });
796
+ it('does nothing if view does not exist', async () => {
797
+ tx.run = mock(() => Promise.resolve({
798
+ id: 'user-1',
799
+ views: [{ aliasEmails: ['a@example.com'], id: 'view-1', name: 'Work' }],
800
+ }));
801
+ const args = { id: 'user-1', updates: { name: 'Updated' }, viewId: 'non-existent-view' };
802
+ await mutators.user.updateView.fn({ args, ctx, tx: tx });
803
+ expect(tx.mutate.user.update).not.toHaveBeenCalled();
804
+ });
805
+ it('leaves non-matching views unchanged', async () => {
806
+ tx.run = mock(() => Promise.resolve({
807
+ id: 'user-1',
808
+ views: [
809
+ { aliasEmails: ['a@example.com'], id: 'view-1', name: 'Work' },
810
+ { aliasEmails: ['b@example.com'], id: 'view-2', name: 'Personal' },
811
+ ],
812
+ }));
813
+ const args = { id: 'user-1', updates: { name: 'Updated Work' }, viewId: 'view-1' };
814
+ await mutators.user.updateView.fn({ args, ctx, tx: tx });
815
+ expect(tx.mutate.user.update).toHaveBeenCalledWith({
816
+ id: 'user-1',
817
+ views: [
818
+ { aliasEmails: ['a@example.com'], id: 'view-1', name: 'Updated Work' },
819
+ { aliasEmails: ['b@example.com'], id: 'view-2', name: 'Personal' },
820
+ ],
821
+ });
822
+ });
823
+ it('schema rejects empty aliasEmails array', () => {
824
+ const args = { id: 'user-1', updates: { aliasEmails: [] }, viewId: 'view-1' };
825
+ const result = v.safeParse(mutatorSchemas.user.updateView, args);
826
+ expect(result.success).toBe(false);
827
+ });
828
+ });
608
829
  });
609
830
  });
@@ -267,6 +267,11 @@ export declare const queries: import("@rocicorp/zero").QueryRegistry<{
267
267
  readonly name: string | null;
268
268
  readonly profilePicture: string | null;
269
269
  readonly undoSendEnabled: boolean;
270
+ readonly views: {
271
+ id: string;
272
+ name: string;
273
+ aliasEmails: string[];
274
+ }[] | null;
270
275
  } & {
271
276
  readonly accounts: readonly ({
272
277
  readonly color: string;
@@ -345,6 +350,17 @@ export declare const queries: import("@rocicorp/zero").QueryRegistry<{
345
350
  } & {
346
351
  serverName: string;
347
352
  };
353
+ readonly views: Omit<{
354
+ type: "json";
355
+ optional: false;
356
+ customType: {
357
+ id: string;
358
+ name: string;
359
+ aliasEmails: string[];
360
+ }[];
361
+ }, "optional"> & {
362
+ optional: true;
363
+ };
348
364
  };
349
365
  primaryKey: readonly [string, ...string[]];
350
366
  } & {
@@ -1083,31 +1099,31 @@ export declare const queries: import("@rocicorp/zero").QueryRegistry<{
1083
1099
  relationships: {
1084
1100
  readonly user: {
1085
1101
  accounts: [{
1086
- readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
1102
+ readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
1087
1103
  readonly destField: readonly ("id" | "userId" | "color" | "displayName" | "imapConnectionStatus" | "mailProcessedCount" | "mailTotalCount" | "primaryAliasId")[];
1088
1104
  readonly destSchema: "account";
1089
1105
  readonly cardinality: "many";
1090
1106
  }];
1091
1107
  contacts: [{
1092
- readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
1108
+ readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
1093
1109
  readonly destField: readonly ("id" | "name" | "userId" | "emailAddress")[];
1094
1110
  readonly destSchema: "contact";
1095
1111
  readonly cardinality: "many";
1096
1112
  }];
1097
1113
  drafts: [{
1098
- readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
1114
+ readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
1099
1115
  readonly destField: readonly ("type" | "status" | "id" | "userId" | "accountId" | "fromAliasId" | "fromEmail" | "fromName" | "referencedMessageId" | "scheduledFor" | "updatedAt" | "body" | "error" | "subject")[];
1100
1116
  readonly destSchema: "draft";
1101
1117
  readonly cardinality: "many";
1102
1118
  }];
1103
1119
  pushNotificationTokens: [{
1104
- readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
1120
+ readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
1105
1121
  readonly destField: readonly ("id" | "createdAt" | "token" | "userId")[];
1106
1122
  readonly destSchema: "userPushNotificationToken";
1107
1123
  readonly cardinality: "many";
1108
1124
  }];
1109
1125
  threads: [{
1110
- readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
1126
+ readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
1111
1127
  readonly destField: readonly ("id" | "userId" | "accountId" | "subject" | "flagged" | "hasAttachments" | "labelIdList" | "latestMessageDate" | "latestMessageId" | "messageCount" | "previewText" | "seen" | "senderEmail" | "senderName" | "words")[];
1112
1128
  readonly destSchema: "thread";
1113
1129
  readonly cardinality: "many";
@@ -1116,7 +1132,7 @@ export declare const queries: import("@rocicorp/zero").QueryRegistry<{
1116
1132
  readonly userPushNotificationToken: {
1117
1133
  user: [{
1118
1134
  readonly sourceField: readonly ("id" | "createdAt" | "token" | "userId")[];
1119
- readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
1135
+ readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
1120
1136
  readonly destSchema: "user";
1121
1137
  readonly cardinality: "one";
1122
1138
  }];
@@ -1124,7 +1140,7 @@ export declare const queries: import("@rocicorp/zero").QueryRegistry<{
1124
1140
  readonly contact: {
1125
1141
  user: [{
1126
1142
  readonly sourceField: readonly ("id" | "name" | "userId" | "emailAddress")[];
1127
- readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
1143
+ readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
1128
1144
  readonly destSchema: "user";
1129
1145
  readonly cardinality: "one";
1130
1146
  }];
@@ -1162,7 +1178,7 @@ export declare const queries: import("@rocicorp/zero").QueryRegistry<{
1162
1178
  }];
1163
1179
  user: [{
1164
1180
  readonly sourceField: readonly ("id" | "userId" | "color" | "displayName" | "imapConnectionStatus" | "mailProcessedCount" | "mailTotalCount" | "primaryAliasId")[];
1165
- readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
1181
+ readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
1166
1182
  readonly destSchema: "user";
1167
1183
  readonly cardinality: "one";
1168
1184
  }];
@@ -1209,7 +1225,7 @@ export declare const queries: import("@rocicorp/zero").QueryRegistry<{
1209
1225
  }];
1210
1226
  user: [{
1211
1227
  readonly sourceField: readonly ("type" | "status" | "id" | "userId" | "accountId" | "fromAliasId" | "fromEmail" | "fromName" | "referencedMessageId" | "scheduledFor" | "updatedAt" | "body" | "error" | "subject")[];
1212
- readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
1228
+ readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
1213
1229
  readonly destSchema: "user";
1214
1230
  readonly cardinality: "one";
1215
1231
  }];
@@ -1254,7 +1270,7 @@ export declare const queries: import("@rocicorp/zero").QueryRegistry<{
1254
1270
  }];
1255
1271
  user: [{
1256
1272
  readonly sourceField: readonly ("id" | "userId" | "accountId" | "subject" | "flagged" | "hasAttachments" | "labelIdList" | "latestMessageDate" | "latestMessageId" | "messageCount" | "previewText" | "seen" | "senderEmail" | "senderName" | "words")[];
1257
- readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
1273
+ readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
1258
1274
  readonly destSchema: "user";
1259
1275
  readonly cardinality: "one";
1260
1276
  }];
@@ -1 +1 @@
1
- {"version":3,"file":"queries.d.ts","sourceRoot":"","sources":["../../src/zero/queries.ts"],"names":[],"mappings":"AAOA,KAAK,OAAO,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAQjC,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoOlB,CAAA"}
1
+ {"version":3,"file":"queries.d.ts","sourceRoot":"","sources":["../../src/zero/queries.ts"],"names":[],"mappings":"AAOA,KAAK,OAAO,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAQjC,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoOlB,CAAA"}