Skip to content

Support for Recursive Discriminated Models #1995

@JeffJassky

Description

@JeffJassky

Feature Request: Support for Recursive Discriminated Models in Pinia ORM

Problem Statement

Currently, Pinia ORM supports discriminated models, but only for one level of inheritance. When working with deeper inheritance hierarchies, such as Document > File > Video, the model isn't correctly hydrated all the way to the most specific type.

Use Case

I'm working with a content management system that has a complex document hierarchy:

Document
    ├── BlockDocument
    └── File
        ├── Video
        └── Audio

Example Models

Model Definitions

// Base Document model
class Document extends Model {
  static entity = 'documents'
  static typeKey = 'documentType'
  
  static fields() {
    return {
      id: this.uid(),
      name: this.string(''),
      documentType: this.string('document')
    }
  }
  
  static types() {
    return {
      'document': Document,
      'file': File,
      'block': BlockDocument
    }
  }
}

class BlockDocument extends Document {
  static fields() {
    return {
      ...super.fields(),
      blocks: this.attr([])
    }
  }
}

class File extends Document {
  static typeKey = 'fileType'

  static fields() {
    return {
      ...super.fields(),
      fileType: this.string(''),
      size: this.number(0)
    }
  }
  
  static types() {
    return {
      'video': Video,
      'audio': Audio
    }
  }
}

class Video extends File {
  static fields() {
    return {
      ...super.fields(),
      duration: this.number(0),
      resolution: this.attr({})
    }
  }
}

class Audio extends File {
  static fields() {
    return {
      ...super.fields(),
      duration: this.number(0),
      bitrate: this.number(0)
    }
  }
}

Current Behavior

const documentData = {
  id: '123',
  name: 'My Video',
  documentType: 'file',
  fileType: 'video',
  size: 1024000,
  duration: 120,
  resolution: { width: 1920, height: 1080 }
}

// This currently only hydrates as a File, not as a Video
const document = useRepo(Document).make(documentData)
console.log(document instanceof Video) // false - should be true
console.log(document instanceof File) // true

Desired Behavior

When I retrieve a document record with both documentType: "document" and fileType: "video", I expect it to be fully hydrated as a Video instance. However, currently it only hydrates to the first level (File), and doesn't continue to the most specific type.

The hydration should recursively check for discriminated types at each level of the inheritance hierarchy, and create an instance of the most specific type applicable.

Solution Proposal

I've implemented a solution by modifying the getHydratedModel function to recursively check for discriminated types. Here's my working implementation:

	getHydratedModel(record, options) {
		const id = this.model.$entity() + this.model.$getKey(record, true);
		const operationId = (options?.operation || "") + id;
		
		// Check cache first
		if (options?.action !== "update") {
			const savedHydratedModel = this.hydratedDataCache.get(operationId);
			if (!this.getNewHydrated && savedHydratedModel) {
				return savedHydratedModel;
			}
		} else {
			this.hydratedDataCache.delete("get" + id);
		}

		// Prepare options only once
		const instanceOptions = { relations: false, ...options || {} };
		
		// Recursive re-instantiation
		let baseModel = this.model;
		let hydratedModel = baseModel.$newInstance(record, instanceOptions);
		const typeKey = baseModel.$typeKey();
		let discriminatedModel = baseModel.$types()[record[typeKey]];

		while (discriminatedModel) {
			const subModelName = discriminatedModel.name;
			
			hydratedModel = discriminatedModel.newRawInstance().$newInstance(record, instanceOptions);
			baseModel = discriminatedModel;
			
			// Get the next level of discrimination, if any
			const nextDiscriminatedType = record[hydratedModel.$typeKey()];
			discriminatedModel = nextDiscriminatedType ? hydratedModel.$types()[nextDiscriminatedType] : null;
		}

		// Cache the result if needed
		if (isEmpty(this.eagerLoad) && options?.operation !== "set") {
			this.hydratedDataCache.set(operationId, hydratedModel);
		}

		return hydratedModel;
	}

This solution properly walks through the inheritance chain until it finds the most specific applicable type.

Benefits

  1. Proper OOP inheritance model support
  2. Better type safety with specific model instances
  3. Access to specific methods and properties without type casting
  4. More intuitive API that matches the domain model hierarchy

Would appreciate if this could be considered for inclusion in the core library!

Additional information

  • Would you be willing to help implement this feature?

Final checks

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions