npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@trapi/query

v2.1.5

Published

A tiny library which provides utility types/functions for request and response query handling.

Downloads

6,634

Readme

@trapi/query 🌈

main codecov Known Vulnerabilities npm version


Important NOTE

This package has been replaced by another package, called: rapiq.


This is a library to build efficient and JSON:API like REST-APIs.

It extends the specification format between request- & response-handling based on the following parameters:

  • fields
    • Description: Return only specific fields or extend the default selection.
    • URL-Parameter: fields
  • filters
    • Description: Filter the data set, according to specific criteria.
    • URL-Parameter: filter
  • relations
    • Description: Include related resources of the primary data.
    • URL-Parameter: include
  • pagination
    • Description: Limit the number of resources returned from the entire collection.
    • URL-Parameter: page
  • sort
    • Description: Sort the resources according to one or more keys in asc/desc direction.
    • URL-Parameter: sort

Table of Contents

Installation

npm install @trapi/query --save

Usage

Build 🏗

The general idea is to construct a BuildInput object and convert it to a string representation with the buildQuery method on the frontend side. The result should then be provided as a URL query string to the backend application. The backend application is then able to process the request, by parsing and interpreting the query string.

The following example should give an insight on how to use this library on the frontend side. Therefore, a type which will represent a User and a method getAPIUsers are defined. The method should perform a request to the resource API to receive a collection of entities.

import axios from "axios";
import {
    buildQuery,
    BuildInput
} from "@trapi/query";

type Profile = {
    id: number;
    avatar: string;
    cover: string;
}

type User = {
    id: number;
    name: string;
    age?: number;
    profile: Profile;
}

type ResponsePayload = {
    data: User[],
    meta: {
        limit: number,
        offset: number,
        total: number
    }
}

export async function getAPIUsers(
    record: BuildInput<User>
): Promise<ResponsePayload> {
    const response = await axios.get('users' + buildQuery(record));

    return response.data;
}

(async () => {
    const record: BuildInput<User> = {
        pagination: {
            limit: 20,
            offset: 10
        },
        filters: {
            id: 1 // some possible values:
            // 1 | [1,2,3] | '!1' | '~1' | ['!1',2,3] | {profile: {avatar: 'xxx.jpg'}}
        },
        fields: ['id', 'name'], // some possible values:
        // 'id' | ['id', 'name'] | '+id' | {user: ['id', 'name'], profile: ['avatar']}
        sort: '-id', // some possible values:
        // 'id' | ['id', 'name'] | '-id' | {id: 'DESC', profile: {avatar: 'ASC'}}
        relations: {
            profile: true
        }
    };

    const query = buildQuery(record);

    // console.log(query);
    // ?filter[id]=1&fields=id,name&page[limit]=20&page[offset]=10&sort=-id&include=profile

    let response = await getAPIUsers(record);

    // do something with the response :)
})();

The next section will describe, on how to parse and interpret the query string on the backend side.

Parsing 🔎

For explanation purposes, two simple entities with a basic relation between them are declared to demonstrate the usage on the backend side:

entities.ts

import {
    Entity,
    PrimaryGeneratedColumn,
    Column,
    OneToOne,
    JoinColumn
} from "typeorm";

@Entity()
export class User {
    @PrimaryGeneratedColumn({unsigned: true})
    id: number;

    @Column({type: 'varchar', length: 30})
    @Index({unique: true})
    name: string;

    @Column({type: 'varchar', length: 255, default: null, nullable: true})
    email: string;

    @OneToOne(() => Profile)
    profile: Profile;
}

@Entity()
export class Profile {
    @PrimaryGeneratedColumn({unsigned: true})
    id: number;

    @Column({type: 'varchar', length: 255, default: null, nullable: true})
    avatar: string;

    @Column({type: 'varchar', length: 255, default: null, nullable: true})
    cover: string;

    @OneToOne(() => User)
    @JoinColumn()
    user: User;
}

Parse - Detailed

In the following code snippet, all query parameter parse functions (parseQueryFields, parseQueryFilters, ...) will be imported separately, to show the usage in detail.

import {Request, Response} from 'express';

import {
    parseQueryFields,
    parseQueryFilters,
    parseQueryRelations,
    parseQueryPagination,
    parseQuerySort,
    Parameter
} from "@trapi/query";

import {
    applyQueryParseOutput,
    useDataSource
} from 'typeorm-extension';

import { User } from './entities';

/**
 * Get many users.
 *
 * Request example
 * - url: /users?page[limit]=10&page[offset]=0&include=profile&filter[id]=1&fields[user]=id,name
 *
 * Return Example:
 * {
 *     data: [
 *         {id: 1, name: 'tada5hi', profile: {avatar: 'avatar.jpg', cover: 'cover.jpg'}}
 *      ],
 *     meta: {
 *        total: 1,
 *        limit: 20,
 *        offset: 0
 *    }
 * }
 * @param req
 * @param res
 */
