# Cookbook

# What is the cookbook for?

In the documentation simple and concise examples are favoured to avoid too much information. But to build on that knowledge, this section allows for exploring patterns/code snippets/complex examples in-depth.

# Recipes

# Table of content

# Extend the collections to fit your needs.

Don't be afraid of changing and overriding methods if that solves your problem. The aim is to make development a breeze.


# Scope building

When you see a pattern which you include in your queries often you might consider adding a "scope" method to your class.

// User.js
import { Model } from '@upfrontjs/framework';

export default class User extends Model {
    experiencedDriver() {
        return this.has('vehicle')
            .where('licensed', true)
            .where('license_acquired_at', '>=', dateTimeLib().subYears(10).toISOString())
            .scope('canDriveAnything');
    }
}

// my-file.js
import User from '@models/User';

const experiencedDrivers = await User.newQuery().experiencedDriver().get();
const experiencedSafeDrivers = await User
    .has('advancedCertification')
    .experiencedDriver()
    .get();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# Sending requests without models

With the default ApiCaller and HandlesApiRequest implementations, you can interact with your api without the need for the models or any of their methods.

import { GlobalConfig, API, ApiResponseHandler } from '@upfrontjs/framework';

const config = new GlobalConfig;
const handler = new ApiResponseHandler;
const api = new API;

const form = new FormData;
// ... collect the form entries

const response = await handler.handle(
    api.call(
        config.get('baseEndPoint').finish('/') + 'form',
        'POST',
        form,
        { 'X-Requested-With': 'Upfront' },
        { query: { parameters: 'to encode' } }
    )
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# Alias methods

There may be some logic you frequently use, but it might not be apparent at the first glance what's happening or can be made simpler to comprehend.

For example, you may express that you want the first model like so:

import User from '@models/User';

const firstUser = (await User.limit(1).get()).first();
1
2
3

This can be simplified like:

// User.js
import { Model } from '@upfrontjs/framework';

export default class User extends Model {
    async first() {
        const users = await this.limit(1).get(); // ModelCollection[User]
        
        return users.first();
    }
}

// my-file.js
import User from '@models/User';

const firstUser = await User.newQuery().first();
// then you may also use it in normal query building
const myFirstUser = await User.where('name', 'like', '%me%').first()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# Extend query builder functionality

Extending/overwriting the model should not be a daunting task. If we wanted we could add an extra method to send data to the server. In this example we add a new field on the sent data which is called appends and we're expecting the server to append additional information on the model response data.

import type { FormatsQueryParameters, QueryParams, StaticToThis } from '@upfrontjs/framework';
import { Model as BaseModel } from '@upfrontjs/framework';

export default class Model extends BaseModel implements FormatsQueryParameters {
    protected appends: string[] = [];

    public append(name: string): this {
        this.appends.push(name);
        return this;
    }

    public static append<T extends StaticToThis<Model>>(this: T, name: string): T['prototype'] {
        this.newQuery().append(name);
    }

    public withoutAppend(name: string): this {
        this.appends = this.appends.filter(appended => appended !== name);
        return this;
    }

    public formatQueryParameters(parameters: QueryParams & Record<string, any>): Record<string, any> {
        if (this.appends.length) {
            parameters.appends = this.appends;
        }

        return parameters;
    }

    public resetQueryParameters(): this {
        this.appends = [];
        return super.resetQueryParameters();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

Now if our other models extend our own model they will have the option to set the appends field on the outgoing requests.

# Send paginated requests

While it's nice to be able to paginate locally it might not be desired to get too much data upfront. In this case a pagination can be implemented that will only get the pages in question on an explicit request. Of course, you might change the typings and the implementation to fit your needs.

// paginator.ts
import type { Attributes, Model } from '@upfrontjs/framework';
import { ModelCollection } from '@upfrontjs/framework';

interface PaginatedApiResponse<T = Attributes> {
    data: T[];
    links: {
        first: string;
        last: string;
        prev: string | null;
        next: string | null;
    };
    meta: {
        current_page: number;
        /**
         * From all the existing records this is where the current items start from.
         */
        from: number;
        /**
         * From all the existing records this is where the current items go to.
         */
        to: number;
        last_page: number;
        /**
         * String representation of a number.
         */
        per_page: string;
        links: {
            url: string | null;
            label: string;
            active: boolean;
        }[];
        /**
         * Total number of records.
         */
        total: number;
        path: string;
    };
}

export interface PaginatedModels<T extends Model> {
    data: ModelCollection<T>;
    next: () => Promise<PaginatedModels<T> | undefined>;
    previous: () => Promise<PaginatedModels<T> | undefined>;
    page: (page: number) => Promise<PaginatedModels<T> | undefined>;
    hasNext: boolean;
    hasPrevious: boolean;
    from: PaginatedApiResponse['meta']['from'];
    to: PaginatedApiResponse['meta']['to'];
    total: PaginatedApiResponse['meta']['total'];
}

async function paginatedModels<T extends Model>(
    builder: T | (new() => T),
    page = 1,
    limit = 25
): Promise<PaginatedModels<T>> {
    const instance = builder instanceof Model ? builder.clone() : new builder();
    
    const response = await instance.limit(limit).page(page).call<PaginatedApiResponse<Attributes<T>>>('GET');
    const modelCollection = new ModelCollection<T>(
        response!.data.map(attributes => instance.new(attributes).setLastSyncedAt())
    );

    return {
        data: modelCollection,
        next: async () => {
            if (!response.links.next) return;
            return paginatedModels(instance, page + 1, limit);
        },
        previous: async () => {
            if (!response.links.prev) return;
            return paginatedModels(instance, page - 1, limit);
        },
        page: async (pageNumber: number) => {
            if (pageNumber > response.meta.last_page || pageNumber < 0) return;
            return paginatedModels(instance, pageNumber, limit);
        },
        from: response.meta.from,
        to: response.meta.to,
        total: response.meta.total,
        hasNext: !!response.links.next,
        hasPrevious: !!response.links.prev
    };
}

export default paginator;

// script.ts
// paginate users where column has the value of 1
const firstPage = await paginateModels(User.where('column', 1));
const secondPage = await firstPage.next();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93

Note: this isn't included in the framework by default because the package is back-end agnostic