├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .npmignore ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── sql-puzzle.jpg ├── src ├── index.ts └── lib │ ├── functions.ts │ └── utils.ts ├── test ├── joins.spec.ts ├── queries.spec.ts └── utils.spec.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | coverage 4 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: [ 7 | 'standard-with-typescript', 8 | ], 9 | overrides: [ 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 'latest', 13 | sourceType: 'module', 14 | project: './tsconfig.json', 15 | }, 16 | rules: { 17 | '@typescript-eslint/return-await': 'off', 18 | semi: ['error', 'always'], 19 | '@typescript-eslint/semi': ['error', 'always'], 20 | 'comma-dangle': 'off', 21 | '@typescript-eslint/comma-dangle': ['error', 'always-multiline'], 22 | 'object-curly-spacing': 'off', 23 | '@typescript-eslint/object-curly-spacing': ['error', 'never'], 24 | 'max-len': ['warn', {code: 100, ignoreComments: true}], 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | 133 | build 134 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | test 3 | .gitignore 4 | .eslintrc.cjs 5 | .eslintignore 6 | jest.config.js 7 | tsconfig.json 8 | src 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "dbaeumer.vscode-eslint", 5 | "firsttris.vscode-jest-runner" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Aidin Rasti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sql-puzzle (WIP) 2 | 3 | sql-puzzle is a type-safe, highly composable, and functional query builder. It is a lightweight wrapper for Sequelize and written in TypeScript. The key idea behind sql-puzzle is to enable composable logic and reusable code. This is accomplished by defining SQL constructs at the most granular level and building upon them. 4 | 5 | ![sql-puzzle](https://github.com/Aidiiin/sql-puzzle/assets/3137261/daf35c34-259d-4e94-9f68-3c14961bdffb) 6 | 7 | ## Install 8 | 9 | Works with node v12, v14, v16, v18, and Sequelize v6. 10 | ```bash 11 | npm install sequelize --save 12 | npm install sql-puzzle --save 13 | ``` 14 | 15 | ## Examples 16 | 17 | ```typescript 18 | import { 19 | findAll, 20 | where, 21 | and, 22 | not, 23 | or, 24 | as, 25 | eq, 26 | from, 27 | select, 28 | raw, 29 | limit, 30 | type Context, 31 | asc, 32 | joinAlias, 33 | model, 34 | innerJoin, 35 | nest, 36 | } from 'sql-puzzle'; 37 | 38 | // define your models as instructed by Sequelize documents 39 | class User extends Model, InferCreationAttributes> { 40 | declare id: number; 41 | declare name: string; 42 | declare email: string; 43 | declare flag: boolean; 44 | } 45 | 46 | class Post extends Model, InferCreationAttributes> { 47 | declare id: number; 48 | declare title: string; 49 | declare content: string; 50 | declare userId: ForeignKey; 51 | 52 | declare static associations: { 53 | user: Association 54 | images: Association 55 | }; 56 | } 57 | 58 | class Comment extends Model, InferCreationAttributes> { 59 | declare id: number; 60 | declare comment: string; 61 | } 62 | 63 | class Image extends Model, InferCreationAttributes> { 64 | declare id: number; 65 | declare path: string; 66 | } 67 | 68 | const ctx = {}; 69 | const idFromUsers = [from(User), raw(true), select('id')] 70 | await findAll( 71 | ...idFromUsers, 72 | limit(3), 73 | asc('name') 74 | )(ctx); 75 | 76 | // build complex conditions 77 | await findAll( 78 | ...idFromUsers, 79 | where( 80 | and( 81 | not('flag', () => true), 82 | or( 83 | eq('id', () => 3), 84 | eq('name', 'aidin'), 85 | ), 86 | ), 87 | ), 88 | limit(20) 89 | )(ctx); 90 | 91 | 92 | 93 | // define reusable sql constructs 94 | const fromPosts = [from('post')] 95 | const selectTitlefromPosts = [...fromPosts, select('title')] 96 | const joinImages = [ 97 | innerJoin( 98 | model(Image), 99 | joinAlias('images'), 100 | ...selectImagePath, 101 | ) 102 | ]; 103 | const orderByTitleAndIdDesc = [desc('title', 'id')]; 104 | // use dynamic values 105 | const limitByValue = [limit((ctx) => ctx.count)]; 106 | 107 | // mix and match sql constructs to build new queries 108 | const res = await findAll( 109 | ...selectTitlefromPosts, 110 | ...joinImages, 111 | ...orderByTitleAndIdDesc, 112 | )(ctx); 113 | 114 | const res = await findAll( 115 | ...selectTitlefromPosts, 116 | ...joinImages, 117 | )(ctx); 118 | 119 | const res = await findAll( 120 | ...selectTitlefromPosts, 121 | ...limitByValue 122 | )(ctx); 123 | 124 | 125 | 126 | const selectImagePath = select('path') 127 | const joinPosts = [ 128 | innerJoin( 129 | model(Post), 130 | joinAlias('posts'), 131 | ...joinImages 132 | ) 133 | ]; 134 | 135 | const res = await findAll( 136 | from(User), nest(true), 137 | ...joinPosts, 138 | asc('id'), 139 | )(ctx); 140 | 141 | const res = await findAll( 142 | from(Post), 143 | nest(true), 144 | ...joinImages, 145 | asc('id'), 146 | )(ctx); 147 | 148 | const res = await findAll( 149 | select('id', as('name', 'new_name')), 150 | from(Post), 151 | nest(true), 152 | ...joinImages, 153 | asc('id'), 154 | )(ctx); 155 | ``` 156 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | // module.exports = { 3 | // preset: 'ts-jest', 4 | // testEnvironment: 'node', 5 | // }; 6 | 7 | const config = { 8 | preset: 'ts-jest/presets/default-esm', 9 | // transform: {'^.+\\.ts?$': 'ts-jest'}, 10 | testEnvironment: 'node', 11 | testRegex: '/test/.*\\.(test|spec)?\\.(ts|tsx)$', 12 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sql-puzzle", 3 | "version": "0.0.5", 4 | "description": "A highly composable and functional query builder for Sequelize, written in TypeScript.", 5 | "main": "./src/index.js", 6 | "typings": "./src/index.d.ts", 7 | "scripts": { 8 | "test": "jest", 9 | "cp": "npm run clean && tsc", 10 | "clean": "rm ./dist -r", 11 | "clean-deps": "rm ./node_modules -r", 12 | "release": "npm run build && cp README.md ./dist/. && cp LICENSE ./dist/. && cp .npmignore ./dist/. && cp package.json ./dist/. && cd dist && npm publish --access public" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/Aidiiin/sql-puzzle.git" 17 | }, 18 | "author": "Aidin Rasti", 19 | "license": "MIT", 20 | "type": "module", 21 | "bugs": { 22 | "url": "https://github.com/Aidiiin/sql-puzzle/issues" 23 | }, 24 | "homepage": "https://github.com/Aidiiin/sql-puzzle#readme", 25 | "devDependencies": { 26 | "@tsconfig/node16-strictest-esm": "^1.0.3", 27 | "@types/jest": "^29.4.0", 28 | "@types/lodash": "^4.14.191", 29 | "@typescript-eslint/eslint-plugin": "^5.52.0", 30 | "@typescript-eslint/parser": "^5.52.0", 31 | "eslint": "^8.34.0", 32 | "eslint-config-standard-with-typescript": "^34.0.0", 33 | "eslint-plugin-import": "^2.27.5", 34 | "eslint-plugin-n": "^15.6.1", 35 | "eslint-plugin-promise": "^6.1.1", 36 | "jest": "^29.4.3", 37 | "sqlite3": "^5.1.6", 38 | "ts-jest": "^29.0.5", 39 | "typescript": "^4.9.5" 40 | }, 41 | "peerDependencies": { 42 | "sequelize": "^6.0.0" 43 | }, 44 | "dependencies": { 45 | "fp-ts": "^2.13.1" 46 | }, 47 | "files": [ 48 | "src" 49 | ], 50 | "tags": [ 51 | "orm", 52 | "sequelize", 53 | "query-builder" 54 | ], 55 | "keywords": [ 56 | "orm", 57 | "sequelize", 58 | "query-builder" 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /sql-puzzle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aidinrs/sql-puzzle/20198e9013bc1c2f21d91b0fb3341ffaa7e9d60d/sql-puzzle.jpg -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/functions'; 2 | -------------------------------------------------------------------------------- /src/lib/functions.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | import { 3 | Model, 4 | type Attributes, 5 | type FindOptions, 6 | type ModelStatic, 7 | Transaction, 8 | type LOCK, 9 | // type ProjectionAlias, 10 | type Transactionable, 11 | Sequelize, 12 | WhereOptions, 13 | WhereOperators, 14 | Op, 15 | type Order, 16 | type OrderItem, 17 | type WhereAttributeHashValue, 18 | WhereAttributeHash, 19 | WhereGeometryOptions, 20 | InferAttributes, 21 | InferCreationAttributes, 22 | IncludeOptions, 23 | ModelType, 24 | GroupOption, 25 | CountOptions, 26 | } from 'sequelize'; 27 | import {Either, right, left, tryCatch} from 'fp-ts/lib/Either'; 28 | import {isCallable, populateQueryOptions} from './utils'; 29 | import {Col, Fn, Json, Literal, Primitive, Where as WhereSq} from 'sequelize/types/utils'; 30 | /* eslint-enable */ 31 | 32 | export type Context = object & Transactionable; 33 | type StringArg = string | ((ctx?: Context) => string); 34 | type ColArg = keyof M | ((ctx?: Context) => keyof M); 35 | type BooleanArg = boolean | ((ctx?: Context) => boolean); 36 | type NumberArg = number | ((ctx?: Context) => number); 37 | 38 | type SelectArg = keyof M 39 | | TypeSafeProjectionAlias 40 | | ((ctx?: Context) => TypeSafeProjectionAlias) 41 | | ((ctx?: Context) => keyof M); 42 | 43 | export type TypeSafeProjectionAlias = 44 | readonly [keyof M | Literal | TypeSafeFnReturn | Col, string]; 45 | 46 | interface SelectAttributes { 47 | attributes: 48 | | { 49 | include?: Array | keyof M> 50 | exclude?: Array 51 | } 52 | | Array> 53 | } 54 | 55 | function include (...args: Array>): (ctx: Context) 56 | => {include: Array | keyof M>} { 57 | return function _include (ctx?: Context): {include: Array | keyof M>} { 58 | const includedAtts: Array | keyof M> = []; 59 | for (const arg of args) { 60 | if (typeof arg === 'function') { 61 | includedAtts.push(arg(ctx)); 62 | } else { 63 | includedAtts.push(arg); 64 | } 65 | } 66 | return {include: includedAtts}; 67 | }; 68 | } 69 | 70 | function exclude (...args: Array>): 71 | (ctx?: Context) => {exclude: Array} { 72 | return function _exclude (ctx?: Context): {exclude: Array} { 73 | const excludedAtts: Array = []; 74 | for (const arg of args) { 75 | if (typeof arg === 'function') { 76 | excludedAtts.push(arg(ctx)); 77 | } else { 78 | excludedAtts.push(arg); 79 | } 80 | } 81 | return {exclude: excludedAtts}; 82 | }; 83 | } 84 | 85 | type Exclude = (ctx?: Context) => {exclude: Array}; 86 | type Include = 87 | (ctx?: Context) => {include: Array | keyof M>}; 88 | 89 | export function select2 (includeArg?: Include, excludeArg?: Exclude): 90 | (ctx?: Context) => SelectAttributes { 91 | if (includeArg == null && excludeArg == null) { 92 | throw new Error('No arguments passed to the select function.'); 93 | } 94 | return function _select (ctx?: Context): SelectAttributes { 95 | const result: SelectAttributes = {attributes: {}}; 96 | if (includeArg != null && !(result.attributes instanceof Array)) { 97 | result.attributes.include = includeArg(ctx).include; 98 | } 99 | 100 | if (excludeArg != null && !(result.attributes instanceof Array)) { 101 | result.attributes.exclude = excludeArg(ctx).exclude; 102 | } 103 | return result; 104 | }; 105 | } 106 | 107 | export function select (...args: Array>): 108 | (ctx?: Context) => SelectAttributes { 109 | if (include == null && exclude == null) { 110 | throw new Error('No arguments passed to the select function.'); 111 | } 112 | return function _select (ctx?: Context): SelectAttributes { 113 | const attributes: Array> = []; 114 | for (const arg of args) { 115 | if (typeof arg === 'function') { 116 | attributes.push(arg(ctx)); 117 | } else { 118 | attributes.push(arg); 119 | } 120 | } 121 | return {attributes}; 122 | }; 123 | } 124 | 125 | export function literal (raw: string): Literal { 126 | return Sequelize.literal(raw); 127 | } 128 | 129 | export function fn (functionName: string, col: keyof M, ...args: unknown[]): 130 | (ctx?: Context) => Fn { 131 | return function _fn (ctx?: Context): Fn { 132 | const functionArgs = []; 133 | for (const arg of args) { 134 | if (typeof arg === 'function') { 135 | functionArgs.push(arg(ctx)); 136 | } else { 137 | functionArgs.push(arg); 138 | } 139 | } 140 | return Sequelize.fn(functionName, Sequelize.col(col.toString()), ...functionArgs); 141 | }; 142 | } 143 | 144 | export type TypeSafeFn = ReturnType>; 145 | export type TypeSafeFnReturn = ReturnType>>; 146 | 147 | export function max (col: keyof M): (ctx?: Context) => Fn { 148 | return fn('max', col); 149 | } 150 | 151 | export function min (col: keyof M): (ctx?: Context) => Fn { 152 | return fn('min', col); 153 | } 154 | 155 | export function sum (col: keyof M): (ctx?: Context) => Fn { 156 | return fn('sum', col); 157 | } 158 | 159 | export function count (col: keyof M): (ctx?: Context) => Fn { 160 | return fn('count', col); 161 | } 162 | 163 | export function distinct (col: keyof M): (ctx?: Context) => Fn { 164 | return fn('distinct', col); 165 | } 166 | 167 | export type AsArg = 168 | | keyof M 169 | | TypeSafeFn 170 | | Literal 171 | | Col 172 | | ((ctx?: Context) => keyof M) 173 | | ((ctx?: Context) => Literal) 174 | | ((ctx?: Context) => Col); 175 | 176 | export function as (col: AsArg, alias: string): 177 | (ctx?: Context) => TypeSafeProjectionAlias { 178 | return function _as (ctx?: Context) { 179 | if (typeof col === 'function') { 180 | return [col(ctx), alias]; 181 | } else { 182 | return [col, alias]; 183 | } 184 | }; 185 | } 186 | 187 | type FromArg = ((ctx?: Context) => ModelStatic) | ModelStatic; 188 | 189 | export function from (arg: FromArg): (ctx?: Context) 190 | => {_from: ModelStatic} { 191 | return function _from (ctx?: Context): {_from: ModelStatic} { 192 | if (isCallable(arg) && arg instanceof Function && typeof arg === 'function') { 193 | return {_from: arg(ctx)}; 194 | } else { 195 | return {_from: arg as ModelStatic}; 196 | } 197 | }; 198 | } 199 | 200 | /** Query options 201 | * --------------------------------------------------**/ 202 | type LockArg = LOCK | ((ctx?: Context) => LOCK); 203 | type OptionArg = BooleanArg | SelectArg | NumberArg | LockArg | StringArg; 204 | 205 | // export type OptionKey = keyof FindOptions> | keyof CountOptions> 206 | export type AllOptions = FindOptions> | CountOptions>; 207 | 208 | export function option ( 209 | key: keyof FindOptions>, 210 | // val: OptionValue, 211 | ): (arg: OptionArg) 212 | => (ctx?: Context) => Pick>, keyof FindOptions>> { 213 | return function ( 214 | arg: OptionArg, 215 | ): (ctx?: Context) => Pick>, keyof FindOptions>> { 216 | return function _option (ctx?: Context): 217 | Pick>, keyof FindOptions>> { 218 | if (typeof arg === 'function') { 219 | return {[key]: arg(ctx)}; 220 | } else { 221 | return {[key]: arg}; 222 | } 223 | }; 224 | }; 225 | } 226 | 227 | export function countOption ( 228 | key: keyof CountOptions>, 229 | // val: OptionValue, 230 | ): (arg: OptionArg) => (ctx?: Context) 231 | => Pick>, keyof CountOptions>> { 232 | return function ( 233 | arg: OptionArg, 234 | ): (ctx?: Context) => Pick>, keyof CountOptions>> { 235 | return function _option (ctx?: Context): 236 | Pick>, keyof CountOptions>> { 237 | if (typeof arg === 'function') { 238 | return {[key]: arg(ctx)}; 239 | } else { 240 | return {[key]: arg}; 241 | } 242 | }; 243 | }; 244 | } 245 | 246 | export const raw: (arg: BooleanArg) 247 | => (ctx?: Context) => Pick>, 'raw'> = 248 | option('raw'); 249 | 250 | export const benchmark: ( 251 | arg: BooleanArg, 252 | ) => (ctx?: Context) => Pick>, 'benchmark'> = option('benchmark'); 253 | 254 | export const skipLocked: ( 255 | arg: BooleanArg, 256 | ) => (ctx?: Context) => Pick>, 'skipLocked'> = option('skipLocked'); 257 | 258 | export const nest: (arg: BooleanArg) 259 | => (ctx?: Context) => Pick>, 'nest'> = 260 | option('nest'); 261 | 262 | export const distinctOpt: (arg: BooleanArg) => (ctx?: Context) 263 | => Pick>, 'distinct'> = countOption('distinct'); 264 | 265 | export const paranoid: ( 266 | arg: BooleanArg, 267 | ) => (ctx?: Context) => Pick>, 'paranoid'> = option('paranoid'); 268 | 269 | export const lock: (arg: LockArg) 270 | => (ctx?: Context) => Pick>, 'lock'> = 271 | option('lock'); 272 | 273 | export const logging: ( 274 | arg: BooleanArg, 275 | ) => (ctx?: Context) => Pick>, 'logging'> = option('logging'); 276 | // export const rejectOnEmpty: () => Pick>, 'rejectOnEmpty'> = option('rejectOnEmpty', true); 277 | // export const searchPath: (path: string) => Pick>, 'searchPath'> = optionKey('searchPath'); 278 | 279 | export const limit: (arg: NumberArg) 280 | => (ctx?: Context) => Pick>, 'limit'> = 281 | option('limit'); 282 | 283 | export const offset: ( 284 | arg: NumberArg, 285 | ) => (ctx?: Context) => Pick>, 'offset'> = option('offset'); 286 | 287 | /** Query conditions 288 | * --------------------------------------------------**/ 289 | 290 | export type WhereArg = 291 | WhereAttributeHash | ((ctx?: Context) => WhereAttributeHash); 292 | 293 | export function where (...args: Array>): 294 | (ctx?: Context) => WhereOptions> { 295 | return function _where (ctx?: Context) { 296 | const criteria = {}; 297 | for (const cond of args) { 298 | if (typeof cond === 'function') { 299 | const temp = cond(ctx); 300 | if (temp != null) { 301 | Object.assign(criteria, temp); 302 | } 303 | } 304 | } 305 | 306 | return {where: criteria}; 307 | }; 308 | } 309 | 310 | export type WhereOp = keyof WhereOperators; 311 | export type WhereCol = keyof WhereAttributeHash>; 312 | 313 | // same as op.ne and Op.not 314 | export type WhereValArgEq = 315 | | WhereOperators[typeof Op.eq] 316 | | ((ctx?: Context) => WhereOperators[typeof Op.eq]); 317 | // same as Op.gt, Op.lt, and Op.lte 318 | export type WhereValArgGte = 319 | | WhereOperators[typeof Op.gte] 320 | | ((ctx?: Context) => WhereOperators[typeof Op.gte]); 321 | export type WhereValArgIs = 322 | | WhereOperators[typeof Op.is] 323 | | ((ctx?: Context) => WhereOperators[typeof Op.is]); 324 | // same as Op.notBetween 325 | export type WhereValArgBetween = 326 | | WhereOperators[typeof Op.between] 327 | | ((ctx?: Context) => WhereOperators[typeof Op.between]); 328 | // same as Op.notIn 329 | export type WhereValArgIn = 330 | | WhereOperators[typeof Op.in] 331 | | ((ctx?: Context) => WhereOperators[typeof Op.in]); 332 | // same as Op.notLike, Op.ilike, and Op.notIlike 333 | export type WhereValArgLike = 334 | | WhereOperators[typeof Op.like] 335 | | ((ctx?: Context) => WhereOperators[typeof Op.like]); 336 | export type WhereValArgOverlap = 337 | | WhereOperators[typeof Op.overlap] 338 | | ((ctx?: Context) => WhereOperators[typeof Op.overlap]); 339 | export type WhereValArgContains = 340 | | WhereOperators[typeof Op.contains] 341 | | ((ctx?: Context) => WhereOperators[typeof Op.contains]); 342 | export type WhereValArgContained = 343 | | WhereOperators[typeof Op.contained] 344 | | ((ctx?: Context) => WhereOperators[typeof Op.contained]); 345 | // same as Op.endsWith and Op.substring 346 | export type WhereValArgStartsWith = 347 | | WhereOperators[typeof Op.startsWith] 348 | | ((ctx?: Context) => WhereOperators[typeof Op.startsWith]); 349 | // same as Op.notRegexp and Op.notIRegexp 350 | export type WhereValArgRegexp = 351 | | WhereOperators[typeof Op.regexp] 352 | | ((ctx?: Context) => WhereOperators[typeof Op.regexp]); 353 | // same as Op.strictRight, Op.noExtendLeft, Op.noExtendRight, and Op.adjacent 354 | export type WhereValArgStrictLeft = 355 | | WhereOperators[typeof Op.strictLeft] 356 | | ((ctx?: Context) => WhereOperators[typeof Op.strictLeft]); 357 | 358 | export type WhereValArg = 359 | | WhereValArgEq 360 | | WhereValArgGte 361 | | WhereValArgIs 362 | | WhereValArgBetween 363 | | WhereValArgIn 364 | | WhereValArgLike 365 | | WhereValArgOverlap 366 | | WhereValArgContains 367 | | WhereValArgContained 368 | | WhereValArgStartsWith 369 | | WhereValArgRegexp 370 | | WhereValArgStrictLeft; 371 | 372 | export function condition ( 373 | op: WhereOp, 374 | col: WhereCol, 375 | val: WhereValArg, 376 | ): (ctx?: Context) => WhereAttributeHash> { 377 | return function _condition (ctx?: Context): WhereAttributeHash> { 378 | if (typeof val === 'function' && val instanceof Function) { 379 | // eslint-disable-next-line @typescript-eslint/consistent-type-assertions 380 | return >>{[col]: {[op]: val(ctx)}}; 381 | } else { 382 | // eslint-disable-next-line @typescript-eslint/consistent-type-assertions 383 | return >>{[col]: {[op]: val}}; 384 | } 385 | }; 386 | } 387 | 388 | export const isTrue: ( 389 | col: WhereCol, 390 | ) => (ctx?: Context) 391 | => WhereAttributeHash> = (col) => condition(Op.is, col, true); 392 | 393 | export const isFalse: (col: WhereCol) 394 | => (ctx?: Context) => WhereAttributeHash> = ( 395 | col, 396 | ) => condition(Op.is, col, false); 397 | 398 | export const isNull: (col: WhereCol) 399 | => (ctx?: Context) => WhereAttributeHash> = ( 400 | col, 401 | ) => condition(Op.is, col, null); 402 | 403 | export const gt: ( 404 | col: WhereCol, 405 | val: WhereValArgGte, 406 | ) => (ctx?: Context) => WhereAttributeHash> = 407 | (col, val) => condition(Op.gt, col, val); 408 | 409 | export const gte: ( 410 | col: WhereCol, 411 | val: WhereValArgGte, 412 | ) => (ctx?: Context) => WhereAttributeHash> = 413 | (col, val) => condition(Op.gte, col, val); 414 | 415 | export const lt: ( 416 | col: WhereCol, 417 | val: WhereValArgGte, 418 | ) => (ctx?: Context) => WhereAttributeHash> = 419 | (col, val) => condition(Op.lt, col, val); 420 | 421 | export const lte: ( 422 | col: WhereCol, 423 | val: WhereValArgGte, 424 | ) => (ctx?: Context) => WhereAttributeHash> = 425 | (col, val) => condition(Op.lte, col, val); 426 | 427 | export const ne: ( 428 | col: WhereCol, 429 | val: WhereValArgEq, 430 | ) => (ctx?: Context) => WhereAttributeHash> = 431 | (col, val) => condition(Op.ne, col, val); 432 | 433 | export const eq: ( 434 | col: WhereCol, 435 | val: WhereValArgEq, 436 | ) => (ctx?: Context) => WhereAttributeHash> = 437 | (col, val) => condition(Op.eq, col, val); 438 | 439 | export const not: ( 440 | col: WhereCol, 441 | val: WhereValArg, 442 | ) => (ctx?: Context) => WhereAttributeHash> = 443 | (col, val) => condition(Op.not, col, val); 444 | 445 | export const notTrue: ( 446 | col: WhereCol, 447 | val: WhereValArgIs, 448 | ) => (ctx?: Context) => WhereAttributeHash> = (col, val) => not(col, true); 449 | 450 | export const notNull: ( 451 | col: WhereCol, 452 | val: WhereValArgIs, 453 | ) => (ctx?: Context) => WhereAttributeHash> = (col, val) => not(col, null); 454 | 455 | export const between: ( 456 | col: WhereCol, 457 | val: WhereValArgBetween, 458 | ) => (ctx?: Context) => WhereAttributeHash> = 459 | (col, val) => condition(Op.between, col, val); 460 | 461 | export const notBetween: ( 462 | col: WhereCol, 463 | val: WhereValArgBetween, 464 | ) => (ctx?: Context) => WhereAttributeHash> = 465 | (col, val) => condition(Op.notBetween, col, val); 466 | 467 | export const isIn: ( 468 | col: WhereCol, 469 | val: WhereValArgIn, 470 | ) => (ctx?: Context) => WhereAttributeHash = (col, val) => condition(Op.in, col, val); 471 | 472 | export const notIn: ( 473 | col: WhereCol, 474 | val: WhereValArgIn, 475 | ) => (ctx?: Context) => WhereAttributeHash> = 476 | (col, val) => condition(Op.notIn, col, val); 477 | 478 | export const like: ( 479 | col: WhereCol, 480 | val: WhereValArgLike, 481 | ) => (ctx?: Context) => WhereAttributeHash> = 482 | (col, val) => condition(Op.like, col, val); 483 | 484 | export const notLike: ( 485 | col: WhereCol, 486 | val: WhereValArgLike, 487 | ) => (ctx?: Context) => WhereAttributeHash> = 488 | (col, val) => condition(Op.notLike, col, val); 489 | 490 | export const iLike: ( 491 | col: WhereCol, 492 | val: WhereValArgLike, 493 | ) => (ctx?: Context) => WhereAttributeHash> = 494 | (col, val) => condition(Op.iLike, col, val); 495 | 496 | export const notILike: ( 497 | col: WhereCol, 498 | val: WhereValArgLike, 499 | ) => (ctx?: Context) => WhereAttributeHash> = 500 | (col, val) => condition(Op.notILike, col, val); 501 | 502 | export const startsWith: ( 503 | col: WhereCol, 504 | val: WhereValArgStartsWith, 505 | ) => (ctx?: Context) => WhereAttributeHash> = 506 | (col, val) => condition(Op.startsWith, col, val); 507 | 508 | export const endsWith: ( 509 | col: WhereCol, 510 | val: WhereValArgStartsWith, 511 | ) => (ctx?: Context) => WhereAttributeHash> = 512 | (col, val) => condition(Op.endsWith, col, val); 513 | 514 | export const substring: ( 515 | col: WhereCol, 516 | val: WhereValArgStartsWith, 517 | ) => (ctx?: Context) => WhereAttributeHash = (col, val) => condition(Op.substring, col, val); 518 | 519 | export const regexp: ( 520 | col: WhereCol, 521 | val: WhereValArgRegexp, 522 | ) => (ctx?: Context) => WhereAttributeHash> = 523 | (col, val) => condition(Op.regexp, col, val); 524 | 525 | export const notRegexp: ( 526 | col: WhereCol, 527 | val: WhereValArgRegexp, 528 | ) => (ctx?: Context) => WhereAttributeHash> = 529 | (col, val) => condition(Op.notRegexp, col, val); 530 | 531 | export const iRegexp: ( 532 | col: WhereCol, 533 | val: WhereValArgRegexp, 534 | ) => (ctx?: Context) => WhereAttributeHash> = 535 | (col, val) => condition(Op.iRegexp, col, val); 536 | 537 | export const notIRegexp: ( 538 | col: WhereCol, 539 | val: WhereValArgRegexp, 540 | ) => (ctx?: Context) => WhereAttributeHash = (col, val) => condition(Op.notIRegexp, col, val); 541 | 542 | export const overlap: ( 543 | col: WhereCol, 544 | val: WhereValArgOverlap, 545 | ) => (ctx?: Context) => WhereAttributeHash> = 546 | (col, val) => condition(Op.overlap, col, val); 547 | 548 | export function contains ( 549 | col: WhereCol, 550 | val: WhereValArgContains, 551 | ): (ctx?: Context) => WhereAttributeHash> { 552 | return function _contains (ctx?: Context): WhereAttributeHash> { 553 | if (typeof val === 'function' && val instanceof Function) { 554 | // eslint-disable-next-line @typescript-eslint/consistent-type-assertions 555 | return >>{[col]: {[Op.contains]: val(ctx)}}; 556 | } else { 557 | // eslint-disable-next-line @typescript-eslint/consistent-type-assertions 558 | return >>{[col]: {[Op.contains]: val}}; 559 | } 560 | }; 561 | } 562 | 563 | export const contained: ( 564 | col: WhereCol, 565 | val: WhereValArgContained, 566 | ) => (ctx?: Context) => WhereAttributeHash> = 567 | (col, val) => condition(Op.contained, col, val); 568 | 569 | export const adjacent: ( 570 | col: WhereCol, 571 | val: WhereValArgStrictLeft, 572 | ) => (ctx?: Context) => WhereAttributeHash> = 573 | (col, val) => condition(Op.adjacent, col, val); 574 | 575 | export const strictLeft: ( 576 | col: WhereCol, 577 | val: WhereValArgStrictLeft, 578 | ) => (ctx?: Context) => WhereAttributeHash> = 579 | (col, val) => condition(Op.strictLeft, col, val); 580 | 581 | export const strictRight: ( 582 | col: WhereCol, 583 | val: WhereValArgStrictLeft, 584 | ) => (ctx?: Context) => WhereAttributeHash> = 585 | (col, val) => condition(Op.strictRight, col, val); 586 | 587 | export const noExtendRight: ( 588 | col: WhereCol, 589 | val: WhereValArgStrictLeft, 590 | ) => (ctx?: Context) => WhereAttributeHash> = 591 | (col, val) => condition(Op.noExtendRight, col, val); 592 | 593 | export const noExtendLeft: ( 594 | col: WhereCol, 595 | val: WhereValArgStrictLeft, 596 | ) => (ctx?: Context) => WhereAttributeHash> = 597 | (col, val) => condition(Op.noExtendLeft, col, val); 598 | 599 | export function and ( 600 | ...args: Array> 601 | ): (ctx?: Context) => WhereAttributeHash> { 602 | return function (ctx?: Context): WhereAttributeHash> { 603 | const criteria = {}; 604 | for (const cond of args) { 605 | if (typeof cond === 'function') { 606 | const temp = cond(ctx); 607 | if (temp === undefined) { 608 | continue; 609 | } 610 | Object.assign(criteria, temp); 611 | } 612 | } 613 | return {[Op.and]: criteria}; 614 | }; 615 | } 616 | 617 | export function or (...args: Array>): (ctx?: Context) 618 | => WhereAttributeHash> { 619 | return function (ctx?): WhereAttributeHash> { 620 | const orOptions = []; 621 | for (const conditions of args) { 622 | if (typeof conditions === 'function') { 623 | const temp = conditions(ctx); 624 | if (temp !== undefined) { 625 | orOptions.push(temp); 626 | } 627 | } 628 | } 629 | return {[Op.or]: orOptions}; 630 | }; 631 | } 632 | 633 | /** Query order 634 | * --------------------------------------------------**/ 635 | 636 | export type OrderArg = OrderItem | ((ctx?: Context) => OrderItem); 637 | export interface OrderReturn { 638 | order: Order[] 639 | } 640 | 641 | export function order (...args: OrderArg[]): (ctx?: Context) => OrderReturn { 642 | return function _order (ctx): OrderReturn { 643 | const values: Order = []; 644 | for (const arg of args) { 645 | if (typeof arg === 'function') { 646 | const temp = arg(ctx); 647 | if (temp !== undefined) { 648 | values.push(temp); 649 | } 650 | } else { 651 | values.push(arg); 652 | } 653 | } 654 | return {order: [values]}; 655 | }; 656 | } 657 | 658 | export const desc: (...args: OrderArg[]) 659 | => (ctx?: Context) => OrderReturn = (...args) => order(...args, 'DESC'); 660 | 661 | export const descNullsFirst: (...args: OrderArg[]) => (ctx?: Context) => OrderReturn = (...args) => 662 | order(...args, 'DESC nulls first'); 663 | 664 | export const descNullsLast: (...args: OrderArg[]) => (ctx?: Context) => OrderReturn = (...args) => 665 | order(...args, 'DESC nulls last'); 666 | 667 | export const asc: (...args: OrderArg[]) 668 | => (ctx?: Context) => OrderReturn = (...args) => order(...args, 'ASC'); 669 | 670 | export const ascNullsFirst: (...args: OrderArg[]) => (ctx?: Context) => OrderReturn = (...args) => 671 | order(...args, 'ASC nulls first'); 672 | 673 | export const ascNullsLast: (...args: OrderArg[]) => (ctx?: Context) => OrderReturn = (...args) => 674 | order(...args, 'ASC nulls last'); 675 | 676 | /** joins 677 | * --------------------------------------------------**/ 678 | export type JoinOptions = Pick; 680 | 681 | export function joinOption ( 682 | key: keyof JoinOptions, 683 | ): (arg: OptionArg) => (ctx?: Context) => Pick { 684 | return function ( 685 | arg: OptionArg, 686 | ): (ctx?: Context) => Pick { 687 | return function _option (ctx?: Context): Pick { 688 | if (typeof arg === 'function') { 689 | return {[key]: arg(ctx)}; 690 | } else { 691 | return {[key]: arg}; 692 | } 693 | }; 694 | }; 695 | } 696 | 697 | export const separate: (arg: BooleanArg) => (ctx?: Context) 698 | => Pick = joinOption('separate'); 699 | export const subQuery: (arg: BooleanArg) => (ctx?: Context) 700 | => Pick = joinOption('subQuery'); 701 | export const joinAlias: (arg: StringArg) => (ctx?: Context) 702 | => Pick = joinOption('as'); 703 | export const limitJoin: (arg: NumberArg) => (ctx?: Context) 704 | => Pick = joinOption('limit'); 705 | 706 | export type JoinOptionReturn = (ctx?: Context) => JoinOptions; 707 | 708 | export function on (...args: Array>): (ctx?: Context) 709 | => WhereOptions> { 710 | return function _on (ctx?: Context) { 711 | const criteria = {}; 712 | for (const cond of args) { 713 | if (typeof cond === 'function') { 714 | const temp = cond(ctx); 715 | if (temp != null) { 716 | Object.assign(criteria, temp); 717 | } 718 | } 719 | } 720 | return {on: criteria}; 721 | }; 722 | } 723 | 724 | export type ModelArg = ModelStatic | ((ctx?: Context) => ModelStatic); 725 | export type ModelReturn = Pick; 726 | export type ModelJoin = (ctx?: Context) => ModelReturn; 727 | 728 | export function model (arg: ModelArg): (ctx?: Context) => ModelReturn { 729 | return function _model (ctx?: Context): ModelReturn { 730 | if (isCallable(arg) && typeof arg === 'function' && arg instanceof Function) { 731 | return {model: arg(ctx)}; 732 | } else { 733 | return {model: arg as ModelStatic}; 734 | } 735 | }; 736 | } 737 | 738 | export type JoinArg = ModelJoin | Where | Select | JoinOptionReturn | Join; 739 | export interface JoinReturn {_join: IncludeOptions[]} 740 | export type Join = (ctx?: Context) => JoinReturn; 741 | 742 | export function join (...args: Array>): (ctx?: Context) => JoinReturn { 743 | const populateOptions = populateQueryOptions(args); 744 | return function _join (ctx?: Context): JoinReturn { 745 | const options = populateOptions(ctx); 746 | return {_join: [options]}; 747 | }; 748 | } 749 | 750 | export const rightJoin: (...args: Array>) 751 | => (ctx?: Context) => JoinReturn = (...args) => 752 | join(joinOption('right')(true), joinOption('required')(false), ...args); 753 | 754 | export const innerJoin: (...args: Array>) 755 | => (ctx?: Context) => JoinReturn = (...args) => join(joinOption('required')(true), ...args); 756 | 757 | export type Where = (ctx?: Context) => WhereOptions>; 758 | 759 | // TODO: through for joins 760 | 761 | /** Group by queries 762 | * --------------------------------------------------**/ 763 | 764 | export function having (...args: Array>): 765 | (ctx?: Context) => WhereOptions> { 766 | return function _having (ctx?: Context) { 767 | const criteria = {}; 768 | for (const cond of args) { 769 | if (typeof cond === 'function') { 770 | const temp = cond(ctx); 771 | if (temp != null) { 772 | Object.assign(criteria, temp); 773 | } 774 | } 775 | } 776 | return {having: criteria}; 777 | }; 778 | } 779 | 780 | export type GroupByArg = StringArg | Fn | Col | ((ctx?: Context) => Fn | Col); 781 | export interface GroupByReturn {group: Array} 782 | 783 | export function groupBy (...args: GroupByArg[]): (ctx?: Context) => GroupByReturn { 784 | return function _groupBy (ctx?: Context): GroupByReturn { 785 | const group: Array = []; 786 | for (const arg of args) { 787 | if (typeof arg === 'function') { 788 | if (ctx != null) { 789 | group.push(arg(ctx)); 790 | } 791 | } 792 | } 793 | 794 | return {group}; 795 | }; 796 | } 797 | 798 | /** Query methods 799 | * --------------------------------------------------**/ 800 | 801 | export type Select = (ctx?: Context) => SelectAttributes; 802 | export type From = (ctx?: Context) => {_from: ModelStatic}; 803 | export type Option = ( 804 | ctx?: Context, 805 | ) => Pick>, keyof FindOptions>>; 806 | export type CountOption = ( 807 | ctx?: Context, 808 | ) => Pick>, keyof CountOptions>>; 809 | 810 | export type FindArgOrder = (ctx?: Context) => OrderReturn; 811 | 812 | export type FindAllArg = Select 813 | | From 814 | | Option 815 | | Where 816 | | FindArgOrder 817 | | Join; 818 | export type FindAllArgReturn = ReturnType>; 819 | 820 | export function findAll ( 821 | ...args: Array> 822 | ): (ctx?: Context) => Promise> { 823 | const populateOptions = populateQueryOptions> & {_from: M}>(args); 824 | return async function _findAllInner (ctx?: Context): Promise> { 825 | try { 826 | const options = populateOptions(ctx); 827 | console.log(JSON.stringify({options})); 828 | const model: ModelStatic = options._from as unknown as ModelStatic; 829 | return right(await model.findAll(options)); 830 | } catch (e: unknown) { 831 | if (e instanceof Error) { 832 | return left(e); 833 | } else { 834 | return left(new Error('Unknown error')); 835 | } 836 | } 837 | }; 838 | } 839 | 840 | export function findAndCountAll ( 841 | ...args: Array> 842 | ): (ctx?: Context) => Promise> { 843 | const populateOptions = populateQueryOptions> & {_from: M}>(args); 844 | return async function _findAndCountAllInner (ctx?: Context): 845 | Promise> { 846 | try { 847 | const options = populateOptions(ctx); 848 | const model: ModelStatic = options._from as unknown as ModelStatic; 849 | return right(await model.findAndCountAll(options)); 850 | } catch (e: unknown) { 851 | if (e instanceof Error) { 852 | return left(e); 853 | } else { 854 | return left(new Error('Unknown error')); 855 | } 856 | } 857 | }; 858 | } 859 | 860 | export function findOne ( 861 | ...args: Array> 862 | ): (ctx?: Context) => Promise> { 863 | const populateOptions = populateQueryOptions> & {_from: M}>(args); 864 | return async function _findOneInner (ctx?: Context): Promise> { 865 | try { 866 | const options = populateOptions(ctx); 867 | const model: ModelStatic = options._from as unknown as ModelStatic; 868 | const result = await model.findOne(options); 869 | if (result != null) { 870 | return right(result); 871 | } else { 872 | return left(new Error('Row not found!')); 873 | } 874 | } catch (e: unknown) { 875 | if (e instanceof Error) { 876 | return left(e); 877 | } else { 878 | return left(new Error('Unknown error')); 879 | } 880 | } 881 | }; 882 | } 883 | 884 | export function aggregate (functionName: string): 885 | (attribute: keyof Attributes, ...args: Array>) 886 | => (ctx?: Context) => Promise> { 887 | return function _aggregate (attribute: keyof Attributes, 888 | ...args: Array>): (ctx?: Context) 889 | => Promise> { 890 | const populateOptions = populateQueryOptions> & {_from: M}>(args); 891 | return async function _aggregateInner (ctx?: Context): Promise> { 892 | try { 893 | const options = populateOptions(ctx); 894 | const model: ModelStatic = options._from as unknown as ModelStatic; 895 | return right(await model.aggregate(attribute, functionName, options)); 896 | } catch (e: unknown) { 897 | if (e instanceof Error) { 898 | return left(e); 899 | } else { 900 | return left(new Error('Unknown error')); 901 | } 902 | } 903 | }; 904 | }; 905 | } 906 | 907 | export const aggSum: (attribute: 908 | keyof Attributes, ...args: Array>) 909 | => (ctx?: Context) => Promise> = aggregate('sum'); 910 | 911 | export const aggMax: (attribute: 912 | keyof Attributes, ...args: Array>) 913 | => (ctx?: Context) => Promise> = aggregate('max'); 914 | 915 | export const aggMin: (attribute: 916 | keyof Attributes, ...args: Array>) 917 | => (ctx?: Context) => Promise> = aggregate('min'); 918 | 919 | export type CountRowsArg = Select 920 | | From 921 | | CountOption 922 | | Where 923 | | FindArgOrder; 924 | 925 | export function countRows ( 926 | ...args: Array> 927 | ): (ctx?: Context) => Promise> { 928 | const populateOptions = populateQueryOptions> & {_from: M}>(args); 929 | return async function _countInner (ctx?: Context): Promise> { 930 | try { 931 | const options = populateOptions(ctx); 932 | const model: ModelStatic = options._from as unknown as ModelStatic; 933 | return right(await model.count(options)); 934 | } catch (e: unknown) { 935 | if (e instanceof Error) { 936 | return left(e); 937 | } else { 938 | return left(new Error('Unknown error')); 939 | } 940 | } 941 | }; 942 | } 943 | 944 | // export function findOrCreate ( 945 | // ...args: Array> 946 | // ): (ctx?: Context) => Promise> { 947 | // const getModel = getValueFromArgs, M>('_from', args); 948 | // const populateOptions = populateQueryOptions, M>(args); 949 | // return async function _countInner (ctx?: Context): Promise> { 950 | // try { 951 | // const model = getModel(ctx); 952 | // const options = populateOptions(ctx); 953 | // return right(await model.findOrCreate(options)); 954 | // } catch (e: unknown) { 955 | // if (e instanceof Error) { 956 | // return left(e); 957 | // } else { 958 | // return left(new Error('Unknown error')); 959 | // } 960 | // } 961 | // }; 962 | // } 963 | 964 | // doc gen for params 965 | 966 | // findCreateFind 967 | // findOrBuild 968 | // findOrCreate 969 | // update 970 | // upsert 971 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import {FindOptions, Transactionable, Attributes, Model, LOCK, IncludeOptions, ModelStatic} from 'sequelize'; 3 | import {type Context, type FindAllArg, type FindAllArgReturn} from './functions'; 4 | /* eslint-enable */ 5 | 6 | /** 7 | * Used to distinguish between classes and functions. 8 | * @param f input 9 | * @returns boolean 10 | */ 11 | export function isCallable (f: any): boolean { 12 | if (typeof f !== 'function') { 13 | return false; 14 | } else if (f.prototype === undefined) { // arrow functinos and class methods doesn't have this property 15 | return true; 16 | } else if (Object.getPrototypeOf(f.prototype).constructor === Model) { 17 | return false; 18 | } else { 19 | return true; 20 | } 21 | } 22 | 23 | export function populateQueryOptions ( 24 | args: Array>, 25 | ): (ctx?: Context) => R { 26 | return function (ctx?: Context): R { 27 | const options: FindOptions = {}; 28 | // todo: add about transaction in docs 29 | if (ctx != null) { 30 | options.transaction = ctx.transaction; 31 | } 32 | const values = args.map((arg: FindAllArg) => { 33 | if (isCallable(arg) && typeof arg === 'function' && arg instanceof Function) { 34 | return arg(ctx); 35 | } else { 36 | return arg; 37 | } 38 | }); 39 | const includes: IncludeOptions[] = []; 40 | values.filter((val) => '_join' in val).forEach((val) => { 41 | if ('_join' in val && Array.isArray(val._join)) { 42 | val._join.forEach((join) => { 43 | includes.push(join); 44 | }); 45 | } 46 | }); 47 | const result: any = values.reduce((acc: object, val: any) => { 48 | if (typeof val === 'object' && val != null) { 49 | return merge(acc, val); 50 | } 51 | return acc; 52 | }, {}); 53 | 54 | if (includes.length > 0) { 55 | result.include = includes; 56 | } 57 | return result; 58 | }; 59 | } 60 | 61 | function merge (acc: object, obj: object): object { 62 | return {...acc, ...obj}; 63 | } 64 | -------------------------------------------------------------------------------- /test/joins.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import {right} from 'fp-ts/lib/Either'; 3 | import { 4 | type InferAttributes, 5 | type InferCreationAttributes, 6 | Model, 7 | Sequelize, 8 | DataTypes, 9 | CreationOptional, 10 | Association, 11 | ForeignKey, 12 | NonAttribute, 13 | } from 'sequelize'; 14 | import { 15 | type Context, 16 | findAll, 17 | type FindAllArg, 18 | from, 19 | raw, 20 | select, 21 | as, 22 | max, 23 | logging, 24 | min, 25 | sum, 26 | count, 27 | distinct, 28 | fn, 29 | limit, 30 | offset, 31 | where, 32 | isNull, 33 | isFalse, 34 | isTrue, 35 | eq, 36 | lt, 37 | lte, 38 | gt, 39 | gte, 40 | ne, 41 | between, 42 | notBetween, 43 | isIn, 44 | notIn, 45 | like, 46 | notLike, 47 | iLike, 48 | notILike, 49 | startsWith, 50 | endsWith, 51 | substring, 52 | regexp, 53 | notRegexp, 54 | iRegexp, 55 | notIRegexp, 56 | overlap, 57 | contains, 58 | contained, 59 | and, 60 | or, 61 | not, 62 | asc, 63 | desc, 64 | model, 65 | nest, 66 | joinAlias, 67 | join, 68 | innerJoin, 69 | rightJoin, 70 | order 71 | } from '../src/lib/functions'; 72 | /* eslint-enable */ 73 | 74 | const db: Sequelize = new Sequelize('sqlite::memory:', {logging: false}); 75 | 76 | class User extends Model, InferCreationAttributes> { 77 | declare id: number; 78 | declare name: string; 79 | declare email: string | null; 80 | declare flag: boolean | null; 81 | // declare createdAt: CreationOptional; 82 | // declare updatedAt: CreationOptional; 83 | declare posts?: NonAttribute; 84 | declare comments?: NonAttribute; 85 | declare static associations: { 86 | comments: Association 87 | posts: Association 88 | }; 89 | } 90 | 91 | class Post extends Model, InferCreationAttributes> { 92 | declare id: number; 93 | declare title: string; 94 | declare content: string; 95 | declare userId: ForeignKey; 96 | 97 | declare static associations: { 98 | user: Association 99 | images: Association 100 | }; 101 | } 102 | 103 | class Comment extends Model, InferCreationAttributes> { 104 | declare id: number; 105 | declare comment: string; 106 | // declare userId: ForeignKey; 107 | // declare postId: ForeignKey; 108 | } 109 | 110 | class Image extends Model, InferCreationAttributes> { 111 | declare id: number; 112 | declare path: string; 113 | // declare postId: ForeignKey; 114 | } 115 | 116 | // const now = new Date(); 117 | const records = [ 118 | { 119 | id: 1, 120 | name: 'janedoe', 121 | email: 'janedoe@', 122 | flag: true, 123 | // createdAt: now, 124 | // updatedAt: now, 125 | }, 126 | { 127 | id: 2, 128 | name: 'janedoe2', 129 | email: 'janedoe2@', 130 | flag: false, 131 | // createdAt: now, 132 | // updatedAt: now, 133 | }, 134 | { 135 | id: 3, 136 | name: 'janedoe3', 137 | email: 'janedoe3@', 138 | flag: false, 139 | // createdAt: now, 140 | // updatedAt: now, 141 | }, 142 | { 143 | id: 4, 144 | name: 'janedoe', 145 | email: 'janedoe4@', 146 | flag: false, 147 | // createdAt: now, 148 | // updatedAt: now, 149 | }, 150 | { 151 | id: 5, 152 | name: 'janedoe5', 153 | email: null, 154 | flag: false, 155 | // createdAt: now, 156 | // updatedAt: now, 157 | }, 158 | ]; 159 | 160 | User.init( 161 | { 162 | id: { 163 | type: DataTypes.INTEGER, 164 | primaryKey: true, 165 | }, 166 | name: { 167 | type: new DataTypes.STRING(128), 168 | allowNull: false, 169 | }, 170 | email: { 171 | type: new DataTypes.STRING(128), 172 | allowNull: true, 173 | }, 174 | flag: { 175 | type: new DataTypes.BOOLEAN(), 176 | allowNull: true, 177 | }, 178 | // createdAt: DataTypes.DATE, 179 | // updatedAt: DataTypes.DATE, 180 | }, 181 | { 182 | tableName: 'users', 183 | sequelize: db, 184 | timestamps: false, 185 | }, 186 | ); 187 | 188 | Post.init( 189 | { 190 | id: { 191 | type: DataTypes.INTEGER, 192 | primaryKey: true, 193 | }, 194 | title: { 195 | type: new DataTypes.STRING(128), 196 | allowNull: false, 197 | }, 198 | content: { 199 | type: new DataTypes.STRING(128), 200 | allowNull: false, 201 | }, 202 | // userId: { 203 | // type: DataTypes.INTEGER, 204 | // allowNull: false, 205 | // }, 206 | }, 207 | { 208 | tableName: 'posts', 209 | sequelize: db, 210 | timestamps: false, 211 | }, 212 | ); 213 | Comment.init( 214 | { 215 | id: { 216 | type: DataTypes.INTEGER, 217 | primaryKey: true, 218 | }, 219 | comment: { 220 | type: new DataTypes.STRING(128), 221 | allowNull: false, 222 | }, 223 | // userId: { 224 | // type: DataTypes.INTEGER, 225 | // allowNull: false, 226 | // }, 227 | // postId: { 228 | // type: DataTypes.INTEGER, 229 | // allowNull: false, 230 | // }, 231 | }, 232 | { 233 | tableName: 'comments', 234 | sequelize: db, 235 | timestamps: false, 236 | }, 237 | ); 238 | Image.init( 239 | { 240 | id: { 241 | type: DataTypes.INTEGER, 242 | primaryKey: true, 243 | }, 244 | path: { 245 | type: new DataTypes.STRING(128), 246 | allowNull: false, 247 | }, 248 | // postId: { 249 | // type: DataTypes.INTEGER, 250 | // allowNull: false, 251 | // }, 252 | }, 253 | { 254 | tableName: 'images', 255 | sequelize: db, 256 | timestamps: false, 257 | }, 258 | ); 259 | 260 | User.hasMany(Post, { 261 | sourceKey: 'id', 262 | foreignKey: 'userId', 263 | as: 'posts', 264 | }); 265 | 266 | User.hasMany(Comment, { 267 | sourceKey: 'id', 268 | foreignKey: 'userId', 269 | as: 'comments', 270 | }); 271 | 272 | Post.hasMany(Image, { 273 | sourceKey: 'id', 274 | foreignKey: 'postId', 275 | as: 'images', 276 | }); 277 | 278 | // Post.belongsTo(User, {targetKey: 'id'}); 279 | // Comment.belongsTo(User, {targetKey: 'id'}); 280 | // Image.belongsTo(Post, {targetKey: 'id'}); 281 | // User.hasOne(Address, { sourceKey: 'id' }); 282 | 283 | const posts = [ 284 | { 285 | id: 1, 286 | title: 'post 1', 287 | content: 'post 1 content', 288 | userId: 1, 289 | }, 290 | { 291 | id: 2, 292 | title: 'post 2', 293 | content: 'post 2 content', 294 | userId: 1, 295 | }, 296 | { 297 | id: 3, 298 | title: 'post 3', 299 | content: 'post 3 content', 300 | userId: 2, 301 | }, 302 | ]; 303 | 304 | const comments = [ 305 | { 306 | id: 1, 307 | comment: 'comment 1 post 3 user 1', 308 | postId: 3, 309 | userId: 1, 310 | }, 311 | { 312 | id: 2, 313 | comment: 'comment 2 post 1 user 1', 314 | postId: 1, 315 | userId: 1, 316 | }, 317 | { 318 | id: 3, 319 | comment: 'comment 3 post 2 user 2', 320 | postId: 2, 321 | userId: 2, 322 | }, 323 | ]; 324 | 325 | const images = [ 326 | { 327 | id: 1, 328 | path: 'image 1 pah', 329 | postId: 3, 330 | }, 331 | { 332 | id: 2, 333 | path: 'image 2 path', 334 | postId: 3, 335 | }, 336 | { 337 | id: 3, 338 | path: 'image 3 path', 339 | postId: 1, 340 | }, 341 | ]; 342 | 343 | const ctx: Context = {}; 344 | 345 | describe('testing joins', () => { 346 | beforeAll(async () => { 347 | try { 348 | await db.sync(); 349 | await User.bulkCreate(records); 350 | await Post.bulkCreate(posts); 351 | await Comment.bulkCreate(comments); 352 | await Image.bulkCreate(images); 353 | } catch (e) { 354 | console.log(e); 355 | } 356 | }); 357 | 358 | afterAll(async () => { 359 | await db.close(); 360 | }); 361 | 362 | test('should return the user left outer joined with posts', async () => { 363 | const res = await findAll( 364 | from(User), raw(true), nest(true), 365 | join(model(Post), joinAlias('posts')), 366 | asc('id'), 367 | // logging(true), 368 | )(ctx); 369 | 370 | expect(res).toEqual(right([ 371 | { 372 | email: 'janedoe@', 373 | flag: 1, 374 | id: 1, 375 | name: 'janedoe', 376 | posts: { 377 | content: 'post 1 content', 378 | id: 1, 379 | title: 'post 1', 380 | userId: 1, 381 | }, 382 | }, 383 | { 384 | email: 'janedoe@', 385 | flag: 1, 386 | id: 1, 387 | name: 'janedoe', 388 | posts: { 389 | content: 'post 2 content', 390 | id: 2, 391 | title: 'post 2', 392 | userId: 1, 393 | }, 394 | }, 395 | { 396 | email: 'janedoe2@', 397 | flag: 0, 398 | id: 2, 399 | name: 'janedoe2', 400 | posts: { 401 | content: 'post 3 content', 402 | id: 3, 403 | title: 'post 3', 404 | userId: 2, 405 | }, 406 | }, 407 | { 408 | email: 'janedoe3@', 409 | flag: 0, 410 | id: 3, 411 | name: 'janedoe3', 412 | posts: { 413 | content: null, 414 | id: null, 415 | title: null, 416 | userId: null, 417 | }, 418 | }, 419 | { 420 | email: 'janedoe4@', 421 | flag: 0, 422 | id: 4, 423 | name: 'janedoe', 424 | posts: { 425 | content: null, 426 | id: null, 427 | title: null, 428 | userId: null, 429 | }, 430 | }, 431 | { 432 | email: null, 433 | flag: 0, 434 | id: 5, 435 | name: 'janedoe5', 436 | posts: { 437 | content: null, 438 | id: null, 439 | title: null, 440 | userId: null, 441 | }, 442 | }, 443 | ])); 444 | }); 445 | 446 | test('should return the user inner joined with posts', async () => { 447 | const res = await findAll( 448 | from(User), 449 | raw(true), 450 | nest(true), 451 | innerJoin(model(Post), joinAlias('posts')), 452 | asc('id'), 453 | // logging(true), 454 | )(ctx); 455 | 456 | expect(res).toEqual(right([ 457 | { 458 | email: 'janedoe@', 459 | flag: 1, 460 | id: 1, 461 | name: 'janedoe', 462 | posts: { 463 | content: 'post 1 content', 464 | id: 1, 465 | title: 'post 1', 466 | userId: 1, 467 | }, 468 | }, 469 | { 470 | email: 'janedoe@', 471 | flag: 1, 472 | id: 1, 473 | name: 'janedoe', 474 | posts: { 475 | content: 'post 2 content', 476 | id: 2, 477 | title: 'post 2', 478 | userId: 1, 479 | }, 480 | }, 481 | { 482 | email: 'janedoe2@', 483 | flag: 0, 484 | id: 2, 485 | name: 'janedoe2', 486 | posts: { 487 | content: 'post 3 content', 488 | id: 3, 489 | title: 'post 3', 490 | userId: 2, 491 | }, 492 | }, 493 | ])); 494 | }); 495 | 496 | test('should return user inner joined with posts inner joined with their images', async () => { 497 | const res = await findAll( 498 | from(User), nest(true), 499 | innerJoin( 500 | model(Post), 501 | joinAlias('posts'), 502 | innerJoin( 503 | model(Image), 504 | joinAlias('images'), 505 | ), 506 | ), 507 | asc('id'), 508 | // logging(true), 509 | )(ctx); 510 | expect(res._tag).toEqual('Right'); 511 | if (res._tag === 'Right') { 512 | expect(res.right.map((user) => { 513 | return user.get({plain: true}); 514 | })).toEqual([ 515 | { 516 | email: 'janedoe@', 517 | flag: true, 518 | id: 1, 519 | name: 'janedoe', 520 | posts: [ 521 | { 522 | content: 'post 1 content', 523 | id: 1, 524 | images: [ 525 | { 526 | id: 3, 527 | path: 'image 3 path', 528 | postId: 1, 529 | }, 530 | ], 531 | title: 'post 1', 532 | userId: 1, 533 | }, 534 | ], 535 | }, 536 | { 537 | email: 'janedoe2@', 538 | flag: false, 539 | id: 2, 540 | name: 'janedoe2', 541 | posts: [ 542 | { 543 | content: 'post 3 content', 544 | id: 3, 545 | images: [ 546 | { 547 | id: 1, 548 | path: 'image 1 pah', 549 | postId: 3, 550 | }, 551 | { 552 | id: 2, 553 | path: 'image 2 path', 554 | postId: 3, 555 | }, 556 | ], 557 | title: 'post 3', 558 | userId: 2, 559 | }, 560 | ], 561 | }, 562 | ], 563 | ); 564 | } 565 | }); 566 | 567 | test('should return users with posts with their path of images', async () => { 568 | const res = await findAll( 569 | from(User), nest(true), 570 | innerJoin( 571 | model(Post), 572 | joinAlias('posts'), 573 | innerJoin( 574 | model(Image), 575 | joinAlias('images'), 576 | select('path'), 577 | ), 578 | ), 579 | asc('id'), 580 | // logging(true), 581 | )(ctx); 582 | expect(res._tag).toEqual('Right'); 583 | if (res._tag === 'Right') { 584 | expect(res.right.map((user) => { 585 | return user.get({plain: true}); 586 | })).toEqual([ 587 | { 588 | email: 'janedoe@', 589 | flag: true, 590 | id: 1, 591 | name: 'janedoe', 592 | posts: [ 593 | { 594 | content: 'post 1 content', 595 | id: 1, 596 | images: [ 597 | { 598 | path: 'image 3 path', 599 | }, 600 | ], 601 | title: 'post 1', 602 | userId: 1, 603 | }, 604 | ], 605 | }, 606 | { 607 | email: 'janedoe2@', 608 | flag: false, 609 | id: 2, 610 | name: 'janedoe2', 611 | posts: [ 612 | { 613 | content: 'post 3 content', 614 | id: 3, 615 | images: [ 616 | { 617 | path: 'image 1 pah', 618 | }, 619 | { 620 | path: 'image 2 path', 621 | }, 622 | ], 623 | title: 'post 3', 624 | userId: 2, 625 | }, 626 | ], 627 | }, 628 | ], 629 | ); 630 | } 631 | }); 632 | 633 | // test('should return the user right joined with posts', async () => { 634 | // const res = await findAll( 635 | // from(User), 636 | // raw(true), 637 | // nest(true), 638 | // rightJoin(model(Post), joinAlias('posts')), 639 | // asc('id'), 640 | // logging(true), 641 | // )(ctx); 642 | 643 | // expect(res).toEqual(right(); 644 | // }); 645 | }); 646 | -------------------------------------------------------------------------------- /test/queries.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import {right} from 'fp-ts/lib/Either'; 3 | import { 4 | type InferAttributes, 5 | type InferCreationAttributes, 6 | Model, 7 | Sequelize, 8 | DataTypes, 9 | CreationOptional, 10 | } from 'sequelize'; 11 | import { 12 | type Context, 13 | findAll, 14 | type FindAllArg, 15 | from, 16 | raw, 17 | select, 18 | as, 19 | max, 20 | logging, 21 | min, 22 | sum, 23 | count, 24 | distinct, 25 | fn, 26 | limit, 27 | offset, 28 | where, 29 | isNull, 30 | isFalse, 31 | isTrue, 32 | eq, 33 | lt, 34 | lte, 35 | gt, 36 | gte, 37 | ne, 38 | between, 39 | notBetween, 40 | isIn, 41 | notIn, 42 | like, 43 | notLike, 44 | iLike, 45 | notILike, 46 | startsWith, 47 | endsWith, 48 | substring, 49 | regexp, 50 | notRegexp, 51 | iRegexp, 52 | notIRegexp, 53 | overlap, 54 | contains, 55 | contained, 56 | and, 57 | or, 58 | not, 59 | asc, 60 | desc 61 | } from '../src/lib/functions'; 62 | /* eslint-enable */ 63 | 64 | const db: Sequelize = new Sequelize('sqlite::memory:', {logging: false}); 65 | 66 | class User extends Model, InferCreationAttributes> { 67 | declare id: number; 68 | declare name: string; 69 | declare email: string | null; 70 | declare flag: boolean | null; 71 | // declare createdAt: CreationOptional; 72 | // declare updatedAt: CreationOptional; 73 | } 74 | 75 | // const now = new Date(); 76 | const records = [ 77 | { 78 | id: 1, 79 | name: 'janedoe', 80 | email: 'janedoe@', 81 | flag: true, 82 | // createdAt: now, 83 | // updatedAt: now, 84 | }, 85 | { 86 | id: 2, 87 | name: 'janedoe2', 88 | email: 'janedoe2@', 89 | flag: false, 90 | // createdAt: now, 91 | // updatedAt: now, 92 | }, 93 | { 94 | id: 3, 95 | name: 'janedoe3', 96 | email: 'janedoe3@', 97 | flag: false, 98 | // createdAt: now, 99 | // updatedAt: now, 100 | }, 101 | { 102 | id: 4, 103 | name: 'janedoe', 104 | email: 'janedoe4@', 105 | flag: false, 106 | // createdAt: now, 107 | // updatedAt: now, 108 | }, 109 | { 110 | id: 5, 111 | name: 'janedoe5', 112 | email: null, 113 | flag: false, 114 | // createdAt: now, 115 | // updatedAt: now, 116 | }, 117 | ]; 118 | const recordsWithoutDate = records.map((a) => { 119 | return { 120 | name: a.name, 121 | id: a.id, 122 | email: a.email, 123 | }; 124 | }); 125 | 126 | User.init( 127 | { 128 | id: { 129 | type: DataTypes.INTEGER, 130 | primaryKey: true, 131 | }, 132 | name: { 133 | type: new DataTypes.STRING(128), 134 | allowNull: false, 135 | }, 136 | email: { 137 | type: new DataTypes.STRING(128), 138 | allowNull: true, 139 | }, 140 | flag: { 141 | type: new DataTypes.BOOLEAN(), 142 | allowNull: true, 143 | }, 144 | // createdAt: DataTypes.DATE, 145 | // updatedAt: DataTypes.DATE, 146 | }, 147 | { 148 | tableName: 'users', 149 | sequelize: db, 150 | timestamps: false, 151 | }, 152 | ); 153 | const args: Array> = [from(User), raw(true)]; 154 | 155 | const ctx: Context = {}; 156 | 157 | describe('testing findAll', () => { 158 | beforeAll(async () => { 159 | try { 160 | await db.sync(); 161 | await User.bulkCreate(records); 162 | } catch (e) { 163 | console.log(e); 164 | } 165 | }); 166 | 167 | afterAll(async () => { 168 | await db.close(); 169 | }); 170 | 171 | test('should return all of the created records', async () => { 172 | expect(await findAll( 173 | from(User), 174 | raw(true), 175 | select('email', 'id', 'name'))(ctx)).toEqual( 176 | right(recordsWithoutDate), 177 | ); 178 | }); 179 | 180 | test('should return only selected columns', async () => { 181 | expect( 182 | await findAll( 183 | ...args, 184 | select('name', () => 'id'), 185 | )(ctx), 186 | ).toEqual( 187 | right( 188 | recordsWithoutDate.map((a) => { 189 | return { 190 | name: a.name, 191 | id: a.id, 192 | }; 193 | }), 194 | ), 195 | ); 196 | }); 197 | 198 | test('should return rows with renamed columns', async () => { 199 | expect( 200 | await findAll( 201 | ...args, 202 | select('name', () => 'id', 'email', as('email', 'email2')), 203 | )(ctx), 204 | ).toEqual( 205 | right( 206 | recordsWithoutDate.map((a) => { 207 | return { 208 | name: a.name, 209 | id: a.id, 210 | email: a.email, 211 | email2: a.email, 212 | }; 213 | }), 214 | ), 215 | ); 216 | }); 217 | 218 | test('should return the row with the max id with an alias', async () => { 219 | expect( 220 | await findAll( 221 | ...args, 222 | select('name', () => 'id', as(max('id'), 'newid')), 223 | )(ctx), 224 | ).toEqual( 225 | right([ 226 | { 227 | id: 5, 228 | name: 'janedoe5', 229 | newid: 5, 230 | }, 231 | ]), 232 | ); 233 | }); 234 | 235 | test('should return the row with the min id with an alias', async () => { 236 | expect( 237 | await findAll( 238 | ...args, 239 | select('name', () => 'id', as(min('id'), 'newid')), 240 | )(ctx), 241 | ).toEqual( 242 | right([ 243 | { 244 | id: 1, 245 | name: 'janedoe', 246 | newid: 1, 247 | }, 248 | ]), 249 | ); 250 | }); 251 | 252 | test('should return sum of ids', async () => { 253 | expect(await findAll(...args, select('name', as(sum('id'), 'sumid')))(ctx)).toEqual( 254 | right([ 255 | { 256 | sumid: 15, 257 | name: 'janedoe', 258 | }, 259 | ]), 260 | ); 261 | }); 262 | 263 | test('should return count of the rows ', async () => { 264 | expect(await findAll(...args, select(as(count('id'), 'total')))(ctx)).toEqual( 265 | right([ 266 | { 267 | total: 5, 268 | }, 269 | ]), 270 | ); 271 | }); 272 | 273 | test('should return rows with altered names', async () => { 274 | expect(await findAll(...args, 275 | select(as(fn('replace', 'name', 'j', 'm'), 'new_name')))(ctx)) 276 | .toEqual(right([ 277 | { 278 | new_name: 'manedoe', 279 | }, 280 | { 281 | new_name: 'manedoe2', 282 | }, 283 | { 284 | new_name: 'manedoe3', 285 | }, 286 | { 287 | new_name: 'manedoe', 288 | }, 289 | { 290 | new_name: 'manedoe5', 291 | }, 292 | ])); 293 | }); 294 | 295 | test('should return rows with distinct names', async () => { 296 | expect(await findAll(...args, select(as(distinct('name'), 'dname')))(ctx)).toEqual( 297 | right([ 298 | { 299 | dname: 'janedoe', 300 | }, 301 | { 302 | dname: 'janedoe2', 303 | }, 304 | { 305 | dname: 'janedoe3', 306 | }, 307 | { 308 | dname: 'janedoe5', 309 | }, 310 | ]), 311 | ); 312 | }); 313 | 314 | test('should return two rows', async () => { 315 | expect(await findAll(...args, select('id'), limit(2))(ctx)).toEqual( 316 | right([ 317 | { 318 | id: 1, 319 | }, 320 | { 321 | id: 2, 322 | }, 323 | ]), 324 | ); 325 | }); 326 | 327 | test('should return the second row', async () => { 328 | expect(await findAll(...args, select('id'), limit(1), offset(1))(ctx)).toEqual( 329 | right([ 330 | { 331 | id: 2, 332 | }, 333 | ]), 334 | ); 335 | }); 336 | 337 | test('should return the row where flag is true', async () => { 338 | expect(await findAll(...args, select('id'), where(isTrue('flag')), limit(1))(ctx)).toEqual( 339 | right([ 340 | { 341 | id: 1, 342 | }, 343 | ]), 344 | ); 345 | }); 346 | 347 | test('should return the row where flag is false', async () => { 348 | expect(await findAll(...args, select('id'), where(isFalse('flag')), limit(1))(ctx)).toEqual( 349 | right([ 350 | { 351 | id: 2, 352 | }, 353 | ]), 354 | ); 355 | }); 356 | 357 | test('should return the row where email is null', async () => { 358 | expect(await findAll(...args, select('id'), where(isNull('email')), limit(1))(ctx)).toEqual( 359 | right([ 360 | { 361 | id: 5, 362 | }, 363 | ]), 364 | ); 365 | }); 366 | 367 | test('should return the row with id = 2', async () => { 368 | expect(await findAll(...args, select('id'), where(eq('id', 2)), limit(1))(ctx)).toEqual( 369 | right([ 370 | { 371 | id: 2, 372 | }, 373 | ]), 374 | ); 375 | }); 376 | 377 | test('should return the row where id lt 2', async () => { 378 | expect(await findAll(...args, select('id'), where(lt('id', 2)), limit(1))(ctx)).toEqual( 379 | right([ 380 | { 381 | id: 1, 382 | }, 383 | ]), 384 | ); 385 | }); 386 | 387 | test('should return the row where id lte 2', async () => { 388 | expect(await findAll(...args, select('id'), where(lte('id', 2)), limit(2))(ctx)).toEqual( 389 | right([ 390 | { 391 | id: 1, 392 | }, 393 | { 394 | id: 2, 395 | }, 396 | ]), 397 | ); 398 | }); 399 | 400 | test('should return the row where id gt 4', async () => { 401 | expect(await findAll(...args, select('id'), where(gt('id', 4)), limit(1))(ctx)).toEqual( 402 | right([ 403 | { 404 | id: 5, 405 | }, 406 | ]), 407 | ); 408 | }); 409 | 410 | test('should return the row where id gte 5', async () => { 411 | expect(await findAll(...args, select('id'), where(gte('id', 5)), limit(1))(ctx)).toEqual( 412 | right([ 413 | { 414 | id: 5, 415 | }, 416 | ]), 417 | ); 418 | }); 419 | 420 | test('should return the row where id not equal 1', async () => { 421 | expect(await findAll(...args, select('id'), where(ne('id', 1)), limit(1))(ctx)).toEqual( 422 | right([ 423 | { 424 | id: 2, 425 | }, 426 | ]), 427 | ); 428 | }); 429 | 430 | test('should return the row where id equal 3', async () => { 431 | expect(await findAll(...args, select('id'), where(eq('id', 3)), limit(1))(ctx)).toEqual( 432 | right([ 433 | { 434 | id: 3, 435 | }, 436 | ]), 437 | ); 438 | }); 439 | 440 | test('should return the row where id betwee 3 and 5', async () => { 441 | expect(await findAll(...args, select('id'), where(between('id', [3, 5])), limit(2))(ctx)).toEqual( 442 | right([ 443 | { 444 | id: 3, 445 | }, 446 | { 447 | id: 4, 448 | }, 449 | ]), 450 | ); 451 | }); 452 | 453 | test('should return the row where id not betwee 2 and 100', async () => { 454 | expect(await findAll(...args, select('id'), where(notBetween('id', [2, 100])), limit(2))(ctx)).toEqual( 455 | right([ 456 | { 457 | id: 1, 458 | }, 459 | ]), 460 | ); 461 | }); 462 | 463 | test('should return the row where id in [2, 3]', async () => { 464 | expect(await findAll(...args, select('id'), where(isIn('id', [2, 3])), limit(2))(ctx)).toEqual( 465 | right([ 466 | { 467 | id: 2, 468 | }, 469 | { 470 | id: 3, 471 | }, 472 | ]), 473 | ); 474 | }); 475 | 476 | test('should return the row where id not in [1,3]', async () => { 477 | expect(await findAll(...args, select('id'), where(notIn('id', [1, 3])), limit(1))(ctx)).toEqual( 478 | right([ 479 | { 480 | id: 2, 481 | }, 482 | ]), 483 | ); 484 | }); 485 | 486 | test('should return the row where name is like janedoe5', async () => { 487 | expect(await findAll(...args, select('id'), where(like('name', 'janedoe5')), limit(1))(ctx)).toEqual( 488 | right([ 489 | { 490 | id: 5, 491 | }, 492 | ]), 493 | ); 494 | }); 495 | 496 | // test('should return the row where name is ilike janedoe5', async () => { 497 | // expect(await findAll(...args, select('id'), where(iLike('name', 'JAnedoe5')), limit(1))(ctx)).toEqual( 498 | // right([ 499 | // { 500 | // id: 5, 501 | // } 502 | // ]), 503 | // ); 504 | // }); 505 | 506 | test('should return the row where name is notlike janedoe5', async () => { 507 | expect(await findAll(...args, select('id'), where(notLike('name', 'janedoe5')), limit(1))(ctx)).toEqual( 508 | right([ 509 | { 510 | id: 1, 511 | }, 512 | ]), 513 | ); 514 | }); 515 | 516 | // test('should return the row where name is notILike janedoe5', async () => { 517 | // expect(await findAll(...args, select('id'), where(notILike('name', 'Janedoe5')), limit(1))(ctx)).toEqual( 518 | // right([ 519 | // { 520 | // id: 1, 521 | // } 522 | // ]), 523 | // ); 524 | // }); 525 | 526 | // startsWith, 527 | // endsWith, 528 | // substring, 529 | // regexp, 530 | // notRegexp, 531 | // iRegexp, 532 | // notIRegexp, 533 | // overlap, 534 | // contains, 535 | // contained, 536 | 537 | test('should return the row where id equal 3 and name = janedoe3', async () => { 538 | expect( 539 | await findAll(...args, select('id'), where(and(eq('id', 3), eq('name', 'janedoe3'))), limit(1))(ctx), 540 | ).toEqual( 541 | right([ 542 | { 543 | id: 3, 544 | }, 545 | ]), 546 | ); 547 | }); 548 | 549 | test('should return the row where id equal 3 or name = janedoe2', async () => { 550 | expect( 551 | await findAll( 552 | ...args, 553 | select('id'), 554 | where( 555 | or( 556 | eq('id', () => 3), 557 | eq('name', 'janedoe2'), 558 | ), 559 | ), 560 | limit(2), 561 | )(ctx), 562 | ).toEqual( 563 | right([ 564 | { 565 | id: 2, 566 | }, 567 | { 568 | id: 3, 569 | }, 570 | ]), 571 | ); 572 | }); 573 | 574 | test('should return the row where flas is true and (id equal 3 or name = janedoe2)', async () => { 575 | expect( 576 | await findAll( 577 | ...args, 578 | select('id'), 579 | where( 580 | and( 581 | isTrue('flag'), 582 | or( 583 | eq('id', () => 3), 584 | eq('name', 'janedoe2'), 585 | ), 586 | ), 587 | ), 588 | limit(2), 589 | )(ctx), 590 | ).toEqual(right([])); 591 | }); 592 | 593 | test('should return the row where flas is true and (id equal 3 or name = janedoe2)', async () => { 594 | expect( 595 | await findAll( 596 | ...args, 597 | select('id'), 598 | where( 599 | and( 600 | not('flag', () => true), 601 | or( 602 | eq('id', () => 3), 603 | eq('name', 'janedoe2'), 604 | ), 605 | ), 606 | ), 607 | limit(2), 608 | )(ctx), 609 | ).toEqual( 610 | right([ 611 | { 612 | id: 2, 613 | }, 614 | { 615 | id: 3, 616 | }, 617 | ]), 618 | ); 619 | }); 620 | 621 | test('should return the row where flas is true and (id equal 3 or name = janedoe2) order by id desc', async () => { 622 | expect( 623 | await findAll( 624 | ...args, 625 | select('id'), 626 | where( 627 | and( 628 | not('flag', () => true), 629 | or( 630 | isIn('id', () => [3, 4]), 631 | eq('name', 'janedoe2'), 632 | ), 633 | ), 634 | ), 635 | limit(3), 636 | desc('id'), 637 | // logging(true), 638 | )(ctx), 639 | ).toEqual( 640 | right([ 641 | { 642 | id: 4, 643 | }, 644 | { 645 | id: 3, 646 | }, 647 | { 648 | id: 2, 649 | }, 650 | ]), 651 | ); 652 | }); 653 | 654 | test('should return the firs three rows asc by id', async () => { 655 | expect( 656 | await findAll( 657 | ...args, 658 | select('id'), 659 | limit(3), 660 | asc('id'), 661 | )(ctx), 662 | ).toEqual( 663 | right([ 664 | { 665 | id: 1, 666 | }, 667 | { 668 | id: 2, 669 | }, 670 | { 671 | id: 3, 672 | }, 673 | ]), 674 | ); 675 | }); 676 | }); 677 | -------------------------------------------------------------------------------- /test/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type InferAttributes, 3 | type InferCreationAttributes, 4 | Model, 5 | type FindOptions, 6 | type Attributes, 7 | } from 'sequelize'; 8 | import {populateQueryOptions} from '../src/lib/utils'; 9 | import {benchmark, type FindAllArg, from, raw, select, type Context} from '../src/lib/functions'; 10 | 11 | class User extends Model, InferCreationAttributes> { 12 | declare id: number; 13 | declare name: string; 14 | declare email: string; 15 | } 16 | 17 | describe('testing populateQueryOptions', () => { 18 | const args: Array> = [ 19 | from(User), 20 | select('name', () => 'id'), 21 | raw(true), 22 | benchmark(true), 23 | ]; 24 | const ctx: Context = {}; 25 | 26 | test('should return the correct option object', () => { 27 | expect(populateQueryOptions> & {_from: User}>(args)(ctx)) 28 | .toStrictEqual({attributes: ['name', 'id'], raw: true, benchmark: true, _from: User}); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node16-strictest-esm/tsconfig.json", 3 | "compilerOptions": { 4 | "preserveConstEnums": true, 5 | "module": "es2022", 6 | "target": "es2021", 7 | "outDir": "dist", 8 | "allowUnusedLabels": true, 9 | "noUnusedLocals": false, 10 | "noUnusedParameters": false, 11 | "importsNotUsedAsValues": "remove", 12 | "exactOptionalPropertyTypes": false, 13 | "declaration": true, 14 | }, 15 | "include": ["src/**/*", "test/**/*"], 16 | "exclude": ["node_modules"], 17 | } 18 | --------------------------------------------------------------------------------