export async function getUsers(req: Request, res: Response) {
    const {fields, filter, include, page, sort} = req.query;

    const dataSource = await useDataSource();
    const repository = dataSource.getRepository(User);
    const query = repository.createQueryBuilder('user');

    // -----------------------------------------------------

    const relationsParsed = parseQueryRelations(include, {
        allowed: 'profile'
    });

    const fieldsParsed = parseQueryFields(fields, {
        defaultAlias: 'user',
        // profile.id can only be used as sorting key, if the relation 'profile' is included.
        allowed: ['id', 'name', 'profile.id', 'profile.avatar'],
        relations: relationsParsed
    });

    const filterParsed = parseQueryFilters(filter, {
        defaultAlias: 'user',
        // profile.id can only be used as sorting key, if the relation 'profile' is included.
        allowed: ['id', 'name', 'profile.id'],
        relations: relationsParsed
    });

    const pageParsed = parseQueryPagination(page, {
        maxLimit: 20
    });

    const sortParsed = parseQuerySort(sort, {
        defaultAlias: 'user',
        // profile.id can only be used as sorting key, if the relation 'profile' is included.
        allowed: ['id', 'name', 'profile.id'],
        relations: relationsParsed
    });

    // -----------------------------------------------------

    // group back parsed parameter back,
    // so they can applied on the db query.
    const parsed = applyQueryParseOutput(query, {
        fields: fieldsParsed,
        // only allow filtering users by id & name
        filters: filterParsed,
        relations: relationsParsed,
        // only allow to select 20 items at maximum.
        pagination: pageParsed,
        sort: sortParsed
    });

    // -----------------------------------------------------

    const [entities, total] = await query.getManyAndCount();

    return res.json({
        data: {
            data: entities,
            meta: {
                total,
                ...parsed.pagination
            }
        }
    });
}

Parse - Short

Another way is to directly import the parseQuery method, which will handle a group of query parameter values & options. The ParseInput argument of the parseQuery method accepts multiple (alias-) property keys.

import { Request, Response } from 'express';

import {
    parseQuery,
    Parameter,
    ParseOutput
} from '@trapi/query';

import {
    applyQueryParseOutput,
    useDataSource
} from 'typeorm-extension';

/**
 * Get many users.
 *
 * ...
 *
 * @param req
 * @param res
 */
export async function getUsers(req: Request, res: Response) {
    // const {fields, filter, include, page, sort} = req.query;

    const output: ParseOutput = parseQuery(req.query, {
        fields: {
            defaultAlias: 'user',
            allowed: ['id', 'name', 'profile.id', 'profile.avatar']
        },
        filters: {
            defaultAlias: 'user',
            allowed: ['id', 'name', 'profile.id']
        },
        relations: {
            allowed: ['profile']
        },
        pagination: {
            maxLimit: 20
        },
        sort: {
            defaultAlias: 'user',
            allowed: ['id', 'name', 'profile.id']
        }
    });

    const dataSource = await useDataSource();
    const repository = dataSource.getRepository(User);
    const query = repository.createQueryBuilder('user');

    // -----------------------------------------------------

    // apply parsed data on the db query.
    const parsed = applyQueryParseOutput(query, output);

    // -----------------------------------------------------

    const [entities, total] = await query.getManyAndCount();

    return res.json({
        data: {
            data: entities,
            meta: {
                total,
                ...output.pagination
            }
        }
    });
}

Parse - Third Party Library

It can even be much simpler to parse the query key-value pairs with the typeorm-extension library, because it uses this library under the hood ⚡. This is much shorter than the previous example and has less explicit dependencies 😁.

read more

Functions

buildQuery

function buildQuery<T>(record: BuildInput<T>, options?: BuildOptions): string

Build a query string from a provided BuildInput.

Example

Simple

import {
    buildQuery,
    Parameter
} from "@trapi/query";

type User = {
    id: number,
    name: string,
    age?: number
}

const query: string = buildQuery<User>({
    fields: ['+age'],
    relations: {
        name: '~pe'
    }
});

console.log(query);
// ?fields=+age&filter[name]=~pe

Type parameters

| Name | Description | |:-------|:----------------------------------------------------------------| | T | A type, interface, or class which represent the data structure. |

Parameters

| Name | Type | Description | |:----------|:------------------|:--------------------------------------------------| | input | BuildInput<T> | Input specification more. | | options | BuildOptions | Options for building fields, filter, include, ... |

Returns

string

