@@ -474,8 +474,110 @@ class _DocumentMask {
474474 return _DocumentMask (fieldPaths);
475475 }
476476
477+ /// Creates a document mask from a list of field paths.
478+ factory _DocumentMask .fromFieldMask (List <FieldPath > fieldMask) {
479+ return _DocumentMask (List .from (fieldMask));
480+ }
481+
482+ /// Creates a document mask with the field names of a document.
483+ /// Recursively extracts all field paths from the data object.
484+ factory _DocumentMask .fromObject (Map <String , Object ?> data) {
485+ final fieldPaths = < FieldPath > [];
486+
487+ void extractFieldPaths (
488+ Map <String , Object ?> currentData, [
489+ FieldPath ? currentPath,
490+ ]) {
491+ var isEmpty = true ;
492+
493+ for (final entry in currentData.entries) {
494+ isEmpty = false ;
495+
496+ final key = entry.key;
497+ final childSegment = FieldPath ([key]);
498+ final childPath = currentPath != null
499+ ? currentPath.append (childSegment)
500+ : childSegment;
501+ final value = entry.value;
502+
503+ if (value is _FieldTransform ) {
504+ if (value.includeInDocumentMask) {
505+ fieldPaths.add (childPath);
506+ }
507+ } else if (value is Map <String , Object ?>) {
508+ extractFieldPaths (value, childPath);
509+ } else if (value != null ) {
510+ fieldPaths.add (childPath);
511+ }
512+ }
513+
514+ // Add a field path for an explicitly updated empty map.
515+ if (currentPath != null && isEmpty) {
516+ fieldPaths.add (currentPath);
517+ }
518+ }
519+
520+ extractFieldPaths (data);
521+ return _DocumentMask (fieldPaths);
522+ }
523+
477524 final List <FieldPath > _sortedPaths;
478525
526+ bool get isEmpty => _sortedPaths.isEmpty;
527+
528+ /// Removes the specified field paths from this document mask.
529+ void removeFields (List <FieldPath > fieldPaths) {
530+ _sortedPaths.removeWhere ((path) => fieldPaths.any ((fp) => path == fp));
531+ }
532+
533+ /// Returns whether this document mask contains the specified field path.
534+ bool contains (FieldPath fieldPath) {
535+ return _sortedPaths.any ((path) => path == fieldPath);
536+ }
537+
538+ /// Applies this DocumentMask to data and returns a new object containing only
539+ /// the fields specified in the mask.
540+ Map <String , Object ?> applyTo (Map <String , Object ?> data) {
541+ final remainingPaths = List <FieldPath >.from (_sortedPaths);
542+
543+ Map <String , Object ?> processObject (
544+ Map <String , Object ?> currentData, [
545+ FieldPath ? currentPath,
546+ ]) {
547+ final result = < String , Object ? > {};
548+
549+ for (final entry in currentData.entries) {
550+ final key = entry.key;
551+ final childSegment = FieldPath ([key]);
552+ final childPath = currentPath != null
553+ ? currentPath.append (childSegment)
554+ : childSegment;
555+
556+ // Check if this field or any of its children are in the mask
557+ final shouldInclude = remainingPaths.any ((path) {
558+ return path == childPath || path.isPrefixOf (childPath);
559+ });
560+
561+ if (shouldInclude) {
562+ final value = entry.value;
563+
564+ if (value is Map <String , Object ?>) {
565+ result[key] = processObject (value, childPath);
566+ } else {
567+ result[key] = value;
568+ }
569+
570+ // Remove this path from remaining
571+ remainingPaths.removeWhere ((path) => path == childPath);
572+ }
573+ }
574+
575+ return result;
576+ }
577+
578+ return processObject (data);
579+ }
580+
479581 firestore_v1.DocumentMask toProto () {
480582 if (_sortedPaths.isEmpty) return firestore_v1.DocumentMask ();
481583
0 commit comments