adonisjs / rfcs

💬 Sharing big changes with community to add them to the AdonisJs eco-system
52 stars 6 forks source link

Presenters (laravel resource) #32

Closed darklight9811 closed 4 years ago

darklight9811 commented 4 years ago

Brief history

Presenters help us format the data from a model/object without altering the object itself. It is just like Laravel API resources, but with a few enhancements.

What problem does it solve?

Presenters help us format the data from a model/object without altering the object itself. It's useful when you want to format it in many different situations presenting different data. Such as paginating/collections, single object, etc.

Proposal

It smartly detects pagination and the object iself.

Abstract class

interface Presenter<T = GenericObject> {
    format (model: T): T;
    formatList? (model: T): T;
}

interface GenericObject {
    [key: string]: any;
}

export default abstract class BasePresenter implements Presenter {
    // -------------------------------------------------
    // Properties
    // -------------------------------------------------

    protected data: GenericObject | GenericObject[];

    // -------------------------------------------------
    // Main methods
    // -------------------------------------------------

    constructor (model: GenericObject, extraData?: GenericObject, formatType?: string) {
        // Is pagination
        const list = this.preparePaginatedData(model);
        if (list) {
            let data;

            // Use custom formatter
            if (formatType) {
                data = this.prepareFormatList(list, formatType);
            }

            // Use default list format if found or just format
            data = this.prepareFormatList(list, (this as any).formatList? 'formatList' : 'format');

            // Insert the rest of the pagination data
            const insert = {...(extraData || {}),...this.formatPagination({...model,data})};

            for (const key in insert) {
                this[key] = insert[key];
            }
        }
        // Object
        else {
            this.data = this[formatType? formatType:'format'](model);

            // Insert extra data
            if (extraData) {
                for (const key in extraData) {
                    this[key] = extraData[key];
                }
            }
        }
    }

    // -------------------------------------------------
    // Abstractions
    // -------------------------------------------------

    public abstract format (model: GenericObject): GenericObject;

    public formatPagination (paginationObject: GenericObject): GenericObject {
        return {
            data:           paginationObject.data,
            totalNumber:    paginationObject.totalNumber,
            perPage:        paginationObject.perPage,
            currentPage:    paginationObject.currentPage,
            qs:             paginationObject.qs,
            url:            paginationObject.url,
            firstPage:      paginationObject.firstPage,
            isEmpty:        paginationObject.isEmpty,
            total:          paginationObject.total,
            hasTotal:       paginationObject.hasTotal,
            lastPage:       paginationObject.lastPage,
            hasMorePages:   paginationObject.hasMorePages,
            hasPages:       paginationObject.hasPages,
        };
    }

    // -------------------------------------------------
    // Helper methods
    // -------------------------------------------------

    private prepareFormatList (data: GenericObject, type: string) {
        return data.map(item => this[type](item));
    }

    private preparePaginatedData (data: GenericObject) {
        if (data.data && Array.isArray(data.data))
            return data.data;
        if (data.rows && Array.isArray(data.rows))
            return data.rows;
    }
}

Example class

export default class UserPresenter extends BasePresenter {
    public format (model: GenericObject): GenericObject {
        return {
            id:         model.id,
            name:       model.name,
            username:   model.username,
            email:      model.email,
        };
    }

    public formatList (model: GenericObject): GenericObject {
        return {
            id:         model.id,
            name:       model.name,
            email:      model.email,
        };
    }

    public formatPagination (model: GenericObject): GenericObject {
        return {
            data:       model.data,
            perPage:    model.perPage,
            total:      model.totalNumber,
            page:       model.currentPage,
        };
    }
}

Example usage

// user service returns a lucid pagination object
public async index ({request}: HttpContextContract) {
    const limit = request.input('limit', 10);
    const page  = request.input('page', 1);
    const data  = await UserService.index(page, limit);

    return new Presenter(data);
}

// user service returns a lucid single model instance
public async show ({params}: HttpContextContract) {
    const data = await UserService.show(params.id);

    return new Presenter(data);
}
thetutlage commented 4 years ago

Is there any reason that this has to be part of the core and not the user code base? Or maybe a separate package all together?

darklight9811 commented 4 years ago

@thetutlage, actually it could be in a separate package, but I think it should be part of the adonisjs organization, or better, considered an official package, the reasons:

RomainLanz commented 4 years ago

There's already a package by @rhwilr (adonis-bumblebee) that does that well. I don't believe this should be added to the core of the framework.

thetutlage commented 4 years ago

The reason Laravel does it doesn't I am forced to do it. If you invest some time reading the AdonisJS docs, you will find, you can perform serialization just by using the models.

Also, I suggest opening RFCs with a broader perspective and first try to understand what AdonisJS is and not consider it as a clone of Laravel.