The function returns a string, which can be parsed with the parseQuery function.

I.e. /users?page[limit]=10&page[offset]=0&include=profile&filter[id]=1&fields[user]=id,name

parseQuery

function parseQuery(input: ParseInput, options?: ParseOptions): ParseOutput

Parse a query string to an efficient data structure ⚡. The output will be an object with each possible value of the Parameter enum as property key and the parsed data as value.

Example

Simple

import {
    FieldOperator,
    FilterOperator,
    parseQuery,
    ParseOutput,
    URLParameter
} from "@trapi/query";

const output: ParseOutput = parseQuery({
    fields: ['+age'],
    filters: {
        name: '~pe'
    }
});

console.log(output);
//{
//    fields: [
//        {key: 'age', operator: FieldOperator.INCLUDE}
//    ],
//    filters: [
//        {key: 'name', value: 'pe', operator: FilterOperator.LIKE}
//   ]
//}

Type parameters

| Name | Description | |:------|:------------|

Parameters

| Name | Type | Description | |:----------|:---------------|:-----------------------------------------------------------------------| | input | ParseInput | Query input data passed e.g. via URL more. | | options | ParseOptions | Options for parsing fields, filter, include, ... more |

Returns

ParseOutput

The function returns an object.

parseQueryParameter

function parseQueryParameter<T extends Parameter>( key: T, input: unknown, options?: ParseParameterOptions<T> ): ParseParameterOutput<T>

Parse a specific query Parameter value to an efficient data structure ⚡.

Example

fields

import {
    FieldOperator,
    FieldsParseOutput,
    Parameter,
    parseQueryParameter,
    URLParameter
} from "@trapi/query";

const output: FieldsParseOutput = parseQueryParameter(
    // 'fields' ||
    // Parameter.FIELDS | URLParameter.FIELDS
    'fields',
    ['+name'],
    {
        allowed: ['id', 'name'],
        defaultAlias: 'user'
    }
);

console.log(output);
// [{key: 'id', value: FieldOperator.INCLUDE}] ||
// [{key: 'id', value: '+'}]

filters

import {
    FiltersParseOutput,
    Parameter,
    parseQueryParameter,
    URLParameter
} from "@trapi/query";

const output: FiltersParseOutput = parseQueryParameter(
    // 'filters' | 'filter' |
    // Parameter.FILTERS | URLParameter.FILTERS
    'filters',
    {id: 1},
    {
        allowed: ['id', 'name'],
        defaultAlias: 'user'
    }
);

console.log(output);
// [{alias: 'user', key: 'id', value: 1, }]

pagination

import {
    PaginationParseOutput,
    Parameter,
    parseQueryParameter,
    URLParameter
} from "@trapi/query";

const output: PaginationParseOutput = parseQueryParameter(
    // 'pagination' | 'page' |
    // Parameter.PAGINATION | URLParameter.PAGINATION
    'pagination',
    {limit: 100},
    {
        maxLimit: 50
    }
);

console.log(output);
// {limit: 50}

relations

import {
    RelationsParseOutput,
    Parameter,
    parseQueryParameter,
    URLParameter
} from "@trapi/query";

const output: RelationsParseOutput = parseQueryParameter(
    // 'relations' || 'include' ||
    // Parameter.RELATIONS | URLParameter.RELATIONS
    'relations',
    ['roles'],
    {
        allowed: ['roles', 'photos'],
        defaultAlias: 'user'
    }
);

console.log(output);
// [{key: 'user.roles', value: 'roles'}]

sort

import {
    SortParseOutput,
    Parameter,
    parseQueryParameter,
    URLParameter
} from "@trapi/query";

const output: SortParseOutput = parseQueryParameter(
    // 'sort' ||
    // Parameter.SORT || URLParameter.SORT
    'sort',
    ['-name'],
    {
        allowed: ['id', 'name'],
        defaultAlias: 'user'
    }
);

console.log(output);
// [{alias: 'user', key: 'name', value: 'DESC'}]

Type parameters

| Name | Description | |:------|:------------|

Parameters

| Name | Type | Description | |:----------|:-----------------------------------|:-----------------------------------------------------------------------| | input | unknown | Query input data passed e.g. via URL more. | | options | ParseParameterOptions<Parameter> | Options for parsing fields, filter, include, ... more |

Returns

ParseOutput

The function returns an object.

Types

Build

BuildOptions

export type BuildOptions = {
    // empty type for now :)
}

BuildInput

export type BuildInput<
    V extends Record<string, any>
> = {
    [T in Parameter | URLParameter]?: BuildParameterInput<T, V>
}

Parse

ParseOptions

export type ParseOptions = {
    /**
     * On default all query keys are enabled.
     */
    [K in Parameter]?: ParseParameterOptions<K> | boolean
}

