1- // True if any property of T is callable, indicating a class instance (e.g.
2- // Temporal.Instant, Date) rather than a plain data interface.
3- type HasMethods < T > = true extends {
4- [ K in keyof T ] -?: NonNullable < T [ K ] > extends ( ...args : never [ ] ) => unknown
5- ? true
6- : never ;
7- } [ keyof T ]
8- ? true
9- : false ;
10-
11- /**
12- * Utility type that derives all valid dot-separated field paths from a
13- * TypeScript interface. Provides compile-time path validation for
14- * {@link FieldMask}.
15- *
16- * Recursion stops at arrays, index signatures (Record/Map), and class
17- * instances with methods (e.g. Temporal.Instant, Date). These are treated
18- * as leaf nodes.
19- *
20- * @example
21- * ```ts
22- * interface Cluster {
23- * name: string;
24- * config: {numWorkers: number; scaling: {min: number}};
25- * }
26- * // "name" | "config" | "config.numWorkers" | "config.scaling" | "config.scaling.min"
27- * type ClusterPaths = FieldPaths<Cluster>;
28- * ```
29- */
30- export type FieldPaths < T , Prefix extends string = '' > = {
31- [ K in keyof T & string ] : NonNullable < T [ K ] > extends unknown [ ]
32- ? `${Prefix } ${K } ` // Array field — leaf, do not recurse.
33- : NonNullable < T [ K ] > extends Record < string , unknown >
34- ? string extends keyof NonNullable < T [ K ] >
35- ? `${Prefix } ${K } ` // Index signature (Record/Map) — leaf, do not recurse.
36- : HasMethods < NonNullable < T [ K ] > > extends true
37- ? `${Prefix } ${K } ` // Class instance with methods — leaf, do not recurse.
38- : `${Prefix } ${K } ` | FieldPaths < NonNullable < T [ K ] > , `${Prefix } ${K } .`>
39- : `${Prefix } ${K } `;
40- } [ keyof T & string ] ;
41-
421// Remove duplicates and paths subsumed by a parent (e.g. "config" subsumes
432// "config.numWorkers").
44- function normalize < P extends string > ( paths : P [ ] ) : P [ ] {
3+ function normalize ( paths : string [ ] ) : string [ ] {
454 const unique = [ ...new Set ( paths ) ] . sort ( ) ;
46- const result : P [ ] = [ ] ;
5+ const result : string [ ] = [ ] ;
476 for ( const path of unique ) {
487 const isSubsumed = result . some ( existing => path . startsWith ( existing + '.' ) ) ;
498 if ( ! isSubsumed ) {
@@ -54,33 +13,125 @@ function normalize<P extends string>(paths: P[]): P[] {
5413}
5514
5615/**
57- * A type-safe field mask implementing google.protobuf.FieldMask semantics.
58- * Provides compile-time path validation via {@link FieldPaths}. Paths are
59- * always normalized: duplicates and paths subsumed by a parent are removed.
16+ * One field entry in a {@link FieldMaskSchema}: its wire-format name and,
17+ * for message-typed fields, a lazy reference to the nested message's
18+ * schema. Array, map, enum, and scalar fields omit `children`.
19+ */
20+ export interface FieldMaskSchemaField {
21+ readonly wire : string ;
22+ readonly children ?: ( ) => FieldMaskSchema ;
23+ }
24+
25+ /**
26+ * Structural description of one message's FieldMask-reachable fields.
27+ * Maps each camelCase field name to its wire-format name and, for
28+ * message-typed fields, a lazy `() => FieldMaskSchema` reference that
29+ * lets recursive and mutually-recursive messages describe themselves
30+ * without temporal-dead-zone pitfalls.
31+ */
32+ export type FieldMaskSchema = Readonly < Record < string , FieldMaskSchemaField > > ;
33+
34+ // Walk a dot-separated camelCase path against a schema, returning the
35+ // equivalent wire-format path. Returns `undefined` when any segment
36+ // fails: a name that isn't a field of the current message, or a
37+ // non-terminal segment that doesn't reference another message. Kept
38+ // module-private — callers go through `FieldMask.build`, which is the
39+ // only sanctioned construction path.
40+ function walkFieldMaskPath (
41+ schema : FieldMaskSchema ,
42+ path : string
43+ ) : string | undefined {
44+ const segments = path . split ( '.' ) ;
45+ const wireSegments : string [ ] = [ ] ;
46+ let current : FieldMaskSchema = schema ;
47+ for ( let i = 0 ; i < segments . length ; i ++ ) {
48+ const seg = segments [ i ] ;
49+ // Existence check before lookup — `current[seg]` is typed as
50+ // FieldMaskSchemaField without noUncheckedIndexedAccess, so an
51+ // undefined check downstream would be flagged "unnecessary".
52+ if ( ! ( seg in current ) ) return undefined ;
53+ const field = current [ seg ] ;
54+ wireSegments . push ( field . wire ) ;
55+ if ( i < segments . length - 1 ) {
56+ if ( field . children === undefined ) return undefined ;
57+ current = field . children ( ) ;
58+ }
59+ }
60+ return wireSegments . join ( '.' ) ;
61+ }
62+
63+ /**
64+ * A field mask implementing google.protobuf.FieldMask semantics.
65+ *
66+ * Every mask is tagged with the message type it targets (`T`) so that
67+ * generated client methods reject a mask built for the wrong message at
68+ * compile time. Users never construct a `FieldMask` directly: the
69+ * constructor is private and the only construction path is a generated
70+ * per-message factory (e.g. `alertFieldMask(...)`) that delegates to
71+ * `FieldMask.build`. Build validates every path against the target
72+ * message's schema and throws synchronously on an unknown path, so
73+ * typos fail at the call site rather than silently on the wire.
74+ *
75+ * The wire-format translation happens once, inside `build`, and the
76+ * resulting wire-format paths are stored on the instance. `toString()`
77+ * joins them with commas when the mask is serialized onto the wire.
6078 *
61- * @example
62- * ```ts
63- * const mask = FieldMask.of<FieldPaths<Cluster>>(
64- * 'displayName',
65- * 'config.numWorkers'
66- * );
67- * ```
79+ * The camelCase paths the caller passed in are not retained. Treat the
80+ * mask as an opaque, immutable handle: construct with a factory, pass
81+ * to a client method, let the SDK serialize it. `paths` is kept
82+ * `private readonly` as a compile-time hint; it's not meant to be
83+ * accessed from outside the class.
6884 */
69- export class FieldMask < TPath extends string = string > {
70- /** The list of field paths in this mask. */
71- readonly paths : TPath [ ] ;
85+ export class FieldMask < T = unknown > {
86+ // Phantom marker: keeps `FieldMask<Alert>` and `FieldMask<Query>`
87+ // compile-time distinct under TypeScript's otherwise-structural
88+ // typing. Never set at runtime.
89+ declare private readonly _tag : T ;
7290
73- private constructor ( paths : TPath [ ] ) {
74- this . paths = normalize ( paths ) ;
91+ // Stored post-translation, normalized wire-format paths. Marked
92+ // `private readonly` so the TS compiler rejects external access and
93+ // internal reassignment; callers should treat the mask as opaque and
94+ // interact through the factory and `toString()`.
95+ private readonly paths : string [ ] ;
96+
97+ private constructor ( paths : string [ ] ) {
98+ this . paths = paths ;
7599 }
76100
77- /** Create a field mask from one or more paths. */
78- static of < P extends string > ( ...paths : P [ ] ) : FieldMask < P > {
79- return new FieldMask ( paths ) ;
101+ /**
102+ * Build a FieldMask from camelCase paths against the target message's
103+ * schema. Validates every path by walking each segment through the
104+ * schema and throws `Unknown field path "<p>"` on the first segment
105+ * that names an unknown field or descends past a scalar leaf. On
106+ * success, the paths are normalized (dedup + subsumption), translated
107+ * to wire format, and stored on the instance.
108+ *
109+ * Reserved for generated per-message factories; user code should call
110+ * the factory (e.g. `alertFieldMask(...)`), which supplies the
111+ * schema before delegating here.
112+ *
113+ * @internal
114+ */
115+ static build < T > ( paths : string [ ] , schema : FieldMaskSchema ) : FieldMask < T > {
116+ const normalized = normalize ( paths ) ;
117+ const wire : string [ ] = [ ] ;
118+ for ( const p of normalized ) {
119+ const w = walkFieldMaskPath ( schema , p ) ;
120+ if ( w === undefined ) {
121+ throw new Error ( `Unknown field path "${ p } "` ) ;
122+ }
123+ wire . push ( w ) ;
124+ }
125+ return new FieldMask < T > ( wire ) ;
80126 }
81127
82- /** Return a new mask with additional paths appended. */
83- append ( ...paths : TPath [ ] ) : FieldMask < TPath > {
84- return new FieldMask ( [ ...this . paths , ...paths ] ) ;
128+ /**
129+ * Serialize the mask to the wire-format string the server expects: a
130+ * comma-separated list of field paths in the message's wire naming
131+ * (proto snake_case for Databricks). The wire paths were computed at
132+ * construction time; this method just joins them.
133+ */
134+ toString ( ) : string {
135+ return this . paths . join ( ',' ) ;
85136 }
86137}
0 commit comments