Skip to content

Instantly share code, notes, and snippets.

@Oskang09
Last active May 28, 2019 07:14
Show Gist options
  • Save Oskang09/51ec26dec4059fa43e9a73805b4d40df to your computer and use it in GitHub Desktop.
Save Oskang09/51ec26dec4059fa43e9a73805b4d40df to your computer and use it in GitHub Desktop.
LoadableAttributes used at Querying

Loadable Attributes

As we know preload & postload for DataObject transforming is important, with this advantages i have implement data loader concept from graphQL. Batch Loader is a factory method for handle all of the keys, required attributes or querying and combine them all into one non-redundant query.

Related site:

Example we have 3 tables, posts, comments and users and we want to get data as below given.

posts: {
    id: 1,
    title: 'Cool post',
    comments: [
        {
            user: {
                id: 2,
                username: 'kwtam',
                display_name: 'kelvin',
            },
            comment: 'Wow cool post, thanks sharing',
        },
        {
            user: {
                id: 3,
                username: 'tplim',
                display_name: 'tienping',
            },
            comment: 'Wow post cool, sharing thanks',
        },
        {
            user: {
                id: 4,
                username: 'wanglin',
                display_name: '',
            },
            comment: 'post cool thanks, sharing wow',
        }
    ],
    postBy: {
        id: 1,
        username: 'oskang09',
        display_name: 'Oska',
    },
    likedBy: [
        {
            id: 3,
            username: 'tplim',
            display_name: 'tienping',
        },
        {
            id: 4,
            username: 'wanglin',
            display_name: '',
        }
    ]
}

Normallay by populate, or any ORM usually they want to get data like this would used about 9 queries or more, But with BatchLoader we only used 3 queries instead of 9.

BatchLoader taking all of the required keys when preload like posts id 1 and postload taking user id [ 1,2,3,4,4,3 ] so we must make it to unique keys. After batch loader factory completely done will build up an action function so mean that developer can choose to run it or not to run it. So in my structure, when users pass in query ela: ['comments'] will load all data required for comments. Here an example of mine definition of loadable attributes using AOP (Aspect Oriented Programming) so they have many other checking in hooks like acl, setting exists or anything.

ela: {
    before: (context) => {
        context._loaders = { 
            merchant: undefined, 
            store: undefined 
        };
        // relation many to one and auto build it
        context._loaders.store = manyToOne(
            context,
            {
                model: 'store',
                pk: 'id'
            }
        );

        context._loaders.merchant = manyToOne(
            context,
            {
                model: 'merchant',
                pk: 'id'
            }
        );

    },
    joins: {
        // calculated fields
        ordered: () => async (data) => {
            data.ordered = {};
            data.cart.forEach(
                ({ productId, count }) => {
                    if (!data.ordered[productId]) {
                        data.ordered[productId] = count;
                    } else {
                        data.ordered[productId] += count;
                    }
                }
            );
        },
        // relational fields
        store: () => async (data, context) => 
            data.store = await context._loaders.store(data.storeId),
        merchant: () => async (data, context) => 
            data.merchant = await context._loaders.merchant(data.merchantId),
    }
},

Example of AOP Hooks

const { 
    authorized,
    apply_setting,
    parameterize_query,
    
    accessible,
    apply_id,
    access_query,
    actions,
    image_crud,
    instance_limit,
    readable,
    writable,
    ela
} = require('@hooks');

const { and, iff, hasSetting, hasAccount, hasParams, isOneOf } = require('@checkers');
const { disallow } = require('@utils');

module.exports = (extraHooks) => {
    const defaultHook = {
        before: {
            all: [
                authorized.before,
                parameterize_query.before,
                apply_setting.before,
                iff(
                    hasAccount,
                    iff(
                        hasSetting('accessible', true),
                        accessible.before,
                        readable.before,
                        writable.before,                                                                                                                                       
                    ),
                    iff(
                        and(hasParams('action'), hasSetting('actions')),
                        actions.before,
                    ),
                    iff(
                        and(hasParams('ela'), hasSetting('ela')),
                        ela.before
                    ),
                    iff(
                        and(hasSetting('instance_limit', true), ({ method }) => isOneOf(method, 'create', 'remove', 'patch')),
                        instance_limit.before
                    ),
                ),
            ],
            create: [
                iff(
                    hasAccount,
                    iff(
                        hasSetting('apply_id', true),
                        apply_id.beforeCreate
                    )
                ),
                iff(
                    hasSetting('image_crud'),
                    image_crud.beforeCreate,
                ),
            ],
            find: [
                iff(
                    hasAccount,
                    iff(
                        hasSetting('access_query', true),
                        access_query.beforeFind,
                    ),
                ),
            ],
            get: [
                iff(
                    hasAccount,
                    iff(
                        hasSetting('access_query', true),
                        access_query.beforeGet,
                    ),
                ),
            ],
            update: [
                disallow(),
            ],
            patch: [
                iff(
                    hasSetting('image_crud'),
                    image_crud.beforeUpdate,
                ),
                iff(
                    and(hasParams('action'), hasSetting('actions')),
                    actions.runAction
                ),
            ],
            remove: [
    
            ],
        },
        after: {
            all: [
                iff(
                    and(hasParams('ela'), hasSetting('ela')),
                    ela.after
                )
            ],
            create: [
                (context) => {
                    if (context.result.merchantId) {
                        context.service.emit('modified', {
                            id: context.result.merchantId,
                            target: 'MERCHANT'
                        });
                    }
                }
            ],
            find: [
                iff(
                    hasSetting('image_crud'),
                    image_crud.afterFind,
                ),
                iff(
                    and(hasAccount, hasSetting('accessible', true)),
                    readable.afterFind   
                ),
            ],
            get: [
                iff(
                    hasSetting('image_crud'),
                    image_crud.afterGet,
                ),
                iff(
                    and(hasAccount, hasSetting('accessible', true)),
                    readable.afterGet 
                ),
            ],
            patch: [
                iff(
                    hasSetting('image_crud'),
                    image_crud.beforeUpdate,
                ),
                (context) => {
                    if (context.result.merchantId) {
                        context.service.emit('modified', {
                            id: context.result.merchantId,
                            target: 'MERCHANT'
                        });
                    }
                }
            ],
            remove: [
                iff(
                    hasSetting('image_crud'),
                    image_crud.afterRemove,
                ),
                (context) => {
                    if (context.result.merchantId) {
                        context.service.emit('modified', {
                            id: context.result.merchantId,
                            target: 'MERCHANT'
                        });
                    }
                }
            ],
        },
        error: {
            all: [
    
            ],
            create: [
                iff(
                    hasSetting('image_crud'),
                    image_crud.errorCreate,
                ),
            ],
            find: [
    
            ],
            get: [
    
            ],
            patch: [
                iff(
                    hasSetting('image_crud'),
                    image_crud.errorUpdate,
                ),
            ],
            remove: [
    
            ],
        }
    };

    if (extraHooks) {
        extraHooks(defaultHook);
    }
    return defaultHook;
};

Advanced

You can also querying by noSQL by given with

{
    ela: {
        field: {
          status: 'CONFIRM',
          $limit: 10,
        }
    }
}

but i haven't implement this into mine app, i would implement when i done frontend sdk, frontend wont have experienced on these query even noSQL.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment