• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

realm / realm-js / 6942430105

21 Nov 2023 10:25AM CUT coverage: 85.405% (-0.8%) from 86.187%
6942430105

Pull #6258

github

web-flow
Apply suggestions from code review

Co-authored-by: LJ <81748770+elle-j@users.noreply.github.com>
Pull Request #6258: Env Driven App Id

897 of 1112 branches covered (0.0%)

Branch coverage included in aggregate %.

2333 of 2670 relevant lines covered (87.38%)

984.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.68
/packages/realm/src/app-services/SyncConfiguration.ts
1
////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2022 Realm Inc.
4
//
5
// Licensed under the Apache License, Version 2.0 (the "License");
6
// you may not use this file except in compliance with the License.
7
// You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing, software
12
// distributed under the License is distributed on an "AS IS" BASIS,
13
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
// See the License for the specific language governing permissions and
15
// limitations under the License.
16
//
17
////////////////////////////////////////////////////////////////////////////
18

19
import { EJSON, ObjectId, UUID } from "bson";
20
import { syncProxyConfig } from "src/platform/sync-proxy-config";
21

22
import {
23
  AnyUser,
24
  BSON,
25
  ClientResetError,
26
  MutableSubscriptionSet,
27
  Realm,
28
  SubscriptionSet,
29
  SyncError,
30
  SyncSession,
31
  TypeAssertionError,
32
  User,
33
  assert,
34
  binding,
35
  toBindingClientResetMode,
36
  toBindingErrorHandler,
37
  toBindingErrorHandlerWithOnManual,
38
  toBindingNotifyAfterClientReset,
39
  toBindingNotifyAfterClientResetWithFallback,
40
  toBindingNotifyBeforeClientReset,
41
  toBindingStopPolicy,
42
} from "../internal";
43

44
export type PartitionValue = string | number | BSON.ObjectId | BSON.UUID | null;
45

46
export enum OpenRealmBehaviorType {
1✔
47
  DownloadBeforeOpen = "downloadBeforeOpen",
1✔
48
  OpenImmediately = "openImmediately",
1✔
49
}
50

51
export enum OpenRealmTimeOutBehavior {
1✔
52
  OpenLocalRealm = "openLocalRealm",
1✔
53
  ThrowException = "throwException",
1✔
54
}
55

56
export type OpenRealmBehaviorConfiguration = {
57
  type: OpenRealmBehaviorType;
58
  timeOut?: number;
59
  timeOutBehavior?: OpenRealmTimeOutBehavior;
60
};
61

62
export type ErrorCallback = (session: SyncSession, error: SyncError | ClientResetError) => void;
63
export type ClientResetFallbackCallback = (session: SyncSession, path: string) => void;
64
export type ClientResetBeforeCallback = (localRealm: Realm) => void;
65
export type ClientResetAfterCallback = (localRealm: Realm, remoteRealm: Realm) => void;
66

67
export enum SessionStopPolicy {
1✔
68
  AfterUpload = "after-upload",
1✔
69
  Immediately = "immediately",
1✔
70
  Never = "never",
1✔
71
}
72

73
/**
74
 */
75
export enum ClientResetMode {
1✔
76
  /** @deprecated See {@link Realm.App.Sync.initiateClientReset} */
77
  Manual = "manual",
1✔
78
  /**
79
   * Download a fresh copy from the server.
80
   */
81
  DiscardUnsyncedChanges = "discardUnsyncedChanges",
1✔
82
  /**
83
   * Merged remote and local, unsynced changes.
84
   */
85
  RecoverUnsyncedChanges = "recoverUnsyncedChanges",
1✔
86
  /**
87
   * Download a fresh copy from the server if recovery of unsynced changes is not possible.
88
   */
89
  RecoverOrDiscardUnsyncedChanges = "recoverOrDiscardUnsyncedChanges",
1✔
90
}
91

92
export type ClientResetManualConfiguration = {
93
  mode: ClientResetMode.Manual;
94
  onManual?: ClientResetFallbackCallback;
95
};
96

