Skip to content

Commit b77334f

Browse files
Move validation of schema change codec up to tree change codec (#26817)
## Description Move validation of schema change codec up to tree change codec. This is incremental progress toward a state where we consistently use codec versions only in places where we include the version in the actual encoded data and dispatch based on that. For places where we do not do explicit versioning of data, we want to use alternatively clearly distinct schemes modularizing the code for handling different data and different versions of the encodings for that data. This case can be strongly typed and use normal direct code dependencies to pull in the relevant logic. As part of this, moving all the schema validation logic further up the stack toward where the version dispatching happens (which is what this PR is doing) helps is get toward a point where we can snapshot the schema for each version of our codecs, and use that to help validate the format not changed. This approach also reduces the need to do schema validation to fewer places, eventually to only within ClientVersionDispatchingCodecBuilder once fully applied.
1 parent 15fa68c commit b77334f

File tree

15 files changed

+231
-199
lines changed

15 files changed

+231
-199
lines changed

packages/dds/tree/src/codec/codec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,27 @@ export interface IJsonCodec<
202202
encodedSchema?: TAnySchema;
203203
}
204204

205+
/**
206+
* Part of a codec.
207+
* @remarks
208+
* Encode and decode logic and schema for some chunk of data.
209+
* Can be composed into larger codecs, and eventually versioned at the top level using
210+
* {@link VersionDispatchingCodecBuilder}.
211+
*
212+
* This portion of a codec is not responsible for managing versioning or validation of the data against the schema.
213+
*/
214+
export interface JsonCodecPart<TDecoded, TEncodedSchema extends TAnySchema, TContext = void>
215+
extends IEncoder<TDecoded, Static<TEncodedSchema>, TContext>,
216+
IDecoder<TDecoded, Static<TEncodedSchema>, TContext> {
217+
/**
218+
* TypeBox schema which describes the encoded format for this chunk of data.
219+
* @remarks
220+
* The user of this codec can use this to build its own larger schema,
221+
* until eventually it is provided to the {@link VersionDispatchingCodecBuilder}.
222+
*/
223+
encodedSchema: TEncodedSchema;
224+
}
225+
205226
/**
206227
* Type erase the more detailed encoded type from a codec.
207228
*/
@@ -398,6 +419,7 @@ export function withSchemaValidation<
398419
}
399420
return codec.decode(encoded, context);
400421
},
422+
encodedSchema: schema,
401423
};
402424
}
403425

