@marcoappio/marco-config 2.0.528 → 2.0.530
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 +161 -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 +56 -10
- package/dist/zero/mutators.d.ts.map +1 -1
- package/dist/zero/mutators.js +38 -0
- package/dist/zero/mutators.test.js +200 -0
- package/dist/zero/queries.d.ts +24 -10
- package/dist/zero/queries.d.ts.map +1 -1
- package/dist/zero/schema.d.ts +28 -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,15 @@ export declare const mutators: import("@rocicorp/zero").MutatorRegistry<{
|
|
|
469
506
|
} & {
|
|
470
507
|
serverName: string;
|
|
471
508
|
};
|
|
509
|
+
readonly views: {
|
|
510
|
+
type: "json";
|
|
511
|
+
optional: false;
|
|
512
|
+
customType: {
|
|
513
|
+
id: string;
|
|
514
|
+
name: string;
|
|
515
|
+
aliasEmails: string[];
|
|
516
|
+
}[];
|
|
517
|
+
};
|
|
472
518
|
};
|
|
473
519
|
primaryKey: readonly [string, ...string[]];
|
|
474
520
|
} & {
|
|
@@ -1207,31 +1253,31 @@ export declare const mutators: import("@rocicorp/zero").MutatorRegistry<{
|
|
|
1207
1253
|
relationships: {
|
|
1208
1254
|
readonly user: {
|
|
1209
1255
|
accounts: [{
|
|
1210
|
-
readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
|
|
1256
|
+
readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
|
|
1211
1257
|
readonly destField: readonly ("id" | "userId" | "color" | "displayName" | "imapConnectionStatus" | "mailProcessedCount" | "mailTotalCount" | "primaryAliasId")[];
|
|
1212
1258
|
readonly destSchema: "account";
|
|
1213
1259
|
readonly cardinality: "many";
|
|
1214
1260
|
}];
|
|
1215
1261
|
contacts: [{
|
|
1216
|
-
readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
|
|
1262
|
+
readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
|
|
1217
1263
|
readonly destField: readonly ("id" | "name" | "userId" | "emailAddress")[];
|
|
1218
1264
|
readonly destSchema: "contact";
|
|
1219
1265
|
readonly cardinality: "many";
|
|
1220
1266
|
}];
|
|
1221
1267
|
drafts: [{
|
|
1222
|
-
readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
|
|
1268
|
+
readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
|
|
1223
1269
|
readonly destField: readonly ("type" | "status" | "id" | "userId" | "accountId" | "fromAliasId" | "fromEmail" | "fromName" | "referencedMessageId" | "scheduledFor" | "updatedAt" | "body" | "error" | "subject")[];
|
|
1224
1270
|
readonly destSchema: "draft";
|
|
1225
1271
|
readonly cardinality: "many";
|
|
1226
1272
|
}];
|
|
1227
1273
|
pushNotificationTokens: [{
|
|
1228
|
-
readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
|
|
1274
|
+
readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
|
|
1229
1275
|
readonly destField: readonly ("id" | "createdAt" | "token" | "userId")[];
|
|
1230
1276
|
readonly destSchema: "userPushNotificationToken";
|
|
1231
1277
|
readonly cardinality: "many";
|
|
1232
1278
|
}];
|
|
1233
1279
|
threads: [{
|
|
1234
|
-
readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
|
|
1280
|
+
readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
|
|
1235
1281
|
readonly destField: readonly ("id" | "userId" | "accountId" | "subject" | "flagged" | "hasAttachments" | "labelIdList" | "latestMessageDate" | "latestMessageId" | "messageCount" | "previewText" | "seen" | "senderEmail" | "senderName" | "words")[];
|
|
1236
1282
|
readonly destSchema: "thread";
|
|
1237
1283
|
readonly cardinality: "many";
|
|
@@ -1240,7 +1286,7 @@ export declare const mutators: import("@rocicorp/zero").MutatorRegistry<{
|
|
|
1240
1286
|
readonly userPushNotificationToken: {
|
|
1241
1287
|
user: [{
|
|
1242
1288
|
readonly sourceField: readonly ("id" | "createdAt" | "token" | "userId")[];
|
|
1243
|
-
readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
|
|
1289
|
+
readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
|
|
1244
1290
|
readonly destSchema: "user";
|
|
1245
1291
|
readonly cardinality: "one";
|
|
1246
1292
|
}];
|
|
@@ -1248,7 +1294,7 @@ export declare const mutators: import("@rocicorp/zero").MutatorRegistry<{
|
|
|
1248
1294
|
readonly contact: {
|
|
1249
1295
|
user: [{
|
|
1250
1296
|
readonly sourceField: readonly ("id" | "name" | "userId" | "emailAddress")[];
|
|
1251
|
-
readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
|
|
1297
|
+
readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
|
|
1252
1298
|
readonly destSchema: "user";
|
|
1253
1299
|
readonly cardinality: "one";
|
|
1254
1300
|
}];
|
|
@@ -1286,7 +1332,7 @@ export declare const mutators: import("@rocicorp/zero").MutatorRegistry<{
|
|
|
1286
1332
|
}];
|
|
1287
1333
|
user: [{
|
|
1288
1334
|
readonly sourceField: readonly ("id" | "userId" | "color" | "displayName" | "imapConnectionStatus" | "mailProcessedCount" | "mailTotalCount" | "primaryAliasId")[];
|
|
1289
|
-
readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
|
|
1335
|
+
readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
|
|
1290
1336
|
readonly destSchema: "user";
|
|
1291
1337
|
readonly cardinality: "one";
|
|
1292
1338
|
}];
|
|
@@ -1333,7 +1379,7 @@ export declare const mutators: import("@rocicorp/zero").MutatorRegistry<{
|
|
|
1333
1379
|
}];
|
|
1334
1380
|
user: [{
|
|
1335
1381
|
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")[];
|
|
1382
|
+
readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
|
|
1337
1383
|
readonly destSchema: "user";
|
|
1338
1384
|
readonly cardinality: "one";
|
|
1339
1385
|
}];
|
|
@@ -1378,7 +1424,7 @@ export declare const mutators: import("@rocicorp/zero").MutatorRegistry<{
|
|
|
1378
1424
|
}];
|
|
1379
1425
|
user: [{
|
|
1380
1426
|
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")[];
|
|
1427
|
+
readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
|
|
1382
1428
|
readonly destSchema: "user";
|
|
1383
1429
|
readonly cardinality: "one";
|
|
1384
1430
|
}];
|
|
@@ -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,203 @@ 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
|
+
});
|
|
689
|
+
describe('deleteView', () => {
|
|
690
|
+
it('deletes a view', async () => {
|
|
691
|
+
tx.run = mock(() => Promise.resolve({
|
|
692
|
+
id: 'user-1',
|
|
693
|
+
views: [
|
|
694
|
+
{ aliasEmails: ['a@example.com'], id: 'view-1', name: 'Work' },
|
|
695
|
+
{ aliasEmails: ['b@example.com'], id: 'view-2', name: 'Personal' },
|
|
696
|
+
],
|
|
697
|
+
}));
|
|
698
|
+
const args = { id: 'user-1', viewId: 'view-1' };
|
|
699
|
+
await mutators.user.deleteView.fn({ args, ctx, tx: tx });
|
|
700
|
+
expect(tx.mutate.user.update).toHaveBeenCalledWith({
|
|
701
|
+
id: 'user-1',
|
|
702
|
+
views: [{ aliasEmails: ['b@example.com'], id: 'view-2', name: 'Personal' }],
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
it('does nothing if user not found', async () => {
|
|
706
|
+
tx.run = mock(() => Promise.resolve(null));
|
|
707
|
+
const args = { id: 'user-1', viewId: 'view-1' };
|
|
708
|
+
await mutators.user.deleteView.fn({ args, ctx, tx: tx });
|
|
709
|
+
expect(tx.mutate.user.update).not.toHaveBeenCalled();
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
describe('updateView', () => {
|
|
713
|
+
it('updates view name', async () => {
|
|
714
|
+
tx.run = mock(() => Promise.resolve({
|
|
715
|
+
id: 'user-1',
|
|
716
|
+
views: [{ aliasEmails: ['a@example.com'], id: 'view-1', name: 'Work' }],
|
|
717
|
+
}));
|
|
718
|
+
const args = { id: 'user-1', updates: { name: 'Updated Work' }, viewId: 'view-1' };
|
|
719
|
+
await mutators.user.updateView.fn({ args, ctx, tx: tx });
|
|
720
|
+
expect(tx.mutate.user.update).toHaveBeenCalledWith({
|
|
721
|
+
id: 'user-1',
|
|
722
|
+
views: [{ aliasEmails: ['a@example.com'], id: 'view-1', name: 'Updated Work' }],
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
it('updates view aliasEmails', async () => {
|
|
726
|
+
tx.run = mock(() => Promise.resolve({
|
|
727
|
+
id: 'user-1',
|
|
728
|
+
views: [{ aliasEmails: ['a@example.com'], id: 'view-1', name: 'Work' }],
|
|
729
|
+
}));
|
|
730
|
+
const args = { id: 'user-1', updates: { aliasEmails: ['x@example.com', 'y@example.com'] }, viewId: 'view-1' };
|
|
731
|
+
await mutators.user.updateView.fn({ args, ctx, tx: tx });
|
|
732
|
+
expect(tx.mutate.user.update).toHaveBeenCalledWith({
|
|
733
|
+
id: 'user-1',
|
|
734
|
+
views: [{ aliasEmails: ['x@example.com', 'y@example.com'], id: 'view-1', name: 'Work' }],
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
it('deduplicates aliasEmails on update', async () => {
|
|
738
|
+
tx.run = mock(() => Promise.resolve({
|
|
739
|
+
id: 'user-1',
|
|
740
|
+
views: [{ aliasEmails: ['a@example.com'], id: 'view-1', name: 'Work' }],
|
|
741
|
+
}));
|
|
742
|
+
const args = {
|
|
743
|
+
id: 'user-1',
|
|
744
|
+
updates: { aliasEmails: ['x@example.com', 'y@example.com', 'x@example.com'] },
|
|
745
|
+
viewId: 'view-1',
|
|
746
|
+
};
|
|
747
|
+
await mutators.user.updateView.fn({ args, ctx, tx: tx });
|
|
748
|
+
expect(tx.mutate.user.update).toHaveBeenCalledWith({
|
|
749
|
+
id: 'user-1',
|
|
750
|
+
views: [{ aliasEmails: ['x@example.com', 'y@example.com'], id: 'view-1', name: 'Work' }],
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
it('updates both name and aliasEmails', async () => {
|
|
754
|
+
tx.run = mock(() => Promise.resolve({
|
|
755
|
+
id: 'user-1',
|
|
756
|
+
views: [{ aliasEmails: ['a@example.com'], id: 'view-1', name: 'Work' }],
|
|
757
|
+
}));
|
|
758
|
+
const args = {
|
|
759
|
+
id: 'user-1',
|
|
760
|
+
updates: { aliasEmails: ['x@example.com'], name: 'New Name' },
|
|
761
|
+
viewId: 'view-1',
|
|
762
|
+
};
|
|
763
|
+
await mutators.user.updateView.fn({ args, ctx, tx: tx });
|
|
764
|
+
expect(tx.mutate.user.update).toHaveBeenCalledWith({
|
|
765
|
+
id: 'user-1',
|
|
766
|
+
views: [{ aliasEmails: ['x@example.com'], id: 'view-1', name: 'New Name' }],
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
it('does nothing if user not found', async () => {
|
|
770
|
+
tx.run = mock(() => Promise.resolve(null));
|
|
771
|
+
const args = { id: 'user-1', updates: { name: 'Updated' }, viewId: 'view-1' };
|
|
772
|
+
await mutators.user.updateView.fn({ args, ctx, tx: tx });
|
|
773
|
+
expect(tx.mutate.user.update).not.toHaveBeenCalled();
|
|
774
|
+
});
|
|
775
|
+
it('does nothing if view does not exist', async () => {
|
|
776
|
+
tx.run = mock(() => Promise.resolve({
|
|
777
|
+
id: 'user-1',
|
|
778
|
+
views: [{ aliasEmails: ['a@example.com'], id: 'view-1', name: 'Work' }],
|
|
779
|
+
}));
|
|
780
|
+
const args = { id: 'user-1', updates: { name: 'Updated' }, viewId: 'non-existent-view' };
|
|
781
|
+
await mutators.user.updateView.fn({ args, ctx, tx: tx });
|
|
782
|
+
expect(tx.mutate.user.update).not.toHaveBeenCalled();
|
|
783
|
+
});
|
|
784
|
+
it('leaves non-matching views unchanged', async () => {
|
|
785
|
+
tx.run = mock(() => Promise.resolve({
|
|
786
|
+
id: 'user-1',
|
|
787
|
+
views: [
|
|
788
|
+
{ aliasEmails: ['a@example.com'], id: 'view-1', name: 'Work' },
|
|
789
|
+
{ aliasEmails: ['b@example.com'], id: 'view-2', name: 'Personal' },
|
|
790
|
+
],
|
|
791
|
+
}));
|
|
792
|
+
const args = { id: 'user-1', updates: { name: 'Updated Work' }, viewId: 'view-1' };
|
|
793
|
+
await mutators.user.updateView.fn({ args, ctx, tx: tx });
|
|
794
|
+
expect(tx.mutate.user.update).toHaveBeenCalledWith({
|
|
795
|
+
id: 'user-1',
|
|
796
|
+
views: [
|
|
797
|
+
{ aliasEmails: ['a@example.com'], id: 'view-1', name: 'Updated Work' },
|
|
798
|
+
{ aliasEmails: ['b@example.com'], id: 'view-2', name: 'Personal' },
|
|
799
|
+
],
|
|
800
|
+
});
|
|
801
|
+
});
|
|
802
|
+
it('schema rejects empty aliasEmails array', () => {
|
|
803
|
+
const args = { id: 'user-1', updates: { aliasEmails: [] }, viewId: 'view-1' };
|
|
804
|
+
const result = v.safeParse(mutatorSchemas.user.updateView, args);
|
|
805
|
+
expect(result.success).toBe(false);
|
|
806
|
+
});
|
|
807
|
+
});
|
|
608
808
|
});
|
|
609
809
|
});
|
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
|
+
}[];
|
|
270
275
|
} & {
|
|
271
276
|
readonly accounts: readonly ({
|
|
272
277
|
readonly color: string;
|
|
@@ -345,6 +350,15 @@ export declare const queries: import("@rocicorp/zero").QueryRegistry<{
|
|
|
345
350
|
} & {
|
|
346
351
|
serverName: string;
|
|
347
352
|
};
|
|
353
|
+
readonly views: {
|
|
354
|
+
type: "json";
|
|
355
|
+
optional: false;
|
|
356
|
+
customType: {
|
|
357
|
+
id: string;
|
|
358
|
+
name: string;
|
|
359
|
+
aliasEmails: string[];
|
|
360
|
+
}[];
|
|
361
|
+
};
|
|
348
362
|
};
|
|
349
363
|
primaryKey: readonly [string, ...string[]];
|
|
350
364
|
} & {
|
|
@@ -1083,31 +1097,31 @@ export declare const queries: import("@rocicorp/zero").QueryRegistry<{
|
|
|
1083
1097
|
relationships: {
|
|
1084
1098
|
readonly user: {
|
|
1085
1099
|
accounts: [{
|
|
1086
|
-
readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
|
|
1100
|
+
readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
|
|
1087
1101
|
readonly destField: readonly ("id" | "userId" | "color" | "displayName" | "imapConnectionStatus" | "mailProcessedCount" | "mailTotalCount" | "primaryAliasId")[];
|
|
1088
1102
|
readonly destSchema: "account";
|
|
1089
1103
|
readonly cardinality: "many";
|
|
1090
1104
|
}];
|
|
1091
1105
|
contacts: [{
|
|
1092
|
-
readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
|
|
1106
|
+
readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
|
|
1093
1107
|
readonly destField: readonly ("id" | "name" | "userId" | "emailAddress")[];
|
|
1094
1108
|
readonly destSchema: "contact";
|
|
1095
1109
|
readonly cardinality: "many";
|
|
1096
1110
|
}];
|
|
1097
1111
|
drafts: [{
|
|
1098
|
-
readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
|
|
1112
|
+
readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
|
|
1099
1113
|
readonly destField: readonly ("type" | "status" | "id" | "userId" | "accountId" | "fromAliasId" | "fromEmail" | "fromName" | "referencedMessageId" | "scheduledFor" | "updatedAt" | "body" | "error" | "subject")[];
|
|
1100
1114
|
readonly destSchema: "draft";
|
|
1101
1115
|
readonly cardinality: "many";
|
|
1102
1116
|
}];
|
|
1103
1117
|
pushNotificationTokens: [{
|
|
1104
|
-
readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
|
|
1118
|
+
readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
|
|
1105
1119
|
readonly destField: readonly ("id" | "createdAt" | "token" | "userId")[];
|
|
1106
1120
|
readonly destSchema: "userPushNotificationToken";
|
|
1107
1121
|
readonly cardinality: "many";
|
|
1108
1122
|
}];
|
|
1109
1123
|
threads: [{
|
|
1110
|
-
readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
|
|
1124
|
+
readonly sourceField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
|
|
1111
1125
|
readonly destField: readonly ("id" | "userId" | "accountId" | "subject" | "flagged" | "hasAttachments" | "labelIdList" | "latestMessageDate" | "latestMessageId" | "messageCount" | "previewText" | "seen" | "senderEmail" | "senderName" | "words")[];
|
|
1112
1126
|
readonly destSchema: "thread";
|
|
1113
1127
|
readonly cardinality: "many";
|
|
@@ -1116,7 +1130,7 @@ export declare const queries: import("@rocicorp/zero").QueryRegistry<{
|
|
|
1116
1130
|
readonly userPushNotificationToken: {
|
|
1117
1131
|
user: [{
|
|
1118
1132
|
readonly sourceField: readonly ("id" | "createdAt" | "token" | "userId")[];
|
|
1119
|
-
readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
|
|
1133
|
+
readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
|
|
1120
1134
|
readonly destSchema: "user";
|
|
1121
1135
|
readonly cardinality: "one";
|
|
1122
1136
|
}];
|
|
@@ -1124,7 +1138,7 @@ export declare const queries: import("@rocicorp/zero").QueryRegistry<{
|
|
|
1124
1138
|
readonly contact: {
|
|
1125
1139
|
user: [{
|
|
1126
1140
|
readonly sourceField: readonly ("id" | "name" | "userId" | "emailAddress")[];
|
|
1127
|
-
readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
|
|
1141
|
+
readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
|
|
1128
1142
|
readonly destSchema: "user";
|
|
1129
1143
|
readonly cardinality: "one";
|
|
1130
1144
|
}];
|
|
@@ -1162,7 +1176,7 @@ export declare const queries: import("@rocicorp/zero").QueryRegistry<{
|
|
|
1162
1176
|
}];
|
|
1163
1177
|
user: [{
|
|
1164
1178
|
readonly sourceField: readonly ("id" | "userId" | "color" | "displayName" | "imapConnectionStatus" | "mailProcessedCount" | "mailTotalCount" | "primaryAliasId")[];
|
|
1165
|
-
readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name")[];
|
|
1179
|
+
readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
|
|
1166
1180
|
readonly destSchema: "user";
|
|
1167
1181
|
readonly cardinality: "one";
|
|
1168
1182
|
}];
|
|
@@ -1209,7 +1223,7 @@ export declare const queries: import("@rocicorp/zero").QueryRegistry<{
|
|
|
1209
1223
|
}];
|
|
1210
1224
|
user: [{
|
|
1211
1225
|
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")[];
|
|
1226
|
+
readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
|
|
1213
1227
|
readonly destSchema: "user";
|
|
1214
1228
|
readonly cardinality: "one";
|
|
1215
1229
|
}];
|
|
@@ -1254,7 +1268,7 @@ export declare const queries: import("@rocicorp/zero").QueryRegistry<{
|
|
|
1254
1268
|
}];
|
|
1255
1269
|
user: [{
|
|
1256
1270
|
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")[];
|
|
1271
|
+
readonly destField: readonly ("id" | "profilePicture" | "undoSendEnabled" | "name" | "views")[];
|
|
1258
1272
|
readonly destSchema: "user";
|
|
1259
1273
|
readonly cardinality: "one";
|
|
1260
1274
|
}];
|
|
@@ -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"}
|