97
export type ClientResetDiscardUnsyncedChangesConfiguration = {
98
  mode: ClientResetMode.DiscardUnsyncedChanges;
99
  onAfter?: ClientResetAfterCallback;
100
  /**
101
   * Called before sync initiates a client reset.
102
   */
103
  onBefore?: ClientResetBeforeCallback;
104
};
105

106
export type ClientResetRecoverUnsyncedChangesConfiguration = {
107
  mode: ClientResetMode.RecoverUnsyncedChanges;
108
  onAfter?: ClientResetAfterCallback;
109
  /**
110
   * Called before sync initiates a client reset.
111
   */
112
  onBefore?: ClientResetBeforeCallback;
113
  /**
114
   * Called if recovery or discard fail.
115
   */
116
  onFallback?: ClientResetFallbackCallback;
117
};
118

119
export type ClientResetRecoverOrDiscardUnsyncedChangesConfiguration = {
120
  mode: ClientResetMode.RecoverOrDiscardUnsyncedChanges;
121
  onAfter?: ClientResetAfterCallback;
122
  /**
123
   * Called before sync initiates a client reset.
124
   */
125
  onBefore?: ClientResetBeforeCallback;
126
  /**
127
   * Called if recovery or discard fail.
128
   */
129
  onFallback?: ClientResetFallbackCallback;
130
};
131

132
export type ClientResetConfig =
133
  | ClientResetManualConfiguration
134
  | ClientResetDiscardUnsyncedChangesConfiguration
135
  | ClientResetRecoverUnsyncedChangesConfiguration
136
  | ClientResetRecoverOrDiscardUnsyncedChangesConfiguration;
137

138
export type SSLConfiguration = {
139
  /**
140
   * Whether the SSL certificates must be validated. This should generally
141
   * be `true` in production.
142
   *
143
   * Default is `true`.
144
   */
145
  validate?: boolean;
146
  /**
147
   * The path where to find trusted SSL certificates used to validate the
148
   * server certificate. If `undefined`, validation will be delegated to
149
   * the provided `validateCertificates` callback.
150
   */
151
  certificatePath?: string;
152
  /**
153
   * A callback function used to validate the server's SSL certificate. It
154
   * is invoked for every certificate in the certificate chain starting from
155
   * the root downward. An SSL connection will be established if all certificates
156
   * are accepted. The certificate will be accepted if the callback returns `true`,
157
   * or rejected if returning `false`. This callback is only invoked if
158
   * `certificatePath` is `undefined`.
159
   */
160
  validateCertificates?: SSLVerifyCallback;
161
};
162

163
export type SSLVerifyCallback = (sslVerifyObject: SSLVerifyObject) => boolean;
164

165
type SSLVerifyCallbackWithListArguments = (
166
  serverAddress: string,
167
  serverPort: number,
168
  pemCertificate: string,
169
  preverifyOk: number, // Acts as `SSLVerifyObject.acceptedByOpenSSL`
170
  depth: number,
171
) => boolean;
172

173
export type SSLVerifyObject = {
174
  /**
175
   * The address that the SSL connection is being established to.
176
   */
177
  serverAddress: string;
178
  /**
179
   * The port that the SSL connection is being established to.
180
   */
181
  serverPort: number;
182
  /**
183
   * The certificate using the PEM format.
184
   */
185
  pemCertificate: string;
186
  /**
187
   * The result of OpenSSL's pre-verification of the certificate. If `true`,
188
   * the certificate has been accepted and will generally be safe to trust.
189
   * If `false`, it has been rejected and the user should do an independent
190
   * validation step.
191
   */
192
  acceptedByOpenSSL: boolean;
193
  /**
194
   * The position of the certificate in the certificate chain. The actual
195
   * server certificate has `depth` 0 (lowest) and also contains the host
196
   * name, while all other certificates up the chain have higher depths in
197
   * increments of 1.
198
   */
199
  depth: number;
200
};
201

202
export enum ProxyType {
1✔
203
  HTTP = "http",
1✔
204
  HTTPS = "https",
1✔
205
}
206

207
export type SyncProxyConfig = {
208
  address: string;
209
  port: number;
210
  type: ProxyType;
211
};
212

