Skip to content

Commit cd8124c

Browse files
committed
Add versionComparer, and better errors
1 parent f70c5c8 commit cd8124c

File tree

12 files changed

+102
-32
lines changed

12 files changed

+102
-32
lines changed

packages/dds/tree/api-report/tree.alpha.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -970,6 +970,7 @@ export interface SchemaCompatibilitySnapshotsOptions {
970970
readonly snapshotDirectory: string;
971971
readonly snapshotUnchangedVersions?: true;
972972
readonly version: string;
973+
readonly versionComparer?: (a: string, b: string) => number;
973974
}
974975

975976
// @public @sealed

packages/dds/tree/api-report/tree.beta.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ export interface SchemaCompatibilitySnapshotsOptions {
478478
readonly snapshotDirectory: string;
479479
readonly snapshotUnchangedVersions?: true;
480480
readonly version: string;
481+
readonly versionComparer?: (a: string, b: string) => number;
481482
}
482483

483484
// @public @sealed

packages/dds/tree/api-report/tree.legacy.beta.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,7 @@ export interface SchemaCompatibilitySnapshotsOptions {
481481
readonly snapshotDirectory: string;
482482
readonly snapshotUnchangedVersions?: true;
483483
readonly version: string;
484+
readonly versionComparer?: (a: string, b: string) => number;
484485
}
485486

486487
// @public @sealed

