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

prepare-sql-query

v1.0.0

Published

Node.js solution for building a complex and flexible PostgreSQL query

Downloads

75

Readme

prepare-sql-query

Node.js solution for building a complex and flexible PostgreSQL query

Installation

npm: npm install prepare-sql-query
yarn: yarn add prepare-sql-query

The reason for this solution

It is necessary to retrieve data with various conditions provided from the frontend, such as filtering by criteria, searching, sorting, limiting, pagination, and other conditions.
It is very useful for using with REST API or graphQL in a controller

Query with filters and search

To create filters and perform searches, the data needs to be formatted correctly. See the example:

import prepareSQLQuery, { removeSpecialSymbols } from 'prepare-sql-query';

// Association field type with db table (needs for correct filtering)
const FILTER_WITH_TABLES = {
  month: {
    table: 'data.articles',
    field: 'created_at',
    query: 'EXTRACT(MONTH FROM data.articles.created_at) = :value'
  },
  year: {
    table: 'data.articles',
    field: 'created_at',
    query: 'EXTRACT(YEAR FROM data.articles.created_at) = :value'
  },
  isPublished: {
    table: 'data.articles',
    field: 'is_published',
    query: null
  }
};

/**
 * Show all data
 */
const showAllData = async ({ meta, search, filters }) => {
  // Addition condition for search
  const { data, totalCount } = await dataWithSearch({ search, filters, meta });

  return {
    totalCount,
    data
  };
};

/**
 * Get model with search data and filtering
 *
 * @param search {string} for search
 * @param filters {object} for filtering { field: value } Needs to use FILTER_WITH_TABLES dict
 * @param meta {object} for sorting and pagination
 * @return {Promise}
 */
const dataWithSearch = async ({ search, filters, meta }) => {
  // Array of objects with conditions in the format: [{ query: 'Condition query string', binding: { key: value } }]
  const where = [];

  // A main query for the resolver
  const mainQuery = 'SELECT data.articles.* FROM data.articles';

  // If a search exists, create an additional query with the necessary fields for searching
  if (search) {
    where.push({
      query: `(substring(data.articles.id::varchar from '^' || :search || '(.*)$') IS NOT NULL OR
               substring(LOWER(data.articles.title) from '^' || :search || '(.*)$') IS NOT NULL OR
               substring(LOWER(data.articles.slug) from '.*' || :search || '(.*)$') IS NOT NULL OR
               substring(to_char(data.articles.created_at, 'DD/MM/YYYY') from '^' || :search || '(.*)$') IS NOT NULL)`,
      binding: { search: removeSpecialSymbols(search.toLowerCase()) }
    });
  }

  // Preparing query (add conditions, bindings)
  const query = await prepareSQLQuery({
    mainQuery,
    where,
    meta,
    sortingTableName: 'data.articles',
    filters,
    filterRules: FILTER_WITH_TABLES
  });

  if (query.totalCount === 0) return { data: [], totalCount: query.totalCount };

  // Now we can call the prepared SQL query with the help of Sequelize for example
  const data = await sequelize.query(query.preparedQuery, {
    model: models.article, // Article Sequelize model. In this example, it is used together with GraphQL
    mapToModel: true,
    replacements: query.bindings
  });

  return { data, totalCount: query.totalCount };
};

Function params:

@param mainQuery {string} the general SQL query  
  Example: 'SELECT data.articles.* FROM data.articles'
  
@param where {array} of objects with WHERE conditions and bindings ([{ query: 'Condition query string', binding: { key: value } }])  
  Example: [{ query: 'data.articles.user_id = :userId', binding: { userId: 5 } }]
  
@param doNotAddWhere {boolean} if true then doesn't need to add WHERE to the query
  If you want to use a complex query that includes a WHERE, set doNotAddWhere to true
  
  Example: "SELECT data.articles.* FROM data.articles WHERE data.articles.additional ->> 'authorName' = :authorName"
  
  This means that all WHERE conditions will be added to the current query using AND
  Don't forget to set the bindings: [{ binding: { authorName: 'Joe' } }]
  
@param groupBy {string|null} with GROUP BY query (for example: 'GROUP BY data.articles.is_published')
  Example: 'GROUP BY data.articles.year, data.articles.month';
  
@param meta {object} meta data (perPage: Int, offset: Int, order: String, orderBy: String), 
  by default { perPage: 25, offset: 0, order: 'ASC', orderBy: By default sort by id DESC and created_at DESC if table not null }
  
@param orderRaw {string|null} SQL order string, for example:
                                'ORDER BY t.year DESC, t.month DESC, t.week DESC'
                              If exist then we will use this data instead meta.order/meta.orderBy/sortingTableName
                              
@param sortingTableName {string|null} name of a table with schema for sorting (for example: 'data.articles') if meta is used

@param filters {object} filter conditions (for example: { isPublished: true })

@param filterRules {object} with dictionary for filtering, in our case, it is FILTER_WITH_TABLES

@return {object} - { preparedQuery, bindings, totalCount }

FILTER_WITH_TABLES structure:

For example:

const FILTER_WITH_TABLES = {
  month: {
    table: 'data.articles',
    field: 'created_at',
    query: 'EXTRACT(MONTH FROM data.articles.created_at) = :value'
  },
  year: {
    table: 'data.articles',
    field: 'created_at',
    query: 'EXTRACT(YEAR FROM data.articles.created_at) = :value'
  },
  isPublished: {
    table: 'data.articles',
    field: 'is_published',
    query: null
  }
};

Where:
keys of object - have to be equal of keys from a frontend filters
table - a table name with schema.
field - a field name for a condition (will be used condition field = :value from the corresponding the filters key) query - an additional query. If it is not null, the field value will be ignored, and only this query will be used