213
/**
214
 * This describes the different options used to create a {@link Realm} instance with Atlas App Services synchronization.
215
 */
216
export type BaseSyncConfiguration = {
217
  /**
218
   * A {@link Realm.User} object obtained by calling `Realm.App.logIn`.
219
   */
220
  user: AnyUser;
221
  /**
222
   * Whether to create a new file and sync in background or wait for the file to be synced.
223
   * @default
224
   * {
225
   *   type: OpenRealmBehaviorType.DownloadBeforeOpen,
226
   *   timeOut: 30 * 1000,
227
   *   timeOutBehavior: OpenRealmTimeOutBehavior.ThrowException,
228
   * }
229
   */
230
  newRealmFileBehavior?: OpenRealmBehaviorConfiguration;
231
  /**
232
   * Whether to open existing file and sync in background or wait for the sync of the file to complete and then open.
233
   * If not set, the Realm will be downloaded before opened.
234
   * @default
235
   * {
236
   *   type: OpenRealmBehaviorType.DownloadBeforeOpen,
237
   *   timeOut: 30 * 1000,
238
   *   timeOutBehavior: OpenRealmTimeOutBehavior.ThrowException,
239
   * }
240
   */
241
  existingRealmFileBehavior?: OpenRealmBehaviorConfiguration;
242
  /**
243
   * A callback function which is called in error situations.
244
   * The callback is passed two arguments: `session` and `syncError`.
245
   * If `syncError.name == "ClientReset"`, `syncError.path` and `syncError.config` are set and `syncError.readOnly` is true (deprecated, see `Realm.App.Sync~ClientResetConfiguration`).
246
   * Otherwise, `syncError` can have up to five properties: `name`, `message`, `isFatal`, `category`, and `code`.
247
   */
248
  onError?: ErrorCallback;
249
  /**
250
   * Custom HTTP headers, which are included when making requests to the server.
251
   */
252
  customHttpHeaders?: Record<string, string>;
253
  /**
254
   * SSL configuration.
255
   */
256
  ssl?: SSLConfiguration;
257
  /**
258
   * Configuration of Client Reset
259
   */
260
  clientReset?: ClientResetConfig;
261
  /**
262
   * Set to true, all async operations (such as opening the Realm with `Realm.open`) will fail when a non-fatal error, such as a timeout, occurs.
263
   */
264
  cancelWaitsOnNonFatalError?: boolean;
265
  /** @internal */
266
  _sessionStopPolicy?: SessionStopPolicy;
267
  /**
268
   * HTTP proxy configuration (node.js/Electron only)
269
   */
270
  proxyConfig?: SyncProxyConfig;
271
};
272

273
export type InitialSubscriptions = {
274
  /**
275
   * A callback to make changes to a SubscriptionSet.
276
   * @see {@link SubscriptionSet.update} for more information.
277
   */
278
  update: (mutableSubscriptions: MutableSubscriptionSet, realm: Realm) => void;
279
  /**
280
   * If `true`, the {@link InitialSubscriptions.update} callback will be rerun every time the Realm is
281
   * opened (e.g. every time a user opens your app), otherwise (by default) it
282
   * will only be run if the Realm does not yet exist.
283
   */
284
  rerunOnOpen?: boolean;
285
};
286

287
export type FlexibleSyncConfiguration = BaseSyncConfiguration & {
288
  flexible: true;
289
  partitionValue?: never;
290
  /**
291
   * Optional object to configure the setup of an initial set of flexible
292
   * sync subscriptions to be used when opening the Realm. If this is specified,
293
   * {@link Realm.open} will not resolve until this set of subscriptions has been
294
   * fully synchronized with the server.
295
   * @example
296
   * const config: Realm.Configuration = {
297
   *   sync: {
298
   *     user,
299
   *     flexible: true,
300
   *     initialSubscriptions: {
301
   *       update: (subs, realm) => {
302
   *         subs.add(realm.objects('Task'));
303
   *       },
304
   *       rerunOnOpen: true,
305
   *     },
306
   *   },
307
   *   // ... rest of config ...
308
   * };
309
   * const realm = await Realm.open(config);
310
   *
311
   * // At this point, the Realm will be open with the data for the initial set
312
   * // subscriptions fully synchronised.
313
   */
314
  initialSubscriptions?: InitialSubscriptions;
315
};
316

