@@ -778,6 +778,95 @@ class Store extends Collection {
778778 }
779779 }
780780 }
781+
782+ /**
783+ * Overrides collection.Base:doSort() to handle "Turbo Mode" (autoInitRecords: false).
784+ * In this mode, items are raw objects which may lack the canonical field names used by Sorters.
785+ * This method "soft hydrates" the raw items by resolving and caching the sort values.
786+ * @param {Object[] } items
787+ * @param {Boolean } silent
788+ * @protected
789+ */
790+ doSort ( items = this . _items , silent = false ) {
791+ let me = this ;
792+
793+ if ( ! me . autoInitRecords && me . model && me . sorters . length > 0 ) {
794+ const
795+ sortProperties = me . sorters . map ( s => s . property ) ,
796+ len = sortProperties . length ;
797+
798+ items . forEach ( item => {
799+ // Ensure item is not already a Record (mixed mode safety)
800+ if ( ! RecordFactory . isRecord ( item ) ) {
801+ for ( let i = 0 ; i < len ; i ++ ) {
802+ const property = sortProperties [ i ] ;
803+
804+ // Only resolve if the property is missing on the raw object
805+ if ( ! Object . hasOwn ( item , property ) ) {
806+ item [ property ] = me . resolveField ( item , property )
807+ }
808+ }
809+ }
810+ } )
811+ }
812+
813+ super . doSort ( items , silent )
814+ }
815+
816+ /**
817+ * Helper to resolve a field value from a raw data object using the Model definition.
818+ * Handles mapping, calculate, and convert.
819+ *
820+ * **Limitations & "Turbo-Safe" Requirement:**
821+ * This method resolves a *single* field in isolation. It does **not** recursively resolve dependencies.
822+ *
823+ * If Field A relies on Field B (e.g., via `calculate` or `convert`), and Field B is also a mapped/calculated field:
824+ * - **On a Record:** Field B is accessible via its getter.
825+ * - **On a Raw Object:** Field B is `undefined`.
826+ *
827+ * Therefore, Model logic (calculate/convert functions) MUST be written to be "Turbo-Safe" / "Polymorphic".
828+ * They must check for both the canonical field name (for Records) AND the raw data key (for Turbo Mode).
829+ *
830+ * @example
831+ * calculate: data => (data.mappedName || data.rawKey) + 1
832+ *
833+ * @param {Object } item The raw data object
834+ * @param {String } fieldName The canonical field name
835+ * @returns {* } The resolved value
836+ * @protected
837+ */
838+ resolveField ( item , fieldName ) {
839+ let me = this ,
840+ field = me . model . getField ( fieldName ) ,
841+ value ;
842+
843+ if ( ! field ) return undefined ;
844+
845+ if ( field . calculate ) {
846+ value = field . calculate ( item )
847+ } else {
848+ // Handle Mapping
849+ if ( field . mapping ) {
850+ let ns = field . mapping . split ( '.' ) ,
851+ key = ns . pop ( ) ,
852+ source = ns . length > 0 ? Neo . ns ( ns , false , item ) : item ;
853+
854+ if ( source && Object . hasOwn ( source , key ) ) {
855+ value = source [ key ]
856+ }
857+ } else {
858+ value = item [ fieldName ]
859+ }
860+
861+ // Handle Convert
862+ if ( field . convert ) {
863+ value = field . convert ( value , item )
864+ }
865+ }
866+
867+ return value
868+ }
869+
781870 /**
782871 * Serializes the instance into a JSON-compatible object for the Neural Link.
783872 * @returns {Object }
0 commit comments