packages/dds/tree/src/simple-tree/api/snapshotCompatibilityChecker.ts

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -254,11 +254,15 @@ export interface SchemaCompatibilitySnapshotsOptions {
254254
* How the `snapshotDirectory` is accessed.
255255
*/
256256
readonly fileSystem: SnapshotFileSystem;
257+
/**
258+
* The current view schema.
259+
*/
260+
readonly schema: TreeViewConfiguration;
257261
/**
258262
* The current application or library version.
259263
* @remarks
260-
* This uses the {@link https://semver.org/#spec-item-11|ordering defined by semver}.
261-
* It is only compared against the version from previous snapshots (taken from this version when they were created by setting `mode` to "update") and the `minVersionForCollaboration`.
264+
* Can use any format supported by {@link SchemaCompatibilitySnapshotsOptions.versionComparer}.
265+
* Only compared against the version from previous snapshots (taken from this version when they were created by setting `mode` to "update") and the `minVersionForCollaboration`.
262266
*
263267
* Typically `minVersionForCollaboration` should be set to the oldest version currently in use, so it's helpful to use a version which can be easily measured to tell if clients are still using it.
264268
* It is also important that this version increases with every new versions of the application or library that is released (and thus might persist content which needs to be supported).
@@ -276,13 +280,11 @@ export interface SchemaCompatibilitySnapshotsOptions {
276280
* and `minVersionForCollaboration` is set appropriately using the same versioning scheme.
277281
*/
278282
readonly version: string;
279-
/**
280-
* The current view schema.
281-
*/
282-
readonly schema: TreeViewConfiguration;
283283
/**
284284
* The minimum version that the current version is expected to be able to collaborate with.
285285
* @remarks
286+
* Can use any format supported by {@link SchemaCompatibilitySnapshotsOptions.versionComparer}.
287+
*
286288
* This defines a range of versions whose schema must be forwards compatible with trees using the current schema:
287289
* Any schema from snapshots with a version greater than or equal to this must be able to view documents created with the current schema.
288290
* This means that if the current `schema` is used to create a {@link TreeView}, then {@link TreeView.upgradeSchema} is used, the older clients,
@@ -298,6 +300,17 @@ export interface SchemaCompatibilitySnapshotsOptions {
298300
* and this corresponds to whatever versioning scheme is used with {@link SchemaCompatibilitySnapshotsOptions.version}.
299301
*/
300302
readonly minVersionForCollaboration: string;
303+
304+
/**
305+
* A comparison function for version strings.
306+
* @remarks
307+
* A comparison function like that provided to {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#comparefn | Array.sort}.
308+
* This is used to partition snapshots into those less than `minVersionForCollaboration` and those greater than or equal to it, as well as to sanity check `version` against the versions of the snapshots.
309+
* If not provided, the ordering is defined by {@link https://semver.org/#spec-item-11|semver}.
310+
* @returns A negative number if `a` is less than `b`, zero if they are equal, or a positive number if `a` is greater than `b`.
311+
*/
312+
readonly versionComparer?: (a: string, b: string) => number;
313+
301314
/**
302315
* When true, every version must be snapshotted, and an increased version number will require a new snapshot.
303316
* @remarks
@@ -325,7 +338,7 @@ export interface SchemaCompatibilitySnapshotsOptions {
325338
/**
326339
* Check `currentViewSchema` for compatibility with a collection of historical schema snapshots stored in `snapshotDirectory`.
327340
*
328-
* @throws Throws errors if the input version strings (including those in snapshot file names) are not valid semver versions.
341+
* @throws Throws errors if the input version strings (including those in snapshot file names) are not valid semver versions when using default semver version comparison.
329342
* @throws Throws errors if the input version strings (including those in snapshot file names) are not ordered as expected (current being the highest, and `minVersionForCollaboration` corresponding to the current version or a lower snapshotted version).
330343
* @throws In `test` mode, throws an error if there is not an up to date snapshot for the current version.
331344
* @throws Throws an error if any snapshotted schema cannot be upgraded to the current schema.
@@ -440,17 +453,23 @@ export function checkSchemaCompatibilitySnapshots(
440453
minVersionForCollaboration,
441454
snapshotUnchangedVersions,
442455
} = options;
443-
if (semver.valid(currentVersion) === null) {
456+
457+
const validateVersion =
458+
options.versionComparer === undefined ? semver.valid : (v: string) => v;
459+
const versionComparer = options.versionComparer ?? semver.compare;
460+
461+
if (validateVersion(currentVersion) === null) {
444462
throw new UsageError(
445463
`Invalid version: ${JSON.stringify(currentVersion)}. Must be a valid semver version.`,
446464
);
447465
}
448-
if (semver.valid(minVersionForCollaboration) === null) {
466+
if (validateVersion(minVersionForCollaboration) === null) {
449467
throw new UsageError(
450468
`Invalid minVersionForCollaboration: ${JSON.stringify(minVersionForCollaboration)}. Must be a valid semver version.`,
451469
);
452470
}
453-
if (!semver.lte(minVersionForCollaboration, currentVersion)) {
471+
472+
if (versionComparer(minVersionForCollaboration, currentVersion) > 0) {
454473
throw new UsageError(
455474
`Invalid minVersionForCollaboration: ${JSON.stringify(minVersionForCollaboration)}. Must be less than or equal to current version ${JSON.stringify(currentVersion)}.`,
456475
);
@@ -463,7 +482,7 @@ export function checkSchemaCompatibilitySnapshots(
463482
}
464483

465484
const currentEncodedForSnapshotting = exportCompatibilitySchemaSnapshot(currentViewSchema);
466-
const snapshots = checker.readAllSchemaSnapshots();
485+
const snapshots = checker.readAllSchemaSnapshots(versionComparer);
467486

468487
const compatibilityErrors: string[] = [];
469488

@@ -504,7 +523,7 @@ export function checkSchemaCompatibilitySnapshots(
504523
updatableError(`No snapshots found.`);
505524
}
506525
} else {
507-
if (semver.lte(latestSnapshot[0], currentVersion)) {
526+
if (versionComparer(latestSnapshot[0], currentVersion) <= 0) {
508527
// Check to see if schema in snapshot is the same as the latest existing snapshot.
509528
const oldString = JSON.stringify(exportCompatibilitySchemaSnapshot(latestSnapshot[1]));
510529
const currentString = JSON.stringify(currentEncodedForSnapshotting);
@@ -564,7 +583,7 @@ export function checkSchemaCompatibilitySnapshots(
564583
);
565584
}
566585

567-
if (semver.eq(snapshotVersion, currentVersion)) {
586+
if (versionComparer(snapshotVersion, currentVersion) === 0) {
568587
if (currentVersion !== snapshotVersion) {
569588
throw new UsageError(
570589
`Snapshot version ${JSON.stringify(snapshotVersion)} is semantically equal but not string equal to current version ${JSON.stringify(currentVersion)}: this is not supported.`,
@@ -578,15 +597,17 @@ export function checkSchemaCompatibilitySnapshots(
578597
`Current version ${JSON.stringify(snapshotVersion)} expected to be equivalent to its snapshot.`,
579598
);
580599
}
581-
} else if (semver.lt(snapshotVersion, currentVersion)) {
600+
} else if (versionComparer(snapshotVersion, currentVersion) < 0) {
582601
if (selectedMinVersionForCollaborationSnapshot === undefined) {
583602
assert(
584603
compatibilityErrors.length > 0,
585604
0xcc7 /* expected compatibility errors for missing min collab version snapshot */,
586605
);
587606
} else {
588607
// Collaboration with this version is expected to work.
589-
if (semver.gte(snapshotVersion, selectedMinVersionForCollaborationSnapshot[0])) {
608+
if (
609+
versionComparer(snapshotVersion, selectedMinVersionForCollaborationSnapshot[0]) >= 0
610+
) {
590611
// Check that the historical version can view documents from the current version, since collaboration with this one is expected to work.
591612
if (!compatibility.snapshotViewOfCurrentDocument.canView) {
592613
const message = `Historical version ${JSON.stringify(snapshotVersion)} cannot view documents from ${JSON.stringify(currentVersion)}: these versions are expected to be able to collaborate due to the selected minVersionForCollaboration snapshot version being ${JSON.stringify(selectedMinVersionForCollaborationSnapshot[0])}.`;
@@ -603,16 +624,17 @@ export function checkSchemaCompatibilitySnapshots(
603624
}
604625
} else {
605626
throw new UsageError(
606-
`Unexpected semver comparison result between snapshot version ${JSON.stringify(snapshotVersion)} and app version ${JSON.stringify(currentVersion)}.`,
627+
`Unexpected comparison result between snapshot version ${JSON.stringify(snapshotVersion)} and app version ${JSON.stringify(currentVersion)}.`,
607628
);
608629
}
609630
}
610631

611632
if (compatibilityErrors.length > 0) {
612633
throw new Error(
613-
`Schema compatibility check failed:\n${compatibilityErrors
614-
.map((e) => ` - ${e}`)
615-
.join("\n")}`,
634+
`Schema compatibility check failed:
635+
${compatibilityErrors.map((e) => ` - ${e}`).join("\n")}
636+
Snapshots in: ${JSON.stringify(options.snapshotDirectory)}.
637+
Snapshots exist for versions: ${JSON.stringify([...snapshots.keys()], undefined, 2)}.`,
616638
);
617639
}
618640
}
@@ -662,7 +684,9 @@ export class SnapshotCompatibilityChecker {
662684
/**
663685
* Returns all schema snapshots stored in the snapshot directory, sorted in order of increasing version.
664686
*/
665-
public readAllSchemaSnapshots(): Map<string, TreeViewConfiguration> {
687+
public readAllSchemaSnapshots(
688+
compare: (a: string, b: string) => number,
689+
): Map<string, TreeViewConfiguration> {
666690
this.ensureSnapshotDirectoryExists();
667691
const files = this.fileSystemMethods.readdirSync(this.snapshotDirectory);
668692
const versions: string[] = [];
@@ -673,7 +697,7 @@ export class SnapshotCompatibilityChecker {
673697
}
674698
}
675699
// Ensures that errors are in a consistent and friendly order, independent of file system order.
676-
versions.sort((a, b) => semver.compare(a, b));
700+
versions.sort(compare);
677701

678702
const snapshots: Map<string, TreeViewConfiguration> = new Map();
679703
for (const version of versions) {

packages/dds/tree/src/test/simple-tree/api/snapshotCompatibilityChecker.spec.ts

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,12 @@ describe("snapshotCompatibilityChecker", () => {
235235
validateError(`Schema compatibility check failed:
236236
- Snapshot for current version "2.0.0" is out of date: schema has changed since latest existing snapshot version "2.0.0". If this is expected, checkSchemaCompatibilitySnapshots can be rerun in "update" mode to update the snapshot.
237237
- Current version "2.0.0" cannot upgrade documents from "2.0.0".
238-
- Current version "2.0.0" expected to be equivalent to its snapshot.`),
238+
- Current version "2.0.0" expected to be equivalent to its snapshot.
239+
Snapshots in: "${testSrcPath}/schemaSnapshots/point".
240+
Snapshots exist for versions: [
241+
"1.0.0",
242+
"2.0.0"
243+
].`),
239244
);
240245

241246
assert.throws(
@@ -253,7 +258,12 @@ describe("snapshotCompatibilityChecker", () => {
253258
- Current version "2.0.0" cannot upgrade documents from "1.0.0".
254259
- Historical version "1.0.0" cannot view documents from "2.0.0": these versions are expected to be able to collaborate due to the selected minVersionForCollaboration snapshot version being "1.0.0".
255260
- Current version "2.0.0" cannot upgrade documents from "2.0.0".
256-
- Current version "2.0.0" expected to be equivalent to its snapshot.`),
261+
- Current version "2.0.0" expected to be equivalent to its snapshot.
262+
Snapshots in: "${testSrcPath}/schemaSnapshots/point".
263+
Snapshots exist for versions: [
264+
"1.0.0",
265+
"2.0.0"
266+
].`),
257267
);
258268

259269
assert.throws(
@@ -267,7 +277,12 @@ describe("snapshotCompatibilityChecker", () => {
267277
snapshotDirectory,
268278
}),
269279
validateError(`Schema compatibility check failed:
270-
- Historical version "1.0.0" cannot view documents from "2.0.0": these versions are expected to be able to collaborate due to the selected minVersionForCollaboration snapshot version being "1.0.0".`),
280+
- Historical version "1.0.0" cannot view documents from "2.0.0": these versions are expected to be able to collaborate due to the selected minVersionForCollaboration snapshot version being "1.0.0".
281+
Snapshots in: "${testSrcPath}/schemaSnapshots/point".
282+
Snapshots exist for versions: [
283+
"1.0.0",
284+
"2.0.0"
285+
].`),
271286
);
272287

273288
// Avoids all the above tested issues, and matches saved snapshot, so should pass
@@ -349,7 +364,9 @@ describe("snapshotCompatibilityChecker", () => {
349364
validateError(
350365
`Schema compatibility check failed:
351366
- No snapshots found. If this is expected, checkSchemaCompatibilitySnapshots can be rerun in "update" mode to update the snapshot.
352-
- No snapshot found with version less than or equal to minVersionForCollaboration "1.0.0".`,
367+
- No snapshot found with version less than or equal to minVersionForCollaboration "1.0.0".
368+
Snapshots in: "dir".
369+
Snapshots exist for versions: [].`,
353370
),
354371
);
355372

@@ -392,7 +409,11 @@ describe("snapshotCompatibilityChecker", () => {
392409
snapshotDirectory,
393410
}),
394411
validateError(`Schema compatibility check failed:
395-
- Snapshot for current version "1.0.0" is out of date: schema has changed since latest existing snapshot version "1.0.0". If this is expected, checkSchemaCompatibilitySnapshots can be rerun in "update" mode to update the snapshot.`),
412+
- Snapshot for current version "1.0.0" is out of date: schema has changed since latest existing snapshot version "1.0.0". If this is expected, checkSchemaCompatibilitySnapshots can be rerun in "update" mode to update the snapshot.
413+
Snapshots in: "dir".
414+
Snapshots exist for versions: [
415+
"1.0.0"
416+
].`),
396417
);
397418

398419
// If the change was desired, a new snapshot can be taken to include it in 2.0.0:
@@ -429,7 +450,13 @@ describe("snapshotCompatibilityChecker", () => {
429450
snapshotDirectory,
430451
}),
431452
validateError(`Schema compatibility check failed:
432-
- Historical version "1.0.0" cannot view documents from "3.0.0": these versions are expected to be able to collaborate due to the selected minVersionForCollaboration snapshot version being "1.0.0".`),
453+
- Historical version "1.0.0" cannot view documents from "3.0.0": these versions are expected to be able to collaborate due to the selected minVersionForCollaboration snapshot version being "1.0.0".
454+
Snapshots in: "dir".
455+
Snapshots exist for versions: [
456+
"1.0.0",
457+
"2.0.0",
458+
"3.0.0"
459+
].`),
433460
);
434461

435462
assert.deepEqual([...snapshots.keys()], ["1.0.0.json", "2.0.0.json", "3.0.0.json"]);
@@ -468,7 +495,13 @@ describe("snapshotCompatibilityChecker", () => {
468495
snapshotUnchangedVersions: true,
469496
}),
470497
validateError(`Schema compatibility check failed:
471-
- No snapshot found for version "3.1.0": snapshotUnchangedVersions is true, so every version must be snapshotted. If this is expected, checkSchemaCompatibilitySnapshots can be rerun in "update" mode to update the snapshot.`),
498+
- No snapshot found for version "3.1.0": snapshotUnchangedVersions is true, so every version must be snapshotted. If this is expected, checkSchemaCompatibilitySnapshots can be rerun in "update" mode to update the snapshot.
499+
Snapshots in: "dir".
500+
Snapshots exist for versions: [
501+
"1.0.0",
502+
"2.0.0",
503+
"3.0.0"
504+
].`),
472505
);
473506

474507
// Here we confirm that even when running update, no new snapshot is taken if the schema is unchanged and snapshotUnchangedVersions is false.
@@ -524,7 +557,14 @@ describe("snapshotCompatibilityChecker", () => {
524557
snapshotUnchangedVersions: true,
525558
}),
526559
validateError(`Schema compatibility check failed:
527-
- Using snapshotUnchangedVersions: a snapshot of the exact minVersionForCollaboration "2.1.0" is required. No snapshot found.`),
560+
- Using snapshotUnchangedVersions: a snapshot of the exact minVersionForCollaboration "2.1.0" is required. No snapshot found.
561+
Snapshots in: "dir".
562+
Snapshots exist for versions: [
563+
"1.0.0",
564+
"2.0.0",
565+
"3.0.0",
566+
"3.1.0"
567+
].`),
528568
);
529569

530570
// Final sanity check that everything is left in a good state.

packages/dds/tree/src/test/schema-snapshots/2.0.0.json renamed to packages/dds/tree/src/test/snapshotCompatibilityCheckerExample/schema-snapshots/2.0.0.json

File renamed without changes.

packages/dds/tree/src/test/simple-tree/api/snapshotCompatibilityCheckerExample/schema.ts renamed to packages/dds/tree/src/test/snapshotCompatibilityCheckerExample/schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License.
44
*/
55

6-
import { SchemaFactory, TreeViewConfiguration } from "../../../../index.js";
6+
import { SchemaFactory, TreeViewConfiguration } from "../../index.js";
77

88
export const treeViewConfiguration: TreeViewConfiguration = new TreeViewConfiguration({
99
schema: SchemaFactory.number,

packages/dds/tree/src/test/simple-tree/api/snapshotCompatibilityCheckerExample/snapshotCompatibilityChecker.example.mts renamed to packages/dds/tree/src/test/snapshotCompatibilityCheckerExample/snapshotCompatibilityChecker.example.mts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import fs from "node:fs";
77
import path from "node:path";
88

9-
import { checkSchemaCompatibilitySnapshots } from "../../../../index.js";
9+
import { checkSchemaCompatibilitySnapshots } from "../../index.js";
1010
// import { checkSchemaCompatibilitySnapshots } from "@fluidframework/tree/beta";
1111

1212
// The TreeViewConfiguration the application uses, which contains the application's schema.
@@ -24,7 +24,7 @@ describe("schema", () => {
2424
// This will depend on how your application organizes its test data.
2525
const snapshotDirectory = path.join(
2626
import.meta.dirname,
27-
"../../../../../src/test/schema-snapshots",
27+
"../../../src/test/snapshotCompatibilityCheckerExample/schema-snapshots",
2828
);
2929
checkSchemaCompatibilitySnapshots({
3030
snapshotDirectory,

packages/dds/tree/src/test/simple-tree/api/snapshotCompatibilityCheckerExample/version.ts renamed to packages/dds/tree/src/test/snapshotCompatibilityCheckerExample/version.ts

File renamed without changes.

packages/framework/fluid-framework/api-report/fluid-framework.alpha.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1340,6 +1340,7 @@ export interface SchemaCompatibilitySnapshotsOptions {
13401340
readonly snapshotDirectory: string;
13411341
readonly snapshotUnchangedVersions?: true;
13421342
readonly version: string;
1343+
readonly versionComparer?: (a: string, b: string) => number;
13431344
}
13441345

13451346
// @public @sealed

0 commit comments

Comments
 (0)