317
export type PartitionSyncConfiguration = BaseSyncConfiguration & {
318
  flexible?: never;
319
  partitionValue: PartitionValue;
320
  initialSubscriptions?: never;
321
};
322

323
export type SyncConfiguration = FlexibleSyncConfiguration | PartitionSyncConfiguration;
324

325
/** @internal */
326
export function toBindingSyncConfig(config: SyncConfiguration): binding.SyncConfig_Relaxed {
327
  const {
328
    user,
329
    flexible,
330
    partitionValue,
331
    onError,
332
    _sessionStopPolicy,
333
    customHttpHeaders,
334
    ssl,
335
    clientReset,
336
    cancelWaitsOnNonFatalError,
337
    proxyConfig,
338
  } = config;
1,522✔
339

340
  return {
1,522✔
341
    user: user.internal,
342
    partitionValue: flexible ? undefined : EJSON.stringify(partitionValue),
1,522✔
343
    flxSyncRequested: !!flexible,
344
    stopPolicy: _sessionStopPolicy
1,522✔
345
      ? toBindingStopPolicy(_sessionStopPolicy)
346
      : binding.SyncSessionStopPolicy.AfterChangesUploaded,
347
    customHttpHeaders,
348
    clientValidateSsl: ssl?.validate,
349
    sslTrustCertificatePath: ssl?.certificatePath,
350
    sslVerifyCallback: ssl?.validateCertificates
1,521!
351
      ? binding.Helpers.makeSslVerifyCallback(toSSLVerifyCallbackWithListArguments(ssl.validateCertificates))
352
      : undefined,
353
    ...parseClientResetConfig(clientReset, onError),
354
    cancelWaitsOnNonfatalError: cancelWaitsOnNonFatalError,
355
    proxyConfig: proxyConfig ? parseSyncProxyConfig(proxyConfig) : syncProxyConfig.create(),
1,520!
356
  };
357
}
358

359
/** @internal */
360
function toSSLVerifyCallbackWithListArguments(verifyCallback: SSLVerifyCallback): SSLVerifyCallbackWithListArguments {
361
  return (serverAddress: string, serverPort: number, pemCertificate: string, preverifyOk: number, depth: number) =>
×
362
    verifyCallback({ serverAddress, serverPort, pemCertificate, acceptedByOpenSSL: !!preverifyOk, depth });
×
363
}
364

365
/** @internal */
366
function parseClientResetConfig(clientReset: ClientResetConfig | undefined, onError: ErrorCallback | undefined) {
367
  if (!clientReset) {
1,521✔
368
    return {
1,505✔
369
      clientResyncMode: undefined,
370
      notifyBeforeClientReset: undefined,
371
      notifyAfterClientReset: undefined,
372
      errorHandler: onError ? toBindingErrorHandler(onError) : undefined,
1,505✔
373
    };
374
  }
375
  switch (clientReset.mode) {
16!
376
    case ClientResetMode.Manual: {
377
      return parseManual(clientReset as ClientResetManualConfiguration, onError);
16✔
378
    }
379
    case ClientResetMode.DiscardUnsyncedChanges: {
380
      return {
×
381
        ...parseDiscardUnsyncedChanges(clientReset as ClientResetDiscardUnsyncedChangesConfiguration),
382
        errorHandler: onError ? toBindingErrorHandler(onError) : undefined,
×
383
      };
384
    }
385
    case ClientResetMode.RecoverUnsyncedChanges: {
386
      return {
×
387
        ...parseRecoverUnsyncedChanges(clientReset as ClientResetRecoverUnsyncedChangesConfiguration),
388
        errorHandler: onError ? toBindingErrorHandler(onError) : undefined,
×
389
      };
390
    }
391
    case ClientResetMode.RecoverOrDiscardUnsyncedChanges: {
392
      return {
×
393
        ...parseRecoverOrDiscardUnsyncedChanges(clientReset as ClientResetRecoverOrDiscardUnsyncedChangesConfiguration),
394
        errorHandler: onError ? toBindingErrorHandler(onError) : undefined,
×
395
      };
396
    }
397
  }
398
}
399

