├── src ├── __tests__ │ ├── unit │ │ ├── README.md │ │ └── sequelize.datasource.unit.ts │ ├── integration │ │ ├── README.md │ │ └── repository.integration.ts │ └── fixtures │ │ ├── application.ts │ │ ├── models │ │ ├── index.ts │ │ ├── patient.model.ts │ │ ├── category.model.ts │ │ ├── test.model.ts │ │ ├── appointment.model.ts │ │ ├── product.model.ts │ │ ├── programming-language.model.ts │ │ ├── todo-list.model.ts │ │ ├── todo.model.ts │ │ ├── book.model.ts │ │ ├── doctor.model.ts │ │ ├── developer.model.ts │ │ ├── task.model.ts │ │ └── user.model.ts │ │ ├── repositories │ │ ├── index.ts │ │ ├── patient.repository.ts │ │ ├── category.repository.ts │ │ ├── product.repository.ts │ │ ├── appointment.repository.ts │ │ ├── programming-language.repository.ts │ │ ├── task.repository.ts │ │ ├── book.repository.ts │ │ ├── todo.repository.ts │ │ ├── todo-list.repository.ts │ │ ├── user.repository.ts │ │ ├── developer.repository.ts │ │ └── doctor.repository.ts │ │ ├── controllers │ │ ├── index.ts │ │ ├── book-category.controller.ts │ │ ├── todo-todo-list.controller.ts │ │ ├── test.controller.base.ts │ │ ├── todo-list-todo.controller.ts │ │ ├── user-todo-list.controller.ts │ │ ├── doctor-patient.controller.ts │ │ ├── patient.controller.ts │ │ ├── todo.controller.ts │ │ ├── doctor.controller.ts │ │ ├── product.controller.ts │ │ ├── todo-list.controller.ts │ │ ├── developer.controller.ts │ │ ├── transaction.controller.ts │ │ ├── user.controller.ts │ │ ├── task.controller.ts │ │ ├── book.controller.ts │ │ ├── category.controller.ts │ │ └── programming-languange.controller.ts │ │ └── datasources │ │ ├── primary.datasource.ts │ │ ├── secondary.datasource.ts │ │ └── config.ts ├── sequelize │ ├── index.ts │ ├── utils.ts │ ├── operator-translation.ts │ ├── sequelize.model.ts │ ├── connector-mapping.ts │ └── sequelize.datasource.base.ts ├── index.ts ├── component.ts ├── keys.ts ├── types.ts └── release_notes │ ├── mymarkdown.ejs │ ├── release-notes.js │ └── post-processing.js ├── .prettierignore ├── .husky ├── pre-commit ├── commit-msg └── prepare-commit-msg ├── .mocharc.json ├── .yo-rc.json ├── .eslintignore ├── .prettierrc ├── typedoc.json ├── docs ├── tutorial │ └── index.md └── index.md ├── tsconfig.json ├── .eslintrc.js ├── commitlint.config.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── workflows │ └── ci.yml ├── PULL_REQUEST_TEMPLATE.md ├── CONTRIBUTING.md └── CODE_OF_CONDUCT.md ├── .vscode ├── tasks.json ├── settings.json └── launch.json ├── mkdocs.yml ├── LICENSE ├── .gitignore ├── DEVELOPING.md ├── .cz-config.js ├── package.json ├── CHANGELOG.md └── README.md /src/__tests__/unit/README.md: -------------------------------------------------------------------------------- 1 | # Unit tests 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | *.json 3 | docs 4 | site 5 | -------------------------------------------------------------------------------- /src/__tests__/integration/README.md: -------------------------------------------------------------------------------- 1 | # Integration tests 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm test 5 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit 5 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "exit": true, 3 | "recursive": true, 4 | "require": "source-map-support/register" 5 | } 6 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "@loopback/cli": { 3 | "packageManager": "npm", 4 | "version": "4.1.3" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/sequelize/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sequelize.datasource.base'; 2 | export * from './sequelize.repository.base'; 3 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | exec < /dev/tty && npx cz --hook || true 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | coverage/ 4 | .eslintrc.js 5 | .cz-config.js 6 | commitlint.config.js 7 | docs/ 8 | site/ 9 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './component'; 2 | export * from './keys'; 3 | export * from './sequelize'; 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "singleQuote": true, 4 | "printWidth": 80, 5 | "trailingComma": "all", 6 | "arrowParens": "avoid" 7 | } 8 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["src/index.ts"], 3 | "out": "docs/api-reference", 4 | "plugin": ["typedoc-plugin-markdown"], 5 | "readme": "none" 6 | } 7 | -------------------------------------------------------------------------------- /docs/tutorial/index.md: -------------------------------------------------------------------------------- 1 | # Basics 2 | 3 | {% 4 | include-markdown "../../README.md" 5 | start="" 6 | end='' 7 | %} 8 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Overview 3 | --- 4 | 5 | {% 6 | include-markdown "../README.md" 7 | start="" 8 | end='' 9 | %} 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "extends": "@loopback/build/config/tsconfig.common.json", 4 | "compilerOptions": { 5 | "outDir": "dist", 6 | "rootDir": "src" 7 | }, 8 | "include": ["src"] 9 | } 10 | -------------------------------------------------------------------------------- /src/sequelize/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Function to check if the `value` is actually an js object (`{}`) 3 | * @param value Target value to check 4 | * @returns `true` is it is an object `false` otherwise 5 | */ 6 | export const isTruelyObject = (value?: unknown) => { 7 | return typeof value === 'object' && !Array.isArray(value) && value !== null; 8 | }; 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@loopback/eslint-config', 3 | rules: { 4 | 'no-extra-boolean-cast': 'off', 5 | '@typescript-eslint/interface-name-prefix': 'off', 6 | 'no-prototype-builtins': 'off', 7 | }, 8 | parserOptions: { 9 | project: './tsconfig.json', 10 | tsconfigRootDir: __dirname, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/component.ts: -------------------------------------------------------------------------------- 1 | import {Component, ContextTags, injectable} from '@loopback/core'; 2 | import {LB4SequelizeComponentBindings} from './keys'; 3 | 4 | // Configure the binding for LB4SequelizeComponent 5 | @injectable({ 6 | tags: {[ContextTags.KEY]: LB4SequelizeComponentBindings.COMPONENT}, 7 | }) 8 | export class LB4SequelizeComponent implements Component { 9 | constructor() {} 10 | } 11 | -------------------------------------------------------------------------------- /src/keys.ts: -------------------------------------------------------------------------------- 1 | import {BindingKey, CoreBindings} from '@loopback/core'; 2 | import {LB4SequelizeComponent} from './component'; 3 | 4 | /** 5 | * Binding keys used by this component. 6 | */ 7 | export namespace LB4SequelizeComponentBindings { 8 | export const COMPONENT = BindingKey.create( 9 | `${CoreBindings.COMPONENTS}.LB4SequelizeComponent`, 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'header-max-length': [2, 'always', 100], 5 | 'body-leading-blank': [2, 'always'], 6 | 'footer-leading-blank': [0, 'always'], 7 | 'references-empty': [2, 'never'], 8 | 'body-empty': [2, 'never'], 9 | }, 10 | parserPreset: { 11 | parserOpts: { 12 | issuePrefixes: ['GH-'], 13 | }, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface defining the component's options object 3 | */ 4 | export interface LB4SequelizeComponentOptions { 5 | // Add the definitions here 6 | } 7 | 8 | /** 9 | * Default options for the component 10 | */ 11 | export const DEFAULT_LOOPBACK4_SEQUELIZE_OPTIONS: LB4SequelizeComponentOptions = 12 | { 13 | // Specify the values here 14 | }; 15 | 16 | /** 17 | * Sequelize Transaction type 18 | */ 19 | export {Transaction} from 'sequelize'; 20 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/application.ts: -------------------------------------------------------------------------------- 1 | import {BootMixin} from '@loopback/boot'; 2 | import {ApplicationConfig} from '@loopback/core'; 3 | import {RepositoryMixin} from '@loopback/repository'; 4 | import {RestApplication} from '@loopback/rest'; 5 | 6 | export class SequelizeSandboxApplication extends BootMixin( 7 | RepositoryMixin(RestApplication), 8 | ) { 9 | constructor(options: ApplicationConfig = {}) { 10 | super(options); 11 | this.projectRoot = __dirname; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './appointment.model'; 2 | export * from './book.model'; 3 | export * from './category.model'; 4 | export * from './developer.model'; 5 | export * from './doctor.model'; 6 | export * from './patient.model'; 7 | export * from './product.model'; 8 | export * from './programming-language.model'; 9 | export * from './task.model'; 10 | export * from './todo-list.model'; 11 | export * from './todo.model'; 12 | export * from './user.model'; 13 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/repositories/index.ts: -------------------------------------------------------------------------------- 1 | export * from './appointment.repository'; 2 | export * from './book.repository'; 3 | export * from './category.repository'; 4 | export * from './developer.repository'; 5 | export * from './doctor.repository'; 6 | export * from './patient.repository'; 7 | export * from './product.repository'; 8 | export * from './programming-language.repository'; 9 | export * from './task.repository'; 10 | export * from './todo-list.repository'; 11 | export * from './todo.repository'; 12 | export * from './user.repository'; 13 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/repositories/patient.repository.ts: -------------------------------------------------------------------------------- 1 | import {inject} from '@loopback/core'; 2 | import {SequelizeCrudRepository} from '../../../sequelize'; 3 | import {PrimaryDataSource} from '../datasources/primary.datasource'; 4 | import {Patient, PatientRelations} from '../models/index'; 5 | 6 | export class PatientRepository extends SequelizeCrudRepository< 7 | Patient, 8 | typeof Patient.prototype.id, 9 | PatientRelations 10 | > { 11 | constructor(@inject('datasources.primary') dataSource: PrimaryDataSource) { 12 | super(Patient, dataSource); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/repositories/category.repository.ts: -------------------------------------------------------------------------------- 1 | import {inject} from '@loopback/core'; 2 | import {SequelizeCrudRepository} from '../../../sequelize'; 3 | import {PrimaryDataSource} from '../datasources/primary.datasource'; 4 | import {Category, CategoryRelations} from '../models/index'; 5 | 6 | export class CategoryRepository extends SequelizeCrudRepository< 7 | Category, 8 | typeof Category.prototype.id, 9 | CategoryRelations 10 | > { 11 | constructor(@inject('datasources.primary') dataSource: PrimaryDataSource) { 12 | super(Category, dataSource); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/repositories/product.repository.ts: -------------------------------------------------------------------------------- 1 | import {inject} from '@loopback/core'; 2 | import {SequelizeCrudRepository} from '../../../sequelize'; 3 | import {SecondaryDataSource} from '../datasources/secondary.datasource'; 4 | import {Product, ProductRelations} from '../models'; 5 | 6 | export class ProductRepository extends SequelizeCrudRepository< 7 | Product, 8 | typeof Product.prototype.id, 9 | ProductRelations 10 | > { 11 | constructor( 12 | @inject('datasources.secondary') dataSource: SecondaryDataSource, 13 | ) { 14 | super(Product, dataSource); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/repositories/appointment.repository.ts: -------------------------------------------------------------------------------- 1 | import {inject} from '@loopback/core'; 2 | import {SequelizeCrudRepository} from '../../../sequelize'; 3 | import {PrimaryDataSource} from '../datasources/primary.datasource'; 4 | import {Appointment, AppointmentRelations} from '../models/index'; 5 | 6 | export class AppointmentRepository extends SequelizeCrudRepository< 7 | Appointment, 8 | typeof Appointment.prototype.id, 9 | AppointmentRelations 10 | > { 11 | constructor(@inject('datasources.primary') dataSource: PrimaryDataSource) { 12 | super(Appointment, dataSource); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/models/patient.model.ts: -------------------------------------------------------------------------------- 1 | import {Entity, model, property} from '@loopback/repository'; 2 | 3 | @model() 4 | export class Patient extends Entity { 5 | @property({ 6 | type: 'number', 7 | id: true, 8 | generated: true, 9 | }) 10 | id?: number; 11 | 12 | @property({ 13 | type: 'string', 14 | required: true, 15 | }) 16 | name: string; 17 | 18 | constructor(data?: Partial) { 19 | super(data); 20 | } 21 | } 22 | 23 | export interface PatientRelations { 24 | // describe navigational properties here 25 | } 26 | 27 | export type PatientWithRelations = Patient & PatientRelations; 28 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/models/category.model.ts: -------------------------------------------------------------------------------- 1 | import {Entity, model, property} from '@loopback/repository'; 2 | 3 | @model() 4 | export class Category extends Entity { 5 | @property({ 6 | type: 'number', 7 | id: true, 8 | generated: true, 9 | }) 10 | id?: number; 11 | 12 | @property({ 13 | type: 'string', 14 | required: true, 15 | }) 16 | name: string; 17 | 18 | constructor(data?: Partial) { 19 | super(data); 20 | } 21 | } 22 | 23 | export interface CategoryRelations { 24 | // describe navigational properties here 25 | } 26 | 27 | export type CategoryWithRelations = Category & CategoryRelations; 28 | -------------------------------------------------------------------------------- /src/sequelize/operator-translation.ts: -------------------------------------------------------------------------------- 1 | import {Operators} from '@loopback/repository'; 2 | import {Op} from 'sequelize'; 3 | 4 | /** 5 | * @key Operator used in loopback 6 | * @value Equivalent operator in Sequelize 7 | */ 8 | export const operatorTranslations: { 9 | [key in Operators]?: symbol; 10 | } = { 11 | eq: Op.eq, 12 | gt: Op.gt, 13 | gte: Op.gte, 14 | lt: Op.lt, 15 | lte: Op.lte, 16 | neq: Op.ne, 17 | between: Op.between, 18 | inq: Op.in, 19 | nin: Op.notIn, 20 | like: Op.like, 21 | nlike: Op.notLike, 22 | ilike: Op.iLike, 23 | nilike: Op.notILike, 24 | regexp: Op.regexp, 25 | and: Op.and, 26 | or: Op.or, 27 | }; 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/repositories/programming-language.repository.ts: -------------------------------------------------------------------------------- 1 | import {inject} from '@loopback/core'; 2 | import {SequelizeCrudRepository} from '../../../sequelize'; 3 | import {PrimaryDataSource} from '../datasources/primary.datasource'; 4 | import { 5 | ProgrammingLanguage, 6 | ProgrammingLanguageRelations, 7 | } from '../models/index'; 8 | 9 | export class ProgrammingLanguageRepository extends SequelizeCrudRepository< 10 | ProgrammingLanguage, 11 | typeof ProgrammingLanguage.prototype.id, 12 | ProgrammingLanguageRelations 13 | > { 14 | constructor(@inject('datasources.primary') dataSource: PrimaryDataSource) { 15 | super(ProgrammingLanguage, dataSource); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/sequelize/sequelize.model.ts: -------------------------------------------------------------------------------- 1 | import {AnyObject, Entity} from '@loopback/repository'; 2 | import {Model} from 'sequelize'; 3 | 4 | export class SequelizeModel extends Model implements Entity { 5 | getId() { 6 | // Method implementation not required as this class is just being used as type not a constructor 7 | return null; 8 | } 9 | getIdObject(): Object { 10 | // Method implementation not required as this class is just being used as type not a constructor 11 | return {}; 12 | } 13 | toObject(_options?: AnyObject | undefined): Object { 14 | // Method implementation not required as this class is just being used as type not a constructor 15 | return {}; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/models/test.model.ts: -------------------------------------------------------------------------------- 1 | import {Entity, model, property} from '@loopback/repository'; 2 | 3 | export const eventTableName = 'tbl_event'; 4 | @model({ 5 | name: eventTableName, 6 | }) 7 | export class Event extends Entity { 8 | @property({ 9 | type: 'number', 10 | id: true, 11 | generated: true, 12 | }) 13 | id?: number; 14 | 15 | constructor(data?: Partial) { 16 | super(data); 17 | } 18 | } 19 | 20 | @model() 21 | export class Box extends Entity { 22 | @property({ 23 | type: 'number', 24 | id: true, 25 | generated: true, 26 | }) 27 | id?: number; 28 | 29 | constructor(data?: Partial) { 30 | super(data); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/repositories/task.repository.ts: -------------------------------------------------------------------------------- 1 | import {inject} from '@loopback/core'; 2 | import {SequelizeCrudRepository} from '../../../sequelize'; 3 | import {PrimaryDataSource} from '../datasources/primary.datasource'; 4 | import {Task, TaskRelations} from '../models/index'; 5 | 6 | export class TaskRepository extends SequelizeCrudRepository< 7 | Task, 8 | typeof Task.prototype.id, 9 | TaskRelations 10 | > { 11 | constructor(@inject('datasources.primary') dataSource: PrimaryDataSource) { 12 | super(Task, dataSource); 13 | } 14 | 15 | protected getDefaultFnRegistry() { 16 | return { 17 | ...super.getDefaultFnRegistry(), 18 | customAlias: () => Math.random(), 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/models/appointment.model.ts: -------------------------------------------------------------------------------- 1 | import {Entity, model, property} from '@loopback/repository'; 2 | 3 | @model() 4 | export class Appointment extends Entity { 5 | @property({ 6 | type: 'number', 7 | id: true, 8 | generated: true, 9 | }) 10 | id?: number; 11 | 12 | @property({ 13 | type: 'number', 14 | }) 15 | doctorId?: number; 16 | 17 | @property({ 18 | type: 'number', 19 | }) 20 | patientId?: number; 21 | 22 | constructor(data?: Partial) { 23 | super(data); 24 | } 25 | } 26 | 27 | export interface AppointmentRelations { 28 | // describe navigational properties here 29 | } 30 | 31 | export type AppointmentWithRelations = Appointment & AppointmentRelations; 32 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/models/product.model.ts: -------------------------------------------------------------------------------- 1 | import {Entity, model, property} from '@loopback/repository'; 2 | 3 | export const TableInSecondaryDB = 'products'; 4 | @model({ 5 | name: TableInSecondaryDB, 6 | }) 7 | export class Product extends Entity { 8 | @property({ 9 | type: 'number', 10 | id: true, 11 | generated: true, 12 | }) 13 | id?: number; 14 | 15 | @property({ 16 | type: 'string', 17 | required: true, 18 | }) 19 | name: string; 20 | 21 | @property({ 22 | type: 'number', 23 | }) 24 | price: number; 25 | 26 | constructor(data?: Partial) { 27 | super(data); 28 | } 29 | } 30 | 31 | export interface ProductRelations { 32 | // describe navigational properties here 33 | } 34 | 35 | export type ProductWithRelations = Product & ProductRelations; 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: 5 | - master 6 | permissions: 7 | contents: write 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-python@v4 14 | with: 15 | python-version: 3.x 16 | - name: Setup Node 17 | uses: actions/setup-node@v3.6.0 18 | with: 19 | node-version: '18.x' 20 | - name: Bootstrap 21 | run: npm ci 22 | - name: Install mkdocs deps/plugins 23 | run: | 24 | pip install mkdocs-material 25 | pip install mkdocs-include-markdown-plugin 26 | - name: Create the docs directory locally in CI 27 | run: npx typedoc 28 | - name: Deploy 🚀 29 | run: mkdocs gh-deploy --force 30 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './book-category.controller'; 2 | export * from './book.controller'; 3 | export * from './category.controller'; 4 | export * from './developer.controller'; 5 | export * from './doctor-patient.controller'; 6 | export * from './doctor.controller'; 7 | export * from './patient.controller'; 8 | export * from './product.controller'; 9 | export * from './programming-languange.controller'; 10 | export * from './task.controller'; 11 | export * from './test.controller.base'; 12 | export * from './todo-list-todo.controller'; 13 | export * from './todo-list.controller'; 14 | export * from './todo-todo-list.controller'; 15 | export * from './todo.controller'; 16 | export * from './transaction.controller'; 17 | export * from './user-todo-list.controller'; 18 | export * from './user.controller'; 19 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/models/programming-language.model.ts: -------------------------------------------------------------------------------- 1 | import {Entity, model, property} from '@loopback/repository'; 2 | 3 | @model() 4 | export class ProgrammingLanguage extends Entity { 5 | @property({ 6 | type: 'number', 7 | id: true, 8 | generated: true, 9 | }) 10 | id?: number; 11 | 12 | @property({ 13 | type: 'string', 14 | required: true, 15 | }) 16 | name: string; 17 | 18 | @property({ 19 | type: 'string', 20 | hidden: true, 21 | }) 22 | secret: string; 23 | 24 | constructor(data?: Partial) { 25 | super(data); 26 | } 27 | } 28 | 29 | export interface ProgrammingLanguageRelations { 30 | // describe navigational properties here 31 | } 32 | 33 | export type ProgrammingLanguageWithRelations = ProgrammingLanguage & 34 | ProgrammingLanguageRelations; 35 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Watch and Compile Project", 8 | "type": "shell", 9 | "command": "npm", 10 | "args": ["--silent", "run", "build:watch"], 11 | "group": { 12 | "kind": "build", 13 | "isDefault": true 14 | }, 15 | "problemMatcher": "$tsc-watch" 16 | }, 17 | { 18 | "label": "Build, Test and Lint", 19 | "type": "shell", 20 | "command": "npm", 21 | "args": ["--silent", "run", "test:dev"], 22 | "group": { 23 | "kind": "test", 24 | "isDefault": true 25 | }, 26 | "problemMatcher": ["$tsc", "$eslint-compact", "$eslint-stylish"] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/models/todo-list.model.ts: -------------------------------------------------------------------------------- 1 | import {Entity, hasMany, model, property} from '@loopback/repository'; 2 | import {Todo} from './todo.model'; 3 | 4 | @model() 5 | export class TodoList extends Entity { 6 | @property({ 7 | type: 'number', 8 | id: true, 9 | generated: true, 10 | }) 11 | id?: number; 12 | 13 | @property({ 14 | type: 'string', 15 | required: true, 16 | }) 17 | title: string; 18 | 19 | @hasMany(() => Todo, { 20 | keyTo: 'todoListId', 21 | }) 22 | todos: Todo[]; 23 | 24 | @property({ 25 | type: 'number', 26 | }) 27 | user?: number; 28 | 29 | constructor(data?: Partial) { 30 | super(data); 31 | } 32 | } 33 | 34 | export interface TodoListRelations { 35 | // describe navigational properties here 36 | } 37 | 38 | export type TodoListWithRelations = TodoList & TodoListRelations; 39 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/models/todo.model.ts: -------------------------------------------------------------------------------- 1 | import {belongsTo, Entity, model, property} from '@loopback/repository'; 2 | import {TodoList, TodoListWithRelations} from './todo-list.model'; 3 | 4 | @model() 5 | export class Todo extends Entity { 6 | @property({ 7 | type: 'number', 8 | id: true, 9 | generated: true, 10 | }) 11 | id?: number; 12 | 13 | @property({ 14 | type: 'string', 15 | required: true, 16 | }) 17 | title: string; 18 | 19 | @property({ 20 | name: 'is_complete', 21 | type: 'boolean', 22 | }) 23 | isComplete?: boolean; 24 | 25 | @belongsTo(() => TodoList, {name: 'todoList'}, {name: 'todo_list_id'}) 26 | todoListId: number; 27 | 28 | constructor(data?: Partial) { 29 | super(data); 30 | } 31 | } 32 | 33 | export interface TodoRelations { 34 | todoList?: TodoListWithRelations; 35 | } 36 | 37 | export type TodoWithRelations = Todo & TodoRelations; 38 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.rulers": [80], 3 | "editor.tabCompletion": "on", 4 | "editor.tabSize": 2, 5 | "editor.trimAutoWhitespace": true, 6 | "editor.formatOnSave": true, 7 | "editor.codeActionsOnSave": { 8 | "source.organizeImports": true, 9 | "source.fixAll.eslint": true 10 | }, 11 | 12 | "files.exclude": { 13 | "**/.DS_Store": true, 14 | "**/.git": true, 15 | "**/.hg": true, 16 | "**/.svn": true, 17 | "**/CVS": true, 18 | }, 19 | "files.insertFinalNewline": true, 20 | "files.trimTrailingWhitespace": true, 21 | 22 | "typescript.tsdk": "./node_modules/typescript/lib", 23 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": false, 24 | "typescript.preferences.quoteStyle": "single", 25 | "eslint.run": "onSave", 26 | "eslint.nodePath": "./node_modules", 27 | "eslint.validate": [ 28 | "javascript", 29 | "typescript" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/models/book.model.ts: -------------------------------------------------------------------------------- 1 | import {Entity, model, property, belongsTo} from '@loopback/repository'; 2 | import {Category} from './category.model'; 3 | 4 | @model() 5 | export class Book extends Entity { 6 | @property({ 7 | type: 'number', 8 | id: true, 9 | generated: true, 10 | }) 11 | id?: number; 12 | 13 | @property({ 14 | type: 'string', 15 | required: true, 16 | }) 17 | title: string; 18 | 19 | @property({ 20 | type: 'number', 21 | postgresql: { 22 | dataType: 'float', 23 | precision: 20, 24 | scale: 4, 25 | }, 26 | }) 27 | rating?: number; 28 | 29 | @belongsTo(() => Category) 30 | categoryId: number; 31 | 32 | constructor(data?: Partial) { 33 | super(data); 34 | } 35 | } 36 | 37 | export interface BookRelations { 38 | // describe navigational properties here 39 | } 40 | 41 | export type BookWithRelations = Book & BookRelations; 42 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/models/doctor.model.ts: -------------------------------------------------------------------------------- 1 | import {Entity, hasMany, model, property} from '@loopback/repository'; 2 | import {Appointment} from './appointment.model'; 3 | import {Patient} from './patient.model'; 4 | 5 | @model() 6 | export class Doctor extends Entity { 7 | @property({ 8 | type: 'number', 9 | id: true, 10 | generated: true, 11 | }) 12 | id?: number; 13 | 14 | @property({ 15 | type: 'string', 16 | required: true, 17 | }) 18 | name: string; 19 | 20 | @hasMany(() => Patient, { 21 | through: { 22 | model: () => Appointment, 23 | keyFrom: 'doctorId', 24 | keyTo: 'patientId', 25 | }, 26 | }) 27 | patients: Patient[]; 28 | 29 | constructor(data?: Partial) { 30 | super(data); 31 | } 32 | } 33 | 34 | export interface DoctorRelations { 35 | // describe navigational properties here 36 | } 37 | 38 | export type DoctorWithRelations = Doctor & DoctorRelations; 39 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/controllers/book-category.controller.ts: -------------------------------------------------------------------------------- 1 | import {repository} from '@loopback/repository'; 2 | import {param, get, getModelSchemaRef} from '@loopback/rest'; 3 | import {Book, Category} from '../models'; 4 | import {BookRepository} from '../repositories'; 5 | 6 | export class BookCategoryController { 7 | constructor( 8 | @repository(BookRepository) 9 | public bookRepository: BookRepository, 10 | ) {} 11 | 12 | @get('/books/{id}/category', { 13 | responses: { 14 | '200': { 15 | description: 'Category belonging to Book', 16 | content: { 17 | 'application/json': { 18 | schema: {type: 'array', items: getModelSchemaRef(Category)}, 19 | }, 20 | }, 21 | }, 22 | }, 23 | }) 24 | async getCategory( 25 | @param.path.number('id') id: typeof Book.prototype.id, 26 | ): Promise { 27 | return this.bookRepository.category(id); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/controllers/todo-todo-list.controller.ts: -------------------------------------------------------------------------------- 1 | import {repository} from '@loopback/repository'; 2 | import {param, get, getModelSchemaRef} from '@loopback/rest'; 3 | import {Todo, TodoList} from '../models'; 4 | import {TodoRepository} from '../repositories'; 5 | 6 | export class TodoTodoListController { 7 | constructor( 8 | @repository(TodoRepository) 9 | public todoRepository: TodoRepository, 10 | ) {} 11 | 12 | @get('/todos/{id}/todo-list', { 13 | responses: { 14 | '200': { 15 | description: 'TodoList belonging to Todo', 16 | content: { 17 | 'application/json': { 18 | schema: {type: 'array', items: getModelSchemaRef(TodoList)}, 19 | }, 20 | }, 21 | }, 22 | }, 23 | }) 24 | async getTodoList( 25 | @param.path.number('id') id: typeof Todo.prototype.id, 26 | ): Promise { 27 | return this.todoRepository.todoList(id); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "node", 5 | "request": "launch", 6 | "name": "Launch Program", 7 | "skipFiles": [ 8 | "/**" 9 | ], 10 | "program": "${workspaceFolder}/dist/index.js", 11 | }, 12 | { 13 | "type": "node", 14 | "request": "launch", 15 | "name": "Run Mocha tests", 16 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 17 | "runtimeArgs": [ 18 | "-r", 19 | "${workspaceRoot}/node_modules/source-map-support/register" 20 | ], 21 | "cwd": "${workspaceRoot}", 22 | "autoAttachChildProcesses": true, 23 | "args": [ 24 | "--config", 25 | "${workspaceRoot}/.mocharc.json", 26 | "${workspaceRoot}/dist/__tests__/**/*.js", 27 | "-t", 28 | "0" 29 | ] 30 | }, 31 | { 32 | "type": "node", 33 | "request": "attach", 34 | "name": "Attach to Process", 35 | "port": 5858 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /src/release_notes/mymarkdown.ejs: -------------------------------------------------------------------------------- 1 | ## Release [<%= range.split('..')[1] %>](https://github.com/sourcefuse/loopback4-sequelize/compare/<%= range %>) <%= new Date().toLocaleDateString('en-us', {year:"numeric", month:"long", day:"numeric"}) 2 | ;%> 3 | Welcome to the <%= new Date().toLocaleDateString('en-us', {year:"numeric", month:"long", day:"numeric"});%> release of loopback4-sequelize. There are many updates in this version that we hope you will like, the key highlights include: 4 | <% commits.forEach(function (commit) { %> 5 | - [<%= commit.issueTitle %>](https://github.com/sourcefuse/loopback4-sequelize/issues/<%= commit.issueno %>) :- [<%= commit.title %>](https://github.com/sourcefuse/loopback4-sequelize/commit/<%= commit.sha1%>) was commited on <%= commit.committerDate %> by [<%= commit.authorName %>](mailto:<%= commit.authorEmail %>) 6 | <% commit.messageLines.forEach(function (message) { %> 7 | - <%= message %> 8 | <% }) %> 9 | <% }) %> 10 | Clink on the above links to understand the changes in detail. 11 | ___ 12 | 13 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/controllers/test.controller.base.ts: -------------------------------------------------------------------------------- 1 | import {AnyObject} from '@loopback/repository'; 2 | import {SyncOptions} from 'sequelize'; 3 | 4 | export abstract class TestControllerBase { 5 | repositories: AnyObject[]; 6 | constructor(...repositories: AnyObject[]) { 7 | this.repositories = repositories; 8 | } 9 | 10 | /** 11 | * `beforeEach` is only for testing purposes in the controller, 12 | * Calling `syncSequelizeModel` ensures that corresponding table 13 | * exists before calling the function. In real project you are supposed 14 | * to run migrations instead, to sync model definitions to the target database. 15 | */ 16 | async beforeEach(options: {syncAll?: boolean} = {}) { 17 | const syncOptions: SyncOptions = {force: true}; 18 | 19 | for (const repository of this.repositories as AnyObject[]) { 20 | if (options.syncAll) { 21 | await repository.syncLoadedSequelizeModels(syncOptions); 22 | continue; 23 | } 24 | await repository.syncSequelizeModel(syncOptions); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/models/developer.model.ts: -------------------------------------------------------------------------------- 1 | import {Entity, model, property, referencesMany} from '@loopback/repository'; 2 | import {ProgrammingLanguage} from './programming-language.model'; 3 | 4 | @model() 5 | export class Developer extends Entity { 6 | @property({ 7 | type: 'number', 8 | id: true, 9 | generated: true, 10 | }) 11 | id?: number; 12 | 13 | @property({ 14 | type: 'string', 15 | required: true, 16 | }) 17 | name: string; 18 | 19 | @referencesMany( 20 | () => ProgrammingLanguage, 21 | {}, 22 | { 23 | type: ['string'], 24 | postgresql: {dataType: 'varchar[]'}, 25 | }, 26 | ) 27 | programmingLanguageIds: number[]; 28 | 29 | @property({ 30 | type: 'string', 31 | hidden: true, 32 | }) 33 | apiSecret: string; 34 | 35 | constructor(data?: Partial) { 36 | super(data); 37 | } 38 | } 39 | 40 | export interface DeveloperRelations { 41 | // describe navigational properties here 42 | } 43 | 44 | export type DeveloperWithRelations = Developer & DeveloperRelations; 45 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Loopback4 Sequelize 2 | 3 | theme: 4 | name: material 5 | features: 6 | - content.code.copy 7 | palette: 8 | - scheme: default 9 | toggle: 10 | icon: material/brightness-7 11 | name: Switch to dark mode 12 | - scheme: slate 13 | toggle: 14 | icon: material/brightness-4 15 | name: Switch to light mode 16 | 17 | docs_dir: docs 18 | 19 | markdown_extensions: 20 | - tables 21 | - pymdownx.highlight: 22 | anchor_linenums: true 23 | - pymdownx.inlinehilite 24 | - pymdownx.snippets 25 | - pymdownx.superfences 26 | 27 | extra: 28 | generator: false 29 | social: 30 | - icon: fontawesome/brands/github 31 | link: https://github.com/sourcefuse/loopback4-sequelize 32 | 33 | plugins: 34 | - search 35 | - include-markdown 36 | 37 | repo_name: sourcefuse/loopback4-sequelize 38 | repo_url: https://github.com/sourcefuse/loopback4-sequelize 39 | 40 | copyright: | 41 | © 2023 Sourceloop 42 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/datasources/primary.datasource.ts: -------------------------------------------------------------------------------- 1 | import {inject, lifeCycleObserver, LifeCycleObserver} from '@loopback/core'; 2 | import { 3 | SequelizeDataSource, 4 | SequelizeDataSourceConfig, 5 | } from '../../../sequelize'; 6 | import {datasourceTestConfig} from './config'; 7 | 8 | // DEVELOPMENT NOTE: 9 | // "Few Test cases for database transaction features won't work for in-memory 10 | // database configuration like sqlite3, change this to postgresql while developing to run 11 | // all test cases of transactional repo including those of isolation levels. 12 | // but ensure it's set to sqlite3 before commiting changes." 13 | 14 | export const config = datasourceTestConfig['primary']['sqlite3']; 15 | 16 | @lifeCycleObserver('datasource') 17 | export class PrimaryDataSource 18 | extends SequelizeDataSource 19 | implements LifeCycleObserver 20 | { 21 | static dataSourceName = 'primary'; 22 | static readonly defaultConfig = config; 23 | 24 | constructor( 25 | @inject('datasources.config.primary', {optional: true}) 26 | dsConfig: SequelizeDataSourceConfig = config, 27 | ) { 28 | super(dsConfig); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2022] [SourceFuse] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/datasources/secondary.datasource.ts: -------------------------------------------------------------------------------- 1 | import {inject, lifeCycleObserver, LifeCycleObserver} from '@loopback/core'; 2 | import { 3 | SequelizeDataSource, 4 | SequelizeDataSourceConfig, 5 | } from '../../../sequelize'; 6 | import {datasourceTestConfig} from './config'; 7 | 8 | // DEVELOPMENT NOTE: 9 | // "Few Test cases for database transaction features won't work for in-memory 10 | // database configuration like sqlite3, change this to postgresql while developing to run 11 | // all test cases of transactional repo including those of isolation levels. 12 | // but ensure it's set to sqlite3 before commiting changes." 13 | export const config = datasourceTestConfig['secondary']['sqlite3']; 14 | 15 | @lifeCycleObserver('datasource') 16 | export class SecondaryDataSource 17 | extends SequelizeDataSource 18 | implements LifeCycleObserver 19 | { 20 | static dataSourceName = 'secondary'; 21 | static readonly defaultConfig = config; 22 | 23 | constructor( 24 | @inject('datasources.config.secondary', {optional: true}) 25 | dsConfig: SequelizeDataSourceConfig = config, 26 | ) { 27 | super(dsConfig); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/repositories/book.repository.ts: -------------------------------------------------------------------------------- 1 | import {Getter, inject} from '@loopback/core'; 2 | import {BelongsToAccessor, repository} from '@loopback/repository'; 3 | import {SequelizeCrudRepository} from '../../../sequelize'; 4 | import {PrimaryDataSource} from '../datasources/primary.datasource'; 5 | import {Book, BookRelations, Category} from '../models/index'; 6 | import {CategoryRepository} from './category.repository'; 7 | 8 | export class BookRepository extends SequelizeCrudRepository< 9 | Book, 10 | typeof Book.prototype.id, 11 | BookRelations 12 | > { 13 | public readonly category: BelongsToAccessor< 14 | Category, 15 | typeof Book.prototype.id 16 | >; 17 | 18 | constructor( 19 | @inject('datasources.primary') dataSource: PrimaryDataSource, 20 | @repository.getter('CategoryRepository') 21 | protected categoryRepositoryGetter: Getter, 22 | ) { 23 | super(Book, dataSource); 24 | this.category = this.createBelongsToAccessorFor( 25 | 'category', 26 | categoryRepositoryGetter, 27 | ); 28 | this.registerInclusionResolver('category', this.category.inclusionResolver); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/repositories/todo.repository.ts: -------------------------------------------------------------------------------- 1 | import {Getter, inject} from '@loopback/core'; 2 | import {BelongsToAccessor, repository} from '@loopback/repository'; 3 | import {SequelizeCrudRepository} from '../../../sequelize'; 4 | import {PrimaryDataSource} from '../datasources/primary.datasource'; 5 | import {Todo, TodoList, TodoRelations} from '../models/index'; 6 | import {TodoListRepository} from './todo-list.repository'; 7 | 8 | export class TodoRepository extends SequelizeCrudRepository< 9 | Todo, 10 | typeof Todo.prototype.id, 11 | TodoRelations 12 | > { 13 | public readonly todoList: BelongsToAccessor< 14 | TodoList, 15 | typeof Todo.prototype.id 16 | >; 17 | 18 | constructor( 19 | @inject('datasources.primary') dataSource: PrimaryDataSource, 20 | @repository.getter('TodoListRepository') 21 | protected todoListRepositoryGetter: Getter, 22 | ) { 23 | super(Todo, dataSource); 24 | this.todoList = this.createBelongsToAccessorFor( 25 | 'todoList', 26 | todoListRepositoryGetter, 27 | ); 28 | this.registerInclusionResolver('todoList', this.todoList.inclusionResolver); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/repositories/todo-list.repository.ts: -------------------------------------------------------------------------------- 1 | import {Getter, inject} from '@loopback/core'; 2 | import {HasManyRepositoryFactory, repository} from '@loopback/repository'; 3 | import {SequelizeCrudRepository} from '../../../sequelize'; 4 | import {PrimaryDataSource} from '../datasources/primary.datasource'; 5 | import {Todo, TodoList, TodoListRelations} from '../models/index'; 6 | import {TodoRepository} from './todo.repository'; 7 | 8 | export class TodoListRepository extends SequelizeCrudRepository< 9 | TodoList, 10 | typeof TodoList.prototype.id, 11 | TodoListRelations 12 | > { 13 | public readonly todos: HasManyRepositoryFactory< 14 | Todo, 15 | typeof TodoList.prototype.id 16 | >; 17 | 18 | constructor( 19 | @inject('datasources.primary') dataSource: PrimaryDataSource, 20 | @repository.getter('TodoRepository') 21 | protected todoRepositoryGetter: Getter, 22 | ) { 23 | super(TodoList, dataSource); 24 | this.todos = this.createHasManyRepositoryFactoryFor( 25 | 'todos', 26 | todoRepositoryGetter, 27 | ); 28 | this.registerInclusionResolver('todos', this.todos.inclusionResolver); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/repositories/user.repository.ts: -------------------------------------------------------------------------------- 1 | import {Getter, inject} from '@loopback/core'; 2 | import {HasOneRepositoryFactory, repository} from '@loopback/repository'; 3 | import {SequelizeCrudRepository} from '../../../sequelize'; 4 | import {PrimaryDataSource} from '../datasources/primary.datasource'; 5 | import {TodoList, User, UserRelations} from '../models/index'; 6 | import {TodoListRepository} from './todo-list.repository'; 7 | 8 | export class UserRepository extends SequelizeCrudRepository< 9 | User, 10 | typeof User.prototype.id, 11 | UserRelations 12 | > { 13 | public readonly todoList: HasOneRepositoryFactory< 14 | TodoList, 15 | typeof User.prototype.id 16 | >; 17 | 18 | constructor( 19 | @inject('datasources.primary') dataSource: PrimaryDataSource, 20 | @repository.getter('TodoListRepository') 21 | protected todoListRepositoryGetter: Getter, 22 | ) { 23 | super(User, dataSource); 24 | this.todoList = this.createHasOneRepositoryFactoryFor( 25 | 'todoList', 26 | todoListRepositoryGetter, 27 | ); 28 | this.registerInclusionResolver('todoList', this.todoList.inclusionResolver); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] Intermediate change (work in progress) 15 | 16 | ## How Has This Been Tested? 17 | 18 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 19 | 20 | - [ ] Test A 21 | - [ ] Test B 22 | 23 | ## Checklist: 24 | 25 | - [ ] Performed a self-review of my own code 26 | - [ ] npm test passes on your machine 27 | - [ ] New tests added or existing tests modified to cover all changes 28 | - [ ] Code conforms with the style guide 29 | - [ ] API Documentation in code was updated 30 | - [ ] Any dependent changes have been merged and published in downstream modules 31 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/models/task.model.ts: -------------------------------------------------------------------------------- 1 | import {Entity, model, property} from '@loopback/repository'; 2 | 3 | @model() 4 | export class Task extends Entity { 5 | @property({ 6 | type: 'number', 7 | id: true, 8 | generated: true, 9 | }) 10 | id?: number; 11 | 12 | @property({ 13 | type: 'string', 14 | required: true, 15 | }) 16 | title: string; 17 | 18 | @property({ 19 | type: 'string', 20 | defaultFn: 'uuid', 21 | }) 22 | uuidv1: string; 23 | 24 | @property({ 25 | type: 'string', 26 | defaultFn: 'uuidv4', 27 | }) 28 | uuidv4: string; 29 | 30 | @property({ 31 | type: 'string', 32 | defaultFn: 'shortid', 33 | }) 34 | shortId: string; 35 | 36 | @property({ 37 | type: 'string', 38 | defaultFn: 'nanoid', 39 | }) 40 | nanoId: string; 41 | 42 | @property({ 43 | type: 'number', 44 | defaultFn: 'customAlias', 45 | }) 46 | customAlias: number; 47 | 48 | @property({ 49 | type: 'string', 50 | defaultFn: 'now', 51 | }) 52 | createdAt: string; 53 | 54 | constructor(data?: Partial) { 55 | super(data); 56 | } 57 | } 58 | 59 | export interface TaskRelations { 60 | // describe navigational properties here 61 | } 62 | 63 | export type TaskWithRelations = Task & TaskRelations; 64 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/repositories/developer.repository.ts: -------------------------------------------------------------------------------- 1 | import {Getter, inject} from '@loopback/core'; 2 | import {ReferencesManyAccessor, repository} from '@loopback/repository'; 3 | import {SequelizeCrudRepository} from '../../../sequelize'; 4 | import {PrimaryDataSource} from '../datasources/primary.datasource'; 5 | import { 6 | Developer, 7 | DeveloperRelations, 8 | ProgrammingLanguage, 9 | } from '../models/index'; 10 | import {ProgrammingLanguageRepository} from './programming-language.repository'; 11 | 12 | export class DeveloperRepository extends SequelizeCrudRepository< 13 | Developer, 14 | typeof Developer.prototype.id, 15 | DeveloperRelations 16 | > { 17 | public readonly programmingLanguages: ReferencesManyAccessor< 18 | ProgrammingLanguage, 19 | typeof Developer.prototype.id 20 | >; 21 | 22 | constructor( 23 | @inject('datasources.primary') dataSource: PrimaryDataSource, 24 | @repository.getter('ProgrammingLanguageRepository') 25 | protected programmingLanguageRepositoryGetter: Getter, 26 | ) { 27 | super(Developer, dataSource); 28 | this.programmingLanguages = this.createReferencesManyAccessorFor( 29 | 'programmingLanguages', 30 | programmingLanguageRepositoryGetter, 31 | ); 32 | this.registerInclusionResolver( 33 | 'programmingLanguages', 34 | this.programmingLanguages.inclusionResolver, 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # Transpiled JavaScript files from Typescript 61 | /dist 62 | 63 | # Cache used by TypeScript's incremental build 64 | *.tsbuildinfo 65 | 66 | /src/sequelize/usercrud.sequelize.repository.base.ts 67 | 68 | # site contains compiled html files, we don't need them because mkdocs-deploy 69 | # can freshly recreate them 70 | /site 71 | 72 | # api-reference 73 | /docs/api-reference 74 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/models/user.model.ts: -------------------------------------------------------------------------------- 1 | import {Entity, hasOne, model, property} from '@loopback/repository'; 2 | import {TodoList} from './todo-list.model'; 3 | 4 | @model() 5 | export class Address extends Entity { 6 | @property({ 7 | type: 'string', 8 | }) 9 | city: string; 10 | 11 | @property({ 12 | type: 'number', 13 | }) 14 | zipCode: number; 15 | } 16 | 17 | @model() 18 | export class User extends Entity { 19 | @property({ 20 | type: 'number', 21 | id: true, 22 | generated: true, 23 | }) 24 | id?: number; 25 | 26 | @property({ 27 | type: 'string', 28 | required: true, 29 | }) 30 | name: string; 31 | 32 | @property({ 33 | type: 'string', 34 | required: true, 35 | }) 36 | email: string; 37 | 38 | @property({ 39 | type: 'boolean', 40 | default: false, 41 | name: 'is_active', 42 | }) 43 | active?: boolean; 44 | 45 | @property({ 46 | type: 'object', 47 | postgresql: { 48 | dataType: 'json', 49 | }, 50 | }) 51 | address: Address; 52 | 53 | @property({ 54 | type: 'date', 55 | }) 56 | dob?: Date; 57 | 58 | @property({ 59 | type: 'string', 60 | hidden: true, 61 | }) 62 | password?: string; 63 | 64 | @hasOne(() => TodoList, {keyTo: 'user'}) 65 | todoList: TodoList; 66 | 67 | constructor(data?: Partial) { 68 | super(data); 69 | } 70 | } 71 | 72 | export interface UserRelations { 73 | // describe navigational properties here 74 | } 75 | 76 | export type UserWithRelations = User & UserRelations; 77 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/repositories/doctor.repository.ts: -------------------------------------------------------------------------------- 1 | import {Getter, inject} from '@loopback/core'; 2 | import { 3 | HasManyThroughRepositoryFactory, 4 | repository, 5 | } from '@loopback/repository'; 6 | import {SequelizeCrudRepository} from '../../../sequelize'; 7 | import {PrimaryDataSource} from '../datasources/primary.datasource'; 8 | import {Appointment, Doctor, DoctorRelations, Patient} from '../models/index'; 9 | import {AppointmentRepository} from './appointment.repository'; 10 | import {PatientRepository} from './patient.repository'; 11 | 12 | export class DoctorRepository extends SequelizeCrudRepository< 13 | Doctor, 14 | typeof Doctor.prototype.id, 15 | DoctorRelations 16 | > { 17 | public readonly patients: HasManyThroughRepositoryFactory< 18 | Patient, 19 | typeof Patient.prototype.id, 20 | Appointment, 21 | typeof Doctor.prototype.id 22 | >; 23 | 24 | constructor( 25 | @inject('datasources.primary') dataSource: PrimaryDataSource, 26 | @repository.getter('AppointmentRepository') 27 | protected appointmentRepositoryGetter: Getter, 28 | @repository.getter('PatientRepository') 29 | protected patientRepositoryGetter: Getter, 30 | ) { 31 | super(Doctor, dataSource); 32 | this.patients = this.createHasManyThroughRepositoryFactoryFor( 33 | 'patients', 34 | patientRepositoryGetter, 35 | appointmentRepositoryGetter, 36 | ); 37 | this.registerInclusionResolver('patients', this.patients.inclusionResolver); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /DEVELOPING.md: -------------------------------------------------------------------------------- 1 | # Developer's Guide 2 | 3 | We use Visual Studio Code for developing LoopBack and recommend the same to our 4 | users. 5 | 6 | ## VSCode setup 7 | 8 | Install the following extensions: 9 | 10 | - [eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) 11 | - [prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 12 | 13 | ## Development workflow 14 | 15 | ### Visual Studio Code 16 | 17 | 1. Start the build task (Cmd+Shift+B) to run TypeScript compiler in the 18 | background, watching and recompiling files as you change them. Compilation 19 | errors will be shown in the VSCode's "PROBLEMS" window. 20 | 21 | 2. Execute "Run Rest Task" from the Command Palette (Cmd+Shift+P) to re-run the 22 | test suite and lint the code for both programming and style errors. Linting 23 | errors will be shown in VSCode's "PROBLEMS" window. Failed tests are printed 24 | to terminal output only. 25 | 26 | ### Other editors/IDEs 27 | 28 | 1. Open a new terminal window/tab and start the continuous build process via 29 | `npm run build:watch`. It will run TypeScript compiler in watch mode, 30 | recompiling files as you change them. Any compilation errors will be printed 31 | to the terminal. 32 | 33 | 2. In your main terminal window/tab, run `npm run test:dev` to re-run the test 34 | suite and lint the code for both programming and style errors. You should run 35 | this command manually whenever you have new changes to test. Test failures 36 | and linter errors will be printed to the terminal. 37 | -------------------------------------------------------------------------------- /src/sequelize/connector-mapping.ts: -------------------------------------------------------------------------------- 1 | import {Dialect as AllSequelizeDialects, PoolOptions} from 'sequelize'; 2 | 3 | export type SupportedLoopbackConnectors = 4 | | 'mysql' 5 | | 'postgresql' 6 | | 'oracle' 7 | | 'sqlite3' 8 | | 'db2'; 9 | /** 10 | * @key Loopback connectors name supported by this extension 11 | * @value Equivalent Dialect in Sequelize 12 | */ 13 | export const SupportedConnectorMapping: { 14 | [key in SupportedLoopbackConnectors]?: AllSequelizeDialects; 15 | } = { 16 | mysql: 'mysql', 17 | postgresql: 'postgres', 18 | oracle: 'oracle', 19 | sqlite3: 'sqlite', 20 | db2: 'db2', 21 | }; 22 | 23 | /** 24 | * Loopback uses different keys for pool options depending on the connector. 25 | */ 26 | export const poolConfigKeys = [ 27 | // mysql 28 | 'connectionLimit', 29 | 'acquireTimeout', 30 | // postgresql 31 | 'min', 32 | 'max', 33 | 'idleTimeoutMillis', 34 | // oracle 35 | 'minConn', 36 | 'maxConn', 37 | 'timeout', 38 | ] as const; 39 | export type LoopbackPoolConfigKey = (typeof poolConfigKeys)[number]; 40 | 41 | export type PoolingEnabledConnector = Exclude< 42 | SupportedLoopbackConnectors, 43 | 'db2' | 'sqlite3' 44 | >; 45 | 46 | export const poolingEnabledConnectors: PoolingEnabledConnector[] = [ 47 | 'mysql', 48 | 'oracle', 49 | 'postgresql', 50 | ]; 51 | 52 | type IConnectionPoolOptions = { 53 | [connectorName in PoolingEnabledConnector]?: { 54 | [sequelizePoolOption in keyof PoolOptions]: LoopbackPoolConfigKey; 55 | }; 56 | }; 57 | 58 | export const ConnectionPoolOptions: IConnectionPoolOptions = { 59 | mysql: { 60 | max: 'connectionLimit', 61 | acquire: 'acquireTimeout', 62 | }, 63 | postgresql: { 64 | min: 'min', 65 | max: 'max', 66 | idle: 'idleTimeoutMillis', 67 | }, 68 | oracle: { 69 | min: 'minConn', 70 | max: 'maxConn', 71 | idle: 'timeout', 72 | }, 73 | }; 74 | -------------------------------------------------------------------------------- /src/release_notes/release-notes.js: -------------------------------------------------------------------------------- 1 | const releaseNotes = require('git-release-notes'); 2 | const simpleGit = require('simple-git/promise'); 3 | const path = require('path'); 4 | const {readFile, writeFile, ensureFile} = require('fs-extra'); 5 | 6 | async function generateReleaseNotes() { 7 | try { 8 | const OPTIONS = { 9 | branch: 'master', 10 | s: './post-processing.js', 11 | }; 12 | const RANGE = await getRange(); 13 | const TEMPLATE = './mymarkdown.ejs'; 14 | 15 | const changelog = await releaseNotes(OPTIONS, RANGE, TEMPLATE); 16 | 17 | const changelogPath = path.resolve(__dirname, '../..', 'CHANGELOG.md'); 18 | await ensureFile(changelogPath); 19 | const currentFile = (await readFile(changelogPath)).toString().trim(); 20 | if (currentFile) { 21 | console.log('Update %s', changelogPath); 22 | } else { 23 | console.log('Create %s', changelogPath); 24 | } 25 | 26 | await writeFile(changelogPath, changelog); 27 | await writeFile(changelogPath, currentFile, {flag: 'a+'}); 28 | await addAndCommit().then(() => { 29 | console.log('Changelog has been updated'); 30 | }); 31 | } catch (ex) { 32 | console.error(ex); 33 | process.exit(1); 34 | } 35 | } 36 | 37 | async function getRange() { 38 | const git = simpleGit(); 39 | const tags = (await git.tag({'--sort': 'committerdate'})).split('\n'); 40 | tags.pop(); 41 | 42 | const startTag = tags.slice(-2)[0]; 43 | const endTag = tags.slice(-1)[0]; 44 | return `${startTag}..${endTag}`; 45 | } 46 | 47 | async function addAndCommit() { 48 | const git = simpleGit(); 49 | await git.add(['../../CHANGELOG.md']); 50 | await git.commit('chore(release): changelog file', { 51 | '--no-verify': null, 52 | }); 53 | await git.push('origin', 'master'); 54 | } 55 | 56 | generateReleaseNotes().catch(ex => { 57 | console.error(ex); 58 | process.exit(1); 59 | }); 60 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/datasources/config.ts: -------------------------------------------------------------------------------- 1 | import {SequelizeDataSourceConfig} from '../../../sequelize'; 2 | 3 | // sqlite3 is to be used while running tests in ci environment 4 | // postgresql can be used for local development (to ensure all transaction test cases passes) 5 | type AvailableConfig = Record< 6 | 'postgresql' | 'sqlite3', 7 | SequelizeDataSourceConfig 8 | >; 9 | 10 | export const datasourceTestConfig: Record< 11 | 'primary' | 'secondary' | 'url' | 'wrongPassword', 12 | AvailableConfig 13 | > = { 14 | primary: { 15 | postgresql: { 16 | name: 'primary', 17 | connector: 'postgresql', 18 | host: 'localhost', 19 | port: 5001, 20 | user: 'postgres', 21 | password: 'super-secret', 22 | database: 'postgres', 23 | }, 24 | sqlite3: { 25 | name: 'primary', 26 | host: '0.0.0.0', 27 | connector: 'sqlite3', 28 | database: 'transaction-primary', 29 | file: ':memory:', 30 | }, 31 | }, 32 | secondary: { 33 | postgresql: { 34 | name: 'secondary', 35 | connector: 'postgresql', 36 | host: 'localhost', 37 | port: 5002, 38 | user: 'postgres', 39 | password: 'super-secret', 40 | database: 'postgres', 41 | }, 42 | sqlite3: { 43 | name: 'secondary', 44 | host: '0.0.0.0', 45 | connector: 'sqlite3', 46 | database: 'transaction-secondary', 47 | file: ':memory:', 48 | }, 49 | }, 50 | url: { 51 | postgresql: { 52 | name: 'using-url', 53 | connector: 'postgresql', 54 | url: 'postgres://postgres:super-secret@localhost:5002/postgres', 55 | }, 56 | sqlite3: { 57 | name: 'using-url', 58 | url: 'sqlite::memory:', 59 | }, 60 | }, 61 | wrongPassword: { 62 | postgresql: { 63 | name: 'wrongPassword', 64 | connector: 'postgresql', 65 | url: 'postgres://postgres:super-secret-wrong@localhost:5002/postgres', 66 | }, 67 | sqlite3: { 68 | name: 'wrongPassword', 69 | url: 'sqlite::memory:', 70 | }, 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # loopback4-sequelize 2 | 3 | ## Contributing 4 | 5 | First off, thank you for considering contributing to the project. It's people like you that helps in keeping this extension useful. 6 | 7 | ### Where do I go from here ? 8 | 9 | If you've noticed a bug or have a question, [search the issue tracker](https://github.com/sourcefuse/loopback4-sequelize/issues) to see if 10 | someone else in the community has already created a ticket. If not, go ahead and 11 | [make one](https://github.com/sourcefuse/loopback4-sequelize/issues/new/choose)! 12 | 13 | ### Fork & create a branch 14 | 15 | If this is something you think you can fix, then [fork](https://help.github.com/articles/fork-a-repo) this repo and 16 | create a branch with a descriptive name. 17 | 18 | A good branch name would be (where issue #325 is the ticket you're working on): 19 | 20 | ```sh 21 | git checkout -b 325-add-new-feature 22 | ``` 23 | 24 | ### Make a Pull Request 25 | 26 | At this point, you should switch back to your main branch and make sure it's 27 | up to date with loopback4-sequelize's main branch: 28 | 29 | ```sh 30 | git remote add upstream git@github.com:sourcefuse/loopback4-sequelize.git 31 | git checkout main 32 | git pull upstream main 33 | ``` 34 | 35 | Then update your feature branch from your local copy of main, and push it! 36 | 37 | ```sh 38 | git checkout 325-add-new-feature 39 | git rebase main 40 | git push --set-upstream origin 325-add-new-feature 41 | ``` 42 | 43 | Finally, go to GitHub and [make a Pull Request](https://help.github.com/articles/creating-a-pull-request). 44 | 45 | ### Keeping your Pull Request updated 46 | 47 | If a maintainer asks you to "rebase" your PR, they're saying that a lot of code 48 | has changed, and that you need to update your branch so it's easier to merge. 49 | 50 | To learn more about rebasing in Git, there are a lot of [good][git rebasing] 51 | [resources][interactive rebase] but here's the suggested workflow: 52 | 53 | ```sh 54 | git checkout 325-add-new-feature 55 | git pull --rebase upstream main 56 | git push --force-with-lease 325-add-new-feature 57 | ``` 58 | 59 | [git rebasing]: http://git-scm.com/book/en/Git-Branching-Rebasing 60 | [interactive rebase]: https://help.github.com/articles/interactive-rebase 61 | -------------------------------------------------------------------------------- /src/release_notes/post-processing.js: -------------------------------------------------------------------------------- 1 | const https = require('node:https'); 2 | const jsdom = require('jsdom'); 3 | module.exports = async function (data, callback) { 4 | const rewritten = []; 5 | for (const commit of data.commits) { 6 | if (commit.title.indexOf('chore(release)') !== -1) { 7 | continue; 8 | } 9 | 10 | const commitTitle = commit.title; 11 | commit.title = commitTitle.substring(0, commitTitle.indexOf('#') - 1); 12 | 13 | commit.messageLines = commit.messageLines.filter(message => { 14 | if (message.indexOf('efs/remotes/origin') === -1) return message; 15 | }); 16 | 17 | commit.messageLines.forEach(message => { 18 | commit.issueno = message.includes('GH-') 19 | ? message.replace('GH-', '').trim() 20 | : null; 21 | }); 22 | 23 | const issueDesc = await getIssueDesc(commit.issueno).then(res => { 24 | return res; 25 | }); 26 | commit.issueTitle = issueDesc; 27 | 28 | commit.committerDate = new Date(commit.committerDate).toLocaleDateString( 29 | 'en-us', 30 | { 31 | year: 'numeric', 32 | month: 'long', 33 | day: 'numeric', 34 | }, 35 | ); 36 | rewritten.push(commit); 37 | } 38 | callback({ 39 | commits: rewritten.filter(Boolean), 40 | range: data.range, 41 | }); 42 | }; 43 | 44 | function getIssueDesc(issueNo) { 45 | return new Promise((resolve, reject) => { 46 | let result = ''; 47 | const req = https.get( 48 | `https://github.com/sourcefuse/loopback4-sequelize/issues/${encodeURIComponent( 49 | issueNo, 50 | )}`, 51 | res => { 52 | res.setEncoding('utf8'); 53 | res.on('data', chunk => { 54 | result = result + chunk; 55 | }); 56 | res.on('end', () => { 57 | const {JSDOM} = jsdom; 58 | const dom = new JSDOM(result); 59 | const title = dom.window.document.getElementsByClassName( 60 | 'js-issue-title markdown-title', 61 | ); 62 | let issueTitle = ''; 63 | for (const ele of title) { 64 | if (ele.nodeName === 'BDI') { 65 | issueTitle = ele.innerHTML; 66 | } 67 | } 68 | resolve(issueTitle); 69 | }); 70 | }, 71 | ); 72 | req.on('error', e => { 73 | reject(e); 74 | }); 75 | req.end(); 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /.cz-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | types: [ 3 | {value: 'feat', name: 'feat: A new feature'}, 4 | {value: 'fix', name: 'fix: A bug fix'}, 5 | {value: 'docs', name: 'docs: Documentation only changes'}, 6 | { 7 | value: 'style', 8 | name: 'style: Changes that do not affect the meaning of the code\n (white-space, formatting, missing semi-colons, etc)', 9 | }, 10 | { 11 | value: 'refactor', 12 | name: 'refactor: A code change that neither fixes a bug nor adds a feature', 13 | }, 14 | { 15 | value: 'perf', 16 | name: 'perf: A code change that improves performance', 17 | }, 18 | {value: 'test', name: 'test: Adding missing tests'}, 19 | { 20 | value: 'chore', 21 | name: 'chore: Changes to the build process or auxiliary tools\n and libraries such as documentation generation', 22 | }, 23 | {value: 'revert', name: 'revert: Reverting a commit'}, 24 | {value: 'WIP', name: 'WIP: Work in progress'}, 25 | ], 26 | 27 | scopes: [ 28 | {name: 'deps'}, 29 | {name: 'chore'}, 30 | {name: 'ci-cd'}, 31 | {name: 'component'}, 32 | {name: 'providers'}, 33 | {name: 'services'}, 34 | {name: 'decorators'}, 35 | {name: 'core'}, 36 | {name: 'repository'}, 37 | {name: 'datasource'}, 38 | {name: 'transaction'}, 39 | ], 40 | 41 | appendBranchNameToCommitMessage: true, 42 | appendIssueFromBranchName: true, 43 | allowTicketNumber: false, 44 | isTicketNumberRequired: false, 45 | 46 | // override the messages, defaults are as follows 47 | messages: { 48 | type: "Select the type of change that you're committing:", 49 | scope: 'Denote the SCOPE of this change:', 50 | // used if allowCustomScopes is true 51 | customScope: 'Denote the SCOPE of this change:', 52 | subject: 'Write a SHORT, IMPERATIVE tense description of the change:\n', 53 | body: 'Provide a LONGER description of the change (mandatory). Use "\\n" to break new line:\n', 54 | breaking: 'List any BREAKING CHANGES (optional):\n', 55 | footer: 'List any ISSUES CLOSED by this change (optional). E.g.: GH-144:\n', 56 | confirmCommit: 'Are you sure you want to proceed with the commit above?', 57 | }, 58 | 59 | allowCustomScopes: false, 60 | allowBreakingChanges: ['feat', 'fix'], 61 | 62 | // limit subject length 63 | subjectLimit: 100, 64 | breaklineChar: '|', // It is supported for fields body and footer. 65 | footerPrefix: '', 66 | askForBreakingChangeFirst: true, // default is false 67 | }; 68 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/controllers/todo-list-todo.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Count, 3 | CountSchema, 4 | Filter, 5 | repository, 6 | Where, 7 | } from '@loopback/repository'; 8 | import { 9 | del, 10 | get, 11 | getModelSchemaRef, 12 | getWhereSchemaFor, 13 | param, 14 | patch, 15 | post, 16 | requestBody, 17 | } from '@loopback/rest'; 18 | import {TodoList, Todo} from '../models'; 19 | import {TodoListRepository} from '../repositories'; 20 | 21 | export class TodoListTodoController { 22 | constructor( 23 | @repository(TodoListRepository) 24 | protected todoListRepository: TodoListRepository, 25 | ) {} 26 | 27 | @get('/todo-lists/{id}/todos', { 28 | responses: { 29 | '200': { 30 | description: 'Array of TodoList has many Todo', 31 | content: { 32 | 'application/json': { 33 | schema: {type: 'array', items: getModelSchemaRef(Todo)}, 34 | }, 35 | }, 36 | }, 37 | }, 38 | }) 39 | async find( 40 | @param.path.number('id') id: number, 41 | @param.query.object('filter') filter?: Filter, 42 | ): Promise { 43 | return this.todoListRepository.todos(id).find(filter); 44 | } 45 | 46 | @post('/todo-lists/{id}/todos', { 47 | responses: { 48 | '200': { 49 | description: 'TodoList model instance', 50 | content: {'application/json': {schema: getModelSchemaRef(Todo)}}, 51 | }, 52 | }, 53 | }) 54 | async create( 55 | @param.path.number('id') id: typeof TodoList.prototype.id, 56 | @requestBody({ 57 | content: { 58 | 'application/json': { 59 | schema: getModelSchemaRef(Todo, { 60 | title: 'NewTodoInTodoList', 61 | exclude: ['id'], 62 | optional: ['todoListId'], 63 | }), 64 | }, 65 | }, 66 | }) 67 | todo: Omit, 68 | ): Promise { 69 | return this.todoListRepository.todos(id).create(todo); 70 | } 71 | 72 | @patch('/todo-lists/{id}/todos', { 73 | responses: { 74 | '200': { 75 | description: 'TodoList.Todo PATCH success count', 76 | content: {'application/json': {schema: CountSchema}}, 77 | }, 78 | }, 79 | }) 80 | async patch( 81 | @param.path.number('id') id: number, 82 | @requestBody({ 83 | content: { 84 | 'application/json': { 85 | schema: getModelSchemaRef(Todo, {partial: true}), 86 | }, 87 | }, 88 | }) 89 | todo: Partial, 90 | @param.query.object('where', getWhereSchemaFor(Todo)) where?: Where, 91 | ): Promise { 92 | return this.todoListRepository.todos(id).patch(todo, where); 93 | } 94 | 95 | @del('/todo-lists/{id}/todos', { 96 | responses: { 97 | '200': { 98 | description: 'TodoList.Todo DELETE success count', 99 | content: {'application/json': {schema: CountSchema}}, 100 | }, 101 | }, 102 | }) 103 | async delete( 104 | @param.path.number('id') id: number, 105 | @param.query.object('where', getWhereSchemaFor(Todo)) where?: Where, 106 | ): Promise { 107 | return this.todoListRepository.todos(id).delete(where); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/controllers/user-todo-list.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Count, 3 | CountSchema, 4 | Filter, 5 | repository, 6 | Where, 7 | } from '@loopback/repository'; 8 | import { 9 | del, 10 | get, 11 | getModelSchemaRef, 12 | getWhereSchemaFor, 13 | param, 14 | patch, 15 | post, 16 | requestBody, 17 | } from '@loopback/rest'; 18 | import {User, TodoList} from '../models'; 19 | import {UserRepository} from '../repositories'; 20 | 21 | export class UserTodoListController { 22 | constructor( 23 | @repository(UserRepository) protected userRepository: UserRepository, 24 | ) {} 25 | 26 | @get('/users/{id}/todo-list', { 27 | responses: { 28 | '200': { 29 | description: 'User has one TodoList', 30 | content: { 31 | 'application/json': { 32 | schema: getModelSchemaRef(TodoList), 33 | }, 34 | }, 35 | }, 36 | }, 37 | }) 38 | async get( 39 | @param.path.number('id') id: number, 40 | @param.query.object('filter') filter?: Filter, 41 | ): Promise { 42 | return this.userRepository.todoList(id).get(filter); 43 | } 44 | 45 | @post('/users/{id}/todo-list', { 46 | responses: { 47 | '200': { 48 | description: 'User model instance', 49 | content: {'application/json': {schema: getModelSchemaRef(TodoList)}}, 50 | }, 51 | }, 52 | }) 53 | async create( 54 | @param.path.number('id') id: typeof User.prototype.id, 55 | @requestBody({ 56 | content: { 57 | 'application/json': { 58 | schema: getModelSchemaRef(TodoList, { 59 | title: 'NewTodoListInUser', 60 | exclude: ['id'], 61 | optional: ['user'], 62 | }), 63 | }, 64 | }, 65 | }) 66 | todoList: Omit, 67 | ): Promise { 68 | return this.userRepository.todoList(id).create(todoList); 69 | } 70 | 71 | @patch('/users/{id}/todo-list', { 72 | responses: { 73 | '200': { 74 | description: 'User.TodoList PATCH success count', 75 | content: {'application/json': {schema: CountSchema}}, 76 | }, 77 | }, 78 | }) 79 | async patch( 80 | @param.path.number('id') id: number, 81 | @requestBody({ 82 | content: { 83 | 'application/json': { 84 | schema: getModelSchemaRef(TodoList, {partial: true}), 85 | }, 86 | }, 87 | }) 88 | todoList: Partial, 89 | @param.query.object('where', getWhereSchemaFor(TodoList)) 90 | where?: Where, 91 | ): Promise { 92 | return this.userRepository.todoList(id).patch(todoList, where); 93 | } 94 | 95 | @del('/users/{id}/todo-list', { 96 | responses: { 97 | '200': { 98 | description: 'User.TodoList DELETE success count', 99 | content: {'application/json': {schema: CountSchema}}, 100 | }, 101 | }, 102 | }) 103 | async delete( 104 | @param.path.number('id') id: number, 105 | @param.query.object('where', getWhereSchemaFor(TodoList)) 106 | where?: Where, 107 | ): Promise { 108 | return this.userRepository.todoList(id).delete(where); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/controllers/doctor-patient.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Count, 3 | CountSchema, 4 | Filter, 5 | repository, 6 | Where, 7 | } from '@loopback/repository'; 8 | import { 9 | del, 10 | get, 11 | getModelSchemaRef, 12 | getWhereSchemaFor, 13 | param, 14 | patch, 15 | post, 16 | requestBody, 17 | } from '@loopback/rest'; 18 | import {Doctor, Patient} from '../models'; 19 | import {DoctorRepository} from '../repositories'; 20 | 21 | export class DoctorPatientController { 22 | constructor( 23 | @repository(DoctorRepository) protected doctorRepository: DoctorRepository, 24 | ) {} 25 | 26 | @get('/doctors/{id}/patients', { 27 | responses: { 28 | '200': { 29 | description: 'Array of Doctor has many Patient through Appointment', 30 | content: { 31 | 'application/json': { 32 | schema: {type: 'array', items: getModelSchemaRef(Patient)}, 33 | }, 34 | }, 35 | }, 36 | }, 37 | }) 38 | async find( 39 | @param.path.number('id') id: number, 40 | @param.query.object('filter') filter?: Filter, 41 | ): Promise { 42 | return this.doctorRepository.patients(id).find(filter); 43 | } 44 | 45 | @post('/doctors/{id}/patients', { 46 | responses: { 47 | '200': { 48 | description: 'create a Patient model instance', 49 | content: {'application/json': {schema: getModelSchemaRef(Patient)}}, 50 | }, 51 | }, 52 | }) 53 | async create( 54 | @param.path.number('id') id: typeof Doctor.prototype.id, 55 | @requestBody({ 56 | content: { 57 | 'application/json': { 58 | schema: getModelSchemaRef(Patient, { 59 | title: 'NewPatientInDoctor', 60 | exclude: ['id'], 61 | }), 62 | }, 63 | }, 64 | }) 65 | patient: Omit, 66 | ): Promise { 67 | return this.doctorRepository.patients(id).create(patient); 68 | } 69 | 70 | @patch('/doctors/{id}/patients', { 71 | responses: { 72 | '200': { 73 | description: 'Doctor.Patient PATCH success count', 74 | content: {'application/json': {schema: CountSchema}}, 75 | }, 76 | }, 77 | }) 78 | async patch( 79 | @param.path.number('id') id: number, 80 | @requestBody({ 81 | content: { 82 | 'application/json': { 83 | schema: getModelSchemaRef(Patient, {partial: true}), 84 | }, 85 | }, 86 | }) 87 | patient: Partial, 88 | @param.query.object('where', getWhereSchemaFor(Patient)) 89 | where?: Where, 90 | ): Promise { 91 | return this.doctorRepository.patients(id).patch(patient, where); 92 | } 93 | 94 | @del('/doctors/{id}/patients', { 95 | responses: { 96 | '200': { 97 | description: 'Doctor.Patient DELETE success count', 98 | content: {'application/json': {schema: CountSchema}}, 99 | }, 100 | }, 101 | }) 102 | async delete( 103 | @param.path.number('id') id: number, 104 | @param.query.object('where', getWhereSchemaFor(Patient)) 105 | where?: Where, 106 | ): Promise { 107 | return this.doctorRepository.patients(id).delete(where); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at support@sourcefuse.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/__tests__/unit/sequelize.datasource.unit.ts: -------------------------------------------------------------------------------- 1 | import {expect} from '@loopback/testlab'; 2 | import {SequelizeDataSource} from '../../sequelize'; 3 | import {SupportedLoopbackConnectors} from '../../sequelize/connector-mapping'; 4 | import {datasourceTestConfig} from '../fixtures/datasources/config'; 5 | import {config as primaryDataSourceConfig} from '../fixtures/datasources/primary.datasource'; 6 | 7 | describe('Sequelize DataSource', () => { 8 | it('throws error when nosql connectors are supplied', () => { 9 | try { 10 | new SequelizeDataSource({ 11 | name: 'db', 12 | user: 'test', 13 | password: 'secret', 14 | connector: 'memory' as SupportedLoopbackConnectors, 15 | }); 16 | } catch (err) { 17 | const result = err.message; 18 | expect(result).which.eql('Specified connector memory is not supported.'); 19 | } 20 | }); 21 | 22 | it('accepts url strings for connection', async () => { 23 | const dataSource = new SequelizeDataSource( 24 | datasourceTestConfig.url[ 25 | primaryDataSourceConfig.connector === 'postgresql' 26 | ? 'postgresql' 27 | : 'sqlite3' 28 | ], 29 | ); 30 | expect(await dataSource.init()).to.not.throwError(); 31 | await dataSource.stop(); 32 | }); 33 | 34 | it('throws error if url strings has wrong password', async function () { 35 | if (primaryDataSourceConfig.connector !== 'postgresql') { 36 | // eslint-disable-next-line @typescript-eslint/no-invalid-this 37 | this.skip(); 38 | } 39 | const dataSource = new SequelizeDataSource( 40 | datasourceTestConfig.wrongPassword.postgresql, 41 | ); 42 | try { 43 | await dataSource.init(); 44 | } catch (err) { 45 | expect(err.message).to.be.eql( 46 | 'password authentication failed for user "postgres"', 47 | ); 48 | } 49 | }); 50 | 51 | it('should be able override sequelize options', async function () { 52 | if (primaryDataSourceConfig.connector !== 'postgresql') { 53 | // eslint-disable-next-line @typescript-eslint/no-invalid-this 54 | this.skip(); 55 | } 56 | const dataSource = new SequelizeDataSource({ 57 | ...datasourceTestConfig.primary.postgresql, 58 | user: 'wrong-username', // expected to be overridden 59 | sequelizeOptions: { 60 | username: datasourceTestConfig.primary.postgresql.user, 61 | }, 62 | }); 63 | expect(await dataSource.init()).to.not.throwError(); 64 | }); 65 | 66 | it('parses pool options for postgresql', async () => { 67 | const dataSource = new SequelizeDataSource({ 68 | name: 'db', 69 | connector: 'postgresql', 70 | min: 10, 71 | max: 20, 72 | idleTimeoutMillis: 18000, 73 | }); 74 | 75 | const poolOptions = dataSource.getPoolOptions(); 76 | 77 | expect(poolOptions).to.have.property('min', 10); 78 | expect(poolOptions).to.have.property('max', 20); 79 | expect(poolOptions).to.have.property('idle', 18000); 80 | expect(poolOptions).to.not.have.property('acquire'); 81 | }); 82 | 83 | it('parses pool options for mysql', async () => { 84 | const dataSource = new SequelizeDataSource({ 85 | name: 'db', 86 | connector: 'mysql', 87 | connectionLimit: 20, 88 | acquireTimeout: 10000, 89 | }); 90 | 91 | const poolOptions = dataSource.getPoolOptions(); 92 | 93 | expect(poolOptions).to.have.property('max', 20); 94 | expect(poolOptions).to.have.property('acquire', 10000); 95 | expect(poolOptions).to.not.have.property('min'); 96 | expect(poolOptions).to.not.have.property('idle'); 97 | }); 98 | 99 | it('parses pool options for oracle', async () => { 100 | const dataSource = new SequelizeDataSource({ 101 | name: 'db', 102 | connector: 'oracle', 103 | minConn: 10, 104 | maxConn: 20, 105 | timeout: 20000, 106 | }); 107 | 108 | const poolOptions = dataSource.getPoolOptions(); 109 | 110 | expect(poolOptions).to.have.property('min', 10); 111 | expect(poolOptions).to.have.property('max', 20); 112 | expect(poolOptions).to.have.property('idle', 20000); 113 | expect(poolOptions).to.not.have.property('acquire'); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/controllers/patient.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Count, 3 | CountSchema, 4 | Filter, 5 | FilterExcludingWhere, 6 | repository, 7 | Where, 8 | } from '@loopback/repository'; 9 | import { 10 | post, 11 | param, 12 | get, 13 | getModelSchemaRef, 14 | patch, 15 | put, 16 | del, 17 | requestBody, 18 | response, 19 | } from '@loopback/rest'; 20 | import {Patient} from '../models'; 21 | import {PatientRepository} from '../repositories'; 22 | 23 | export class PatientController { 24 | constructor( 25 | @repository(PatientRepository) 26 | public patientRepository: PatientRepository, 27 | ) {} 28 | 29 | @post('/patients') 30 | @response(200, { 31 | description: 'Patient model instance', 32 | content: {'application/json': {schema: getModelSchemaRef(Patient)}}, 33 | }) 34 | async create( 35 | @requestBody({ 36 | content: { 37 | 'application/json': { 38 | schema: getModelSchemaRef(Patient, { 39 | title: 'NewPatient', 40 | exclude: ['id'], 41 | }), 42 | }, 43 | }, 44 | }) 45 | patient: Omit, 46 | ): Promise { 47 | return this.patientRepository.create(patient); 48 | } 49 | 50 | @get('/patients/count') 51 | @response(200, { 52 | description: 'Patient model count', 53 | content: {'application/json': {schema: CountSchema}}, 54 | }) 55 | async count(@param.where(Patient) where?: Where): Promise { 56 | return this.patientRepository.count(where); 57 | } 58 | 59 | @get('/patients') 60 | @response(200, { 61 | description: 'Array of Patient model instances', 62 | content: { 63 | 'application/json': { 64 | schema: { 65 | type: 'array', 66 | items: getModelSchemaRef(Patient, {includeRelations: true}), 67 | }, 68 | }, 69 | }, 70 | }) 71 | async find( 72 | @param.filter(Patient) filter?: Filter, 73 | ): Promise { 74 | return this.patientRepository.find(filter); 75 | } 76 | 77 | @patch('/patients') 78 | @response(200, { 79 | description: 'Patient PATCH success count', 80 | content: {'application/json': {schema: CountSchema}}, 81 | }) 82 | async updateAll( 83 | @requestBody({ 84 | content: { 85 | 'application/json': { 86 | schema: getModelSchemaRef(Patient, {partial: true}), 87 | }, 88 | }, 89 | }) 90 | patient: Patient, 91 | @param.where(Patient) where?: Where, 92 | ): Promise { 93 | return this.patientRepository.updateAll(patient, where); 94 | } 95 | 96 | @get('/patients/{id}') 97 | @response(200, { 98 | description: 'Patient model instance', 99 | content: { 100 | 'application/json': { 101 | schema: getModelSchemaRef(Patient, {includeRelations: true}), 102 | }, 103 | }, 104 | }) 105 | async findById( 106 | @param.path.number('id') id: number, 107 | @param.filter(Patient, {exclude: 'where'}) 108 | filter?: FilterExcludingWhere, 109 | ): Promise { 110 | return this.patientRepository.findById(id, filter); 111 | } 112 | 113 | @patch('/patients/{id}') 114 | @response(204, { 115 | description: 'Patient PATCH success', 116 | }) 117 | async updateById( 118 | @param.path.number('id') id: number, 119 | @requestBody({ 120 | content: { 121 | 'application/json': { 122 | schema: getModelSchemaRef(Patient, {partial: true}), 123 | }, 124 | }, 125 | }) 126 | patient: Patient, 127 | ): Promise { 128 | await this.patientRepository.updateById(id, patient); 129 | } 130 | 131 | @put('/patients/{id}') 132 | @response(204, { 133 | description: 'Patient PUT success', 134 | }) 135 | async replaceById( 136 | @param.path.number('id') id: number, 137 | @requestBody() patient: Patient, 138 | ): Promise { 139 | await this.patientRepository.replaceById(id, patient); 140 | } 141 | 142 | @del('/patients/{id}') 143 | @response(204, { 144 | description: 'Patient DELETE success', 145 | }) 146 | async deleteById(@param.path.number('id') id: number): Promise { 147 | await this.patientRepository.deleteById(id); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/controllers/todo.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Count, 3 | CountSchema, 4 | Filter, 5 | FilterExcludingWhere, 6 | repository, 7 | Where, 8 | } from '@loopback/repository'; 9 | import { 10 | del, 11 | get, 12 | getModelSchemaRef, 13 | param, 14 | patch, 15 | post, 16 | put, 17 | requestBody, 18 | response, 19 | } from '@loopback/rest'; 20 | import {Todo} from '../models'; 21 | import {TodoRepository} from '../repositories'; 22 | import {TestControllerBase} from './test.controller.base'; 23 | 24 | export class TodoController extends TestControllerBase { 25 | constructor( 26 | @repository(TodoRepository) 27 | public todoRepository: TodoRepository, 28 | ) { 29 | super(todoRepository); 30 | } 31 | 32 | @post('/todos') 33 | @response(200, { 34 | description: 'Todo model instance', 35 | content: {'application/json': {schema: getModelSchemaRef(Todo)}}, 36 | }) 37 | async create( 38 | @requestBody({ 39 | content: { 40 | 'application/json': { 41 | schema: getModelSchemaRef(Todo, { 42 | title: 'NewTodo', 43 | exclude: ['id'], 44 | }), 45 | }, 46 | }, 47 | }) 48 | todo: Omit, 49 | ): Promise { 50 | return this.todoRepository.create(todo); 51 | } 52 | 53 | @get('/todos/count') 54 | @response(200, { 55 | description: 'Todo model count', 56 | content: {'application/json': {schema: CountSchema}}, 57 | }) 58 | async count(@param.where(Todo) where?: Where): Promise { 59 | return this.todoRepository.count(where); 60 | } 61 | 62 | @get('/todos') 63 | @response(200, { 64 | description: 'Array of Todo model instances', 65 | content: { 66 | 'application/json': { 67 | schema: { 68 | type: 'array', 69 | items: getModelSchemaRef(Todo, {includeRelations: true}), 70 | }, 71 | }, 72 | }, 73 | }) 74 | async find(@param.filter(Todo) filter?: Filter): Promise { 75 | return this.todoRepository.find(filter); 76 | } 77 | 78 | @patch('/todos') 79 | @response(200, { 80 | description: 'Todo PATCH success count', 81 | content: {'application/json': {schema: CountSchema}}, 82 | }) 83 | async updateAll( 84 | @requestBody({ 85 | content: { 86 | 'application/json': { 87 | schema: getModelSchemaRef(Todo, {partial: true}), 88 | }, 89 | }, 90 | }) 91 | todo: Todo, 92 | @param.where(Todo) where?: Where, 93 | ): Promise { 94 | return this.todoRepository.updateAll(todo, where); 95 | } 96 | 97 | @get('/todos/{id}') 98 | @response(200, { 99 | description: 'Todo model instance', 100 | content: { 101 | 'application/json': { 102 | schema: getModelSchemaRef(Todo, {includeRelations: true}), 103 | }, 104 | }, 105 | }) 106 | async findById( 107 | @param.path.number('id') id: number, 108 | @param.filter(Todo, {exclude: 'where'}) filter?: FilterExcludingWhere, 109 | ): Promise { 110 | return this.todoRepository.findById(id, filter); 111 | } 112 | 113 | @patch('/todos/{id}') 114 | @response(204, { 115 | description: 'Todo PATCH success', 116 | }) 117 | async updateById( 118 | @param.path.number('id') id: number, 119 | @requestBody({ 120 | content: { 121 | 'application/json': { 122 | schema: getModelSchemaRef(Todo, {partial: true}), 123 | }, 124 | }, 125 | }) 126 | todo: Todo, 127 | ): Promise { 128 | await this.todoRepository.updateById(id, todo); 129 | } 130 | 131 | @put('/todos/{id}') 132 | @response(204, { 133 | description: 'Todo PUT success', 134 | }) 135 | async replaceById( 136 | @param.path.number('id') id: number, 137 | @requestBody() todo: Todo, 138 | ): Promise { 139 | await this.todoRepository.replaceById(id, todo); 140 | } 141 | 142 | @del('/todos/{id}') 143 | @response(204, { 144 | description: 'Todo DELETE success', 145 | }) 146 | async deleteById(@param.path.number('id') id: number): Promise { 147 | await this.todoRepository.deleteById(id); 148 | } 149 | 150 | @get('/todos/sync-sequelize-model') 151 | @response(200) 152 | async syncSequelizeModel(): Promise { 153 | await this.beforeEach(); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/controllers/doctor.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Count, 3 | CountSchema, 4 | Filter, 5 | FilterExcludingWhere, 6 | repository, 7 | Where, 8 | } from '@loopback/repository'; 9 | import { 10 | del, 11 | get, 12 | getModelSchemaRef, 13 | param, 14 | patch, 15 | post, 16 | put, 17 | requestBody, 18 | response, 19 | } from '@loopback/rest'; 20 | import {Doctor} from '../models'; 21 | import {DoctorRepository} from '../repositories'; 22 | import {TestControllerBase} from './test.controller.base'; 23 | 24 | export class DoctorController extends TestControllerBase { 25 | constructor( 26 | @repository(DoctorRepository) 27 | public doctorRepository: DoctorRepository, 28 | ) { 29 | super(doctorRepository); 30 | } 31 | 32 | @post('/doctors') 33 | @response(200, { 34 | description: 'Doctor model instance', 35 | content: {'application/json': {schema: getModelSchemaRef(Doctor)}}, 36 | }) 37 | async create( 38 | @requestBody({ 39 | content: { 40 | 'application/json': { 41 | schema: getModelSchemaRef(Doctor, { 42 | title: 'NewDoctor', 43 | exclude: ['id'], 44 | }), 45 | }, 46 | }, 47 | }) 48 | doctor: Omit, 49 | ): Promise { 50 | return this.doctorRepository.create(doctor); 51 | } 52 | 53 | @get('/doctors/count') 54 | @response(200, { 55 | description: 'Doctor model count', 56 | content: {'application/json': {schema: CountSchema}}, 57 | }) 58 | async count(@param.where(Doctor) where?: Where): Promise { 59 | return this.doctorRepository.count(where); 60 | } 61 | 62 | @get('/doctors') 63 | @response(200, { 64 | description: 'Array of Doctor model instances', 65 | content: { 66 | 'application/json': { 67 | schema: { 68 | type: 'array', 69 | items: getModelSchemaRef(Doctor, {includeRelations: true}), 70 | }, 71 | }, 72 | }, 73 | }) 74 | async find(@param.filter(Doctor) filter?: Filter): Promise { 75 | return this.doctorRepository.find(filter); 76 | } 77 | 78 | @patch('/doctors') 79 | @response(200, { 80 | description: 'Doctor PATCH success count', 81 | content: {'application/json': {schema: CountSchema}}, 82 | }) 83 | async updateAll( 84 | @requestBody({ 85 | content: { 86 | 'application/json': { 87 | schema: getModelSchemaRef(Doctor, {partial: true}), 88 | }, 89 | }, 90 | }) 91 | doctor: Doctor, 92 | @param.where(Doctor) where?: Where, 93 | ): Promise { 94 | return this.doctorRepository.updateAll(doctor, where); 95 | } 96 | 97 | @get('/doctors/{id}') 98 | @response(200, { 99 | description: 'Doctor model instance', 100 | content: { 101 | 'application/json': { 102 | schema: getModelSchemaRef(Doctor, {includeRelations: true}), 103 | }, 104 | }, 105 | }) 106 | async findById( 107 | @param.path.number('id') id: number, 108 | @param.filter(Doctor, {exclude: 'where'}) 109 | filter?: FilterExcludingWhere, 110 | ): Promise { 111 | return this.doctorRepository.findById(id, filter); 112 | } 113 | 114 | @patch('/doctors/{id}') 115 | @response(204, { 116 | description: 'Doctor PATCH success', 117 | }) 118 | async updateById( 119 | @param.path.number('id') id: number, 120 | @requestBody({ 121 | content: { 122 | 'application/json': { 123 | schema: getModelSchemaRef(Doctor, {partial: true}), 124 | }, 125 | }, 126 | }) 127 | doctor: Doctor, 128 | ): Promise { 129 | await this.doctorRepository.updateById(id, doctor); 130 | } 131 | 132 | @put('/doctors/{id}') 133 | @response(204, { 134 | description: 'Doctor PUT success', 135 | }) 136 | async replaceById( 137 | @param.path.number('id') id: number, 138 | @requestBody() doctor: Doctor, 139 | ): Promise { 140 | await this.doctorRepository.replaceById(id, doctor); 141 | } 142 | 143 | @del('/doctors/{id}') 144 | @response(204, { 145 | description: 'Doctor DELETE success', 146 | }) 147 | async deleteById(@param.path.number('id') id: number): Promise { 148 | await this.doctorRepository.deleteById(id); 149 | } 150 | 151 | @get('/doctors/sync-sequelize-model') 152 | @response(200) 153 | async syncSequelizeModel(): Promise { 154 | await this.beforeEach({syncAll: true}); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/controllers/product.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Count, 3 | CountSchema, 4 | Filter, 5 | FilterExcludingWhere, 6 | repository, 7 | Where, 8 | } from '@loopback/repository'; 9 | import { 10 | del, 11 | get, 12 | getModelSchemaRef, 13 | param, 14 | patch, 15 | post, 16 | put, 17 | requestBody, 18 | response, 19 | } from '@loopback/rest'; 20 | import {Product} from '../models'; 21 | import {ProductRepository} from '../repositories'; 22 | import {TestControllerBase} from './test.controller.base'; 23 | 24 | export class ProductController extends TestControllerBase { 25 | constructor( 26 | @repository(ProductRepository) 27 | public productRepository: ProductRepository, 28 | ) { 29 | super(productRepository); 30 | } 31 | 32 | @post('/products') 33 | @response(200, { 34 | description: 'Product model instance', 35 | content: {'application/json': {schema: getModelSchemaRef(Product)}}, 36 | }) 37 | async create( 38 | @requestBody({ 39 | content: { 40 | 'application/json': { 41 | schema: getModelSchemaRef(Product, { 42 | title: 'NewProduct', 43 | exclude: ['id'], 44 | }), 45 | }, 46 | }, 47 | }) 48 | product: Omit, 49 | ): Promise { 50 | return this.productRepository.create(product); 51 | } 52 | 53 | @get('/products/count') 54 | @response(200, { 55 | description: 'Product model count', 56 | content: {'application/json': {schema: CountSchema}}, 57 | }) 58 | async count(@param.where(Product) where?: Where): Promise { 59 | return this.productRepository.count(where); 60 | } 61 | 62 | @get('/products') 63 | @response(200, { 64 | description: 'Array of Product model instances', 65 | content: { 66 | 'application/json': { 67 | schema: { 68 | type: 'array', 69 | items: getModelSchemaRef(Product, {includeRelations: true}), 70 | }, 71 | }, 72 | }, 73 | }) 74 | async find( 75 | @param.filter(Product) filter?: Filter, 76 | ): Promise { 77 | return this.productRepository.find(filter); 78 | } 79 | 80 | @patch('/products') 81 | @response(200, { 82 | description: 'Product PATCH success count', 83 | content: {'application/json': {schema: CountSchema}}, 84 | }) 85 | async updateAll( 86 | @requestBody({ 87 | content: { 88 | 'application/json': { 89 | schema: getModelSchemaRef(Product, {partial: true}), 90 | }, 91 | }, 92 | }) 93 | product: Product, 94 | @param.where(Product) where?: Where, 95 | ): Promise { 96 | return this.productRepository.updateAll(product, where); 97 | } 98 | 99 | @get('/products/{id}') 100 | @response(200, { 101 | description: 'Product model instance', 102 | content: { 103 | 'application/json': { 104 | schema: getModelSchemaRef(Product, {includeRelations: true}), 105 | }, 106 | }, 107 | }) 108 | async findById( 109 | @param.path.number('id') id: number, 110 | @param.filter(Product, {exclude: 'where'}) 111 | filter?: FilterExcludingWhere, 112 | ): Promise { 113 | return this.productRepository.findById(id, filter); 114 | } 115 | 116 | @patch('/products/{id}') 117 | @response(204, { 118 | description: 'Product PATCH success', 119 | }) 120 | async updateById( 121 | @param.path.number('id') id: number, 122 | @requestBody({ 123 | content: { 124 | 'application/json': { 125 | schema: getModelSchemaRef(Product, {partial: true}), 126 | }, 127 | }, 128 | }) 129 | product: Product, 130 | ): Promise { 131 | await this.productRepository.updateById(id, product); 132 | } 133 | 134 | @put('/products/{id}') 135 | @response(204, { 136 | description: 'Product PUT success', 137 | }) 138 | async replaceById( 139 | @param.path.number('id') id: number, 140 | @requestBody() product: Product, 141 | ): Promise { 142 | await this.productRepository.replaceById(id, product); 143 | } 144 | 145 | @del('/products/{id}') 146 | @response(204, { 147 | description: 'Product DELETE success', 148 | }) 149 | async deleteById(@param.path.number('id') id: number): Promise { 150 | await this.productRepository.deleteById(id); 151 | } 152 | 153 | @get('/products/sync-sequelize-model') 154 | @response(200) 155 | async syncSequelizeModel(): Promise { 156 | await this.beforeEach(); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/controllers/todo-list.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Count, 3 | CountSchema, 4 | Filter, 5 | FilterExcludingWhere, 6 | repository, 7 | Where, 8 | } from '@loopback/repository'; 9 | import { 10 | del, 11 | get, 12 | getModelSchemaRef, 13 | param, 14 | patch, 15 | post, 16 | put, 17 | requestBody, 18 | response, 19 | } from '@loopback/rest'; 20 | import {TodoList} from '../models'; 21 | import {TodoListRepository} from '../repositories'; 22 | import {TestControllerBase} from './test.controller.base'; 23 | 24 | export class TodoListController extends TestControllerBase { 25 | constructor( 26 | @repository(TodoListRepository) 27 | public todoListRepository: TodoListRepository, 28 | ) { 29 | super(todoListRepository); 30 | } 31 | 32 | @post('/todo-lists') 33 | @response(200, { 34 | description: 'TodoList model instance', 35 | content: {'application/json': {schema: getModelSchemaRef(TodoList)}}, 36 | }) 37 | async create( 38 | @requestBody({ 39 | content: { 40 | 'application/json': { 41 | schema: getModelSchemaRef(TodoList, { 42 | title: 'NewTodoList', 43 | exclude: ['id'], 44 | }), 45 | }, 46 | }, 47 | }) 48 | todoList: Omit, 49 | ): Promise { 50 | return this.todoListRepository.create(todoList); 51 | } 52 | 53 | @get('/todo-lists/count') 54 | @response(200, { 55 | description: 'TodoList model count', 56 | content: {'application/json': {schema: CountSchema}}, 57 | }) 58 | async count(@param.where(TodoList) where?: Where): Promise { 59 | return this.todoListRepository.count(where); 60 | } 61 | 62 | @get('/todo-lists') 63 | @response(200, { 64 | description: 'Array of TodoList model instances', 65 | content: { 66 | 'application/json': { 67 | schema: { 68 | type: 'array', 69 | items: getModelSchemaRef(TodoList, {includeRelations: true}), 70 | }, 71 | }, 72 | }, 73 | }) 74 | async find( 75 | @param.filter(TodoList) filter?: Filter, 76 | ): Promise { 77 | return this.todoListRepository.find(filter); 78 | } 79 | 80 | @patch('/todo-lists') 81 | @response(200, { 82 | description: 'TodoList PATCH success count', 83 | content: {'application/json': {schema: CountSchema}}, 84 | }) 85 | async updateAll( 86 | @requestBody({ 87 | content: { 88 | 'application/json': { 89 | schema: getModelSchemaRef(TodoList, {partial: true}), 90 | }, 91 | }, 92 | }) 93 | todoList: TodoList, 94 | @param.where(TodoList) where?: Where, 95 | ): Promise { 96 | return this.todoListRepository.updateAll(todoList, where); 97 | } 98 | 99 | @get('/todo-lists/{id}') 100 | @response(200, { 101 | description: 'TodoList model instance', 102 | content: { 103 | 'application/json': { 104 | schema: getModelSchemaRef(TodoList, {includeRelations: true}), 105 | }, 106 | }, 107 | }) 108 | async findById( 109 | @param.path.number('id') id: number, 110 | @param.filter(TodoList, {exclude: 'where'}) 111 | filter?: FilterExcludingWhere, 112 | ): Promise { 113 | return this.todoListRepository.findById(id, filter); 114 | } 115 | 116 | @patch('/todo-lists/{id}') 117 | @response(204, { 118 | description: 'TodoList PATCH success', 119 | }) 120 | async updateById( 121 | @param.path.number('id') id: number, 122 | @requestBody({ 123 | content: { 124 | 'application/json': { 125 | schema: getModelSchemaRef(TodoList, {partial: true}), 126 | }, 127 | }, 128 | }) 129 | todoList: TodoList, 130 | ): Promise { 131 | await this.todoListRepository.updateById(id, todoList); 132 | } 133 | 134 | @put('/todo-lists/{id}') 135 | @response(204, { 136 | description: 'TodoList PUT success', 137 | }) 138 | async replaceById( 139 | @param.path.number('id') id: number, 140 | @requestBody() todoList: TodoList, 141 | ): Promise { 142 | await this.todoListRepository.replaceById(id, todoList); 143 | } 144 | 145 | @del('/todo-lists/{id}') 146 | @response(204, { 147 | description: 'TodoList DELETE success', 148 | }) 149 | async deleteById(@param.path.number('id') id: number): Promise { 150 | await this.todoListRepository.deleteById(id); 151 | } 152 | 153 | @get('/todo-lists/sync-sequelize-model') 154 | @response(200) 155 | async syncSequelizeModel(): Promise { 156 | await this.beforeEach({syncAll: true}); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/controllers/developer.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Count, 3 | CountSchema, 4 | Filter, 5 | FilterExcludingWhere, 6 | repository, 7 | Where, 8 | } from '@loopback/repository'; 9 | import { 10 | del, 11 | get, 12 | getModelSchemaRef, 13 | param, 14 | patch, 15 | post, 16 | put, 17 | requestBody, 18 | response, 19 | } from '@loopback/rest'; 20 | import {Developer} from '../models'; 21 | import {DeveloperRepository} from '../repositories'; 22 | import {TestControllerBase} from './test.controller.base'; 23 | 24 | export class DeveloperController extends TestControllerBase { 25 | constructor( 26 | @repository(DeveloperRepository) 27 | public developerRepository: DeveloperRepository, 28 | ) { 29 | super(developerRepository); 30 | } 31 | 32 | @post('/developers') 33 | @response(200, { 34 | description: 'Developer model instance', 35 | content: {'application/json': {schema: getModelSchemaRef(Developer)}}, 36 | }) 37 | async create( 38 | @requestBody({ 39 | content: { 40 | 'application/json': { 41 | schema: getModelSchemaRef(Developer, { 42 | title: 'NewDeveloper', 43 | exclude: ['id'], 44 | }), 45 | }, 46 | }, 47 | }) 48 | developer: Omit, 49 | ): Promise { 50 | return this.developerRepository.create(developer); 51 | } 52 | 53 | @get('/developers/count') 54 | @response(200, { 55 | description: 'Developer model count', 56 | content: {'application/json': {schema: CountSchema}}, 57 | }) 58 | async count( 59 | @param.where(Developer) where?: Where, 60 | ): Promise { 61 | return this.developerRepository.count(where); 62 | } 63 | 64 | @get('/developers') 65 | @response(200, { 66 | description: 'Array of Developer model instances', 67 | content: { 68 | 'application/json': { 69 | schema: { 70 | type: 'array', 71 | items: getModelSchemaRef(Developer, {includeRelations: true}), 72 | }, 73 | }, 74 | }, 75 | }) 76 | async find( 77 | @param.filter(Developer) filter?: Filter, 78 | ): Promise { 79 | return this.developerRepository.find(filter); 80 | } 81 | 82 | @patch('/developers') 83 | @response(200, { 84 | description: 'Developer PATCH success count', 85 | content: {'application/json': {schema: CountSchema}}, 86 | }) 87 | async updateAll( 88 | @requestBody({ 89 | content: { 90 | 'application/json': { 91 | schema: getModelSchemaRef(Developer, {partial: true}), 92 | }, 93 | }, 94 | }) 95 | developer: Developer, 96 | @param.where(Developer) where?: Where, 97 | ): Promise { 98 | return this.developerRepository.updateAll(developer, where); 99 | } 100 | 101 | @get('/developers/{id}') 102 | @response(200, { 103 | description: 'Developer model instance', 104 | content: { 105 | 'application/json': { 106 | schema: getModelSchemaRef(Developer, {includeRelations: true}), 107 | }, 108 | }, 109 | }) 110 | async findById( 111 | @param.path.number('id') id: number, 112 | @param.filter(Developer, {exclude: 'where'}) 113 | filter?: FilterExcludingWhere, 114 | ): Promise { 115 | return this.developerRepository.findById(id, filter); 116 | } 117 | 118 | @patch('/developers/{id}') 119 | @response(204, { 120 | description: 'Developer PATCH success', 121 | }) 122 | async updateById( 123 | @param.path.number('id') id: number, 124 | @requestBody({ 125 | content: { 126 | 'application/json': { 127 | schema: getModelSchemaRef(Developer, {partial: true}), 128 | }, 129 | }, 130 | }) 131 | developer: Developer, 132 | ): Promise { 133 | await this.developerRepository.updateById(id, developer); 134 | } 135 | 136 | @put('/developers/{id}') 137 | @response(204, { 138 | description: 'Developer PUT success', 139 | }) 140 | async replaceById( 141 | @param.path.number('id') id: number, 142 | @requestBody() developer: Developer, 143 | ): Promise { 144 | await this.developerRepository.replaceById(id, developer); 145 | } 146 | 147 | @del('/developers/{id}') 148 | @response(204, { 149 | description: 'Developer DELETE success', 150 | }) 151 | async deleteById(@param.path.number('id') id: number): Promise { 152 | await this.developerRepository.deleteById(id); 153 | } 154 | 155 | @get('/developers/sync-sequelize-model') 156 | @response(200) 157 | async syncSequelizeModel(): Promise { 158 | await this.beforeEach({syncAll: true}); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback4-sequelize", 3 | "version": "2.3.0", 4 | "description": "Loopback 4 Extension That Provides Sequelize Crud Repository Compatible With Default Loopback Models.", 5 | "keywords": [ 6 | "loopback-extension", 7 | "loopback", 8 | "loopback4-sequelize", 9 | "sequelize", 10 | "repository" 11 | ], 12 | "main": "dist/index.js", 13 | "types": "dist/index.d.ts", 14 | "engines": { 15 | "node": "14 || 16 || 17 || 18" 16 | }, 17 | "scripts": { 18 | "build": "lb-tsc", 19 | "build:watch": "lb-tsc --watch", 20 | "lint": "npm run eslint && npm run prettier:check", 21 | "lint:fix": "npm run eslint:fix && npm run prettier:fix", 22 | "prettier:cli": "lb-prettier \"**/*.ts\" \"**/*.js\"", 23 | "prettier:check": "npm run prettier:cli -- -l", 24 | "prettier:fix": "npm run prettier:cli -- --write", 25 | "eslint": "lb-eslint --report-unused-disable-directives .", 26 | "eslint:fix": "npm run eslint -- --fix", 27 | "pretest": "npm run rebuild", 28 | "test": "lb-mocha --allow-console-logs \"dist/__tests__\"", 29 | "posttest": "npm run lint", 30 | "test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js && npm run posttest", 31 | "clean": "lb-clean dist *.tsbuildinfo .eslintcache", 32 | "prepare": "husky install", 33 | "rebuild": "npm run clean && npm run build", 34 | "export-api-ref": "npx typedoc" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/sourcefuse/loopback4-sequelize" 39 | }, 40 | "author": "Sourcefuse", 41 | "license": "MIT", 42 | "files": [ 43 | "README.md", 44 | "dist", 45 | "src", 46 | "!*/__tests__" 47 | ], 48 | "peerDependencies": { 49 | "@loopback/core": "^4.0.9", 50 | "@loopback/repository": "^5.1.4", 51 | "@loopback/rest": "^12.0.9" 52 | }, 53 | "dependencies": { 54 | "debug": "^4.3.4", 55 | "sequelize": "^6.28.0", 56 | "tslib": "^2.0.0", 57 | "uuid": "^9.0.0" 58 | }, 59 | "devDependencies": { 60 | "@commitlint/cli": "^17.4.2", 61 | "@commitlint/config-conventional": "^17.4.2", 62 | "@loopback/boot": "^5.0.9", 63 | "@loopback/build": "^9.0.9", 64 | "@loopback/core": "^4.0.9", 65 | "@loopback/eslint-config": "^13.0.9", 66 | "@loopback/repository": "^5.1.4", 67 | "@loopback/rest": "^12.0.9", 68 | "@loopback/testlab": "^5.0.9", 69 | "@semantic-release/changelog": "^6.0.2", 70 | "@semantic-release/commit-analyzer": "^9.0.2", 71 | "@semantic-release/git": "^10.0.1", 72 | "@semantic-release/npm": "^9.0.2", 73 | "@semantic-release/release-notes-generator": "^10.0.3", 74 | "@types/lodash": "^4.14.195", 75 | "@types/node": "^14.18.36", 76 | "@types/uuid": "^9.0.2", 77 | "commitizen": "^4.3.0", 78 | "cz-conventional-changelog": "^3.3.0", 79 | "cz-customizable": "^7.0.0", 80 | "cz-customizable-ghooks": "^2.0.0", 81 | "eslint": "^8.35.0", 82 | "git-release-notes": "^5.0.0", 83 | "husky": "^8.0.3", 84 | "jsdom": "^21.1.0", 85 | "lodash": "^4.17.21", 86 | "pg": "^8.8.0", 87 | "pg-hstore": "^2.3.4", 88 | "semantic-release": "^19.0.5", 89 | "simple-git": "^3.16.1", 90 | "source-map-support": "^0.5.21", 91 | "sqlite3": "5.1.4", 92 | "typedoc": "^0.24.0", 93 | "typedoc-plugin-markdown": "^3.15.3", 94 | "typescript": "~4.9.4" 95 | }, 96 | "config": { 97 | "commitizen": { 98 | "path": "./node_modules/cz-customizable" 99 | } 100 | }, 101 | "publishConfig": { 102 | "registry": "https://registry.npmjs.org/" 103 | }, 104 | "overrides": { 105 | "git-release-notes": { 106 | "ejs": "^3.1.8", 107 | "yargs": "^17.6.2" 108 | }, 109 | "@semantic-release/npm": { 110 | "npm": "^9.4.2" 111 | } 112 | }, 113 | "release": { 114 | "branches": [ 115 | "master" 116 | ], 117 | "plugins": [ 118 | [ 119 | "@semantic-release/commit-analyzer", 120 | { 121 | "preset": "angular", 122 | "releaseRules": [ 123 | { 124 | "type": "chore", 125 | "scope": "deps", 126 | "release": "patch" 127 | } 128 | ] 129 | } 130 | ], 131 | "@semantic-release/release-notes-generator", 132 | "@semantic-release/changelog", 133 | "@semantic-release/npm", 134 | [ 135 | "@semantic-release/git", 136 | { 137 | "assets": [ 138 | "package.json", 139 | "CHANGELOG.md" 140 | ], 141 | "message": "chore(release): ${nextRelease.version} semantic" 142 | } 143 | ], 144 | "@semantic-release/github" 145 | ], 146 | "repositoryUrl": "git@github.com:sourcefuse/loopback4-sequelize.git" 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/controllers/transaction.controller.ts: -------------------------------------------------------------------------------- 1 | import {AnyObject, repository} from '@loopback/repository'; 2 | import { 3 | get, 4 | getModelSchemaRef, 5 | HttpErrors, 6 | post, 7 | requestBody, 8 | } from '@loopback/rest'; 9 | import {TodoList} from '../models'; 10 | import {ProductRepository, TodoListRepository} from '../repositories'; 11 | import {Transaction} from './../../../types'; 12 | import {TestControllerBase} from './test.controller.base'; 13 | 14 | export class TransactionController extends TestControllerBase { 15 | constructor( 16 | @repository(TodoListRepository) 17 | public todoListRepository: TodoListRepository, 18 | @repository(ProductRepository) 19 | public productRepository: ProductRepository, 20 | ) { 21 | super(todoListRepository, productRepository); 22 | } 23 | 24 | // create todo-list entry using transaction 25 | @post('/transactions/todo-lists/commit') 26 | async ensureTransactionCommit( 27 | @requestBody({ 28 | content: { 29 | 'application/json': { 30 | schema: getModelSchemaRef(TodoList, { 31 | title: 'NewTodoList', 32 | exclude: ['id'], 33 | }), 34 | }, 35 | }, 36 | }) 37 | todoList: Omit, 38 | ): Promise { 39 | const tx = await this.todoListRepository.beginTransaction({ 40 | isolationLevel: Transaction.ISOLATION_LEVELS.SERIALIZABLE, 41 | }); 42 | 43 | try { 44 | const created = await this.todoListRepository.create(todoList, { 45 | transaction: tx, 46 | }); 47 | await tx.commit(); 48 | return created; 49 | } catch (err) { 50 | await tx.rollback(); 51 | throw err; 52 | } 53 | } 54 | 55 | // create todo-list entry using transaction but rollback 56 | @post('/transactions/todo-lists/rollback') 57 | async ensureRollback( 58 | @requestBody({ 59 | content: { 60 | 'application/json': { 61 | schema: getModelSchemaRef(TodoList, { 62 | title: 'NewTodoList', 63 | exclude: ['id'], 64 | }), 65 | }, 66 | }, 67 | }) 68 | todoList: Omit, 69 | ): Promise { 70 | const tx = await this.todoListRepository.beginTransaction({ 71 | isolationLevel: Transaction.ISOLATION_LEVELS.READ_COMMITTED, 72 | }); 73 | 74 | const created = await this.todoListRepository.create(todoList, { 75 | transaction: tx, 76 | }); 77 | await tx.rollback(); 78 | 79 | // In real applications if you're rolling back. Don't return created entities to user 80 | // For test cases it's required here. (to get the id) 81 | return created; 82 | } 83 | 84 | // create todo-list entry using transaction but don't commit or rollback 85 | @post('/transactions/todo-lists/isolation/read_commited') 86 | async ensureIsolatedTransaction( 87 | @requestBody({ 88 | content: { 89 | 'application/json': { 90 | schema: getModelSchemaRef(TodoList, { 91 | title: 'NewTodoList', 92 | exclude: ['id'], 93 | }), 94 | }, 95 | }, 96 | }) 97 | todoList: Omit, 98 | ): Promise { 99 | const tx = await this.todoListRepository.beginTransaction({ 100 | isolationLevel: Transaction.ISOLATION_LEVELS.READ_COMMITTED, 101 | }); 102 | 103 | const created = await this.todoListRepository.create(todoList, { 104 | transaction: tx, 105 | }); 106 | 107 | let err: AnyObject = {}; 108 | 109 | // reading before commit in READ_COMMITED level should not find the entity 110 | const findBeforeCommit = await this.todoListRepository 111 | .findById(created.id) 112 | .catch(e => (err = e)); 113 | 114 | await tx.commit(); 115 | 116 | // throwing it after commit to avoid deadlocks 117 | if (err) { 118 | throw err; 119 | } 120 | return findBeforeCommit as TodoList; 121 | } 122 | 123 | @get('/transactions/ensure-local') 124 | async ensureLocalTransactions(): Promise { 125 | // "Todo List" model is from Primary Datasource 126 | // and "AnyObject" model is from Secondary Datasource 127 | // this test case is to ensure transaction created on 128 | // one datasource can't be used in another 129 | const tx = await this.todoListRepository.beginTransaction({ 130 | isolationLevel: Transaction.ISOLATION_LEVELS.SERIALIZABLE, 131 | }); 132 | 133 | let err: AnyObject | null = null; 134 | 135 | try { 136 | await this.productRepository.create( 137 | { 138 | name: 'phone', 139 | price: 5000, 140 | }, 141 | { 142 | transaction: tx, 143 | }, 144 | ); 145 | } catch (e) { 146 | err = e; 147 | } 148 | 149 | await tx.commit(); 150 | 151 | if (err) { 152 | throw new HttpErrors[406](err.message); 153 | } 154 | 155 | // Won't reach till here if test passes 156 | throw new HttpErrors[406]('Product created with non-local transaction.'); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/controllers/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Count, 3 | CountSchema, 4 | Filter, 5 | FilterExcludingWhere, 6 | repository, 7 | Where, 8 | } from '@loopback/repository'; 9 | import { 10 | del, 11 | get, 12 | getModelSchemaRef, 13 | param, 14 | patch, 15 | post, 16 | put, 17 | requestBody, 18 | response, 19 | } from '@loopback/rest'; 20 | import {User} from '../models'; 21 | import {UserRepository} from '../repositories'; 22 | import {TestControllerBase} from './test.controller.base'; 23 | 24 | export class UserController extends TestControllerBase { 25 | constructor( 26 | @repository(UserRepository) 27 | public userRepository: UserRepository, 28 | ) { 29 | super(userRepository); 30 | } 31 | 32 | @post('/users') 33 | @response(200, { 34 | description: 'User model instance', 35 | content: {'application/json': {schema: getModelSchemaRef(User)}}, 36 | }) 37 | async create( 38 | @requestBody({ 39 | content: { 40 | 'application/json': { 41 | schema: getModelSchemaRef(User, { 42 | title: 'NewUser', 43 | exclude: ['id'], 44 | }), 45 | }, 46 | }, 47 | }) 48 | user: Omit, 49 | ): Promise { 50 | return this.userRepository.create(user); 51 | } 52 | 53 | @post('/users-bulk') 54 | @response(200, { 55 | description: 'User model instances', 56 | content: { 57 | 'application/json': { 58 | schema: { 59 | type: 'array', 60 | items: getModelSchemaRef(User), 61 | }, 62 | }, 63 | }, 64 | }) 65 | async createAll( 66 | @requestBody({ 67 | content: { 68 | 'application/json': { 69 | schema: { 70 | type: 'array', 71 | items: getModelSchemaRef(User, { 72 | title: 'NewUser', 73 | exclude: ['id'], 74 | }), 75 | }, 76 | }, 77 | }, 78 | }) 79 | users: Array>, 80 | ): Promise { 81 | return this.userRepository.createAll(users); 82 | } 83 | 84 | @get('/users/count') 85 | @response(200, { 86 | description: 'User model count', 87 | content: {'application/json': {schema: CountSchema}}, 88 | }) 89 | async count(@param.where(User) where?: Where): Promise { 90 | return this.userRepository.count(where); 91 | } 92 | 93 | @get('/users') 94 | @response(200, { 95 | description: 'Array of User model instances', 96 | content: { 97 | 'application/json': { 98 | schema: { 99 | type: 'array', 100 | items: getModelSchemaRef(User, {includeRelations: true}), 101 | }, 102 | }, 103 | }, 104 | }) 105 | async find(@param.filter(User) filter?: Filter): Promise { 106 | return this.userRepository.find(filter); 107 | } 108 | 109 | @patch('/users') 110 | @response(200, { 111 | description: 'User PATCH success count', 112 | content: {'application/json': {schema: CountSchema}}, 113 | }) 114 | async updateAll( 115 | @requestBody({ 116 | content: { 117 | 'application/json': { 118 | schema: getModelSchemaRef(User, {partial: true}), 119 | }, 120 | }, 121 | }) 122 | user: User, 123 | @param.where(User) where?: Where, 124 | ): Promise { 125 | return this.userRepository.updateAll(user, where); 126 | } 127 | 128 | @get('/users/{id}') 129 | @response(200, { 130 | description: 'User model instance', 131 | content: { 132 | 'application/json': { 133 | schema: getModelSchemaRef(User, {includeRelations: true}), 134 | }, 135 | }, 136 | }) 137 | async findById( 138 | @param.path.number('id') id: number, 139 | @param.filter(User, {exclude: 'where'}) filter?: FilterExcludingWhere, 140 | ): Promise { 141 | return this.userRepository.findById(id, filter); 142 | } 143 | 144 | @patch('/users/{id}') 145 | @response(204, { 146 | description: 'User PATCH success', 147 | }) 148 | async updateById( 149 | @param.path.number('id') id: number, 150 | @requestBody({ 151 | content: { 152 | 'application/json': { 153 | schema: getModelSchemaRef(User, {partial: true}), 154 | }, 155 | }, 156 | }) 157 | user: User, 158 | ): Promise { 159 | await this.userRepository.updateById(id, user); 160 | } 161 | 162 | @put('/users/{id}') 163 | @response(204, { 164 | description: 'User PUT success', 165 | }) 166 | async replaceById( 167 | @param.path.number('id') id: number, 168 | @requestBody() user: User, 169 | ): Promise { 170 | await this.userRepository.replaceById(id, user); 171 | } 172 | 173 | @del('/users/{id}') 174 | @response(204, { 175 | description: 'User DELETE success', 176 | }) 177 | async deleteById(@param.path.number('id') id: number): Promise { 178 | await this.userRepository.deleteById(id); 179 | } 180 | 181 | @get('/users/sync-sequelize-model') 182 | @response(200) 183 | async syncSequelizeModel(): Promise { 184 | await this.beforeEach(); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/controllers/task.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Count, 3 | CountSchema, 4 | Filter, 5 | FilterExcludingWhere, 6 | repository, 7 | Where, 8 | } from '@loopback/repository'; 9 | import { 10 | del, 11 | get, 12 | getModelSchemaRef, 13 | param, 14 | patch, 15 | post, 16 | put, 17 | requestBody, 18 | response, 19 | } from '@loopback/rest'; 20 | import {Task} from '../models'; 21 | import {TaskRepository} from '../repositories'; 22 | import {TestControllerBase} from './test.controller.base'; 23 | export class TaskController extends TestControllerBase { 24 | constructor( 25 | @repository(TaskRepository) 26 | public taskRepository: TaskRepository, 27 | ) { 28 | super(taskRepository); 29 | } 30 | 31 | @post('/tasks') 32 | @response(200, { 33 | description: 'task model instance', 34 | content: {'application/json': {schema: getModelSchemaRef(Task)}}, 35 | }) 36 | async create( 37 | @requestBody({ 38 | content: { 39 | 'application/json': { 40 | schema: getModelSchemaRef(Task, { 41 | title: 'NewTask', 42 | exclude: ['id'], 43 | }), 44 | }, 45 | }, 46 | }) 47 | task: Omit, 48 | ): Promise { 49 | return this.taskRepository.create(task); 50 | } 51 | 52 | @post('/tasks-bulk') 53 | @response(200, { 54 | description: 'task model instances', 55 | content: { 56 | 'application/json': { 57 | schema: { 58 | type: 'array', 59 | items: getModelSchemaRef(Task), 60 | }, 61 | }, 62 | }, 63 | }) 64 | async createAll( 65 | @requestBody({ 66 | content: { 67 | 'application/json': { 68 | schema: { 69 | type: 'array', 70 | items: getModelSchemaRef(Task, { 71 | title: 'NewTask', 72 | exclude: ['id'], 73 | }), 74 | }, 75 | }, 76 | }, 77 | }) 78 | tasks: Array>, 79 | ): Promise { 80 | return this.taskRepository.createAll(tasks); 81 | } 82 | 83 | @get('/tasks/count') 84 | @response(200, { 85 | description: 'Task model count', 86 | content: {'application/json': {schema: CountSchema}}, 87 | }) 88 | async count(@param.where(Task) where?: Where): Promise { 89 | return this.taskRepository.count(where); 90 | } 91 | 92 | @get('/tasks') 93 | @response(200, { 94 | description: 'Array of Task model instances', 95 | content: { 96 | 'application/json': { 97 | schema: { 98 | type: 'array', 99 | items: getModelSchemaRef(Task, {includeRelations: true}), 100 | }, 101 | }, 102 | }, 103 | }) 104 | async find(@param.filter(Task) filter?: Filter): Promise { 105 | return this.taskRepository.find(filter); 106 | } 107 | 108 | @patch('/tasks') 109 | @response(200, { 110 | description: 'Task PATCH success count', 111 | content: {'application/json': {schema: CountSchema}}, 112 | }) 113 | async updateAll( 114 | @requestBody({ 115 | content: { 116 | 'application/json': { 117 | schema: getModelSchemaRef(Task, {partial: true}), 118 | }, 119 | }, 120 | }) 121 | task: Task, 122 | @param.where(Task) where?: Where, 123 | ): Promise { 124 | return this.taskRepository.updateAll(task, where); 125 | } 126 | 127 | @get('/tasks/{id}') 128 | @response(200, { 129 | description: 'Task model instance', 130 | content: { 131 | 'application/json': { 132 | schema: getModelSchemaRef(Task, {includeRelations: true}), 133 | }, 134 | }, 135 | }) 136 | async findById( 137 | @param.path.number('id') id: number, 138 | @param.filter(Task, {exclude: 'where'}) 139 | filter?: FilterExcludingWhere, 140 | ): Promise { 141 | return this.taskRepository.findById(id, filter); 142 | } 143 | 144 | @patch('/tasks/{id}') 145 | @response(204, { 146 | description: 'Task PATCH success', 147 | }) 148 | async updateById( 149 | @param.path.number('id') id: number, 150 | @requestBody({ 151 | content: { 152 | 'application/json': { 153 | schema: getModelSchemaRef(Task, {partial: true}), 154 | }, 155 | }, 156 | }) 157 | task: Task, 158 | ): Promise { 159 | await this.taskRepository.updateById(id, task); 160 | } 161 | 162 | @put('/tasks/{id}') 163 | @response(204, { 164 | description: 'Task PUT success', 165 | }) 166 | async replaceById( 167 | @param.path.number('id') id: number, 168 | @requestBody() task: Task, 169 | ): Promise { 170 | await this.taskRepository.replaceById(id, task); 171 | } 172 | 173 | @del('/tasks/{id}') 174 | @response(204, { 175 | description: 'Task DELETE success', 176 | }) 177 | async deleteById(@param.path.number('id') id: number): Promise { 178 | await this.taskRepository.deleteById(id); 179 | } 180 | 181 | @get('/tasks/sync-sequelize-model') 182 | @response(200) 183 | async syncSequelizeModel(): Promise { 184 | await this.beforeEach(); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/controllers/book.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Count, 3 | CountSchema, 4 | Filter, 5 | FilterExcludingWhere, 6 | repository, 7 | Where, 8 | } from '@loopback/repository'; 9 | import { 10 | del, 11 | get, 12 | getModelSchemaRef, 13 | param, 14 | patch, 15 | post, 16 | put, 17 | requestBody, 18 | response, 19 | } from '@loopback/rest'; 20 | import {Book} from '../models'; 21 | import {BookRepository} from '../repositories'; 22 | import {TestControllerBase} from './test.controller.base'; 23 | 24 | export class BookController extends TestControllerBase { 25 | constructor( 26 | @repository(BookRepository) 27 | public bookRepository: BookRepository, 28 | ) { 29 | super(bookRepository); 30 | } 31 | 32 | @post('/books') 33 | @response(200, { 34 | description: 'Book model instance', 35 | content: {'application/json': {schema: getModelSchemaRef(Book)}}, 36 | }) 37 | async create( 38 | @requestBody({ 39 | content: { 40 | 'application/json': { 41 | schema: getModelSchemaRef(Book, { 42 | title: 'NewBook', 43 | exclude: ['id'], 44 | }), 45 | }, 46 | }, 47 | }) 48 | book: Omit, 49 | ): Promise { 50 | return this.bookRepository.create(book); 51 | } 52 | 53 | @post('/books-bulk') 54 | @response(200, { 55 | description: 'Book model instances', 56 | content: { 57 | 'application/json': { 58 | schema: { 59 | type: 'array', 60 | items: getModelSchemaRef(Book), 61 | }, 62 | }, 63 | }, 64 | }) 65 | async createAll( 66 | @requestBody({ 67 | content: { 68 | 'application/json': { 69 | schema: { 70 | type: 'array', 71 | items: getModelSchemaRef(Book, { 72 | title: 'NewBook', 73 | exclude: ['id'], 74 | }), 75 | }, 76 | }, 77 | }, 78 | }) 79 | books: Array>, 80 | ): Promise { 81 | return this.bookRepository.createAll(books); 82 | } 83 | 84 | @get('/books/count') 85 | @response(200, { 86 | description: 'Book model count', 87 | content: {'application/json': {schema: CountSchema}}, 88 | }) 89 | async count(@param.where(Book) where?: Where): Promise { 90 | return this.bookRepository.count(where); 91 | } 92 | 93 | @get('/books') 94 | @response(200, { 95 | description: 'Array of Book model instances', 96 | content: { 97 | 'application/json': { 98 | schema: { 99 | type: 'array', 100 | items: getModelSchemaRef(Book, {includeRelations: true}), 101 | }, 102 | }, 103 | }, 104 | }) 105 | async find(@param.filter(Book) filter?: Filter): Promise { 106 | return this.bookRepository.find(filter); 107 | } 108 | 109 | @patch('/books') 110 | @response(200, { 111 | description: 'Book PATCH success count', 112 | content: {'application/json': {schema: CountSchema}}, 113 | }) 114 | async updateAll( 115 | @requestBody({ 116 | content: { 117 | 'application/json': { 118 | schema: getModelSchemaRef(Book, {partial: true}), 119 | }, 120 | }, 121 | }) 122 | book: Book, 123 | @param.where(Book) where?: Where, 124 | ): Promise { 125 | return this.bookRepository.updateAll(book, where); 126 | } 127 | 128 | @get('/books/{id}') 129 | @response(200, { 130 | description: 'Book model instance', 131 | content: { 132 | 'application/json': { 133 | schema: getModelSchemaRef(Book, {includeRelations: true}), 134 | }, 135 | }, 136 | }) 137 | async findById( 138 | @param.path.number('id') id: number, 139 | @param.filter(Book, {exclude: 'where'}) filter?: FilterExcludingWhere, 140 | ): Promise { 141 | return this.bookRepository.findById(id, filter); 142 | } 143 | 144 | @patch('/books/{id}') 145 | @response(204, { 146 | description: 'Book PATCH success', 147 | }) 148 | async updateById( 149 | @param.path.number('id') id: number, 150 | @requestBody({ 151 | content: { 152 | 'application/json': { 153 | schema: getModelSchemaRef(Book, {partial: true}), 154 | }, 155 | }, 156 | }) 157 | book: Book, 158 | ): Promise { 159 | await this.bookRepository.updateById(id, book); 160 | } 161 | 162 | @put('/books/{id}') 163 | @response(204, { 164 | description: 'Book PUT success', 165 | }) 166 | async replaceById( 167 | @param.path.number('id') id: number, 168 | @requestBody() book: Book, 169 | ): Promise { 170 | await this.bookRepository.replaceById(id, book); 171 | } 172 | 173 | @del('/books/{id}') 174 | @response(204, { 175 | description: 'Book DELETE success', 176 | }) 177 | async deleteById(@param.path.number('id') id: number): Promise { 178 | await this.bookRepository.deleteById(id); 179 | } 180 | 181 | @get('/books/sync-sequelize-model') 182 | @response(200) 183 | async syncSequelizeModel(): Promise { 184 | await this.beforeEach({syncAll: true}); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/controllers/category.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Count, 3 | CountSchema, 4 | Filter, 5 | FilterExcludingWhere, 6 | repository, 7 | Where, 8 | } from '@loopback/repository'; 9 | import { 10 | del, 11 | get, 12 | getModelSchemaRef, 13 | param, 14 | patch, 15 | post, 16 | put, 17 | requestBody, 18 | response, 19 | } from '@loopback/rest'; 20 | import {Category} from '../models'; 21 | import {CategoryRepository} from '../repositories'; 22 | 23 | export class CategoryController { 24 | constructor( 25 | @repository(CategoryRepository) 26 | public categoryRepository: CategoryRepository, 27 | ) {} 28 | 29 | @post('/categories') 30 | @response(200, { 31 | description: 'Category model instance', 32 | content: {'application/json': {schema: getModelSchemaRef(Category)}}, 33 | }) 34 | async create( 35 | @requestBody({ 36 | content: { 37 | 'application/json': { 38 | schema: getModelSchemaRef(Category, { 39 | title: 'NewCategory', 40 | exclude: ['id'], 41 | }), 42 | }, 43 | }, 44 | }) 45 | category: Omit, 46 | ): Promise { 47 | return this.categoryRepository.create(category); 48 | } 49 | 50 | @post('/categories-bulk') 51 | @response(200, { 52 | description: 'Category model instances', 53 | content: { 54 | 'application/json': { 55 | schema: { 56 | type: 'array', 57 | items: getModelSchemaRef(Category), 58 | }, 59 | }, 60 | }, 61 | }) 62 | async createAll( 63 | @requestBody({ 64 | content: { 65 | 'application/json': { 66 | schema: { 67 | type: 'array', 68 | items: getModelSchemaRef(Category, { 69 | title: 'NewCategory', 70 | exclude: ['id'], 71 | }), 72 | }, 73 | }, 74 | }, 75 | }) 76 | categories: Array>, 77 | ): Promise { 78 | return this.categoryRepository.createAll(categories); 79 | } 80 | 81 | @get('/categories/count') 82 | @response(200, { 83 | description: 'Category model count', 84 | content: {'application/json': {schema: CountSchema}}, 85 | }) 86 | async count(@param.where(Category) where?: Where): Promise { 87 | return this.categoryRepository.count(where); 88 | } 89 | 90 | @get('/categories') 91 | @response(200, { 92 | description: 'Array of Category model instances', 93 | content: { 94 | 'application/json': { 95 | schema: { 96 | type: 'array', 97 | items: getModelSchemaRef(Category, {includeRelations: true}), 98 | }, 99 | }, 100 | }, 101 | }) 102 | async find( 103 | @param.filter(Category) filter?: Filter, 104 | ): Promise { 105 | return this.categoryRepository.find(filter); 106 | } 107 | 108 | @patch('/categories') 109 | @response(200, { 110 | description: 'Category PATCH success count', 111 | content: {'application/json': {schema: CountSchema}}, 112 | }) 113 | async updateAll( 114 | @requestBody({ 115 | content: { 116 | 'application/json': { 117 | schema: getModelSchemaRef(Category, {partial: true}), 118 | }, 119 | }, 120 | }) 121 | category: Category, 122 | @param.where(Category) where?: Where, 123 | ): Promise { 124 | return this.categoryRepository.updateAll(category, where); 125 | } 126 | 127 | @get('/categories/{id}') 128 | @response(200, { 129 | description: 'Category model instance', 130 | content: { 131 | 'application/json': { 132 | schema: getModelSchemaRef(Category, {includeRelations: true}), 133 | }, 134 | }, 135 | }) 136 | async findById( 137 | @param.path.number('id') id: number, 138 | @param.filter(Category, {exclude: 'where'}) 139 | filter?: FilterExcludingWhere, 140 | ): Promise { 141 | return this.categoryRepository.findById(id, filter); 142 | } 143 | 144 | @patch('/categories/{id}') 145 | @response(204, { 146 | description: 'Category PATCH success', 147 | }) 148 | async updateById( 149 | @param.path.number('id') id: number, 150 | @requestBody({ 151 | content: { 152 | 'application/json': { 153 | schema: getModelSchemaRef(Category, {partial: true}), 154 | }, 155 | }, 156 | }) 157 | category: Category, 158 | ): Promise { 159 | await this.categoryRepository.updateById(id, category); 160 | } 161 | 162 | @put('/categories/{id}') 163 | @response(204, { 164 | description: 'Category PUT success', 165 | }) 166 | async replaceById( 167 | @param.path.number('id') id: number, 168 | @requestBody() category: Category, 169 | ): Promise { 170 | await this.categoryRepository.replaceById(id, category); 171 | } 172 | 173 | @del('/categories/{id}') 174 | @response(204, { 175 | description: 'Category DELETE success', 176 | }) 177 | async deleteById(@param.path.number('id') id: number): Promise { 178 | await this.categoryRepository.deleteById(id); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/controllers/programming-languange.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Count, 3 | CountSchema, 4 | Filter, 5 | FilterExcludingWhere, 6 | repository, 7 | Where, 8 | } from '@loopback/repository'; 9 | import { 10 | del, 11 | get, 12 | getModelSchemaRef, 13 | param, 14 | patch, 15 | post, 16 | put, 17 | requestBody, 18 | response, 19 | } from '@loopback/rest'; 20 | import {ProgrammingLanguage} from '../models'; 21 | import {ProgrammingLanguageRepository} from '../repositories'; 22 | 23 | export class ProgrammingLanguangeController { 24 | constructor( 25 | @repository(ProgrammingLanguageRepository) 26 | public programmingLanguageRepository: ProgrammingLanguageRepository, 27 | ) {} 28 | 29 | @post('/programming-languages') 30 | @response(200, { 31 | description: 'ProgrammingLanguage model instance', 32 | content: { 33 | 'application/json': {schema: getModelSchemaRef(ProgrammingLanguage)}, 34 | }, 35 | }) 36 | async create( 37 | @requestBody({ 38 | content: { 39 | 'application/json': { 40 | schema: getModelSchemaRef(ProgrammingLanguage, { 41 | title: 'NewProgrammingLanguage', 42 | exclude: ['id'], 43 | }), 44 | }, 45 | }, 46 | }) 47 | programmingLanguage: Omit, 48 | ): Promise { 49 | return this.programmingLanguageRepository.create(programmingLanguage); 50 | } 51 | 52 | @post('/programming-languages-bulk') 53 | @response(200, { 54 | description: 'ProgrammingLanguage model instances', 55 | content: { 56 | 'application/json': { 57 | schema: { 58 | type: 'array', 59 | items: getModelSchemaRef(ProgrammingLanguage), 60 | }, 61 | }, 62 | }, 63 | }) 64 | async createAll( 65 | @requestBody({ 66 | content: { 67 | 'application/json': { 68 | schema: { 69 | type: 'array', 70 | items: getModelSchemaRef(ProgrammingLanguage, { 71 | title: 'NewProgrammingLanguage', 72 | exclude: ['id'], 73 | }), 74 | }, 75 | }, 76 | }, 77 | }) 78 | programmingLanguages: Array>, 79 | ): Promise { 80 | return this.programmingLanguageRepository.createAll(programmingLanguages); 81 | } 82 | 83 | @get('/programming-languages/count') 84 | @response(200, { 85 | description: 'ProgrammingLanguage model count', 86 | content: {'application/json': {schema: CountSchema}}, 87 | }) 88 | async count( 89 | @param.where(ProgrammingLanguage) where?: Where, 90 | ): Promise { 91 | return this.programmingLanguageRepository.count(where); 92 | } 93 | 94 | @get('/programming-languages') 95 | @response(200, { 96 | description: 'Array of ProgrammingLanguage model instances', 97 | content: { 98 | 'application/json': { 99 | schema: { 100 | type: 'array', 101 | items: getModelSchemaRef(ProgrammingLanguage, { 102 | includeRelations: true, 103 | }), 104 | }, 105 | }, 106 | }, 107 | }) 108 | async find( 109 | @param.filter(ProgrammingLanguage) filter?: Filter, 110 | ): Promise { 111 | return this.programmingLanguageRepository.find(filter); 112 | } 113 | 114 | @patch('/programming-languages') 115 | @response(200, { 116 | description: 'ProgrammingLanguage PATCH success count', 117 | content: {'application/json': {schema: CountSchema}}, 118 | }) 119 | async updateAll( 120 | @requestBody({ 121 | content: { 122 | 'application/json': { 123 | schema: getModelSchemaRef(ProgrammingLanguage, {partial: true}), 124 | }, 125 | }, 126 | }) 127 | programmingLanguage: ProgrammingLanguage, 128 | @param.where(ProgrammingLanguage) where?: Where, 129 | ): Promise { 130 | return this.programmingLanguageRepository.updateAll( 131 | programmingLanguage, 132 | where, 133 | ); 134 | } 135 | 136 | @get('/programming-languages/{id}') 137 | @response(200, { 138 | description: 'ProgrammingLanguage model instance', 139 | content: { 140 | 'application/json': { 141 | schema: getModelSchemaRef(ProgrammingLanguage, { 142 | includeRelations: true, 143 | }), 144 | }, 145 | }, 146 | }) 147 | async findById( 148 | @param.path.number('id') id: number, 149 | @param.filter(ProgrammingLanguage, {exclude: 'where'}) 150 | filter?: FilterExcludingWhere, 151 | ): Promise { 152 | return this.programmingLanguageRepository.findById(id, filter); 153 | } 154 | 155 | @patch('/programming-languages/{id}') 156 | @response(204, { 157 | description: 'ProgrammingLanguage PATCH success', 158 | }) 159 | async updateById( 160 | @param.path.number('id') id: number, 161 | @requestBody({ 162 | content: { 163 | 'application/json': { 164 | schema: getModelSchemaRef(ProgrammingLanguage, {partial: true}), 165 | }, 166 | }, 167 | }) 168 | programmingLanguage: ProgrammingLanguage, 169 | ): Promise { 170 | await this.programmingLanguageRepository.updateById( 171 | id, 172 | programmingLanguage, 173 | ); 174 | } 175 | 176 | @put('/programming-languages/{id}') 177 | @response(204, { 178 | description: 'ProgrammingLanguage PUT success', 179 | }) 180 | async replaceById( 181 | @param.path.number('id') id: number, 182 | @requestBody() programmingLanguage: ProgrammingLanguage, 183 | ): Promise { 184 | await this.programmingLanguageRepository.replaceById( 185 | id, 186 | programmingLanguage, 187 | ); 188 | } 189 | 190 | @del('/programming-languages/{id}') 191 | @response(204, { 192 | description: 'ProgrammingLanguage DELETE success', 193 | }) 194 | async deleteById(@param.path.number('id') id: number): Promise { 195 | await this.programmingLanguageRepository.deleteById(id); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Release [v2.3.0](https://github.com/sourcefuse/loopback4-sequelize/compare/v2.2.1..v2.3.0) April 24, 2023 2 | Welcome to the April 24, 2023 release of loopback4-sequelize. There are many updates in this version that we hope you will like, the key highlights include: 3 | 4 | - [Deprecate this package in favour of <code>@loopback/sequelize</code>](https://github.com/sourcefuse/loopback4-sequelize/issues/29) :- [docs(chore): add deprecation notice ](https://github.com/sourcefuse/loopback4-sequelize/commit/b7dc0d28370f0ffffded0664b547ffbdbd3dd67e) was commited on April 24, 2023 by [Shubham P](mailto:shubham.prajapat@sourcefuse.com) 5 | 6 | - and promote use of official `@loopback/sequelize` 7 | 8 | - GH-29 9 | 10 | 11 | - [accept connection pooling, ssl and url string option in datasource](https://github.com/sourcefuse/loopback4-sequelize/issues/26) :- [feat(datasource): implement connection pooling and sequelize specific options support ](https://github.com/sourcefuse/loopback4-sequelize/commit/86f0642174810a348fab4e1871386dfe890602aa) was commited on March 17, 2023 by [Shubham P](mailto:shubham.prajapat@sourcefuse.com) 12 | 13 | - * feat(datasource): add connection pooling support 14 | 15 | - adds ability to prase connection pooling options for postgres, mysql and 16 | 17 | - oracle 18 | 19 | - GH-26 20 | 21 | - * feat(datasource): add capability to pass options directly to sequelize 22 | 23 | - added ability to connect with url 24 | 25 | - added new property called `sequelizeOptions` in datasource config to 26 | 27 | - allow 28 | 29 | - direct option forwarding to sequelize instance 30 | 31 | - GH-26 32 | 33 | 34 | Clink on the above links to understand the changes in detail. 35 | ___ 36 | 37 | # [2.3.0](https://github.com/sourcefuse/loopback4-sequelize/compare/v2.2.1...v2.3.0) (2023-04-24) 38 | 39 | 40 | ### Features 41 | 42 | * **datasource:** implement connection pooling and sequelize specific options support ([#27](https://github.com/sourcefuse/loopback4-sequelize/issues/27)) ([86f0642](https://github.com/sourcefuse/loopback4-sequelize/commit/86f0642174810a348fab4e1871386dfe890602aa)), closes [#26](https://github.com/sourcefuse/loopback4-sequelize/issues/26) [#26](https://github.com/sourcefuse/loopback4-sequelize/issues/26) 43 | 44 | ## Release [v2.2.1](https://github.com/sourcefuse/loopback4-sequelize/compare/v2.2.0..v2.2.1) March 13, 2023 45 | Welcome to the March 13, 2023 release of loopback4-sequelize. There are many updates in this version that we hope you will like, the key highlights include: 46 | 47 | - [loopback version update](https://github.com/sourcefuse/loopback4-sequelize/issues/23) :- [](https://github.com/sourcefuse/loopback4-sequelize/commit/4db7a68879d87a6b21ef8a4154c9e15f96737856) was commited on March 10, 2023 by [gautam23-sf](mailto:gautam.agarwal@sourcefuse.com) 48 | 49 | - loopback version update 50 | 51 | - GH-23 52 | 53 | 54 | Clink on the above links to understand the changes in detail. 55 | ___ 56 | 57 | ## [2.2.1](https://github.com/sourcefuse/loopback4-sequelize/compare/v2.2.0...v2.2.1) (2023-03-13) 58 | 59 | ## Release [v2.2.0](https://github.com/sourcefuse/loopback4-sequelize/compare/v2.1.0..v2.2.0) March 3, 2023 60 | Welcome to the March 3, 2023 release of loopback4-sequelize. There are many updates in this version that we hope you will like, the key highlights include: 61 | 62 | - [Loopback4-sequelize should support transactions](https://github.com/sourcefuse/loopback4-sequelize/issues/21) :- [feat(transaction): add transaction support ](https://github.com/sourcefuse/loopback4-sequelize/commit/331238df107ea0c0929037a3a8faa2ff77739c1c) was commited on March 3, 2023 by [Shubham P](mailto:shubham.prajapat@sourcefuse.com) 63 | 64 | - provide beginTransaction method to SequelizeCrudRepository with somewhat 65 | 66 | - similar usage as loopback 67 | 68 | - GH-21 69 | 70 | 71 | Clink on the above links to understand the changes in detail. 72 | ___ 73 | 74 | # [2.2.0](https://github.com/sourcefuse/loopback4-sequelize/compare/v2.1.0...v2.2.0) (2023-03-03) 75 | 76 | 77 | ### Features 78 | 79 | * **transaction:** add transaction support ([#22](https://github.com/sourcefuse/loopback4-sequelize/issues/22)) ([331238d](https://github.com/sourcefuse/loopback4-sequelize/commit/331238df107ea0c0929037a3a8faa2ff77739c1c)), closes [#21](https://github.com/sourcefuse/loopback4-sequelize/issues/21) 80 | 81 | ## Release [v2.1.0](https://github.com/sourcefuse/loopback4-sequelize/compare/v2.0.1..v2.1.0) February 20, 2023 82 | Welcome to the February 20, 2023 release of loopback4-sequelize. There are many updates in this version that we hope you will like, the key highlights include: 83 | 84 | - [Generate a detailed changelog](https://github.com/sourcefuse/loopback4-sequelize/issues/19) :- [](https://github.com/sourcefuse/loopback4-sequelize/commit/a32826122f937905d5853ee5636bdd5ea00866d7) was commited on February 20, 2023 by [Yesha Mavani](mailto:yesha.mavani@sourcefuse.com) 85 | 86 | - changelog with issue and commit details will be generated 87 | 88 | - GH-19 89 | 90 | 91 | - [](https://github.com/sourcefuse/loopback4-sequelize/issues/) :- [](https://github.com/sourcefuse/loopback4-sequelize/commit/e279bec63d77ffed1f34094fa15d1ad0f308f78e) was commited on February 16, 2023 by [Sunny](mailto:sunny.tyagi@sourcefuse.com) 92 | 93 | 94 | Clink on the above links to understand the changes in detail. 95 | ___ 96 | 97 | # [2.1.0](https://github.com/sourcefuse/loopback4-sequelize/compare/v2.0.1...v2.1.0) (2023-02-20) 98 | 99 | 100 | ### Features 101 | 102 | * **chore:** generating detailed changelog ([a328261](https://github.com/sourcefuse/loopback4-sequelize/commit/a32826122f937905d5853ee5636bdd5ea00866d7)), closes [#19](https://github.com/sourcefuse/loopback4-sequelize/issues/19) 103 | 104 | ## [2.0.1](https://github.com/sourcefuse/loopback4-sequelize/compare/v2.0.0...v2.0.1) (2023-01-23) 105 | 106 | 107 | ### Bug Fixes 108 | 109 | * **repository:** make `deleteById` independent ([#13](https://github.com/sourcefuse/loopback4-sequelize/issues/13)) ([0ef9dfe](https://github.com/sourcefuse/loopback4-sequelize/commit/0ef9dfe4eb310073ef51e663196ccedacf43d2fa)), closes [#12](https://github.com/sourcefuse/loopback4-sequelize/issues/12) 110 | 111 | # [2.0.0](https://github.com/sourcefuse/loopback4-sequelize/compare/v1.0.0...v2.0.0) (2023-01-03) 112 | 113 | 114 | ### Bug Fixes 115 | 116 | * **ci-cd:** add commitlint and husky ([#8](https://github.com/sourcefuse/loopback4-sequelize/issues/8)) ([c879812](https://github.com/sourcefuse/loopback4-sequelize/commit/c879812a387a4499d8a6244a48cac622e5b22e7a)), closes [#0](https://github.com/sourcefuse/loopback4-sequelize/issues/0) [#0](https://github.com/sourcefuse/loopback4-sequelize/issues/0) [#0](https://github.com/sourcefuse/loopback4-sequelize/issues/0) [#0](https://github.com/sourcefuse/loopback4-sequelize/issues/0) 117 | * **ci-cd:** change default branch to master ([de4037c](https://github.com/sourcefuse/loopback4-sequelize/commit/de4037c9529e3ebe857eb3345d1f5af73796f020)), closes [#7](https://github.com/sourcefuse/loopback4-sequelize/issues/7) 118 | * **repository:** exclude hidden properties in response body ([3e254fd](https://github.com/sourcefuse/loopback4-sequelize/commit/3e254fd48a307c18e8ede54ce01c87804d29fbe0)), closes [#3](https://github.com/sourcefuse/loopback4-sequelize/issues/3) 119 | 120 | 121 | ### Features 122 | 123 | * **repository:** add support for relational query ([#6](https://github.com/sourcefuse/loopback4-sequelize/issues/6)) ([c99bb59](https://github.com/sourcefuse/loopback4-sequelize/commit/c99bb59b2b1ef4401363f7465a9b62cec74bf5e1)) 124 | 125 | 126 | ### BREAKING CHANGES 127 | 128 | * **repository:** `SequelizeRepository` is renamed to `SequelizeCrudRepository` 129 | 130 | * test(repository): add test cases for relations 131 | 132 | namely for fields, order and limit filter along with createAll, 133 | updateAll, delete and relations such as `@hasOne`, `@hasMany`, `@belongsTo` 134 | `@hasMany through`, `@referencesMany` and for INNER JOIN using `required: true` flag 135 | 136 | # 1.0.0 (2022-10-19) 137 | 138 | 139 | ### Features 140 | 141 | * **commitzen:** add commitzen ([8ef6720](https://github.com/sourcefuse/loopback4-sequelize/commit/8ef672021bf472e64c762024e7f21e8785808f8b)) 142 | * **datasource:** add sequelize datasource base class ([3fcf68f](https://github.com/sourcefuse/loopback4-sequelize/commit/3fcf68fbd0f70be809b9634232983e31b8c42705)) 143 | * **repository:** add sequelize repository base class ([a1dd46f](https://github.com/sourcefuse/loopback4-sequelize/commit/a1dd46f1142318d9b18446d0f8f71a474726ae95)) -------------------------------------------------------------------------------- /src/sequelize/sequelize.datasource.base.ts: -------------------------------------------------------------------------------- 1 | import {LifeCycleObserver} from '@loopback/core'; 2 | import { 3 | AnyObject, 4 | Command, 5 | NamedParameters, 6 | Options, 7 | PositionalParameters, 8 | } from '@loopback/repository'; 9 | import debugFactory from 'debug'; 10 | import { 11 | PoolOptions, 12 | QueryOptions, 13 | Sequelize, 14 | Options as SequelizeOptions, 15 | Transaction, 16 | TransactionOptions, 17 | } from 'sequelize'; 18 | import { 19 | ConnectionPoolOptions, 20 | LoopbackPoolConfigKey, 21 | PoolingEnabledConnector, 22 | SupportedLoopbackConnectors, 23 | poolConfigKeys, 24 | poolingEnabledConnectors, 25 | SupportedConnectorMapping as supportedConnectorMapping, 26 | } from './connector-mapping'; 27 | 28 | const debug = debugFactory('loopback:sequelize:datasource'); 29 | const queryLogging = debugFactory('loopback:sequelize:queries'); 30 | 31 | /** 32 | * Sequelize DataSource Class 33 | */ 34 | export class SequelizeDataSource implements LifeCycleObserver { 35 | name: string; 36 | settings = {}; 37 | constructor(public config: SequelizeDataSourceConfig) { 38 | if ( 39 | this.config.connector && 40 | !(this.config.connector in supportedConnectorMapping) 41 | ) { 42 | throw new Error( 43 | `Specified connector ${ 44 | this.config.connector ?? this.config.dialect 45 | } is not supported.`, 46 | ); 47 | } 48 | } 49 | 50 | sequelize?: Sequelize; 51 | sequelizeConfig: SequelizeOptions; 52 | async init(): Promise { 53 | const {config} = this; 54 | const { 55 | connector, 56 | file, 57 | schema, 58 | database, 59 | host, 60 | port, 61 | user, 62 | username, 63 | password, 64 | } = config; 65 | 66 | this.sequelizeConfig = { 67 | database, 68 | dialect: connector ? supportedConnectorMapping[connector] : undefined, 69 | storage: file, 70 | host, 71 | port, 72 | schema, 73 | username: user ?? username, 74 | password, 75 | logging: queryLogging, 76 | pool: this.getPoolOptions(), 77 | ...config.sequelizeOptions, 78 | }; 79 | 80 | if (config.url) { 81 | this.sequelize = new Sequelize(config.url, this.sequelizeConfig); 82 | } else { 83 | this.sequelize = new Sequelize(this.sequelizeConfig); 84 | } 85 | 86 | await this.sequelize.authenticate(); 87 | debug('Connection has been established successfully.'); 88 | } 89 | async start(..._injectedArgs: unknown[]): Promise {} 90 | async stop() { 91 | await this.sequelize?.close(); 92 | } 93 | 94 | automigrate() { 95 | throw new Error( 96 | 'SequelizeDataSourceError: Migrations are not supported. Use `db-migrate` package instead.', 97 | ); 98 | } 99 | autoupdate() { 100 | throw new Error( 101 | 'SequelizeDataSourceError: Migrations are not supported. Use `db-migrate` package instead.', 102 | ); 103 | } 104 | 105 | /** 106 | * Begin a new transaction. 107 | * 108 | * @param [options] Options {isolationLevel: '...'} 109 | * @returns A promise which resolves to a Sequelize Transaction object 110 | */ 111 | async beginTransaction( 112 | options?: TransactionOptions | TransactionOptions['isolationLevel'], 113 | ): Promise { 114 | /** 115 | * Default Isolation level for transactions is `READ_COMMITTED`, to be consistent with loopback default. 116 | * See: https://loopback.io/doc/en/lb4/Using-database-transactions.html#isolation-levels 117 | */ 118 | const DEFAULT_ISOLATION_LEVEL = Transaction.ISOLATION_LEVELS.READ_COMMITTED; 119 | 120 | if (typeof options === 'string') { 121 | // Received `isolationLevel` as the first argument 122 | options = { 123 | isolationLevel: options, 124 | }; 125 | } else if (options === undefined) { 126 | options = { 127 | isolationLevel: DEFAULT_ISOLATION_LEVEL, 128 | }; 129 | } else { 130 | options.isolationLevel = 131 | options.isolationLevel ?? DEFAULT_ISOLATION_LEVEL; 132 | } 133 | 134 | return this.sequelize!.transaction(options); 135 | } 136 | 137 | /** 138 | * Execute a SQL command. 139 | * 140 | * **WARNING:** In general, it is always better to perform database actions 141 | * through repository methods. Directly executing SQL may lead to unexpected 142 | * results, corrupted data, security vulnerabilities and other issues. 143 | * 144 | * @example 145 | * 146 | * ```ts 147 | * // MySQL 148 | * const result = await repo.execute( 149 | * 'SELECT * FROM Products WHERE size > ?', 150 | * [42] 151 | * ); 152 | * 153 | * // PostgreSQL 154 | * const result = await repo.execute( 155 | * 'SELECT * FROM Products WHERE size > $1', 156 | * [42] 157 | * ); 158 | * ``` 159 | * 160 | * @param command A parameterized SQL command or query. 161 | * @param parameters List of parameter values to use. 162 | * @param options Additional options, for example `transaction`. 163 | * @returns A promise which resolves to the command output. The output type (data structure) is database specific and 164 | * often depends on the command executed. 165 | */ 166 | async execute( 167 | command: Command, 168 | parameters?: NamedParameters | PositionalParameters, 169 | options?: Options, 170 | ): Promise { 171 | if (!this.sequelize) { 172 | throw Error( 173 | `The datasource "${this.name}" doesn't have sequelize instance bound to it.`, 174 | ); 175 | } 176 | 177 | if (typeof command !== 'string') { 178 | command = JSON.stringify(command); 179 | } 180 | 181 | options = options ?? {}; 182 | 183 | const queryOptions: QueryOptions = {}; 184 | if (options?.transaction) { 185 | queryOptions.transaction = options.transaction; 186 | } 187 | 188 | let targetReplacementKey: 'replacements' | 'bind' = 'bind'; 189 | if (command.includes('?')) { 190 | // matches queries like "SELECT * from user where name = ?" 191 | targetReplacementKey = 'replacements'; 192 | } else if (command.match(/\$\w/g)) { 193 | // matches queries containing parameters starting with dollar sign ($param or $1, $2) 194 | targetReplacementKey = 'bind'; 195 | } 196 | 197 | if (parameters) { 198 | queryOptions[targetReplacementKey] = parameters; 199 | } 200 | const result = await this.sequelize.query(command, queryOptions); 201 | 202 | // Sequelize returns the select query result in an array at index 0 and at index 1 is the actual Result instance 203 | // Whereas in juggler it is returned directly as plain array. 204 | // Below condition maps that 0th index to final result to match juggler's behaviour 205 | if (command.match(/^select/i) && result.length >= 1) { 206 | return result[0]; 207 | } 208 | 209 | return result; 210 | } 211 | 212 | getPoolOptions(): PoolOptions | undefined { 213 | const config: SequelizeDataSourceConfig = this.config; 214 | const specifiedPoolOptions = Object.keys(config).some(key => 215 | poolConfigKeys.includes(key as LoopbackPoolConfigKey), 216 | ); 217 | const supportsPooling = 218 | config.connector && 219 | (poolingEnabledConnectors as string[]).includes(config.connector); 220 | 221 | if (!(supportsPooling && specifiedPoolOptions)) { 222 | return; 223 | } 224 | const optionMapping = 225 | ConnectionPoolOptions[config.connector as PoolingEnabledConnector]; 226 | 227 | if (!optionMapping) { 228 | return; 229 | } 230 | 231 | const {min, max, acquire, idle} = optionMapping; 232 | const options: PoolOptions = {}; 233 | if (max && config[max]) { 234 | options.max = config[max]; 235 | } 236 | if (min && config[min]) { 237 | options.min = config[min]; 238 | } 239 | if (acquire && config[acquire]) { 240 | options.acquire = config[acquire]; 241 | } 242 | if (idle && config[idle]) { 243 | options.idle = config[idle]; 244 | } 245 | return options; 246 | } 247 | } 248 | 249 | export type SequelizeDataSourceConfig = { 250 | name?: string; 251 | user?: string; 252 | connector?: SupportedLoopbackConnectors; 253 | url?: string; 254 | /** 255 | * Additional sequelize options that are passed directly to 256 | * Sequelize when initializing the connection. 257 | * Any options provided in this way will take priority over 258 | * other configurations that may come from parsing the loopback style configurations. 259 | * 260 | * eg. 261 | * ```ts 262 | * let config = { 263 | * name: 'db', 264 | * connector: 'postgresql', 265 | * sequelizeOptions: { 266 | * dialectOptions: { 267 | * rejectUnauthorized: false, 268 | * ca: fs.readFileSync('/path/to/root.crt').toString(), 269 | * } 270 | * } 271 | * }; 272 | * ``` 273 | */ 274 | sequelizeOptions?: SequelizeOptions; 275 | } & AnyObject; 276 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated! 2 | 3 | As of 24th April 2023, [`loopback4-sequelize`](https://www.npmjs.com/package/loopback4-sequelize) is fully deprecated. New changes are expected to land on the official replacement of this package here: [`@loopback/sequelize`](https://www.npmjs.com/package/@loopback/sequelize) and the same is recommended for current and future users. 4 | 5 | # loopback4-sequelize 6 | 7 | [![LoopBack]()](http://loopback.io/) 8 | 9 | 10 | 11 | This is a loopback4 extension that provides Sequelize's query builder at repository level in any loopback 4 application. It has zero learning curve as it follows similar interface as `DefaultCrudRepository`. For relational databases, Sequelize is a popular ORM of choice. 12 | 13 | For pending features, refer to the [Limitations](#limitations) section below. 14 | 15 | ## Installation 16 | 17 | To install this extension in your Loopback 4 project, run the following command: 18 | 19 | ```sh 20 | npm install loopback4-sequelize 21 | ``` 22 | 23 | You'll also need to install the driver for your preferred database: 24 | 25 | ```sh 26 | # One of the following: 27 | npm install --save pg pg-hstore # Postgres 28 | npm install --save mysql2 29 | npm install --save mariadb 30 | npm install --save sqlite3 31 | npm install --save tedious # Microsoft SQL Server 32 | npm install --save oracledb # Oracle Database 33 | ``` 34 | 35 | ## Usage 36 | 37 | > You can watch a video overview of this extension by [clicking here](https://youtu.be/ZrUxIk63oRc). 38 | 39 | 40 | 41 | Both newly developed and existing projects can benefit from the extension by simply changing the parent classes in the target Data Source and Repositories. 42 | 43 | ### Step 1: Configure DataSource 44 | 45 | Change the parent class from `juggler.DataSource` to `SequelizeDataSource` like below. 46 | 47 | ```ts title="pg.datasource.ts" 48 | // ... 49 | import {SequelizeDataSource} from 'loopback4-sequelize'; 50 | 51 | // ... 52 | export class PgDataSource 53 | extends SequelizeDataSource 54 | implements LifeCycleObserver { 55 | // ... 56 | } 57 | ``` 58 | 59 | `SequelizeDataSource` accepts commonly used config in the same way as loopback did. So in most cases you won't need to change your existing configuration. But if you want to use sequelize specific options pass them in `sequelizeOptions` like below: 60 | 61 | ```ts 62 | let config = { 63 | name: 'db', 64 | connector: 'postgresql', 65 | sequelizeOptions: { 66 | username: 'postgres', 67 | password: 'secret', 68 | dialectOptions: { 69 | ssl: { 70 | rejectUnauthorized: false, 71 | ca: fs.readFileSync('/path/to/root.crt').toString(), 72 | }, 73 | }, 74 | }, 75 | }; 76 | ``` 77 | 78 | > Note: Options provided in `sequelizeOptions` will take priority over others, For eg. if you have password specified in both `config.password` and `config.password.sequelizeOptions` the latter one will be used. 79 | 80 | ### Step 2: Configure Repository 81 | 82 | Change the parent class from `DefaultCrudRepository` to `SequelizeCrudRepository` like below. 83 | 84 | ```ts title="your.repository.ts" 85 | // ... 86 | import {SequelizeCrudRepository} from 'loopback4-sequelize'; 87 | 88 | export class YourRepository extends SequelizeCrudRepository< 89 | YourModel, 90 | typeof YourModel.prototype.id, 91 | YourModelRelations 92 | > { 93 | // ... 94 | } 95 | ``` 96 | 97 | ## Relations 98 | 99 | ### Supported Loopback Relations 100 | 101 | With `SequelizeCrudRepository`, you can utilize following relations without any additional configuration: 102 | 103 | 1. [HasMany Relation](https://loopback.io/doc/en/lb4/HasMany-relation.html) 104 | 2. [BelongsTo Relation](https://loopback.io/doc/en/lb4/BelongsTo-relation.html) 105 | 3. [HasOne Relation](https://loopback.io/doc/en/lb4/HasOne-relation.html) 106 | 4. [HasManyThrough Relation](https://loopback.io/doc/en/lb4/HasManyThrough-relation.html) 107 | 5. [ReferencesMany Relation](https://loopback.io/doc/en/lb4/ReferencesMany-relation.html) 108 | 109 | The default relation configuration, generated using the [lb4 relation](https://loopback.io/doc/en/lb4/Relation-generator.html) command (i.e. inclusion resolvers in the repository and property decorators in the model), remain unchanged. 110 | 111 | ### INNER JOIN 112 | 113 | > Check the demo video of using inner joins here: https://youtu.be/ZrUxIk63oRc?t=76 114 | 115 | When using `SequelizeCrudRepository`, the `find()`, `findOne()`, and `findById()` methods accept a new option called `required` in the include filter. Setting this option to `true` will result in an inner join query that explicitly requires the specified condition for the child model. If the row does not meet this condition, it will not be fetched and returned. 116 | 117 | An example of the filter object might look like this to fetch the books who contains "Art" in their title, which belongs to category "Programming": 118 | 119 | ```json 120 | { 121 | "where": {"title": {"like": "%Art%"}}, 122 | "include": [ 123 | { 124 | "relation": "category", 125 | "scope": { 126 | "where": { 127 | "name": "Programming" 128 | } 129 | }, 130 | "required": true // 👈 131 | } 132 | ] 133 | } 134 | ``` 135 | 136 | ## SQL Transactions 137 | 138 | A Sequelize repository can perform operations in a transaction using the `beginTransaction()` method. 139 | 140 | ### Isolation levels 141 | 142 | When you call `beginTransaction()`, you can optionally specify a transaction isolation level. It support the following isolation levels: 143 | 144 | - `Transaction.ISOLATION_LEVELS.READ_UNCOMMITTED` (default) 145 | - `Transaction.ISOLATION_LEVELS.READ_COMMITTED` 146 | - `Transaction.ISOLATION_LEVELS.REPEATABLE_READ` 147 | - `Transaction.ISOLATION_LEVELS.SERIALIZABLE` 148 | 149 | ### Options 150 | 151 | Following are the supported options: 152 | 153 | ```ts 154 | { 155 | autocommit?: boolean; 156 | isolationLevel?: Transaction.ISOLATION_LEVELS; 157 | type?: Transaction.TYPES; 158 | deferrable?: string | Deferrable; 159 | /** 160 | * Parent transaction. 161 | */ 162 | transaction?: Transaction | null; 163 | } 164 | ``` 165 | 166 | ### Example 167 | 168 | ```ts 169 | // Get repository instances. In a typical application, instances are injected 170 | // via dependency injection using `@repository` decorator. 171 | const userRepo = await app.getRepository(UserRepository); 172 | 173 | // Begin a new transaction. 174 | // It's also possible to call `userRepo.dataSource.beginTransaction` instead. 175 | const tx = await userRepo.beginTransaction({ 176 | isolationLevel: Transaction.ISOLATION_LEVELS.SERIALIZABLE, 177 | }); 178 | 179 | try { 180 | // Then, we do some calls passing this transaction as an option: 181 | const user = await userRepo.create( 182 | { 183 | firstName: 'Jon', 184 | lastName: 'Doe', 185 | }, 186 | {transaction: tx}, 187 | ); 188 | 189 | await userRepo.updateById( 190 | user.id, 191 | { 192 | firstName: 'John', 193 | }, 194 | {transaction: tx}, 195 | ); 196 | 197 | // If the execution reaches this line, no errors were thrown. 198 | // We commit the transaction. 199 | await tx.commit(); 200 | } catch (error) { 201 | // If the execution reaches this line, an error was thrown. 202 | // We rollback the transaction. 203 | await tx.rollback(); 204 | } 205 | ``` 206 | 207 | Switching from loopback defaults to sequelize transaction is as simple as [this commit](https://github.com/shubhamp-sf/loopback4-sequelize-transaction-example/commit/321791c93ffd10c3af13e8b891396ae99b632a23) in [loopback4-sequelize-transaction-example](https://github.com/shubhamp-sf/loopback4-sequelize-transaction-example). 208 | 209 | 210 | 211 | ## Debug strings reference 212 | 213 | There are three built-in debug strings available in this extension to aid in debugging. To learn more about how to use them, see [this page](https://loopback.io/doc/en/lb4/Setting-debug-strings.html). 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 |
StringDescription
Datasource
loopback:sequelize:datasourceDatabase Connections logs
loopback:sequelize:queriesLogs Executed SQL Queries and Parameters
Repository
loopback:sequelize:modelbuilderLogs Translation of Loopback Models Into Sequelize Supported Definitions. Helpful When Debugging Datatype Issues
241 | 242 | ## Limitations 243 | 244 | Please note, the current implementation does not support the following: 245 | 246 | 1. Loopback Migrations (via default `migrate.ts`). Though you're good if using external packages like [`db-migrate`](https://www.npmjs.com/package/db-migrate). 247 | 248 | Community contribution is welcome. 249 | 250 | ## Feedback 251 | 252 | If you've noticed a bug or have a question or have a feature request, [search the issue tracker](https://github.com/sourcefuse/loopback4-sequelize/issues) to see if someone else in the community has already created a ticket. 253 | If not, go ahead and [make one](https://github.com/sourcefuse/loopback4-sequelize/issues/new/choose)! 254 | All feature requests are welcome. Implementation time may vary. Feel free to contribute the same, if you can. 255 | If you think this extension is useful, please [star](https://help.github.com/en/articles/about-stars) it. Appreciation really helps in keeping this project alive. 256 | 257 | ## Contributing 258 | 259 | Please read [CONTRIBUTING.md](https://github.com/sourcefuse/loopback4-sequelize/blob/master/.github/CONTRIBUTING.md) for details on the process for submitting pull requests to us. 260 | 261 | ## Code of conduct 262 | 263 | Code of conduct guidelines [here](https://github.com/sourcefuse/loopback4-sequelize/blob/master/.github/CODE_OF_CONDUCT.md). 264 | 265 | ## License 266 | 267 | [MIT](https://github.com/sourcefuse/loopback4-sequelize/blob/master/LICENSE) 268 | 269 | 270 | -------------------------------------------------------------------------------- /src/__tests__/integration/repository.integration.ts: -------------------------------------------------------------------------------- 1 | import {AnyObject} from '@loopback/repository'; 2 | import {RestApplication} from '@loopback/rest'; 3 | import { 4 | Client, 5 | StubbedInstanceWithSinonAccessor, 6 | TestSandbox, 7 | createRestAppClient, 8 | createStubInstance, 9 | expect, 10 | givenHttpServerConfig, 11 | } from '@loopback/testlab'; 12 | import _ from 'lodash'; 13 | import {resolve} from 'path'; 14 | import {UniqueConstraintError} from 'sequelize'; 15 | import {fail} from 'should'; 16 | import {validate as uuidValidate, version as uuidVersion} from 'uuid'; 17 | import {SequelizeCrudRepository, SequelizeDataSource} from '../../sequelize'; 18 | import {SequelizeSandboxApplication} from '../fixtures/application'; 19 | import {config as primaryDataSourceConfig} from '../fixtures/datasources/primary.datasource'; 20 | import {config as secondaryDataSourceConfig} from '../fixtures/datasources/secondary.datasource'; 21 | import {ProgrammingLanguage, TableInSecondaryDB} from '../fixtures/models'; 22 | import {Box, Event, eventTableName} from '../fixtures/models/test.model'; 23 | import { 24 | DeveloperRepository, 25 | ProgrammingLanguageRepository, 26 | TaskRepository, 27 | UserRepository, 28 | } from '../fixtures/repositories'; 29 | 30 | type Entities = 31 | | 'users' 32 | | 'todo-lists' 33 | | 'todos' 34 | | 'doctors' 35 | | 'developers' 36 | | 'books' 37 | | 'products'; 38 | 39 | describe('Sequelize CRUD Repository (integration)', function () { 40 | // eslint-disable-next-line @typescript-eslint/no-invalid-this 41 | this.timeout(5000); 42 | const sandbox = new TestSandbox(resolve(__dirname, '../../.sandbox')); 43 | 44 | let app: SequelizeSandboxApplication; 45 | let userRepo: UserRepository; 46 | let taskRepo: TaskRepository; 47 | let developerRepo: DeveloperRepository; 48 | let languagesRepo: ProgrammingLanguageRepository; 49 | let client: Client; 50 | let datasource: StubbedInstanceWithSinonAccessor; 51 | 52 | beforeEach('reset sandbox', () => sandbox.reset()); 53 | beforeEach(getAppAndClient); 54 | afterEach(async () => { 55 | if (app) await app.stop(); 56 | (app as unknown) = undefined; 57 | }); 58 | 59 | describe('General', () => { 60 | beforeEach(async () => { 61 | await client.get('/users/sync-sequelize-model').send(); 62 | }); 63 | 64 | it('throws original error context from sequelize', async () => { 65 | const userWithId = { 66 | id: 1, 67 | name: 'Joe', 68 | active: true, 69 | }; 70 | const firstUser = await userRepo.create(userWithId); 71 | expect(firstUser).to.have.property('id', userWithId.id); 72 | try { 73 | throw await userRepo.create(userWithId); 74 | } catch (err) { 75 | expect(err).to.be.instanceOf(UniqueConstraintError); 76 | } 77 | }); 78 | 79 | describe('defaultFn Support', () => { 80 | beforeEach(async () => { 81 | await client.get('/tasks/sync-sequelize-model').send(); 82 | }); 83 | it('supports defaultFn: "uuid" in property decorator', async () => { 84 | const task = await taskRepo.create({title: 'Task 1'}); 85 | 86 | expect(uuidValidate(task.uuidv1)).to.be.true(); 87 | expect(uuidVersion(task.uuidv1)).to.be.eql(1); 88 | }); 89 | 90 | it('supports defaultFn: "uuidv4" in property decorator', async () => { 91 | const task = await taskRepo.create({title: 'Task title'}); 92 | 93 | expect(uuidValidate(task.uuidv4)).to.be.true(); 94 | expect(uuidVersion(task.uuidv4)).to.be.eql(4); 95 | }); 96 | 97 | it('supports defaultFn: "nanoid" in property decorator', async () => { 98 | const task = await taskRepo.create({title: 'Task title'}); 99 | 100 | expect(task.nanoId).to.be.String(); 101 | expect(task.nanoId.length).to.be.eql(taskRepo.NANO_ID_LENGTH); 102 | }); 103 | 104 | it('supports defaultFn: "now" in property decorator', async () => { 105 | const task = await taskRepo.create({title: 'Task title'}); 106 | if (task.createdAt) { 107 | const isValidDate = _.isDate(new Date(task.createdAt)); 108 | expect(isValidDate).to.be.true(); 109 | } else { 110 | fail( 111 | task.createdAt, 112 | '', 113 | 'task.createdAt is falsy, date is expected.', 114 | 'to be in date format', 115 | ); 116 | } 117 | }); 118 | 119 | it('supports custom defining aliases for defaultFn in property decorator', async () => { 120 | const task = await taskRepo.create({title: 'Task title'}); 121 | expect(task.customAlias).to.be.Number(); 122 | expect(task.customAlias).to.be.belowOrEqual(1); 123 | expect(task.customAlias).to.be.above(0); 124 | }); 125 | }); 126 | }); 127 | 128 | describe('Without Relations', () => { 129 | beforeEach(async () => { 130 | await client.get('/users/sync-sequelize-model').send(); 131 | }); 132 | 133 | it('creates an entity', async () => { 134 | const user = getDummyUser(); 135 | const createResponse = await client.post('/users').send(user); 136 | expect(createResponse.body).deepEqual({ 137 | id: 1, 138 | ..._.omit(user, ['password']), 139 | }); 140 | }); 141 | 142 | it('returns model data without hidden props in response of create', async () => { 143 | const user = getDummyUser(); 144 | const createResponse = await client.post('/users').send(user); 145 | expect(createResponse.body).not.to.have.property('password'); 146 | }); 147 | 148 | it('[create] allows accessing hidden props before serializing', async () => { 149 | const user = getDummyUser(); 150 | const userData = await userRepo.create({ 151 | name: user.name, 152 | address: user.address as AnyObject, 153 | email: user.email, 154 | password: user.password, 155 | dob: user.dob, 156 | active: user.active, 157 | }); 158 | 159 | expect(userData).to.have.property('password'); 160 | expect(userData.password).to.be.eql(user.password); 161 | const afterResponse = userData.toJSON(); 162 | expect(afterResponse).to.not.have.property('password'); 163 | }); 164 | 165 | it('[find] allows accessing hidden props before serializing', async () => { 166 | const user = getDummyUser(); 167 | await userRepo.create({ 168 | name: user.name, 169 | address: user.address as AnyObject, 170 | email: user.email, 171 | password: user.password, 172 | dob: user.dob, 173 | active: user.active, 174 | }); 175 | 176 | const userData = await userRepo.find(); 177 | 178 | expect(userData[0]).to.have.property('password'); 179 | expect(userData[0].password).to.be.eql(user.password); 180 | const afterResponse = userData[0].toJSON(); 181 | expect(afterResponse).to.not.have.property('password'); 182 | }); 183 | 184 | it('[findById] allows accessing hidden props before serializing', async () => { 185 | const user = getDummyUser(); 186 | const createdUser = await userRepo.create({ 187 | name: user.name, 188 | address: user.address as AnyObject, 189 | email: user.email, 190 | password: user.password, 191 | dob: user.dob, 192 | active: user.active, 193 | }); 194 | 195 | const userData = await userRepo.findById(createdUser.id); 196 | 197 | expect(userData).to.have.property('password'); 198 | expect(userData.password).to.be.eql(user.password); 199 | const afterResponse = userData.toJSON(); 200 | expect(afterResponse).to.not.have.property('password'); 201 | }); 202 | 203 | it('creates an entity and finds it', async () => { 204 | const user = getDummyUser(); 205 | await client.post('/users').send(user); 206 | const userResponse = await client.get('/users').send(); 207 | expect(userResponse.body).deepEqual([ 208 | { 209 | id: 1, 210 | ..._.omit(user, ['password']), 211 | }, 212 | ]); 213 | }); 214 | 215 | it('counts created entities', async () => { 216 | await client.post('/users').send(getDummyUser()); 217 | const getResponse = await client.get('/users/count').send(); 218 | expect(getResponse.body).to.have.property('count', 1); 219 | }); 220 | 221 | it('fetches an entity', async () => { 222 | const createResponse = await client.post('/users').send(getDummyUser()); 223 | const getResponse = await client.get(`/users/${createResponse.body.id}`); 224 | expect(getResponse.body).deepEqual(createResponse.body); 225 | }); 226 | 227 | it('creates bulk entities', async () => { 228 | const users = [getDummyUser(), getDummyUser(), getDummyUser()]; 229 | const createAllResponse = await client.post('/users-bulk').send(users); 230 | expect(createAllResponse.body.length).to.be.equal(users.length); 231 | }); 232 | 233 | it('deletes entity', async () => { 234 | const users = [getDummyUser(), getDummyUser(), getDummyUser()]; 235 | const createAllResponse = await client.post('/users-bulk').send(users); 236 | const deleteResponse = await client 237 | .delete(`/users/${createAllResponse.body[0].id}`) 238 | .send(); 239 | expect(deleteResponse.statusCode).to.be.equal(204); 240 | }); 241 | 242 | it('updates created entity', async () => { 243 | const user = getDummyUser(); 244 | const createResponse = await client.post('/users').send(user); 245 | const condition = {id: createResponse.body.id}; 246 | const patchResponse = await client 247 | .patch(`/users?where=${encodeURIComponent(JSON.stringify(condition))}`) 248 | .send({ 249 | name: 'Bar', 250 | }); 251 | expect(patchResponse.body).to.have.property('count', 1); 252 | }); 253 | 254 | it('can execute raw sql command without parameters', async function () { 255 | await client.post('/users').send(getDummyUser({name: 'Foo'})); 256 | 257 | const queryResult = await userRepo.execute('SELECT * from "user"'); 258 | 259 | expect(queryResult).to.have.length(1); 260 | expect(queryResult[0]).property('name').to.be.eql('Foo'); 261 | }); 262 | 263 | it('can execute raw sql command (select) using named parameters', async function () { 264 | await client.post('/users').send(getDummyUser({name: 'Foo'})); 265 | const bar = getDummyUser({name: 'Bar'}); 266 | await client.post('/users').send(bar); 267 | 268 | const queryResult = await userRepo.execute( 269 | 'SELECT * from "user" where name = $name', 270 | { 271 | name: 'Bar', 272 | }, 273 | ); 274 | 275 | expect(queryResult).to.have.length(1); 276 | expect(queryResult[0]).property('name').to.be.eql(bar.name); 277 | expect(queryResult[0]).property('email').to.be.eql(bar.email); 278 | }); 279 | 280 | it('can execute raw sql command (select) using positional parameters', async () => { 281 | await client.post('/users').send(getDummyUser({name: 'Foo'})); 282 | const bar = getDummyUser({name: 'Bar'}); 283 | await client.post('/users').send(bar); 284 | 285 | const queryResult = await userRepo.execute( 286 | 'SELECT * from "user" where name = $1', 287 | ['Bar'], 288 | ); 289 | 290 | expect(queryResult).to.have.length(1); 291 | expect(queryResult[0]).property('name').to.be.eql(bar.name); 292 | expect(queryResult[0]).property('email').to.be.eql(bar.email); 293 | }); 294 | 295 | it('can execute raw sql command (insert) using positional parameters', async () => { 296 | const user = getDummyUser({name: 'Foo', active: true}); 297 | if (primaryDataSourceConfig.connector === 'sqlite3') { 298 | // sqlite handles object and dates differently 299 | // it requires format like 2007-01-01 10:00:00 (https://stackoverflow.com/a/1933735/14200863) 300 | // and sequelize's sqlite dialect parses object returned from db so below reassignments are required here 301 | user.dob = '2023-05-23T04:12:22.234Z'; 302 | user.address = JSON.stringify(user.address); 303 | } 304 | 305 | // since the model mapping is not performed when executing raw queries 306 | // any column renaming need to be changed manually 307 | user.is_active = user.active; 308 | delete user.active; 309 | 310 | await userRepo.execute( 311 | 'INSERT INTO "user" (name, email, password, is_active, address, dob) VALUES ($1, $2, $3, $4, $5, $6)', 312 | [ 313 | user.name, 314 | user.email, 315 | user.password, 316 | user.is_active, 317 | user.address, 318 | user.dob, 319 | ], 320 | ); 321 | 322 | const users = await userRepo.execute('SELECT * from "user"'); 323 | 324 | expect(users).to.have.length(1); 325 | expect(users[0]).property('name').to.be.eql(user.name); 326 | expect(users[0]).property('email').to.be.eql(user.email); 327 | expect(users[0]).property('password').to.be.eql(user.password); 328 | expect(users[0]).property('address').to.be.eql(user.address); 329 | expect(new Date(users[0].dob)).to.be.eql(new Date(user.dob!)); 330 | expect(users[0]).property('is_active').to.be.ok(); 331 | }); 332 | 333 | it('can execute raw sql command (insert) using named parameters', async () => { 334 | const user = getDummyUser({name: 'Foo', active: true}); 335 | if (primaryDataSourceConfig.connector === 'sqlite3') { 336 | user.dob = '2023-05-23T04:12:22.234Z'; 337 | user.address = JSON.stringify(user.address); 338 | } 339 | 340 | // since the model mapping is not performed when executing raw queries 341 | // any column renaming need to be changed manually 342 | user.is_active = user.active; 343 | delete user.active; 344 | 345 | await userRepo.execute( 346 | 'INSERT INTO "user" (name, email, password, is_active, address, dob) VALUES ($name, $email, $password, $is_active, $address, $dob)', 347 | user, 348 | ); 349 | 350 | const users = await userRepo.execute('SELECT * from "user"'); 351 | 352 | expect(users).to.have.length(1); 353 | expect(users[0]).property('name').to.be.eql(user.name); 354 | expect(users[0]).property('email').to.be.eql(user.email); 355 | expect(users[0]).property('password').to.be.eql(user.password); 356 | expect(users[0]).property('address').to.be.eql(user.address); 357 | expect(new Date(users[0].dob)).to.be.eql(new Date(user.dob!)); 358 | expect(users[0]).property('is_active').to.be.ok(); 359 | }); 360 | 361 | it('can execute raw sql command (insert) using question mark replacement', async () => { 362 | const user = getDummyUser({name: 'Foo', active: true}); 363 | if (primaryDataSourceConfig.connector === 'sqlite3') { 364 | user.dob = '2023-05-23T04:12:22.234Z'; 365 | } 366 | 367 | // when using replacements (using "?" mark) 368 | // sequelize when escaping those values needs them as string (See: https://github.com/sequelize/sequelize/blob/v6/src/sql-string.js#L65-L77) 369 | user.address = JSON.stringify(user.address); 370 | 371 | // since the model mapping is not performed when executing raw queries 372 | // any column renaming need to be changed manually 373 | user.is_active = user.active; 374 | delete user.active; 375 | 376 | await userRepo.execute( 377 | 'INSERT INTO "user" (name, email, is_active, address, dob) VALUES (?, ?, ?, ?, ?)', 378 | [user.name, user.email, user.is_active, user.address, user.dob], 379 | ); 380 | 381 | const users = await userRepo.execute('SELECT * from "user"'); 382 | 383 | expect(users).to.have.length(1); 384 | expect(users[0]).property('name').to.be.eql(user.name); 385 | expect(users[0]).property('email').to.be.eql(user.email); 386 | expect(users[0]) 387 | .property('address') 388 | .to.be.oneOf(JSON.parse(user.address as string), user.address); 389 | expect(new Date(users[0].dob)).to.be.eql(new Date(user.dob!)); 390 | expect(users[0]).property('is_active').to.be.ok(); 391 | }); 392 | 393 | it('supports `fields` filter', async () => { 394 | const user = getDummyUser(); 395 | const createResponse = await client.post('/users').send(user); 396 | const filter = {fields: {name: true}}; 397 | const getResponse = await client.get( 398 | `/users/${createResponse.body.id}?filter=${encodeURIComponent( 399 | JSON.stringify(filter), 400 | )}`, 401 | ); 402 | expect(getResponse.body).deepEqual({name: user.name}); 403 | }); 404 | 405 | it('supports `order` filter', async () => { 406 | const users = [ 407 | getDummyUser({name: 'ABoy'}), 408 | getDummyUser({name: 'BBoy'}), 409 | getDummyUser({name: 'CBoy'}), 410 | ]; 411 | const createAllResponse = await client.post('/users-bulk').send(users); 412 | const reversedArray = [...createAllResponse.body].reverse(); 413 | const filter = { 414 | order: 'name DESC', 415 | }; 416 | const getResponse = await client.get( 417 | `/users?filter=${encodeURIComponent(JSON.stringify(filter))}`, 418 | ); 419 | expect(getResponse.body).to.be.deepEqual(reversedArray); 420 | }); 421 | 422 | it('supports `limit` filter', async () => { 423 | const users = [getDummyUser(), getDummyUser(), getDummyUser()]; 424 | await client.post('/users-bulk').send(users); 425 | const filter = { 426 | limit: 1, 427 | }; 428 | const getResponse = await client.get( 429 | `/users?filter=${encodeURIComponent(JSON.stringify(filter))}`, 430 | ); 431 | expect(getResponse.body.length).to.be.equal(filter.limit); 432 | }); 433 | 434 | it('uses table name if explicitly specified in model', async () => { 435 | const repo = new SequelizeCrudRepository(Event, datasource); 436 | expect(repo.getTableName()).to.be.eql(eventTableName); 437 | }); 438 | 439 | it('uses exact model class name as table name for mysql', async () => { 440 | datasource.stubs.sequelizeConfig = {dialect: 'mysql'}; 441 | const repo = new SequelizeCrudRepository(Box, datasource); 442 | expect(repo.getTableName()).to.be.eql(Box.name); 443 | }); 444 | 445 | it('uses lowercased model class name as table name for postgres', async () => { 446 | datasource.stubs.sequelizeConfig = {dialect: 'postgres'}; 447 | const repo = new SequelizeCrudRepository(Box, datasource); 448 | expect(repo.getTableName()).to.be.eql(Box.name.toLowerCase()); 449 | }); 450 | }); 451 | 452 | describe('With Relations', () => { 453 | async function migrateSchema(entities: Array) { 454 | for (const route of entities) { 455 | await client.get(`/${route}/sync-sequelize-model`).send(); 456 | } 457 | } 458 | 459 | it('supports @hasOne', async () => { 460 | await migrateSchema(['users', 'todo-lists']); 461 | 462 | const user = getDummyUser(); 463 | const userRes = await client.post('/users').send(user); 464 | const todoList = getDummyTodoList({user: userRes.body.id}); 465 | const todoListRes = await client.post('/todo-lists').send(todoList); 466 | 467 | const filter = {include: ['todoList']}; 468 | const relationRes = await client.get( 469 | `/users?filter=${encodeURIComponent(JSON.stringify(filter))}`, 470 | ); 471 | 472 | expect(relationRes.body).to.be.deepEqual([ 473 | { 474 | ...userRes.body, 475 | todoList: todoListRes.body, 476 | }, 477 | ]); 478 | }); 479 | 480 | it('supports @hasMany', async () => { 481 | await migrateSchema(['todos', 'todo-lists']); 482 | 483 | const todoList = getDummyTodoList(); 484 | const todoListRes = await client.post('/todo-lists').send(todoList); 485 | 486 | const todo = getDummyTodo({todoListId: todoListRes.body.id}); 487 | const todoRes = await client.post('/todos').send(todo); 488 | 489 | const filter = {include: ['todos']}; 490 | const relationRes = await client.get( 491 | `/todo-lists?filter=${encodeURIComponent(JSON.stringify(filter))}`, 492 | ); 493 | 494 | expect(relationRes.body).to.be.deepEqual([ 495 | { 496 | ...todoListRes.body, 497 | todos: [todoRes.body], 498 | user: null, 499 | }, 500 | ]); 501 | }); 502 | 503 | it('supports @belongsTo', async () => { 504 | await migrateSchema(['users', 'todos', 'todo-lists']); 505 | 506 | const userRes = await client.post('/users').send(getDummyUser()); 507 | 508 | const todoListRes = await client 509 | .post('/todo-lists') 510 | .send(getDummyTodoList({user: userRes.body.id})); 511 | 512 | const todo = getDummyTodo({todoListId: todoListRes.body.id}); 513 | const todoRes = await client.post('/todos').send(todo); 514 | 515 | const filter = {include: ['todoList']}; 516 | const relationRes = await client.get( 517 | `/todos?filter=${encodeURIComponent(JSON.stringify(filter))}`, 518 | ); 519 | 520 | expect(relationRes.body).to.be.deepEqual([ 521 | { 522 | ...todoRes.body, 523 | todoList: todoListRes.body, 524 | }, 525 | ]); 526 | }); 527 | 528 | it('supports @hasMany through', async () => { 529 | await migrateSchema(['doctors']); 530 | 531 | const doctorRes = await client.post('/doctors').send(getDummyDoctor()); 532 | const patientRes = await client 533 | .post(`/doctors/${1}/patients`) 534 | .send(getDummyPatient()); 535 | 536 | const filter = {include: ['patients']}; 537 | const relationRes = await client.get( 538 | `/doctors?filter=${encodeURIComponent(JSON.stringify(filter))}`, 539 | ); 540 | 541 | /** 542 | * Manually Remove through table data as sqlite3 doesn't support `attributes: []` using sequelize 543 | */ 544 | delete relationRes.body[0].patients[0].Appointment; 545 | 546 | expect(relationRes.body).to.be.deepEqual([ 547 | { 548 | ...doctorRes.body, 549 | patients: [patientRes.body], 550 | }, 551 | ]); 552 | }); 553 | 554 | it('supports @referencesMany', async () => { 555 | await migrateSchema(['developers']); 556 | 557 | const programmingLanguages = [ 558 | getDummyProgrammingLanguage({name: 'JS', secret: 'Practice'}), 559 | getDummyProgrammingLanguage({name: 'Java', secret: 'Practice'}), 560 | getDummyProgrammingLanguage({name: 'Dot Net', secret: 'Practice'}), 561 | ]; 562 | const createAllResponse = await client 563 | .post('/programming-languages-bulk') 564 | .send(programmingLanguages); 565 | 566 | const createDeveloperResponse = await client.post('/developers').send( 567 | getDummyDeveloper({ 568 | apiSecret: 'xyz-123-abcd', 569 | programmingLanguageIds: createAllResponse.body.map( 570 | (language: {id: number}) => language.id, 571 | ), 572 | }), 573 | ); 574 | 575 | const filter = {include: ['programmingLanguages']}; 576 | const relationRes = await client 577 | .get( 578 | `/developers/${ 579 | createDeveloperResponse.body.id 580 | }?filter=${encodeURIComponent(JSON.stringify(filter))}`, 581 | ) 582 | .send(); 583 | 584 | if (primaryDataSourceConfig.connector === 'sqlite3') { 585 | /** 586 | * sqlite3 doesn't support array data type using it will convert values 587 | * to comma saperated string 588 | */ 589 | createDeveloperResponse.body.programmingLanguageIds = 590 | createDeveloperResponse.body.programmingLanguageIds.join(','); 591 | } 592 | 593 | expect(relationRes.body).to.be.deepEqual({ 594 | ...createDeveloperResponse.body, 595 | programmingLanguages: createAllResponse.body, 596 | }); 597 | }); 598 | 599 | it('hides hidden props for nested entities included with referencesMany relation', async () => { 600 | await developerRepo.syncLoadedSequelizeModels({force: true}); 601 | 602 | const programmingLanguages = [ 603 | getDummyProgrammingLanguage({name: 'JS', secret: 'woo'}), 604 | getDummyProgrammingLanguage({name: 'Java', secret: 'woo'}), 605 | getDummyProgrammingLanguage({name: 'Dot Net', secret: 'woo'}), 606 | ]; 607 | 608 | const createAllResponse = await languagesRepo.createAll( 609 | programmingLanguages, 610 | ); 611 | expect(createAllResponse[0]).to.have.property('secret'); 612 | 613 | const createDeveloperResponse = await developerRepo.create( 614 | getDummyDeveloper({ 615 | apiSecret: 'xyz-123-abcd', 616 | programmingLanguageIds: createAllResponse.map( 617 | (language: ProgrammingLanguage) => language.id, 618 | ), 619 | }), 620 | ); 621 | 622 | const filter = {include: ['programmingLanguages']}; 623 | const relationRes = await developerRepo.findById( 624 | createDeveloperResponse.id, 625 | filter, 626 | ); 627 | 628 | if (primaryDataSourceConfig.connector === 'sqlite3') { 629 | /** 630 | * sqlite3 doesn't support array data type using it will convert values 631 | * to comma saperated string 632 | */ 633 | createDeveloperResponse.programmingLanguageIds = 634 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 635 | createDeveloperResponse.programmingLanguageIds.join(',') as any; 636 | } 637 | 638 | expect(relationRes.toJSON()).to.be.deepEqual({ 639 | ...createDeveloperResponse.toJSON(), 640 | programmingLanguages: createAllResponse.map(e => e.toJSON()), 641 | }); 642 | }); 643 | 644 | it('supports INNER JOIN', async () => { 645 | await migrateSchema(['books']); 646 | 647 | const categories = [ 648 | {name: 'Programming'}, 649 | {name: 'Cooking'}, 650 | {name: 'Self Help'}, 651 | ]; 652 | 653 | const categoryResponse = await client 654 | .post('/categories-bulk') 655 | .send(categories); 656 | 657 | type Category = {name: string; id: number}; 658 | 659 | const books = [ 660 | { 661 | title: 'The Art of Cooking', 662 | categoryId: categoryResponse.body.find( 663 | (cat: Category) => cat.name === 'Cooking', 664 | ).id, 665 | }, 666 | { 667 | title: 'JavaScript the Art of Web', 668 | categoryId: categoryResponse.body.find( 669 | (cat: Category) => cat.name === 'Programming', 670 | ).id, 671 | }, 672 | { 673 | title: '7 Rules of life', 674 | categoryId: categoryResponse.body.find( 675 | (cat: Category) => cat.name === 'Self Help', 676 | ).id, 677 | }, 678 | ]; 679 | 680 | await client.post('/books-bulk').send(books); 681 | 682 | const filter = { 683 | where: {title: {like: '%Art%'}}, 684 | include: [ 685 | { 686 | relation: 'category', 687 | scope: {where: {name: 'Programming'}}, 688 | required: true, 689 | }, 690 | ], 691 | }; 692 | 693 | const relationRes = await client 694 | .get(`/books?filter=${encodeURIComponent(JSON.stringify(filter))}`) 695 | .send(); 696 | 697 | // If only 1 entry is returned it ensures that the cooking entry is not envolved. 698 | // Confirming the fact that it used inner join behind the scenes 699 | expect(relationRes.body.length).to.be.equal(1); 700 | }); 701 | 702 | it('throws error if the repository does not have registered resolvers', async () => { 703 | try { 704 | await userRepo.find({ 705 | include: ['nonExistingRelation'], 706 | }); 707 | } catch (err) { 708 | expect(err.message).to.be.eql( 709 | `Invalid "filter.include" entries: "nonExistingRelation"`, 710 | ); 711 | expect(err.statusCode).to.be.eql(400); 712 | expect(err.code).to.be.eql('INVALID_INCLUSION_FILTER'); 713 | } 714 | }); 715 | }); 716 | 717 | describe('Connections', () => { 718 | async function migrateSchema(entities: Array) { 719 | for (const route of entities) { 720 | await client.get(`/${route}/sync-sequelize-model`).send(); 721 | } 722 | } 723 | it('can work with two datasources together', async () => { 724 | await migrateSchema(['todo-lists', 'products']); 725 | 726 | // todo-lists model uses primary datasource 727 | const todoList = getDummyTodoList(); 728 | const todoListCreateRes = await client.post('/todo-lists').send(todoList); 729 | 730 | // products model uses secondary datasource 731 | const product = getDummyProduct(); 732 | const productCreateRes = await client.post('/products').send(product); 733 | 734 | expect(todoListCreateRes.body).to.have.properties('id', 'title'); 735 | expect(productCreateRes.body).to.have.properties('id', 'name', 'price'); 736 | expect(todoListCreateRes.body.title).to.be.equal(todoList.title); 737 | expect(productCreateRes.body.name).to.be.equal(product.name); 738 | }); 739 | }); 740 | 741 | describe('Transactions', () => { 742 | const DB_ERROR_MESSAGES = { 743 | invalidTransaction: [ 744 | `relation "${TableInSecondaryDB}" does not exist`, 745 | `SQLITE_ERROR: no such table: ${TableInSecondaryDB}`, 746 | ], 747 | }; 748 | async function migrateSchema(entities: Array) { 749 | for (const route of entities) { 750 | await client.get(`/${route}/sync-sequelize-model`).send(); 751 | } 752 | } 753 | 754 | it('retrieves model instance once transaction is committed', async () => { 755 | await migrateSchema(['todo-lists']); 756 | 757 | const todoList = getDummyTodoList(); 758 | const todoListCreateRes = await client 759 | .post('/transactions/todo-lists/commit') 760 | .send(todoList); 761 | 762 | const todoListReadRes = await client.get( 763 | `/todo-lists/${todoListCreateRes.body.id}`, 764 | ); 765 | 766 | expect(todoListReadRes.body).to.have.properties('id', 'title'); 767 | expect(todoListReadRes.body.title).to.be.equal(todoList.title); 768 | }); 769 | 770 | it('can rollback transaction', async function () { 771 | await migrateSchema(['todo-lists']); 772 | 773 | const todoList = getDummyTodoList(); 774 | const todoListCreateRes = await client 775 | .post('/transactions/todo-lists/rollback') 776 | .send(todoList); 777 | 778 | const todoListReadRes = await client.get( 779 | `/todo-lists/${todoListCreateRes.body.id}`, 780 | ); 781 | 782 | expect(todoListReadRes.body).to.have.properties('error'); 783 | expect(todoListReadRes.body.error.code).to.be.equal('ENTITY_NOT_FOUND'); 784 | }); 785 | 786 | it('ensures transactions are isolated', async function () { 787 | if (primaryDataSourceConfig.connector === 'sqlite3') { 788 | // Skip "READ_COMMITED" test for sqlite3 as it doesn't support it through isolationLevel options. 789 | // eslint-disable-next-line @typescript-eslint/no-invalid-this 790 | this.skip(); 791 | } else { 792 | await migrateSchema(['todo-lists']); 793 | 794 | const todoList = getDummyTodoList(); 795 | const todoListCreateRes = await client 796 | .post('/transactions/todo-lists/isolation/read_commited') 797 | .send(todoList); 798 | 799 | expect(todoListCreateRes.body).to.have.properties('error'); 800 | expect(todoListCreateRes.body.error.code).to.be.equal( 801 | 'ENTITY_NOT_FOUND', 802 | ); 803 | } 804 | }); 805 | 806 | it('ensures local transactions (should not use transaction with another repository of different datasource)', async function () { 807 | if (secondaryDataSourceConfig.connector === 'sqlite3') { 808 | // Skip local transactions test for sqlite3 as it doesn't support it through isolationLevel options. 809 | // eslint-disable-next-line @typescript-eslint/no-invalid-this 810 | this.skip(); 811 | } else { 812 | await migrateSchema(['todo-lists', 'products']); 813 | 814 | const response = await client.get('/transactions/ensure-local').send(); 815 | 816 | expect(response.body).to.have.properties('error'); 817 | expect(response.body.error.message).to.be.oneOf( 818 | DB_ERROR_MESSAGES.invalidTransaction, 819 | ); 820 | } 821 | }); 822 | }); 823 | 824 | async function getAppAndClient() { 825 | const artifacts: AnyObject = { 826 | datasources: ['config', 'primary.datasource', 'secondary.datasource'], 827 | models: [ 828 | 'index', 829 | 'todo.model', 830 | 'todo-list.model', 831 | 'user.model', 832 | 'doctor.model', 833 | 'patient.model', 834 | 'appointment.model', 835 | 'programming-language.model', 836 | 'developer.model', 837 | 'book.model', 838 | 'category.model', 839 | 'product.model', 840 | 'task.model', 841 | ], 842 | repositories: [ 843 | 'index', 844 | 'todo.repository', 845 | 'todo-list.repository', 846 | 'user.repository', 847 | 'doctor.repository', 848 | 'patient.repository', 849 | 'appointment.repository', 850 | 'developer.repository', 851 | 'programming-language.repository', 852 | 'book.repository', 853 | 'category.repository', 854 | 'product.repository', 855 | 'task.repository', 856 | ], 857 | controllers: [ 858 | 'index', 859 | 'book-category.controller', 860 | 'book.controller', 861 | 'category.controller', 862 | 'developer.controller', 863 | 'doctor-patient.controller', 864 | 'doctor.controller', 865 | 'patient.controller', 866 | 'programming-languange.controller', 867 | 'todo-list-todo.controller', 868 | 'todo-list.controller', 869 | 'todo-todo-list.controller', 870 | 'todo.controller', 871 | 'user-todo-list.controller', 872 | 'user.controller', 873 | 'transaction.controller', 874 | 'product.controller', 875 | 'test.controller.base', 876 | 'task.controller', 877 | ], 878 | }; 879 | 880 | const copyFilePromises: Array> = []; 881 | 882 | for (const folder in artifacts) { 883 | artifacts[folder].forEach((fileName: string) => { 884 | copyFilePromises.push( 885 | sandbox.copyFile( 886 | resolve(__dirname, `../fixtures/${folder}/${fileName}.js`), 887 | `${folder}/${fileName}.js`, 888 | ), 889 | ); 890 | }); 891 | } 892 | 893 | await Promise.all([ 894 | sandbox.copyFile(resolve(__dirname, '../fixtures/application.js')), 895 | 896 | ...copyFilePromises, 897 | ]); 898 | 899 | const MyApp = require(resolve( 900 | sandbox.path, 901 | 'application.js', 902 | )).SequelizeSandboxApplication; 903 | app = new MyApp({ 904 | rest: givenHttpServerConfig(), 905 | }); 906 | 907 | await app.boot(); 908 | await app.start(); 909 | 910 | userRepo = await app.getRepository(UserRepository); 911 | taskRepo = await app.getRepository(TaskRepository); 912 | developerRepo = await app.getRepository(DeveloperRepository); 913 | languagesRepo = await app.getRepository(ProgrammingLanguageRepository); 914 | datasource = createStubInstance(SequelizeDataSource); 915 | client = createRestAppClient(app as RestApplication); 916 | } 917 | 918 | function getDummyUser(overwrite = {}) { 919 | const date = new Date(); 920 | const timestamp = date.toISOString(); 921 | 922 | type DummyUser = { 923 | name: string; 924 | email: string; 925 | active?: boolean; 926 | address: AnyObject | string; 927 | password?: string; 928 | dob: Date | string; 929 | } & AnyObject; 930 | 931 | const user: DummyUser = { 932 | name: 'Foo', 933 | email: 'email@example.com', 934 | active: true, 935 | address: {city: 'Indore', zipCode: 452001}, 936 | password: 'secret', 937 | dob: timestamp, 938 | ...overwrite, 939 | }; 940 | return user; 941 | } 942 | function getDummyTodoList(overwrite = {}) { 943 | const todoList = { 944 | title: 'My Todo List', 945 | ...overwrite, 946 | }; 947 | return todoList; 948 | } 949 | function getDummyProduct(overwrite = {}) { 950 | const todoList = { 951 | name: 'Phone', 952 | price: 5000, 953 | ...overwrite, 954 | }; 955 | return todoList; 956 | } 957 | 958 | function getDummyTodo(overwrite = {}) { 959 | const todo = { 960 | title: 'Fix Bugs', 961 | isComplete: false, // can never be true :P 962 | ...overwrite, 963 | }; 964 | return todo; 965 | } 966 | function getDummyDoctor(overwrite = {}) { 967 | const doctor = { 968 | name: 'Dr. Foo', 969 | ...overwrite, 970 | }; 971 | return doctor; 972 | } 973 | function getDummyPatient(overwrite = {}) { 974 | const patient = { 975 | name: 'Foo', 976 | ...overwrite, 977 | }; 978 | return patient; 979 | } 980 | function getDummyProgrammingLanguage(overwrite = {}) { 981 | const programmingLanguage = { 982 | name: 'JavaScript', 983 | ...overwrite, 984 | }; 985 | return programmingLanguage; 986 | } 987 | function getDummyDeveloper(overwrite = {}) { 988 | const developer = { 989 | name: 'Foo', 990 | ...overwrite, 991 | }; 992 | return developer; 993 | } 994 | }); 995 | --------------------------------------------------------------------------------