ParseParameterOptions

ParseInput

export type ParseInput = {
    [K in Parameter | URLParameter]?: any
}

Parameter/URLParameter

ParseOutput

export type ParseOutput = {
    [K in Parameter]?: ParseParameterOutput<K>
}

ParseParameterOutput

ParseParameter

  • ParseParameterOptions<T extends ParameterType | URLParameterType> is a generic type and returns the available options for a given parameter type, e.g.
  • ParseParameterOutput<T extends ParameterType | URLParameterType> is a generic type and returns the parsed output data for a given parameter type, e.g.

Parameter

Parameter

export enum Parameter {
    FILTERS = 'filters',
    FIELDS = 'fields',
    PAGINATION = 'pagination',
    RELATIONS = 'relations',
    SORT = 'sort'
}

export type ParameterType = `${Parameter}`;

URLParameter

export enum URLParameter {
    FILTERS = 'filter',
    FIELDS = 'fields',
    PAGINATION = 'page',
    RELATIONS = 'include',
    SORT = 'sort'
}

export type URLParameterType = `${Parameter}`;

Fields

FieldsParseOptions

export type FieldsParseOptions =
    ParseOptionsBase<Parameter.FIELDS, Record<string, string[]> | string[]>;

The type structure looks like this:

{
    aliasMapping?: Record<string, string>,
    allowed?: Record<string, string[]> | string[],
    relations?: RelationsParseOutput,
    defaultAlias?: string
}

FieldsParseOutput

export enum FieldOperator {
    INCLUDE = '+',
    EXCLUDE = '-'
}

export type FieldsParseOutputElement =
    ParseOutputElementBase<Parameter.FIELDS, FieldOperator>;
export type FieldsParseOutput =
    FieldsParseOutputElement[];

The type structure looks like this:

{
    // relation/resource alias
    alias?: string,

    // field name
    key: string,

    // '+' | '-'
    value?: FieldOperator
}

Filters

FiltersParseOptions

export type FiltersParseOptions =
    ParseOptionsBase<Parameter.FILTERS>

The type structure looks like this:

{
    aliasMapping?: Record<string, string>,
    allowed?: string[],
    relations?: RelationsParseOutput,
    defaultAlias?: string
}

FiltersParseOutput

export enum FilterOperatorLabel {
    NEGATION = 'negation',
    LIKE = 'like',
    IN = 'in'
}

export type FiltersParseOutputElement =
    ParseOutputElementBase<
        Parameter.FILTERS,
        FilterValue<string | number | boolean | null>
    > & {
        operator?: {
            [K in FilterOperatorLabel]?: boolean
        }
    };
export type FiltersParseOutput = FiltersParseOutputElement[];
{
    // relation/resource alias
    alias?: string,

    // filter name
    key: string,

    // {in: ..., ...}
    operator?: {
        [K in FilterOperatorLabel]?: boolean
    },

    value: FilterValue<string | number | boolean | null>
}

Pagination

PaginationParseOptions

export type PaginationParseOptions =
    ParseOptionsBase<Parameter.PAGINATION> & {
        maxLimit?: number
    };

The type structure looks like this:

{
    maxLimit?: number
}

PaginationParseOutput

export type PaginationParseOutput = {
    limit?: number,
    offset?: number
};

Relations

RelationsParseOptions

export type RelationsParseOptions =
    ParseOptionsBase<Parameter.SORT, string[] | string[][]>;

The type structure looks like this:

{
    aliasMapping?: Record<string, string>,
    allowed?: string[],
    defaultAlias?: string,
    includeParents?: boolean | string[] | string
}

RelationsParseOutput

export type RelationsParseOutputElement =
    ParseOutputElementBase<Parameter.RELATIONS, string>;
export type RelationsParseOutput = RelationsParseOutputElement[];

The type structure looks like this:

{
    // relation relative depth path
    key: string,

    // relation alias
    value: string
}

Sort

SortParseOptions

export type SortParseOptions = ParseOptionsBase<Parameter.SORT, string[] | string[][]>;

The type structure looks like this:

{
    aliasMapping?: Record<string, string>,
    allowed?: string[] | string[][],
    defaultAlias?: string
    relations?: RelationsParseOutput
}

SortParseOutput

export enum SortDirection {
    ASC = 'ASC',
    DESC = 'DESC'
}

export type SortParseOutputElement =
    ParseOutputElementBase<Parameter.SORT, SortDirection>;
export type SortParseOutput = SortParseOutputElement[];

The type structure looks like this:

{
    // resource/relation alias
    alias?: string,

    // field name
    key: string,

    // 'ASC' | 'DESC'
    value: SortDirection
}