400
/** @internal */
401
function parseManual(clientReset: ClientResetManualConfiguration, onError: ErrorCallback | undefined) {
402
  return {
16✔
403
    clientResyncMode: toBindingClientResetMode(clientReset.mode),
404
    errorHandler: toBindingErrorHandlerWithOnManual(onError, clientReset.onManual),
405
  };
406
}
407

408
/** @internal */
409
function parseDiscardUnsyncedChanges(clientReset: ClientResetDiscardUnsyncedChangesConfiguration) {
410
  return {
×
411
    clientResyncMode: toBindingClientResetMode(clientReset.mode),
412
    notifyBeforeClientReset: clientReset.onBefore ? toBindingNotifyBeforeClientReset(clientReset.onBefore) : undefined,
×
413
    notifyAfterClientReset: clientReset.onAfter ? toBindingNotifyAfterClientReset(clientReset.onAfter) : undefined,
×
414
  };
415
}
416

417
/** @internal */
418
function parseRecoverUnsyncedChanges(clientReset: ClientResetRecoverUnsyncedChangesConfiguration) {
419
  return {
×
420
    clientResyncMode: toBindingClientResetMode(clientReset.mode),
421
    notifyBeforeClientReset: clientReset.onBefore ? toBindingNotifyBeforeClientReset(clientReset.onBefore) : undefined,
×
422
    notifyAfterClientReset: clientReset.onAfter
×
423
      ? toBindingNotifyAfterClientResetWithFallback(clientReset.onAfter, clientReset.onFallback)
424
      : undefined,
425
  };
426
}
427

428
/** @internal */
429
function parseRecoverOrDiscardUnsyncedChanges(clientReset: ClientResetRecoverOrDiscardUnsyncedChangesConfiguration) {
430
  return {
×
431
    clientResyncMode: toBindingClientResetMode(clientReset.mode),
432
    notifyBeforeClientReset: clientReset.onBefore ? toBindingNotifyBeforeClientReset(clientReset.onBefore) : undefined,
×
433
    notifyAfterClientReset: clientReset.onAfter
×
434
      ? toBindingNotifyAfterClientResetWithFallback(clientReset.onAfter, clientReset.onFallback)
435
      : undefined,
436
  };
437
}
438

439
/** @internal */
440
function parseProxyType(proxyType: ProxyType) {
441
  switch (proxyType) {
×
442
    case ProxyType.HTTP:
443
      return binding.ProxyType.Http;
×
444
    case ProxyType.HTTPS:
445
      return binding.ProxyType.Https;
×
446
  }
447
}
448

449
/** @internal */
450
function parseSyncProxyConfig(syncProxyConfig: SyncProxyConfig) {
451
  return {
×
452
    ...syncProxyConfig,
453
    type: parseProxyType(syncProxyConfig.type),
454
  };
455
}
456

457
/**
458
 * Validate the fields of a user-provided realm sync configuration.
459
 * @internal
460
 */
