├── test ├── mocha.opts ├── index.ts └── src │ ├── decorators │ ├── index.ts │ └── View.ts │ ├── index.ts │ └── Graph.ts ├── .gitignore ├── tsconfig.json ├── .npmignore ├── .github └── workflows │ └── build.yml ├── index.ts ├── src ├── helpers │ ├── index.ts │ └── query.ts ├── types │ ├── DataPage.ts │ ├── ranges │ │ ├── IRange.ts │ │ ├── index.ts │ │ ├── NumericRange.ts │ │ └── DateRange.ts │ ├── JsonObject.ts │ ├── FieldsInput.ts │ ├── index.ts │ ├── PaginationInput.ts │ ├── OrderByInput.ts │ └── FilterInput.ts ├── decorators │ ├── index.ts │ ├── Emittable.ts │ ├── AssociatedWith.ts │ ├── View.ts │ ├── NullableIndex.ts │ ├── ColumnIndex.ts │ └── DynamicView.ts ├── index.ts ├── Graph.ts └── BaseModel.ts ├── README.md ├── eslint.config.mjs ├── package.json └── LICENSE /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register 2 | --require source-map-support/register 3 | --recursive 4 | --bail 5 | --full-trace 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .ssh/ 2 | dist/ 3 | tmp/ 4 | out-tsc/ 5 | build/ 6 | .nyc_output/ 7 | node_modules/ 8 | .idea/ 9 | .project 10 | .classpath 11 | .c9/ 12 | *.launch 13 | .settings/ 14 | *.sublime-workspace 15 | .vscode/* 16 | .sass-cache/ 17 | connect.lock/ 18 | coverage/ 19 | typings/ 20 | docs/ 21 | debug* 22 | *.txt 23 | *.js 24 | *.d.ts 25 | *.js.map 26 | *.pid 27 | *.log 28 | *.swp 29 | *.tgz 30 | .env 31 | .eslintrc 32 | .editorconfig 33 | .gitlab-ci.yml 34 | .DS_Store 35 | Thumbs.db 36 | TODO.* 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": true, 6 | "strictNullChecks": true, 7 | "removeComments": false, 8 | "noUnusedLocals": false, 9 | "noUnusedParameters": false, 10 | "moduleResolution": "node", 11 | "sourceMap": true, 12 | "inlineSources": true, 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true, 15 | "target": "es2017", 16 | "lib": [ 17 | "dom", 18 | "es2017" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .travis.yml 3 | .dockerignore 4 | .codebeatignore 5 | .codebeatsettings 6 | .github 7 | 8 | .ssh/ 9 | dist/ 10 | tmp/ 11 | out-tsc/ 12 | build/ 13 | .nyc_output/ 14 | 15 | node_modules/ 16 | 17 | .idea/ 18 | .project 19 | .classpath 20 | .c9/ 21 | *.launch 22 | .settings/ 23 | *.sublime-workspace 24 | .vscode/* 25 | .env 26 | 27 | .sass-cache/ 28 | connect.lock/ 29 | coverage/ 30 | typings/ 31 | docs/ 32 | 33 | debug* 34 | *.pid 35 | *.log 36 | .DS_Store 37 | Thumbs.db 38 | 39 | test/ 40 | 41 | *.js.map 42 | *.ts 43 | !*.d.ts 44 | tsconfig.json 45 | 46 | *.tgz 47 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | node-version: [lts/*] 14 | 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | 24 | - name: Install dependencies 25 | run: npm install 26 | 27 | - name: Run tests 28 | run: npm test 29 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * Copyright (c) 2019, imqueue.com 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 11 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 12 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 13 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 14 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 15 | * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 16 | * PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | export * from './src'; 19 | -------------------------------------------------------------------------------- /test/src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * Copyright (c) 2019, imqueue.com 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 11 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 12 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 13 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 14 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 15 | * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 16 | * PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | export * from './View'; 19 | -------------------------------------------------------------------------------- /test/src/index.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * Copyright (c) 2019, imqueue.com 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 11 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 12 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 13 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 14 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 15 | * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 16 | * PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | export * from './decorators'; 19 | export * from './Graph'; 20 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * I'm Queue Software Project 5 | * Copyright (C) 2025 imqueue.com 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | * 20 | * If you want to use this code in a closed source (commercial) project, you can 21 | * purchase a proprietary commercial license. Please contact us at 22 | * to get commercial licensing options. 23 | */ 24 | export * from './src'; 25 | -------------------------------------------------------------------------------- /src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * I'm Queue Software Project 5 | * Copyright (C) 2025 imqueue.com 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | * 20 | * If you want to use this code in a closed source (commercial) project, you can 21 | * purchase a proprietary commercial license. Please contact us at 22 | * to get commercial licensing options. 23 | */ 24 | export * from './query'; 25 | -------------------------------------------------------------------------------- /test/src/Graph.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * Copyright (c) 2019, imqueue.com 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 11 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 12 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 13 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 14 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 15 | * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 16 | * PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | import { expect } from 'chai'; 19 | import { Graph } from '../../src'; 20 | 21 | describe('Graph', () => { 22 | it('should be a function', () => { 23 | expect(typeof Graph).equals('function'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/src/decorators/View.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * Copyright (c) 2019, imqueue.com 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 11 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 12 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 13 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 14 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 15 | * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 16 | * PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | 19 | import { expect } from "chai"; 20 | import { View } from "../../.."; 21 | 22 | describe('View', () => { 23 | it('should be a function', () => { 24 | expect(typeof View).equals('function'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/types/DataPage.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * I'm Queue Software Project 5 | * Copyright (C) 2025 imqueue.com 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | * 20 | * If you want to use this code in a closed source (commercial) project, you can 21 | * purchase a proprietary commercial license. Please contact us at 22 | * to get commercial licensing options. 23 | */ 24 | export interface DataPage { 25 | total: number; 26 | data: T; 27 | } 28 | -------------------------------------------------------------------------------- /src/types/ranges/IRange.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * I'm Queue Software Project 5 | * Copyright (C) 2025 imqueue.com 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | * 20 | * If you want to use this code in a closed source (commercial) project, you can 21 | * purchase a proprietary commercial license. Please contact us at 22 | * to get commercial licensing options. 23 | */ 24 | export interface IRange { 25 | start: any; 26 | end: any; 27 | } 28 | -------------------------------------------------------------------------------- /src/types/ranges/index.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * I'm Queue Software Project 5 | * Copyright (C) 2025 imqueue.com 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | * 20 | * If you want to use this code in a closed source (commercial) project, you can 21 | * purchase a proprietary commercial license. Please contact us at 22 | * to get commercial licensing options. 23 | */ 24 | export * from './IRange'; 25 | export * from './DateRange'; 26 | export * from './NumericRange'; 27 | -------------------------------------------------------------------------------- /src/types/JsonObject.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * I'm Queue Software Project 5 | * Copyright (C) 2025 imqueue.com 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | * 20 | * If you want to use this code in a closed source (commercial) project, you can 21 | * purchase a proprietary commercial license. Please contact us at 22 | * to get commercial licensing options. 23 | */ 24 | import { indexed } from '@imqueue/rpc'; 25 | 26 | @indexed(() => `[property: string]: any`) 27 | export class JsonObject { 28 | [property: string]: any; 29 | } 30 | -------------------------------------------------------------------------------- /src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * I'm Queue Software Project 5 | * Copyright (C) 2025 imqueue.com 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | * 20 | * If you want to use this code in a closed source (commercial) project, you can 21 | * purchase a proprietary commercial license. Please contact us at 22 | * to get commercial licensing options. 23 | */ 24 | export * from './DynamicView'; 25 | export * from './View'; 26 | export * from './NullableIndex'; 27 | export * from './AssociatedWith'; 28 | export * from './ColumnIndex'; 29 | export * from './Emittable'; 30 | -------------------------------------------------------------------------------- /src/types/FieldsInput.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * I'm Queue Software Project 5 | * Copyright (C) 2025 imqueue.com 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | * 20 | * If you want to use this code in a closed source (commercial) project, you can 21 | * purchase a proprietary commercial license. Please contact us at 22 | * to get commercial licensing options. 23 | */ 24 | import { indexed } from '@imqueue/rpc'; 25 | 26 | @indexed(() => `[fieldName: string]: false | ${FieldsInput.name}`) 27 | export class FieldsInput { 28 | [fieldName: string]: false | FieldsInput; 29 | } 30 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * I'm Queue Software Project 5 | * Copyright (C) 2025 imqueue.com 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | * 20 | * If you want to use this code in a closed source (commercial) project, you can 21 | * purchase a proprietary commercial license. Please contact us at 22 | * to get commercial licensing options. 23 | */ 24 | export * from './ranges'; 25 | export * from './FieldsInput'; 26 | export * from './FilterInput'; 27 | export * from './PaginationInput'; 28 | export * from './OrderByInput'; 29 | export * from './JsonObject'; 30 | export * from './DataPage'; 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @imqueue/sequelize 2 | 3 | [![Build Status](https://img.shields.io/github/actions/workflow/status/imqueue/sequelize/build.yml)](https://github.com/imqueue/sequelize) 4 | [![Known Vulnerabilities](https://snyk.io/test/github/imqueue/sequelize/badge.svg?targetFile=package.json)](https://snyk.io/test/github/imqueue/sequelize?targetFile=package.json) 5 | [![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://rawgit.com/imqueue/sequelize/master/LICENSE) 6 | 7 | Sequelize ORM refines for @imqueue 8 | 9 | # Install 10 | 11 | ~~~bash 12 | npm i --save @imqueue/sequelize 13 | ~~~ 14 | 15 | # Docs 16 | 17 | ~~~bash 18 | git clone git@github.com:imqueue/sequelize.git 19 | cd sequelize 20 | npm i 21 | npm run doc 22 | ~~~ 23 | 24 | # Usage 25 | 26 | ~~~typescript 27 | import { database, query } from '@imqueue/sequelize'; 28 | 29 | const sequelize = database({ 30 | logger: console, 31 | modelsPath: './src/orm/models', 32 | sequelize: { 33 | benchmark: true, 34 | dialect: 'postgres', 35 | storage: 'sequelize', 36 | pool: { 37 | max: 250, 38 | min: 2, 39 | idle: 30000, 40 | acquire: 30000, 41 | }, 42 | }, 43 | }); 44 | 45 | 46 | ~~~ 47 | 48 | ## License 49 | 50 | This project is licensed under the GNU General Public License v3.0. 51 | See the [LICENSE](LICENSE) 52 | -------------------------------------------------------------------------------- /src/types/ranges/NumericRange.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * I'm Queue Software Project 5 | * Copyright (C) 2025 imqueue.com 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | * 20 | * If you want to use this code in a closed source (commercial) project, you can 21 | * purchase a proprietary commercial license. Please contact us at 22 | * to get commercial licensing options. 23 | */ 24 | import { property } from '@imqueue/rpc'; 25 | import { IRange } from './IRange'; 26 | 27 | export class NumericRange implements IRange { 28 | @property('number') 29 | public start: number; 30 | 31 | @property('number') 32 | public end: number; 33 | } 34 | -------------------------------------------------------------------------------- /src/types/PaginationInput.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * I'm Queue Software Project 5 | * Copyright (C) 2025 imqueue.com 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | * 20 | * If you want to use this code in a closed source (commercial) project, you can 21 | * purchase a proprietary commercial license. Please contact us at 22 | * to get commercial licensing options. 23 | */ 24 | import { property } from '@imqueue/rpc'; 25 | 26 | export class PaginationInput { 27 | @property('number') 28 | public offset: number; 29 | 30 | @property('number') 31 | public limit: number; 32 | 33 | @property('number', true) 34 | public count?: number; 35 | } 36 | -------------------------------------------------------------------------------- /src/types/ranges/DateRange.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * I'm Queue Software Project 5 | * Copyright (C) 2025 imqueue.com 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | * 20 | * If you want to use this code in a closed source (commercial) project, you can 21 | * purchase a proprietary commercial license. Please contact us at 22 | * to get commercial licensing options. 23 | */ 24 | import { property } from '@imqueue/rpc'; 25 | import { IRange } from './IRange'; 26 | 27 | export class DateRange implements IRange { 28 | @property('string | Date') 29 | public start: string | Date; 30 | 31 | @property('string | Date') 32 | public end: string | Date; 33 | } 34 | -------------------------------------------------------------------------------- /src/decorators/Emittable.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * I'm Queue Software Project 5 | * Copyright (C) 2025 imqueue.com 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | * 20 | * If you want to use this code in a closed source (commercial) project, you can 21 | * purchase a proprietary commercial license. Please contact us at 22 | * to get commercial licensing options. 23 | */ 24 | import 'reflect-metadata'; 25 | 26 | // noinspection JSUnusedGlobalSymbols 27 | /** 28 | * Declares emittable model, which would have bound trigger sending 29 | * table rows change events using PostgreSQL NOTIFY command. 30 | * This is PostgreSQL only feature 31 | */ 32 | export function Emittable(target: any) { 33 | // todo: implement 34 | } 35 | -------------------------------------------------------------------------------- /src/types/OrderByInput.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * I'm Queue Software Project 5 | * Copyright (C) 2025 imqueue.com 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | * 20 | * If you want to use this code in a closed source (commercial) project, you can 21 | * purchase a proprietary commercial license. Please contact us at 22 | * to get commercial licensing options. 23 | */ 24 | import { indexed } from '@imqueue/rpc'; 25 | 26 | /** 27 | * Allowed ordering directions 28 | */ 29 | export enum OrderDirection { 30 | asc = 'ASC', 31 | desc = 'DESC', 32 | } 33 | 34 | export const ENUM_ORDER_DIRECTION = `'${ 35 | OrderDirection.asc}' | '${ 36 | OrderDirection.desc}'`; 37 | 38 | @indexed(() => `[fieldName: string]: ${ENUM_ORDER_DIRECTION}`) 39 | export class OrderByInput { 40 | [fieldName: string]: OrderDirection; 41 | } 42 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 2 | import globals from "globals"; 3 | import tsParser from "@typescript-eslint/parser"; 4 | import path from "node:path"; 5 | import { fileURLToPath } from "node:url"; 6 | import js from "@eslint/js"; 7 | import { FlatCompat } from "@eslint/eslintrc"; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | const compat = new FlatCompat({ 12 | baseDirectory: __dirname, 13 | recommendedConfig: js.configs.recommended, 14 | allConfig: js.configs.all 15 | }); 16 | 17 | export default [ 18 | ...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"), 19 | { 20 | plugins: { 21 | "@typescript-eslint": typescriptEslint, 22 | }, 23 | 24 | languageOptions: { 25 | globals: { 26 | ...globals.node, 27 | }, 28 | 29 | parser: tsParser, 30 | }, 31 | 32 | rules: { 33 | "max-len": ["error", { 34 | code: 80, 35 | }], 36 | 37 | "new-parens": "error", 38 | "no-caller": "error", 39 | "no-cond-assign": ["error", "always"], 40 | "no-multiple-empty-lines": "off", 41 | 42 | quotes: ["error", "single", { 43 | avoidEscape: true, 44 | }], 45 | 46 | "arrow-parens": "off", 47 | "no-bitwise": "off", 48 | "sort-keys": "off", 49 | "no-console": "off", 50 | "max-classes-per-file": "off", 51 | "no-unused-expressions": "off", 52 | "@typescript-eslint/interface-name-prefix": "off", 53 | "comma-dangle": ["error", "always-multiline"], 54 | "@typescript-eslint/no-namespace": "off", 55 | "@typescript-eslint/no-extraneous-class": "off", 56 | }, 57 | }, 58 | ]; 59 | -------------------------------------------------------------------------------- /src/decorators/AssociatedWith.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * I'm Queue Software Project 5 | * Copyright (C) 2025 imqueue.com 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | * 20 | * If you want to use this code in a closed source (commercial) project, you can 21 | * purchase a proprietary commercial license. Please contact us at 22 | * to get commercial licensing options. 23 | */ 24 | export interface IAssociated { 25 | model: any; 26 | input: any; 27 | modelFieldName?: string; 28 | } 29 | 30 | /** 31 | * Defines property for build association between filter 32 | * fields and specific model 33 | * 34 | * @param {IAssociated} association - input data 35 | */ 36 | export function AssociatedWith( 37 | cb: () => IAssociated, 38 | ) { 39 | return (target: any, key: string) => { 40 | const association = cb(); 41 | 42 | if (!association) { 43 | return target; 44 | } 45 | 46 | if (!association.modelFieldName) { 47 | association.modelFieldName = key; 48 | } 49 | 50 | Object.defineProperty(target, key, { 51 | value: { 52 | model: association.model, 53 | input: association.input, 54 | key: association.modelFieldName, 55 | }, 56 | }); 57 | 58 | return target; 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/decorators/View.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * I'm Queue Software Project 5 | * Copyright (C) 2025 imqueue.com 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | * 20 | * If you want to use this code in a closed source (commercial) project, you can 21 | * purchase a proprietary commercial license. Please contact us at 22 | * to get commercial licensing options. 23 | */ 24 | import 'reflect-metadata'; 25 | import { ModelOptions } from 'sequelize'; 26 | import { addOptions, setModelName } from 'sequelize-typescript'; 27 | 28 | export interface IViewDefineOptions extends ModelOptions { 29 | viewDefinition: string; 30 | } 31 | 32 | // noinspection JSUnusedGlobalSymbols 33 | /** 34 | * Decorator factory: @View 35 | * 36 | * Adding view support for sequelize models, making sure views 37 | * could be defined in a safe way without a problems with sync/drop ops, 38 | * etc. 39 | * This decorator simply annotate a model entity the same way @Table does, 40 | * adding extra option "treatAsView" which is utilized by a BaseModel 41 | * class to override native behavior of sequelize models. 42 | * 43 | * @param {IViewDefineOptions | string} options - view definition options 44 | * @return {(...args: any[] => any)} - view annotation decorator 45 | */ 46 | export function View(options: IViewDefineOptions | string) { 47 | if (typeof options === 'string') { 48 | options = { viewDefinition: options }; 49 | } else if (!options || !options.viewDefinition.trim()) { 50 | throw new TypeError('View definition is missing!'); 51 | } 52 | 53 | return (target: any) => annotate(target, options as IViewDefineOptions); 54 | } 55 | 56 | /** 57 | * Does the job to define the view table 58 | * 59 | * @param {any} target - model class 60 | * @param {IViewDefineOptions} options - view definition options 61 | */ 62 | function annotate(target: any, options: IViewDefineOptions): void { 63 | Object.assign(options, { treatAsView: true }); 64 | 65 | setModelName(target.prototype, options.modelName || target.name); 66 | addOptions(target.prototype, options); 67 | } 68 | -------------------------------------------------------------------------------- /src/decorators/NullableIndex.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * I'm Queue Software Project 5 | * Copyright (C) 2025 imqueue.com 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | * 20 | * If you want to use this code in a closed source (commercial) project, you can 21 | * purchase a proprietary commercial license. Please contact us at 22 | * to get commercial licensing options. 23 | */ 24 | import { ColumnIndexOptions, FunctionType, ColumnIndex } from './ColumnIndex'; 25 | 26 | type Omit = Pick>; 27 | 28 | export type NullableColumnIndexOptions = Omit; 29 | 30 | // noinspection JSUnusedGlobalSymbols 31 | export function NullableIndex( 32 | options: Partial, 33 | ): FunctionType; 34 | // noinspection JSUnusedGlobalSymbols 35 | export function NullableIndex( 36 | target: any, 37 | propertyName: string, 38 | propertyDescriptor?: PropertyDescriptor, 39 | ): void; 40 | // noinspection JSUnusedGlobalSymbols 41 | export function NullableIndex(...args: any[]): FunctionType | void { 42 | if (args.length >= 2) { 43 | const [target, propertyName, propertyDescriptor] = args; 44 | const nullPredicate = `"${propertyName}" IS NULL`; 45 | const notNullPredicate = `"${propertyName}" IS NOT NULL`; 46 | 47 | ColumnIndex({ 48 | expression: nullPredicate, 49 | predicate: nullPredicate, 50 | })(target, propertyName, propertyDescriptor); 51 | ColumnIndex({ 52 | expression: notNullPredicate, 53 | predicate: notNullPredicate, 54 | })(target, propertyName, propertyDescriptor); 55 | 56 | return; 57 | } 58 | 59 | return ( 60 | target: any, 61 | propertyName: string, 62 | propertyDescriptor?: PropertyDescriptor, 63 | ) => { 64 | ColumnIndex(Object.assign(args[0], { 65 | expression: `"${propertyName}" IS NULL` 66 | }))(target, propertyName, propertyDescriptor); 67 | ColumnIndex(Object.assign(args[0], { 68 | expression: `"${propertyName}" IS NOT NULL` 69 | }))(target, propertyName, propertyDescriptor); 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /src/decorators/ColumnIndex.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * I'm Queue Software Project 5 | * Copyright (C) 2025 imqueue.com 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | * 20 | * If you want to use this code in a closed source (commercial) project, you can 21 | * purchase a proprietary commercial license. Please contact us at 22 | * to get commercial licensing options. 23 | */ 24 | import 'reflect-metadata'; 25 | import { addOptions, getOptions } from 'sequelize-typescript'; 26 | export type FunctionType = (...args: any[]) => any; 27 | 28 | export enum IndexMethod { 29 | // noinspection JSUnusedGlobalSymbols 30 | BTREE = 'BTREE', 31 | HASH = 'HASH', 32 | GIST = 'GIST', 33 | SPGIST = 'SPGIST', 34 | GIN = 'GIN', 35 | BRIN = 'BRIN', 36 | } 37 | 38 | export enum SortOrder { 39 | // noinspection JSUnusedGlobalSymbols 40 | ASC = 'ASC', 41 | DESC = 'DESC', 42 | } 43 | 44 | export interface ColumnIndexOptions { 45 | name: string; 46 | method: IndexMethod; 47 | concurrently: boolean; 48 | nullsFirst: boolean; 49 | order: SortOrder; 50 | predicate: string; 51 | expression: string; 52 | include: string[]; 53 | collation: string; 54 | opClass: string; 55 | tablespace: string; 56 | safe: boolean; 57 | unique: boolean; 58 | } 59 | 60 | export function ColumnIndex(options: Partial): FunctionType; 61 | export function ColumnIndex( 62 | target: any, 63 | propertyName: string, 64 | propertyDescriptor?: PropertyDescriptor, 65 | ): void; 66 | export function ColumnIndex(...args: any[]): FunctionType | void { 67 | if (args.length >= 2) { 68 | const [target, propertyName, propertyDescriptor] = args; 69 | 70 | return annotate(target, propertyName, propertyDescriptor); 71 | } 72 | 73 | return ( 74 | target: any, 75 | propertyName: string, 76 | propertyDescriptor?: PropertyDescriptor, 77 | ) => { 78 | annotate(target, propertyName, propertyDescriptor, args[0]); 79 | }; 80 | } 81 | 82 | function annotate( 83 | target: any, 84 | propertyName: string, 85 | propertyDescriptor?: PropertyDescriptor, 86 | options: Partial = {}, 87 | ): void { 88 | const indices = (getOptions(target) as any).indices || []; 89 | addOptions(target, { 90 | indices: [...indices, { 91 | column: propertyName, 92 | options, 93 | }], 94 | } as any); 95 | } 96 | 97 | 98 | -------------------------------------------------------------------------------- /src/decorators/DynamicView.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * I'm Queue Software Project 3 | * Copyright (C) 2025 imqueue.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * If you want to use this code in a closed source (commercial) project, you can 19 | * purchase a proprietary commercial license. Please contact us at 20 | * to get commercial licensing options. 21 | */ 22 | import 'reflect-metadata'; 23 | import { addOptions, setModelName } from 'sequelize-typescript'; 24 | import { IViewDefineOptions } from './View'; 25 | 26 | /** 27 | * Key/Value parameter store 28 | */ 29 | export interface ViewParams { 30 | [name: string]: string; 31 | } 32 | export interface IDynamicViewDefineOptions extends IViewDefineOptions { 33 | viewParams: ViewParams; 34 | viewDefinition: string; 35 | isDynamicView?: boolean; 36 | } 37 | 38 | export const MATCHER = '@\\{([a-z0-9_]+?)\\}'; 39 | export const RX_MATCHER = new RegExp(MATCHER, 'gi'); 40 | export const RX_NAME_MATCHER = new RegExp(MATCHER, 'i'); 41 | 42 | // noinspection JSUnusedGlobalSymbols 43 | /** 44 | * Decorator factory: @View 45 | * 46 | * Adding view support for sequelize models, making sure views 47 | * could be defined in a safe way without a problems with sync/drop ops, etc. 48 | * This decorator simply annotate a model entity the same way @Table does, 49 | * adding extra option "treatAsView" which is utilized by a BaseModel 50 | * class to override native behavior of sequelize models. 51 | * 52 | * @param {IViewDefineOptions | string} options - view definition options 53 | * @return {() => any} - view annotation decorator 54 | */ 55 | export function DynamicView( 56 | options: IDynamicViewDefineOptions, 57 | ) { 58 | if (!options || !options.viewDefinition.trim()) { 59 | throw new TypeError('View definition is missing!'); 60 | } 61 | 62 | // we are dynamic, no choice here! 63 | options.isDynamicView = true; 64 | 65 | const viewDef = options.viewDefinition || ''; 66 | const viewParams = options.viewParams || {}; 67 | 68 | (viewDef.match(RX_MATCHER) || []).forEach(param => { 69 | const [, name] = (param.match(RX_NAME_MATCHER) || ['', '']); 70 | 71 | if (typeof viewParams[name] !== 'string') { 72 | throw new TypeError( 73 | `View definition contains param '${ 74 | name}', but it was not provided`, 75 | ); 76 | } 77 | }); 78 | 79 | return (target: any) => annotate(target, options as IViewDefineOptions); 80 | } 81 | 82 | /** 83 | * Does the job to define the view table 84 | * 85 | * @param {any} target - model class 86 | * @param {IViewDefineOptions} options - view definition options 87 | */ 88 | function annotate(target: any, options: IViewDefineOptions): void { 89 | Object.assign(options, { treatAsView: true }); 90 | 91 | setModelName(target.prototype, options.modelName || target.name); 92 | addOptions(target.prototype, options); 93 | } 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@imqueue/sequelize", 3 | "version": "3.0.3", 4 | "description": "Sequelize ORM refines for @imqueue", 5 | "main": "index.js", 6 | "scripts": { 7 | "prepublishOnly": "npm run build", 8 | "build": "tsc", 9 | "mocha": "nyc mocha", 10 | "show:test": "/usr/bin/env node -e \"import('open').then(open => open.default('file://`pwd`/coverage/index.html',{wait:false}));\"", 11 | "show:doc": "/usr/bin/env node -e \"import('open').then(open => open.default('file://`pwd`/docs/index.html',{wait:false}));\"", 12 | "test": "npm run build && npm run mocha && npm run show:test", 13 | "clean:dts": "find . -name '*.d.ts' -not -wholename '*node_modules*' -type f -delete", 14 | "clean:map": "find . -name '*.js.map' -not -wholename '*node_modules*' -type f -delete", 15 | "clean:js": "find . -name '*.js' -not -wholename '*node_modules*' -type f -delete", 16 | "clean:ts": "find . -name '*.ts' -not -wholename '*node_modules*' -not -wholename '*.d.ts' -type f -delete", 17 | "clean:test": "rm -rf .nyc_output coverage", 18 | "clean:doc": "rm -rf docs", 19 | "clean": "npm run clean:test && npm run clean:dts && npm run clean:map && npm run clean:js && npm run clean:doc", 20 | "doc": "npm run clean:doc && typedoc --excludePrivate --excludeExternals --hideGenerator --exclude \"**/+(debug|test|node_modules|docs|coverage|.nyc_output)/**/*\" --out ./docs . && npm run show:doc", 21 | "help": "npm-scripts-help" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git@github.com:/imqueue/sequelize" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/imqueue/sequelize/issues" 29 | }, 30 | "homepage": "https://github.com/imqueue/sequelize", 31 | "author": "imqueue.com ", 32 | "license": "ISC", 33 | "dependencies": { 34 | "@imqueue/core": "^2.0.13", 35 | "@imqueue/js": "^2.0.0", 36 | "@imqueue/rpc": "^2.0.17", 37 | "@types/bluebird": "^3.5.42", 38 | "@types/validator": "^13.15.3", 39 | "bluebird": "^3.7.2", 40 | "chalk": "^5.6.2", 41 | "dotenv": "^17.2.2", 42 | "pg": "^8.16.3", 43 | "sequelize": "^6.37.7", 44 | "sequelize-typescript": "^2.1.6", 45 | "sql-formatter": "^15.6.9", 46 | "uuid": "^13.0.0" 47 | }, 48 | "devDependencies": { 49 | "@eslint/eslintrc": "^3.3.1", 50 | "@eslint/js": "^9.35.0", 51 | "@types/chai": "^5.2.2", 52 | "@types/mocha": "^10.0.10", 53 | "@types/mock-require": "^3.0.0", 54 | "@types/node": "^24.3.1", 55 | "@types/pg": "^8.15.5", 56 | "@types/sinon": "^17.0.4", 57 | "@types/uuid": "^10.0.0", 58 | "@typescript-eslint/eslint-plugin": "^8.43.0", 59 | "@typescript-eslint/parser": "^8.43.0", 60 | "@typescript-eslint/typescript-estree": "^8.43.0", 61 | "chai": "^6.0.1", 62 | "eslint": "^9.35.0", 63 | "globals": "^16.4.0", 64 | "minimist": "^1.2.8", 65 | "mocha": "^11.7.2", 66 | "mocha-lcov-reporter": "^1.3.0", 67 | "mock-require": "^3.0.3", 68 | "npm-scripts-help": "^0.8.0", 69 | "nyc": "^17.1.0", 70 | "open": "^10.1.2", 71 | "reflect-metadata": "^0.2.2", 72 | "sequelize-cli": "^6.6.3", 73 | "sinon": "^21.0.0", 74 | "source-map-support": "^0.5.21", 75 | "ts-node": "^10.9.2", 76 | "typedoc": "^0.28.12", 77 | "typescript": "^5.9.2" 78 | }, 79 | "typescript": { 80 | "definitions": "index.d.ts" 81 | }, 82 | "nyc": { 83 | "check-coverage": false, 84 | "extension": [ 85 | ".ts" 86 | ], 87 | "exclude": [ 88 | "**/*.d.ts", 89 | "**/test/**" 90 | ], 91 | "require": [ 92 | "ts-node/register" 93 | ], 94 | "reporter": [ 95 | "html", 96 | "text", 97 | "text-summary", 98 | "lcovonly" 99 | ] 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/types/FilterInput.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * I'm Queue Software Project 5 | * Copyright (C) 2025 imqueue.com 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | * 20 | * If you want to use this code in a closed source (commercial) project, you can 21 | * purchase a proprietary commercial license. Please contact us at 22 | * to get commercial licensing options. 23 | */ 24 | import { property } from '@imqueue/rpc'; 25 | import { Op } from 'sequelize'; 26 | 27 | export const FILTER_OPS = { 28 | $and: Op.and, 29 | $or: Op.or, 30 | $gt: Op.gt, 31 | $gte: Op.gte, 32 | $lt: Op.lt, 33 | $lte: Op.lte, 34 | $ne: Op.ne, 35 | $eq: Op.eq, 36 | $not: Op.not, 37 | $between: Op.between, 38 | $notBetween: Op.notBetween, 39 | $in: Op.in, 40 | $notIn: Op.notIn, 41 | $like: Op.like, 42 | $notLike: Op.notLike, 43 | $iLike: Op.iLike, 44 | $notILike: Op.notILike, 45 | $regexp: Op.regexp, 46 | $notRegexp: Op.notRegexp, 47 | $iRegexp: Op.iRegexp, 48 | $notIRegexp: Op.notIRegexp, 49 | $overlap: Op.overlap, 50 | $contains: Op.contains, 51 | $contained: Op.contained, 52 | $any: Op.any, 53 | $adjacent: Op.adjacent, 54 | $strictLeft: Op.strictLeft, 55 | $strictRight: Op.strictRight, 56 | $noExtendRight: Op.noExtendRight, 57 | $noExtendLeft: Op.noExtendLeft, 58 | }; 59 | 60 | export class FilterInput { 61 | @property( 62 | 'FilterInput | Array', 63 | true) 64 | public $and?: 65 | FilterInput | Array; 66 | 67 | @property( 68 | 'FilterInput | Array', 69 | true) 70 | public $or?: 71 | FilterInput | Array; 72 | 73 | @property('number', true) 74 | public $gt?: number; 75 | 76 | @property('number', true) 77 | public $gte?: number; 78 | 79 | @property('number', true) 80 | public $lt?: number; 81 | 82 | @property('number', true) 83 | public $lte?: number; 84 | 85 | @property('number | string', true) 86 | public $ne?: number | string; 87 | 88 | @property('number | string | boolean | null', true) 89 | public $eq?: number | string | boolean | null; 90 | 91 | @property('boolean', true) 92 | public $not?: boolean; 93 | 94 | @property('Array', true) 95 | public $between?: Array; 96 | 97 | @property('Array', true) 98 | public $notBetween?: Array; 99 | 100 | @property('Array', true) 101 | public $in?: Array; 102 | 103 | @property('Array', true) 104 | public $notIn?: Array; 105 | 106 | @property('string', true) 107 | public $like?: string; 108 | 109 | @property('string', true) 110 | public $notLike?: string; 111 | 112 | @property('string', true) 113 | public $iLike?: string; 114 | 115 | @property('string', true) 116 | public $notILike?: string; 117 | 118 | @property('string', true) 119 | public $regexp?: string; 120 | 121 | @property('string', true) 122 | public $notRegexp?: string; 123 | 124 | @property('string', true) 125 | public $iRegexp?: string; 126 | 127 | @property('string', true) 128 | public $notIRegexp?: string; 129 | 130 | @property('[number, number]', true) 131 | public $overlap?: [number, number]; 132 | 133 | @property('number | [number, number]', true) 134 | public $contains?: number | [number, number]; 135 | 136 | @property('[number, number]', true) 137 | public $contained?: [number, number]; 138 | 139 | @property('number[] | string[]', true) 140 | public $any?: number[] | string[]; 141 | 142 | @property('[number, number]', true) 143 | public $adjacent?: [number, number]; 144 | 145 | @property('[number, number]', true) 146 | public $strictLeft?: [number, number]; 147 | 148 | @property('[number, number]', true) 149 | public $strictRight?: [number, number]; 150 | 151 | @property('[number, number]', true) 152 | public $noExtendRight?: [number, number]; 153 | 154 | @property('[number, number]', true) 155 | public $noExtendLeft?: [number, number]; 156 | } 157 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * Copyright (c) 2019, imqueue.com 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 11 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 12 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 13 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 14 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 15 | * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 16 | * PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | import { js } from '@imqueue/js'; 19 | import { DEFAULT_IMQ_SERVICE_OPTIONS, ILogger } from '@imqueue/rpc'; 20 | import * as fs from 'fs'; 21 | import chalk from 'chalk'; 22 | import { resolve, sep } from 'path'; 23 | import { SequelizeOptions } from 'sequelize-typescript'; 24 | import { Sequelize } from './BaseModel'; 25 | import isDefined = js.isDefined; 26 | import isOk = js.isOk; 27 | 28 | /* models exports! */ 29 | export * from './Graph'; 30 | export * from './BaseModel'; 31 | export * from './helpers'; 32 | export * from './decorators'; 33 | export * from './types'; 34 | 35 | const JS_EXT_RX = /\.js$/; 36 | 37 | /** 38 | * Returns all files list from a given directory 39 | * 40 | * @param {string} dir 41 | * @return {string[]} 42 | */ 43 | function walk(dir: string) { 44 | let results: string[] = []; 45 | 46 | for (let file of fs.readdirSync(dir)) { 47 | file = resolve(dir, file); 48 | 49 | const stat = fs.statSync(file); 50 | 51 | if (stat && stat.isDirectory()) { 52 | results = results.concat(walk(file)); 53 | } else { 54 | results.push(file); 55 | } 56 | } 57 | 58 | return results; 59 | } 60 | 61 | /** 62 | * ORM database() configuration options. Database connection string 63 | * can be provided explicitly or by setting DB_CONN_STR environment variable. 64 | * 65 | * @type {{ 66 | * logger: ILogger, 67 | * connectionString: string, 68 | * sequelize: SequelizeOptions, 69 | * modelsPath: string 70 | * }} IMQORMOptions 71 | */ 72 | export interface IMQORMOptions { 73 | logger: ILogger; 74 | connectionString?: string; 75 | sequelize: SequelizeOptions; 76 | modelsPath: string; 77 | } 78 | 79 | /** 80 | * Database connection string from environment variable 81 | * 82 | * @type {string} 83 | */ 84 | export const DB_CONN_STR = process.env.DB_CONN_STR; 85 | 86 | /** 87 | * SQL prettify flag. Can be set by environment variable 88 | * SQL_PRETTIFY = 1|0. By default is false. 89 | * 90 | * @type {boolean} 91 | */ 92 | export const SQL_PRETTIFY = +(process.env.SQL_PRETTIFY || 0) > 0; 93 | 94 | /** 95 | * SQL colorize flag. Can be set by environment variable 96 | * SQL_COLORIZE = 1|0. By default is false. 97 | * 98 | * @type {boolean} 99 | */ 100 | export const SQL_COLORIZE = +(process.env.SQL_COLORIZE || 0) > 0; 101 | 102 | // tslint:disable-next-line:no-var-requires 103 | const sqlFormatter = require('sql-formatter'); 104 | const RX_SQL_NUM_LAYOUT = /\s+(['"]?\d+['"]?,?)\r?\n/g; 105 | const RX_SQL_NUM_END = /(\d+['"]?)\s+(\))/g; 106 | const RX_BRK_DBL_AND = /&\s+&/g; 107 | const RX_BRK_CAST = /\s+(\[|::)(\s+)?/g; 108 | const RX_BRK_POCKETS = /(\$)\s+(\d)/g; 109 | const RX_SQL_PREFIX = /Execut(ed|ing) \(default\):/; 110 | 111 | /** 112 | * Returns pretty formatted SQL string of the given input SQL string 113 | * 114 | * @param {string} sql 115 | * @return {string} 116 | */ 117 | export function formatSql(sql: string): string { 118 | return SQL_PRETTIFY ? 119 | sqlFormatter.format(sql) 120 | .replace(RX_SQL_NUM_LAYOUT, '$1 ') 121 | .replace(RX_SQL_NUM_END, '$1$2') 122 | .replace(RX_BRK_DBL_AND, '&&') 123 | .replace(RX_BRK_CAST, '$1') 124 | .replace(RX_BRK_POCKETS, '$1$2') 125 | : sql; 126 | } 127 | 128 | // noinspection SuspiciousTypeOfGuard 129 | /** 130 | * Returns logging routine property for sequelize config options 131 | * taking into account configured SQL_PRETTIFY, SQL_COLORIZE environment 132 | * variables options. 133 | * 134 | * @param {ILogger} logger 135 | * @return {(sql: string, time: number) => string} 136 | */ 137 | const logging = (logger: ILogger) => (sql: string, time: number) => 138 | logger.log(SQL_COLORIZE 139 | ? `${(chalk.bold.yellow as (...args: any[]) => any)('SQL Query:')} ${ 140 | (chalk.cyan as (...args: any[]) => any)( 141 | formatSql(sql.replace(RX_SQL_PREFIX, '') 142 | ))}` 143 | : `SQL Query: ${formatSql(sql.replace(RX_SQL_PREFIX, ''))}`, 144 | (typeof time === 'number' ? `executed in ${time} ms` : '') 145 | ); 146 | 147 | let orm: Sequelize; 148 | 149 | // noinspection JSUnusedGlobalSymbols 150 | /** 151 | * Initialized all known by this package database models and 152 | * returns instance of Sequelize, mapped with these models 153 | * 154 | * @param {IMQORMOptions} [options] 155 | * @return {Sequelize} 156 | */ 157 | export function database( 158 | options?: IMQORMOptions, 159 | ): Sequelize { 160 | if (typeof orm !== 'undefined') { 161 | return orm; 162 | } else if (typeof options === 'undefined') { 163 | throw new TypeError( 164 | 'First call of database() must provide valid options!' 165 | ); 166 | } 167 | 168 | if (!options.connectionString) { 169 | if (!DB_CONN_STR) { 170 | throw new TypeError( 171 | 'Either environment DB_CONN_STR should be set or ' + 172 | 'connectionString property given!' 173 | ); 174 | } 175 | 176 | options.connectionString = DB_CONN_STR; 177 | } 178 | 179 | if (!options.connectionString) { 180 | throw new TypeError('Database connection string is required!'); 181 | } 182 | 183 | if (!options.logger) { 184 | options.logger = DEFAULT_IMQ_SERVICE_OPTIONS.logger 185 | || console as ILogger; 186 | } 187 | 188 | options.sequelize.logging = 189 | !isDefined(options.sequelize.logging) || 190 | isOk(options.sequelize.logging) 191 | ? logging( 192 | typeof options.sequelize.logging === 'function' 193 | ? options.sequelize.logging as any as ILogger 194 | : options.logger, 195 | ) 196 | : options.sequelize.logging as boolean; 197 | 198 | orm = new Sequelize(options.connectionString as string, options.sequelize); 199 | 200 | orm.addModels(walk(resolve(options.modelsPath)) 201 | .filter(name => JS_EXT_RX.test(name)) 202 | .map(filename => require(filename)[ 203 | filename.split(sep) 204 | .reverse()[0] 205 | .replace(JS_EXT_RX, '') 206 | ])); 207 | 208 | options.logger.log('Database models initialized...'); 209 | 210 | return orm; 211 | } 212 | -------------------------------------------------------------------------------- /src/Graph.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * I'm Queue Software Project 5 | * Copyright (C) 2025 imqueue.com 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | * 20 | * If you want to use this code in a closed source (commercial) project, you can 21 | * purchase a proprietary commercial license. Please contact us at 22 | * to get commercial licensing options. 23 | */ 24 | /** 25 | * Graph internal storage data type 26 | * 27 | * @type {Map} 28 | */ 29 | export type GraphMap = Map; 30 | 31 | /** 32 | * Callback type used on graph traversal iterations steps. It will obtain 33 | * vertex as a first argument - is a vertex on iteration visit, and a map 34 | * of visited vertices as a second argument. 35 | * If this callback returns false value it will break iteration cycle. 36 | * 37 | * @type {(vertex: T, visited: Map): false | void} 38 | */ 39 | export type GraphForeachCallback = ( 40 | vertex: T, 41 | visited: Map 42 | ) => false | void; 43 | 44 | /** 45 | * Class Graph 46 | * Simple undirected, unweighted graph data structure implementation 47 | * with DFS (depth-first search traversal implementation) 48 | */ 49 | export class Graph { 50 | /** 51 | * Internal graph data storage 52 | * 53 | * @access private 54 | * @type {GraphMap} 55 | */ 56 | private list: GraphMap = new Map(); 57 | 58 | /** 59 | * Adds vertices to graph 60 | * 61 | * @param {...any[]} vertex - vertices to add 62 | * @return {Graph} 63 | */ 64 | public addVertex(...vertex: T[]): Graph { 65 | for (const v of vertex) { 66 | this.list.set(v, []); 67 | } 68 | 69 | return this; 70 | } 71 | 72 | // noinspection JSUnusedGlobalSymbols 73 | /** 74 | * Removes vertices from a graph with all their edges 75 | * 76 | * @param {...any[]} vertex 77 | * @return {Graph} 78 | */ 79 | public delVertex(...vertex: T[]): Graph { 80 | for (const v of vertex) { 81 | this.list.delete(v); 82 | } 83 | 84 | return this; 85 | } 86 | 87 | /** 88 | * Adds an edges to a given vertex 89 | * 90 | * @param {any} fromVertex 91 | * @param {...any[]} toVertex 92 | * @return {Graph} 93 | */ 94 | public addEdge(fromVertex: T, ...toVertex: T[]): Graph { 95 | let edges = this.list.get(fromVertex); 96 | 97 | if (!edges) { 98 | this.addVertex(fromVertex); 99 | edges = this.list.get(fromVertex) as T[]; 100 | } 101 | 102 | edges.push(...toVertex); 103 | 104 | return this; 105 | } 106 | 107 | // noinspection JSUnusedGlobalSymbols 108 | /** 109 | * Removes given edges from a given vertex 110 | * 111 | * @param {any} fromVertex - target vertex to remove edges from 112 | * @param {...any[]} toVertex - edges to remove 113 | * @return {Graph} 114 | */ 115 | public delEdge(fromVertex: T, ...toVertex: T[]): Graph { 116 | const edges = this.list.get(fromVertex); 117 | 118 | if (!(edges && edges.length)) { 119 | return this; 120 | } 121 | 122 | for (const vertex of toVertex) { 123 | while (~edges.indexOf(vertex)) { 124 | edges.splice(edges.indexOf(vertex), 1); 125 | } 126 | } 127 | 128 | return this; 129 | } 130 | 131 | /** 132 | * Checks if a given vertex has given edge, returns true if has, false - 133 | * otherwise 134 | * 135 | * @param {any} vertex 136 | * @param {any} edge 137 | * @return {boolean} 138 | */ 139 | public hasEdge(vertex: T, edge: T): boolean { 140 | return !!~(this.list.get(vertex) || []).indexOf(edge); 141 | } 142 | 143 | /** 144 | * Checks if this graph contains given vertex, returns true if contains, 145 | * false - otherwise 146 | * 147 | * @param {any} vertex 148 | * @return {boolean} 149 | */ 150 | public hasVertex(vertex: T): boolean { 151 | return this.list.has(vertex); 152 | } 153 | 154 | // noinspection JSUnusedGlobalSymbols 155 | /** 156 | * Performs DFS traversal over graph, executing on each step passed callback 157 | * function. If callback returns false - will stop traversal at that 158 | * step. 159 | * 160 | * @param {GraphForeachCallback} callback 161 | * @return {Graph} 162 | */ 163 | public forEach(callback: GraphForeachCallback): Graph { 164 | const visited = new Map(); 165 | 166 | for (const node of this.list.keys()) { 167 | this.walk(node, callback, visited); 168 | } 169 | 170 | return this; 171 | } 172 | 173 | /** 174 | * Performs DFS walk over graph staring from given vertex, unless 175 | * graph path is end for that vertex. So, literally, it performs 176 | * walking through a possible path down the staring vertex in a graph. 177 | * 178 | * @param {any} vertex 179 | * @param {GraphForeachCallback} callback 180 | * @param {Map()} visited 181 | * @return {Graph} 182 | */ 183 | public walk( 184 | vertex: T, 185 | callback?: GraphForeachCallback, 186 | visited = new Map(), 187 | ): Graph { 188 | if (!visited.get(vertex)){ 189 | visited.set(vertex, true); 190 | 191 | if (callback && callback(vertex, visited) === false) { 192 | return this; 193 | } 194 | 195 | for (const neighbor of this.list.get(vertex) || []) { 196 | this.walk(neighbor, callback, visited); 197 | } 198 | } 199 | 200 | return this; 201 | } 202 | 203 | // noinspection JSUnusedGlobalSymbols 204 | /** 205 | * Returns max possible path down the graph for a given vertex, 206 | * using DFS traversal over the path 207 | * 208 | * @param {any} vertex 209 | * @return {IterableIterator} 210 | */ 211 | public path(vertex: T): IterableIterator { 212 | const visited = new Map(); 213 | 214 | this.walk(vertex, undefined, visited); 215 | 216 | return visited.keys(); 217 | } 218 | 219 | // noinspection JSUnusedGlobalSymbols 220 | /** 221 | * Returns true if graph has al least one cycled path in it, 222 | * false - otherwise 223 | * 224 | * @return {boolean} 225 | */ 226 | public isCycled(): boolean { 227 | const visited = new Map(); 228 | const stack = new Map(); 229 | 230 | for (const node of this.list.keys()) { 231 | if (this.detectCycle(node, visited, stack)) { 232 | return true; 233 | } 234 | } 235 | 236 | return false; 237 | } 238 | 239 | // noinspection JSUnusedGlobalSymbols 240 | /** 241 | * Returns list of vertices in this graph 242 | * 243 | * @return {IterableIterator} 244 | */ 245 | public vertices(): IterableIterator { 246 | return this.list.keys(); 247 | } 248 | 249 | /** 250 | * Performs recursive cycles detection on a graph. 251 | * Private method. If you need to detect cycles, use isCycled() instead. 252 | * 253 | * @access private 254 | * @param {any} vertex 255 | * @param {Map} visited 256 | * @param {Map} stack 257 | */ 258 | private detectCycle( 259 | vertex: T, 260 | visited: Map, 261 | stack: Map, 262 | ): boolean { 263 | if (!visited.get(vertex)) { 264 | visited.set(vertex, true); 265 | stack.set(vertex, true); 266 | 267 | for (const currentNode of this.list.get(vertex) || []) { 268 | if ((!visited.get(currentNode) && this.detectCycle( 269 | currentNode, visited, stack, 270 | )) || stack.get(currentNode)) { 271 | return true; 272 | } 273 | } 274 | } 275 | 276 | stack.set(vertex, false); 277 | 278 | return false; 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright © 2007 Free Software Foundation, Inc. 5 | 6 | Everyone is permitted to copy and distribute verbatim copies of this license 7 | document, but changing it is not allowed. 8 | 9 | Preamble 10 | The GNU General Public License is a free, copyleft license for software and 11 | other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed to take 14 | away your freedom to share and change the works. By contrast, the GNU General 15 | Public License is intended to guarantee your freedom to share and change all 16 | versions of a program--to make sure it remains free software for all its users. 17 | We, the Free Software Foundation, use the GNU General Public License for most of 18 | our software; it applies also to any other work released this way by its 19 | authors. You can apply it to your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not price. Our 22 | General Public Licenses are designed to make sure that you have the freedom to 23 | distribute copies of free software (and charge for them if you wish), that you 24 | receive source code or can get it if you want it, that you can change the 25 | software or use pieces of it in new free programs, and that you know you can 26 | do these things. 27 | 28 | To protect your rights, we need to prevent others from denying you these rights 29 | or asking you to surrender the rights. Therefore, you have certain 30 | responsibilities if you distribute copies of the software, or if you modify it: 31 | responsibilities to respect the freedom of others. 32 | 33 | For example, if you distribute copies of such a program, whether gratis or for a 34 | fee, you must pass on to the recipients the same freedoms that you received. You 35 | must make sure that they, too, receive or can get the source code. And you must 36 | show them these terms so they know their rights. 37 | 38 | Developers that use the GNU GPL protect your rights with two steps: (1) assert 39 | copyright on the software, and (2) offer you this License giving you legal 40 | permission to copy, distribute and/or modify it. 41 | 42 | For the developers' and authors' protection, the GPL clearly explains that there 43 | is no warranty for this free software. For both users' and authors' sake, the 44 | GPL requires that modified versions be marked as changed, so that their problems 45 | will not be attributed erroneously to authors of previous versions. 46 | 47 | Some devices are designed to deny users access to install or run modified 48 | versions of the software inside them, although the manufacturer can do so. This 49 | is fundamentally incompatible with the aim of protecting users' freedom to 50 | change the software. The systematic pattern of such abuse occurs in the area of 51 | products for individuals to use, which is precisely where it is most 52 | unacceptable. Therefore, we have designed this version of the GPL to prohibit 53 | the practice for those products. If such problems arise substantially in other 54 | domains, we stand ready to extend this provision to those domains in future 55 | versions of the GPL, as needed to protect the freedom of users. 56 | 57 | Finally, every program is threatened constantly by software patents. States 58 | should not allow patents to restrict development and use of software on 59 | general-purpose computers, but in those that do, we wish to avoid the special 60 | danger that patents applied to a free program could make it effectively 61 | proprietary. To prevent this, the GPL assures that patents cannot be used to 62 | render the program non-free. 63 | 64 | The precise terms and conditions for copying, distribution and modification 65 | follow. 66 | 67 | TERMS AND CONDITIONS 68 | 0. Definitions. 69 | “This License” refers to version 3 of the GNU General Public License. 70 | 71 | “Copyright” also means copyright-like laws that apply to other kinds of works, 72 | such as semiconductor masks. 73 | 74 | “The Program” refers to any copyrightable work licensed under this License. Each 75 | licensee is addressed as “you”. “Licensees” and “recipients” may be individuals 76 | or organizations. 77 | 78 | To “modify” a work means to copy from or adapt all or part of the work in a 79 | fashion requiring copyright permission, other than the making of an exact copy. 80 | The resulting work is called a “modified version” of the earlier work or a work 81 | “based on” the earlier work. 82 | 83 | A “covered work” means either the unmodified Program or a work based on the 84 | Program. 85 | 86 | To “propagate” a work means to do anything with it that, without permission, 87 | would make you directly or secondarily liable for infringement under applicable 88 | copyright law, except executing it on a computer or modifying a private copy. 89 | Propagation includes copying, distribution (with or without modification), 90 | making available to the public, and in some countries other activities as well. 91 | 92 | To “convey” a work means any kind of propagation that enables other parties to 93 | make or receive copies. Mere interaction with a user through a computer network, 94 | with no transfer of a copy, is not conveying. 95 | 96 | An interactive user interface displays “Appropriate Legal Notices” to the extent 97 | that it includes a convenient and prominently visible feature that (1) displays 98 | an appropriate copyright notice, and (2) tells the user that there is no 99 | warranty for the work (except to the extent that warranties are provided), that 100 | licensees may convey the work under this License, and how to view a copy of this 101 | License. If the interface presents a list of user commands or options, such as a 102 | menu, a prominent item in the list meets this criterion. 103 | 104 | 1. Source Code. 105 | The “source code” for a work means the preferred form of the work for making 106 | modifications to it. “Object code” means any non-source form of a work. 107 | 108 | A “Standard Interface” means an interface that either is an official standard 109 | defined by a recognized standards body, or, in the case of interfaces specified 110 | for a particular programming language, one that is widely used among developers 111 | working in that language. 112 | 113 | The “System Libraries” of an executable work include anything, other than the 114 | work as a whole, that (a) is included in the normal form of packaging a Major 115 | Component, but which is not part of that Major Component, and (b) serves only to 116 | enable use of the work with that Major Component, or to implement a Standard 117 | Interface for which an implementation is available to the public in source code 118 | form. A “Major Component”, in this context, means a major essential component 119 | (kernel, window system, and so on) of the specific operating system (if any) on 120 | which the executable work runs, or a compiler used to produce the work, or an 121 | object code interpreter used to run it. 122 | 123 | The “Corresponding Source” for a work in object code form means all the source 124 | code needed to generate, install, and (for an executable work) run the object 125 | code and to modify the work, including scripts to control those activities. 126 | However, it does not include the work's System Libraries, or general-purpose 127 | tools or generally available free programs which are used unmodified in 128 | performing those activities but which are not part of the work. For example, 129 | Corresponding Source includes interface definition files associated with source 130 | files for the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, such as by 132 | intimate data communication or control flow between those subprograms and other 133 | parts of the work. 134 | 135 | The Corresponding Source need not include anything that users can regenerate 136 | automatically from other parts of the Corresponding Source. 137 | 138 | The Corresponding Source for a work in source code form is that same work. 139 | 140 | 2. Basic Permissions. 141 | All rights granted under this License are granted for the term of copyright on 142 | the Program, and are irrevocable provided the stated conditions are met. This 143 | License explicitly affirms your unlimited permission to run the unmodified 144 | Program. The output from running a covered work is covered by this License only 145 | if the output, given its content, constitutes a covered work. This License 146 | acknowledges your rights of fair use or other equivalent, as provided by 147 | copyright law. 148 | 149 | You may make, run and propagate covered works that you do not convey, without 150 | conditions so long as your license otherwise remains in force. You may convey 151 | covered works to others for the sole purpose of having them make modifications 152 | exclusively for you, or provide you with facilities for running those works, 153 | provided that you comply with the terms of this License in conveying all 154 | material for which you do not control copyright. Those thus making or running 155 | the covered works for you must do so exclusively on your behalf, under your 156 | direction and control, on terms that prohibit them from making any copies of 157 | your copyrighted material outside their relationship with you. 158 | 159 | Conveying under any other circumstances is permitted solely under the conditions 160 | stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 161 | 162 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 163 | No covered work shall be deemed part of an effective technological measure under 164 | any applicable law fulfilling obligations under article 11 of the WIPO copyright 165 | treaty adopted on 20 December 1996, or similar laws prohibiting or restricting 166 | circumvention of such measures. 167 | 168 | When you convey a covered work, you waive any legal power to forbid 169 | circumvention of technological measures to the extent such circumvention is 170 | effected by exercising rights under this License with respect to the covered 171 | work, and you disclaim any intention to limit operation or modification of the 172 | work as a means of enforcing, against the work's users, your or third parties' 173 | legal rights to forbid circumvention of technological measures. 174 | 175 | 4. Conveying Verbatim Copies. 176 | You may convey verbatim copies of the Program's source code as you receive it, 177 | in any medium, provided that you conspicuously and appropriately publish on each 178 | copy an appropriate copyright notice; keep intact all notices stating that this 179 | License and any non-permissive terms added in accord with section 7 apply to the 180 | code; keep intact all notices of the absence of any warranty; and give all 181 | recipients a copy of this License along with the Program. 182 | 183 | You may charge any price or no price for each copy that you convey, and you may 184 | offer support or warranty protection for a fee. 185 | 186 | 5. Conveying Modified Source Versions. 187 | You may convey a work based on the Program, or the modifications to produce it 188 | from the Program, in the form of source code under the terms of section 4, 189 | provided that you also meet all of these conditions: 190 | 191 | a) The work must carry prominent notices stating that you modified it, and 192 | giving a relevant date. 193 | b) The work must carry prominent notices stating that it is released under this 194 | License and any conditions added under section 7. This requirement modifies 195 | the requirement in section 4 to “keep intact all notices”. 196 | c) You must license the entire work, as a whole, under this License to anyone 197 | who comes into possession of a copy. This License will therefore apply, along 198 | with any applicable section 7 additional terms, to the whole of the work, 199 | and all its parts, regardless of how they are packaged. This License gives no 200 | permission to license the work in any other way, but it does not invalidate 201 | such permission if you have separately received it. 202 | d) If the work has interactive user interfaces, each must display Appropriate 203 | Legal Notices; however, if the Program has interactive interfaces that do not 204 | display Appropriate Legal Notices, your work need not make them do so. 205 | A compilation of a covered work with other separate and independent works,which 206 | are not by their nature extensions of the covered work, and which are not 207 | combined with it such as to form a larger program, in or on a volume of a 208 | storage or distribution medium, is called an “aggregate” if the compilation and 209 | its resulting copyright are not used to limit the access or legal rights of the 210 | compilation's users beyond what the individual works permit. Inclusion of a 211 | covered work in an aggregate does not cause this License to apply to the other 212 | parts of the aggregate. 213 | 214 | 6. Conveying Non-Source Forms. 215 | You may convey a covered work in object code form under the terms of sections 4 216 | and 5, provided that you also convey the machine-readable Corresponding Source 217 | under the terms of this License, in one of these ways: 218 | 219 | a) Convey the object code in, or embodied in, a physical product (including a 220 | physical distribution medium), accompanied by the Corresponding Source fixed 221 | on a durable physical medium customarily used for software interchange. 222 | b) Convey the object code in, or embodied in, a physical product (including a 223 | physical distribution medium), accompanied by a written offer, valid for at 224 | least three years and valid for as long as you offer spare parts or customer 225 | support for that product model, to give anyone who possesses the object code 226 | either (1) a copy of the Corresponding Source for all the software in the 227 | product that is covered by this License, on a durable physical medium 228 | customarily used for software interchange, for a price no more than your 229 | reasonable cost of physically performing this conveying of source, or (2) 230 | access to copy the Corresponding Source from a network server at no charge. 231 | c) Convey individual copies of the object code with a copy of the written offer 232 | to provide the Corresponding Source. This alternative is allowed only 233 | occasionally and noncommercially, and only if you received the object code 234 | with such an offer, in accord with subsection 6b. 235 | d) Convey the object code by offering access from a designated place (gratis or 236 | for a charge), and offer equivalent access to the Corresponding Source in the 237 | same way through the same place at no further charge. You need not require 238 | recipients to copy the Corresponding Source along with the object code. If 239 | the place to copy the object code is a network server, the Corresponding 240 | Source may be on a different server (operated by you or a third party) that 241 | supports equivalent copying facilities, provided you maintain clear 242 | directions next to the object code saying where to find the Corresponding 243 | Source. Regardless of what server hosts the Corresponding Source, you remain 244 | obligated to ensure that it is available for as long as needed to satisfy 245 | these requirements. 246 | e) Convey the object code using peer-to-peer transmission, provided you inform 247 | other peers where the object code and Corresponding Source of the work are 248 | being offered to the general public at no charge under subsection 6d. 249 | A separable portion of the object code, whose source code is excluded from the 250 | Corresponding Source as a System Library, need not be included in conveying the 251 | object code work. 252 | 253 | A “User Product” is either (1) a “consumer product”, which means any tangible 254 | personal property which is normally used for personal, family, or household 255 | purposes, or (2) anything designed or sold for incorporation into a dwelling. 256 | In determining whether a product is a consumer product, doubtful cases shall be 257 | resolved in favor of coverage. For a particular product received by a particular 258 | user, “normally used” refers to a typical or common use of that class of 259 | product, regardless of the status of the particular user or of the way in which 260 | the particular user actually uses, or expects or is expected to use, the 261 | product. A product is a consumer product regardless of whether the product has 262 | substantial commercial, industrial or non-consumer uses, unless such uses 263 | represent the only significant mode of use of the product. 264 | 265 | “Installation Information” for a User Product means any methods, procedures, 266 | authorization keys, or other information required to install and execute 267 | modified versions of a covered work in that User Product from a modified version 268 | of its Corresponding Source. The information must suffice to ensure that the 269 | continued functioning of the modified object code is in no case prevented or 270 | interfered with solely because modification has been made. 271 | 272 | If you convey an object code work under this section in, or with, or 273 | specifically for use in, a User Product, and the conveying occurs as part of a 274 | transaction in which the right of possession and use of the User Product is 275 | transferred to the recipient in perpetuity or for a fixed term (regardless of 276 | how the transaction is characterized), the Corresponding Source conveyed under 277 | this section must be accompanied by the Installation Information. But this 278 | requirement does not apply if neither you nor any third party retains the 279 | ability to install modified object code on the User Product (for example, the 280 | work has been installed in ROM). 281 | 282 | The requirement to provide Installation Information does not include a 283 | requirement to continue to provide support service, warranty, or updates for a 284 | work that has been modified or installed by the recipient, or for the User 285 | Product in which it has been modified or installed. Access to a network may be 286 | denied when the modification itself materially and adversely affects the 287 | operation of the network or violates the rules and protocols for communication 288 | across the network. 289 | 290 | Corresponding Source conveyed, and Installation Information provided, in accord 291 | with this section must be in a format that is publicly documented (and with an 292 | implementation available to the public in source code form), and must require no 293 | special password or key for unpacking, reading or copying. 294 | 295 | 7. Additional Terms. 296 | “Additional permissions” are terms that supplement the terms of this License by 297 | making exceptions from one or more of its conditions. Additional permissions 298 | that are applicable to the entire Program shall be treated as though they were 299 | included in this License, to the extent that they are valid under applicable 300 | law. If additional permissions apply only to part of the Program, that part may 301 | be used separately under those permissions, but the entire Program remains 302 | governed by this License without regard to the additional permissions. 303 | 304 | When you convey a copy of a covered work, you may at your option remove any 305 | additional permissions from that copy, or from any part of it. (Additional 306 | permissions may be written to require their own removal in certain cases when 307 | you modify the work.) You may place additional permissions on material, added by 308 | you to a covered work, for which you have or can give appropriate copyright 309 | permission. 310 | 311 | Notwithstanding any other provision of this License, for material you add to a 312 | covered work, you may (if authorized by the copyright holders of that material) 313 | supplement the terms of this License with terms: 314 | 315 | a) Disclaiming warranty or limiting liability differently from the terms of 316 | sections 15 and 16 of this License; or 317 | b) Requiring preservation of specified reasonable legal notices or author 318 | attributions in that material or in the Appropriate Legal Notices displayed 319 | by works containing it; or 320 | c) Prohibiting misrepresentation of the origin of that material, or requiring 321 | that modified versions of such material be marked in reasonable ways as 322 | different from the original version; or 323 | d) Limiting the use for publicity purposes of names of licensors or authors of 324 | the material; or 325 | e) Declining to grant rights under trademark law for use of some trade names, 326 | trademarks, or service marks; or 327 | f) Requiring indemnification of licensors and authors of that material by anyone 328 | who conveys the material (or modified versions of it) with contractual 329 | assumptions of liability to the recipient, for any liability that these 330 | contractual assumptions directly impose on those licensors and authors. 331 | All other non-permissive additional terms are considered “further restrictions” 332 | within the meaning of section 10. If the Program as you received it, or any part 333 | of it, contains a notice stating that it is governed by this License along with 334 | a term that is a further restriction, you may remove that term. If a license 335 | document contains a further restriction but permits relicensing or conveying 336 | under this License, you may add to a covered work material governed by the terms 337 | of that license document, provided that the further restriction does not survive 338 | such relicensing or conveying. 339 | 340 | If you add terms to a covered work in accord with this section, you must place, 341 | in the relevant source files, a statement of the additional terms that apply to 342 | those files, or a notice indicating where to find the applicable terms. 343 | 344 | Additional terms, permissive or non-permissive, may be stated in the form of a 345 | separately written license, or stated as exceptions; the above requirements 346 | apply either way. 347 | 348 | 8. Termination. 349 | You may not propagate or modify a covered work except as expressly provided 350 | under this License. Any attempt otherwise to propagate or modify it is void, and 351 | will automatically terminate your rights under this License (including any 352 | patent licenses granted under the third paragraph of section 11). 353 | 354 | However, if you cease all violation of this License, then your license from a 355 | particular copyright holder is reinstated (a) provisionally, unless and until 356 | the copyright holder explicitly and finally terminates your license, and (b) 357 | permanently, if the copyright holder fails to notify you of the violation by 358 | some reasonable means prior to 60 days after the cessation. 359 | 360 | Moreover, your license from a particular copyright holder is reinstated 361 | permanently if the copyright holder notifies you of the violation by some 362 | reasonable means, this is the first time you have received notice of violation 363 | of this License (for any work) from that copyright holder, and you cure the 364 | violation prior to 30 days after your receipt of the notice. 365 | 366 | Termination of your rights under this section does not terminate the licenses of 367 | parties who have received copies or rights from you under this License. If your 368 | rights have been terminated and not permanently reinstated, you do not qualify 369 | to receive new licenses for the same material under section 10. 370 | 371 | 9. Acceptance Not Required for Having Copies. 372 | You are not required to accept this License in order to receive or run a copy of 373 | the Program. Ancillary propagation of a covered work occurring solely as a 374 | consequence of using peer-to-peer transmission to receive a copy likewise does 375 | not require acceptance. However, nothing other than this License grants you 376 | permission to propagate or modify any covered work. These actions infringe 377 | copyright if you do not accept this License. Therefore, by modifying or 378 | propagating a covered work, you indicate your acceptance of this License to do 379 | so. 380 | 381 | 10. Automatic Licensing of Downstream Recipients. 382 | Each time you convey a covered work, the recipient automatically receives a 383 | license from the original licensors, to run, modify and propagate that work, 384 | subject to this License. You are not responsible for enforcing compliance by 385 | third parties with this License. 386 | 387 | An “entity transaction” is a transaction transferring control of an 388 | organization, or substantially all assets of one, or subdividing an 389 | organization, or merging organizations. If propagation of a covered work results 390 | from an entity transaction, each party to that transaction who receives a copy 391 | of the work also receives whatever licenses to the work the party's predecessor 392 | in interest had or could give under the previous paragraph, plus a right to 393 | possession of the Corresponding Source of the work from the predecessor in 394 | interest, if the predecessor has it or can get it with reasonable efforts. 395 | 396 | You may not impose any further restrictions on the exercise of the rights 397 | granted or affirmed under this License. For example, you may not impose a 398 | license fee, royalty, or other charge for exercise of rights granted under this 399 | License, and you may not initiate litigation (including a cross-claim or 400 | counterclaim in a lawsuit) alleging that any patent claim is infringed by 401 | making, using, selling, offering for sale, or importing the Program or any 402 | portion of it. 403 | 404 | 11. Patents. 405 | A “contributor” is a copyright holder who authorizes use under this License of 406 | the Program or a work on which the Program is based. The work thus licensed is 407 | called the contributor's “contributor version”. 408 | 409 | A contributor's “essential patent claims” are all patent claims owned or 410 | controlled by the contributor, whether already acquired or hereafter acquired, 411 | that would be infringed by some manner, permitted by this License, of making, 412 | using, or selling its contributor version, but do not include claims that would 413 | be infringed only as a consequence of further modification of the contributor 414 | version. For purposes of this definition, “control” includes the right to grant 415 | patent sublicenses in a manner consistent with the requirements of this License. 416 | 417 | Each contributor grants you a non-exclusive, worldwide, royalty-free patent 418 | license under the contributor's essential patent claims, to make, use, sell, 419 | offer for sale, import and otherwise run, modify and propagate the contents of 420 | its contributor version. 421 | 422 | In the following three paragraphs, a “patent license” is any express agreement 423 | or commitment, however denominated, not to enforce a patent (such as an express 424 | permission to practice a patent or covenant not to sue for patent infringement). 425 | To “grant” such a patent license to a party means to make such an agreement or 426 | commitment not to enforce a patent against the party. 427 | 428 | If you convey a covered work, knowingly relying on a patent license, and the 429 | Corresponding Source of the work is not available for anyone to copy, free of 430 | charge and under the terms of this License, through a publicly available network 431 | server or other readily accessible means, then you must either (1) cause the 432 | Corresponding Source to be so available, or (2) arrange to deprive yourself of 433 | the benefit of the patent license for this particular work, or (3) arrange, in a 434 | manner consistent with the requirements of this License, to extend the patent 435 | license to downstream recipients. “Knowingly relying” means you have actual 436 | knowledge that, but for the patent license, your conveying the covered work in a 437 | country, or your recipient's use of the covered work in a country, would 438 | infringe one or more identifiable patents in that country that you have reason 439 | to believe are valid. 440 | 441 | If, pursuant to or in connection with a single transaction or arrangement, you 442 | convey, or propagate by procuring conveyance of, a covered work, and grant a 443 | patent license to some of the parties receiving the covered work authorizing 444 | them to use, propagate, modify or convey a specific copy of the covered work, 445 | then the patent license you grant is automatically extended to all recipients of 446 | the covered work and works based on it. 447 | 448 | A patent license is “discriminatory” if it does not include within the scope of 449 | its coverage, prohibits the exercise of, or is conditioned on the non-exercise 450 | of one or more of the rights that are specifically granted under this License. 451 | You may not convey a covered work if you are a party to an arrangement with a 452 | third party that is in the business of distributing software, under which you 453 | make payment to the third party based on the extent of your activity of 454 | conveying the work, and under which the third party grants, to any of the 455 | parties who would receive the covered work from you, a discriminatory patent 456 | license (a) in connection with copies of the covered work conveyed by you (or 457 | copies made from those copies), or (b) primarily for and in connection with 458 | specific products or compilations that contain the covered work, unless you 459 | entered into that arrangement, or that patent license was granted, prior to 28 460 | March 2007. 461 | 462 | Nothing in this License shall be construed as excluding or limiting any implied 463 | license or other defenses to infringement that may otherwise be available to you 464 | under applicable patent law. 465 | 466 | 12. No Surrender of Others' Freedom. 467 | If conditions are imposed on you (whether by court order, agreement or 468 | otherwise) that contradict the conditions of this License, they do not excuse 469 | you from the conditions of this License. If you cannot convey a covered work so 470 | as to satisfy simultaneously your obligations under this License and any other 471 | pertinent obligations, then as a consequence you may not convey it at all. For 472 | example, if you agree to terms that obligate you to collect a royalty for 473 | further conveying from those to whom you convey the Program, the only way you 474 | could satisfy both those terms and this License would be to refrain entirely 475 | from conveying the Program. 476 | 477 | 13. Use with the GNU Affero General Public License. 478 | Notwithstanding any other provision of this License, you have permission to link 479 | or combine any covered work with a work licensed under version 3 of the GNU 480 | Affero General Public License into a single combined work, and to convey the 481 | resulting work. The terms of this License will continue to apply to the part 482 | which is the covered work, but the special requirements of the GNU Affero 483 | General Public License, section 13, concerning interaction through a network 484 | will apply to the combination as such. 485 | 486 | 14. Revised Versions of this License. 487 | The Free Software Foundation may publish revised and/or new versions of the GNU 488 | General Public License from time to time. Such new versions will be similar in 489 | spirit to the present version, but may differ in detail to address new problems 490 | or concerns. 491 | 492 | Each version is given a distinguishing version number. If the Program specifies 493 | that a certain numbered version of the GNU General Public License “or any later 494 | version” applies to it, you have the option of following the terms and 495 | conditions either of that numbered version or of any later version published by 496 | the Free Software Foundation. If the Program does not specify a version number 497 | of the GNU General Public License, you may choose any version ever published by 498 | the Free Software Foundation. 499 | 500 | If the Program specifies that a proxy can decide which future versions of the 501 | GNU General Public License can be used, that proxy's public statement of 502 | acceptance of a version permanently authorizes you to choose that version for 503 | the Program. 504 | 505 | Later license versions may give you additional or different permissions. 506 | However, no additional obligations are imposed on any author or copyright holder 507 | as a result of your choosing to follow a later version. 508 | 509 | 15. Disclaimer of Warranty. 510 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 511 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER 512 | PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER 513 | EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 514 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE 515 | QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 516 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 517 | 518 | 16. Limitation of Liability. 519 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY 520 | COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS 521 | PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, 522 | INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE 523 | THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED 524 | INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE 525 | PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY 526 | HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 527 | 528 | 17. Interpretation of Sections 15 and 16. 529 | If the disclaimer of warranty and limitation of liability provided above cannot 530 | be given local legal effect according to their terms, reviewing courts shall 531 | apply local law that most closely approximates an absolute waiver of all civil 532 | liability in connection with the Program, unless a warranty or assumption of 533 | liability accompanies a copy of the Program in return for a fee. 534 | 535 | END OF TERMS AND CONDITIONS 536 | 537 | How to Apply These Terms to Your New Programs 538 | If you develop a new program, and you want it to be of the greatest possible use 539 | to the public, the best way to achieve this is to make it free software which 540 | everyone can redistribute and change under these terms. 541 | 542 | To do so, attach the following notices to the program. It is safest to attach 543 | them to the start of each source file to most effectively state the exclusion of 544 | warranty; and each file should have at least the “copyright” line and a pointer 545 | to where the full notice is found. 546 | 547 | 548 | Copyright (C) 549 | 550 | This program is free software: you can redistribute it and/or modify 551 | it under the terms of the GNU General Public License as published by 552 | the Free Software Foundation, either version 3 of the License, or 553 | (at your option) any later version. 554 | 555 | This program is distributed in the hope that it will be useful, 556 | but WITHOUT ANY WARRANTY; without even the implied warranty of 557 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 558 | GNU General Public License for more details. 559 | 560 | You should have received a copy of the GNU General Public License 561 | along with this program. If not, see . 562 | Also add information on how to contact you by electronic and paper mail. 563 | 564 | If the program does terminal interaction, make it output a short notice like 565 | this when it starts in an interactive mode: 566 | 567 | Copyright (C) 568 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 569 | This is free software, and you are welcome to redistribute it 570 | under certain conditions; type `show c' for details. 571 | The hypothetical commands `show w' and `show c' should show the appropriate 572 | parts of the General Public License. Of course, your program's commands might be 573 | different; for a GUI interface, you would use an “about box”. 574 | 575 | You should also get your employer (if you work as a programmer) or school, if 576 | any, to sign a “copyright disclaimer” for the program, if necessary. For more 577 | information on this, and how to apply and follow the GNU GPL, see 578 | . 579 | 580 | The GNU General Public License does not permit incorporating your program into 581 | proprietary programs. If your program is a subroutine library, you may consider 582 | it more useful to permit linking proprietary applications with the library. If 583 | this is what you want to do, use the GNU Lesser General Public License instead 584 | of this License. But first, please read 585 | . -------------------------------------------------------------------------------- /src/helpers/query.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * I'm Queue Software Project 5 | * Copyright (C) 2025 imqueue.com 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | * 20 | * If you want to use this code in a closed source (commercial) project, you can 21 | * purchase a proprietary commercial license. Please contact us at 22 | * to get commercial licensing options. 23 | */ 24 | import { js, object } from '@imqueue/js'; 25 | import { CountOptions, Includeable, Transaction } from 'sequelize'; 26 | import { 27 | Association, 28 | FindOptions, 29 | IncludeOptions, 30 | ModelAttributes, 31 | Op, 32 | } from 'sequelize'; 33 | import { Model } from 'sequelize-typescript'; 34 | import { Literal } from 'sequelize/types/utils'; 35 | import { database } from '..'; 36 | import { BaseModel, SaveOptions, Sequelize } from '../BaseModel'; 37 | import { 38 | FieldsInput, 39 | FILTER_OPS, 40 | FilterInput, 41 | OrderByInput, 42 | OrderDirection, 43 | PaginationInput, 44 | } from '../types'; 45 | import { ModelAttributeColumnReferencesOptions } from 'sequelize/types/model'; 46 | 47 | export namespace query { 48 | import isObject = js.isObject; 49 | import isArray = js.isArray; 50 | 51 | const RX_OP = /^\$/; 52 | const RX_LIKE = /%/; 53 | const RX_LTE = /^<=/; 54 | const RX_GTE = /^>=/; 55 | const RX_LT = /^/; 57 | const RX_EQ = /^=/; 58 | const RX_RANGE = /Range$/; 59 | const RX_SPACE = /\s/; 60 | const RX_SQL_CLEAN = /\s+(;|$)/; 61 | const RX_SQL_END = /;?$/; 62 | 63 | interface PureDataFunction { 64 | , T>( 65 | model: typeof Model, 66 | input: T, 67 | attributes?: string[], 68 | ): ModelAttributes; 69 | , T>( 70 | model: typeof Model, 71 | input: T[], 72 | attributes?: string[], 73 | ): ModelAttributes[]; 74 | } 75 | 76 | /** 77 | * Performs safe trimming space characters inside SQL query input string 78 | * and inline it. 79 | * 80 | * @param {string} input - input string 81 | * @return {string} - sanitized string 82 | */ 83 | export function safeSqlSpaceCleanup(input: string): string { 84 | let output = ''; 85 | let opened = false; 86 | let space = false; 87 | 88 | for (const char of input) { 89 | if (!opened && RX_SPACE.test(char)) { 90 | if (!space) { 91 | output += ' '; 92 | } 93 | 94 | space = true; 95 | } else { 96 | output += char; 97 | space = false; 98 | } 99 | 100 | if (char === '\'') { 101 | opened = !opened; 102 | } 103 | } 104 | 105 | return output; 106 | } 107 | 108 | // tslint:disable-next-line:max-line-length 109 | // noinspection JSUnusedGlobalSymbols 110 | /** 111 | * SQL tag used to tag sql queries 112 | * 113 | * @param {string | TemplateStringsArray} sqlQuery 114 | * @param {...any[]} [rest] - anything else 115 | * @return {string} 116 | */ 117 | export function sql( 118 | sqlQuery: string | TemplateStringsArray, 119 | ...rest: any[] 120 | ): string { 121 | return safeSqlSpaceCleanup(String(sqlQuery)) 122 | .replace(RX_SQL_CLEAN, '') 123 | .replace(RX_SQL_END, ';'); 124 | } 125 | 126 | /** 127 | * Extracts pure data from given input data for a given model 128 | * 129 | * @param {typeof Model} model 130 | * @param {any | any[]} input 131 | * @param {string[]} [attributes] 132 | * @return {any} 133 | */ 134 | export const pureData: PureDataFunction = >( 135 | model: typeof Model, 136 | input: T | T[], 137 | attributes?: string[], 138 | ) => { 139 | attributes = attributes || Object.keys(model.rawAttributes || {}); 140 | 141 | if (isArray(input)) { 142 | return (input as T[]).map(inputItem => pureData( 143 | model, 144 | inputItem, 145 | attributes as string[], 146 | )); 147 | } 148 | 149 | return Object.keys(input as any).reduce((res: any, prop: string) => { 150 | if (~(attributes as string[]).indexOf(prop)) { 151 | res[prop] = (input as any)[prop]; 152 | } 153 | 154 | return res; 155 | }, {}); 156 | }; 157 | 158 | // noinspection JSUnusedGlobalSymbols 159 | /** 160 | * Omits non-related properties from a given fields map object associated 161 | * with the given model 162 | * 163 | * @param {typeof Model} model 164 | * @param {any} fields 165 | * @return {string[]} 166 | */ 167 | export function pureFields( 168 | model: typeof BaseModel, 169 | fields: any, 170 | ): string[] | true { 171 | if (!fields) { 172 | return true; 173 | } 174 | 175 | const attributes = Object.keys(model.rawAttributes || {}); 176 | const list = Object.keys( 177 | Object.keys(fields).reduce((res: any, prop: string) => { 178 | if (~attributes.indexOf(prop)) { 179 | res[prop] = fields[prop]; 180 | } 181 | 182 | return res; 183 | }, {}), 184 | ); 185 | 186 | // make sure it contains primary key fields 187 | // that's a tiny trade-off to make sure we won't loose it for a domain 188 | // logic to use 189 | primaryKeys(model).forEach(fieldName => 190 | !~list.indexOf(fieldName) && list.push(fieldName), 191 | ); 192 | 193 | return list; 194 | } 195 | 196 | // noinspection JSUnusedGlobalSymbols 197 | /** 198 | * Returns true if given fields contains associations from given model, 199 | * false otherwise 200 | * 201 | * @param {typeof Model} model 202 | * @param {any} fields 203 | * @return {boolean} 204 | */ 205 | export function needNesting(model: typeof Model, fields: any): boolean { 206 | if (!fields) { 207 | return false; 208 | } 209 | 210 | const associations = Object.keys(model.associations || {}); 211 | const properties = Object.keys(fields); 212 | 213 | if (!associations.length) { 214 | return false; 215 | } 216 | 217 | return associations.some(name => !!~properties.indexOf(name)); 218 | } 219 | 220 | /** 221 | * Returns list of filtered attributes from model through a given list of 222 | * user requested fields 223 | * 224 | * @param {any} attributes 225 | * @param {string[]} fields 226 | * @param {typeof BaseModel} [model] 227 | * @return {string[]} 228 | */ 229 | export function filtered( 230 | attributes: any, 231 | fields: string[], 232 | model?: typeof BaseModel, 233 | ): string[] { 234 | let filteredAttributes = (attributes 235 | ? Object.keys(attributes).filter(attr => ~fields.indexOf(attr)) 236 | : []); 237 | 238 | if (!filteredAttributes.length && model) { 239 | filteredAttributes = primaryKeys(model); 240 | } 241 | 242 | return filteredAttributes; 243 | } 244 | 245 | /** 246 | * Extracts foreign keys existing on the given model for a given list of 247 | * associations and returns as field names list 248 | * 249 | * @param {typeof BaseModel} model 250 | * @param {string[]} relations 251 | * @return {string[]} 252 | */ 253 | export function foreignKeys( 254 | model: typeof BaseModel, 255 | relations: string[], 256 | ): string[] { 257 | const associations: { 258 | [name: string]: Association, 259 | } = model.associations || {}; 260 | 261 | return relations.map((name: string) => { 262 | const association = (associations[name] || {}) as any; 263 | 264 | if (association.source === model && 265 | association.foreignKey && (!( 266 | association.sourceKey || 267 | association.associationType === 'BelongsToMany' 268 | )) 269 | ) { 270 | return association.foreignKey; 271 | } 272 | 273 | return null; 274 | }) 275 | .filter(idField => idField) || []; 276 | } 277 | 278 | /** 279 | * Merges given arrays of scalars making sure they contains unique values 280 | * 281 | * @param {any[][]} args 282 | * @return {any[]} 283 | */ 284 | function arrayMergeUnique(...args: any[][]): any[] { 285 | const result: any[] = []; 286 | 287 | for (const arr of args) { 288 | result.push(...arr); 289 | } 290 | 291 | return result.filter((item, index) => result.indexOf(item) === index); 292 | } 293 | 294 | /** 295 | * Makes sure all merge arguments are merged into a given query 296 | * 297 | * @param {any} [queryOptions] 298 | * @param {...any[]} merge 299 | * @return - merged query options 300 | */ 301 | export function mergeQuery(queryOptions: any = {}, ...merge: any[]): any { 302 | for (const item of merge) { 303 | if (!item) { 304 | continue; 305 | } 306 | 307 | for (const prop of Object.keys(item)) { 308 | const err = `Given ${prop} option is invalid!`; 309 | 310 | if (typeof queryOptions[prop] === 'undefined') { 311 | queryOptions[prop] = item[prop]; 312 | continue; 313 | } 314 | 315 | if (typeof item[prop] === 'undefined') { 316 | continue; 317 | } 318 | 319 | if (isArray(queryOptions[prop])) { 320 | if (!isArray(item[prop])) { 321 | throw new TypeError(err); 322 | } 323 | 324 | for (const element of item[prop]) { 325 | if (!~queryOptions[prop].indexOf(element)) { 326 | queryOptions[prop].push(element); 327 | } 328 | } 329 | continue; 330 | } 331 | 332 | if (isObject(queryOptions[prop])) { 333 | if (!isObject(item[prop])) { 334 | throw new TypeError(err); 335 | } 336 | 337 | Object.assign(queryOptions[prop], item[prop]); 338 | continue; 339 | } 340 | 341 | queryOptions[prop] = item[prop]; 342 | } 343 | } 344 | 345 | return queryOptions; 346 | } 347 | 348 | /** 349 | * Automatically map query joins and requested attributes from a given 350 | * fields map to a given model and returns query find options. Additionally 351 | * will merge all given options as the rest arguments. 352 | * 353 | * @param {Model} model - model to build query for 354 | * @param {any} fields - map of the fields requested by a user or a list 355 | * of fields for a root object (without associations) 356 | * @param {...Array | undefined>} merge - other query parts to 357 | * merge with 358 | * @return {T} - query options type specified by a call 359 | */ 360 | export function autoQuery( 361 | model: any, 362 | fields?: any, 363 | ...merge: (Partial | undefined)[] 364 | ): T { 365 | const queryOptions: any = {}; 366 | const { order } = merge.find((item: any) => item && !!item.order) || 367 | {} as any; 368 | 369 | if (order && isArray(order)) { 370 | // make sure order arg will not break selection 371 | for (const [field] of order) { 372 | if (fields && typeof fields[field] === 'undefined') { 373 | fields[field] = false; 374 | } 375 | } 376 | } 377 | 378 | if (isArray(fields)) { 379 | queryOptions.attributes = filtered( 380 | model.rawAttributes, fields, model, 381 | ); 382 | } else if (fields) { 383 | const fieldNames = Object.keys(fields); 384 | // relations which are requested by a user 385 | const relations = filtered(model.associations, fieldNames); 386 | // attributes which are requested by a user 387 | queryOptions.attributes = arrayMergeUnique( 388 | filtered(model.rawAttributes, fieldNames, model), 389 | foreignKeys(model, relations), 390 | ); 391 | 392 | // we may want to check if the given field is being filtered 393 | // and build where clause for it 394 | Object.assign(queryOptions, toWhereOptions( 395 | queryOptions.attributes.reduce((res: any, attr: string) => { 396 | if (fields[attr] !== false) { 397 | res[attr] = fields[attr]; 398 | } 399 | 400 | return res; 401 | }, {}), 402 | )); 403 | 404 | if (relations.length) { 405 | queryOptions.include = []; 406 | 407 | for (const rel of relations) { 408 | const relModel = model.associations[rel].target; 409 | 410 | // noinspection TypeScriptUnresolvedVariable 411 | queryOptions.include.push({ 412 | model: relModel, 413 | as: model.associations[rel].options.as, 414 | ...autoQuery(relModel, fields[rel]), 415 | } as any); 416 | } 417 | } 418 | } 419 | 420 | if (merge.length) { 421 | mergeQuery(queryOptions, ...merge); 422 | } 423 | 424 | return queryOptions as T; 425 | } 426 | 427 | /** 428 | * Return names of primary key fields for a given model. 429 | * 430 | * @param {typeof BaseModel} model 431 | * @return {string[]} 432 | */ 433 | export function primaryKeys(model: typeof BaseModel): string[] { 434 | const fields = model.rawAttributes; 435 | 436 | return Object.keys(fields).filter(name => fields[name].primaryKey); 437 | } 438 | 439 | /** 440 | * Related entity arguments type used to be passed to createEntity() 441 | * subsequent calls. 442 | * 443 | * @type {RelationArgs} 444 | * @access private 445 | */ 446 | type RelationArgs = [ 447 | any, 448 | any, 449 | FieldsInput | undefined, 450 | Transaction | undefined, 451 | string 452 | ][]; 453 | 454 | /** 455 | * Foreign key map representation, where related property name references 456 | * parent property name. 457 | * 458 | * @type {ForeignKeyMap} 459 | * @access private 460 | */ 461 | interface ForeignKeyMap { 462 | [property: string]: string; 463 | } 464 | 465 | /** 466 | * Returns foreign key map for a given pair of parent model and related 467 | * model. 468 | * 469 | * @param {typeof BaseModel} parent 470 | * @param {typeof BaseModel} model 471 | * @return {ForeignKeyMap} 472 | * @access private 473 | */ 474 | export function foreignKeysMap( 475 | parent: typeof BaseModel, 476 | model: typeof BaseModel, 477 | ): ForeignKeyMap | null { 478 | let found = false; 479 | 480 | const map: ForeignKeyMap = Object.keys(model.rawAttributes) 481 | .reduce((fkMap, name) => { 482 | const relation = 483 | model.rawAttributes[name].references as ModelAttributeColumnReferencesOptions 484 | ; 485 | 486 | if (relation && 487 | relation.model === parent.name && relation.key 488 | ) { 489 | fkMap[name] = relation.key; 490 | found = true; 491 | } 492 | 493 | return fkMap; 494 | }, {} as ForeignKeyMap); 495 | 496 | return found ? map : null; 497 | } 498 | 499 | /** 500 | * Prepares input for a given model and builds found relation arguments 501 | * 502 | * @access private 503 | * @param {any} input 504 | * @param {string[]} relations 505 | * @param {typeof BaseModel} model 506 | * @param {FieldsInput} [fields] 507 | * @param {Transaction} [transaction] 508 | * @param {T} [parent] 509 | * @return {RelationArgs} 510 | */ 511 | function prepareInput>( 512 | input: any, 513 | relations: string[], 514 | model: typeof BaseModel, 515 | fields?: FieldsInput, 516 | transaction?: Transaction, 517 | parent?: T, 518 | ): RelationArgs { 519 | const args: RelationArgs = []; 520 | 521 | for (const relation of relations) { 522 | args.push([ 523 | model.associations[relation].target, 524 | input[relation], 525 | fields ? fields[relation] as FieldsInput : undefined, 526 | transaction, 527 | relation, 528 | ]); 529 | 530 | delete input[relation]; 531 | } 532 | 533 | if (parent) { 534 | const foreignKey = foreignKeysMap( 535 | parent.constructor as typeof BaseModel, 536 | model, 537 | ); 538 | 539 | foreignKey && Object.keys(foreignKey).forEach(property => { 540 | if (!(input as any)[property]) { 541 | (input as any)[property] = (parent as any)[ 542 | foreignKey[property] 543 | ]; 544 | } 545 | }); 546 | } 547 | 548 | return args; 549 | } 550 | 551 | // noinspection JSUnusedGlobalSymbols 552 | /** 553 | * Recursively creates entity and all it's relations from a given input 554 | * using a given model. 555 | * 556 | * @param {T} model - model class to map entity to 557 | * @param {I} input - data input object related to a given model 558 | * @param {FieldsInput} [fields] - fields map to return on created entity 559 | * @param {Transaction} [transaction] - transaction 560 | * @return {Promise>} 561 | */ 562 | export async function createEntity, I>( 563 | model: typeof BaseModel, 564 | input: I, 565 | fields?: FieldsInput, 566 | transaction?: Transaction, 567 | ): Promise> { 568 | return await doCreateEntity( 569 | model, 570 | input, 571 | fields, 572 | transaction, 573 | undefined, 574 | undefined, 575 | false, 576 | !transaction, 577 | ); 578 | } 579 | 580 | /** 581 | * Recursively creates entity and all it's relations from a given input 582 | * using a given model. 583 | * 584 | * @param {T} model 585 | * @param {I | I[]} input 586 | * @param {FieldsInput} [fields] 587 | * @param {Transaction} [transaction] 588 | * @param {string} [parentProperty] 589 | * @param {boolean} [noAppend] 590 | * @param {T} parent 591 | * @param {boolean} doCommit 592 | * @return {Promise>} 593 | * @access private 594 | */ 595 | async function doCreateEntity, I>( 596 | model: typeof BaseModel, 597 | input: I | I[], 598 | fields?: FieldsInput, 599 | transaction?: Transaction, 600 | parentProperty?: string, 601 | parent?: T, 602 | noAppend: boolean = false, 603 | doCommit: boolean = true, 604 | ): Promise> { 605 | transaction = transaction || await database().transaction({ 606 | autocommit: false, 607 | }); 608 | 609 | // todo: this could be optimized through bulk operations 610 | if (isArray(input) && parentProperty && parent) { 611 | parent.appendChild( 612 | parentProperty, 613 | await Promise.all((input as I[]).map(inputItem => 614 | doCreateEntity( 615 | model, inputItem, fields, transaction, 616 | parentProperty, parent, true, doCommit, 617 | )), 618 | ), 619 | ); 620 | 621 | return parent; 622 | } 623 | 624 | if (fields) { 625 | primaryKeys(model).forEach(name => 626 | !fields[name] && (fields[name] = false)); 627 | } 628 | 629 | const fieldNames = Object.keys(input as any); 630 | const relationArgs = prepareInput( 631 | input, filtered(model.associations, fieldNames), 632 | model, fields, transaction, parent); 633 | const entity = new (model as any)(input as any as ModelAttributes); 634 | 635 | await entity.save({ 636 | transaction, 637 | returning: fields 638 | ? filtered(model.rawAttributes, Object.keys(fields), model) 639 | : true, 640 | } as SaveOptions); 641 | 642 | if (!noAppend && parentProperty && parent) { 643 | parent.appendChild(parentProperty, entity); 644 | } 645 | 646 | await Promise.all(relationArgs.map(async args => { 647 | args.push(entity); 648 | await doCreateEntity(...args); 649 | })); 650 | 651 | if (!parent && doCommit) { 652 | await transaction.commit(); 653 | } 654 | 655 | return entity; 656 | } 657 | 658 | // noinspection JSUnusedGlobalSymbols 659 | /** 660 | * Builds and returns count query for a given query options and model. 661 | * 662 | * @param {Model} model 663 | * @param {any} fields 664 | * @param {Array | undefined>} merge 665 | * @return {CountOptions} 666 | */ 667 | export function autoCountQuery( 668 | model: any, 669 | fields?: any, 670 | ...merge: (Partial | undefined)[] 671 | ): CountOptions { 672 | const queryOptions = autoQuery(model, fields, ...merge); 673 | 674 | if (queryOptions.attributes) { 675 | delete queryOptions.attributes; 676 | } 677 | 678 | queryOptions.distinct = true; 679 | queryOptions.col = primaryKeys(model).shift() as string; 680 | 681 | return queryOptions; 682 | } 683 | 684 | // noinspection JSUnusedGlobalSymbols 685 | /** 686 | * Builds proper paging options query part 687 | * 688 | * @param {PaginationInput} [pageOptions] - obtained pagination input 689 | * from remote 690 | * @return {FindOptions} - pagination part of the query 691 | */ 692 | export function toLimitOptions( 693 | pageOptions?: PaginationInput, 694 | ): FindOptions { 695 | const page: FindOptions = {}; 696 | 697 | if (!pageOptions || !+pageOptions.limit) { 698 | return page; 699 | } 700 | 701 | page.offset = 0; 702 | page.limit = 0; 703 | 704 | const count = pageOptions.count || 0; 705 | 706 | if (pageOptions.offset) { 707 | page.offset = pageOptions.offset; 708 | } 709 | 710 | if (pageOptions.limit) { 711 | page.limit = Math.abs(pageOptions.limit); 712 | } 713 | 714 | if (pageOptions.limit < 0) { 715 | if (page.offset === 0) { 716 | page.offset = count - page.limit; 717 | } 718 | 719 | if (page.offset < 0) { 720 | page.offset = 0; 721 | } 722 | } 723 | 724 | return page; 725 | } 726 | 727 | /** 728 | * Ensures order by value is correct or returns default (ASC) if not. This 729 | * would prevent from any possible injections or errors. 730 | * 731 | * @param {any} value 732 | * @return {OrderDirection} 733 | */ 734 | function toOrderDirection(value: any): OrderDirection { 735 | if (String(value).toLocaleLowerCase() === 'desc') { 736 | return OrderDirection.desc; 737 | } else { 738 | return OrderDirection.asc; 739 | } 740 | } 741 | 742 | // noinspection JSUnusedGlobalSymbols 743 | /** 744 | * Constructs order by part of the query from a given input orderBy object 745 | * 746 | * @param {any} orderBy 747 | */ 748 | export function toOrderOptions( 749 | orderBy?: OrderByInput, 750 | ): FindOptions { 751 | const order: FindOptions = {}; 752 | 753 | if (!orderBy) { 754 | return order; 755 | } 756 | 757 | const fields: string[] = Object.keys(orderBy); 758 | 759 | if (!fields.length) { 760 | return order; 761 | } 762 | 763 | order.order = []; 764 | 765 | for (const field of fields) { 766 | (order.order as [string, string][]).push( 767 | [field, toOrderDirection(orderBy[field])], 768 | ); 769 | } 770 | 771 | return order; 772 | } 773 | 774 | // noinspection JSUnusedGlobalSymbols 775 | /** 776 | * Adds or null check to a given where field values 777 | * 778 | * @param {string | string[]} value 779 | * @return {FindOptions} 780 | */ 781 | export function orNull(value: string | string[]): Partial { 782 | if (isArray(value)) { 783 | return { [Op.or]: [null, ...value] } as FindOptions; 784 | } 785 | 786 | return { [Op.or]: [null, value] } as FindOptions; 787 | } 788 | 789 | /** 790 | * Rich filters implementation. Actually by doing this we allow outside 791 | * calls to replicate what sequelize does for us: building rich where 792 | * clauses. 793 | * 794 | * @param {FilterInput} filter 795 | * @return {FindOptions} 796 | */ 797 | function parseFilter(filter: FilterInput): FindOptions { 798 | const clause: FindOptions = {}; 799 | 800 | if (Object.prototype.toString.call(filter) === '[object Object]') { 801 | for (const op of Object.keys(filter)) { 802 | if ((FILTER_OPS as any)[op]) { 803 | (clause as any)[(FILTER_OPS as any)[op]] = parseFilter( 804 | (filter as any)[op], 805 | ); 806 | } else { 807 | (clause as any)[op] = parseFilter((filter as any)[op]); 808 | } 809 | } 810 | } else { 811 | // that's recursive value reached 812 | return filter as any; 813 | } 814 | 815 | return clause; 816 | } 817 | 818 | /** 819 | * This gives us an ability to simulate ILIKE, <, >, <=, >=, = right withing 820 | * given values in the filter. 821 | * 822 | * @param {string} prop 823 | * @param {any} data 824 | * @return {IFindOptions>} 825 | */ 826 | function parseFilterValue(prop: string, data: any): FindOptions { 827 | const value: any = { [prop]: data }; 828 | 829 | if (typeof data !== 'string') { 830 | return value; 831 | } 832 | 833 | if (RX_LIKE.test(data)) { 834 | value[prop] = { [Op.iLike]: data }; 835 | } else if (RX_GTE.test(data)) { 836 | value[prop] = { [Op.gte]: parseValue(data.replace(RX_GTE, '')) }; 837 | } else if (RX_GT.test(data)) { 838 | value[prop] = { [Op.gt]: parseValue(data.replace(RX_GT, '')) }; 839 | } else if (RX_LTE.test(data)) { 840 | value[prop] = { [Op.lte]: parseValue(data.replace(RX_LTE, '')) }; 841 | } else if (RX_LT.test(data)) { 842 | value[prop] = { [Op.lt]: parseValue(data.replace(RX_LT, '')) }; 843 | } else if (RX_EQ.test(data)) { 844 | value[prop] = { [Op.eq]: parseValue(data.replace(RX_EQ, '')) }; 845 | } 846 | 847 | return value as FindOptions; 848 | } 849 | 850 | /** 851 | * Parses a given value 852 | * @param value 853 | */ 854 | function parseValue(value: string) { 855 | try { 856 | const date = new Date(value); 857 | if (date.toISOString() === value) { 858 | return date; 859 | } 860 | } catch (err) { /* not a date */ } 861 | 862 | return ((+value + '') === value) ? +value : value; 863 | } 864 | 865 | /** 866 | * Builds toWhereOptions clause query sub-part for a given filter type 867 | * 868 | * @param {T} filter 869 | * @param {new () => T} inputType 870 | * @return {any} - toWhereOptions clause options 871 | */ 872 | export function toWhereOptions( 873 | filter?: T, 874 | inputType?: new () => T, 875 | ): any { 876 | if (!filter) { 877 | return {}; 878 | } 879 | 880 | object.clearObject(filter); 881 | 882 | let inputData = null; 883 | 884 | if (inputType) { 885 | inputData = new inputType(); 886 | } 887 | 888 | const options: any = {}; 889 | 890 | for (const prop of Object.keys(filter)) { 891 | let data: any = (filter as any)[prop]; 892 | const inputDataProp = inputData && (inputData as any)[prop]; 893 | 894 | if (inputData && inputDataProp) { 895 | const includeData = { 896 | model: inputDataProp.model, 897 | required: true, 898 | ...toWhereOptions(withRangeFilters(data), 899 | inputDataProp.input, 900 | ), 901 | }; 902 | 903 | // NOTE: If included data contains fields which are empty, 904 | // it should be deleted 905 | object.clearObject(filter); 906 | options.include = js.isArray(options.include) 907 | ? options.include.concat(includeData) 908 | : [includeData]; 909 | 910 | continue; 911 | } 912 | 913 | if (isArray(data)) { 914 | if (data.length === 0) { 915 | continue; 916 | } 917 | 918 | if (data.length === 1) { 919 | data = data[0]; 920 | } 921 | } 922 | 923 | if (data === undefined) { 924 | continue; 925 | } 926 | 927 | options.where = options.where || {}; 928 | 929 | if (RX_OP.test(prop)) { 930 | Object.assign( 931 | options.where, 932 | parseFilter({ [prop]: data, } as FilterInput), 933 | ); 934 | } else if (data && data.start && data.end) { // range filter 935 | Object.assign(options.where, { 936 | [prop]: { [Op.between]: [data.start, data.end] }, 937 | }); 938 | } else if ( 939 | Object.prototype.toString.call(data) === '[object Object]' 940 | ) { 941 | Object.assign(options.where, { [prop]: parseFilter(data) }); 942 | } else if (isArray(data)) { 943 | Object.assign(options.where, { 944 | [prop]: buildWhereFromArray(data), 945 | }); 946 | } else { 947 | Object.assign(options.where, parseFilterValue(prop, data)); 948 | } 949 | } 950 | 951 | return options; 952 | } 953 | 954 | /** 955 | * Builds where operations (conditions) from an array of values using 956 | * OR operator between given conditions. 957 | * 958 | * @param {any[]} data 959 | * @return any 960 | */ 961 | export function buildWhereFromArray(data: any[]): any { 962 | const ops: any[] = []; 963 | const ins: any[] = []; 964 | 965 | for (const value of data) { 966 | if (RX_LIKE.test(value)) { 967 | ops.push({ [Op.iLike]: value }); 968 | } else if (RX_GTE.test(value)) { 969 | ops.push({ [Op.gte]: parseValue(value.replace(RX_GTE, '')) }); 970 | } else if (RX_GT.test(value)) { 971 | ops.push({ [Op.gt]: parseValue(value.replace(RX_GT, '')) }); 972 | } else if (RX_LTE.test(value)) { 973 | ops.push({ [Op.lte]: parseValue(value.replace(RX_LTE, '')) }); 974 | } else if (RX_LT.test(value)) { 975 | ops.push({ [Op.lt]: parseValue(value.replace(RX_LT, '')) }); 976 | } else if (RX_EQ.test(value)) { 977 | ops.push({ [Op.eq]: parseValue(value.replace(RX_EQ, '')) }); 978 | } else { 979 | ins.push(value); 980 | } 981 | } 982 | 983 | if (!ops.length && ins.length) { 984 | return { [Op.in]: ins }; 985 | } 986 | 987 | if (ins.length) { 988 | ops.push({ [Op.in]: ins }); 989 | } 990 | 991 | return { [Op.or]: ops }; 992 | } 993 | 994 | // noinspection JSUnusedGlobalSymbols 995 | /** 996 | * Will apply a range rule on a given filters. The rule is simple. If 997 | * filter query contains fields named as [ColumnName]IRange it will try to 998 | * convert those fields to a proper range filter if the value is a proper 999 | * RangeFilter interface as { start: something, end: something } 1000 | * If nothing is matched will simply ignores and keep filtering props 1001 | * as them are. 1002 | * 1003 | * @param {any} filter 1004 | * @return {any} 1005 | */ 1006 | export function withRangeFilters(filter: any) { 1007 | if (!filter) { 1008 | return filter; 1009 | } 1010 | 1011 | for (const prop of Object.keys(filter)) { 1012 | const col = prop.replace(RX_RANGE, ''); 1013 | 1014 | if (col === prop) { // not a range filter 1015 | if (isObject(filter[prop])) { 1016 | withRangeFilters(filter[prop]); 1017 | } 1018 | 1019 | continue; 1020 | } 1021 | 1022 | const signature = Object.keys(filter[prop]) + ''; 1023 | 1024 | if (!~['start,end', 'end,start'].indexOf(signature)) { 1025 | continue; // not a range filter signature 1026 | } 1027 | 1028 | if (filter[col]) { 1029 | throw new TypeError( 1030 | `Only one of filtering options "${col 1031 | }" or "${prop}" can be passed as filtering option!`, 1032 | ); 1033 | } 1034 | 1035 | filter[col] = filter[prop]; 1036 | delete filter[prop]; 1037 | } 1038 | 1039 | return filter; 1040 | } 1041 | 1042 | // noinspection JSUnusedGlobalSymbols 1043 | /** 1044 | * Looks up and returns include options in a given query using an array of 1045 | * given models as a search path 1046 | * 1047 | * @param {FindOptions} queryOptions 1048 | * @param {Array} path 1049 | * @return {IncludeOptions | null} 1050 | */ 1051 | export function getInclude( 1052 | queryOptions: FindOptions, 1053 | path: typeof Model[], 1054 | ): IncludeOptions | null { 1055 | const currentModel = path.shift(); 1056 | 1057 | for (const include of (queryOptions.include as any || [])) { 1058 | const model = (include as IncludeOptions).model; 1059 | 1060 | // noinspection JSIncompatibleTypesComparison 1061 | if (model === currentModel) { 1062 | if (!path.length) { 1063 | return include as IncludeOptions; 1064 | } else { 1065 | return getInclude(include as FindOptions, path); 1066 | } 1067 | } 1068 | } 1069 | 1070 | return null; 1071 | } 1072 | 1073 | // noinspection JSUnusedLocalSymbols,JSCommentMatchesSignature 1074 | /** 1075 | * Returns sequelize Literal build from a given string or string template 1076 | * Actually it's an alias for Sequelize.Literal 1077 | * 1078 | * @example 1079 | * ```typescript 1080 | * const owner = 3; 1081 | * const query = { 1082 | * where: L`(SELECT COUNT(*) FROM "SomeTable" WHERE owner = ${E(id)}) = 0` 1083 | * } 1084 | * ``` 1085 | * @param {TemplateStringsArray | string} str 1086 | * @return {Literal} 1087 | */ 1088 | export function L( 1089 | str: TemplateStringsArray | string, 1090 | ..._: any[] 1091 | ): Literal { 1092 | return Sequelize.literal(str as string); 1093 | } 1094 | 1095 | /** 1096 | * Escapes given argument. If argument is not a number or a string will 1097 | * convert it to 'NULL' 1098 | * 1099 | * @param {any} input 1100 | * @return {string | number} 1101 | */ 1102 | export function E(input: any) { 1103 | if (typeof input === 'number') { 1104 | return +input; 1105 | } 1106 | 1107 | if (typeof input === 'string') { 1108 | return `'${input}'`; 1109 | } 1110 | 1111 | return 'NULL'; 1112 | } 1113 | 1114 | // noinspection JSUnusedGlobalSymbols 1115 | /** 1116 | * Removes given properties from the given object 1117 | * 1118 | * @param {any} obj 1119 | * @param {...string[]} props 1120 | * @return {any} 1121 | */ 1122 | export function skip(obj: any, ...props: string[]) { 1123 | if (!obj) { 1124 | return obj; 1125 | } 1126 | 1127 | for (const prop of props) { 1128 | delete obj[prop]; 1129 | } 1130 | 1131 | return obj; 1132 | } 1133 | 1134 | // noinspection JSUnusedGlobalSymbols 1135 | /** 1136 | * Traverses given query object, lookups for includes matching 1137 | * the given arguments of include options and overrides those are matching 1138 | * by model and alias with the provided option. 1139 | * 1140 | * @param {FindOptions | CountOptions} queryOptions 1141 | * @param {...IncludeOptions[]} options 1142 | * @return {FindOptions | CountOptions} 1143 | */ 1144 | export function overrideJoin( 1145 | queryOptions: FindOptions | CountOptions, 1146 | ...options: IncludeOptions[] 1147 | ): FindOptions | CountOptions { 1148 | if (!(queryOptions && queryOptions.include) || !options.length) { 1149 | return queryOptions; 1150 | } 1151 | 1152 | for (const { model, ...fields } of options) { 1153 | let found = false; 1154 | 1155 | for (const include of queryOptions.include as IncludeOptions[]) { 1156 | const as = fields.as; 1157 | 1158 | if (include as any === model || ( 1159 | include.model === model && (!as || as === include.as) 1160 | )) { 1161 | Object.assign(include, fields); 1162 | found = true; 1163 | } 1164 | } 1165 | 1166 | if (!found) { 1167 | (queryOptions.include as any[]).push({ 1168 | model, 1169 | ...fields, 1170 | } as Includeable); 1171 | } 1172 | } 1173 | 1174 | return queryOptions; 1175 | } 1176 | } 1177 | -------------------------------------------------------------------------------- /src/BaseModel.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * @imqueue/sequelize - Sequelize ORM refines for @imqueue 3 | * 4 | * I'm Queue Software Project 5 | * Copyright (C) 2025 imqueue.com 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | * 20 | * If you want to use this code in a closed source (commercial) project, you can 21 | * purchase a proprietary commercial license. Please contact us at 22 | * to get commercial licensing options. 23 | */ 24 | /** 25 | * This modules provides several additional features on top of 26 | * original Sequelize ORM implementation: 27 | * 1. Support of views. To define a model as a view in database, use 28 | * @View(CREATE_VIEW_SQL: string) decorator factory 29 | * @example 30 | * ~~~typescript 31 | * @View( 32 | * `CREATE OR REPLACE VIEW "ProductRevenue" AS 33 | * SELECT "productId" as "id", SUM("payment") as "revenue" 34 | * FROM "Order" GROUP BY "productId"` 35 | * ) 36 | * export class ProductRevenue extends BaseModel { 37 | * @Column(DataType.BIGINT) 38 | * @PrimaryKey 39 | * public readonly id: number; 40 | * 41 | * @Column(DataType.NUMBER({ decimals: 2, precision: 12 }})) 42 | * public readonly revenue: number; 43 | * } 44 | * ~~~ 45 | * All views would be automatically synced after all tables synced, when 46 | * Sequelize.sync() called. 47 | * 48 | * 2. Support of { returning: [field1, field2, ...] } instead of 49 | * { returning: boolean }. This gives an ability to fetch and return only 50 | * requested properties during insert/update operations. By the way it 51 | * would require to cast the proper options type passed to corresponding 52 | * sequelize operation. Those types have been mimic in this module, so 53 | * them should be exported from it: 54 | * @example 55 | * ~~~typescript 56 | * import { SaveOptions, Scope, UpdateOptions } from './orm'; 57 | * // 58 | * // ... init models and somewhere inside async function: 59 | * // 60 | * const scope = new Scope({ 61 | * name: 'test', 62 | * description: 'Test Scope', 63 | * schema: {}, 64 | * }); 65 | * // take into account type casting to prevent TS errors: 66 | * await scope.save({ returning: ['id', 'name'] } as SaveOptions); 67 | * console.log(JSON.stringify(scope)); // {"id":2,"name":"test"} 68 | * // or 69 | * const [count, scopes] = await Scope.update({ name: "TEST" }, { 70 | * where: { id: 2 }, 71 | * // take into account type casting to prevent TS errors: 72 | * returning: ['id', 'name'] as UpdateOptions, 73 | * }); 74 | * console.log(JSON.stringify(scopes)); // {"id":2,"name":"TEST"} 75 | * ~~~ 76 | * This behavior gives an ability to reduce data exchange between DB and 77 | * application as well as between app and external source as far as 78 | * serialized model will contain only requested data (if any remote source 79 | * applicable). 80 | * So, whenever you found TS error about returning option on your code, 81 | * simply cast options to the same type name as error states, but import 82 | * this type from this module. 83 | */ 84 | import { Graph } from './Graph'; 85 | 86 | export * from 'sequelize-typescript'; 87 | 88 | import Promise = require('bluebird'); 89 | import { 90 | BuildOptions as BuildOptionsOrigin, 91 | BulkCreateOptions as BulkCreateOptionsOrigin, 92 | CreateOptions as CreateOptionsOrigin, 93 | DropOptions, 94 | FindOptions as FindOptionsOrigin, 95 | Identifier, IncludeOptions, 96 | InitOptions as InitOptionsOrigin, 97 | ModelAttributes, 98 | ModelOptions, ModelType, 99 | QueryInterface as QueryInterfaceOrigin, 100 | QueryOptions as QueryOptionsOrigin, 101 | QueryOptionsWithType, 102 | QueryOptionsWithWhere, 103 | SaveOptions as InstanceSaveOptionsOrigin, 104 | SyncOptions as SyncOptionsOrigin, 105 | UpdateOptions as UpdateOptionsOrigin, 106 | UpsertOptions as UpsertOptionsOrigin, 107 | WhereOptions, 108 | } from 'sequelize'; 109 | import { 110 | DataType, 111 | Model, 112 | Sequelize as SequelizeOrigin, 113 | } from 'sequelize-typescript'; 114 | import QueryTypes = require('sequelize/types/query-types'); 115 | import { 116 | ColumnIndexOptions, 117 | IDynamicViewDefineOptions, 118 | RX_MATCHER, 119 | RX_NAME_MATCHER, 120 | ViewParams, 121 | } from './decorators'; 122 | import { query } from './helpers'; 123 | import sql = query.sql; 124 | import E = query.E; 125 | import { ModelAttributeColumnOptions } from 'sequelize/types/model'; 126 | import { TableName } from 'sequelize/types/dialects/abstract/query-interface'; 127 | 128 | export type Modify = Pick> & R; 129 | 130 | /** 131 | * Original toJSON method from sequelize's Model class. 132 | */ 133 | const toJSON = Model.prototype.toJSON; 134 | const RX_CREATE_VIEW = new RegExp('create\\s+(or\\s+replace\\s+)?' + 135 | '(materialized\\s+)?view\\s+(.*?)\\s+as', 'i'); 136 | const RX_SQL_END = /;$/; 137 | const RX_RETURNING = /returning\s+\*/i; 138 | const ALIAS_PATH_DELIMITER = '->'; 139 | 140 | /** 141 | * Extends original SyncOptions from Sequelize to add view support 142 | */ 143 | export interface SyncOptions extends SyncOptionsOrigin { 144 | treatAsView?: boolean; 145 | withNoViews?: boolean; 146 | withoutDrop?: boolean; 147 | } 148 | export interface FindOptions extends FindOptionsOrigin { 149 | viewParams?: ViewParams; 150 | } 151 | export interface InitOptions 152 | extends InitOptionsOrigin, IDynamicViewDefineOptions {} 153 | export interface ReturningOptions { 154 | returning?: boolean | string[]; 155 | } 156 | export interface WithIncludeMap extends InitOptions { 157 | includeMap?: { 158 | [propertyName: string]: WithIncludeMap & IncludeOptions & FindOptions; 159 | }; 160 | includeNames?: string[]; 161 | parent: WithIncludeMap; 162 | } 163 | // noinspection JSUnusedGlobalSymbols 164 | export type IModelClass> = new () => T; 165 | 166 | // noinspection JSUnusedGlobalSymbols 167 | export type UpsertOptions = 168 | Modify; 169 | // noinspection JSUnusedGlobalSymbols 170 | export type BuildOptions = 171 | Modify; 172 | // noinspection JSUnusedGlobalSymbols 173 | export type BulkCreateOptions = 174 | Modify; 175 | // noinspection JSUnusedGlobalSymbols 176 | export type QueryOptions = 177 | Modify; 178 | // noinspection JSUnusedGlobalSymbols 179 | export type UpdateOptions = 180 | Modify; 181 | // noinspection JSUnusedGlobalSymbols 182 | export type CreateOptions = 183 | Modify; 184 | // noinspection JSUnusedGlobalSymbols 185 | export type SaveOptions = 186 | Modify; 187 | 188 | /** 189 | * Extends original QueryInterface from sequelize to add support of create/drop 190 | * views. 191 | */ 192 | export interface QueryInterface extends QueryInterfaceOrigin { 193 | sequelize: Sequelize; 194 | dropView(viewName: string, options?: DropOptions): Promise; 195 | createView(viewName: string, viewDefinition: string): Promise; 196 | } 197 | 198 | const castNumber = (value: any) => +value; 199 | const NUMBERS_MAP = new Map number>([ 200 | [DataType.BIGINT.name, castNumber], 201 | [DataType.NUMBER.name, castNumber], 202 | [DataType.INTEGER.name, castNumber], 203 | [DataType.FLOAT.name, castNumber], 204 | [DataType.REAL.name, castNumber], 205 | [DataType.DECIMAL.name, castNumber], 206 | [DataType.MEDIUMINT.name, castNumber], 207 | [DataType.SMALLINT.name, castNumber], 208 | [DataType.TINYINT.name, castNumber], 209 | [DataType.DOUBLE.name, castNumber], 210 | ]); 211 | 212 | function fixReturningOptions(options?: ReturningOptions) { 213 | if (options && 214 | options.returning && 215 | Array.isArray(options.returning) && 216 | !options.returning.length 217 | ) { 218 | options.returning = false; 219 | } 220 | } 221 | 222 | /** 223 | * Overrides queryInterface behavior to add support of views definition 224 | * 225 | * @param {QueryInterface} queryInterface 226 | */ 227 | function override(queryInterface: QueryInterfaceOrigin): QueryInterface { 228 | 229 | const { 230 | insert, 231 | upsert, 232 | bulkInsert, 233 | update, 234 | bulkUpdate, 235 | bulkDelete, 236 | select, 237 | increment, 238 | rawSelect, 239 | queryGenerator, 240 | } = queryInterface as QueryInterface; 241 | const del = (queryInterface as QueryInterface).delete; 242 | 243 | /** 244 | * Inserts a new record 245 | */ 246 | (queryInterface as QueryInterface).insert = function( 247 | instance: Model, 248 | tableName: string, 249 | values: object, 250 | options?: QueryOptions, 251 | ): Promise { 252 | fixReturningOptions(options); 253 | 254 | return insert.call(this, 255 | instance, tableName, values, options); 256 | }; 257 | 258 | /** 259 | * Inserts or Updates a record in the database 260 | */ 261 | (queryInterface as QueryInterface).upsert = function( 262 | tableName: string, 263 | values: object, 264 | updateValues: object, 265 | model: typeof Model, 266 | options?: QueryOptions 267 | ): Promise { 268 | fixReturningOptions(options); 269 | 270 | return upsert.call(this, 271 | tableName, values, updateValues, model, options); 272 | }; 273 | 274 | /** 275 | * Inserts multiple records at once 276 | */ 277 | (queryInterface as QueryInterface).bulkInsert = function( 278 | tableName: string, 279 | records: object[], 280 | options?: QueryOptions, 281 | attributes?: Record, 282 | ): Promise { 283 | fixReturningOptions(options); 284 | 285 | return bulkInsert.call(this, 286 | tableName, records, options, attributes); 287 | }; 288 | 289 | /** 290 | * Updates a row 291 | */ 292 | (queryInterface as any).update = function( 293 | instance: M, 294 | tableName: TableName, 295 | values: object, 296 | identifier: WhereOptions, 297 | options?: QueryOptions 298 | ): Promise { 299 | fixReturningOptions(options); 300 | 301 | return update.call(this, 302 | instance, tableName, values, identifier, options); 303 | }; 304 | 305 | /** 306 | * Updates multiple rows at once 307 | */ 308 | (queryInterface as QueryInterface).bulkUpdate = function( 309 | tableName: string, 310 | values: object, 311 | identifier: WhereOptions, 312 | options?: QueryOptions, 313 | attributes?: string[] | string 314 | ): Promise { 315 | fixReturningOptions(options); 316 | 317 | return bulkUpdate.call(this, 318 | tableName, values, identifier, options, attributes); 319 | }; 320 | 321 | /** 322 | * Deletes a row 323 | */ 324 | (queryInterface as QueryInterface).delete = function( 325 | instance: Model | null, 326 | tableName: string, 327 | identifier: WhereOptions, 328 | options?: QueryOptions, 329 | ): Promise { 330 | fixReturningOptions(options); 331 | 332 | return del.call(this, 333 | instance, tableName, identifier, options); 334 | }; 335 | 336 | /** 337 | * Deletes multiple rows at once 338 | */ 339 | (queryInterface as QueryInterface).bulkDelete = function( 340 | tableName: TableName, 341 | identifier: WhereOptions, 342 | options?: QueryOptions, 343 | model?: ModelType, 344 | ): Promise { 345 | fixReturningOptions(options); 346 | 347 | return bulkDelete.call(this, 348 | tableName, identifier, options, model); 349 | }; 350 | 351 | /** 352 | * Increments a row value 353 | */ 354 | (queryInterface as QueryInterface).increment = function( 355 | instance: Model, 356 | tableName: string, 357 | values: object, 358 | identifier: WhereOptions, 359 | options?: QueryOptions 360 | ): Promise { 361 | fixReturningOptions(options); 362 | 363 | return increment.call(this, 364 | instance, tableName, values, identifier, options); 365 | }; 366 | 367 | /** 368 | * Drops view from database 369 | * 370 | * @param {string} viewName - view name to drop 371 | * @param {DropOptions} [options] - drop operation options 372 | */ 373 | (queryInterface as QueryInterface).dropView = function( 374 | viewName: string, 375 | options: DropOptions = {}, 376 | ) { 377 | const dropViewSql = `DROP VIEW IF EXISTS "${ 378 | viewName}"${options.cascade ? ' CASCADE' : ''}`; 379 | 380 | return this.sequelize.query( 381 | dropViewSql, 382 | this.sequelize.options, 383 | ); 384 | }; 385 | 386 | /** 387 | * Creates view in a database. Makes sure given view name corresponds to 388 | * the name inside given create SQL query. 389 | * 390 | * @param {string} viewName - view name to create 391 | * @param {string} viewDefinition - raw sql query to create the view 392 | */ 393 | (queryInterface as QueryInterface).createView = function( 394 | viewName: string, 395 | viewDefinition: string, 396 | ) { 397 | const rx = new RegExp( 398 | `\\s*create\\s+(or\\s+replace\\s+)?(temp|temporary\s+)?view\\s+"?${ 399 | viewName 400 | }"?\\s+`, 401 | 'i', 402 | ); 403 | 404 | if (!rx.test(viewDefinition)) { 405 | throw new TypeError( 406 | 'Given view definition does not match given view name', 407 | ); 408 | } 409 | 410 | return this.sequelize.query( 411 | viewDefinition, 412 | this.sequelize.options, 413 | ); 414 | }; 415 | 416 | /** 417 | * Returns selected rows 418 | */ 419 | (queryInterface as QueryInterface).select = function( 420 | model: ModelType | null, 421 | tableName: TableName, 422 | options?: QueryOptionsWithWhere, 423 | ): Promise { 424 | fixReturningOptions(options as any); 425 | 426 | return select.call(this, 427 | model, tableName, options); 428 | }; 429 | 430 | /** 431 | * Increments a row value 432 | */ 433 | (queryInterface as QueryInterface).increment = function( 434 | instance: Model, 435 | tableName: string, 436 | values: object, 437 | identifier: WhereOptions, 438 | options?: QueryOptions 439 | ): Promise { 440 | fixReturningOptions(options); 441 | 442 | return increment.call(this, 443 | instance, tableName, values, identifier, options); 444 | }; 445 | 446 | /** 447 | * Selects raw without parsing the string into an object 448 | */ 449 | (queryInterface as QueryInterface).rawSelect = function( 450 | tableName: TableName, 451 | options: QueryOptionsWithWhere, 452 | attributeSelector: string | string[], 453 | model?: ModelType, 454 | ): Promise { 455 | fixReturningOptions(options as any); 456 | 457 | return rawSelect.call(this, 458 | tableName, options, attributeSelector, model); 459 | }; 460 | 461 | /** 462 | * Override queryGenerator behavior for DynamicViews on select queries 463 | */ 464 | const { selectQuery } = queryGenerator as any; 465 | 466 | // takes into account dynamic view can be included 467 | function fixIncludes( 468 | options: WithIncludeMap & IncludeOptions, 469 | sqlQuery: string, 470 | parentViewParams?: ViewParams, 471 | path: string = '', 472 | ): string { 473 | const model = options.model as unknown as typeof BaseModel; 474 | const modelOptions: InitOptions = ( 475 | (model || {} as any).options || {} as any 476 | ) as InitOptions; 477 | 478 | path = path 479 | ? `${path}${ALIAS_PATH_DELIMITER}${options.as}` 480 | : (options.as || ''); 481 | 482 | if (modelOptions.isDynamicView && ( 483 | options.viewParams || parentViewParams 484 | )) { 485 | 486 | const viewParams = Object.assign({}, 487 | parentViewParams || {}, 488 | options.viewParams || {}, 489 | ); 490 | 491 | sqlQuery = sqlQuery.replace( 492 | `JOIN "${model.getTableName()}" AS "${path}"`, 493 | `JOIN (${model.getViewDefinition(viewParams, true) 494 | .replace(RX_SQL_END, '') 495 | }) AS "${path}"`, 496 | ); 497 | } 498 | 499 | if (options.includeMap) { 500 | for (const prop of Object.keys(options.includeMap)) { 501 | sqlQuery = fixIncludes( 502 | options.includeMap[prop], 503 | sqlQuery, 504 | parentViewParams, 505 | path, 506 | ); 507 | } 508 | } 509 | 510 | return sqlQuery; 511 | } 512 | 513 | (queryGenerator as any).selectQuery = ( 514 | tableName: string, 515 | options: FindOptions, 516 | model: typeof BaseModel, 517 | ) => { 518 | const modelOptions: InitOptions = model.options as InitOptions; 519 | let sqlQuery = selectQuery.call( 520 | queryGenerator as any, 521 | tableName, options, model, 522 | ); 523 | const viewParams = Object.assign({}, modelOptions.viewParams); 524 | 525 | if (modelOptions.isDynamicView && options.viewParams) { 526 | Object.assign(viewParams, options.viewParams); 527 | 528 | sqlQuery = sqlQuery.replace( 529 | `FROM "${tableName}" AS`, 530 | `FROM (${model.getViewDefinition(viewParams, true) 531 | .replace(RX_SQL_END, '') 532 | }) AS`, 533 | ); 534 | } 535 | 536 | return fixIncludes( 537 | options as WithIncludeMap, 538 | sqlQuery, 539 | options.viewParams, 540 | ); 541 | }; 542 | 543 | return queryInterface as QueryInterface; 544 | } 545 | 546 | /** 547 | * Overriding sequelize behavior to support views 548 | */ 549 | export class Sequelize extends SequelizeOrigin { 550 | 551 | /** 552 | * Returns an instance of QueryInterface. 553 | * Supports views. 554 | * 555 | * @return {QueryInterface} 556 | */ 557 | public getQueryInterface(): QueryInterface { 558 | const self: any = this; 559 | 560 | super.getQueryInterface(); 561 | 562 | if (typeof self.queryInterface.dropView !== 'function') { 563 | self.queryInterface = override(self.queryInterface); 564 | } 565 | 566 | return self.queryInterface; 567 | } 568 | 569 | /** 570 | * Overrides original sequelize define method. Supports views. 571 | * 572 | * @param {string} modelName 573 | * @param {ModelAttributes} attributes 574 | * @param {ModelOptions} [options] 575 | * @return {typeof BaseModel} 576 | */ 577 | public define( 578 | modelName: string, 579 | attributes: ModelAttributes, 580 | options?: ModelOptions, 581 | ): any { 582 | const opts: any = options || {}; 583 | 584 | opts.modelName = modelName; 585 | opts.sequelize = this; 586 | 587 | const model = class extends BaseModel {}; 588 | 589 | (model as any).init(attributes, opts); 590 | 591 | return model as any; 592 | } 593 | 594 | /** 595 | * Sync all defined models to the DB. Including views! 596 | * 597 | * @param {SyncOptions} [options] 598 | * @return {Promise} 599 | */ 600 | public sync(options?: SyncOptions): Promise { 601 | const withViews = !options || (options && !options.withNoViews); 602 | const syncResult = super.sync(options); 603 | 604 | syncResult.then(async result => { 605 | await this.syncIndices(options); 606 | return result; 607 | }); 608 | 609 | return (withViews 610 | ? syncResult.then(() => this.syncViews()) 611 | : syncResult 612 | ) as unknown as Promise; 613 | } 614 | 615 | /** 616 | * Synchronizes indices defined for models 617 | * 618 | * @param {SyncOptions} options 619 | * @return {Promise} 620 | */ 621 | public syncIndices(options?: SyncOptions): Promise { 622 | return Promise.all(this.getModelsWithIndices().map(model => 623 | model.syncIndices(options))); 624 | } 625 | 626 | /** 627 | * Syncs all defined views to the DB. 628 | * 629 | * @return {Promise} 630 | */ 631 | public syncViews(options?: SyncOptions): Promise { 632 | const views = this.getViews(); 633 | 634 | return Promise.all(views.map((view) => view.syncView(options))); 635 | } 636 | 637 | public getModelsWithIndices() { 638 | const models: typeof BaseModel[] = []; 639 | 640 | (this as any).modelManager.models.forEach((model: any) => { 641 | if (model && model.options && 642 | model.options.indices && model.options.indices.length 643 | ) { 644 | models.push(model); 645 | } 646 | }); 647 | 648 | return models; 649 | } 650 | 651 | /** 652 | * Returns list of all defined as views models. 653 | * 654 | * @return {Array} 655 | */ 656 | public getViews(): typeof BaseModel[] { 657 | const views: typeof BaseModel[] = []; 658 | 659 | (this as any).modelManager.models.forEach((model: any) => { 660 | if (model && model.options && model.options.treatAsView) { 661 | views.push(model); 662 | } 663 | }); 664 | 665 | return views; 666 | } 667 | 668 | /** 669 | * Overriding native query() method to support { returning: string[] } 670 | * option for queries in a proper way 671 | * 672 | * @param {string | { query: string, values: any[] }} sqlQuery 673 | * @param {QueryOptions} options 674 | */ 675 | public query( 676 | sqlQuery: string | { query: string, values: any[] }, 677 | options?: QueryOptions | QueryOptionsWithType, 678 | ): Promise { 679 | if (options && 680 | Array.isArray((options as QueryOptions).returning) && 681 | ((options as QueryOptions).returning as string[]).length 682 | ) { 683 | const sqlText = (typeof sqlQuery === 'string' 684 | ? sqlQuery 685 | : sqlQuery.query 686 | ).replace(RX_RETURNING, `RETURNING ${ 687 | ((options as QueryOptions).returning as string[]) 688 | .map(field => `"${field}"`).join(', ') 689 | }`); 690 | 691 | if (typeof sqlQuery === 'string') { 692 | sqlQuery = sqlText; 693 | } else { 694 | sqlQuery.query = sqlText; 695 | } 696 | } 697 | 698 | const original = super.query; 699 | 700 | return original.call(this, sqlQuery, options).then((entities: any) => { 701 | if (!(entities && Array.isArray(entities) && entities.length)) { 702 | return entities; 703 | } 704 | 705 | for (const entity of entities) { 706 | // noinspection SuspiciousTypeOfGuard 707 | if (entity instanceof BaseModel && options) { 708 | // noinspection TypeScriptUnresolvedVariable 709 | (entity as any)._options.returning = ( 710 | options as QueryOptions 711 | ).returning; 712 | } 713 | } 714 | 715 | return entities; 716 | }); 717 | } 718 | } 719 | 720 | /** 721 | * Base Model class extends native sequelize Model class 722 | */ 723 | export abstract class BaseModel extends Model> { 724 | 725 | /** 726 | // noinspection JSUnusedGlobalSymbols 727 | * Override native drop method to add support of view drops 728 | * 729 | * @param {DropOptions} options 730 | * @return {Promise} 731 | */ 732 | public static drop(options?: DropOptions): Promise { 733 | const self: any = this; 734 | const method = self.options && self.options.treatAsView 735 | ? 'dropView' 736 | : 'dropTable'; 737 | 738 | // noinspection TypeScriptUnresolvedVariable 739 | return self.QueryInterface[method]( 740 | self.getTableName(), 741 | options, 742 | ); 743 | } 744 | 745 | // noinspection JSUnusedGlobalSymbols 746 | /** 747 | * Sync this Model to the DB, that is create the table. Upon success, the 748 | * callback will be called with the model instance (this). 749 | * Supports views. 750 | * 751 | * @param {SyncOptions} [options] 752 | */ 753 | public static sync(options?: SyncOptions): Promise { 754 | if ((this as any).options && (this as any).options.treatAsView) { 755 | // all views skipped until all tables defined 756 | return Promise.resolve(); 757 | } 758 | 759 | return super.sync(options) as unknown as Promise; 760 | } 761 | 762 | /** 763 | * Syncs view to the DB. 764 | * 765 | * @param {SyncOptions} [options] 766 | * @return {Promise} 767 | */ 768 | public static syncView(options?: SyncOptions): Promise { 769 | const self: any = this; 770 | // noinspection TypeScriptUnresolvedVariable 771 | const queryInterface = self.QueryInterface || self.queryInterface; 772 | 773 | if (options && options.withoutDrop) { 774 | return queryInterface.createView( 775 | self.getTableName(), 776 | self.getViewDefinition(), 777 | ); 778 | } 779 | 780 | return queryInterface.dropView(self.getTableName()) 781 | .then(() => queryInterface.createView( 782 | self.getTableName(), 783 | self.getViewDefinition(), 784 | )); 785 | } 786 | 787 | /** 788 | * Returns view definition SQL string. 789 | * 790 | * @param {ViewParams} [viewParams] 791 | * @param {boolean} [asQuery] 792 | * @return {string} 793 | */ 794 | public static getViewDefinition( 795 | viewParams: ViewParams = {}, 796 | asQuery: boolean = false, 797 | ) { 798 | const self: any = this; 799 | let viewDef: string = self.options.viewDefinition || ''; 800 | 801 | viewParams = Object.assign({}, 802 | self.options.viewParams, 803 | viewParams || {}, 804 | ); 805 | 806 | if (self.options.isDynamicView) { 807 | (viewDef.match(RX_MATCHER) || []).forEach(param => { 808 | // noinspection JSUnusedLocalSymbols 809 | const [_, name] = (param.match(RX_NAME_MATCHER) || ['', '']); 810 | const RX_PARAM = new RegExp(`@\{${name}\}`, 'g'); 811 | 812 | viewDef = viewDef.replace(RX_PARAM, E(viewParams[name]) + ''); 813 | }); 814 | } 815 | 816 | if (asQuery) { 817 | viewDef = viewDef.replace(RX_CREATE_VIEW, ''); 818 | } 819 | 820 | return sql(viewDef); 821 | } 822 | 823 | /** 824 | * Synchronizes configured indices on this model 825 | * 826 | * @param {SyncOptions} options 827 | * @return {Promise} 828 | */ 829 | public static syncIndices(options?: SyncOptions): Promise { 830 | const indices: { 831 | column: string; 832 | options: ColumnIndexOptions; 833 | }[] = (this.options as any).indices; 834 | 835 | return Promise.all(indices.map((indexOptions, i) => 836 | this.syncIndex(indexOptions.column, indexOptions.options, i + 1))); 837 | } 838 | 839 | /** 840 | * Builds and executes index definition SQL query for the given 841 | * column and options. Position is used for auto index naming when 842 | * index name is auto-generated 843 | * 844 | * @param {string} column 845 | * @param {ColumnIndexOptions} options 846 | * @param {number} position 847 | * @return {Promise} 848 | */ 849 | public static syncIndex( 850 | column: string, 851 | options: ColumnIndexOptions, 852 | position: number, 853 | ) { 854 | const self: any = this; 855 | const indexName: string = options.name || 856 | `${this.getTableName()}_${column}_idx${position}`; 857 | const chain = Promise.resolve(true); 858 | // noinspection TypeScriptUnresolvedVariable 859 | const queryInterface = self.QueryInterface || self.queryInterface; 860 | 861 | if (!options.safe) { 862 | chain.then(() => queryInterface.sequelize.query(` 863 | DROP INDEX${options.concurrently 864 | ? ' CONCURRENTLY' : ''} IF EXISTS "${indexName}" 865 | `)); 866 | } 867 | 868 | // tslint:disable-next-line:max-line-length 869 | // noinspection TypeScriptUnresolvedVariable,PointlessBooleanExpressionJS 870 | chain.then(() => queryInterface.sequelize.query(` 871 | CREATE${options.unique 872 | ? ' UNIQUE' : ''} INDEX${options.concurrently 873 | ? ' CONCURRENTLY' : ''}${options.safe 874 | ? ' IF NOT EXISTS' : ''} "${indexName}"${options.method 875 | ? ` USING ${options.method}` : ''} ON "${ 876 | this.getTableName()}" (${options.expression 877 | ? `(${options.expression})` : `"${column}"`})${options.collation 878 | ? ` COLLATE ${options.collation}` : ''}${options.opClass 879 | ? ` ${options.opClass}` : ''}${options.order 880 | ? ` ${options.order}` : ''}${options.nullsFirst === true 881 | ? ' NULLS FIRST' : (options.nullsFirst === false 882 | ? ' NULLS LAST' : '')}${options.tablespace 883 | ? ` TABLESPACE ${options.tablespace}` : ''}${ 884 | options.predicate 885 | ? ` WHERE ${options.predicate}` : ''} 886 | `)); 887 | 888 | return chain; 889 | } 890 | 891 | // Make sure finders executed on views properly map numeric types 892 | /** 893 | * Search for multiple instances. 894 | * 895 | * @see {Sequelize#query} 896 | */ 897 | public static findAll( 898 | options?: FindOptions, 899 | ): Promise { 900 | const method = super.findAll; 901 | const original = method.call(this, options); 902 | 903 | if (!(this as any).options.treatAsView) { 904 | return original; 905 | } 906 | 907 | return original.then((result: any[]) => { 908 | if (result && !Array.isArray(result)) { 909 | return (result as BaseModel).fixNumbers() as any as M; 910 | } else if (result) { 911 | result.map((entity: any) => 912 | entity.fixNumbers() as M); 913 | } 914 | 915 | return result as any as M; 916 | }); 917 | } 918 | 919 | // noinspection JSAnnotator 920 | /** 921 | * Search for a single instance by its primary key. This applies LIMIT 1, 922 | * so the listener will always be called with a single instance. 923 | */ 924 | public static findByPk( 925 | identifier?: Identifier, 926 | options?: Omit, 927 | ): Promise { 928 | const method = super.findByPk; 929 | const original = method.call(this, identifier, options); 930 | 931 | if (!(this as any).options.treatAsView) { 932 | return original; 933 | } 934 | 935 | return original.then((result: any) => { 936 | if (result) { 937 | result.fixNumbers(); 938 | } 939 | 940 | return result as any as M; 941 | }); 942 | } 943 | 944 | // noinspection JSAnnotator 945 | /** 946 | * Search for a single instance. This applies LIMIT 1, so the listener will 947 | * always be called with a single instance. 948 | */ 949 | public static findOne( 950 | options?: FindOptions, 951 | ): Promise { 952 | const method = super.findOne; 953 | const original = method.call(this, options); 954 | 955 | if (!(this as any).options.treatAsView) { 956 | return original; 957 | } 958 | 959 | return original.then((result: any) => { 960 | if (result) { 961 | result.fixNumbers(); 962 | } 963 | 964 | return result as any as M; 965 | }); 966 | } 967 | 968 | // noinspection JSUnusedGlobalSymbols 969 | /** 970 | * Restores native serialization state, clearing returning options 971 | * saved during insert/update query execution 972 | * 973 | * @return {BaseModel} 974 | */ 975 | public restoreSerialization() { 976 | // noinspection TypeScriptUnresolvedVariable 977 | delete (this as any)._options.returning; 978 | 979 | return this; 980 | } 981 | 982 | /** 983 | * Appends child node to this entity as if it was joined through query 984 | * 985 | * @param {string} name 986 | * @param {any} data 987 | * @return {BaseModel} 988 | */ 989 | public appendChild(name: string, data: any) { 990 | // noinspection TypeScriptUnresolvedVariable 991 | const returning = (this as any)._options.returning; 992 | 993 | if (returning && 994 | Array.isArray(returning) && 995 | !~returning.indexOf(name) 996 | ) { 997 | returning.push(name); 998 | } 999 | 1000 | (this as any)[name] = data; 1001 | 1002 | return this; 1003 | } 1004 | 1005 | /** 1006 | * Serializes this model instance to JSON 1007 | */ 1008 | public toJSON(): any { 1009 | const serialized: any = toJSON.call(this); 1010 | const props = Object.keys(this); 1011 | // noinspection TypeScriptUnresolvedVariable 1012 | const returning: boolean | string[] = (this as any)._options.returning; 1013 | 1014 | for (const prop of props) { 1015 | this.verifyProperty(prop, serialized); 1016 | } 1017 | 1018 | if (Array.isArray(returning)) { 1019 | const serializedProps = Object.keys(serialized); 1020 | 1021 | for (const prop of serializedProps) { 1022 | if (!~(returning as string[]).indexOf(prop)) { 1023 | delete serialized[prop]; 1024 | } 1025 | } 1026 | } 1027 | 1028 | return serialized; 1029 | } 1030 | 1031 | // noinspection JSUnusedGlobalSymbols 1032 | /** 1033 | * Casts numeric types to numbers for this model if it 1034 | * was not properly done during query selection and mapping. 1035 | * This may occurs sometimes when dealing with Views 1036 | * 1037 | * @return {BaseModel} 1038 | */ 1039 | public fixNumbers(): BaseModel { 1040 | const model = this.sequelize.models[ 1041 | this.constructor.name 1042 | ] as any as BaseModel; 1043 | const columns = Object.keys((model as any).rawAttributes); 1044 | 1045 | for (const column of columns) { 1046 | const value = (this as any)[column]; 1047 | 1048 | if (value === undefined || value === null) { 1049 | continue; 1050 | } 1051 | 1052 | const cast = NUMBERS_MAP.get( 1053 | (model as any).rawAttributes[column].type.constructor.name, 1054 | ); 1055 | 1056 | if (cast) { 1057 | (this as any)[column] = cast(value); 1058 | } 1059 | } 1060 | 1061 | return this; 1062 | } 1063 | 1064 | /** 1065 | * Makes sure given property properly serialized 1066 | * 1067 | * @access private 1068 | * @param {string} prop 1069 | * @param {any} serialized 1070 | */ 1071 | private verifyProperty(prop: string, serialized: any) { 1072 | // add more skipping props if needed... 1073 | if (~['__eagerlyLoadedAssociations'].indexOf(prop)) { 1074 | return ; 1075 | } 1076 | 1077 | const val = (this as any)[prop]; 1078 | 1079 | if (!serialized[prop] && val !== this && val instanceof Model) { 1080 | serialized[prop] = toJSON.call(val); 1081 | } 1082 | 1083 | if (val instanceof Array) { 1084 | this.verifyArray(val, prop, serialized); 1085 | } 1086 | } 1087 | 1088 | /** 1089 | * Makes sure given array property properly serialized 1090 | * 1091 | * @access private 1092 | * @param {Array} arr 1093 | * @param {string} prop 1094 | * @param {any} serialized 1095 | */ 1096 | private verifyArray(arr: any[], prop: string, serialized: any) { 1097 | if (!serialized[prop]) { 1098 | serialized[prop] = []; 1099 | } 1100 | 1101 | for (let i = 0; i < arr.length; i++) { 1102 | const val = (this as any)[prop][i]; 1103 | 1104 | if (val instanceof Model) { 1105 | serialized[prop][i] = toJSON.call(val); 1106 | } 1107 | 1108 | serialized[prop][i] = val && val.toJSON 1109 | ? val.toJSON() 1110 | : JSON.parse(JSON.stringify(val)); 1111 | } 1112 | } 1113 | 1114 | /** 1115 | * Returns graph representation of the model associations. 1116 | * This would allow to traverse model association paths and detect 1117 | * cycles. 1118 | * 1119 | * @param {Graph} [graph] 1120 | * @return {Graph} 1121 | */ 1122 | public static toGraph( 1123 | graph = new Graph(), 1124 | ): Graph { 1125 | if (!graph.hasVertex(this)) { 1126 | graph.addVertex(this); 1127 | } 1128 | 1129 | for (const field of Object.keys(this.associations)) { 1130 | const relation = this.associations[field] as any; 1131 | const { target, options } = relation; 1132 | const through = options && options.through && options.through.model; 1133 | 1134 | if (through && graph.hasEdge(this, through)) { 1135 | continue; 1136 | } 1137 | 1138 | if (through) { 1139 | graph.addEdge(this, through); 1140 | through.toGraph(graph); 1141 | 1142 | if (target && graph.hasEdge(through, target)) { 1143 | continue; 1144 | } 1145 | 1146 | if (target && !graph.hasVertex(target)) { 1147 | graph.addEdge(through, target); 1148 | target.toGraph(graph); 1149 | } 1150 | } else { 1151 | if (target && graph.hasEdge(this, target)) { 1152 | continue; 1153 | } 1154 | 1155 | if (target) { 1156 | graph.addEdge(this, target); 1157 | target.toGraph(graph); 1158 | } 1159 | } 1160 | } 1161 | 1162 | return graph; 1163 | } 1164 | } 1165 | --------------------------------------------------------------------------------