@@ -529,6 +551,14 @@ export const FluidClientVersion = {
529551
*/
530552
export const currentVersion: MinimumVersionForCollab = runtimeUtilsCleanedPackageVersion;
531553

554+
/**
555+
* TODO:
556+
* This needs to be documented.
557+
* Its documentation should cover at least the following:
558+
* - Is this used for anything other than testing.
559+
* - What should be included as children. For example should it include versioned codecs which dispatch base on the min version for collaboration? If so, what version of them should be used?
560+
* - What risks does having this mitigate?
561+
*/
532562
export interface CodecTree {
533563
readonly name: string;
534564
readonly version: FormatVersion;

packages/dds/tree/src/codec/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export {
2828
extractJsonValidator,
2929
type CodecName,
3030
eraseEncodedType,
31+
type JsonCodecPart,
3132
} from "./codec.js";
3233
export {
3334
DiscriminatedUnionDispatcher,

packages/dds/tree/src/feature-libraries/index.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,8 @@ export { DetachedFieldIndexSummarizer } from "./detachedFieldIndexSummarizer.js"
194194

195195
export {
196196
type SchemaChange,
197-
makeSchemaChangeCodecs,
197+
makeSchemaChangeCodec,
198198
EncodedSchemaChange,
199-
getCodecTreeForSchemaChangeFormat,
200-
SchemaChangeFormatVersion,
201199
} from "./schema-edits/index.js";
202200

203201
export { makeMitigatedChangeFamily } from "./mitigatedChangeFamily.js";

packages/dds/tree/src/feature-libraries/schema-edits/index.ts

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

6-
export {
7-
makeSchemaChangeCodecs,
8-
getCodecTreeForSchemaChangeFormat,
9-
SchemaChangeFormatVersion,
10-
} from "./schemaChangeCodecs.js";
6+
export { makeSchemaChangeCodec } from "./schemaChangeCodecs.js";
117
export type { SchemaChange } from "./schemaChangeTypes.js";
128
export { EncodedSchemaChange } from "./schemaChangeFormat.js";

packages/dds/tree/src/feature-libraries/schema-edits/schemaChangeCodecs.ts

Lines changed: 8 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -4,73 +4,24 @@
44
*/
55

66
import { assert } from "@fluidframework/core-utils/internal";
7-
import type { MinimumVersionForCollab } from "@fluidframework/runtime-definitions/internal";
87

9-
import {
10-
type CodecTree,
11-
type CodecWriteOptions,
12-
type ICodecFamily,
13-
type IJsonCodec,
14-
makeCodecFamily,
15-
withSchemaValidation,
16-
} from "../../codec/index.js";
17-
import { strictEnum, type Values } from "../../util/index.js";
8+
import type { CodecWriteOptions, JsonCodecPart } from "../../codec/index.js";
189
import { schemaCodecBuilder } from "../schema-index/index.js";
1910

2011
import { EncodedSchemaChange } from "./schemaChangeFormat.js";
2112
import type { SchemaChange } from "./schemaChangeTypes.js";
2213

2314
/**
24-
* Create a family of schema change codecs.
25-
* @param options - Specifies common codec options, including which `validator` to use.
26-
* @returns The composed codec family.
27-
* @remarks
28-
* Data encoded with this codec is not versioned.
29-
* Users of this codec must therefore ensure that the decoder always knows which version was used.
30-
*/
31-
export function makeSchemaChangeCodecs(
32-
options: CodecWriteOptions,
33-
): ICodecFamily<SchemaChange> {
34-
// TODO:
35-
// Inlining the schema change codec V1 into its parent codec,
36-
// removing the use of codec family here.
37-
return makeCodecFamily([[SchemaChangeFormatVersion.v1, makeSchemaChangeCodecV1(options)]]);
38-
}
39-
40-
/**
41-
* The format version for the schema change.
42-
* @remarks
43-
* The SchemaChangeFormat is not explicitly versioned in the data.
44-
*
45-
* TODO: Inline this codec's formats into the parent codec that references it, rather than treating this like a versioned codec. See related notes in makeSchemaChangeCodecs.
46-
*/
47-
export const SchemaChangeFormatVersion = strictEnum("SchemaChangeFormatVersion", {
48-
v1: 1,
49-
});
50-
export type SchemaChangeFormatVersion = Values<typeof SchemaChangeFormatVersion>;
51-
52-
export function getCodecTreeForSchemaChangeFormat(
53-
version: SchemaChangeFormatVersion,
54-
clientVersion: MinimumVersionForCollab,
55-
): CodecTree {
56-
return {
57-
name: "SchemaChange",
58-
version,
59-
children: [schemaCodecBuilder.getCodecTree(clientVersion)],
60-
};
61-
}
62-
63-
/**
64-
* This is independently versioned from the schemaCodec version.
15+
* Creates a codec for schema changes.
6516
* @param options - The codec options.
66-
* @returns The composed schema change codec.
17+
* @returns The composed schema change codec part.
6718
*/
68-
function makeSchemaChangeCodecV1(
19+
export function makeSchemaChangeCodec(
6920
options: CodecWriteOptions,
70-
): IJsonCodec<SchemaChange, EncodedSchemaChange> {
21+
): JsonCodecPart<SchemaChange, typeof EncodedSchemaChange> {
7122
const schemaCodec = schemaCodecBuilder.build(options);
72-
const schemaChangeCodec: IJsonCodec<SchemaChange, EncodedSchemaChange> = {
73-
encode: (schemaChange) => {
23+
return {
24+
encode: (schemaChange: SchemaChange): EncodedSchemaChange => {
7425
assert(
7526
!schemaChange.isInverse,
7627
0x933 /* Inverse schema changes should never be transmitted */,
@@ -80,7 +31,7 @@ function makeSchemaChangeCodecV1(
8031
old: schemaCodec.encode(schemaChange.schema.old),
8132
};
8233
},
83-
decode: (encoded) => {
34+
decode: (encoded: EncodedSchemaChange): SchemaChange => {
8435
return {
8536
schema: {
8637
new: schemaCodec.decode(encoded.new),
@@ -91,6 +42,4 @@ function makeSchemaChangeCodecV1(
9142
},
9243
encodedSchema: EncodedSchemaChange,
9344
};
94-
95-
return withSchemaValidation(EncodedSchemaChange, schemaChangeCodec, options.jsonValidator);
9645
}

packages/dds/tree/src/shared-tree/sharedTreeChangeCodecs.ts

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
DiscriminatedUnionDispatcher,
1313
type FormatVersion,
1414
type ICodecFamily,
15-
type ICodecOptions,
1615
type IJsonCodec,
1716
makeCodecFamily,
1817
withSchemaValidation,
@@ -21,12 +20,10 @@ import type { ChangeEncodingContext, TreeStoredSchema } from "../core/index.js";
2120
import {
2221
ModularChangeFormatVersion,
2322
type ModularChangeset,
24-
type SchemaChange,
25-
SchemaChangeFormatVersion,
2623
defaultSchemaPolicy,
2724
getCodecTreeForModularChangeFormat,
28-
getCodecTreeForSchemaChangeFormat,
29-
makeSchemaChangeCodecs,
25+
makeSchemaChangeCodec,
26+
schemaCodecBuilder,
3027
} from "../feature-libraries/index.js";
3128
import {
3229
strictEnum,
@@ -45,9 +42,6 @@ export function makeSharedTreeChangeCodecFamily(
4542
modularChangeCodecFamily: ICodecFamily<ModularChangeset, ChangeEncodingContext>,
4643
options: CodecWriteOptions,
4744
): ICodecFamily<SharedTreeChange, ChangeEncodingContext> {
48-
// TODO: since this is using the SchemaChangeCodec without explicit versioning,
49-
// it would probably be better to depend on its format directly without going through the codec family.
50-
const schemaChangeCodecs = makeSchemaChangeCodecs(options);
5145
const versions: [
5246
FormatVersion,
5347
IJsonCodec<
@@ -56,22 +50,15 @@ export function makeSharedTreeChangeCodecFamily(
5650
EncodedSharedTreeChange,
5751
ChangeEncodingContext
5852
>,
59-
][] = [...dependenciesForChangeFormat.entries()].map(
60-
([format, { modularChange, schemaChange }]) => [
61-
format,
62-
makeSharedTreeChangeCodec(
63-
modularChangeCodecFamily.resolve(modularChange),
64-
schemaChangeCodecs.resolve(schemaChange),
65-
options,
66-
),
67-
],
68-
);
53+
][] = [...dependenciesForChangeFormat.entries()].map(([format, { modularChange }]) => [
54+
format,
55+
makeSharedTreeChangeCodec(modularChangeCodecFamily.resolve(modularChange), options),
56+
]);
6957
return makeCodecFamily(versions);
7058
}
7159

7260
interface ChangeFormatDependencies {
7361
readonly modularChange: ModularChangeFormatVersion;
74-
readonly schemaChange: SchemaChangeFormatVersion;
7562
}
7663

7764
/**
@@ -117,21 +104,18 @@ export const dependenciesForChangeFormat = new Map<
117104
SharedTreeChangeFormatVersion.v3,
118105
{
119106
modularChange: ModularChangeFormatVersion.v3,
120-
schemaChange: SchemaChangeFormatVersion.v1,
121107
},
122108
],
123109
[
124110
SharedTreeChangeFormatVersion.v4,
125111
{
126112
modularChange: ModularChangeFormatVersion.v4,
127-
schemaChange: SchemaChangeFormatVersion.v1,
128113
},
129114
],
130115
[
131116
SharedTreeChangeFormatVersion.v5,
132117
{
133118
modularChange: ModularChangeFormatVersion.v5,
134-
schemaChange: SchemaChangeFormatVersion.v1,
135119
},
136120
],
137121
]);
@@ -140,14 +124,14 @@ export function getCodecTreeForChangeFormat(
140124
version: SharedTreeChangeFormatVersion,
141125
clientVersion: MinimumVersionForCollab,
142126
): CodecTree {
143-
const { modularChange, schemaChange } =
127+
const { modularChange } =
144128
dependenciesForChangeFormat.get(version) ?? fail(0xc78 /* Unknown change format */);
145129
return {
146130
name: "SharedTreeChange",
147131
version,
148132
children: [
149133
getCodecTreeForModularChangeFormat(modularChange),
150-
getCodecTreeForSchemaChangeFormat(schemaChange, clientVersion),
134+
schemaCodecBuilder.getCodecTree(clientVersion),
151135
],
152136
};
153137
}
@@ -159,14 +143,14 @@ function makeSharedTreeChangeCodec(
159143
JsonCompatibleReadOnly,
160144
ChangeEncodingContext
161145
>,
162-
schemaChangeCodec: IJsonCodec<SchemaChange>,
163-
codecOptions: ICodecOptions,
146+
codecOptions: CodecWriteOptions,
164147
): IJsonCodec<
165148
SharedTreeChange,
166149
EncodedSharedTreeChange,
167150
EncodedSharedTreeChange,
168151
ChangeEncodingContext
169152
> {
153+
const schemaChangeCodec = makeSchemaChangeCodec(codecOptions);
170154
const decoderLibrary = new DiscriminatedUnionDispatcher<
171155
EncodedSharedTreeInnerChange,
172156
[context: ChangeEncodingContext],
@@ -187,7 +171,7 @@ function makeSharedTreeChangeCodec(
187171
});
188172

189173
return withSchemaValidation(
190-
EncodedSharedTreeChange,
174+
EncodedSharedTreeChange(schemaChangeCodec.encodedSchema),
191175
{
192176
encode: (change, context) => {
193177
const changes: EncodedSharedTreeInnerChange[] = [];

packages/dds/tree/src/shared-tree/sharedTreeChangeFormat.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,32 @@
33
* Licensed under the MIT License.
44
*/
55

6-
import { type Static, Type } from "@sinclair/typebox";
6+
import { type Static, type TSchema, Type } from "@sinclair/typebox";
77

8+
import type { EncodedSchemaChange } from "../feature-libraries/index.js";
89
import { JsonCompatibleReadOnlySchema } from "../util/index.js";
910

10-
export const EncodedSharedTreeInnerChange = Type.Object({
11-
schema: Type.Optional(JsonCompatibleReadOnlySchema),
12-
data: Type.Optional(JsonCompatibleReadOnlySchema),
13-
});
11+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
12+
export function EncodedSharedTreeInnerChange<TEncodedSchema extends TSchema>(
13+
encodedSchemaSchema: TEncodedSchema,
14+
) {
15+
return Type.Object({
16+
schema: Type.Optional(encodedSchemaSchema),
17+
data: Type.Optional(JsonCompatibleReadOnlySchema),
18+
});
19+
}
1420

15-
export type EncodedSharedTreeInnerChange = Static<typeof EncodedSharedTreeInnerChange>;
21+
export type EncodedSharedTreeInnerChange<
22+
TEncodedSchema extends TSchema = typeof EncodedSchemaChange,
23+
> = Static<ReturnType<typeof EncodedSharedTreeInnerChange<TEncodedSchema>>>;
1624

17-
export const EncodedSharedTreeChange = Type.Array(EncodedSharedTreeInnerChange);
25+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
26+
export function EncodedSharedTreeChange<TEncodedSchema extends TSchema>(
27+
encodedSchemaSchema: TEncodedSchema,
28+
) {
29+
return Type.Array(EncodedSharedTreeInnerChange(encodedSchemaSchema));
30+
}
1831

19-
export type EncodedSharedTreeChange = Static<typeof EncodedSharedTreeChange>;
32+
export type EncodedSharedTreeChange<
33+
TEncodedSchema extends TSchema = typeof EncodedSchemaChange,
34+
> = Static<ReturnType<typeof EncodedSharedTreeChange<TEncodedSchema>>>;

0 commit comments

Comments
 (0)