461
export function validateSyncConfiguration(config: unknown): asserts config is SyncConfiguration {
462
  assert.object(config, "'sync' on realm configuration", { allowArrays: false });
1,030✔
463
  const {
464
    user,
465
    newRealmFileBehavior,
466
    existingRealmFileBehavior,
467
    onError,
468
    customHttpHeaders,
469
    ssl,
470
    clientReset,
471
    flexible,
472
    cancelWaitOnNonFatalError: cancelWaitsOnNonFatalError,
473
  } = config;
1,030✔
474

475
  assert.instanceOf(user, User, "'user' on realm sync configuration");
1,030✔
476
  if (cancelWaitsOnNonFatalError !== undefined) {
1,029!
477
    assert.boolean(cancelWaitsOnNonFatalError, "'cancelWaitOnNonFatalError' on sync configuration");
×
478
  }
479
  if (newRealmFileBehavior !== undefined) {
1,029✔
480
    validateOpenRealmBehaviorConfiguration(newRealmFileBehavior, "newRealmFileBehavior");
26✔
481
  }
482
  if (existingRealmFileBehavior !== undefined) {
1,029✔
483
    validateOpenRealmBehaviorConfiguration(existingRealmFileBehavior, "existingRealmFileBehavior");
19✔
484
  }
485
  if (onError !== undefined) {
1,029✔
486
    assert.function(onError, "'onError' on realm sync configuration");
10✔
487
  }
488
  if (customHttpHeaders !== undefined) {
1,029✔
489
    assert.object(customHttpHeaders, "'customHttpHeaders' on realm sync configuration", { allowArrays: false });
10✔
490
    for (const key in customHttpHeaders) {
10✔
491
      assert.string(customHttpHeaders[key], "all property values of 'customHttpHeaders' on realm sync configuration");
2✔
492
    }
493
  }
494
  if (ssl !== undefined) {
1,029✔
495
    validateSSLConfiguration(ssl);
8✔
496
  }
497
  if (clientReset !== undefined) {
1,029✔
498
    validateClientResetConfiguration(clientReset);
12✔
499
  }
500
  // Assume the user intends to use Flexible Sync for all truthy values provided.
501
  if (flexible) {
1,029✔
502
    validateFlexibleSyncConfiguration(config);
644✔
503
  } else {
504
    validatePartitionSyncConfiguration(config);
385✔
505
  }
506
}
507

508
/**
509
 * Validate the fields of a user-provided open realm behavior configuration.
510
 */
511
function validateOpenRealmBehaviorConfiguration(
512
  config: unknown,
513
  target: string,
514
): asserts config is OpenRealmBehaviorConfiguration {
515
  assert.object(config, `'${target}' on realm sync configuration`, { allowArrays: false });
45✔
516
  assert(
45✔
517
    config.type === OpenRealmBehaviorType.DownloadBeforeOpen || config.type === OpenRealmBehaviorType.OpenImmediately,
57✔
518
    `'${target}.type' on realm sync configuration must be either '${OpenRealmBehaviorType.DownloadBeforeOpen}' or '${OpenRealmBehaviorType.OpenImmediately}'.`,
519
  );
520
  if (config.timeOut !== undefined) {
45✔
521
    assert.number(config.timeOut, `'${target}.timeOut' on realm sync configuration`);
18✔
522
  }
523
  if (config.timeOutBehavior !== undefined) {
45✔
524
    assert(
18✔
525
      config.timeOutBehavior === OpenRealmTimeOutBehavior.OpenLocalRealm ||
23✔
526
        config.timeOutBehavior === OpenRealmTimeOutBehavior.ThrowException,
527
      `'${target}.timeOutBehavior' on realm sync configuration must be either '${OpenRealmTimeOutBehavior.OpenLocalRealm}' or '${OpenRealmTimeOutBehavior.ThrowException}'.`,
528
    );
529
  }
530
}
531

532
/**
533
 * Validate the fields of a user-provided SSL configuration.
534
 */
535
function validateSSLConfiguration(config: unknown): asserts config is SSLConfiguration {
536
  assert.object(config, "'ssl' on realm sync configuration");
8✔
537
  if (config.validate !== undefined) {
8✔
538
    assert.boolean(config.validate, "'ssl.validate' on realm sync configuration");
8✔
539
  }
540
  if (config.certificatePath !== undefined) {
8!
541
    assert.string(config.certificatePath, "'ssl.certificatePath' on realm sync configuration");
×
542
  }
543
  if (config.validateCertificates !== undefined) {
8!
544
    assert.function(config.validateCertificates, "'ssl.validateCertificates' on realm sync configuration");
×
545
  }
546
}
547

548
/**
549
 * Validate the fields of a user-provided client reset configuration.
550
 */
