@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.
- package/dist/constants/index.d.ts +4 -0
- package/dist/constants/index.d.ts.map +1 -1
- package/dist/constants/user.d.ts +4 -0
- package/dist/constants/user.d.ts.map +1 -1
- package/dist/constants/user.js +4 -0
- package/dist/zero/index.d.ts +169 -30
- package/dist/zero/index.d.ts.map +1 -1
- package/dist/zero/mutatorSchemas.d.ts +47 -0
- package/dist/zero/mutatorSchemas.d.ts.map +1 -1
- package/dist/zero/mutatorSchemas.js +22 -0
- package/dist/zero/mutators.d.ts +58 -10
- package/dist/zero/mutators.d.ts.map +1 -1
- package/dist/zero/mutators.js +38 -0
- package/dist/zero/mutators.test.js +221 -0
- package/dist/zero/queries.d.ts +26 -10
- package/dist/zero/queries.d.ts.map +1 -1
- package/dist/zero/schema.d.ts +32 -10
- package/dist/zero/schema.d.ts.map +1 -1
- package/dist/zero/schema.js +1 -0
- package/package.json +1 -1
|
@@ -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
|
};
|
package/dist/zero/mutators.d.ts
CHANGED
|
@@ -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":"
|
|
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"}
|
package/dist/zero/mutators.js
CHANGED
|
@@ -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
|
});
|
package/dist/zero/queries.d.ts
CHANGED
|
@@ -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
|
|
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"}
|