551
function validateClientResetConfiguration(config: unknown): asserts config is ClientResetConfig {
552
  assert.object(config, "'clientReset' on realm sync configuration", { allowArrays: false });
12✔
553
  const modes = Object.values(ClientResetMode);
12✔
554
  assert(
12✔
555
    modes.includes(config.mode as ClientResetMode),
556
    `'clientReset' on realm sync configuration must be one of the following: '${modes.join("', '")}'`,
557
  );
558
  if (config.onManual !== undefined) {
12✔
559
    assert.function(config.onManual, "'clientReset.onManual' on realm sync configuration");
6✔
560
  }
561
  if (config.onAfter !== undefined) {
12!
562
    assert.function(config.onAfter, "'clientReset.onAfter' on realm sync configuration");
×
563
  }
564
  if (config.onBefore !== undefined) {
12!
565
    assert.function(config.onBefore, "'clientReset.onBefore' on realm sync configuration");
×
566
  }
567
  if (config.onFallback !== undefined) {
12!
568
    assert.function(config.onFallback, "'clientReset.onFallback' on realm sync configuration");
×
569
  }
570
}
571

572
/**
573
 * Validate the fields of a user-provided realm flexible sync configuration.
574
 */
575
function validateFlexibleSyncConfiguration(
576
  config: Record<string, unknown>,
577
): asserts config is FlexibleSyncConfiguration {
578
  const { flexible, partitionValue, initialSubscriptions } = config;
644✔
579

580
  assert(
644✔
581
    flexible === true,
582
    "'flexible' must always be true for realms using flexible sync. To enable partition-based sync, remove 'flexible' and specify 'partitionValue'.",
583
  );
584
  if (initialSubscriptions !== undefined) {
644✔
585
    assert.object(initialSubscriptions, "'initialSubscriptions' on realm sync configuration", { allowArrays: false });
36✔
586
    assert.function(initialSubscriptions.update, "'initialSubscriptions.update' on realm sync configuration");
36✔
587
    if (initialSubscriptions.rerunOnOpen !== undefined) {
33✔
588
      assert.boolean(
16✔
589
        initialSubscriptions.rerunOnOpen,
590
        "'initialSubscriptions.rerunOnOpen' on realm sync configuration",
591
      );
592
    }
593
  }
594
  assert(
640✔
595
    partitionValue === undefined,
596
    "'partitionValue' cannot be specified when flexible sync is enabled. To enable partition-based sync, remove 'flexible' and specify 'partitionValue'.",
597
  );
598
}
599

600
/**
601
 * Validate the fields of a user-provided realm partition sync configuration.
602
 */
603
function validatePartitionSyncConfiguration(
604
  config: Record<string, unknown>,
605
): asserts config is PartitionSyncConfiguration {
606
  const { flexible, partitionValue, initialSubscriptions } = config;
385✔
607

608
  validatePartitionValue(partitionValue);
385✔
609
  // We only allow `flexible` to be `true` (for Flexible Sync) or `undefined` (for Partition Sync).
610
  // `{ flexible: false }` is not allowed because TypeScript cannot discriminate that type correctly
611
  // with `strictNullChecks` disabled, and there is no real use case for `{ flexible: false }`.
612
  assert(
385✔
613
    flexible === undefined,
614
    "'flexible' can only be specified to enable flexible sync. To enable flexible sync, remove 'partitionValue' and set 'flexible' to true.",
615
  );
616
  assert(
384✔
617
    initialSubscriptions === undefined,
618
    "'initialSubscriptions' can only be specified when flexible sync is enabled. To enable flexible sync, remove 'partitionValue' and set 'flexible' to true.",
619
  );
620
}
621

622
/**
623
 * Validate the user-provided partition value of a realm sync configuration.
624
 */
625
function validatePartitionValue(value: unknown): asserts value is PartitionValue {
626
  if (typeof value === "number") {
385✔
627
    assert(
26✔
628
      Number.isSafeInteger(value),
629
      `Expected 'partitionValue' on realm sync configuration to be an integer, got ${value}.`,
630
    );
631
  } else {
632
    assert(
359✔
633
      typeof value === "string" || value instanceof ObjectId || value instanceof UUID || value === null,
407✔
634
      `Expected 'partitionValue' on realm sync configuration to be an integer, string, ObjectId, UUID, or null, got ${TypeAssertionError.deriveType(
635
        value,
636
      )}.`,
637
    );
638
  }
639
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc