[
  {
    "path": ".dockerignore",
    "content": "/.docker\n/.github\n/.vscode\n/docker\n/Dockerfile\n/docker-compose.yml\n/node_modules\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*"
  },
  {
    "path": ".env.example",
    "content": "### Please consult https://llana.io/configuration for full details  ###\n\nSOFTWARE_VERSION_TAG=latest\nPORT=3000\nADMIN_EMAIL=test@test.com\n\nDATABASE_URI=\n\nJWT_KEY=S$3cr3tK3y\nJWT_REFRESH_KEY=S$3cr3tK3yRefresh\n\n#Auth Settings\nAUTH_USER_TABLE_NAME=User\n\n#Delete Settings\nSOFT_DELETE_COLUMN=deletedAt\n\n#Logging\nLOG_LEVELS=\"error,warn,log,debug,verbose\"\n#LOG_LEVELS=\"error,warn,log\"\n\n#URL of your Llana instance\nBASE_URL_API=https://api.my-llana.com\n#URL of your frontend application\nBASE_URL_APP=https://www.my-llana.com\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file\n\nversion: 2\nupdates:\n  - package-ecosystem: \"npm\"\n    directory: \"/\" # Location of package manifests\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/codeql.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\n# ******** NOTE ********\n# We have attempted to detect the languages in your repository. Please check\n# the `language` matrix defined below to confirm you have the correct set of\n# supported CodeQL languages.\n#\nname: \"CodeQL\"\n\non:\n  pull_request:\n    branches: [ \"main\" ]\n  schedule:\n    - cron: '29 7 * * 2'\n\njobs:\n  analyze:\n    name: Analyze (${{ matrix.language }})\n    # Runner size impacts CodeQL analysis time. To learn more, please see:\n    #   - https://gh.io/recommended-hardware-resources-for-running-codeql\n    #   - https://gh.io/supported-runners-and-hardware-resources\n    #   - https://gh.io/using-larger-runners (GitHub.com only)\n    # Consider using larger runners or machines with greater resources for possible analysis time improvements.\n    runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}\n    permissions:\n      # required for all workflows\n      security-events: write\n\n      # required to fetch internal or private CodeQL packs\n      packages: read\n\n      # only required for workflows in private repositories\n      actions: read\n      contents: read\n\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n        - language: javascript-typescript\n          build-mode: none\n        # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'\n        # Use `c-cpp` to analyze code written in C, C++ or both\n        # Use 'java-kotlin' to analyze code written in Java, Kotlin or both\n        # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both\n        # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,\n        # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.\n        # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how\n        # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v4\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v3\n      with:\n        languages: ${{ matrix.language }}\n        build-mode: ${{ matrix.build-mode }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file.\n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n\n        # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs\n        # queries: security-extended,security-and-quality\n\n    # If the analyze step fails for one of the languages you are analyzing with\n    # \"We were unable to automatically build your code\", modify the matrix above\n    # to set the build mode to \"manual\" for that language. Then modify this step\n    # to build your code.\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun\n    - if: matrix.build-mode == 'manual'\n      shell: bash\n      run: |\n        echo 'If you are using a \"manual\" build mode for one or more of the' \\\n          'languages you are analyzing, replace this with the commands to build' \\\n          'your code, for example:'\n        echo '  make bootstrap'\n        echo '  make release'\n        exit 1\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v3\n      with:\n        category: \"/language:${{matrix.language}}\"\n"
  },
  {
    "path": ".github/workflows/pr.yml",
    "content": "#\n# GitHub Actions workflow.\n#\n# Perfoms the following actions on a pull request:\n# * Checkout the code\n# * Install Node.js\n# * Prepare the environment\n# * Install dependencies\n# * Lint the code\n# * Run the tests\n#\n\nname: 'PR Checks: Llana'\n\non:\n    pull_request:\n        branches:\n            - main\n    workflow_dispatch:\n    workflow_call:\n\njobs:\n    pr_checks:\n        name: 'Pull Request Package: Llana'\n        runs-on: ubuntu-latest\n\n        steps:\n            - name: 'Checkout'\n              uses: actions/checkout@v4\n              with:\n                  token: ${{ secrets.GH_CI_CD_RELEASE }}\n\n            - name: Install Node.js\n              uses: actions/setup-node@v4\n              with:\n                  node-version: 22.16.0\n\n            - name: Install Docker using Docker's official script\n              run: |\n                  curl -fsSL https://get.docker.com -o get-docker.sh\n                  sudo sh get-docker.sh\n              continue-on-error: false\n\n            - name: Install Docker Compose\n              run: |\n                  sudo curl -L \"https://github.com/docker/compose/releases/download/v2.3.3/docker-compose-$(uname -s)-$(uname -m)\" -o /usr/local/bin/docker-compose\n                  sudo chmod +x /usr/local/bin/docker-compose\n                  docker-compose --version\n              continue-on-error: false\n\n            - name: Install dependencies\n              run: npm install\n\n            - name: Lint\n              run: npm run lint\n\n            - name: Setup Docker\n              run: npm run docker:dev\n\n            - name: Test\n              run: npm run test\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "#\n# GitHub Actions workflow.\n#\n# Releases the package to npm when a push into main is detected.\n# * Checkout the code\n# * Install Node.js\n# * Install dependencies\n# * Pull the latest changes\n# * Bump version number\n# * Release to NPM\n# * Pull the latest changes\n# * Generate Docker meta\n# * Build and push image\n\n#\n\nname: 'Release Package: Llana'\n\non:\n    push:\n        branches:\n            - main\n    workflow_dispatch:\n\njobs:\n\n    release:\n        name: 'Release Package: CLI'\n        runs-on: ubuntu-latest\n        if: ${{ !contains(github.event.head_commit.message, '#skip-release') }}\n        permissions:\n            contents: write\n        steps:\n\n            -   name: 'Checkout'\n                uses: actions/checkout@v4\n                with:\n                    token: ${{ secrets.GH_CI_CD_RELEASE }}\n\n            -   name: 'Install Node.js'\n                uses: actions/setup-node@v4\n                with:\n                    node-version: 22.16.0\n\n            -   name: 'Install dependencies'\n                run: npm install\n\n            -   run: git pull --force\n\n            -   name: 'Version Bump'\n                id: version\n                if: ${{ !contains(github.event.head_commit.message, '#skip-version-bump') }}\n                uses: phips28/gh-action-bump-version@master\n                env:\n                    GITHUB_TOKEN: ${{ secrets.GH_CI_CD_RELEASE }}\n                with:\n                    major-wording: 'MAJOR'\n                    minor-wording: 'feature,feat'\n                    patch-wording: 'patch,fixes,fix,misc,docs,refactor'    # Providing patch-wording will override commits\n                    commit-message: 'CI: Bump Version to {{version}} [skip ci]'\n                    tag-prefix: 'v'\n\n            -   run: git pull --force #Ensure we have the latest package version before pushing to NPM / Docker\n\n            -   name: 'Authenticate with NPM'\n                if: ${{ !contains(github.event.head_commit.message, '#skip-npm-publish') }}\n                run: echo -e \"//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}\" > .npmrc\n\n            -   name: 'Publishing package'\n                if: ${{ !contains(github.event.head_commit.message, '#skip-npm-publish') }}\n                run: npm publish --no-git-checks --access public\n\n            -   name: Docker meta\n                id: meta\n                uses: docker/metadata-action@v5\n                with:\n                  # list of Docker images to use as base name for tags\n                  images: |\n                    juicyllama/llana\n                   # ghcr.io/username/app            \n                  # generate Docker tags based on the following events/attributes\n                  tags: |\n                    type=semver,pattern=v{{version}},value=${{ steps.version.outputs.newTag }}\n                    type=semver,pattern=v{{major}}.{{minor}},value=${{ steps.version.outputs.newTag }}\n                    type=semver,pattern=v{{major}},value=${{ steps.version.outputs.newTag }}\n                    type=sha\n\n            -   name: Set up QEMU\n                uses: docker/setup-qemu-action@v3\n\n            -   name: Set up Docker Buildx\n                uses: docker/setup-buildx-action@v3\n\n            -   name: Login to Docker Hub\n                uses: docker/login-action@v3\n                with:\n                    username: juicyllama\n                    password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n            #Checkout again to get latest package.json after bump and before we deploy\n            -   name: 'Checkout'\n                uses: actions/checkout@v4\n                with:\n                    token: ${{ secrets.GH_CI_CD_RELEASE }}\n\n            -   name: Build and push image\n                uses: docker/build-push-action@v6\n                if: ${{ !contains(github.event.head_commit.message, '#skip-docker-publish') }}\n                with:\n                    file: ./docker/images/base/Dockerfile\n                    sbom: true\n                    provenance: mode=max\n                    push: true\n                    tags: ${{ steps.meta.outputs.tags }}\n                    labels: ${{ steps.meta.outputs.labels }}\n                    platforms: linux/amd64,linux/arm64"
  },
  {
    "path": ".github/workflows/snyk-security.yml",
    "content": "name: Snyk Security\n\non:\n  pull_request:\n    branches: [\"main\"]\n\npermissions:\n  contents: read\n\njobs:\n  snyk:\n    permissions:\n      contents: read # for actions/checkout to fetch code\n      security-events: write # for github/codeql-action/upload-sarif to upload SARIF results\n      actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Set up Snyk CLI to check for security issues\n        # Snyk can be used to break the build when it detects security issues.\n        # In this case we want to upload the SAST issues to GitHub Code Scanning\n        uses: snyk/actions/setup@806182742461562b67788a64410098c9d9b96adb\n\n        # For Snyk Open Source you must first set up the development environment for your application's dependencies\n        # For example for Node\n        #- uses: actions/setup-node@v4\n        #  with:\n        #    node-version: 20\n        \n        continue-on-error: true\n\n        env:\n          # This is where you will need to introduce the Snyk API token created with your Snyk account\n          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}\n\n        # Runs Snyk Code (SAST) analysis and uploads result into GitHub.\n        # Use || true to not fail the pipeline\n      - name: Snyk Code test\n        run: snyk code test --sarif > snyk-code.sarif || true\n"
  },
  {
    "path": ".gitignore",
    "content": "#Env\n.env\n.env.*\n!.env.example\n\n#llana specific\nopenapi.json\n\n\n# Created by .ignore support plugin (hsz.mobi)\n### JetBrains template\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff:\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/dictionaries\n\n# Sensitive or high-churn files:\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.xml\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n\n# Gradle:\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# CMake\ncmake-build-debug/\n\n# Mongo Explorer plugin:\n.idea/**/mongoSettings.xml\n\n## File-based project format:\n*.iws\n\n## Plugin-specific files:\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n### VisualStudio template\n## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore\n\n# User-specific files\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n\n# Visual Studio 2015 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUNIT\n*.VisualState.xml\nTestResult.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# Benchmark Results\nBenchmarkDotNet.Artifacts/\n\n# .NET Core\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n**/Properties/launchSettings.json\n\n*_i.c\n*_p.c\n*_i.h\n*.ilk\n*.meta\n*.obj\n*.pch\n*.pdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# Visual Studio Trace Files\n*.e2e\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# JustCode is a .NET coding add-in\n.JustCode\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# AxoCover is a Code Coverage Tool\n.axoCover/*\n!.axoCover/settings.json\n\n# Visual Studio code coverage results\n*.coverage\n*.coveragexml\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# Note: Comment the next line if you want to checkin your web deploy settings,\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# The packages folder can be ignored because of Package Restore\n**/[Pp]ackages/*\n# except build/, which is used as an MSBuild target.\n!**/[Pp]ackages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/[Pp]ackages/repositories.config\n# NuGet v3's project.json files produces more ignorable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n*.appx\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\norleans.codegen.cs\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\n\n# SQL Server files\n*.mdf\n*.ldf\n*.ndf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\nnode_modules/\n\n# Typescript v1 declaration files\ntypings/\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)\n*.vbw\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# JetBrains Rider\n.idea/\n*.sln.iml\n\n# IDE - VSCode\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n\n# CodeRush\n.cr/\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake - Uncomment if you are using it\n# tools/**\n# !tools/packages.config\n\n# Tabs Studio\n*.tss\n\n# Telerik's JustMock configuration file\n*.jmconfig\n\n# BizTalk build output\n*.btp.cs\n*.btm.cs\n*.odx.cs\n*.xsd.cs\n\n# OpenCover UI analysis results\nOpenCover/\ncoverage/\n\n### macOS template\n# General\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n=======\n# Local\ndist\n.webpack\n.serverless/**/*.zip\n\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n\t\"version\": \"0.2.0\",\n\t\"configurations\": [\n\t\t{\n\t\t\t\"name\": \"start\",\n\t\t\t\"type\": \"node\",\n\t\t\t\"request\": \"launch\",\n\t\t\t\"runtimeExecutable\": \"npm\",\n\t\t\t\"console\": \"integratedTerminal\",\n\t\t\t\"runtimeArgs\": [\"run\", \"start\"],\n\t\t\t\"env\": {\n\t\t\t\t\"LOG_LEVEL\": \"3\",\n\t\t\t\t\"LOGGING\": \"query\"\n\t\t\t},\n\t\t\t\"cwd\": \"${workspaceFolder}\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"test\",\n\t\t\t\"type\": \"node\",\n\t\t\t\"request\": \"launch\",\n\t\t\t\"runtimeExecutable\": \"npm\",\n\t\t\t\"console\": \"integratedTerminal\",\n\t\t\t\"runtimeArgs\": [\"run\", \"test\"],\n\t\t\t\"env\": {\n\t\t\t\t\"LOG_LEVEL\": \"2\"\n\t\t\t},\n\t\t\t\"cwd\": \"${workspaceFolder}\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"test:mysql\",\n\t\t\t\"type\": \"node\",\n\t\t\t\"request\": \"launch\",\n\t\t\t\"runtimeExecutable\": \"npm\",\n\t\t\t\"console\": \"integratedTerminal\",\n\t\t\t\"runtimeArgs\": [\"run\", \"test:mysql\"],\n\t\t\t\"env\": {\n\t\t\t\t\"LOG_LEVEL\": \"2\"\n\t\t\t},\n\t\t\t\"cwd\": \"${workspaceFolder}\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"test:mongodb\",\n\t\t\t\"type\": \"node\",\n\t\t\t\"request\": \"launch\",\n\t\t\t\"runtimeExecutable\": \"npm\",\n\t\t\t\"console\": \"integratedTerminal\",\n\t\t\t\"runtimeArgs\": [\"run\", \"test:mongodb\"],\n\t\t\t\"env\": {\n\t\t\t\t\"LOG_LEVEL\": \"2\"\n\t\t\t},\n\t\t\t\"cwd\": \"${workspaceFolder}\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"test:mssql\",\n\t\t\t\"type\": \"node\",\n\t\t\t\"request\": \"launch\",\n\t\t\t\"runtimeExecutable\": \"npm\",\n\t\t\t\"console\": \"integratedTerminal\",\n\t\t\t\"runtimeArgs\": [\"run\", \"test:mssql\"],\n\t\t\t\"env\": {\n\t\t\t\t\"LOG_LEVEL\": \"2\"\n\t\t\t},\n\t\t\t\"cwd\": \"${workspaceFolder}\"\n\t\t}\n\t]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"cSpell.words\": [\n        \"Bootup\",\n        \"LLANA\"\n    ]\n}"
  },
  {
    "path": ".vscode/tasks.json",
    "content": "{\n    \"version\": \"2.0.0\",\n    \"tasks\": [\n        {\n            \"label\": \"Start Docker\",\n            \"type\": \"shell\",\n            \"command\": \"npm run start:docker\",\n            \"windows\": {\n                \"command\": \"npm run start:docker\"\n            },\n            \"group\": \"none\",\n            \"presentation\": {\n                \"reveal\": \"always\",\n                \"panel\": \"new\"\n            },\n            // \"runOptions\": {\n            //     \"runOn\": \"folderOpen\",\n            // }\n        },\n    ]\n}"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n\n# Llana\n\n  <a href=\"https://juicyllama.com/\" target=\"_blank\">\n    <img src=\"https://juicyllama.com/assets/images/llana-rounded.png\" width=\"100\" alt=\"JuicyLlama Logo\" />\n  </a>\n\n</div>\n\n## Database REST API in minutes\n\nLlana is a lightweight API wrapper that exposes a REST API for any database within minutes. Stop wasting time building endpoints, just connect your database and start playing. Open source, free to use, and no vendor lock-in.\n\n## Documentation\n\nVisit [Llana.io](https://llana.io) for full documentation.\n\n\n## Databases\n\nWe are working to support all major databases, if you would like to contribute to the open source project and help integrate your preferred database flavor, checkout our [contribution guidelines](https://llana.io/developers/contributing).\n\n[ExpressJs Database Integration Guide](https://expressjs.com/en/guide/database-integration.html)\n\n![ORACLE](https://badgen.net/badge/ORACLE/In%20Development/orange)<br>\n![MYSQL](https://badgen.net/badge/MYSQL/Version%201/green)<br>\n![MSSQL](https://badgen.net/badge/MSSQL/Beta%20Phase/green)<br>\n![POSTGRES](https://badgen.net/badge/POSTGRES/Beta%20Phase/green)<br>\n![MONGODB](https://badgen.net/badge/MONGODB/Beta%20Phase/green)<br>\n![REDIS](https://badgen.net/badge/REDIS/Help%20Welcomed/red)<br>\n![SNOWFLAKE](https://badgen.net/badge/SNOWFLAKE/Help%20Welcomed/red)<br>\n![ELASTICSEARCH](https://badgen.net/badge/ELASTICSEARCH/Help%20Welcomed/red)<br>\n![SQLITE](https://badgen.net/badge/SQLITE/Help%20Welcomed/red)<br>\n![CASSANDRA](https://badgen.net/badge/CASSANDRA/Help%20Welcomed/red)<br>\n![MARIADB](https://badgen.net/badge/MARIADB/Help%20Welcomed/red)<br>\n\n[See the complete breakdown of which data sources are integrated](https://llana.io/data-sources/overview)\n\n## Integrations\n\n![n8n](https://n8n.io)\n![Nuxt](https://nuxt.com)\n"
  },
  {
    "path": "demo/databases/airtable.ts",
    "content": "import 'dotenv/config'\nimport { Logger } from '../../src/helpers/Logger'\nimport axios, { AxiosRequestConfig } from 'axios'\n\n// Data\nconst Customers = require('./json/Customer.json')\nconst Employees = require('./json/Employee.json')\nconst Shippers = require('./json/Shipper.json')\n\nconst ENDPOINT = 'https://api.airtable.com/v0'\nconst AIRTABLE = process.env.DATABASE_URI as string\nconst DOMAIN = 'AIRTABLE'\nconst [apiKey, baseId] = AIRTABLE.split('://')[1].split('@')\nconst logger = new Logger()\n\nconst user = {\n\tuserId: 1,\n\temail: 'test@test.com',\n\tpassword: '$2a$10$jm6bM7acpRa18Vdy8FSqIu4yzWAdSgZgRtRrx8zknIeZhSqPJjJU.',\n\trole: 'ADMIN',\n\tfirstName: 'Jon',\n\tlastName: 'Doe',\n}\n\nconst buildUsers = async () => {\n\tconst table = 'User'\n\n\tconst tableRequest = {\n\t\tmethod: 'POST',\n\t\turl: `${ENDPOINT}/meta/bases/${baseId}/tables`,\n\t\tdata: {\n\t\t\tname: table,\n\t\t\tfields: [\n\t\t\t\t{ name: 'userId', type: 'number', options: { precision: 0 } },\n\t\t\t\t{ name: 'email', type: 'email' },\n\t\t\t\t{ name: 'password', type: 'singleLineText' },\n\t\t\t\t{\n\t\t\t\t\tname: 'role',\n\t\t\t\t\ttype: 'singleSelect',\n\t\t\t\t\toptions: {\n\t\t\t\t\t\tchoices: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tname: 'ADMIN',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tname: 'USER',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{ name: 'firstName', type: 'singleLineText' },\n\t\t\t\t{ name: 'lastName', type: 'singleLineText' },\n\t\t\t],\n\t\t},\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t},\n\t}\n\n\tconst recordsRequest = {\n\t\tmethod: 'POST',\n\t\turl: `${ENDPOINT}/${baseId}/${table}`,\n\t\tdata: {\n\t\t\trecords: [\n\t\t\t\t{\n\t\t\t\t\tfields: user,\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t},\n\t}\n\n\treturn await build(table, tableRequest, recordsRequest)\n}\n\nconst buildUserApiKey = async userTable => {\n\tconst table = 'UserApiKey'\n\n\tconst tableRequest = {\n\t\tmethod: 'POST',\n\t\turl: `${ENDPOINT}/meta/bases/${baseId}/tables`,\n\t\tdata: {\n\t\t\tname: table,\n\t\t\tfields: [\n\t\t\t\t{ name: 'id', type: 'number', options: { precision: 0 } },\n\t\t\t\t{ name: 'userId', type: 'multipleRecordLinks', options: { linkedTableId: userTable.id } },\n\t\t\t\t{ name: 'apiKey', type: 'singleLineText' },\n\t\t\t],\n\t\t},\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t},\n\t}\n\n\tconst recordsRequest = {\n\t\tmethod: 'POST',\n\t\turl: `${ENDPOINT}/${baseId}/${table}`,\n\t\tdata: {\n\t\t\trecords: [\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\tid: 1,\n\t\t\t\t\t\tuserId: [userTable.records[0].id],\n\t\t\t\t\t\tapiKey: 'Ex@mp1eS$Cu7eAp!K3y',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t},\n\t}\n\n\treturn await build(table, tableRequest, recordsRequest)\n}\n\nconst buildCustomers = async () => {\n\tconst table = 'Customer'\n\n\tconst fields = Object.keys(Customers[0])\n\t\t.map(field => {\n\t\t\treturn field !== 'custId' ? { name: field, type: 'singleLineText' } : null\n\t\t})\n\t\t.filter(field => field !== null)\n\n\tconst tableRequest = {\n\t\tmethod: 'POST',\n\t\turl: `${ENDPOINT}/meta/bases/${baseId}/tables`,\n\t\tdata: {\n\t\t\tname: table,\n\t\t\tfields: [\n\t\t\t\t{ name: 'custId', type: 'number', options: { precision: 0 } },\n\t\t\t\t{ name: 'userId', type: 'number', options: { precision: 0 } },\n\t\t\t\t...fields,\n\t\t\t],\n\t\t},\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t},\n\t}\n\n\tconst recordsRequest = {\n\t\tmethod: 'POST',\n\t\turl: `${ENDPOINT}/${baseId}/${table}`,\n\t\tdata: {\n\t\t\trecords: Customers.map(customer => {\n\t\t\t\treturn { fields: customer }\n\t\t\t}),\n\t\t},\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t},\n\t}\n\n\treturn await build(table, tableRequest, recordsRequest)\n}\n\nconst buildEmployees = async () => {\n\tconst table = 'Employee'\n\tconst fields = Object.keys(Employees[0])\n\t\t.map(field => {\n\t\t\treturn field !== 'employeeId' ? { name: field, type: 'singleLineText' } : null\n\t\t})\n\t\t.filter(field => field !== null)\n\n\tconst tableRequest = {\n\t\tmethod: 'POST',\n\t\turl: `${ENDPOINT}/meta/bases/${baseId}/tables`,\n\t\tdata: {\n\t\t\tname: table,\n\t\t\tfields: [{ name: 'employeeId', type: 'number', options: { precision: 0 } }, ...fields],\n\t\t},\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t},\n\t}\n\n\tconst recordsRequest = {\n\t\tmethod: 'POST',\n\t\turl: `${ENDPOINT}/${baseId}/${table}`,\n\t\tdata: {\n\t\t\trecords: Employees.map(employee => {\n\t\t\t\treturn { fields: employee }\n\t\t\t}),\n\t\t},\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t},\n\t}\n\n\treturn await build(table, tableRequest, recordsRequest)\n}\n\nconst buildShippers = async () => {\n\tconst table = 'Shipper'\n\tconst fields = Object.keys(Shippers[0])\n\t\t.map(field => {\n\t\t\treturn field !== 'shipperId' ? { name: field, type: 'singleLineText' } : null\n\t\t})\n\t\t.filter(field => field !== null)\n\n\tconst tableRequest = {\n\t\tmethod: 'POST',\n\t\turl: `${ENDPOINT}/meta/bases/${baseId}/tables`,\n\t\tdata: {\n\t\t\tname: table,\n\t\t\tfields: [{ name: 'shipperId', type: 'number', options: { precision: 0 } }, ...fields],\n\t\t},\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t},\n\t}\n\n\tconst recordsRequest = {\n\t\tmethod: 'POST',\n\t\turl: `${ENDPOINT}/${baseId}/${table}`,\n\t\tdata: {\n\t\t\trecords: Shippers.map(shipper => {\n\t\t\t\treturn { fields: shipper }\n\t\t\t}),\n\t\t},\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t},\n\t}\n\n\treturn await build(table, tableRequest, recordsRequest)\n}\n\nconst buildSalesOrders = async (shipperTable, customerTable, employeeTable) => {\n\tconst table = 'SalesOrder'\n\n\tlet timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone ?? 'client'\n\tif (timeZone === 'UTC') {\n\t\ttimeZone = 'utc'\n\t}\n\n\tconst tableRequest = {\n\t\tmethod: 'POST',\n\t\turl: `${ENDPOINT}/meta/bases/${baseId}/tables`,\n\t\tdata: {\n\t\t\tname: table,\n\t\t\tfields: [\n\t\t\t\t{ name: 'orderId', type: 'number', options: { precision: 0 } },\n\t\t\t\t{ name: 'freight', type: 'number', options: { precision: 2 } },\n\t\t\t\t{ name: 'shipCity', type: 'singleLineText' },\n\t\t\t\t{ name: 'shipName', type: 'singleLineText' },\n\t\t\t\t{\n\t\t\t\t\tname: 'orderDate',\n\t\t\t\t\ttype: 'dateTime',\n\t\t\t\t\toptions: {\n\t\t\t\t\t\ttimeZone,\n\t\t\t\t\t\tdateFormat: {\n\t\t\t\t\t\t\tformat: 'YYYY-MM-DD',\n\t\t\t\t\t\t\tname: 'iso',\n\t\t\t\t\t\t},\n\t\t\t\t\t\ttimeFormat: {\n\t\t\t\t\t\t\tformat: 'HH:mm',\n\t\t\t\t\t\t\tname: '24hour',\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{ name: 'shipperId', type: 'multipleRecordLinks', options: { linkedTableId: shipperTable.id } },\n\t\t\t\t{ name: 'custId', type: 'multipleRecordLinks', options: { linkedTableId: customerTable.id } },\n\t\t\t\t{ name: 'employeeId', type: 'multipleRecordLinks', options: { linkedTableId: employeeTable.id } },\n\t\t\t\t{ name: 'shipRegion', type: 'singleLineText' },\n\t\t\t\t{ name: 'shipAddress', type: 'singleLineText' },\n\t\t\t\t{ name: 'shipCountry', type: 'singleLineText' },\n\t\t\t\t{ name: 'shipPostalCode', type: 'singleLineText' },\n\t\t\t\t{\n\t\t\t\t\tname: 'shippedDate',\n\t\t\t\t\ttype: 'dateTime',\n\t\t\t\t\toptions: {\n\t\t\t\t\t\ttimeZone,\n\t\t\t\t\t\tdateFormat: {\n\t\t\t\t\t\t\tformat: 'YYYY-MM-DD',\n\t\t\t\t\t\t\tname: 'iso',\n\t\t\t\t\t\t},\n\t\t\t\t\t\ttimeFormat: {\n\t\t\t\t\t\t\tformat: 'HH:mm',\n\t\t\t\t\t\t\tname: '24hour',\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tname: 'requiredDate',\n\t\t\t\t\ttype: 'dateTime',\n\t\t\t\t\toptions: {\n\t\t\t\t\t\ttimeZone,\n\t\t\t\t\t\tdateFormat: {\n\t\t\t\t\t\t\tformat: 'YYYY-MM-DD',\n\t\t\t\t\t\t\tname: 'iso',\n\t\t\t\t\t\t},\n\t\t\t\t\t\ttimeFormat: {\n\t\t\t\t\t\t\tformat: 'HH:mm',\n\t\t\t\t\t\t\tname: '24hour',\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t},\n\t}\n\n\tconst recordsRequest = {\n\t\tmethod: 'POST',\n\t\turl: `${ENDPOINT}/${baseId}/${table}`,\n\t\tdata: {\n\t\t\trecords: [\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 1,\n\t\t\t\t\t\tfreight: 32.38,\n\t\t\t\t\t\tshipCity: 'Reims',\n\t\t\t\t\t\tshipName: 'Ship to 85-B',\n\t\t\t\t\t\torderDate: '2006-07-04 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[0].id],\n\t\t\t\t\t\tcustId: [customerTable.records[0].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[0].id],\n\t\t\t\t\t\tshipRegion: null,\n\t\t\t\t\t\tshipAddress: \"6789 rue de l'Abbaye\",\n\t\t\t\t\t\tshipCountry: 'France',\n\t\t\t\t\t\tshippedDate: '2006-07-16 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-08-01 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10345',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 2,\n\t\t\t\t\t\tfreight: 11.61,\n\t\t\t\t\t\tshipCity: 'Münster',\n\t\t\t\t\t\tshipName: 'Ship to 79-C',\n\t\t\t\t\t\torderDate: '2006-07-05 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[0].id],\n\t\t\t\t\t\tcustId: [customerTable.records[1].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[0].id],\n\t\t\t\t\t\tshipRegion: null,\n\t\t\t\t\t\tshipAddress: 'Luisenstr. 9012',\n\t\t\t\t\t\tshipCountry: 'Germany',\n\t\t\t\t\t\tshippedDate: '2006-07-10 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-08-16 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10328',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 3,\n\t\t\t\t\t\tfreight: 65.83,\n\t\t\t\t\t\tshipCity: 'Rio de Janeiro',\n\t\t\t\t\t\tshipName: 'Destination SCQXA',\n\t\t\t\t\t\torderDate: '2006-07-08 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[0].id],\n\t\t\t\t\t\tcustId: [customerTable.records[2].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[0].id],\n\t\t\t\t\t\tshipRegion: 'RJ',\n\t\t\t\t\t\tshipAddress: 'Rua do Paço, 7890',\n\t\t\t\t\t\tshipCountry: 'Brazil',\n\t\t\t\t\t\tshippedDate: '2006-07-12 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-08-05 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10195',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 4,\n\t\t\t\t\t\tfreight: 41.34,\n\t\t\t\t\t\tshipCity: 'Lyon',\n\t\t\t\t\t\tshipName: 'Ship to 84-A',\n\t\t\t\t\t\torderDate: '2006-07-08 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[0].id],\n\t\t\t\t\t\tcustId: [customerTable.records[3].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[0].id],\n\t\t\t\t\t\tshipRegion: null,\n\t\t\t\t\t\tshipAddress: '3456, rue du Commerce',\n\t\t\t\t\t\tshipCountry: 'France',\n\t\t\t\t\t\tshippedDate: '2006-07-15 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-08-05 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10342',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 5,\n\t\t\t\t\t\tfreight: 51.3,\n\t\t\t\t\t\tshipCity: 'Charleroi',\n\t\t\t\t\t\tshipName: 'Ship to 76-B',\n\t\t\t\t\t\torderDate: '2006-07-09 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[1].id],\n\t\t\t\t\t\tcustId: [customerTable.records[4].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[1].id],\n\t\t\t\t\t\tshipRegion: null,\n\t\t\t\t\t\tshipAddress: 'Boulevard Tirou, 9012',\n\t\t\t\t\t\tshipCountry: 'Belgium',\n\t\t\t\t\t\tshippedDate: '2006-07-11 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-08-06 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10318',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 6,\n\t\t\t\t\t\tfreight: 58.17,\n\t\t\t\t\t\tshipCity: 'Rio de Janeiro',\n\t\t\t\t\t\tshipName: 'Destination JPAIY',\n\t\t\t\t\t\torderDate: '2006-07-10 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[1].id],\n\t\t\t\t\t\tcustId: [customerTable.records[5].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[1].id],\n\t\t\t\t\t\tshipRegion: 'RJ',\n\t\t\t\t\t\tshipAddress: 'Rua do Paço, 8901',\n\t\t\t\t\t\tshipCountry: 'Brazil',\n\t\t\t\t\t\tshippedDate: '2006-07-16 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-07-24 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10196',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 7,\n\t\t\t\t\t\tfreight: 22.98,\n\t\t\t\t\t\tshipCity: 'Bern',\n\t\t\t\t\t\tshipName: 'Destination YUJRD',\n\t\t\t\t\t\torderDate: '2006-07-11 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[1].id],\n\t\t\t\t\t\tcustId: [customerTable.records[4].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[1].id],\n\t\t\t\t\t\tshipRegion: null,\n\t\t\t\t\t\tshipAddress: 'Hauptstr. 1234',\n\t\t\t\t\t\tshipCountry: 'Switzerland',\n\t\t\t\t\t\tshippedDate: '2006-07-23 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-08-08 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10139',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 8,\n\t\t\t\t\t\tfreight: 148.33,\n\t\t\t\t\t\tshipCity: 'Genève',\n\t\t\t\t\t\tshipName: 'Ship to 68-A',\n\t\t\t\t\t\torderDate: '2006-07-12 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[1].id],\n\t\t\t\t\t\tcustId: [customerTable.records[6].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[2].id],\n\t\t\t\t\t\tshipRegion: null,\n\t\t\t\t\t\tshipAddress: 'Starenweg 6789',\n\t\t\t\t\t\tshipCountry: 'Switzerland',\n\t\t\t\t\t\tshippedDate: '2006-07-15 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-08-09 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10294',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 9,\n\t\t\t\t\t\tfreight: 13.97,\n\t\t\t\t\t\tshipCity: 'Resende',\n\t\t\t\t\t\tshipName: 'Ship to 88-B',\n\t\t\t\t\t\torderDate: '2006-07-15 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[2].id],\n\t\t\t\t\t\tcustId: [customerTable.records[7].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[2].id],\n\t\t\t\t\t\tshipRegion: 'SP',\n\t\t\t\t\t\tshipAddress: 'Rua do Mercado, 5678',\n\t\t\t\t\t\tshipCountry: 'Brazil',\n\t\t\t\t\t\tshippedDate: '2006-07-17 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-08-12 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10354',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 10,\n\t\t\t\t\t\tfreight: 81.91,\n\t\t\t\t\t\tshipCity: 'San Cristóbal',\n\t\t\t\t\t\tshipName: 'Destination JYDLM',\n\t\t\t\t\t\torderDate: '2006-07-16 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[2].id],\n\t\t\t\t\t\tcustId: [customerTable.records[8].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[3].id],\n\t\t\t\t\t\tshipRegion: 'Táchira',\n\t\t\t\t\t\tshipAddress: 'Carrera1234 con Ave. Carlos Soublette #8-35',\n\t\t\t\t\t\tshipCountry: 'Venezuela',\n\t\t\t\t\t\tshippedDate: '2006-07-22 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-08-13 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10199',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 11,\n\t\t\t\t\t\tfreight: 140.51,\n\t\t\t\t\t\tshipCity: 'Graz',\n\t\t\t\t\t\tshipName: 'Destination RVDMF',\n\t\t\t\t\t\torderDate: '2006-07-17 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[2].id],\n\t\t\t\t\t\tcustId: [customerTable.records[9].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[3].id],\n\t\t\t\t\t\tshipRegion: null,\n\t\t\t\t\t\tshipAddress: 'Kirchgasse 9012',\n\t\t\t\t\t\tshipCountry: 'Austria',\n\t\t\t\t\t\tshippedDate: '2006-07-23 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-08-14 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10157',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 12,\n\t\t\t\t\t\tfreight: 3.25,\n\t\t\t\t\t\tshipCity: 'México D.F.',\n\t\t\t\t\t\tshipName: 'Destination LGGCH',\n\t\t\t\t\t\torderDate: '2006-07-18 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[2].id],\n\t\t\t\t\t\tcustId: [customerTable.records[9].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[4].id],\n\t\t\t\t\t\tshipRegion: null,\n\t\t\t\t\t\tshipAddress: 'Sierras de Granada 9012',\n\t\t\t\t\t\tshipCountry: 'Mexico',\n\t\t\t\t\t\tshippedDate: '2006-07-25 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-08-15 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10137',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 13,\n\t\t\t\t\t\tfreight: 55.09,\n\t\t\t\t\t\tshipCity: 'Köln',\n\t\t\t\t\t\tshipName: 'Ship to 56-A',\n\t\t\t\t\t\torderDate: '2006-07-19 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[0].id],\n\t\t\t\t\t\tcustId: [customerTable.records[9].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[4].id],\n\t\t\t\t\t\tshipRegion: null,\n\t\t\t\t\t\tshipAddress: 'Mehrheimerstr. 0123',\n\t\t\t\t\t\tshipCountry: 'Germany',\n\t\t\t\t\t\tshippedDate: '2006-07-29 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-08-16 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10258',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 14,\n\t\t\t\t\t\tfreight: 3.05,\n\t\t\t\t\t\tshipCity: 'Rio de Janeiro',\n\t\t\t\t\t\tshipName: 'Ship to 61-B',\n\t\t\t\t\t\torderDate: '2006-07-19 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[1].id],\n\t\t\t\t\t\tcustId: [customerTable.records[9].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[4].id],\n\t\t\t\t\t\tshipRegion: 'RJ',\n\t\t\t\t\t\tshipAddress: 'Rua da Panificadora, 6789',\n\t\t\t\t\t\tshipCountry: 'Brazil',\n\t\t\t\t\t\tshippedDate: '2006-07-30 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-08-16 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10274',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 15,\n\t\t\t\t\t\tfreight: 48.29,\n\t\t\t\t\t\tshipCity: 'Albuquerque',\n\t\t\t\t\t\tshipName: 'Ship to 65-B',\n\t\t\t\t\t\torderDate: '2006-07-22 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[2].id],\n\t\t\t\t\t\tcustId: [customerTable.records[9].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[4].id],\n\t\t\t\t\t\tshipRegion: 'NM',\n\t\t\t\t\t\tshipAddress: '8901 Milton Dr.',\n\t\t\t\t\t\tshipCountry: 'USA',\n\t\t\t\t\t\tshippedDate: '2006-07-25 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-08-19 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10286',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 16,\n\t\t\t\t\t\tfreight: 146.06,\n\t\t\t\t\t\tshipCity: 'Graz',\n\t\t\t\t\t\tshipName: 'Destination FFXKT',\n\t\t\t\t\t\torderDate: '2006-07-23 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[2].id],\n\t\t\t\t\t\tcustId: [customerTable.records[0].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[4].id],\n\t\t\t\t\t\tshipRegion: null,\n\t\t\t\t\t\tshipAddress: 'Kirchgasse 0123',\n\t\t\t\t\t\tshipCountry: 'Austria',\n\t\t\t\t\t\tshippedDate: '2006-07-31 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-08-20 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10158',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 17,\n\t\t\t\t\t\tfreight: 3.67,\n\t\t\t\t\t\tshipCity: 'Bräcke',\n\t\t\t\t\t\tshipName: 'Destination KBSBN',\n\t\t\t\t\t\torderDate: '2006-07-24 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[2].id],\n\t\t\t\t\t\tcustId: [customerTable.records[1].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[4].id],\n\t\t\t\t\t\tshipRegion: null,\n\t\t\t\t\t\tshipAddress: 'Åkergatan 9012',\n\t\t\t\t\t\tshipCountry: 'Sweden',\n\t\t\t\t\t\tshippedDate: '2006-08-23 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-08-21 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10167',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 18,\n\t\t\t\t\t\tfreight: 55.28,\n\t\t\t\t\t\tshipCity: 'Strasbourg',\n\t\t\t\t\t\tshipName: 'Ship to 7-A',\n\t\t\t\t\t\torderDate: '2006-07-25 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[2].id],\n\t\t\t\t\t\tcustId: [customerTable.records[2].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[2].id],\n\t\t\t\t\t\tshipRegion: null,\n\t\t\t\t\t\tshipAddress: '0123, place Kléber',\n\t\t\t\t\t\tshipCountry: 'France',\n\t\t\t\t\t\tshippedDate: '2006-08-12 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-08-22 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10329',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 19,\n\t\t\t\t\t\tfreight: 25.73,\n\t\t\t\t\t\tshipCity: 'Oulu',\n\t\t\t\t\t\tshipName: 'Ship to 87-B',\n\t\t\t\t\t\torderDate: '2006-07-26 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[2].id],\n\t\t\t\t\t\tcustId: [customerTable.records[2].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[2].id],\n\t\t\t\t\t\tshipRegion: null,\n\t\t\t\t\t\tshipAddress: 'Torikatu 2345',\n\t\t\t\t\t\tshipCountry: 'Finland',\n\t\t\t\t\t\tshippedDate: '2006-07-31 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-09-06 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10351',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 20,\n\t\t\t\t\t\tfreight: 208.58,\n\t\t\t\t\t\tshipCity: 'München',\n\t\t\t\t\t\tshipName: 'Destination VAPXU',\n\t\t\t\t\t\torderDate: '2006-07-29 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[2].id],\n\t\t\t\t\t\tcustId: [customerTable.records[2].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[2].id],\n\t\t\t\t\t\tshipRegion: null,\n\t\t\t\t\t\tshipAddress: 'Berliner Platz 0123',\n\t\t\t\t\t\tshipCountry: 'Germany',\n\t\t\t\t\t\tshippedDate: '2006-08-06 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-08-26 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10168',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 21,\n\t\t\t\t\t\tfreight: 66.29,\n\t\t\t\t\t\tshipCity: 'Caracas',\n\t\t\t\t\t\tshipName: 'Destination QJVQH',\n\t\t\t\t\t\torderDate: '2006-07-30 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[2].id],\n\t\t\t\t\t\tcustId: [customerTable.records[2].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[2].id],\n\t\t\t\t\t\tshipRegion: 'DF',\n\t\t\t\t\t\tshipAddress: '5ª Ave. Los Palos Grandes 5678',\n\t\t\t\t\t\tshipCountry: 'Venezuela',\n\t\t\t\t\t\tshippedDate: '2006-08-02 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-08-27 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10193',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 22,\n\t\t\t\t\t\tfreight: 4.56,\n\t\t\t\t\t\tshipCity: 'Seattle',\n\t\t\t\t\t\tshipName: 'Ship to 89-B',\n\t\t\t\t\t\torderDate: '2006-07-31 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[2].id],\n\t\t\t\t\t\tcustId: [customerTable.records[3].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[0].id],\n\t\t\t\t\t\tshipRegion: 'WA',\n\t\t\t\t\t\tshipAddress: '8901 - 12th Ave. S.',\n\t\t\t\t\t\tshipCountry: 'USA',\n\t\t\t\t\t\tshippedDate: '2006-08-09 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-08-14 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10357',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 23,\n\t\t\t\t\t\tfreight: 136.54,\n\t\t\t\t\t\tshipCity: 'Oulu',\n\t\t\t\t\t\tshipName: 'Ship to 87-B',\n\t\t\t\t\t\torderDate: '2006-08-01 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[2].id],\n\t\t\t\t\t\tcustId: [customerTable.records[3].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[0].id],\n\t\t\t\t\t\tshipRegion: null,\n\t\t\t\t\t\tshipAddress: 'Torikatu 2345',\n\t\t\t\t\t\tshipCountry: 'Finland',\n\t\t\t\t\t\tshippedDate: '2006-08-02 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-08-29 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10351',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 24,\n\t\t\t\t\t\tfreight: 4.54,\n\t\t\t\t\t\tshipCity: 'Lander',\n\t\t\t\t\t\tshipName: 'Ship to 75-C',\n\t\t\t\t\t\torderDate: '2006-08-01 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[2].id],\n\t\t\t\t\t\tcustId: [customerTable.records[3].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[0].id],\n\t\t\t\t\t\tshipRegion: 'WY',\n\t\t\t\t\t\tshipAddress: 'P.O. Box 7890',\n\t\t\t\t\t\tshipCountry: 'USA',\n\t\t\t\t\t\tshippedDate: '2006-08-30 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-08-29 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10316',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tfields: {\n\t\t\t\t\t\torderId: 25,\n\t\t\t\t\t\tfreight: 98.03,\n\t\t\t\t\t\tshipCity: 'Albuquerque',\n\t\t\t\t\t\tshipName: 'Ship to 65-A',\n\t\t\t\t\t\torderDate: '2006-08-02 00:00:00.000000',\n\t\t\t\t\t\tshipperId: [shipperTable.records[2].id],\n\t\t\t\t\t\tcustId: [customerTable.records[4].id],\n\t\t\t\t\t\temployeeId: [employeeTable.records[0].id],\n\t\t\t\t\t\tshipRegion: 'NM',\n\t\t\t\t\t\tshipAddress: '7890 Milton Dr.',\n\t\t\t\t\t\tshipCountry: 'USA',\n\t\t\t\t\t\tshippedDate: '2006-08-06 00:00:00.000000',\n\t\t\t\t\t\trequiredDate: '2006-08-30 00:00:00.000000',\n\t\t\t\t\t\tshipPostalCode: '10285',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t},\n\t}\n\n\treturn await build(table, tableRequest, recordsRequest)\n}\n\nconst build = async (table: string, tableRequest: AxiosRequestConfig<any>, recordsRequest: AxiosRequestConfig<any>) => {\n\tlet tableResponse\n\n\ttry {\n\t\ttableResponse = await axios(tableRequest)\n\t\tlogger.log(`${table} table created (#${tableResponse.data?.id})`, DOMAIN)\n\n\t\tlet records: any[] = []\n\n\t\ttry {\n\t\t\tif (recordsRequest.data.records.length > 10) {\n\t\t\t\tconst chunk = 10\n\t\t\t\tfor (let i = 0; i < recordsRequest.data.records.length; i += chunk) {\n\t\t\t\t\tconst recordsResponse = await axios({\n\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\turl: `${ENDPOINT}/${baseId}/${table}`,\n\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\trecords: recordsRequest.data.records.slice(i, i + chunk),\n\t\t\t\t\t\t},\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t\trecords = records.concat(recordsResponse.data?.records)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst recordsResponse = await axios(recordsRequest)\n\t\t\t\trecords = recordsResponse.data?.records\n\t\t\t}\n\n\t\t\tif (records.length) {\n\t\t\t\tfor (const record of records) {\n\t\t\t\t\tlogger.log(`${table} #${record.id} created`, DOMAIN)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlogger.log(`Seeded ${records.length} records`, DOMAIN)\n\n\t\t\treturn {\n\t\t\t\tid: tableResponse.data.id,\n\t\t\t\trecords,\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tlogger.error(`Error creating ${table} records`, DOMAIN)\n\t\t\tthrow new Error(`Error creating ${table} records`)\n\t\t}\n\t} catch (error) {\n\t\tif (error.response.data.error.type === 'DUPLICATE_TABLE_NAME') {\n\t\t\tlogger.warn(`${table} table already exists`, DOMAIN)\n\n\t\t\tconst tablesResponse = await axios({\n\t\t\t\tmethod: 'GET',\n\t\t\t\turl: `${ENDPOINT}/meta/bases/${baseId}/tables`,\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tconst filteredTable = tablesResponse.data.tables.find((t: any) => t.name === table)\n\n\t\t\tconst recordsResponse = await axios({\n\t\t\t\tmethod: 'GET',\n\t\t\t\turl: `${ENDPOINT}/${baseId}/${table}`,\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t\t\t},\n\t\t\t})\n\n\t\t\treturn {\n\t\t\t\tid: filteredTable.id,\n\t\t\t\t...recordsResponse.data,\n\t\t\t}\n\t\t} else {\n\t\t\tlogger.error(`Error creating ${table} table`, DOMAIN)\n\t\t\tthrow new Error(`Error creating ${table} table`)\n\t\t}\n\t}\n}\n\nconst seed = async () => {\n\tlogger.log('Seeding Airtable database', DOMAIN)\n\n\tconst userTable = await buildUsers()\n\tconst userApiKeyTable = await buildUserApiKey(userTable)\n\tconst customerTable = await buildCustomers()\n\tconst employeeTable = await buildEmployees()\n\tconst shipperTable = await buildShippers()\n\tawait buildSalesOrders(shipperTable, customerTable, employeeTable)\n}\n\nseed()\n"
  },
  {
    "path": "demo/databases/json/Customer.json",
    "content": "[{\n    \"userId\": 1,\n    \"custId\": 1,\n    \"fax\": \"030-0123456\",\n    \"city\": \"Berlin\",\n    \"email\": null,\n    \"phone\": \"030-3456789\",\n    \"mobile\": null,\n    \"region\": null,\n    \"address\": \"Obere Str. 0123\",\n    \"country\": \"Germany\",\n    \"postalCode\": \"10092\",\n    \"companyName\": \"Customer NRZBB\",\n    \"contactName\": \"Allen, Michael\",\n    \"contactTitle\": \"Sales Representative\"\n}, {\n    \"userId\": 1,\n    \"custId\": 2,\n    \"fax\": \"(5) 456-7890\",\n    \"city\": \"México D.F.\",\n    \"email\": null,\n    \"phone\": \"(5) 789-0123\",\n    \"mobile\": null,\n    \"region\": null,\n    \"address\": \"Avda. de la Constitución 5678\",\n    \"country\": \"Mexico\",\n    \"postalCode\": \"10077\",\n    \"companyName\": \"Customer MLTDN\",\n    \"contactName\": \"Hassall, Mark\",\n    \"contactTitle\": \"Owner\"\n}, {\n    \"userId\": 1,\n    \"custId\": 3,\n    \"fax\": null,\n    \"city\": \"México D.F.\",\n    \"email\": null,\n    \"phone\": \"(5) 123-4567\",\n    \"mobile\": null,\n    \"region\": null,\n    \"address\": \"Mataderos  7890\",\n    \"country\": \"Mexico\",\n    \"postalCode\": \"10097\",\n    \"companyName\": \"Customer KBUDE\",\n    \"contactName\": \"Peoples, John\",\n    \"contactTitle\": \"Owner\"\n}, {\n    \"userId\": 1,\n    \"custId\": 4,\n    \"fax\": \"(171) 456-7891\",\n    \"city\": \"London\",\n    \"email\": null,\n    \"phone\": \"(171) 456-7890\",\n    \"mobile\": null,\n    \"region\": null,\n    \"address\": \"7890 Hanover Sq.\",\n    \"country\": \"UK\",\n    \"postalCode\": \"10046\",\n    \"companyName\": \"Customer HFBZG\",\n    \"contactName\": \"Arndt, Torsten\",\n    \"contactTitle\": \"Sales Representative\"\n}, {\n    \"userId\": 1,\n    \"custId\": 5,\n    \"fax\": \"0921-23 45 67\",\n    \"city\": \"Luleå\",\n    \"email\": null,\n    \"phone\": \"0921-67 89 01\",\n    \"mobile\": null,\n    \"region\": null,\n    \"address\": \"Berguvsvägen  5678\",\n    \"country\": \"Sweden\",\n    \"postalCode\": \"10112\",\n    \"companyName\": \"Customer HGVLZ\",\n    \"contactName\": \"Higginbotham, Tom\",\n    \"contactTitle\": \"Order Administrator\"\n}, {\n    \"userId\": 1,\n    \"custId\": 6,\n    \"fax\": \"0621-12345\",\n    \"city\": \"Mannheim\",\n    \"email\": null,\n    \"phone\": \"0621-67890\",\n    \"mobile\": null,\n    \"region\": null,\n    \"address\": \"Forsterstr. 7890\",\n    \"country\": \"Germany\",\n    \"postalCode\": \"10117\",\n    \"companyName\": \"Customer XHXJV\",\n    \"contactName\": \"Poland, Carole\",\n    \"contactTitle\": \"Sales Representative\"\n}, {\n    \"userId\": 1,\n    \"custId\": 7,\n    \"fax\": \"67.89.01.24\",\n    \"city\": \"Strasbourg\",\n    \"email\": null,\n    \"phone\": \"67.89.01.23\",\n    \"mobile\": null,\n    \"region\": null,\n    \"address\": \"2345, place Kléber\",\n    \"country\": \"France\",\n    \"postalCode\": \"10089\",\n    \"companyName\": \"Customer QXVLA\",\n    \"contactName\": \"Bansal, Dushyant\",\n    \"contactTitle\": \"Marketing Manager\"\n}, {\n    \"userId\": 1,\n    \"custId\": 8,\n    \"fax\": \"(91) 012 34 56\",\n    \"city\": \"Madrid\",\n    \"email\": null,\n    \"phone\": \"(91) 345 67 89\",\n    \"mobile\": null,\n    \"region\": null,\n    \"address\": \"C/ Araquil, 0123\",\n    \"country\": \"Spain\",\n    \"postalCode\": \"10104\",\n    \"companyName\": \"Customer QUHWH\",\n    \"contactName\": \"Ilyina, Julia\",\n    \"contactTitle\": \"Owner\"\n}, {\n    \"userId\": 1,\n    \"custId\": 9,\n    \"fax\": \"23.45.67.80\",\n    \"city\": \"Marseille\",\n    \"email\": null,\n    \"phone\": \"23.45.67.89\",\n    \"mobile\": null,\n    \"region\": null,\n    \"address\": \"6789, rue des Bouchers\",\n    \"country\": \"France\",\n    \"postalCode\": \"10105\",\n    \"companyName\": \"Customer RTXGC\",\n    \"contactName\": \"Raghav, Amritansh\",\n    \"contactTitle\": \"Owner\"\n}, {\n    \"userId\": 1,\n    \"custId\": 10,\n    \"fax\": \"(604) 678-9012\",\n    \"city\": \"Tsawassen\",\n    \"email\": null,\n    \"phone\": \"(604) 901-2345\",\n    \"mobile\": null,\n    \"region\": \"BC\",\n    \"address\": \"8901 Tsawassen Blvd.\",\n    \"country\": \"Canada\",\n    \"postalCode\": \"10111\",\n    \"companyName\": \"Customer EEALV\",\n    \"contactName\": \"Bassols, Pilar Colome\",\n    \"contactTitle\": \"Accounting Manager\"\n}]"
  },
  {
    "path": "demo/databases/json/Employee.json",
    "content": "[{\n    \"employeeId\": 1,\n    \"city\": \"Seattle\",\n    \"email\": null,\n    \"notes\": null,\n    \"phone\": \"(206) 555-0101\",\n    \"photo\": null,\n    \"title\": \"CEO\",\n    \"mobile\": null,\n    \"region\": \"WA\",\n    \"address\": \"7890 - 20th Ave. E., Apt. 2A\",\n    \"country\": \"USA\",\n    \"hireDate\": \"2002-05-01 00:00:00.000000\",\n    \"lastName\": \"Davis\",\n    \"birthDate\": \"1958-12-08 00:00:00.000000\",\n    \"extension\": null,\n    \"firstName\": \"Sara\",\n    \"photoPath\": null,\n    \"postalCode\": \"10003\",\n    \"titleOfCourtesy\": \"Ms.\"\n}, {\n    \"employeeId\": 2,\n    \"city\": \"Tacoma\",\n    \"email\": null,\n    \"notes\": null,\n    \"phone\": \"(206) 555-0100\",\n    \"photo\": null,\n    \"title\": \"Vice President, Sales\",\n    \"mobile\": null,\n    \"region\": \"WA\",\n    \"address\": \"9012 W. Capital Way\",\n    \"country\": \"USA\",\n    \"hireDate\": \"2002-08-14 00:00:00.000000\",\n    \"lastName\": \"Funk\",\n    \"birthDate\": \"1962-02-19 00:00:00.000000\",\n    \"extension\": null,\n    \"firstName\": \"Don\",\n    \"photoPath\": null,\n    \"postalCode\": \"10001\",\n    \"titleOfCourtesy\": \"Dr.\"\n}, {\n    \"employeeId\": 3,\n    \"city\": \"Kirkland\",\n    \"email\": null,\n    \"notes\": null,\n    \"phone\": \"(206) 555-0103\",\n    \"photo\": null,\n    \"title\": \"Sales Manager\",\n    \"mobile\": null,\n    \"region\": \"WA\",\n    \"address\": \"2345 Moss Bay Blvd.\",\n    \"country\": \"USA\",\n    \"hireDate\": \"2002-04-01 00:00:00.000000\",\n    \"lastName\": \"Lew\",\n    \"birthDate\": \"1973-08-30 00:00:00.000000\",\n    \"extension\": null,\n    \"firstName\": \"Judy\",\n    \"photoPath\": null,\n    \"postalCode\": \"10007\",\n    \"titleOfCourtesy\": \"Ms.\"\n}, {\n    \"employeeId\": 4,\n    \"city\": \"Redmond\",\n    \"email\": null,\n    \"notes\": null,\n    \"phone\": \"(206) 555-0104\",\n    \"photo\": null,\n    \"title\": \"Sales Representative\",\n    \"mobile\": null,\n    \"region\": \"WA\",\n    \"address\": \"5678 Old Redmond Rd.\",\n    \"country\": \"USA\",\n    \"hireDate\": \"2003-05-03 00:00:00.000000\",\n    \"lastName\": \"Peled\",\n    \"birthDate\": \"1947-09-19 00:00:00.000000\",\n    \"extension\": null,\n    \"firstName\": \"Yael\",\n    \"photoPath\": null,\n    \"postalCode\": \"10009\",\n    \"titleOfCourtesy\": \"Mrs.\"\n}, {\n    \"employeeId\": 5,\n    \"city\": \"London\",\n    \"email\": null,\n    \"notes\": null,\n    \"phone\": \"(71) 234-5678\",\n    \"photo\": null,\n    \"title\": \"Sales Manager\",\n    \"mobile\": null,\n    \"region\": null,\n    \"address\": \"8901 Garrett Hill\",\n    \"country\": \"UK\",\n    \"hireDate\": \"2003-10-17 00:00:00.000000\",\n    \"lastName\": \"Buck\",\n    \"birthDate\": \"1965-03-04 00:00:00.000000\",\n    \"extension\": null,\n    \"firstName\": \"Sven\",\n    \"photoPath\": null,\n    \"postalCode\": \"10004\",\n    \"titleOfCourtesy\": \"Mr.\"\n}]"
  },
  {
    "path": "demo/databases/json/Shipper.json",
    "content": "[{\n    \"shipperId\": 1,\n    \"phone\": \"(503) 555-0137\",\n    \"companyName\": \"Shipper GVSUA\"\n}, {\n    \"shipperId\": 2,\n    \"phone\": \"(425) 555-0136\",\n    \"companyName\": \"Shipper ETYNR\"\n}, {\n    \"shipperId\": 3,\n    \"phone\": \"(415) 555-0138\",\n    \"companyName\": \"Shipper ZHISN\"\n}]"
  },
  {
    "path": "demo/databases/mongodb.js",
    "content": "//seed\ndb = db.getSiblingDB('llana');\n\ndb.User.insert({\n    \"email\": \"test@test.com\", \n    \"password\": \"$2a$10$jm6bM7acpRa18Vdy8FSqIu4yzWAdSgZgRtRrx8zknIeZhSqPJjJU.\",\n    \"role\": \"ADMIN\",\n    \"firstName\": \"Jon\",\n    \"lastName\": \"Doe\",\n    \"createdAt\": \"2000-01-01 00:00:01\",\n    \"updatedAt\": \"2000-01-01 00:00:01\",\n    \"deletedAt\": null,\n});\n\n//get last inserted id\n\nconst user = db.User.findOne({email: \"test@test.com\"});\n\n// Manual Relations Table\ndb.createCollection(\"_llana_relation\")\n\ndb.getCollection(\"_llana_relation\").insertMany([{\n    \"table\": \"Customer\",\n    \"column\": \"_id\",\n    \"org_table\": \"SalesOrder\",\n    \"org_column\": \"custId\"\n},{\n    \"table\": \"Customer\",\n    \"column\": \"userId\",\n    \"org_table\": \"User\",\n    \"org_column\": \"_id\"\n}, {\n    \"table\": \"Employee\",\n    \"column\": \"_id\",\n    \"org_table\": \"SalesOrder\",\n    \"org_column\": \"employeeId\"\n}, {\n    \"table\": \"Shipper\",\n    \"column\": \"_id\",\n    \"org_table\": \"SalesOrder\",\n    \"org_column\": \"shipperId\"\n},{\n    \"table\": \"User\",\n    \"column\": \"_id\",\n    \"org_table\": \"_llana_webhook\",\n    \"org_column\": \"user_identifier\"\n},{\n    \"table\": \"User\",\n    \"column\": \"_id\",\n    \"org_table\": \"UserApiKey\",\n    \"org_column\": \"userId\"\n}]);\n\ndb.createCollection(\"_llana_webhook\")\n\ndb.getCollection(\"_llana_webhook\").insert({\n    \"type\": \"POST\", \n    \"url\": \"https://wh9491c816237e1c710e.free.beeceptor.com\",\n    \"table\": \"Customer\",\n    \"user_identifier\": user._id,\n    \"on_create\": true,\n    \"on_update\":  true,\n    \"on_delete\":  true,\n    \"deletedAt\": null,\n});\n\nconst webhook = db.getCollection(\"_llana_webhook\").findOne({table: \"Customer\"});\n\ndb.createCollection(\"_llana_webhook_log\")\n\ndb.getCollection(\"_llana_webhook_log\").insert({\n    \"webhook_id\": webhook._id, \n    \"type\": \"INSERT\",\n    \"url\": \"https://wh9491c816237e1c710e.free.beeceptor.com\",\n    \"record_key\": \"custId\",\n    \"record_id\": new ObjectId(),\n    \"attempt\": 1,\n    \"delivered\":  true,\n    \"response_status\": 200,\n    \"response_message\": \"Success\",\n    \"created_at\":  new Date(),\n    \"next_attempt_at\":  null,\n    \"delivered_at\": new Date(),\n});\n\n\ndb.UserApiKey.insert({\n    \"userId\": user._id,\n    \"apiKey\": \"Ex@mp1eS$Cu7eAp!K3y\",\n    \"createdAt\": \"2000-01-01 00:00:01\",\n    \"updatedAt\": \"2000-01-01 00:00:01\",\n    \"deletedAt\": null\n});\n\n// Insert customers\n\nconst customers = db.Customer.insertMany([{\n    \"userId\": user._id,\n    \"custId\": 1,\n    \"fax\": \"030-0123456\",\n    \"city\": \"Berlin\",\n    \"email\": null,\n    \"phone\": \"030-3456789\",\n    \"mobile\": null,\n    \"region\": null,\n    \"address\": \"Obere Str. 0123\",\n    \"country\": \"Germany\",\n    \"postalCode\": \"10092\",\n    \"companyName\": \"Customer NRZBB\",\n    \"contactName\": \"Allen, Michael\",\n    \"contactTitle\": \"Sales Representative\"\n}, {\n    \"userId\": user._id,\n    \"custId\": 2,\n    \"fax\": \"(5) 456-7890\",\n    \"city\": \"México D.F.\",\n    \"email\": null,\n    \"phone\": \"(5) 789-0123\",\n    \"mobile\": null,\n    \"region\": null,\n    \"address\": \"Avda. de la Constitución 5678\",\n    \"country\": \"Mexico\",\n    \"postalCode\": \"10077\",\n    \"companyName\": \"Customer MLTDN\",\n    \"contactName\": \"Hassall, Mark\",\n    \"contactTitle\": \"Owner\"\n}, {\n    \"userId\": user._id,\n    \"custId\": 3,\n    \"fax\": null,\n    \"city\": \"México D.F.\",\n    \"email\": null,\n    \"phone\": \"(5) 123-4567\",\n    \"mobile\": null,\n    \"region\": null,\n    \"address\": \"Mataderos  7890\",\n    \"country\": \"Mexico\",\n    \"postalCode\": \"10097\",\n    \"companyName\": \"Customer KBUDE\",\n    \"contactName\": \"Peoples, John\",\n    \"contactTitle\": \"Owner\"\n}, {\n    \"userId\": user._id,\n    \"custId\": 4,\n    \"fax\": \"(171) 456-7891\",\n    \"city\": \"London\",\n    \"email\": null,\n    \"phone\": \"(171) 456-7890\",\n    \"mobile\": null,\n    \"region\": null,\n    \"address\": \"7890 Hanover Sq.\",\n    \"country\": \"UK\",\n    \"postalCode\": \"10046\",\n    \"companyName\": \"Customer HFBZG\",\n    \"contactName\": \"Arndt, Torsten\",\n    \"contactTitle\": \"Sales Representative\"\n}, {\n    \"userId\": user._id,\n    \"custId\": 5,\n    \"fax\": \"0921-23 45 67\",\n    \"city\": \"Luleå\",\n    \"email\": null,\n    \"phone\": \"0921-67 89 01\",\n    \"mobile\": null,\n    \"region\": null,\n    \"address\": \"Berguvsvägen  5678\",\n    \"country\": \"Sweden\",\n    \"postalCode\": \"10112\",\n    \"companyName\": \"Customer HGVLZ\",\n    \"contactName\": \"Higginbotham, Tom\",\n    \"contactTitle\": \"Order Administrator\"\n}, {\n    \"userId\": user._id,\n    \"custId\": 6,\n    \"fax\": \"0621-12345\",\n    \"city\": \"Mannheim\",\n    \"email\": null,\n    \"phone\": \"0621-67890\",\n    \"mobile\": null,\n    \"region\": null,\n    \"address\": \"Forsterstr. 7890\",\n    \"country\": \"Germany\",\n    \"postalCode\": \"10117\",\n    \"companyName\": \"Customer XHXJV\",\n    \"contactName\": \"Poland, Carole\",\n    \"contactTitle\": \"Sales Representative\"\n}, {\n    \"userId\": user._id,\n    \"custId\": 7,\n    \"fax\": \"67.89.01.24\",\n    \"city\": \"Strasbourg\",\n    \"email\": null,\n    \"phone\": \"67.89.01.23\",\n    \"mobile\": null,\n    \"region\": null,\n    \"address\": \"2345, place Kléber\",\n    \"country\": \"France\",\n    \"postalCode\": \"10089\",\n    \"companyName\": \"Customer QXVLA\",\n    \"contactName\": \"Bansal, Dushyant\",\n    \"contactTitle\": \"Marketing Manager\"\n}, {\n    \"userId\": user._id,\n    \"custId\": 8,\n    \"fax\": \"(91) 012 34 56\",\n    \"city\": \"Madrid\",\n    \"email\": null,\n    \"phone\": \"(91) 345 67 89\",\n    \"mobile\": null,\n    \"region\": null,\n    \"address\": \"C/ Araquil, 0123\",\n    \"country\": \"Spain\",\n    \"postalCode\": \"10104\",\n    \"companyName\": \"Customer QUHWH\",\n    \"contactName\": \"Ilyina, Julia\",\n    \"contactTitle\": \"Owner\"\n}, {\n    \"userId\": user._id,\n    \"custId\": 9,\n    \"fax\": \"23.45.67.80\",\n    \"city\": \"Marseille\",\n    \"email\": null,\n    \"phone\": \"23.45.67.89\",\n    \"mobile\": null,\n    \"region\": null,\n    \"address\": \"6789, rue des Bouchers\",\n    \"country\": \"France\",\n    \"postalCode\": \"10105\",\n    \"companyName\": \"Customer RTXGC\",\n    \"contactName\": \"Raghav, Amritansh\",\n    \"contactTitle\": \"Owner\"\n}, {\n    \"userId\": user._id,\n    \"custId\": 10,\n    \"fax\": \"(604) 678-9012\",\n    \"city\": \"Tsawassen\",\n    \"email\": null,\n    \"phone\": \"(604) 901-2345\",\n    \"mobile\": null,\n    \"region\": \"BC\",\n    \"address\": \"8901 Tsawassen Blvd.\",\n    \"country\": \"Canada\",\n    \"postalCode\": \"10111\",\n    \"companyName\": \"Customer EEALV\",\n    \"contactName\": \"Bassols, Pilar Colome\",\n    \"contactTitle\": \"Accounting Manager\"\n}]);\n\nconst customer1 = db.Customer.findOne({companyName: \"Customer NRZBB\"});\nconst customer2 = db.Customer.findOne({companyName: \"Customer MLTDN\"});\nconst customer3 = db.Customer.findOne({companyName: \"Customer KBUDE\"});\nconst customer4 = db.Customer.findOne({companyName: \"Customer HFBZG\"});\nconst customer5 = db.Customer.findOne({companyName: \"Customer HGVLZ\"});\nconst customer6 = db.Customer.findOne({companyName: \"Customer XHXJV\"});\nconst customer7 = db.Customer.findOne({companyName: \"Customer QXVLA\"});\nconst customer8 = db.Customer.findOne({companyName: \"Customer QUHWH\"});\nconst customer9 = db.Customer.findOne({companyName: \"Customer RTXGC\"});\nconst customer10 = db.Customer.findOne({companyName: \"Customer EEALV\"});\n\n// Employees\n\nconst employees = db.Employee.insertMany([{\n    \"employeeId\": 1,\n    \"city\": \"Seattle\",\n    \"email\": null,\n    \"notes\": null,\n    \"phone\": \"(206) 555-0101\",\n    \"photo\": null,\n    \"title\": \"CEO\",\n    \"mobile\": null,\n    \"region\": \"WA\",\n    \"address\": \"7890 - 20th Ave. E., Apt. 2A\",\n    \"country\": \"USA\",\n    \"hireDate\": \"2000-01-01 00:00:01\",\n    \"lastname\": \"Davis\",\n    \"birthDate\": \"2000-01-01 00:00:01\",\n    \"extension\": null,\n    \"firstname\": \"Sara\",\n    \"photoPath\": null,\n    \"postalCode\": \"10003\",\n    \"titleOfCourtesy\": \"Ms.\"\n}, {\n    \"employeeId\": 2,\n    \"city\": \"Tacoma\",\n    \"email\": null,\n    \"notes\": null,\n    \"phone\": \"(206) 555-0100\",\n    \"photo\": null,\n    \"title\": \"Vice President, Sales\",\n    \"mobile\": null,\n    \"region\": \"WA\",\n    \"address\": \"9012 W. Capital Way\",\n    \"country\": \"USA\",\n    \"hireDate\": \"2000-01-01 00:00:01\",\n    \"lastname\": \"Funk\",\n    \"birthDate\": \"2000-01-01 00:00:01\",\n    \"extension\": null,\n    \"firstname\": \"Don\",\n    \"photoPath\": null,\n    \"postalCode\": \"10001\",\n    \"titleOfCourtesy\": \"Dr.\"\n}, {\n    \"employeeId\": 3,\n    \"city\": \"Kirkland\",\n    \"email\": null,\n    \"notes\": null,\n    \"phone\": \"(206) 555-0103\",\n    \"photo\": null,\n    \"title\": \"Sales Manager\",\n    \"mobile\": null,\n    \"region\": \"WA\",\n    \"address\": \"2345 Moss Bay Blvd.\",\n    \"country\": \"USA\",\n    \"hireDate\": \"2000-01-01 00:00:01\",\n    \"lastname\": \"Lew\",\n    \"birthDate\": \"2000-01-01 00:00:01\",\n    \"extension\": null,\n    \"firstname\": \"Judy\",\n    \"photoPath\": null,\n    \"postalCode\": \"10007\",\n    \"titleOfCourtesy\": \"Ms.\"\n}, {\n    \"employeeId\": 4,\n    \"city\": \"Redmond\",\n    \"email\": null,\n    \"notes\": null,\n    \"phone\": \"(206) 555-0104\",\n    \"photo\": null,\n    \"title\": \"Sales Representative\",\n    \"mobile\": null,\n    \"region\": \"WA\",\n    \"address\": \"5678 Old Redmond Rd.\",\n    \"country\": \"USA\",\n    \"hireDate\": \"2000-01-01 00:00:01\",\n    \"lastname\": \"Peled\",\n    \"birthDate\": \"2000-01-01 00:00:01\",\n    \"extension\": null,\n    \"firstname\": \"Yael\",\n    \"photoPath\": null,\n    \"postalCode\": \"10009\",\n    \"titleOfCourtesy\": \"Mrs.\"\n}, {\n    \"employeeId\": 5,\n    \"city\": \"London\",\n    \"email\": null,\n    \"notes\": null,\n    \"phone\": \"(71) 234-5678\",\n    \"photo\": null,\n    \"title\": \"Sales Manager\",\n    \"mobile\": null,\n    \"region\": null,\n    \"address\": \"8901 Garrett Hill\",\n    \"country\": \"UK\",\n    \"hireDate\": \"2000-01-01 00:00:01\",\n    \"lastname\": \"Buck\",\n    \"birthDate\": \"2000-01-01 00:00:01\",\n    \"extension\": null,\n    \"firstname\": \"Sven\",\n    \"photoPath\": null,\n    \"postalCode\": \"10004\",\n    \"titleOfCourtesy\": \"Mr.\"\n}]);\n\nconst employee1 = db.Employee.findOne({firstname: \"Sara\"});\nconst employee2 = db.Employee.findOne({firstname: \"Don\"});\nconst employee3 = db.Employee.findOne({firstname: \"Judy\"});\nconst employee4 = db.Employee.findOne({firstname: \"Yael\"});\nconst employee5 = db.Employee.findOne({firstname: \"Sven\"});\n\n// Shippers\n\nconst shippers = db.Shipper.insertMany([{\n    \"shipperId\": 1,\n    \"phone\": \"(503) 555-0137\",\n    \"companyName\": \"Shipper GVSUA\"\n}, {\n    \"shipperId\": 2,\n    \"phone\": \"(425) 555-0136\",\n    \"companyName\": \"Shipper ETYNR\"\n}, {\n    \"shipperId\": 3,\n    \"phone\": \"(415) 555-0138\",\n    \"companyName\": \"Shipper ZHISN\"\n}]);\n\n// Get Shipper Ids\nconst shipper1 = db.Shipper.findOne({companyName: \"Shipper GVSUA\"});\nconst shipper2 = db.Shipper.findOne({companyName: \"Shipper ETYNR\"});\nconst shipper3 = db.Shipper.findOne({companyName: \"Shipper ZHISN\"});\n\n// Orders\n\nconst orders = db.SalesOrder.insertMany([{\n            \"freight\": 32.38,\n            \"shipCity\": \"Reims\",\n            \"shipName\": \"Ship to 85-B\",\n            \"orderDate\": \"2006-07-04 00:00:00.000000\",\n            \"shipperId\": shipper1._id,\n            \"custId\": customer1._id,\n            \"employeeId\": employee1._id,\n            \"shipRegion\": null,\n            \"shipAddress\": \"6789 rue de l'Abbaye\",\n            \"shipCountry\": \"France\",\n            \"shippedDate\": \"2006-07-16 00:00:00.000000\",\n            \"requiredDate\": \"2006-08-01 00:00:00.000000\",\n            \"shipPostalCode\": \"10345\"\n        }, {\n            \"freight\": 11.61,\n            \"shipCity\": \"Münster\",\n            \"shipName\": \"Ship to 79-C\",\n            \"orderDate\": \"2006-07-05 00:00:00.000000\",\n            \"shipperId\": shipper1._id,\n            \"custId\": customer2._id,\n            \"employeeId\": employee1._id,\n            \"shipRegion\": null,\n            \"shipAddress\": \"Luisenstr. 9012\",\n            \"shipCountry\": \"Germany\",\n            \"shippedDate\": \"2006-07-10 00:00:00.000000\",\n            \"requiredDate\": \"2006-08-16 00:00:00.000000\",\n            \"shipPostalCode\": \"10328\"\n        }, {\n            \"freight\": 65.83,\n            \"shipCity\": \"Rio de Janeiro\",\n            \"shipName\": \"Destination SCQXA\",\n            \"orderDate\": \"2006-07-08 00:00:00.000000\",\n            \"shipperId\": shipper1._id,\n            \"custId\": customer3._id,\n            \"employeeId\": employee1._id,\n            \"shipRegion\": \"RJ\",\n            \"shipAddress\": \"Rua do Paço, 7890\",\n            \"shipCountry\": \"Brazil\",\n            \"shippedDate\": \"2006-07-12 00:00:00.000000\",\n            \"requiredDate\": \"2006-08-05 00:00:00.000000\",\n            \"shipPostalCode\": \"10195\"\n        }, {\n            \"freight\": 41.34,\n            \"shipCity\": \"Lyon\",\n            \"shipName\": \"Ship to 84-A\",\n            \"orderDate\": \"2006-07-08 00:00:00.000000\",\n            \"shipperId\": shipper1._id,\n            \"custId\": customer4._id,\n            \"employeeId\": employee1._id,\n            \"shipRegion\": null,\n            \"shipAddress\": \"3456, rue du Commerce\",\n            \"shipCountry\": \"France\",\n            \"shippedDate\": \"2006-07-15 00:00:00.000000\",\n            \"requiredDate\": \"2006-08-05 00:00:00.000000\",\n            \"shipPostalCode\": \"10342\"\n        }, {\n            \"freight\": 51.30,\n            \"shipCity\": \"Charleroi\",\n            \"shipName\": \"Ship to 76-B\",\n            \"orderDate\": \"2006-07-09 00:00:00.000000\",\n            \"shipperId\": shipper2._id,\n            \"custId\": customer5._id,\n            \"employeeId\": employee2._id,\n            \"shipRegion\": null,\n            \"shipAddress\": \"Boulevard Tirou, 9012\",\n            \"shipCountry\": \"Belgium\",\n            \"shippedDate\": \"2006-07-11 00:00:00.000000\",\n            \"requiredDate\": \"2006-08-06 00:00:00.000000\",\n            \"shipPostalCode\": \"10318\"\n        }, {\n            \"freight\": 58.17,\n            \"shipCity\": \"Rio de Janeiro\",\n            \"shipName\": \"Destination JPAIY\",\n            \"orderDate\": \"2006-07-10 00:00:00.000000\",\n            \"shipperId\": shipper2._id,\n            \"custId\": customer6._id,\n            \"employeeId\": employee2._id,\n            \"shipRegion\": \"RJ\",\n            \"shipAddress\": \"Rua do Paço, 8901\",\n            \"shipCountry\": \"Brazil\",\n            \"shippedDate\": \"2006-07-16 00:00:00.000000\",\n            \"requiredDate\": \"2006-07-24 00:00:00.000000\",\n            \"shipPostalCode\": \"10196\"\n        }, {\n            \"freight\": 22.98,\n            \"shipCity\": \"Bern\",\n            \"shipName\": \"Destination YUJRD\",\n            \"orderDate\": \"2006-07-11 00:00:00.000000\",\n            \"shipperId\": shipper2._id,\n            \"custId\": customer5._id,\n            \"employeeId\": employee2._id,\n            \"shipRegion\": null,\n            \"shipAddress\": \"Hauptstr. 1234\",\n            \"shipCountry\": \"Switzerland\",\n            \"shippedDate\": \"2006-07-23 00:00:00.000000\",\n            \"requiredDate\": \"2006-08-08 00:00:00.000000\",\n            \"shipPostalCode\": \"10139\"\n        }, {\n            \"freight\": 148.33,\n            \"shipCity\": \"Genève\",\n            \"shipName\": \"Ship to 68-A\",\n            \"orderDate\": \"2006-07-12 00:00:00.000000\",\n            \"shipperId\": shipper2._id,\n            \"custId\": customer7._id,\n            \"employeeId\": employee3._id,\n            \"shipRegion\": null,\n            \"shipAddress\": \"Starenweg 6789\",\n            \"shipCountry\": \"Switzerland\",\n            \"shippedDate\": \"2006-07-15 00:00:00.000000\",\n            \"requiredDate\": \"2006-08-09 00:00:00.000000\",\n            \"shipPostalCode\": \"10294\"\n        }, {\n            \"freight\": 13.97,\n            \"shipCity\": \"Resende\",\n            \"shipName\": \"Ship to 88-B\",\n            \"orderDate\": \"2006-07-15 00:00:00.000000\",\n            \"shipperId\": shipper3._id,\n            \"custId\": customer8._id,\n            \"employeeId\": employee3._id,\n            \"shipRegion\": \"SP\",\n            \"shipAddress\": \"Rua do Mercado, 5678\",\n            \"shipCountry\": \"Brazil\",\n            \"shippedDate\": \"2006-07-17 00:00:00.000000\",\n            \"requiredDate\": \"2006-08-12 00:00:00.000000\",\n            \"shipPostalCode\": \"10354\"\n        }, {\n            \"freight\": 81.91,\n            \"shipCity\": \"San Cristóbal\",\n            \"shipName\": \"Destination JYDLM\",\n            \"orderDate\": \"2006-07-16 00:00:00.000000\",\n            \"shipperId\": shipper3._id,\n            \"custId\": customer9._id,\n            \"employeeId\": employee4._id,\n            \"shipRegion\": \"Táchira\",\n            \"shipAddress\": \"Carrera1234 con Ave. Carlos Soublette #8-35\",\n            \"shipCountry\": \"Venezuela\",\n            \"shippedDate\": \"2006-07-22 00:00:00.000000\",\n            \"requiredDate\": \"2006-08-13 00:00:00.000000\",\n            \"shipPostalCode\": \"10199\"\n        }, {\n            \"freight\": 140.51,\n            \"shipCity\": \"Graz\",\n            \"shipName\": \"Destination RVDMF\",\n            \"orderDate\": \"2006-07-17 00:00:00.000000\",\n            \"shipperId\": shipper3._id,\n            \"custId\": customer10._id,\n            \"employeeId\": employee4._id,\n            \"shipRegion\": null,\n            \"shipAddress\": \"Kirchgasse 9012\",\n            \"shipCountry\": \"Austria\",\n            \"shippedDate\": \"2006-07-23 00:00:00.000000\",\n            \"requiredDate\": \"2006-08-14 00:00:00.000000\",\n            \"shipPostalCode\": \"10157\"\n        }, {\n            \"freight\": 3.25,\n            \"shipCity\": \"México D.F.\",\n            \"shipName\": \"Destination LGGCH\",\n            \"orderDate\": \"2006-07-18 00:00:00.000000\",\n            \"shipperId\": shipper3._id,\n            \"custId\": customer10._id,\n            \"employeeId\": employee5._id,\n            \"shipRegion\": null,\n            \"shipAddress\": \"Sierras de Granada 9012\",\n            \"shipCountry\": \"Mexico\",\n            \"shippedDate\": \"2006-07-25 00:00:00.000000\",\n            \"requiredDate\": \"2006-08-15 00:00:00.000000\",\n            \"shipPostalCode\": \"10137\"\n        }, {\n            \"freight\": 55.09,\n            \"shipCity\": \"Köln\",\n            \"shipName\": \"Ship to 56-A\",\n            \"orderDate\": \"2006-07-19 00:00:00.000000\",\n           \"shipperId\": shipper1._id,\n            \"custId\": customer10._id,\n            \"employeeId\": employee5._id,\n            \"shipRegion\": null,\n            \"shipAddress\": \"Mehrheimerstr. 0123\",\n            \"shipCountry\": \"Germany\",\n            \"shippedDate\": \"2006-07-29 00:00:00.000000\",\n            \"requiredDate\": \"2006-08-16 00:00:00.000000\",\n            \"shipPostalCode\": \"10258\"\n        }, {\n            \"freight\": 3.05,\n            \"shipCity\": \"Rio de Janeiro\",\n            \"shipName\": \"Ship to 61-B\",\n            \"orderDate\": \"2006-07-19 00:00:00.000000\",\n            \"shipperId\": shipper2._id,\n            \"custId\": customer10._id,\n            \"employeeId\": employee5._id,\n            \"shipRegion\": \"RJ\",\n            \"shipAddress\": \"Rua da Panificadora, 6789\",\n            \"shipCountry\": \"Brazil\",\n            \"shippedDate\": \"2006-07-30 00:00:00.000000\",\n            \"requiredDate\": \"2006-08-16 00:00:00.000000\",\n            \"shipPostalCode\": \"10274\"\n        }, {\n            \"freight\": 48.29,\n            \"shipCity\": \"Albuquerque\",\n            \"shipName\": \"Ship to 65-B\",\n            \"orderDate\": \"2006-07-22 00:00:00.000000\",\n            \"shipperId\": shipper3._id,\n            \"custId\": customer10._id,\n            \"employeeId\": employee5._id,\n            \"shipRegion\": \"NM\",\n            \"shipAddress\": \"8901 Milton Dr.\",\n            \"shipCountry\": \"USA\",\n            \"shippedDate\": \"2006-07-25 00:00:00.000000\",\n            \"requiredDate\": \"2006-08-19 00:00:00.000000\",\n            \"shipPostalCode\": \"10286\"\n        }, {\n            \"freight\": 146.06,\n            \"shipCity\": \"Graz\",\n            \"shipName\": \"Destination FFXKT\",\n            \"orderDate\": \"2006-07-23 00:00:00.000000\",\n           \"shipperId\": shipper3._id,\n            \"custId\": customer1._id,\n            \"employeeId\": employee5._id,\n            \"shipRegion\": null,\n            \"shipAddress\": \"Kirchgasse 0123\",\n            \"shipCountry\": \"Austria\",\n            \"shippedDate\": \"2006-07-31 00:00:00.000000\",\n            \"requiredDate\": \"2006-08-20 00:00:00.000000\",\n            \"shipPostalCode\": \"10158\"\n        }, {\n            \"freight\": 3.67,\n            \"shipCity\": \"Bräcke\",\n            \"shipName\": \"Destination KBSBN\",\n            \"orderDate\": \"2006-07-24 00:00:00.000000\",\n           \"shipperId\": shipper3._id,\n            \"custId\": customer2._id,\n            \"employeeId\": employee5._id,\n            \"shipRegion\": null,\n            \"shipAddress\": \"Åkergatan 9012\",\n            \"shipCountry\": \"Sweden\",\n            \"shippedDate\": \"2006-08-23 00:00:00.000000\",\n            \"requiredDate\": \"2006-08-21 00:00:00.000000\",\n            \"shipPostalCode\": \"10167\"\n        }, {\n            \"freight\": 55.28,\n            \"shipCity\": \"Strasbourg\",\n            \"shipName\": \"Ship to 7-A\",\n            \"orderDate\": \"2006-07-25 00:00:00.000000\",\n           \"shipperId\": shipper3._id,\n            \"custId\": customer3._id,\n            \"employeeId\": employee3._id,\n            \"shipRegion\": null,\n            \"shipAddress\": \"0123, place Kléber\",\n            \"shipCountry\": \"France\",\n            \"shippedDate\": \"2006-08-12 00:00:00.000000\",\n            \"requiredDate\": \"2006-08-22 00:00:00.000000\",\n            \"shipPostalCode\": \"10329\"\n        }, {\n            \"freight\": 25.73,\n            \"shipCity\": \"Oulu\",\n            \"shipName\": \"Ship to 87-B\",\n            \"orderDate\": \"2006-07-26 00:00:00.000000\",\n           \"shipperId\": shipper3._id,\n            \"custId\": customer3._id,\n            \"employeeId\": employee3._id,\n            \"shipRegion\": null,\n            \"shipAddress\": \"Torikatu 2345\",\n            \"shipCountry\": \"Finland\",\n            \"shippedDate\": \"2006-07-31 00:00:00.000000\",\n            \"requiredDate\": \"2006-09-06 00:00:00.000000\",\n            \"shipPostalCode\": \"10351\"\n        }, {\n            \"freight\": 208.58,\n            \"shipCity\": \"München\",\n            \"shipName\": \"Destination VAPXU\",\n            \"orderDate\": \"2006-07-29 00:00:00.000000\",\n            \"shipperId\": shipper3._id,\n            \"custId\": customer3._id,\n            \"employeeId\": employee3._id,\n            \"shipRegion\": null,\n            \"shipAddress\": \"Berliner Platz 0123\",\n            \"shipCountry\": \"Germany\",\n            \"shippedDate\": \"2006-08-06 00:00:00.000000\",\n            \"requiredDate\": \"2006-08-26 00:00:00.000000\",\n            \"shipPostalCode\": \"10168\"\n        }, {\n            \"freight\": 66.29,\n            \"shipCity\": \"Caracas\",\n            \"shipName\": \"Destination QJVQH\",\n            \"orderDate\": \"2006-07-30 00:00:00.000000\",\n            \"shipperId\": shipper3._id,\n            \"custId\": customer3._id,\n            \"employeeId\": employee3._id,\n            \"shipRegion\": \"DF\",\n            \"shipAddress\": \"5ª Ave. Los Palos Grandes 5678\",\n            \"shipCountry\": \"Venezuela\",\n            \"shippedDate\": \"2006-08-02 00:00:00.000000\",\n            \"requiredDate\": \"2006-08-27 00:00:00.000000\",\n            \"shipPostalCode\": \"10193\"\n        }, {\n            \"freight\": 4.56,\n            \"shipCity\": \"Seattle\",\n            \"shipName\": \"Ship to 89-B\",\n            \"orderDate\": \"2006-07-31 00:00:00.000000\",\n            \"shipperId\": shipper3._id,\n            \"custId\": customer4._id,\n            \"employeeId\": employee1._id,\n            \"shipRegion\": \"WA\",\n            \"shipAddress\": \"8901 - 12th Ave. S.\",\n            \"shipCountry\": \"USA\",\n            \"shippedDate\": \"2006-08-09 00:00:00.000000\",\n            \"requiredDate\": \"2006-08-14 00:00:00.000000\",\n            \"shipPostalCode\": \"10357\"\n        }, {\n            \"freight\": 136.54,\n            \"shipCity\": \"Oulu\",\n            \"shipName\": \"Ship to 87-B\",\n            \"orderDate\": \"2006-08-01 00:00:00.000000\",\n            \"shipperId\": shipper3._id,\n            \"custId\": customer4._id,\n            \"employeeId\": employee1._id,\n            \"shipRegion\": null,\n            \"shipAddress\": \"Torikatu 2345\",\n            \"shipCountry\": \"Finland\",\n            \"shippedDate\": \"2006-08-02 00:00:00.000000\",\n            \"requiredDate\": \"2006-08-29 00:00:00.000000\",\n            \"shipPostalCode\": \"10351\"\n        }, {\n            \"freight\": 4.54,\n            \"shipCity\": \"Lander\",\n            \"shipName\": \"Ship to 75-C\",\n            \"orderDate\": \"2006-08-01 00:00:00.000000\",\n            \"shipperId\": shipper3._id,\n            \"custId\": customer4._id,\n            \"employeeId\": employee1._id,\n            \"shipRegion\": \"WY\",\n            \"shipAddress\": \"P.O. Box 7890\",\n            \"shipCountry\": \"USA\",\n            \"shippedDate\": \"2006-08-30 00:00:00.000000\",\n            \"requiredDate\": \"2006-08-29 00:00:00.000000\",\n            \"shipPostalCode\": \"10316\"\n        }, {\n            \"freight\": 98.03,\n            \"shipCity\": \"Albuquerque\",\n            \"shipName\": \"Ship to 65-A\",\n            \"orderDate\": \"2006-08-02 00:00:00.000000\",\n            \"shipperId\": shipper3._id,\n            \"custId\": customer5._id,\n            \"employeeId\": employee1._id,\n            \"shipRegion\": \"NM\",\n            \"shipAddress\": \"7890 Milton Dr.\",\n            \"shipCountry\": \"USA\",\n            \"shippedDate\": \"2006-08-06 00:00:00.000000\",\n            \"requiredDate\": \"2006-08-30 00:00:00.000000\",\n            \"shipPostalCode\": \"10285\"\n        }]);\n\n\n\nprint(\"Data has been written to the collections\");\n"
  },
  {
    "path": "demo/databases/mssql.sql",
    "content": "CREATE DATABASE llana;\n\nUSE llana;\n\nCREATE TABLE [User] (\n  id int NOT NULL IDENTITY\n  ,email varchar(255) NOT NULL\n  ,password varchar(255) NOT NULL\n  ,role varchar(30) check (role in ('ADMIN','USER')) DEFAULT 'USER'\n  ,firstName varchar(255) DEFAULT NULL\n  ,lastName varchar(255) DEFAULT NULL\n  ,createdAt datetime2(0) DEFAULT GETDATE()\n  ,updatedAt datetime2(0) DEFAULT GETDATE() /* ON UPDATE GETDATE() */\n  ,deletedAt datetime2(0) DEFAULT NULL\n  ,PRIMARY KEY (id)\n  ,CONSTRAINT id UNIQUE (id)\n  ,CONSTRAINT uniqueEmail UNIQUE (email)\n) ;\n\nSET IDENTITY_INSERT [User] ON;\n\nINSERT INTO [User] (id, email, password, role, firstName, lastName, createdAt, updatedAt, deletedAt) VALUES (1, 'test@test.com', '$2a$10$jm6bM7acpRa18Vdy8FSqIu4yzWAdSgZgRtRrx8zknIeZhSqPJjJU.', 'ADMIN', 'Jon', 'Doe', '2000-01-01 00:00:01', '2000-01-01 00:00:00', NULL);\n\nSET IDENTITY_INSERT [User] OFF;\n\n\nCREATE TABLE UserApiKey (\n  id int NOT NULL IDENTITY\n  ,userId int NOT NULL\n  ,apiKey varchar(255) NOT NULL\n  ,createdAt datetime2(0) DEFAULT GETDATE()\n  ,updatedAt datetime2(0) DEFAULT GETDATE() /* ON UPDATE GETDATE() */\n  ,deletedAt datetime2(0) DEFAULT NULL\n  ,PRIMARY KEY (id)\n  ,CONSTRAINT UserApiKeyUserId FOREIGN KEY (userId) REFERENCES [User] (id) ON DELETE CASCADE ON UPDATE NO ACTION\n) ;\n\nCREATE INDEX [user] ON UserApiKey (userId);\n\nSET IDENTITY_INSERT UserApiKey ON;\n\nINSERT INTO UserApiKey (id, userId, apiKey, createdAt, updatedAt, deletedAt) VALUES (1, 1, 'Ex@mp1eS$Cu7eAp!K3y', '2000-01-01 00:00:00', '2000-01-01 00:00:00', NULL);\n\nSET IDENTITY_INSERT UserApiKey OFF;\n\n\nCREATE TABLE Customer (\n  custId INT IDENTITY NOT NULL\n  ,userId int NOT NULL\n  ,companyName VARCHAR(40) NOT NULL\n  ,contactName VARCHAR(60) NULL\n  ,contactTitle VARCHAR(30) NULL\n  ,address VARCHAR(60) NULL\n  ,city VARCHAR(15) NULL\n  ,region VARCHAR(15) NULL\n  ,postalCode VARCHAR(10) NULL\n  ,country VARCHAR(15) NULL\n  ,phone VARCHAR(24) NULL\n  ,mobile VARCHAR(24) NULL\n  ,email VARCHAR(225) NULL\n  ,fax VARCHAR(24) NULL\n  ,createdAt datetime2(0) DEFAULT GETDATE()\n  ,updatedAt datetime2(0) DEFAULT GETDATE() /* ON UPDATE GETDATE() */\n  ,deletedAt datetime2(0) DEFAULT NULL\n  ,PRIMARY KEY (custId)\n  ,CONSTRAINT CustomerUserId FOREIGN KEY (userId) REFERENCES [User] (id) ON DELETE CASCADE ON UPDATE NO ACTION\n  ) ;\n\nCREATE TABLE Employee (\n  employeeId INT IDENTITY NOT NULL\n  ,lastName VARCHAR(20) NOT NULL\n  ,firstName VARCHAR(10) NOT NULL\n  ,title VARCHAR(30) NULL\n  ,titleOfCourtesy VARCHAR(25) NULL\n  ,birthDate DATETIME2(0) NULL\n  ,hireDate DATETIME2(0) NULL\n  ,address VARCHAR(60) NULL\n  ,city VARCHAR(15) NULL\n  ,region VARCHAR(15) NULL\n  ,postalCode VARCHAR(10) NULL\n  ,country VARCHAR(15) NULL\n  ,phone VARCHAR(24) NULL\n  ,extension VARCHAR(4) NULL\n  ,mobile VARCHAR(24) NULL\n  ,email VARCHAR(225) NULL\n  ,photo VARBINARY(max) NULL\n  ,notes VARBINARY(max) NULL\n  ,mgrId INT NULL\n  ,photoPath VARCHAR(255) NULL\n  ,createdAt datetime2(0) DEFAULT GETDATE()\n  ,updatedAt datetime2(0) DEFAULT GETDATE() /* ON UPDATE GETDATE() */\n  ,deletedAt datetime2(0) DEFAULT NULL\n  ,PRIMARY KEY (employeeId)\n  ) ;\n\n\n\nCREATE TABLE Shipper (\n  shipperId INT IDENTITY NOT NULL\n  ,companyName VARCHAR(40) NOT NULL\n  ,phone VARCHAR(44) NULL\n  ,createdAt datetime2(0) DEFAULT GETDATE()\n  ,updatedAt datetime2(0) DEFAULT GETDATE() /* ON UPDATE GETDATE() */\n  ,deletedAt datetime2(0) DEFAULT NULL\n  ,PRIMARY KEY (ShipperId)\n  ) ;\n\n\nCREATE TABLE SalesOrder (\n  orderId INT IDENTITY NOT NULL\n  ,custId INT NOT NULL\n  ,employeeId INT NULL\n  ,orderDate DATETIME2(0) NULL\n  ,requiredDate DATETIME2(0) NULL\n  ,shippedDate DATETIME2(0) NULL\n  ,shipperId INT NOT NULL\n  ,freight DECIMAL(10, 2) NULL\n  ,shipName VARCHAR(40) NULL\n  ,shipAddress VARCHAR(60) NULL\n  ,shipCity VARCHAR(15) NULL\n  ,shipRegion VARCHAR(15) NULL\n  ,shipPostalCode VARCHAR(10) NULL\n  ,shipCountry VARCHAR(15) NULL\n  ,createdAt datetime2(0) DEFAULT GETDATE()\n  ,updatedAt datetime2(0) DEFAULT GETDATE() /* ON UPDATE GETDATE() */\n  ,deletedAt datetime2(0) DEFAULT NULL\n  ,PRIMARY KEY (orderId)\n   , FOREIGN KEY (shipperId)\n      REFERENCES Shipper(shipperId)\n   ,FOREIGN KEY (custId)\n      REFERENCES Customer(custId) \n\n  ) ;\n\n\nSET IDENTITY_INSERT Employee ON;\n\nINSERT  INTO Employee(employeeId, lastName, firstName, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalCode, country, phone, mgrid)\n  VALUES(1, N'Davis', N'Sara', N'CEO', N'Ms.', '1958-12-08 00:00:00.000', '2002-05-01 00:00:00.000', N'7890 - 20th Ave. E., Apt. 2A', N'Seattle', N'WA', N'10003', N'USA', N'(206) 555-0101', NULL);\nINSERT  INTO Employee(employeeId, lastName, firstName, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalCode, country, phone, mgrid)\n  VALUES(2, N'Funk', N'Don', N'Vice President, Sales', N'Dr.', '1962-02-19 00:00:00.000', '2002-08-14 00:00:00.000', N'9012 W. Capital Way', N'Tacoma', N'WA', N'10001', N'USA', N'(206) 555-0100', 1);\nINSERT  INTO Employee(employeeId, lastName, firstName, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalCode, country, phone, mgrid)\n  VALUES(3, N'Lew', N'Judy', N'Sales Manager', N'Ms.', '1973-08-30 00:00:00.000', '2002-04-01 00:00:00.000', N'2345 Moss Bay Blvd.', N'Kirkland', N'WA', N'10007', N'USA', N'(206) 555-0103', 2);\nINSERT  INTO Employee(employeeId, lastName, firstName, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalCode, country, phone, mgrid)\n  VALUES(4, N'Peled', N'Yael', N'Sales Representative', N'Mrs.', '1947-09-19 00:00:00.000', '2003-05-03 00:00:00.000', N'5678 Old Redmond Rd.', N'Redmond', N'WA', N'10009', N'USA', N'(206) 555-0104', 3);\nINSERT  INTO Employee(employeeId, lastName, firstName, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalCode, country, phone, mgrid)\n  VALUES(5, N'Buck', N'Sven', N'Sales Manager', N'Mr.', '1965-03-04 00:00:00.000', '2003-10-17 00:00:00.000', N'8901 Garrett Hill', N'London', NULL, N'10004', N'UK', N'(71) 234-5678', 2);\nINSERT  INTO Employee(employeeId, lastName, firstName, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalCode, country, phone, mgrid)\n  VALUES(6, N'Suurs', N'Paul', N'Sales Representative', N'Mr.', '1973-07-02 00:00:00.000', '2003-10-17 00:00:00.000', N'3456 Coventry House, Miner Rd.', N'London', NULL, N'10005', N'UK', N'(71) 345-6789', 5);\nINSERT  INTO Employee(employeeId, lastName, firstName, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalCode, country, phone, mgrid)\n  VALUES(7, N'King', N'Russell', N'Sales Representative', N'Mr.', '1970-05-29 00:00:00.000', '2004-01-02 00:00:00.000', N'6789 Edgeham Hollow, Winchester Way', N'London', NULL, N'10002', N'UK', N'(71) 123-4567', 5);\nINSERT  INTO Employee(employeeId, lastName, firstName, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalCode, country, phone, mgrid)\n  VALUES(8, N'Cameron', N'Maria', N'Sales Representative', N'Ms.', '1968-01-09 00:00:00.000', '2004-03-05 00:00:00.000', N'4567 - 11th Ave. N.E.', N'Seattle', N'WA', N'10006', N'USA', N'(206) 555-0102', 3);\nINSERT  INTO Employee(employeeId, lastName, firstName, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalCode, country, phone, mgrid)\n  VALUES(9, N'Dolgopyatova', N'Zoya', N'Sales Representative', N'Ms.', '1976-01-27 00:00:00.000', '2004-11-15 00:00:00.000', N'1234 Houndstooth Rd.', N'London', NULL, N'10008', N'UK', N'(71) 456-7890', 5);\n\nSET IDENTITY_INSERT Employee OFF;\n\n\nSET IDENTITY_INSERT Shipper ON;\n\nINSERT  INTO Shipper(shipperId, companyName, phone)\n  VALUES(1, N'Shipper GVSUA', N'(503) 555-0137');\nINSERT  INTO Shipper(shipperId, companyName, phone)\n  VALUES(2, N'Shipper ETYNR', N'(425) 555-0136');\nINSERT  INTO Shipper(shipperId, companyName, phone)\n  VALUES(3, N'Shipper ZHISN', N'(415) 555-0138');\n\nSET IDENTITY_INSERT Shipper OFF;\n\nSET IDENTITY_INSERT Customer ON;\n\n\nINSERT  INTO Customer(custId, userId, companyName, contactName, contactTitle, address, city, region, postalCode, country, phone, fax)\n  VALUES(1, 1, N'Customer NRZBB', N'Allen, Michael', N'Sales Representative', N'Obere Str. 0123', N'Berlin', NULL, N'10092', N'Germany', N'030-3456789', N'030-0123456');\nINSERT  INTO Customer(custId, userId, companyName, contactName, contactTitle, address, city, region, postalCode, country, phone, fax)\n  VALUES(2, 1, N'Customer MLTDN', N'Hassall, Mark', N'Owner', N'Avda. de la Constitución 5678', N'México D.F.', NULL, N'10077', N'Mexico', N'(5) 789-0123', N'(5) 456-7890');\nINSERT  INTO Customer(custId, userId, companyName, contactName, contactTitle, address, city, region, postalCode, country, phone, fax)\n  VALUES(3, 1, N'Customer KBUDE', N'Peoples, John', N'Owner', N'Mataderos  7890', N'México D.F.', NULL, N'10097', N'Mexico', N'(5) 123-4567', NULL);\nINSERT  INTO Customer(custId, userId, companyName, contactName, contactTitle, address, city, region, postalCode, country, phone, fax)\n  VALUES(4, 1, N'Customer HFBZG', N'Arndt, Torsten', N'Sales Representative', N'7890 Hanover Sq.', N'London', NULL, N'10046', N'UK', N'(171) 456-7890', N'(171) 456-7891');\nINSERT  INTO Customer(custId, userId, companyName, contactName, contactTitle, address, city, region, postalCode, country, phone, fax)\n  VALUES(5, 1, N'Customer HGVLZ', N'Higginbotham, Tom', N'Order Administrator', N'Berguvsvägen  5678', N'Luleå', NULL, N'10112', N'Sweden', N'0921-67 89 01', N'0921-23 45 67');\nINSERT  INTO Customer(custId, userId, companyName, contactName, contactTitle, address, city, region, postalCode, country, phone, fax)\n  VALUES(6, 1, N'Customer XHXJV', N'Poland, Carole', N'Sales Representative', N'Forsterstr. 7890', N'Mannheim', NULL, N'10117', N'Germany', N'0621-67890', N'0621-12345');\nINSERT  INTO Customer(custId, userId, companyName, contactName, contactTitle, address, city, region, postalCode, country, phone, fax)\n  VALUES(7, 1, N'Customer QXVLA', N'Bansal, Dushyant', N'Marketing Manager', N'2345, place Kléber', N'Strasbourg', NULL, N'10089', N'France', N'67.89.01.23', N'67.89.01.24');\nINSERT  INTO Customer(custId, userId, companyName, contactName, contactTitle, address, city, region, postalCode, country, phone, fax)\n  VALUES(8, 1, N'Customer QUHWH', N'Ilyina, Julia', N'Owner', N'C/ Araquil, 0123', N'Madrid', NULL, N'10104', N'Spain', N'(91) 345 67 89', N'(91) 012 34 56');\nINSERT  INTO Customer(custId, userId, companyName, contactName, contactTitle, address, city, region, postalCode, country, phone, fax)\n  VALUES(9, 1, N'Customer RTXGC', N'Raghav, Amritansh', N'Owner', N'6789, rue des Bouchers', N'Marseille', NULL, N'10105', N'France', N'23.45.67.89', N'23.45.67.80');\nINSERT  INTO Customer(custId, userId, companyName, contactName, contactTitle, address, city, region, postalCode, country, phone, fax)\n  VALUES(10, 1, N'Customer EEALV', N'Bassols, Pilar Colome', N'Accounting Manager', N'8901 Tsawassen Blvd.', N'Tsawassen', N'BC', N'10111', N'Canada', N'(604) 901-2345', N'(604) 678-9012');\n\nSET IDENTITY_INSERT Customer OFF;\n\n\nSET IDENTITY_INSERT SalesOrder ON;\n\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(1, 1, 5, '2006-07-04 00:00:00.000', '2006-08-01 00:00:00.000', '2006-07-16 00:00:00.000', 3, 32.38, N'Ship to 85-B', N'6789 rue de l''Abbaye', N'Reims', NULL, N'10345', N'France');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(2, 2, 6, '2006-07-05 00:00:00.000', '2006-08-16 00:00:00.000', '2006-07-10 00:00:00.000', 1, 11.61, N'Ship to 79-C', N'Luisenstr. 9012', N'Münster', NULL, N'10328', N'Germany');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(3, 3, 4, '2006-07-08 00:00:00.000', '2006-08-05 00:00:00.000', '2006-07-12 00:00:00.000', 2, 65.83, N'Destination SCQXA', N'Rua do Paço, 7890', N'Rio de Janeiro', N'RJ', N'10195', N'Brazil');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(4, 4, 3, '2006-07-08 00:00:00.000', '2006-08-05 00:00:00.000', '2006-07-15 00:00:00.000', 1, 41.34, N'Ship to 84-A', N'3456, rue du Commerce', N'Lyon', NULL, N'10342', N'France');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(5, 5, 4, '2006-07-09 00:00:00.000', '2006-08-06 00:00:00.000', '2006-07-11 00:00:00.000', 2, 51.30, N'Ship to 76-B', N'Boulevard Tirou, 9012', N'Charleroi', NULL, N'10318', N'Belgium');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(6, 6, 3, '2006-07-10 00:00:00.000', '2006-07-24 00:00:00.000', '2006-07-16 00:00:00.000', 2, 58.17, N'Destination JPAIY', N'Rua do Paço, 8901', N'Rio de Janeiro', N'RJ', N'10196', N'Brazil');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(7, 7, 5, '2006-07-11 00:00:00.000', '2006-08-08 00:00:00.000', '2006-07-23 00:00:00.000', 2, 22.98, N'Destination YUJRD', N'Hauptstr. 1234', N'Bern', NULL, N'10139', N'Switzerland');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(8, 8, 9, '2006-07-12 00:00:00.000', '2006-08-09 00:00:00.000', '2006-07-15 00:00:00.000', 3, 148.33, N'Ship to 68-A', N'Starenweg 6789', N'Genève', NULL, N'10294', N'Switzerland');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(9, 9, 3, '2006-07-15 00:00:00.000', '2006-08-12 00:00:00.000', '2006-07-17 00:00:00.000', 2, 13.97, N'Ship to 88-B', N'Rua do Mercado, 5678', N'Resende', N'SP', N'10354', N'Brazil');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(10, 10, 4, '2006-07-16 00:00:00.000', '2006-08-13 00:00:00.000', '2006-07-22 00:00:00.000', 3, 81.91, N'Destination JYDLM', N'Carrera1234 con Ave. Carlos Soublette #8-35', N'San Cristóbal', N'Táchira', N'10199', N'Venezuela');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(11, 1, 1, '2006-07-17 00:00:00.000', '2006-08-14 00:00:00.000', '2006-07-23 00:00:00.000', 1, 140.51, N'Destination RVDMF', N'Kirchgasse 9012', N'Graz', NULL, N'10157', N'Austria');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(12, 2, 4, '2006-07-18 00:00:00.000', '2006-08-15 00:00:00.000', '2006-07-25 00:00:00.000', 3, 3.25, N'Destination LGGCH', N'Sierras de Granada 9012', N'México D.F.', NULL, N'10137', N'Mexico');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(13, 3, 4, '2006-07-19 00:00:00.000', '2006-08-16 00:00:00.000', '2006-07-29 00:00:00.000', 1, 55.09, N'Ship to 56-A', N'Mehrheimerstr. 0123', N'Köln', NULL, N'10258', N'Germany');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(14, 4, 4, '2006-07-19 00:00:00.000', '2006-08-16 00:00:00.000', '2006-07-30 00:00:00.000', 2, 3.05, N'Ship to 61-B', N'Rua da Panificadora, 6789', N'Rio de Janeiro', N'RJ', N'10274', N'Brazil');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(15, 5, 8, '2006-07-22 00:00:00.000', '2006-08-19 00:00:00.000', '2006-07-25 00:00:00.000', 3, 48.29, N'Ship to 65-B', N'8901 Milton Dr.', N'Albuquerque', N'NM', N'10286', N'USA');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(16, 6, 9, '2006-07-23 00:00:00.000', '2006-08-20 00:00:00.000', '2006-07-31 00:00:00.000', 3, 146.06, N'Destination FFXKT', N'Kirchgasse 0123', N'Graz', NULL, N'10158', N'Austria');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(17, 7, 6, '2006-07-24 00:00:00.000', '2006-08-21 00:00:00.000', '2006-08-23 00:00:00.000', 3, 3.67, N'Destination KBSBN', N'Åkergatan 9012', N'Bräcke', NULL, N'10167', N'Sweden');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(18, 8, 2, '2006-07-25 00:00:00.000', '2006-08-22 00:00:00.000', '2006-08-12 00:00:00.000', 1, 55.28, N'Ship to 7-A', N'0123, place Kléber', N'Strasbourg', NULL, N'10329', N'France');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(19, 9, 3, '2006-07-26 00:00:00.000', '2006-09-06 00:00:00.000', '2006-07-31 00:00:00.000', 3, 25.73, N'Ship to 87-B', N'Torikatu 2345', N'Oulu', NULL, N'10351', N'Finland');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(20, 10, 4, '2006-07-29 00:00:00.000', '2006-08-26 00:00:00.000', '2006-08-06 00:00:00.000', 1, 208.58, N'Destination VAPXU', N'Berliner Platz 0123', N'München', NULL, N'10168', N'Germany');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(21, 1, 8, '2006-07-30 00:00:00.000', '2006-08-27 00:00:00.000', '2006-08-02 00:00:00.000', 3, 66.29, N'Destination QJVQH', N'5ª Ave. Los Palos Grandes 5678', N'Caracas', N'DF', N'10193', N'Venezuela');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(22, 2, 5, '2006-07-31 00:00:00.000', '2006-08-14 00:00:00.000', '2006-08-09 00:00:00.000', 1, 4.56, N'Ship to 89-B', N'8901 - 12th Ave. S.', N'Seattle', N'WA', N'10357', N'USA');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(23, 3, 1, '2006-08-01 00:00:00.000', '2006-08-29 00:00:00.000', '2006-08-02 00:00:00.000', 1, 136.54, N'Ship to 87-B', N'Torikatu 2345', N'Oulu', NULL, N'10351', N'Finland');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(24, 4, 6, '2006-08-01 00:00:00.000', '2006-08-29 00:00:00.000', '2006-08-30 00:00:00.000', 2, 4.54, N'Ship to 75-C', N'P.O. Box 7890', N'Lander', N'WY', N'10316', N'USA');\nINSERT  INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(25, 5, 6, '2006-08-02 00:00:00.000', '2006-08-30 00:00:00.000', '2006-08-06 00:00:00.000', 2, 98.03, N'Ship to 65-A', N'7890 Milton Dr.', N'Albuquerque', N'NM', N'10285', N'USA');\n\nSET IDENTITY_INSERT SalesOrder OFF;\n"
  },
  {
    "path": "demo/databases/mysql.sql",
    "content": "CREATE DATABASE IF NOT EXISTS llana;\n\nUSE llana;\n\nCREATE TABLE IF NOT EXISTS `User` (\n  id int NOT NULL AUTO_INCREMENT\n  ,email varchar(255) NOT NULL\n  ,password varchar(255) NOT NULL\n  ,role enum('ADMIN','USER') DEFAULT 'USER'\n  ,firstName varchar(255) DEFAULT NULL\n  ,lastName varchar(255) DEFAULT NULL\n  ,createdAt datetime DEFAULT CURRENT_TIMESTAMP\n  ,updatedAt datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP\n  ,deletedAt datetime DEFAULT NULL\n  ,PRIMARY KEY (id)\n  ,UNIQUE KEY id (id)\n  ,UNIQUE KEY uniqueEmail (email)\n) ENGINE=INNODB;\n\nINSERT IGNORE INTO `User` (`id`, `email`, `password`, `role`, `firstName`, `lastName`, `createdAt`, `updatedAt`, `deletedAt`) VALUES (1, 'test@test.com', '$2a$10$jm6bM7acpRa18Vdy8FSqIu4yzWAdSgZgRtRrx8zknIeZhSqPJjJU.', 'ADMIN', 'Jon', 'Doe', '2000-01-01 00:00:01', '2000-01-01 00:00:00', NULL);\n\nCREATE TABLE IF NOT EXISTS `UserApiKey` (\n  id int NOT NULL AUTO_INCREMENT\n  ,userId int NOT NULL\n  ,apiKey varchar(255) NOT NULL\n  ,createdAt datetime DEFAULT CURRENT_TIMESTAMP\n  ,updatedAt datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP\n  ,deletedAt datetime DEFAULT NULL\n  ,PRIMARY KEY (id)\n  ,KEY user (userId)\n  ,CONSTRAINT UserApiKeyUserId FOREIGN KEY (userId) REFERENCES User (id) ON DELETE CASCADE ON UPDATE RESTRICT\n) ENGINE=INNODB;\n\nINSERT IGNORE INTO `UserApiKey` (`id`, `userId`, `apiKey`, `createdAt`, `updatedAt`, `deletedAt`) VALUES (1, 1, 'Ex@mp1eS$Cu7eAp!K3y', '2000-01-01 00:00:00', '2000-01-01 00:00:00', NULL);\n\nCREATE TABLE IF NOT EXISTS Customer (\n  custId INT AUTO_INCREMENT NOT NULL\n  ,userId int NOT NULL\n  ,companyName VARCHAR(40) NOT NULL\n  ,contactName VARCHAR(60) NULL\n  ,contactTitle VARCHAR(30) NULL\n  ,address VARCHAR(60) NULL\n  ,city VARCHAR(15) NULL\n  ,region VARCHAR(15) NULL\n  ,postalCode VARCHAR(10) NULL\n  ,country VARCHAR(15) NULL\n  ,phone VARCHAR(24) NULL\n  ,mobile VARCHAR(24) NULL\n  ,email VARCHAR(225) NULL\n  ,fax VARCHAR(24) NULL\n  ,createdAt datetime DEFAULT CURRENT_TIMESTAMP\n  ,updatedAt datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP\n  ,deletedAt datetime DEFAULT NULL\n  ,PRIMARY KEY (custId)\n  ,CONSTRAINT CustomerUserId FOREIGN KEY (userId) REFERENCES User (id) ON DELETE CASCADE ON UPDATE RESTRICT\n  ) ENGINE=INNODB;\n\nCREATE TABLE IF NOT EXISTS Employee (\n  employeeId INT AUTO_INCREMENT NOT NULL\n  ,lastName VARCHAR(20) NOT NULL\n  ,firstName VARCHAR(10) NOT NULL\n  ,title VARCHAR(30) NULL\n  ,titleOfCourtesy VARCHAR(25) NULL\n  ,birthDate DATETIME NULL\n  ,hireDate DATETIME NULL\n  ,address VARCHAR(60) NULL\n  ,city VARCHAR(15) NULL\n  ,region VARCHAR(15) NULL\n  ,postalCode VARCHAR(10) NULL\n  ,country VARCHAR(15) NULL\n  ,phone VARCHAR(24) NULL\n  ,extension VARCHAR(4) NULL\n  ,mobile VARCHAR(24) NULL\n  ,email VARCHAR(225) NULL\n  ,photo BLOB NULL\n  ,notes TEXT NULL\n  ,photoPath VARCHAR(255) NULL\n  ,mgrid INT NULL\n  ,createdAt datetime DEFAULT CURRENT_TIMESTAMP\n  ,updatedAt datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP\n  ,deletedAt datetime DEFAULT NULL\n  ,PRIMARY KEY (employeeId)\n  ) ENGINE=INNODB;\n\n\nCREATE TABLE IF NOT EXISTS Shipper (\n  shipperId INT AUTO_INCREMENT NOT NULL\n  ,companyName VARCHAR(40) NOT NULL\n  ,phone VARCHAR(44) NULL\n  ,createdAt datetime DEFAULT CURRENT_TIMESTAMP\n  ,updatedAt datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP\n  ,deletedAt datetime DEFAULT NULL\n  ,PRIMARY KEY (ShipperId)\n  ) ENGINE=INNODB;\n\n\nCREATE TABLE IF NOT EXISTS SalesOrder (\n  orderId INT AUTO_INCREMENT NOT NULL\n  ,custId INT NOT NULL\n  ,employeeId INT NULL\n  ,orderDate DATETIME NULL\n  ,requiredDate DATETIME NULL\n  ,shippedDate DATETIME NULL\n  ,shipperId INT NOT NULL\n  ,freight DECIMAL(10, 2) NULL\n  ,shipName VARCHAR(40) NULL\n  ,shipAddress VARCHAR(60) NULL\n  ,shipCity VARCHAR(15) NULL\n  ,shipRegion VARCHAR(15) NULL\n  ,shipPostalCode VARCHAR(10) NULL\n  ,shipCountry VARCHAR(15) NULL\n  ,createdAt datetime DEFAULT CURRENT_TIMESTAMP\n  ,updatedAt datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP\n  ,deletedAt datetime DEFAULT NULL\n  ,PRIMARY KEY (orderId)\n   , FOREIGN KEY (shipperId)\n      REFERENCES Shipper(shipperId)\n   ,FOREIGN KEY (custId)\n      REFERENCES Customer(custId) \n  ) ENGINE=INNODB;\n\n\n\n-- Populate Employess table \n\nINSERT IGNORE INTO Employee(employeeId, lastName, firstName, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalCode, country, phone, mgrid)\n  VALUES(1, N'Davis', N'Sara', N'CEO', N'Ms.', '1958-12-08 00:00:00.000', '2002-05-01 00:00:00.000', N'7890 - 20th Ave. E., Apt. 2A', N'Seattle', N'WA', N'10003', N'USA', N'(206) 555-0101', NULL);\nINSERT IGNORE INTO Employee(employeeId, lastName, firstName, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalCode, country, phone, mgrid)\n  VALUES(2, N'Funk', N'Don', N'Vice President, Sales', N'Dr.', '1962-02-19 00:00:00.000', '2002-08-14 00:00:00.000', N'9012 W. Capital Way', N'Tacoma', N'WA', N'10001', N'USA', N'(206) 555-0100', 1);\nINSERT IGNORE INTO Employee(employeeId, lastName, firstName, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalCode, country, phone, mgrid)\n  VALUES(3, N'Lew', N'Judy', N'Sales Manager', N'Ms.', '1973-08-30 00:00:00.000', '2002-04-01 00:00:00.000', N'2345 Moss Bay Blvd.', N'Kirkland', N'WA', N'10007', N'USA', N'(206) 555-0103', 2);\nINSERT IGNORE INTO Employee(employeeId, lastName, firstName, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalCode, country, phone, mgrid)\n  VALUES(4, N'Peled', N'Yael', N'Sales Representative', N'Mrs.', '1947-09-19 00:00:00.000', '2003-05-03 00:00:00.000', N'5678 Old Redmond Rd.', N'Redmond', N'WA', N'10009', N'USA', N'(206) 555-0104', 3);\nINSERT IGNORE INTO Employee(employeeId, lastName, firstName, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalCode, country, phone, mgrid)\n  VALUES(5, N'Buck', N'Sven', N'Sales Manager', N'Mr.', '1965-03-04 00:00:00.000', '2003-10-17 00:00:00.000', N'8901 Garrett Hill', N'London', NULL, N'10004', N'UK', N'(71) 234-5678', 2);\nINSERT IGNORE INTO Employee(employeeId, lastName, firstName, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalCode, country, phone, mgrid)\n  VALUES(6, N'Suurs', N'Paul', N'Sales Representative', N'Mr.', '1973-07-02 00:00:00.000', '2003-10-17 00:00:00.000', N'3456 Coventry House, Miner Rd.', N'London', NULL, N'10005', N'UK', N'(71) 345-6789', 5);\nINSERT IGNORE INTO Employee(employeeId, lastName, firstName, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalCode, country, phone, mgrid)\n  VALUES(7, N'King', N'Russell', N'Sales Representative', N'Mr.', '1970-05-29 00:00:00.000', '2004-01-02 00:00:00.000', N'6789 Edgeham Hollow, Winchester Way', N'London', NULL, N'10002', N'UK', N'(71) 123-4567', 5);\nINSERT IGNORE INTO Employee(employeeId, lastName, firstName, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalCode, country, phone, mgrid)\n  VALUES(8, N'Cameron', N'Maria', N'Sales Representative', N'Ms.', '1968-01-09 00:00:00.000', '2004-03-05 00:00:00.000', N'4567 - 11th Ave. N.E.', N'Seattle', N'WA', N'10006', N'USA', N'(206) 555-0102', 3);\nINSERT IGNORE INTO Employee(employeeId, lastName, firstName, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalCode, country, phone, mgrid)\n  VALUES(9, N'Dolgopyatova', N'Zoya', N'Sales Representative', N'Ms.', '1976-01-27 00:00:00.000', '2004-11-15 00:00:00.000', N'1234 Houndstooth Rd.', N'London', NULL, N'10008', N'UK', N'(71) 456-7890', 5);\n\n-- ---  \n\nINSERT IGNORE INTO Customer(custId, userId, companyName, contactName, contactTitle, address, city, region, postalCode, country, phone, fax)\n  VALUES(1, 1, N'Customer NRZBB', N'Allen, Michael', N'Sales Representative', N'Obere Str. 0123', N'Berlin', NULL, N'10092', N'Germany', N'030-3456789', N'030-0123456');\nINSERT IGNORE INTO Customer(custId, userId, companyName, contactName, contactTitle, address, city, region, postalCode, country, phone, fax)\n  VALUES(2, 1, N'Customer MLTDN', N'Hassall, Mark', N'Owner', N'Avda. de la Constitución 5678', N'México D.F.', NULL, N'10077', N'Mexico', N'(5) 789-0123', N'(5) 456-7890');\nINSERT IGNORE INTO Customer(custId, userId, companyName, contactName, contactTitle, address, city, region, postalCode, country, phone, fax)\n  VALUES(3, 1, N'Customer KBUDE', N'Peoples, John', N'Owner', N'Mataderos  7890', N'México D.F.', NULL, N'10097', N'Mexico', N'(5) 123-4567', NULL);\nINSERT IGNORE INTO Customer(custId, userId, companyName, contactName, contactTitle, address, city, region, postalCode, country, phone, fax)\n  VALUES(4, 1, N'Customer HFBZG', N'Arndt, Torsten', N'Sales Representative', N'7890 Hanover Sq.', N'London', NULL, N'10046', N'UK', N'(171) 456-7890', N'(171) 456-7891');\nINSERT IGNORE INTO Customer(custId, userId, companyName, contactName, contactTitle, address, city, region, postalCode, country, phone, fax)\n  VALUES(5, 1, N'Customer HGVLZ', N'Higginbotham, Tom', N'Order Administrator', N'Berguvsvägen  5678', N'Luleå', NULL, N'10112', N'Sweden', N'0921-67 89 01', N'0921-23 45 67');\nINSERT IGNORE INTO Customer(custId, userId, companyName, contactName, contactTitle, address, city, region, postalCode, country, phone, fax)\n  VALUES(6, 1, N'Customer XHXJV', N'Poland, Carole', N'Sales Representative', N'Forsterstr. 7890', N'Mannheim', NULL, N'10117', N'Germany', N'0621-67890', N'0621-12345');\nINSERT IGNORE INTO Customer(custId, userId, companyName, contactName, contactTitle, address, city, region, postalCode, country, phone, fax)\n  VALUES(7, 1, N'Customer QXVLA', N'Bansal, Dushyant', N'Marketing Manager', N'2345, place Kléber', N'Strasbourg', NULL, N'10089', N'France', N'67.89.01.23', N'67.89.01.24');\nINSERT IGNORE INTO Customer(custId, userId, companyName, contactName, contactTitle, address, city, region, postalCode, country, phone, fax)\n  VALUES(8, 1, N'Customer QUHWH', N'Ilyina, Julia', N'Owner', N'C/ Araquil, 0123', N'Madrid', NULL, N'10104', N'Spain', N'(91) 345 67 89', N'(91) 012 34 56');\nINSERT IGNORE INTO Customer(custId, userId, companyName, contactName, contactTitle, address, city, region, postalCode, country, phone, fax)\n  VALUES(9, 1, N'Customer RTXGC', N'Raghav, Amritansh', N'Owner', N'6789, rue des Bouchers', N'Marseille', NULL, N'10105', N'France', N'23.45.67.89', N'23.45.67.80');\nINSERT IGNORE INTO Customer(custId, userId, companyName, contactName, contactTitle, address, city, region, postalCode, country, phone, fax)\n  VALUES(10,1, N'Customer EEALV', N'Bassols, Pilar Colome', N'Accounting Manager', N'8901 Tsawassen Blvd.', N'Tsawassen', N'BC', N'10111', N'Canada', N'(604) 901-2345', N'(604) 678-9012');\n\n\nINSERT IGNORE INTO Shipper(shipperId, companyName, phone)\n  VALUES(1, N'Shipper GVSUA', N'(503) 555-0137');\nINSERT IGNORE INTO Shipper(shipperId, companyName, phone)\n  VALUES(2, N'Shipper ETYNR', N'(425) 555-0136');\nINSERT IGNORE INTO Shipper(shipperId, companyName, phone)\n  VALUES(3, N'Shipper ZHISN', N'(415) 555-0138');\n\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(1, 1, 5, '2006-07-04 00:00:00.000', '2006-08-01 00:00:00.000', '2006-07-16 00:00:00.000', 3, 32.38, N'Ship to 85-B', N'6789 rue de l''Abbaye', N'Reims', NULL, N'10345', N'France');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(2, 2, 6, '2006-07-05 00:00:00.000', '2006-08-16 00:00:00.000', '2006-07-10 00:00:00.000', 1, 11.61, N'Ship to 79-C', N'Luisenstr. 9012', N'Münster', NULL, N'10328', N'Germany');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(3, 3, 4, '2006-07-08 00:00:00.000', '2006-08-05 00:00:00.000', '2006-07-12 00:00:00.000', 2, 65.83, N'Destination SCQXA', N'Rua do Paço, 7890', N'Rio de Janeiro', N'RJ', N'10195', N'Brazil');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(4, 4, 3, '2006-07-08 00:00:00.000', '2006-08-05 00:00:00.000', '2006-07-15 00:00:00.000', 1, 41.34, N'Ship to 84-A', N'3456, rue du Commerce', N'Lyon', NULL, N'10342', N'France');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(5, 5, 4, '2006-07-09 00:00:00.000', '2006-08-06 00:00:00.000', '2006-07-11 00:00:00.000', 2, 51.30, N'Ship to 76-B', N'Boulevard Tirou, 9012', N'Charleroi', NULL, N'10318', N'Belgium');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(6, 6, 3, '2006-07-10 00:00:00.000', '2006-07-24 00:00:00.000', '2006-07-16 00:00:00.000', 2, 58.17, N'Destination JPAIY', N'Rua do Paço, 8901', N'Rio de Janeiro', N'RJ', N'10196', N'Brazil');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(7, 7, 5,'2006-07-11 00:00:00.000', '2006-08-08 00:00:00.000', '2006-07-23 00:00:00.000', 2, 22.98, N'Destination YUJRD', N'Hauptstr. 1234', N'Bern', NULL, N'10139', N'Switzerland');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(8, 8, 9, '2006-07-12 00:00:00.000', '2006-08-09 00:00:00.000', '2006-07-15 00:00:00.000', 3, 148.33, N'Ship to 68-A', N'Starenweg 6789', N'Genève', NULL, N'10294', N'Switzerland');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(9, 9, 3, '2006-07-15 00:00:00.000', '2006-08-12 00:00:00.000', '2006-07-17 00:00:00.000', 2, 13.97, N'Ship to 88-B', N'Rua do Mercado, 5678', N'Resende', N'SP', N'10354', N'Brazil');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\nVALUES(10, 10, 4, '2006-07-16 00:00:00.000', '2006-08-13 00:00:00.000', '2006-07-22 00:00:00.000', 3, 81.91, N'Destination JYDLM', N'Carrera1234 con Ave. Carlos Soublette #8-35', N'San Cristóbal', N'Táchira', N'10199', N'Venezuela');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(11, 1, 1,  '2006-07-17 00:00:00.000', '2006-08-14 00:00:00.000', '2006-07-23 00:00:00.000', 1, 140.51, N'Destination RVDMF', N'Kirchgasse 9012', N'Graz', NULL, N'10157', N'Austria');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(12, 2, 4,  '2006-07-18 00:00:00.000', '2006-08-15 00:00:00.000', '2006-07-25 00:00:00.000', 3, 3.25, N'Destination LGGCH', N'Sierras de Granada 9012', N'México D.F.', NULL, N'10137', N'Mexico');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(13, 3, 4, '2006-07-19 00:00:00.000', '2006-08-16 00:00:00.000', '2006-07-29 00:00:00.000', 1, 55.09, N'Ship to 56-A', N'Mehrheimerstr. 0123', N'Köln', NULL, N'10258', N'Germany');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(14, 4, 4,'2006-07-19 00:00:00.000', '2006-08-16 00:00:00.000', '2006-07-30 00:00:00.000', 2, 3.05, N'Ship to 61-B', N'Rua da Panificadora, 6789', N'Rio de Janeiro', N'RJ', N'10274', N'Brazil');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(15, 5, 8, '2006-07-22 00:00:00.000', '2006-08-19 00:00:00.000', '2006-07-25 00:00:00.000', 3, 48.29, N'Ship to 65-B', N'8901 Milton Dr.', N'Albuquerque', N'NM', N'10286', N'USA');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(16, 6, 9, '2006-07-23 00:00:00.000', '2006-08-20 00:00:00.000', '2006-07-31 00:00:00.000', 3, 146.06, N'Destination FFXKT', N'Kirchgasse 0123', N'Graz', NULL, N'10158', N'Austria');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(17, 7, 6, '2006-07-24 00:00:00.000', '2006-08-21 00:00:00.000', '2006-08-23 00:00:00.000', 3, 3.67, N'Destination KBSBN', N'Åkergatan 9012', N'Bräcke', NULL, N'10167', N'Sweden');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(18, 8, 2, '2006-07-25 00:00:00.000', '2006-08-22 00:00:00.000', '2006-08-12 00:00:00.000', 1, 55.28, N'Ship to 7-A', N'0123, place Kléber', N'Strasbourg', NULL, N'10329', N'France');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(19, 9, 3, '2006-07-26 00:00:00.000', '2006-09-06 00:00:00.000', '2006-07-31 00:00:00.000', 3, 25.73, N'Ship to 87-B', N'Torikatu 2345', N'Oulu', NULL, N'10351', N'Finland');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n   VALUES(20, 10, 4, '2006-07-29 00:00:00.000', '2006-08-26 00:00:00.000', '2006-08-06 00:00:00.000', 1, 208.58, N'Destination VAPXU', N'Berliner Platz 0123', N'München', NULL, N'10168', N'Germany');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(21, 1, 8,  '2006-07-30 00:00:00.000', '2006-08-27 00:00:00.000', '2006-08-02 00:00:00.000', 3, 66.29, N'Destination QJVQH', N'5ª Ave. Los Palos Grandes 5678', N'Caracas', N'DF', N'10193', N'Venezuela');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(22, 2, 5, '2006-07-31 00:00:00.000', '2006-08-14 00:00:00.000', '2006-08-09 00:00:00.000', 1, 4.56, N'Ship to 89-B', N'8901 - 12th Ave. S.', N'Seattle', N'WA', N'10357', N'USA');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(23, 3, 1, '2006-08-01 00:00:00.000', '2006-08-29 00:00:00.000', '2006-08-02 00:00:00.000', 1, 136.54, N'Ship to 87-B', N'Torikatu 2345', N'Oulu', NULL, N'10351', N'Finland');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(24, 4, 6, '2006-08-01 00:00:00.000', '2006-08-29 00:00:00.000', '2006-08-30 00:00:00.000', 2, 4.54, N'Ship to 75-C', N'P.O. Box 7890', N'Lander', N'WY', N'10316', N'USA');\nINSERT IGNORE INTO SalesOrder(orderId, custId, employeeId, orderDate, requiredDate, shippedDate, shipperId, freight, shipName, shipAddress, shipCity, shipRegion, shipPostalCode, shipCountry)\n  VALUES(25, 5, 6, '2006-08-02 00:00:00.000', '2006-08-30 00:00:00.000', '2006-08-06 00:00:00.000', 2, 98.03, N'Ship to 65-A', N'7890 Milton Dr.', N'Albuquerque', N'NM', N'10285', N'USA');\n"
  },
  {
    "path": "demo/databases/postgres.sql",
    "content": "DROP TABLE IF EXISTS \"User\";\n\nDROP TYPE IF EXISTS userrole;\nCREATE TYPE userrole AS ENUM ('ADMIN','USER');\n\nCREATE TABLE \"User\" \n  ( \n     \"id\"         SERIAL PRIMARY KEY NOT NULL, \n     \"email\"      VARCHAR (255) NOT NULL, \n     \"password\"   VARCHAR (255) NOT NULL, \n     \"role\"       userrole NOT NULL, \n     \"firstName\"  VARCHAR (255) NULL,\n     \"lastName\"   VARCHAR (255) NULL,\n     \"createdAt\"  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n     \"updatedAt\"  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n     \"deletedAt\"  TIMESTAMP NULL,\n     UNIQUE(\"email\")\n  ); \n\nINSERT INTO \"User\"(\"email\", \"password\", \"role\", \"firstName\", \"lastName\", \"createdAt\", \"updatedAt\", \"deletedAt\")\nVALUES('test@test.com', '$2a$10$jm6bM7acpRa18Vdy8FSqIu4yzWAdSgZgRtRrx8zknIeZhSqPJjJU.', 'ADMIN', 'Jon', 'Doe', '2000-01-01 00:00:01', '2000-01-01 00:00:00', NULL);\n\n  \nDROP TABLE IF EXISTS \"UserApiKey\";\n\nCREATE TABLE \"UserApiKey\"\n  ( \n     \"id\"         SERIAL PRIMARY KEY NOT NULL, \n     \"userId\"     INT NOT NULL, \n     \"apiKey\"     VARCHAR (255) NOT NULL, \n     \"createdAt\"  TIMESTAMP DEFAULT CURRENT_TIMESTAMP, \n     \"updatedAt\"  TIMESTAMP DEFAULT CURRENT_TIMESTAMP, \n     \"deletedAt\"  TIMESTAMP NULL, \n     CONSTRAINT UserApiKeyUserId FOREIGN KEY (\"userId\") REFERENCES \"User\" (\"id\") ON DELETE CASCADE ON UPDATE RESTRICT\n  );\n\nINSERT INTO \"UserApiKey\"(\"userId\", \"apiKey\", \"createdAt\", \"updatedAt\", \"deletedAt\")\nVALUES (1, 'Ex@mp1eS$Cu7eAp!K3y', '2000-01-01 00:00:01', '2000-01-01 00:00:01', NULL);\n\nDROP TABLE IF EXISTS \"Customer\";\n\nCREATE TABLE \"Customer\" \n  ( \n     \"custId\"       SERIAL PRIMARY KEY NOT NULL, \n     \"userId\"     INT NOT NULL, \n     \"companyName\"  VARCHAR (40) NOT NULL, \n      \"email\"      VARCHAR (255) NULL, \n     \"contactName\"  VARCHAR (60) NULL, \n     \"contactTitle\" VARCHAR (30) NULL, \n     address      VARCHAR (60) NULL, \n     city         VARCHAR (15) NULL, \n     region       VARCHAR (15) NULL, \n     \"postalCode\"   VARCHAR (10) NULL, \n     country      VARCHAR (15) NULL, \n     phone        VARCHAR (24) NULL, \n     fax          VARCHAR (24) NULL,\n     \"createdAt\"  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n     \"updatedAt\"  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n     \"deletedAt\"  TIMESTAMP NULL,\n      CONSTRAINT CustomerUserId FOREIGN KEY (\"userId\") REFERENCES \"User\" (\"id\") ON DELETE CASCADE ON UPDATE RESTRICT\n  ); \n\n\nDROP TABLE IF EXISTS \"Employee\";\nCREATE TABLE \"Employee\" \n  ( \n     \"employeeId\"      SERIAL  PRIMARY KEY NOT NULL, \n     \"email\"      VARCHAR (255) NULL, \n     \"lastName\"        VARCHAR (20) NOT NULL, \n     \"firstName\"       VARCHAR (10) NOT NULL, \n     title           VARCHAR (30) NULL, \n     \"titleOfCourtesy\" VARCHAR (25) NULL, \n     \"birthDate\"       TIMESTAMP NULL, \n     \"hireDate\"        TIMESTAMP NULL, \n     address         VARCHAR (60) NULL, \n     city            VARCHAR (15) NULL, \n     region          VARCHAR (15) NULL, \n     \"postalCode\"      VARCHAR (10) NULL, \n     country         VARCHAR (15) NULL, \n     phone       VARCHAR (24) NULL, \n     extension       VARCHAR (4) NULL, \n     photo           BYTEA NULL, \n     notes           TEXT NULL, \n     mobile        VARCHAR (30) NULL, \n     \"photoPath\"       VARCHAR (255) NULL,\n     \"createdAt\"  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n     \"updatedAt\"  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n     \"deletedAt\"  TIMESTAMP NULL\n  ); \n\nDROP TABLE IF EXISTS \"Supplier\";\nCREATE TABLE \"Supplier\" \n  ( \n     \"supplierId\"   SERIAL PRIMARY KEY NOT NULL, \n     \"companyName\"  VARCHAR (40) NOT NULL, \n     \"contactName\"  VARCHAR (60) NULL, \n     \"contactTitle\" VARCHAR (30) NULL, \n     address       VARCHAR (60) NULL, \n     city          VARCHAR (15) NULL, \n     region        VARCHAR (15) NULL, \n     \"postalCode\"    VARCHAR (10) NULL, \n     country       VARCHAR (15) NULL, \n     phone         VARCHAR (24) NULL, \n     fax           VARCHAR (24) NULL,\n     \"createdAt\"  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n     \"updatedAt\"  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n     \"deletedAt\"  TIMESTAMP NULL\n  );\n\nDROP TABLE IF EXISTS \"Shipper\";\nCREATE TABLE \"Shipper\" \n  ( \n     \"shipperId\"   SERIAL NOT NULL, \n     \"companyName\" VARCHAR (40) NOT NULL, \n     phone       VARCHAR (44) NULL,\n     \"createdAt\"  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n     \"updatedAt\"  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n     PRIMARY KEY ( \"shipperId\" ) \n  ); \n\nDROP TABLE IF EXISTS \"SalesOrder\";\nCREATE TABLE \"SalesOrder\" \n  ( \n     \"orderId\"        SERIAL NOT NULL, \n     \"custId\"         INT NULL, \n     \"employeeId\"     INT NULL, \n     \"orderDate\"      TIMESTAMP NULL, \n     \"requiredDate\"   TIMESTAMP NULL, \n     \"shippedDate\"    TIMESTAMP NULL, \n     \"shipperId\"      INT NULL, \n     \"freight\"        DECIMAL(10, 2) NULL, \n     \"shipName\"       VARCHAR (40) NULL, \n     \"shipAddress\"    VARCHAR (60) NULL, \n     \"shipCity\"       VARCHAR (15) NULL, \n     \"shipRegion\"     VARCHAR (15) NULL, \n     \"shipPostalCode\" VARCHAR (10) NULL, \n     \"shipCountry\"    VARCHAR (15) NULL,\n     \"createdAt\"  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n     \"updatedAt\"  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n     \"deletedAt\"  TIMESTAMP NULL,\n     PRIMARY KEY ( \"orderId\" ),\n     FOREIGN KEY ( \"custId\" ) REFERENCES \"Customer\" ( \"custId\" ) ON DELETE SET NULL ON UPDATE RESTRICT,\n      FOREIGN KEY ( \"employeeId\" ) REFERENCES \"Employee\" ( \"employeeId\" ) ON DELETE SET NULL ON UPDATE RESTRICT,\n      FOREIGN KEY ( \"shipperId\" ) REFERENCES \"Shipper\" ( \"shipperId\" ) ON DELETE SET NULL ON UPDATE RESTRICT\n  );\n\n\n\n\n-- Populate Employess table\n\n\nINSERT INTO \"Employee\"(\"lastName\", \"firstName\", \"title\", \"titleOfCourtesy\", \"birthDate\", \"hireDate\", \"address\", \"city\", \"region\", \"postalCode\", \"country\", \"phone\")\n  VALUES(N'Davis', N'Sara', N'CEO', N'Ms.', '19581208 00:00:00.000', '20020501 00:00:00.000', N'7890 - 20th Ave. E., Apt. 2A', N'Seattle', N'WA', N'10003', N'USA', N'(206) 555-0101');\nINSERT INTO \"Employee\"(\"lastName\", \"firstName\", \"title\", \"titleOfCourtesy\", \"birthDate\", \"hireDate\", \"address\", \"city\", \"region\", \"postalCode\", \"country\", \"phone\")  \n  VALUES(N'Funk', N'Don', N'Vice President, Sales', N'Dr.', '19620219 00:00:00.000', '20020814 00:00:00.000', N'9012 W. Capital Way', N'Tacoma', N'WA', N'10001', N'USA', N'(206) 555-0100');\nINSERT INTO \"Employee\"(\"lastName\", \"firstName\", \"title\", \"titleOfCourtesy\", \"birthDate\", \"hireDate\", \"address\", \"city\", \"region\", \"postalCode\", \"country\", \"phone\")  \n  VALUES(N'Lew', N'Judy', N'Sales Manager', N'Ms.', '19730830 00:00:00.000', '20020401 00:00:00.000', N'2345 Moss Bay Blvd.', N'Kirkland', N'WA', N'10007', N'USA', N'(206) 555-0103');\nINSERT INTO \"Employee\"(\"lastName\", \"firstName\", \"title\", \"titleOfCourtesy\", \"birthDate\", \"hireDate\", \"address\", \"city\", \"region\", \"postalCode\", \"country\", \"phone\")  \n  VALUES(N'Peled', N'Yael', N'Sales Representative', N'Mrs.', '19470919 00:00:00.000', '20030503 00:00:00.000', N'5678 Old Redmond Rd.', N'Redmond', N'WA', N'10009', N'USA', N'(206) 555-0104');\nINSERT INTO \"Employee\"(\"lastName\", \"firstName\", \"title\", \"titleOfCourtesy\", \"birthDate\", \"hireDate\", \"address\", \"city\", \"region\", \"postalCode\", \"country\", \"phone\")  \n  VALUES(N'Buck', N'Sven', N'Sales Manager', N'Mr.', '19650304 00:00:00.000', '20031017 00:00:00.000', N'8901 Garrett Hill', N'London', NULL, N'10004', N'UK', N'(71) 234-5678');\nINSERT INTO \"Employee\"(\"lastName\", \"firstName\", \"title\", \"titleOfCourtesy\", \"birthDate\", \"hireDate\", \"address\", \"city\", \"region\", \"postalCode\", \"country\", \"phone\")  \n  VALUES(N'Suurs', N'Paul', N'Sales Representative', N'Mr.', '19730702 00:00:00.000', '20031017 00:00:00.000', N'3456 Coventry House, Miner Rd.', N'London', NULL, N'10005', N'UK', N'(71) 345-6789');\nINSERT INTO \"Employee\"(\"lastName\", \"firstName\", \"title\", \"titleOfCourtesy\", \"birthDate\", \"hireDate\", \"address\", \"city\", \"region\", \"postalCode\", \"country\", \"phone\")  \n  VALUES(N'King', N'Russell', N'Sales Representative', N'Mr.', '19700529 00:00:00.000', '20040102 00:00:00.000', N'6789 Edgeham Hollow, Winchester Way', N'London', NULL, N'10002', N'UK', N'(71) 123-4567');\nINSERT INTO \"Employee\"(\"lastName\", \"firstName\", \"title\", \"titleOfCourtesy\", \"birthDate\", \"hireDate\", \"address\", \"city\", \"region\", \"postalCode\", \"country\", \"phone\")  \n  VALUES(N'Cameron', N'Maria', N'Sales Representative', N'Ms.', '19680109 00:00:00.000', '20040305 00:00:00.000', N'4567 - 11th Ave. N.E.', N'Seattle', N'WA', N'10006', N'USA', N'(206) 555-0102');\nINSERT INTO \"Employee\"(\"lastName\", \"firstName\", \"title\", \"titleOfCourtesy\", \"birthDate\", \"hireDate\", \"address\", \"city\", \"region\", \"postalCode\", \"country\", \"phone\")  \n  VALUES(N'Dolgopyatova', N'Zoya', N'Sales Representative', N'Ms.', '19760127 00:00:00.000', '20041115 00:00:00.000', N'1234 Houndstooth Rd.', N'London', NULL, N'10008', N'UK', N'(71) 456-7890');\n\n-- ---  Populate \"Supplier\"\n\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(1, N'\"Supplier\" SWRXU', N'Adolphi, Stephan', N'Purchasing Manager', N'2345 Gilbert St.', N'London', NULL, N'10023', N'UK', N'(171) 456-7890', NULL);\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(2, N'\"Supplier\" VHQZD', N'Hance, Jim', N'Order Administrator', N'P.O. Box 5678', N'New Orleans', N'LA', N'10013', N'USA', N'(100) 555-0111', NULL);\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(3, N'\"Supplier\" STUAZ', N'Parovszky, Alfons', N'Sales Representative', N'1234 Oxford Rd.', N'Ann Arbor', N'MI', N'10026', N'USA', N'(313) 555-0109', N'(313) 555-0112');\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(4, N'\"Supplier\" QOVFD', N'Balázs, Erzsébet', N'Marketing Manager', N'7890 Sekimai Musashino-shi', N'Tokyo', NULL, N'10011', N'Japan', N'(03) 6789-0123', NULL);\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(5, N'\"Supplier\" EQPNC', N'Holm, Michael', N'Export Administrator', N'Calle del Rosal 4567', N'Oviedo', N'Asturias', N'10029', N'Spain', N'(98) 123 45 67', NULL);\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(6, N'\"Supplier\" QWUSF', N'Popkova, Darya', N'Marketing Representative', N'8901 Setsuko Chuo-ku', N'Osaka', NULL, N'10028', N'Japan', N'(06) 789-0123', NULL);\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(7, N'\"Supplier\" GQRCV', N'Ræbild, Jesper', N'Marketing Manager', N'5678 Rose St. Moonie Ponds', N'Melbourne', N'Victoria', N'10018', N'Australia', N'(03) 123-4567', N'(03) 456-7890');\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(8, N'\"Supplier\" BWGYE', N'Iallo, Lucio', N'Sales Representative', N'9012 King''s Way', N'Manchester', NULL, N'10021', N'UK', N'(161) 567-8901', NULL);\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(9, N'\"Supplier\" QQYEU', N'Basalik, Evan', N'Sales Agent', N'Kaloadagatan 4567', N'Göteborg', NULL, N'10022', N'Sweden', N'031-345 67 89', N'031-678 90 12');\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(10, N'\"Supplier\" UNAHG', N'Barnett, Dave', N'Marketing Manager', N'Av. das Americanas 2345', N'Sao Paulo', NULL, N'10034', N'Brazil', N'(11) 345 6789', NULL);\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(11, N'\"Supplier\" ZPYVS', N'Jain, Mukesh', N'Sales Manager', N'Tiergartenstraße 3456', N'Berlin', NULL, N'10016', N'Germany', N'(010) 3456789', NULL);\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(12, N'\"Supplier\" SVIYA', N'Regev, Barak', N'International Marketing Mgr.', N'Bogenallee 9012', N'Frankfurt', NULL, N'10024', N'Germany', N'(069) 234567', NULL);\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(13, N'\"Supplier\" TEGSC', N'Brehm, Peter', N'Coordinator Foreign Markets', N'Frahmredder 3456', N'Cuxhaven', NULL, N'10019', N'Germany', N'(04721) 1234', N'(04721) 2345');\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(14, N'\"Supplier\" KEREV', N'Keil, Kendall', N'Sales Representative', N'Viale Dante, 6789', N'Ravenna', NULL, N'10015', N'Italy', N'(0544) 56789', N'(0544) 34567');\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(15, N'\"Supplier\" NZLIF', N'Sałas-Szlejter, Karolina', N'Marketing Manager', N'Hatlevegen 1234', N'Sandvika', NULL, N'10025', N'Norway', N'(0)9-012345', NULL);\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(16, N'\"Supplier\" UHZRG', N'Scholl, Thorsten', N'Regional Account Rep.', N'8901 - 8th Avenue Suite 210', N'Bend', N'OR', N'10035', N'USA', N'(503) 555-0108', NULL);\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(17, N'\"Supplier\" QZGUF', N'Kleinerman, Christian', N'Sales Representative', N'Brovallavägen 0123', N'Stockholm', NULL, N'10033', N'Sweden', N'08-234 56 78', NULL);\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(18, N'\"Supplier\" LVJUA', N'Canel, Fabrice', N'Sales Manager', N'3456, Rue des Francs-Bourgeois', N'Paris', NULL, N'10031', N'France', N'(1) 90.12.34.56', N'(1) 01.23.45.67');\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(19, N'\"Supplier\" JDNUG', N'Chapman, Greg', N'Wholesale Account Agent', N'Order Processing Dept. 7890 Paul Revere Blvd.', N'Boston', N'MA', N'10027', N'USA', N'(617) 555-0110', N'(617) 555-0113');\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(20, N'\"Supplier\" CIYNM', N'Köszegi, Emília', N'Owner', N'6789 Serangoon Loop, Suite #402', N'Singapore', NULL, N'10037', N'Singapore', N'012-3456', NULL);\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(21, N'\"Supplier\" XOXZA', N'Shakespear, Paul', N'Sales Manager', N'Lyngbysild Fiskebakken 9012', N'Lyngby', NULL, N'10012', N'Denmark', N'67890123', N'78901234');\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(22, N'\"Supplier\" FNUXM', N'Skelly, Bonnie L.', N'Accounting Manager', N'Verkoop Rijnweg 8901', N'Zaandam', NULL, N'10014', N'Netherlands', N'(12345) 8901', N'(12345) 5678');\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(23, N'\"Supplier\" ELCRN', N'LaMee, Brian', N'\"Product\" Manager', N'Valtakatu 1234', N'Lappeenranta', NULL, N'10032', N'Finland', N'(953) 78901', NULL);\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(24, N'\"Supplier\" JNNES', N'Clark, Molly', N'Sales Representative', N'6789 Prince Edward Parade Hunter''s Hill', N'Sydney', N'NSW', N'10030', N'Australia', N'(02) 234-5678', N'(02) 567-8901');\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(25, N'\"Supplier\" ERVYZ', N'Sprenger, Christof', N'Marketing Manager', N'7890 Rue St. Laurent', N'Montréal', N'Québec', N'10017', N'Canada', N'(514) 456-7890', NULL);\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(26, N'\"Supplier\" ZWZDM', N'Cunha, Gonçalo', N'Order Administrator', N'Via dei Gelsomini, 5678', N'Salerno', NULL, N'10020', N'Italy', N'(089) 4567890', N'(089) 4567890');\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(27, N'\"Supplier\" ZRYDZ', N'Leoni, Alessandro', N'Sales Manager', N'4567, rue H. Voiron', N'Montceau', NULL, N'10036', N'France', N'89.01.23.45', NULL);\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(28, N'\"Supplier\" OAVQT', N'Teper, Jeff', N'Sales Representative', N'Bat. B 2345, rue des Alpes', N'Annecy', NULL, N'10010', N'France', N'01.23.45.67', N'89.01.23.45');\nINSERT INTO \"Supplier\"(\"supplierId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(29, N'\"Supplier\" OGLRK', N'Walters, Rob', N'Accounting Manager', N'0123 rue Chasseur', N'Ste-Hyacinthe', N'Québec', N'10009', N'Canada', N'(514) 567-890', N'(514) 678-9012');\n\n\n\nINSERT INTO \"Customer\"(\"userId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(1, N'Customer NRZBB', N'Allen, Michael', N'Sales Representative', N'Obere Str. 0123', N'Berlin', NULL, N'10092', N'Germany', N'030-3456789', N'030-0123456');\nINSERT INTO \"Customer\"(\"userId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(1, N'Customer MLTDN', N'Hassall, Mark', N'Owner', N'Avda. de la Constitución 5678', N'México D.F.', NULL, N'10077', N'Mexico', N'(5) 789-0123', N'(5) 456-7890');\nINSERT INTO \"Customer\"(\"userId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(1, N'Customer KBUDE', N'Peoples, John', N'Owner', N'Mataderos  7890', N'México D.F.', NULL, N'10097', N'Mexico', N'(5) 123-4567', NULL);\nINSERT INTO \"Customer\"(\"userId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(1, N'Customer HFBZG', N'Arndt, Torsten', N'Sales Representative', N'7890 Hanover Sq.', N'London', NULL, N'10046', N'UK', N'(171) 456-7890', N'(171) 456-7891');\nINSERT INTO \"Customer\"(\"userId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(1, N'Customer HGVLZ', N'Higginbotham, Tom', N'Order Administrator', N'Berguvsvägen  5678', N'Luleå', NULL, N'10112', N'Sweden', N'0921-67 89 01', N'0921-23 45 67');\nINSERT INTO \"Customer\"(\"userId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(1, N'Customer XHXJV', N'Poland, Carole', N'Sales Representative', N'Forsterstr. 7890', N'Mannheim', NULL, N'10117', N'Germany', N'0621-67890', N'0621-12345');\nINSERT INTO \"Customer\"(\"userId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(1, N'Customer QXVLA', N'Bansal, Dushyant', N'Marketing Manager', N'2345, place Kléber', N'Strasbourg', NULL, N'10089', N'France', N'67.89.01.23', N'67.89.01.24');\nINSERT INTO \"Customer\"(\"userId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(1, N'Customer QUHWH', N'Ilyina, Julia', N'Owner', N'C/ Araquil, 0123', N'Madrid', NULL, N'10104', N'Spain', N'(91) 345 67 89', N'(91) 012 34 56');\nINSERT INTO \"Customer\"(\"userId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(1, N'Customer RTXGC', N'Raghav, Amritansh', N'Owner', N'6789, rue des Bouchers', N'Marseille', NULL, N'10105', N'France', N'23.45.67.89', N'23.45.67.80');\nINSERT INTO \"Customer\"(\"userId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(1, N'Customer EEALV', N'Bassols, Pilar Colome', N'Accounting Manager', N'8901 Tsawassen Blvd.', N'Tsawassen', N'BC', N'10111', N'Canada', N'(604) 901-2345', N'(604) 678-9012');\nINSERT INTO \"Customer\"(\"userId\", \"companyName\", \"contactName\", \"contactTitle\", address, city, region, \"postalCode\", country, phone, fax)\n  VALUES(1, N'Customer UBHAU', N'Jaffe, David', N'Sales Representative', N'Fauntleroy Circus 4567', N'London', NULL, N'10064', N'UK', N'(171) 789-0123', NULL);\n\nINSERT INTO \"Shipper\"(\"shipperId\", \"companyName\", phone)\n  VALUES(1, N'\"Shipper\" GVSUA', N'(503) 555-0137');\nINSERT INTO \"Shipper\"(\"shipperId\", \"companyName\", phone)\n  VALUES(2, N'\"Shipper\" ETYNR', N'(425) 555-0136');\nINSERT INTO \"Shipper\"(\"shipperId\", \"companyName\", phone)\n  VALUES(3, N'\"Shipper\" ZHISN', N'(415) 555-0138');\n\n\nINSERT INTO \"SalesOrder\"(\"orderId\", \"custId\", \"employeeId\", \"orderDate\", \"requiredDate\", \"shippedDate\", \"shipperId\", freight, \"shipName\", \"shipAddress\", \"shipCity\", \"shipRegion\", \"shipPostalCode\", \"shipCountry\")\n  VALUES(1, 1, 5, '20060704 00:00:00.000', '20060801 00:00:00.000', '20060716 00:00:00.000', 3, 32.38, N'Ship to 85-B', N'6789 rue de l''Abbaye', N'Reims', NULL, N'10345', N'France');\nINSERT INTO \"SalesOrder\"(\"orderId\", \"custId\", \"employeeId\", \"orderDate\", \"requiredDate\", \"shippedDate\", \"shipperId\", freight, \"shipName\", \"shipAddress\", \"shipCity\", \"shipRegion\", \"shipPostalCode\", \"shipCountry\")\n  VALUES(2, 2, 6, '20060705 00:00:00.000', '20060816 00:00:00.000', '20060710 00:00:00.000', 1, 11.61, N'Ship to 79-C', N'Luisenstr. 9012', N'Münster', NULL, N'10328', N'Germany');\nINSERT INTO \"SalesOrder\"(\"orderId\", \"custId\", \"employeeId\", \"orderDate\", \"requiredDate\", \"shippedDate\", \"shipperId\", freight, \"shipName\", \"shipAddress\", \"shipCity\", \"shipRegion\", \"shipPostalCode\", \"shipCountry\")\n  VALUES(3, 3, 4, '20060708 00:00:00.000', '20060805 00:00:00.000', '20060712 00:00:00.000', 2, 65.83, N'Destination SCQXA', N'Rua do Paço, 7890', N'Rio de Janeiro', N'RJ', N'10195', N'Brazil');\nINSERT INTO \"SalesOrder\"(\"orderId\", \"custId\", \"employeeId\", \"orderDate\", \"requiredDate\", \"shippedDate\", \"shipperId\", freight, \"shipName\", \"shipAddress\", \"shipCity\", \"shipRegion\", \"shipPostalCode\", \"shipCountry\")\n  VALUES(4, 4, 3, '20060708 00:00:00.000', '20060805 00:00:00.000', '20060715 00:00:00.000', 1, 41.34, N'Ship to 84-A', N'3456, rue du Commerce', N'Lyon', NULL, N'10342', N'France');\nINSERT INTO \"SalesOrder\"(\"orderId\", \"custId\", \"employeeId\", \"orderDate\", \"requiredDate\", \"shippedDate\", \"shipperId\", freight, \"shipName\", \"shipAddress\", \"shipCity\", \"shipRegion\", \"shipPostalCode\", \"shipCountry\")\n  VALUES(5, 5, 4, '20060709 00:00:00.000', '20060806 00:00:00.000', '20060711 00:00:00.000', 2, 51.30, N'Ship to 76-B', N'Boulevard Tirou, 9012', N'Charleroi', NULL, N'10318', N'Belgium');\nINSERT INTO \"SalesOrder\"(\"orderId\", \"custId\", \"employeeId\", \"orderDate\", \"requiredDate\", \"shippedDate\", \"shipperId\", freight, \"shipName\", \"shipAddress\", \"shipCity\", \"shipRegion\", \"shipPostalCode\", \"shipCountry\")\n  VALUES(6, 6, 3, '20060710 00:00:00.000', '20060724 00:00:00.000', '20060716 00:00:00.000', 2, 58.17, N'Destination JPAIY', N'Rua do Paço, 8901', N'Rio de Janeiro', N'RJ', N'10196', N'Brazil');\nINSERT INTO \"SalesOrder\"(\"orderId\", \"custId\", \"employeeId\", \"orderDate\", \"requiredDate\", \"shippedDate\", \"shipperId\", freight, \"shipName\", \"shipAddress\", \"shipCity\", \"shipRegion\", \"shipPostalCode\", \"shipCountry\")\n  VALUES(7, 7, 5, '20060711 00:00:00.000', '20060808 00:00:00.000', '20060723 00:00:00.000', 2, 22.98, N'Destination YUJRD', N'Hauptstr. 1234', N'Bern', NULL, N'10139', N'Switzerland');\nINSERT INTO \"SalesOrder\"(\"orderId\", \"custId\", \"employeeId\", \"orderDate\", \"requiredDate\", \"shippedDate\", \"shipperId\", freight, \"shipName\", \"shipAddress\", \"shipCity\", \"shipRegion\", \"shipPostalCode\", \"shipCountry\")\n  VALUES(8, 8, 9, '20060712 00:00:00.000', '20060809 00:00:00.000', '20060715 00:00:00.000', 3, 148.33, N'Ship to 68-A', N'Starenweg 6789', N'Genève', NULL, N'10294', N'Switzerland');\nINSERT INTO \"SalesOrder\"(\"orderId\", \"custId\", \"employeeId\", \"orderDate\", \"requiredDate\", \"shippedDate\", \"shipperId\", freight, \"shipName\", \"shipAddress\", \"shipCity\", \"shipRegion\", \"shipPostalCode\", \"shipCountry\")\n  VALUES(9, 9, 3, '20060715 00:00:00.000', '20060812 00:00:00.000', '20060717 00:00:00.000', 2, 13.97, N'Ship to 88-B', N'Rua do Mercado, 5678', N'Resende', N'SP', N'10354', N'Brazil');\nINSERT INTO \"SalesOrder\"(\"orderId\", \"custId\", \"employeeId\", \"orderDate\", \"requiredDate\", \"shippedDate\", \"shipperId\", freight, \"shipName\", \"shipAddress\", \"shipCity\", \"shipRegion\", \"shipPostalCode\", \"shipCountry\")\n  VALUES(10, 1, 4, '20060716 00:00:00.000', '20060813 00:00:00.000', '20060722 00:00:00.000', 3, 81.91, N'Destination JYDLM', N'Carrera1234 con Ave. Carlos Soublette #8-35', N'San Cristóbal', N'Táchira', N'10199', N'Venezuela');\nINSERT INTO \"SalesOrder\"(\"orderId\", \"custId\", \"employeeId\", \"orderDate\", \"requiredDate\", \"shippedDate\", \"shipperId\", freight, \"shipName\", \"shipAddress\", \"shipCity\", \"shipRegion\", \"shipPostalCode\", \"shipCountry\")\n  VALUES(11, 2, 1, '20060717 00:00:00.000', '20060814 00:00:00.000', '20060723 00:00:00.000', 1, 140.51, N'Destination RVDMF', N'Kirchgasse 9012', N'Graz', NULL, N'10157', N'Austria');\nINSERT INTO \"SalesOrder\"(\"orderId\", \"custId\", \"employeeId\", \"orderDate\", \"requiredDate\", \"shippedDate\", \"shipperId\", freight, \"shipName\", \"shipAddress\", \"shipCity\", \"shipRegion\", \"shipPostalCode\", \"shipCountry\")\n  VALUES(12, 3, 4, '20060718 00:00:00.000', '20060815 00:00:00.000', '20060725 00:00:00.000', 3, 3.25, N'Destination LGGCH', N'Sierras de Granada 9012', N'México D.F.', NULL, N'10137', N'Mexico');\nINSERT INTO \"SalesOrder\"(\"orderId\", \"custId\", \"employeeId\", \"orderDate\", \"requiredDate\", \"shippedDate\", \"shipperId\", freight, \"shipName\", \"shipAddress\", \"shipCity\", \"shipRegion\", \"shipPostalCode\", \"shipCountry\")\n  VALUES(13, 4, 4, '20060719 00:00:00.000', '20060816 00:00:00.000', '20060729 00:00:00.000', 1, 55.09, N'Ship to 56-A', N'Mehrheimerstr. 0123', N'Köln', NULL, N'10258', N'Germany');\nINSERT INTO \"SalesOrder\"(\"orderId\", \"custId\", \"employeeId\", \"orderDate\", \"requiredDate\", \"shippedDate\", \"shipperId\", freight, \"shipName\", \"shipAddress\", \"shipCity\", \"shipRegion\", \"shipPostalCode\", \"shipCountry\")\n  VALUES(14, 5, 4, '20060719 00:00:00.000', '20060816 00:00:00.000', '20060730 00:00:00.000', 2, 3.05, N'Ship to 61-B', N'Rua da Panificadora, 6789', N'Rio de Janeiro', N'RJ', N'10274', N'Brazil');\nINSERT INTO \"SalesOrder\"(\"orderId\", \"custId\", \"employeeId\", \"orderDate\", \"requiredDate\", \"shippedDate\", \"shipperId\", freight, \"shipName\", \"shipAddress\", \"shipCity\", \"shipRegion\", \"shipPostalCode\", \"shipCountry\")\n  VALUES(15, 6, 8, '20060722 00:00:00.000', '20060819 00:00:00.000', '20060725 00:00:00.000', 3, 48.29, N'Ship to 65-B', N'8901 Milton Dr.', N'Albuquerque', N'NM', N'10286', N'USA');\nINSERT INTO \"SalesOrder\"(\"orderId\", \"custId\", \"employeeId\", \"orderDate\", \"requiredDate\", \"shippedDate\", \"shipperId\", freight, \"shipName\", \"shipAddress\", \"shipCity\", \"shipRegion\", \"shipPostalCode\", \"shipCountry\")\n  VALUES(16, 7, 9, '20060723 00:00:00.000', '20060820 00:00:00.000', '20060731 00:00:00.000', 3, 146.06, N'Destination FFXKT', N'Kirchgasse 0123', N'Graz', NULL, N'10158', N'Austria');\nINSERT INTO \"SalesOrder\"(\"orderId\", \"custId\", \"employeeId\", \"orderDate\", \"requiredDate\", \"shippedDate\", \"shipperId\", freight, \"shipName\", \"shipAddress\", \"shipCity\", \"shipRegion\", \"shipPostalCode\", \"shipCountry\")\n  VALUES(17, 8, 6, '20060724 00:00:00.000', '20060821 00:00:00.000', '20060823 00:00:00.000', 3, 3.67, N'Destination KBSBN', N'Åkergatan 9012', N'Bräcke', NULL, N'10167', N'Sweden');\nINSERT INTO \"SalesOrder\"(\"orderId\", \"custId\", \"employeeId\", \"orderDate\", \"requiredDate\", \"shippedDate\", \"shipperId\", freight, \"shipName\", \"shipAddress\", \"shipCity\", \"shipRegion\", \"shipPostalCode\", \"shipCountry\")\n  VALUES(18, 7, 2, '20060725 00:00:00.000', '20060822 00:00:00.000', '20060812 00:00:00.000', 1, 55.28, N'Ship to 7-A', N'0123, place Kléber', N'Strasbourg', NULL, N'10329', N'France');\nINSERT INTO \"SalesOrder\"(\"orderId\", \"custId\", \"employeeId\", \"orderDate\", \"requiredDate\", \"shippedDate\", \"shipperId\", freight, \"shipName\", \"shipAddress\", \"shipCity\", \"shipRegion\", \"shipPostalCode\", \"shipCountry\")\n  VALUES(19, 9, 3, '20060726 00:00:00.000', '20060906 00:00:00.000', '20060731 00:00:00.000', 3, 25.73, N'Ship to 87-B', N'Torikatu 2345', N'Oulu', NULL, N'10351', N'Finland');\nINSERT INTO \"SalesOrder\"(\"orderId\", \"custId\", \"employeeId\", \"orderDate\", \"requiredDate\", \"shippedDate\", \"shipperId\", freight, \"shipName\", \"shipAddress\", \"shipCity\", \"shipRegion\", \"shipPostalCode\", \"shipCountry\")\n  VALUES(20, 1, 4, '20060729 00:00:00.000', '20060826 00:00:00.000', '20060806 00:00:00.000', 1, 208.58, N'Destination VAPXU', N'Berliner Platz 0123', N'München', NULL, N'10168', N'Germany');\n"
  },
  {
    "path": "demo/databases/sqlite.sql",
    "content": "-- Sqlite SQL script \n\nPRAGMA encoding=\"UTF-8\";\n\n\nDROP TABLE IF EXISTS Customer;\n\nCREATE TABLE Customer (\n  entityId INTEGER PRIMARY KEY AUTOINCREMENT,\n  companyName VARCHAR(40) NOT NULL,\n  contactName VARCHAR(30) NULL,\n  contactTitle VARCHAR(30) NULL,\n  address VARCHAR(60) NULL,\n  city VARCHAR(15) NULL,\n  region VARCHAR(15) NULL,\n  postalCode VARCHAR(10) NULL,\n  country VARCHAR(15) NULL,\n  phone VARCHAR(24) NULL,\n  mobile VARCHAR(24) NULL,\n  email VARCHAR(225) NULL,\n  fax VARCHAR(24) NULL\n  );\n\n\nDROP TABLE IF EXISTS Employee;\n\nCREATE TABLE Employee (\n  entityId INTEGER PRIMARY KEY AUTOINCREMENT,\n  lastname VARCHAR(20) NOT NULL,\n  firstname VARCHAR(10) NOT NULL,\n  title VARCHAR(30) NULL,\n  titleOfCourtesy VARCHAR(25) NULL,\n  birthDate DATETIME NULL,\n  hireDate DATETIME NULL,\n  address VARCHAR(60) NULL,\n  city VARCHAR(15) NULL,\n  region VARCHAR(15) NULL,\n  postalCode VARCHAR(10) NULL,\n  country VARCHAR(15) NULL,\n  phone VARCHAR(24) NULL,\n  extension VARCHAR(4) NULL,\n  mobile VARCHAR(24) NULL,\n  email VARCHAR(225) NULL,\n  photo BLOB NULL,\n  notes BLOB NULL,\n  mgrId INT NULL,\n  photoPath VARCHAR(255) NULL\n  );\n\n\nCREATE TABLE Shipper (\n  entityId INTEGER PRIMARY KEY AUTOINCREMENT,\n  companyName VARCHAR(40) NOT NULL,\n  phone VARCHAR(44) NULL\n  );\n\n\n\n\nCREATE TABLE SalesOrder (\n  entityId INTEGER PRIMARY KEY AUTOINCREMENT,\n  customerId INT NOT NULL,\n  employeeId INT NULL,\n  orderDate DATETIME NULL,\n  requiredDate DATETIME NULL,\n  shippedDate DATETIME NULL,\n  shipperId INT NOT NULL,\n  freight DECIMAL(10, 2) NULL,\n  shipName VARCHAR(40) NULL,\n  shipAddress VARCHAR(60) NULL,\n  shipCity VARCHAR(15) NULL,\n  shipRegion VARCHAR(15) NULL,\n  shipPostalCode VARCHAR(10) NULL,\n  shipCountry VARCHAR(15) NULL,\n  FOREIGN KEY (shipperId) REFERENCES Shipper(entityId),\n  FOREIGN KEY (customerId) REFERENCES Customer(entityId) \n\n  );\n\n\n\n\n-- Indexing & Foreign Key\n\nCREATE UNIQUE INDEX IF NOT EXISTS IDX_CustomerId_CustomerTypeId ON CustomerCustomerDemographics (customerId, customerTypeId);\n\nCREATE UNIQUE INDEX IF NOT EXISTS IDX_EmployeeId_TerritoryCode ON EmployeeTerritory (employeeId, territoryCode);\n\nCREATE UNIQUE INDEX IF NOT EXISTS IDX_OrderId_ProductId ON OrderDetail (orderId, productId);\n\n\n\n-- Populate Employess table\n\nINSERT INTO Employee(entityId, lastname, firstname, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalcode, country, phone, mgrid)\n  VALUES(1, 'Davis', 'Sara', 'CEO', 'Ms.', '1958-12-08 00:00:00.000', '2002-05-01 00:00:00.000', '7890 - 20th Ave. E., Apt. 2A', 'Seattle', 'WA', '10003', 'USA', '(206) 555-0101', NULL);\nINSERT INTO Employee(entityid, lastname, firstname, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalcode, country, phone, mgrid)\n  VALUES(2, 'Funk', 'Don', 'Vice President, Sales', 'Dr.', '1962-02-19 00:00:00.000', '2002-08-14 00:00:00.000', '9012 W. Capital Way', 'Tacoma', 'WA', '10001', 'USA', '(206) 555-0100', 1);\nINSERT INTO Employee(entityid, lastname, firstname, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalcode, country, phone, mgrid)\n  VALUES(3, 'Lew', 'Judy', 'Sales Manager', 'Ms.', '1973-08-30 00:00:00.000', '2002-04-01 00:00:00.000', '2345 Moss Bay Blvd.', 'Kirkland', 'WA', '10007', 'USA', '(206) 555-0103', 2);\nINSERT INTO Employee(entityid, lastname, firstname, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalcode, country, phone, mgrid)\n  VALUES(4, 'Peled', 'Yael', 'Sales Representative', 'Mrs.', '1947-09-19 00:00:00.000', '2003-05-03 00:00:00.000', '5678 Old Redmond Rd.', 'Redmond', 'WA', '10009', 'USA', '(206) 555-0104', 3);\nINSERT INTO Employee(entityid, lastname, firstname, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalcode, country, phone, mgrid)\n  VALUES(5, 'Buck', 'Sven', 'Sales Manager', 'Mr.', '1965-03-04 00:00:00.000', '2003-10-17 00:00:00.000', '8901 Garrett Hill', 'London', NULL, '10004', 'UK', '(71) 234-5678', 2);\nINSERT INTO Employee(entityid, lastname, firstname, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalcode, country, phone, mgrid)\n  VALUES(6, 'Suurs', 'Paul', 'Sales Representative', 'Mr.', '1973-07-02 00:00:00.000', '2003-10-17 00:00:00.000', '3456 Coventry House, Miner Rd.', 'London', NULL, '10005', 'UK', '(71) 345-6789', 5);\nINSERT INTO Employee(entityid, lastname, firstname, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalcode, country, phone, mgrid)\n  VALUES(7, 'King', 'Russell', 'Sales Representative', 'Mr.', '1970-05-29 00:00:00.000', '2004-01-02 00:00:00.000', '6789 Edgeham Hollow, Winchester Way', 'London', NULL, '10002', 'UK', '(71) 123-4567', 5);\nINSERT INTO Employee(entityid, lastname, firstname, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalcode, country, phone, mgrid)\n  VALUES(8, 'Cameron', 'Maria', 'Sales Representative', 'Ms.', '1968-01-09 00:00:00.000', '2004-03-05 00:00:00.000', '4567 - 11th Ave. N.E.', 'Seattle', 'WA', '10006', 'USA', '(206) 555-0102', 3);\nINSERT INTO Employee(entityid, lastname, firstname, title, titleofcourtesy, birthdate, hiredate, address, city, region, postalcode, country, phone, mgrid)\n  VALUES(9, 'Dolgopyatova', 'Zoya', 'Sales Representative', 'Ms.', '1976-01-27 00:00:00.000', '2004-11-15 00:00:00.000', '1234 Houndstooth Rd.', 'London', NULL, '10008', 'UK', '(71) 456-7890', 5);\n\n-- ---  \n\n\n\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(1, 'Customer NRZBB', 'Allen, Michael', 'Sales Representative', 'Obere Str. 0123', 'Berlin', NULL, '10092', 'Germany', '030-3456789', '030-0123456');\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(2, 'Customer MLTDN', 'Hassall, Mark', 'Owner', 'Avda. de la Constitución 5678', 'México D.F.', NULL, '10077', 'Mexico', '(5) 789-0123', '(5) 456-7890');\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(3, 'Customer KBUDE', 'Peoples, John', 'Owner', 'Mataderos  7890', 'México D.F.', NULL, '10097', 'Mexico', '(5) 123-4567', NULL);\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(4, 'Customer HFBZG', 'Arndt, Torsten', 'Sales Representative', '7890 Hanover Sq.', 'London', NULL, '10046', 'UK', '(171) 456-7890', '(171) 456-7891');\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(5, 'Customer HGVLZ', 'Higginbotham, Tom', 'Order Administrator', 'Berguvsvägen  5678', 'Luleå', NULL, '10112', 'Sweden', '0921-67 89 01', '0921-23 45 67');\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(6, 'Customer XHXJV', 'Poland, Carole', 'Sales Representative', 'Forsterstr. 7890', 'Mannheim', NULL, '10117', 'Germany', '0621-67890', '0621-12345');\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(7, 'Customer QXVLA', 'Bansal, Dushyant', 'Marketing Manager', '2345, place Kléber', 'Strasbourg', NULL, '10089', 'France', '67.89.01.23', '67.89.01.24');\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(8, 'Customer QUHWH', 'Ilyina, Julia', 'Owner', 'C/ Araquil, 0123', 'Madrid', NULL, '10104', 'Spain', '(91) 345 67 89', '(91) 012 34 56');\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(9, 'Customer RTXGC', 'Raghav, Amritansh', 'Owner', '6789, rue des Bouchers', 'Marseille', NULL, '10105', 'France', '23.45.67.89', '23.45.67.80');\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(10, 'Customer EEALV', 'Bassols, Pilar Colome', 'Accounting Manager', '8901 Tsawassen Blvd.', 'Tsawassen', 'BC', '10111', 'Canada', '(604) 901-2345', '(604) 678-9012');\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(11, 'Customer UBHAU', 'Jaffe, David', 'Sales Representative', 'Fauntleroy Circus 4567', 'London', NULL, '10064', 'UK', '(171) 789-0123', NULL);\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(12, 'Customer PSNMQ', 'Ray, Mike', 'Sales Agent', 'Cerrito 3456', 'Buenos Aires', NULL, '10057', 'Argentina', '(1) 890-1234', '(1) 567-8901');\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(13, 'Customer VMLOG', 'Benito, Almudena', 'Marketing Manager', 'Sierras de Granada 7890', 'México D.F.', NULL, '10056', 'Mexico', '(5) 456-7890', '(5) 123-4567');\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(14, 'Customer WNMAF', 'Jelitto, Jacek', 'Owner', 'Hauptstr. 0123', 'Bern', NULL, '10065', 'Switzerland', '0452-678901', NULL);\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(15, 'Customer JUWXK', 'Richardson, Shawn', 'Sales Associate', 'Av. dos Lusíadas, 6789', 'Sao Paulo', 'SP', '10087', 'Brazil', '(11) 012-3456', NULL);\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(16, 'Customer GYBBY', 'Birkby, Dana', 'Sales Representative', 'Berkeley Gardens 0123 Brewery', 'London', NULL, '10039', 'UK', '(171) 234-5678', '(171) 234-5679');\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(17, 'Customer FEVNN', 'Jones, TiAnna', 'Order Administrator', 'Walserweg 4567', 'Aachen', NULL, '10067', 'Germany', '0241-789012', '0241-345678');\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(18, 'Customer BSVAR', 'Rizaldy, Arif', 'Owner', '3456, rue des Cinquante Otages', 'Nantes', NULL, '10041', 'France', '89.01.23.45', '89.01.23.46');\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(19, 'Customer RFNQC', 'Boseman, Randall', 'Sales Agent', '5678 King George', 'London', NULL, '10110', 'UK', '(171) 345-6789', '(171) 345-6780');\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(20, 'Customer THHDP', 'Kane, John', 'Sales Manager', 'Kirchgasse 9012', 'Graz', NULL, '10059', 'Austria', '1234-5678', '9012-3456');\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(21, 'Customer KIdPX', 'Russo, Giuseppe', 'Marketing Assistant', 'Rua Orós, 3456', 'Sao Paulo', 'SP', '10096', 'Brazil', '(11) 456-7890', NULL);\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(22, 'Customer DTDMN', 'Bueno, Janaina Burdan, Neville', 'Accounting Manager', 'C/ Moralzarzal, 5678', 'Madrid', NULL, '10080', 'Spain', '(91) 890 12 34', '(91) 567 89 01');\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(23, 'Customer WVFAF', 'Khanna, Karan', 'Assistant Sales Agent', '4567, chaussée de Tournai', 'Lille', NULL, '10048', 'France', '45.67.89.01', '45.67.89.02');\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(24, 'Customer CYZTN', 'San Juan, Patricia', 'Owner', 'Åkergatan 5678', 'Bräcke', NULL, '10114', 'Sweden', '0695-67 89 01', NULL);\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(25, 'Customer AZJED', 'Carlson, Jason', 'Marketing Manager', 'Berliner Platz 9012', 'München', NULL, '10091', 'Germany', '089-8901234', '089-5678901');\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(26, 'Customer USDBG', 'Koch, Paul', 'Marketing Manager', '9012, rue Royale', 'Nantes', NULL, '10101', 'France', '34.56.78.90', '34.56.78.91');\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(27, 'Customer WMFEA', 'Schmöllerl, Martin', 'Sales Representative', 'Via Monte Bianco 4567', 'Torino', NULL, '10099', 'Italy', '011-2345678', '011-9012345');\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(28, 'Customer XYUFB', 'Cavaglieri, Giorgio', 'Sales Manager', 'Jardim das rosas n. 8901', 'Lisboa', NULL, '10054', 'Portugal', '(1) 456-7890', '(1) 123-4567');\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(29, 'Customer MDLWA', 'Kolesnikova, Katerina', 'Marketing Manager', 'Rambla de Cataluña, 8901', 'Barcelona', NULL, '10081', 'Spain', '(93) 789 0123', '(93) 456 7890');\nINSERT INTO Customer(entityid, companyname, contactname, contacttitle, address, city, region, postalcode, country, phone, fax)\n  VALUES(30, 'Customer KSLQF', 'Shabalin, Rostislav', 'Sales Manager', 'C/ Romero, 1234', 'Sevilla', NULL, '10075', 'Spain', '(95) 901 23 45', NULL);\n\n-- Shipper \n\nINSERT INTO Shipper(entityid, companyname, phone)\n  VALUES(1, 'Shipper GVSUA', '(503) 555-0137');\nINSERT INTO Shipper(entityid, companyname, phone)\n  VALUES(2, 'Shipper ETYNR', '(425) 555-0136');\nINSERT INTO Shipper(entityid, companyname, phone)\n  VALUES(3, 'Shipper ZHISN', '(415) 555-0138');\n\n\n\n-- Sales Order\n\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10248, 85, 5, '2006-07-04 00:00:00.000', '2006-08-01 00:00:00.000', '2006-07-16 00:00:00.000', 3, 32.38, 'Ship to 85-B', '6789 rue de l''Abbaye', 'Reims', NULL, '10345', 'France');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10249, 79, 6, '2006-07-05 00:00:00.000', '2006-08-16 00:00:00.000', '2006-07-10 00:00:00.000', 1, 11.61, 'Ship to 79-C', 'Luisenstr. 9012', 'Münster', NULL, '10328', 'Germany');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10250, 34, 4, '2006-07-08 00:00:00.000', '2006-08-05 00:00:00.000', '2006-07-12 00:00:00.000', 2, 65.83, 'Destination SCQXA', 'Rua do Paço, 7890', 'Rio de Janeiro', 'RJ', '10195', 'Brazil');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10251, 84, 3, '2006-07-08 00:00:00.000', '2006-08-05 00:00:00.000', '2006-07-15 00:00:00.000', 1, 41.34, 'Ship to 84-A', '3456, rue du Commerce', 'Lyon', NULL, '10342', 'France');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10252, 76, 4, '2006-07-09 00:00:00.000', '2006-08-06 00:00:00.000', '2006-07-11 00:00:00.000', 2, 51.30, 'Ship to 76-B', 'Boulevard Tirou, 9012', 'Charleroi', NULL, '10318', 'Belgium');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10253, 34, 3, '2006-07-10 00:00:00.000', '2006-07-24 00:00:00.000', '2006-07-16 00:00:00.000', 2, 58.17, 'Destination JPAIY', 'Rua do Paço, 8901', 'Rio de Janeiro', 'RJ', '10196', 'Brazil');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10254, 14, 5, '2006-07-11 00:00:00.000', '2006-08-08 00:00:00.000', '2006-07-23 00:00:00.000', 2, 22.98, 'Destination YUJRD', 'Hauptstr. 1234', 'Bern', NULL, '10139', 'Switzerland');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10255, 68, 9, '2006-07-12 00:00:00.000', '2006-08-09 00:00:00.000', '2006-07-15 00:00:00.000', 3, 148.33, 'Ship to 68-A', 'Starenweg 6789', 'Genève', NULL, '10294', 'Switzerland');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10256, 88, 3, '2006-07-15 00:00:00.000', '2006-08-12 00:00:00.000', '2006-07-17 00:00:00.000', 2, 13.97, 'Ship to 88-B', 'Rua do Mercado, 5678', 'Resende', 'SP', '10354', 'Brazil');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10257, 35, 4, '2006-07-16 00:00:00.000', '2006-08-13 00:00:00.000', '2006-07-22 00:00:00.000', 3, 81.91, 'Destination JYDLM', 'Carrera1234 con Ave. Carlos Soublette #8-35', 'San Cristóbal', 'Táchira', '10199', 'Venezuela');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10258, 20, 1, '2006-07-17 00:00:00.000', '2006-08-14 00:00:00.000', '2006-07-23 00:00:00.000', 1, 140.51, 'Destination RVDMF', 'Kirchgasse 9012', 'Graz', NULL, '10157', 'Austria');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10259, 13, 4, '2006-07-18 00:00:00.000', '2006-08-15 00:00:00.000', '2006-07-25 00:00:00.000', 3, 3.25, 'Destination LGGCH', 'Sierras de Granada 9012', 'México D.F.', NULL, '10137', 'Mexico');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10260, 56, 4, '2006-07-19 00:00:00.000', '2006-08-16 00:00:00.000', '2006-07-29 00:00:00.000', 1, 55.09, 'Ship to 56-A', 'Mehrheimerstr. 0123', 'Köln', NULL, '10258', 'Germany');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10261, 61, 4, '2006-07-19 00:00:00.000', '2006-08-16 00:00:00.000', '2006-07-30 00:00:00.000', 2, 3.05, 'Ship to 61-B', 'Rua da Panificadora, 6789', 'Rio de Janeiro', 'RJ', '10274', 'Brazil');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10262, 65, 8, '2006-07-22 00:00:00.000', '2006-08-19 00:00:00.000', '2006-07-25 00:00:00.000', 3, 48.29, 'Ship to 65-B', '8901 Milton Dr.', 'Albuquerque', 'NM', '10286', 'USA');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10263, 20, 9, '2006-07-23 00:00:00.000', '2006-08-20 00:00:00.000', '2006-07-31 00:00:00.000', 3, 146.06, 'Destination FFXKT', 'Kirchgasse 0123', 'Graz', NULL, '10158', 'Austria');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10264, 24, 6, '2006-07-24 00:00:00.000', '2006-08-21 00:00:00.000', '2006-08-23 00:00:00.000', 3, 3.67, 'Destination KBSBN', 'Åkergatan 9012', 'Bräcke', NULL, '10167', 'Sweden');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10265, 7, 2, '2006-07-25 00:00:00.000', '2006-08-22 00:00:00.000', '2006-08-12 00:00:00.000', 1, 55.28, 'Ship to 7-A', '0123, place Kléber', 'Strasbourg', NULL, '10329', 'France');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10266, 87, 3, '2006-07-26 00:00:00.000', '2006-09-06 00:00:00.000', '2006-07-31 00:00:00.000', 3, 25.73, 'Ship to 87-B', 'Torikatu 2345', 'Oulu', NULL, '10351', 'Finland');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10267, 25, 4, '2006-07-29 00:00:00.000', '2006-08-26 00:00:00.000', '2006-08-06 00:00:00.000', 1, 208.58, 'Destination VAPXU', 'Berliner Platz 0123', 'München', NULL, '10168', 'Germany');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10268, 33, 8, '2006-07-30 00:00:00.000', '2006-08-27 00:00:00.000', '2006-08-02 00:00:00.000', 3, 66.29, 'Destination QJVQH', '5ª Ave. Los Palos Grandes 5678', 'Caracas', 'DF', '10193', 'Venezuela');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10269, 89, 5, '2006-07-31 00:00:00.000', '2006-08-14 00:00:00.000', '2006-08-09 00:00:00.000', 1, 4.56, 'Ship to 89-B', '8901 - 12th Ave. S.', 'Seattle', 'WA', '10357', 'USA');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10270, 87, 1, '2006-08-01 00:00:00.000', '2006-08-29 00:00:00.000', '2006-08-02 00:00:00.000', 1, 136.54, 'Ship to 87-B', 'Torikatu 2345', 'Oulu', NULL, '10351', 'Finland');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10271, 75, 6, '2006-08-01 00:00:00.000', '2006-08-29 00:00:00.000', '2006-08-30 00:00:00.000', 2, 4.54, 'Ship to 75-C', 'P.O. Box 7890', 'Lander', 'WY', '10316', 'USA');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10272, 65, 6, '2006-08-02 00:00:00.000', '2006-08-30 00:00:00.000', '2006-08-06 00:00:00.000', 2, 98.03, 'Ship to 65-A', '7890 Milton Dr.', 'Albuquerque', 'NM', '10285', 'USA');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10273, 63, 3, '2006-08-05 00:00:00.000', '2006-09-02 00:00:00.000', '2006-08-12 00:00:00.000', 3, 76.07, 'Ship to 63-A', 'Taucherstraße 1234', 'Cunewalde', NULL, '10279', 'Germany');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10274, 85, 6, '2006-08-06 00:00:00.000', '2006-09-03 00:00:00.000', '2006-08-16 00:00:00.000', 1, 6.01, 'Ship to 85-B', '6789 rue de l''Abbaye', 'Reims', NULL, '10345', 'France');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10275, 49, 1, '2006-08-07 00:00:00.000', '2006-09-04 00:00:00.000', '2006-08-09 00:00:00.000', 1, 26.93, 'Ship to 49-A', 'Via Ludovico il Moro 8901', 'Bergamo', NULL, '10235', 'Italy');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10276, 80, 8, '2006-08-08 00:00:00.000', '2006-08-22 00:00:00.000', '2006-08-14 00:00:00.000', 3, 13.84, 'Ship to 80-C', 'Avda. Azteca 5678', 'México D.F.', NULL, '10334', 'Mexico');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10277, 52, 2, '2006-08-09 00:00:00.000', '2006-09-06 00:00:00.000', '2006-08-13 00:00:00.000', 3, 125.77, 'Ship to 52-A', 'Heerstr. 9012', 'Leipzig', NULL, '10247', 'Germany');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10278, 5, 8, '2006-08-12 00:00:00.000', '2006-09-09 00:00:00.000', '2006-08-16 00:00:00.000', 2, 92.69, 'Ship to 5-C', 'Berguvsvägen  1234', 'Luleå', NULL, '10269', 'Sweden');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10279, 44, 8, '2006-08-13 00:00:00.000', '2006-09-10 00:00:00.000', '2006-08-16 00:00:00.000', 2, 25.83, 'Ship to 44-A', 'Magazinweg 4567', 'Frankfurt a.M.', NULL, '10222', 'Germany');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10280, 5, 2, '2006-08-14 00:00:00.000', '2006-09-11 00:00:00.000', '2006-09-12 00:00:00.000', 1, 8.98, 'Ship to 5-B', 'Berguvsvägen  0123', 'Luleå', NULL, '10268', 'Sweden');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10281, 69, 4, '2006-08-14 00:00:00.000', '2006-08-28 00:00:00.000', '2006-08-21 00:00:00.000', 1, 2.94, 'Ship to 69-A', 'Gran Vía, 9012', 'Madrid', NULL, '10297', 'Spain');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10282, 69, 4, '2006-08-15 00:00:00.000', '2006-09-12 00:00:00.000', '2006-08-21 00:00:00.000', 1, 12.69, 'Ship to 69-B', 'Gran Vía, 0123', 'Madrid', NULL, '10298', 'Spain');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10283, 46, 3, '2006-08-16 00:00:00.000', '2006-09-13 00:00:00.000', '2006-08-23 00:00:00.000', 3, 84.81, 'Ship to 46-A', 'Carrera 0123 con Ave. Bolívar #65-98 Llano Largo', 'Barquisimeto', 'Lara', '10227', 'Venezuela');\nINSERT INTO SalesOrder(entityid, customerid, employeeid, orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10284, 44, 4, '2006-08-19 00:00:00.000', '2006-09-16 00:00:00.000', '2006-08-27 00:00:00.000', 1, 76.56, 'Ship to 44-A', 'Magazinweg 4567', 'Frankfurt a.M.', NULL, '10222', 'Germany');\nINSERT INTO SalesOrder(entityid, customerid, employeeid,orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10285, 63, 1, '2006-08-20 00:00:00.000', '2006-09-17 00:00:00.000', '2006-08-26 00:00:00.000', 2, 76.83, 'Ship to 63-B', 'Taucherstraße 2345', 'Cunewalde', NULL, '10280', 'Germany');\nINSERT INTO SalesOrder(entityid, customerid, employeeid,orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10286, 63, 8, '2006-08-21 00:00:00.000', '2006-09-18 00:00:00.000', '2006-08-30 00:00:00.000', 3, 229.24, 'Ship to 63-B', 'Taucherstraße 2345', 'Cunewalde', NULL, '10280', 'Germany');\nINSERT INTO SalesOrder(entityid, customerid, employeeid,orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10287, 67, 8, '2006-08-22 00:00:00.000', '2006-09-19 00:00:00.000', '2006-08-28 00:00:00.000', 3, 12.76, 'Ship to 67-A', 'Av. Copacabana, 3456', 'Rio de Janeiro', 'RJ', '10291', 'Brazil');\nINSERT INTO SalesOrder(entityid, customerid, employeeid,orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10288, 66, 4, '2006-08-23 00:00:00.000', '2006-09-20 00:00:00.000', '2006-09-03 00:00:00.000', 1, 7.45, 'Ship to 66-C', 'Strada Provinciale 2345', 'Reggio Emilia', NULL, '10290', 'Italy');\nINSERT INTO SalesOrder(entityid, customerid, employeeid,orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10289, 11, 7, '2006-08-26 00:00:00.000', '2006-09-23 00:00:00.000', '2006-08-28 00:00:00.000', 3, 22.77, 'Destination DLEUN', 'Fauntleroy Circus 4567', 'London', NULL, '10132', 'UK');\nINSERT INTO SalesOrder(entityid, customerid, employeeid,orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10290, 15, 8, '2006-08-27 00:00:00.000', '2006-09-24 00:00:00.000', '2006-09-03 00:00:00.000', 1, 79.70, 'Destination HQZHO', 'Av. dos Lusíadas, 4567', 'Sao Paulo', 'SP', '10142', 'Brazil');\nINSERT INTO SalesOrder(entityid, customerid, employeeid,orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10291, 61, 6, '2006-08-27 00:00:00.000', '2006-09-24 00:00:00.000', '2006-09-04 00:00:00.000', 2, 6.40, 'Ship to 61-A', 'Rua da Panificadora, 5678', 'Rio de Janeiro', 'RJ', '10273', 'Brazil');\nINSERT INTO SalesOrder(entityid, customerid, employeeid,orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10292, 81, 1, '2006-08-28 00:00:00.000', '2006-09-25 00:00:00.000', '2006-09-02 00:00:00.000', 2, 1.35, 'Ship to 81-A', 'Av. Inês de Castro, 6789', 'Sao Paulo', 'SP', '10335', 'Brazil');\nINSERT INTO SalesOrder(entityid, customerid, employeeid,orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10293, 80, 1, '2006-08-29 00:00:00.000', '2006-09-26 00:00:00.000', '2006-09-11 00:00:00.000', 3, 21.18, 'Ship to 80-B', 'Avda. Azteca 4567', 'México D.F.', NULL, '10333', 'Mexico');\nINSERT INTO SalesOrder(entityid, customerid, employeeid,orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10294, 65, 4, '2006-08-30 00:00:00.000', '2006-09-27 00:00:00.000', '2006-09-05 00:00:00.000', 2, 147.26, 'Ship to 65-A', '7890 Milton Dr.', 'Albuquerque', 'NM', '10285', 'USA');\nINSERT INTO SalesOrder(entityid, customerid, employeeid,orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10295, 85, 2, '2006-09-02 00:00:00.000', '2006-09-30 00:00:00.000', '2006-09-10 00:00:00.000', 2, 1.15, 'Ship to 85-C', '7890 rue de l''Abbaye', 'Reims', NULL, '10346', 'France');\nINSERT INTO SalesOrder(entityid, customerid, employeeid,orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10296, 46, 6, '2006-09-03 00:00:00.000', '2006-10-01 00:00:00.000', '2006-09-11 00:00:00.000', 1, 0.12, 'Ship to 46-C', 'Carrera 2345 con Ave. Bolívar #65-98 Llano Largo', 'Barquisimeto', 'Lara', '10229', 'Venezuela');\nINSERT INTO SalesOrder(entityid, customerid, employeeid,orderdate, requireddate, shippeddate, shipperid, freight, shipname, shipaddress, shipcity, shipregion, shippostalcode, shipcountry)\n  VALUES(10297, 7, 5, '2006-09-04 00:00:00.000', '2006-10-16 00:00:00.000', '2006-09-10 00:00:00.000', 2, 5.74, 'Ship to 7-C', '2345, place Kléber', 'Strasbourg', NULL, '10331', 'France');\n"
  },
  {
    "path": "docker/docker-compose.dev.yml",
    "content": "name: llana\n\nnetworks:\n    llana-network:\n        driver: bridge\n        name: llana-network\n\nvolumes:\n    llana-mysql-data:\n        name: llana-mysql-data\n        driver: local\n    llana-postgres-data:\n        name: llana-postgres-data\n        driver: local\n    llana-mongodb-data:\n        name: llana-mongodb-data\n        driver: local\n    llana-mssql-data:\n        name: llana-mssql-data\n        driver: local\n    llana-redis-cache: #To be used for caching not as a data source\n        name: llana-redis-cache\n        driver: local\n\nservices:\n    llana-mysql:\n        image: mysql\n        restart: always\n        container_name: llana-mysql\n        ports:\n            - '3306:3306'\n        environment:\n            MYSQL_ROOT_PASSWORD: pass\n            MYSQL_USER: user\n            MYSQL_PASSWORD: pass\n            MYSQL_DATABASE: llana\n            MYSQL_ROOT_HOST: '%'\n        command: ['--init-file', '/docker-entrypoint-initdb.d/init.sql']\n        volumes:\n            - llana-mysql-data:/var/lib/mysql\n            - ../demo/databases/mysql.sql:/docker-entrypoint-initdb.d/init.sql\n        networks:\n            - llana-network\n        healthcheck: \n            test: [\"CMD\", \"mysqladmin\" ,\"ping\", \"-h\", \"localhost\"]\n            interval: 10s\n            timeout: 3s\n            retries: 10\n            start_period: 10s\n\n    llana-postgres:\n        image: postgres\n        restart: always\n        container_name: llana-postgres\n        ports:\n            - '5432:5432'\n        environment:\n            POSTGRES_DB: llana\n            POSTGRES_USER: user\n            POSTGRES_PASSWORD: pass\n            ALLOW_IP_RANGE: '0.0.0.0/0'\n        volumes:\n            - llana-postgres-data:/var/lib/postgresql/data/\n            - ../demo/databases/postgres.sql:/docker-entrypoint-initdb.d/init.sql\n        networks:\n            - llana-network\n\n    llana-mongodb:\n        image: mongo\n        restart: always\n        container_name: llana-mongodb\n        ports:\n            - '27017:27017'\n        environment:\n            MONGO_INITDB_ROOT_USERNAME: user\n            MONGO_INITDB_ROOT_PASSWORD: pass\n            MONGO_INITDB_DATABASE: llana\n        volumes:\n            - llana-mongodb-data:/data/db\n            - ../demo/databases/mongodb.js:/docker-entrypoint-initdb.d/seed.js\n        networks:\n            - llana-network\n\n    llana-mssql:\n        image: mcr.microsoft.com/mssql/server:2022-latest\n        restart: always\n        container_name: llana-mssql\n        ports:\n            - '1433:1433'\n        environment:\n            ACCEPT_EULA: Y\n            MSSQL_SA_PASSWORD: 'S7!0nGpAw0rD'\n        volumes:\n            - llana-mssql-data:/var/opt/mssql\n            - ../demo/databases/mssql.sql:/docker-entrypoint-initdb.d/mssql.sql\n        networks:\n            - llana-network\n        healthcheck:\n            test: /opt/mssql-tools18/bin/sqlcmd  -C -S localhost -U sa -P \"$$MSSQL_SA_PASSWORD\" -Q \"SELECT 1\" -b -o /dev/null\n            interval: 10s\n            timeout: 3s\n            retries: 10\n            start_period: 10s\n        command:\n            - /bin/bash\n            - -c\n            - |\n                /opt/mssql/bin/sqlservr &\n                pid=$$!\n\n                echo \"Waiting for MS SQL to be available ⏳\"\n\n                /opt/mssql-tools18/bin/sqlcmd -C  -l 30 -S localhost -h-1 -V1 -U sa -P $$MSSQL_SA_PASSWORD -Q \"SET NOCOUNT ON SELECT \\\"YAY WE ARE UP\\\" , @@servername\"\n                is_up=$$?\n                while [ $$is_up -ne 0 ] ; do\n                echo -e $$(date)\n                /opt/mssql-tools18/bin/sqlcmd -C -l 30 -S localhost -h-1 -V1 -U sa -P $$MSSQL_SA_PASSWORD -Q \"SET NOCOUNT ON SELECT \\\"YAY WE ARE UP\\\" , @@servername\"\n                is_up=$$?\n                sleep 5\n                done\n\n                echo \"MS SQL is up and running 🚀\"\n\n\n                /opt/mssql-tools18/bin/sqlcmd -C -U sa -P $$MSSQL_SA_PASSWORD -Q \"CREATE DATABASE llana;\"\n                /opt/mssql-tools18/bin/sqlcmd -C -U sa -P $$MSSQL_SA_PASSWORD -l 30 -e -i /docker-entrypoint-initdb.d/mssql.sql\n\n                echo \"Script Execution is complete. Waiting for MS SQL Process to terminate 🎉\"\n\n                wait $$pid\n\n    llana-redis-cache:\n        image: redis\n        restart: always\n        container_name: llana-redis-cache\n        ports:\n            - '6379:6379'\n        networks:\n            - llana-network\n        volumes:\n            - llana-redis-cache:/data"
  },
  {
    "path": "docker/docker-compose.test.prod.build.yml",
    "content": "name: llana\n\nservices:\n    llana-test-prod-build-app:\n        container_name: llana-test-prod-build-app\n        build:\n            context: .\n            dockerfile: docker/images/base/Dockerfile\n        ports:\n            - \"3000:3000\"\n        environment:\n            DATABASE_URI: ${DATABASE_URI}\n            JWT_KEY: ${JWT_KEY}\n            JWT_REFRESH_KEY: ${JWT_REFRESH_KEY}\n            HOSTS: ${HOSTS}\n            SOFT_DELETE_COLUMN: ${SOFT_DELETE_COLUMN}"
  },
  {
    "path": "docker/docker-compose.test.prod.yml",
    "content": "name: llana\n\nservices:\n    llana-prod-test-app:\n        container_name: llana-prod-test-app\n        image: juicyllama/llana:latest\n        ports:\n            - \"3000:3000\"\n        environment:\n            DATABASE_URI: ${DATABASE_URI}\n            JWT_KEY: ${JWT_KEY}\n            JWT_REFRESH_KEY: ${JWT_REFRESH_KEY}\n            HOSTS: ${HOSTS}\n            SOFT_DELETE_COLUMN: ${SOFT_DELETE_COLUMN}"
  },
  {
    "path": "docker/images/base/Dockerfile",
    "content": "###################\n# BUILD\n###################\n\nARG NODE_VERSION=22\n\n# Use a builder step to download various dependencies\nFROM node:${NODE_VERSION}-alpine AS build\n\n# Install git and other OS dependencies\nRUN apk add --no-cache git\n\nWORKDIR /usr/src/app\n\nCOPY . .\n\nRUN chown -R node:node /usr/src/app\n\nUSER node\n\nRUN cd /usr/src/app\n\n# Install the dependencies\nRUN npm ci\nRUN npm run build\n\n###################\n# PRODUCTION\n###################\n\nFROM node:${NODE_VERSION}-alpine AS production\n\nWORKDIR /usr/src/app\n\n# Copy the app from the build stage\nCOPY --chown=node:node --from=build /usr/src/app .\n\nRUN chown -R node:node /usr/src/app\n\nUSER node\n\nRUN cd /usr/src/app\n\nCMD [ \"npm\", \"run\", \"start\" ]\n\n"
  },
  {
    "path": "docker/images/llana/Dockerfile",
    "content": "###################\n# PRODUCTION\n###################\n\nFROM juicyllama/llana:latest\n\nUSER node\n\nRUN cd /usr/src/app\n\nCMD [ \"npm\", \"run\", \"start\" ]"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import typescriptEslintEslintPlugin from '@typescript-eslint/eslint-plugin'\nimport globals from 'globals'\nimport tsParser from '@typescript-eslint/parser'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport js from '@eslint/js'\nimport { FlatCompat } from '@eslint/eslintrc'\nimport simpleImportSort from 'eslint-plugin-simple-import-sort'\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = path.dirname(__filename)\nconst compat = new FlatCompat({\n\tbaseDirectory: __dirname,\n\trecommendedConfig: js.configs.recommended,\n\tallConfig: js.configs.all,\n})\n\nexport default [\n\t{\n\t\tignores: ['**/.eslintrc.js', '**/*.spec.ts', '**/*.test.ts', '**/*.paused.ts'],\n\t},\n\t...compat.extends('plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'),\n\t{\n\t\tplugins: {\n\t\t\t'@typescript-eslint': typescriptEslintEslintPlugin,\n\t\t\t\"simple-import-sort\": simpleImportSort,\n\t\t},\n\n\t\tlanguageOptions: {\n\t\t\tglobals: {\n\t\t\t\t...globals.node,\n\t\t\t\t...globals.jest,\n\t\t\t},\n\n\t\t\tparser: tsParser,\n\t\t\tecmaVersion: 5,\n\t\t\tsourceType: 'commonjs',\n\n\t\t\tparserOptions: {\n\t\t\t\tproject: 'tsconfig.json',\n\t\t\t},\n\t\t},\n\n\t\trules: {\n\t\t\t'@typescript-eslint/interface-name-prefix': 'off',\n\t\t\t'@typescript-eslint/explicit-function-return-type': 'off',\n\t\t\t'@typescript-eslint/explicit-module-boundary-types': 'off',\n\t\t\t'@typescript-eslint/no-explicit-any': 'off',\n\t\t\t'@typescript-eslint/no-empty-object-type': 'off',\n\t\t\t'prefer-const': 'off',\n\t\t\t'simple-import-sort/imports': 'error',\n      \t\t'simple-import-sort/exports': 'error',\n\t\t},\n\t},\n]\n"
  },
  {
    "path": "nest-cli.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/nest-cli\",\n  \"collection\": \"@nestjs/schematics\",\n  \"sourceRoot\": \"src\",\n  \"compilerOptions\": {\n    \"deleteOutDir\": true,\n    \"assets\": [{\n      \"include\": \"**/*.handlebars\", \n      \"outDir\": \"./dist/src\"\n      },{\n        \"include\": \"../public\",\n        \"outDir\": \"dist/public\",\n        \"watchAssets\": true\n      },\n      {\n        \"include\": \"../views\",\n        \"outDir\": \"dist/views\",\n        \"watchAssets\": true\n      }],\n      \"watchAssets\": true,\n    \"tsConfigPath\": \"tsconfig.build.json\"\n  },\n  \"watchOptions\": {\n    \"aggregateTimeout\": 500,\n    \"poll\": 1000\n}\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n\t\"name\": \"@juicyllama/llana\",\n\t\"version\": \"1.32.0\",\n\t\"description\": \"API Wrapper for Databases - Llana is a no-code API wrapper that exposes a REST API for any database within minutes. No longer spend time building APIs, just connect your database and start using the API. Open source, free to use, and no vendor lock-in.\",\n\t\"author\": {\n\t\t\"name\": \"JuicyLlama Studio\",\n\t\t\"email\": \"studio@juicyllama.com\",\n\t\t\"url\": \"https://juicyllama.com\"\n\t},\n\t\"publishConfig\": {\n\t\t\"access\": \"public\",\n\t\t\"registry\": \"https://registry.npmjs.org/\"\n\t},\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"https://github.com/juicyllama/llana\"\n\t},\n\t\"bugs\": \"https://llana.io\",\n\t\"license\": \"BSD-4-Clause\",\n\t\"readmeFilename\": \"README.md\",\n\t\"tags\": [\n\t\t\"llana\",\n\t\t\"api\"\n\t],\n\t\"keywords\": [\n\t\t\"llana\",\n\t\t\"api\"\n\t],\n\t\"scripts\": {\n\t\t\"prebuild\": \"rimraf dist\",\n\t\t\"build\": \"nest build\",\n\t\t\"format\": \"prettier --write \\\"**/*.ts\\\"\",\n\t\t\"lint\": \"eslint \\\"{src,apps,libs,test}/**/*.ts\\\" --fix\",\n\t\t\"start\": \"nest start\",\n\t\t\"start:dev\": \"nest start --watch\",\n\t\t\"start:debug\": \"nest start --debug --watch\",\n\t\t\"start:ngrok\": \"~/ngrok http http://localhost:3000/ --subdomain=llana\",\n\t\t\"docker:dev\": \"sh ./scripts/docker.dev.sh\",\n\t\t\"docker:dev:up\": \"docker compose -f ./docker/docker-compose.dev.yml up --build --detach\",\n\t\t\"docker:dev:down\": \"docker compose -f ./docker/docker-compose.dev.yml down  --remove-orphans --volumes\",\n\t\t\"docker:prod:build\": \"sh ./scripts/docker.build.prod.sh\",\n\t\t\"docker:prod:test\": \"sh ./scripts/docker.prod.sh\",\n\t\t\"precommit\": \"npm run test && npm run lint && npm run format\",\n\t\t\"commit\": \"git pull && git add . && git commit -m \\\"chore: update\\\" && git push\",\n\t\t\"test\": \"sh ./scripts/test.sh\",\n\t\t\"test:current\": \"jest --bail --runInBand --detectOpenHandles --forceExit\",\n\t\t\"test:mysql\": \"export DATABASE_URI=mysql://user:pass@localhost:3306/llana && jest --bail --runInBand --detectOpenHandles --forceExit\",\n\t\t\"test:postgresql\": \"export DATABASE_URI=postgresql://user:pass@localhost:5432/llana && jest --bail --runInBand --detectOpenHandles --forceExit\",\n\t\t\"test:mongodb\": \"export DATABASE_URI=mongodb://user:pass@localhost:27017/llana && jest --bail --runInBand --detectOpenHandles --forceExit\",\n\t\t\"test:mssql\": \"export DATABASE_URI=mssql://sa:S7!0nGpAw0rD@localhost:1433/llana && jest --bail --runInBand --detectOpenHandles --forceExit\",\n\t\t\"seed:airtable\": \"ts-node ./demo/databases/airtable.ts\"\n\t},\n\t\"dependencies\": {\n\t\t\"@nestjs/cache-manager\": \"^3.0.1\",\n\t\t\"@nestjs/common\": \"^11.1.1\",\n\t\t\"@nestjs/config\": \"^4.0.2\",\n\t\t\"@nestjs/core\": \"^11.1.1\",\n\t\t\"@nestjs/jwt\": \"^11.0.0\",\n\t\t\"@nestjs/passport\": \"^11.0.5\",\n\t\t\"@nestjs/platform-express\": \"^11.1.1\",\n\t\t\"@nestjs/platform-socket.io\": \"^11.1.1\",\n\t\t\"@nestjs/schedule\": \"^6.0.0\",\n\t\t\"@nestjs/websockets\": \"^11.1.1\",\n\t\t\"@types/mssql\": \"^9.1.7\",\n\t\t\"@types/pg\": \"^8.15.2\",\n\t\t\"argon2\": \"^0.43.0\",\n\t\t\"axios\": \"^1.9.0\",\n\t\t\"bcrypt\": \"^6.0.0\",\n\t\t\"cache-manager\": \"^6.4.3\",\n\t\t\"class-transformer\": \"^0.5.1\",\n\t\t\"class-validator\": \"^0.14.2\",\n\t\t\"escape-html\": \"^1.0.3\",\n\t\t\"express-basic-auth\": \"^1.2.1\",\n\t\t\"express-handlebars\": \"^8.0.3\",\n\t\t\"hbs\": \"^4.2.0\",\n\t\t\"ioredis\": \"^5.6.1\",\n\t\t\"joi\": \"^17.13.3\",\n\t\t\"jsonwebtoken\": \"^9.0.2\",\n\t\t\"lodash\": \"^4.17.21\",\n\t\t\"mongodb\": \"^6.16.0\",\n\t\t\"mssql\": \"^11.0.1\",\n\t\t\"mysql2\": \"^3.14.1\",\n\t\t\"openapi-types\": \"^12.1.3\",\n\t\t\"passport-local\": \"^1.0.0\",\n\t\t\"pg\": \"^8.16.0\",\n\t\t\"pg-promise\": \"^11.13.0\",\n\t\t\"reflect-metadata\": \"^0.2.2\",\n\t\t\"rxjs\": \"^7.8.2\",\n\t\t\"socket.io\": \"^4.8.1\",\n\t\t\"sqlstring\": \"^2.3.3\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@eslint/eslintrc\": \"^3.3.1\",\n\t\t\"@eslint/js\": \"^9.27.0\",\n\t\t\"@nestjs/cli\": \"^11.0.7\",\n\t\t\"@nestjs/schematics\": \"^11.0.5\",\n\t\t\"@nestjs/testing\": \"^11.1.1\",\n\t\t\"@swc/cli\": \"^0.7.7\",\n\t\t\"@swc/core\": \"^1.11.29\",\n\t\t\"@types/bcrypt\": \"^5.0.2\",\n\t\t\"@types/express\": \"^5.0.2\",\n\t\t\"@types/jest\": \"^29.5.14\",\n\t\t\"@types/node\": \"^22.15.21\",\n\t\t\"@types/supertest\": \"^6.0.3\",\n\t\t\"@typescript-eslint/eslint-plugin\": \"^8.32.1\",\n\t\t\"@typescript-eslint/parser\": \"^8.32.1\",\n\t\t\"eslint\": \"^9.27.0\",\n\t\t\"eslint-config-prettier\": \"^10.1.5\",\n\t\t\"eslint-plugin-prettier\": \"^5.4.0\",\n\t\t\"eslint-plugin-simple-import-sort\": \"^12.1.1\",\n\t\t\"globals\": \"^16.1.0\",\n\t\t\"jest\": \"^29.7.0\",\n\t\t\"prettier\": \"^3.5.3\",\n\t\t\"rimraf\": \"^5.0.5\",\n\t\t\"socket.io-client\": \"^4.8.1\",\n\t\t\"source-map-support\": \"^0.5.21\",\n\t\t\"supertest\": \"^7.1.1\",\n\t\t\"ts-jest\": \"^29.3.4\",\n\t\t\"ts-loader\": \"^9.5.2\",\n\t\t\"ts-node\": \"^10.9.2\",\n\t\t\"tsconfig-paths\": \"^4.2.0\",\n\t\t\"typescript\": \"^5.8.3\",\n\t\t\"webpack\": \"^5.99.9\"\n\t},\n\t\"overrides\": {\n\t\t\"multer\": \"2.0.0\"\n\t},\n\t\"resolutions\": {\n\t\t\"multer\": \"2.0.0\"\n\t},\n\t\"engines\": {\n\t\t\"node\": \">=22.0.0\",\n\t\t\"npm\": \">=8.3.0\"\n\t},\n\t\"jest\": {\n\t\t\"moduleFileExtensions\": [\n\t\t\t\"js\",\n\t\t\t\"json\",\n\t\t\t\"ts\"\n\t\t],\n\t\t\"rootDir\": \"src\",\n\t\t\"testRegex\": \".*\\\\.spec\\\\.ts$\",\n\t\t\"transform\": {\n\t\t\t\"^.+\\\\.(t|j)s$\": \"ts-jest\"\n\t\t},\n\t\t\"collectCoverageFrom\": [\n\t\t\t\"**/*.(t|j)s\"\n\t\t],\n\t\t\"coverageDirectory\": \"../coverage\",\n\t\t\"testEnvironment\": \"node\",\n\t\t\"testTimeout\": 20000,\n\t\t\"moduleNameMapper\": {\n\t\t\t\"^src/(.*)$\": \"<rootDir>/$1\"\n\t\t}\n\t},\n\t\"prettier\": {\n\t\t\"singleQuote\": true,\n\t\t\"trailingComma\": \"all\",\n\t\t\"tabWidth\": 4,\n\t\t\"useTabs\": true,\n\t\t\"semi\": false,\n\t\t\"printWidth\": 120,\n\t\t\"arrowParens\": \"avoid\",\n\t\t\"bracketSpacing\": true,\n\t\t\"bracketSameLine\": true\n\t}\n}\n"
  },
  {
    "path": "pr_description.md",
    "content": "# Improve Response Errors to be More Descriptive\n\n## Description\nThis PR implements standardized error handling across all datasources with consistent error enums and descriptive messages. It addresses [Issue #150](https://github.com/juicyllama/llana/issues/150) by providing more meaningful errors from requests to controllers when datasources fail.\n\n## Changes\n- Added `DatabaseErrorType` enum with common error types (DUPLICATE_RECORD, UNIQUE_KEY_VIOLATION, etc.)\n- Updated `IsUniqueResponse` to include an `error` field for descriptive messages\n- Enhanced all datasources (MySQL, PostgreSQL, MSSQL, MongoDB, Airtable) to map database-specific errors to standardized types\n- Updated controllers to return structured error responses with both `message` (enum value) and `error` (descriptive text) fields\n- Added test for duplicate record error response format\n\n## Example Error Response\n```json\n{\n  \"message\": \"DUPLICATE_RECORD\",\n  \"error\": \"Error inserting record as a duplicate already exists\"\n}\n```\n\n## Testing\n- Verified error handling across all datasources\n- Ensured consistent error responses regardless of underlying database technology\n- Added test case for duplicate record error\n\nLink to Devin run: https://app.devin.ai/sessions/af27b986e35f45abb404cd14469283bf\nRequested by: andy@juicyllama.com\n"
  },
  {
    "path": "scripts/docker.build.prod.sh",
    "content": "#!/bin/bash\ndocker-compose rm -f ./docker/docker-compose.test.prod.build.yml\ndocker compose -f ./docker/docker-compose.test.prod.build.yml up --build"
  },
  {
    "path": "scripts/docker.dev.sh",
    "content": "#!/bin/bash\ndocker compose -f ./docker/docker-compose.dev.yml down  --remove-orphans --volumes\ndocker compose -f ./docker/docker-compose.dev.yml rm \ndocker compose -f ./docker/docker-compose.dev.yml up --build --detach"
  },
  {
    "path": "scripts/docker.prod.sh",
    "content": "#!/bin/bash\ndocker-compose rm -f ./docker/docker-compose.test.prod.yml\ndocker compose -f ./docker/docker-compose.test.prod.yml up --build"
  },
  {
    "path": "scripts/install.sh",
    "content": "#!/bin/bash\n\n## check if .env file exists, if not create it from .env.example\n\nif [ ! -f .env ]; then\n    echo \"Creating .env file from .env.example\"\n    cp .env.example .env\n\n    echo \"Print .env to make sure it was copied over\"\n    cat .env\nfi\n\nexport $(grep -v '^#' .env | xargs)\n\n## generate a randomly secure JWT_KEY for the .env file if ! exists\n\nif [ -z \"${JWT_KEY}\" ]; then\n    echo \"Generating a secure JWT_KEY\"\n    JWT_KEY=$(node -e \"console.log(require('crypto').randomBytes(32).toString('hex'));\")\n\n    ## Replace the JWT_KEY in the .env file\n    sed -i -e \"s/JWT_KEY=/JWT_KEY=${JWT_KEY}/\" .env\n    rm -rf .env-e\nfi\n\n\nif [ -z \"${JWT_REFRESH_KEY}\" ]; then\n    echo \"Generating a secure JWT_REFRESH_KEY\"\n    JWT_REFRESH_KEY=$(node -e \"console.log(require('crypto').randomBytes(32).toString('hex'));\")\n\n    ## Replace the JWT_REFRESH_KEY in the .env file\n    sed -i -e \"s/JWT_REFRESH_KEY=/JWT_REFRESH_KEY=${JWT_REFRESH_KEY}/\" .env\n    rm -rf .env-e\nfi\n"
  },
  {
    "path": "scripts/test.sh",
    "content": "#!/bin/sh\n\n## Objective is to run over each data source and run the tests, allowing us to fully test every datasource each time we run the tests\n\n## Create array of data sources (string[])\ndata_sources=\"mysql postgresql mongodb mssql\"\n\nerrored=false\n\n## Loop over each data source and run the tests\nfor data_source in $data_sources\ndo\n    echo \"Running tests for $data_source\"\n\n    if [ \"$errored\" = true ]; then\n        echo \"Skipping $data_source as already errored\"\n        continue\n    fi\n\n    ## Run the tests via npm eg. npm run test:mysql\n    if ! npm run test:$data_source; then\n        ## If the tests fail, print an error message\n        echo \"Tests failed for $data_source\"\n\n        errored=true\n    fi\n\ndone\n\nif [ \"$errored\" = true ]; then\n    echo \"Tests failed\"\n    exit 1\nelse\n    echo \"Tests passed\"\n    exit 0\nfi"
  },
  {
    "path": "src/app.constants.ts",
    "content": "import { DataSourceType } from './types/datasource.types'\n\nexport const NON_FIELD_PARAMS = ['fields', 'limit', 'offset', 'sort', 'page', 'relations', 'join']\nexport const LLANA_PUBLIC_TABLES = '_llana_public_tables'\nexport const LLANA_ROLES_TABLE = '_llana_role'\nexport const LLANA_RELATION_TABLE = '_llana_relation'\nexport const LLANA_WEBHOOK_TABLE = '_llana_webhook'\nexport const LLANA_WEBHOOK_LOG_TABLE = '_llana_webhook_log'\nexport const LLANA_DATA_CACHING_TABLE = '_llana_data_caching'\nexport const APP_BOOT_CONTEXT = 'AppBootup'\nexport const CACHE_DEFAULT_TABLE_SCHEMA_TTL = 3600000 // 1 hour\nexport const CACHE_DEFAULT_IDENTITY_DATA_TTL = 600000 // 10 minutes\nexport const CACHE_DEFAULT_WS_IDENTITY_DATA_TTL = 3600000 * 24 * 2 // 2 days\nexport const CACHE_DEFAULT_WEBHOOK_TTL = 3600000 * 24 * 2 // 2 days\nexport const WEBHOOK_LOG_DAYS = 1\nexport const NON_RELATIONAL_DBS = [DataSourceType.MONGODB]\n"
  },
  {
    "path": "src/app.controller.auth.test.spec.ts",
    "content": "import { INestApplication } from '@nestjs/common'\nimport { Test } from '@nestjs/testing'\nimport { JwtModule } from '@nestjs/jwt'\nimport { ConfigModule, ConfigService, ConfigFactory } from '@nestjs/config'\nimport * as request from 'supertest'\nimport { castArray } from 'lodash'\n\nimport { AppModule } from './app.module'\nimport { TIMEOUT } from './testing/testing.const'\nimport { Logger } from './helpers/Logger'\n\n// Import configs\nimport auth from './config/auth.config'\nimport database from './config/database.config'\nimport hosts from './config/hosts.config'\nimport jwt from './config/jwt.config'\nimport roles from './config/roles.config'\nimport { envValidationSchema } from './config/env.validation'\nimport { ACCESS_TOKEN_COOKIE_NAME, REFRESH_TOKEN_COOKIE_NAME } from './auth/auth.constants'\n\n// Type the config imports\nconst configs: ConfigFactory[] = [auth, database, hosts, jwt, roles]\n\ndescribe('App > Controller > Auth', () => {\n\tlet app: INestApplication\n\n\tlet access_token: string, refresh_token: string\n\tlet logger = new Logger()\n\n\tbeforeAll(async () => {\n\t\tconst moduleRef = await Test.createTestingModule({\n\t\t\timports: [\n\t\t\t\tConfigModule.forRoot({\n\t\t\t\t\tload: configs,\n\t\t\t\t\tvalidationSchema: envValidationSchema,\n\t\t\t\t\tisGlobal: true,\n\t\t\t\t}),\n\t\t\t\tJwtModule.registerAsync({\n\t\t\t\t\timports: [ConfigModule],\n\t\t\t\t\tuseFactory: async (configService: ConfigService) => ({\n\t\t\t\t\t\tsecret: configService.get('jwt.secret'),\n\t\t\t\t\t\tsignOptions: configService.get('jwt.signOptions'),\n\t\t\t\t\t}),\n\t\t\t\t\tinject: [ConfigService],\n\t\t\t\t}),\n\t\t\t\tAppModule,\n\t\t\t],\n\t\t}).compile()\n\t\tapp = moduleRef.createNestApplication()\n\t\tawait app.init()\n\n\t\t// Expose the app object globally for debugging\n\t\t;(global as any).app = app\n\t}, TIMEOUT)\n\n\tbeforeEach(() => {\n\t\tlogger.debug('===========================================')\n\t\tlogger.log('🧪 ' + expect.getState().currentTestName)\n\t\tlogger.debug('===========================================')\n\t})\n\n\tdescribe('Failed Login', () => {\n\t\tit('Missing username', async function () {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.post(`/auth/login`)\n\t\t\t\t.send({\n\t\t\t\t\tpassword: 'test',\n\t\t\t\t})\n\t\t\t\t.expect(401)\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body.statusCode).toEqual(401)\n\t\t})\n\n\t\tit('Missing password', async () => {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.post(`/auth/login`)\n\t\t\t\t.send({\n\t\t\t\t\tusername: 'test@test.com',\n\t\t\t\t})\n\t\t\t\t.expect(401)\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body.statusCode).toEqual(401)\n\t\t})\n\n\t\tit('Wrong username', async () => {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.post(`/auth/login`)\n\t\t\t\t.send({\n\t\t\t\t\tusername: 'wrong@username.com',\n\t\t\t\t\tpassword: 'test',\n\t\t\t\t})\n\t\t\t\t.expect(401)\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body.statusCode).toEqual(401)\n\t\t\texpect(result.body.message).toEqual('Unauthorized')\n\t\t})\n\n\t\tit('Wrong password', async () => {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.post(`/auth/login`)\n\t\t\t\t.send({\n\t\t\t\t\tusername: 'wrong@username.com',\n\t\t\t\t\tpassword: 'wrong',\n\t\t\t\t})\n\t\t\t\t.expect(401)\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body.statusCode).toEqual(401)\n\t\t\texpect(result.body.message).toEqual('Unauthorized')\n\t\t})\n\t})\n\n\tdescribe('Successful Login', () => {\n\t\tit('Correct username & password', async () => {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.post(`/auth/login`)\n\t\t\t\t.send({\n\t\t\t\t\tusername: 'test@test.com',\n\t\t\t\t\tpassword: 'test',\n\t\t\t\t})\n\t\t\t\t.expect(200)\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body.access_token).toBeDefined()\n\t\t\taccess_token = getCookieValueFromHeader(result, ACCESS_TOKEN_COOKIE_NAME) // cookie token\n\t\t\trefresh_token = getCookieValueFromHeader(result, REFRESH_TOKEN_COOKIE_NAME) // cookie token\n\t\t\texpect(access_token).toBeDefined()\n\t\t\texpect(refresh_token).toBeDefined()\n\t\t})\n\t})\n\n\tdescribe('Access Token Works', () => {\n\t\tit('Get Profile (Bearer header)', async () => {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.get(`/auth/profile`)\n\t\t\t\t.set('Authorization', `Bearer ${access_token}`)\n\t\t\t\t.expect(200)\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body.email).toBeDefined()\n\t\t})\n\n\t\tit('Get Profile (Cookie token)', async () => {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.get(`/auth/profile`)\n\t\t\t\t.set('Cookie', `${ACCESS_TOKEN_COOKIE_NAME}=${access_token}`)\n\t\t\t\t.expect(200)\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body.email).toBeDefined()\n\t\t})\n\n\t\tit('Get Profile With Relations', async () => {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.get(`/auth/profile?relations=UserApiKey`)\n\t\t\t\t.set('Authorization', `Bearer ${access_token}`)\n\t\t\t\t.expect(200)\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body.email).toBeDefined()\n\t\t\texpect(result.body.UserApiKey).toBeDefined()\n\t\t\texpect(result.body.UserApiKey.length).toBeGreaterThan(0)\n\t\t\texpect(result.body.UserApiKey[0].apiKey).toBeDefined()\n\t\t})\n\t})\n\n\tdescribe('Refresh', () => {\n\t\tit('Sets new access token and refresh token cookies', async () => {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.post(`/auth/refresh`)\n\t\t\t\t.set('Cookie', `${REFRESH_TOKEN_COOKIE_NAME}=${refresh_token}`)\n\t\t\t\t.then(async res => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst accessToken = getCookieValueFromHeader(res, ACCESS_TOKEN_COOKIE_NAME) // cookie token\n\t\t\t\t\t\texpect(res.body.access_token).toBeDefined() // bearer token\n\t\t\t\t\t\texpect(accessToken).toBeDefined()\n\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\tconsole.error(res.headers)\n\t\t\t\t\t\texpect(e).toMatch('error')\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch(async e => {\n\t\t\t\t\texpect(e).toMatch('error')\n\t\t\t\t})\n\t\t})\n\t})\n\n\tdescribe('Logout', () => {\n\t\tit('Clears access token and refresh token cookies', async () => {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.post(`/auth/logout`)\n\t\t\t\t.set('Cookie', `${ACCESS_TOKEN_COOKIE_NAME}=${access_token}`)\n\t\t\t\t.then(async res => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst accessToken = getCookieValueFromHeader(res, ACCESS_TOKEN_COOKIE_NAME) // cookie token\n\t\t\t\t\t\tconst refreshToken = getCookieValueFromHeader(res, REFRESH_TOKEN_COOKIE_NAME) // cookie token\n\t\t\t\t\t\texpect(accessToken).toBeFalsy()\n\t\t\t\t\t\texpect(refreshToken).toBeFalsy()\n\t\t\t\t\t\texpect(res.body.success).toBeTruthy()\n\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\tconsole.error(res.headers)\n\t\t\t\t\t\texpect(e).toMatch('error')\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch(async e => {\n\t\t\t\t\texpect(e).toMatch('error')\n\t\t\t\t})\n\t\t})\n\t})\n\n\tafterAll(async () => {\n\t\tawait app.close()\n\t}, TIMEOUT)\n})\n\nexport function getCookieValueFromHeader(res: any, cookieName: string) {\n\tif (!res.headers['set-cookie']) {\n\t\treturn undefined\n\t}\n\tconst cookies: Array<string> = castArray(res.headers['set-cookie'])\n\tconst cookie = cookies.find(cookie => cookie.startsWith(cookieName + '='))\n\treturn cookie?.split('=')[1].split(';')[0]\n}\n"
  },
  {
    "path": "src/app.controller.auth.ts",
    "content": "import {\n\tBadRequestException,\n\tController,\n\tGet,\n\tHeaders,\n\tParseArrayPipe,\n\tPost,\n\tQuery as QueryParams,\n\tReq,\n\tRes,\n\tUseGuards,\n} from '@nestjs/common'\nimport { CookieOptions, Response as ExpressResponse } from 'express'\n\nimport { AuthService } from './app.service.auth'\nimport { ACCESS_TOKEN_COOKIE_NAME, REFRESH_TOKEN_COOKIE_NAME } from './auth/auth.constants'\nimport { LocalAuthGuard } from './auth/guards/local-auth.guard'\nimport { HeaderParams } from './dtos/requests.dto'\nimport { FindOneResponseObject } from './dtos/response.dto'\nimport { Authentication } from './helpers/Authentication'\nimport { Logger } from './helpers/Logger'\nimport { Query } from './helpers/Query'\nimport { Response } from './helpers/Response'\nimport { Schema } from './helpers/Schema'\nimport { AuthenticatedRequest } from './types/auth.types'\nimport { DataSourceFindOneOptions, QueryPerform, WhereOperator } from './types/datasource.types'\nimport { RolePermission } from './types/roles.types'\nimport { Env } from './utils/Env'\n\n@Controller('auth')\nexport class AuthController {\n\tlogger = new Logger('AuthController')\n\n\tconstructor(\n\t\tprivate readonly authService: AuthService,\n\t\tprivate readonly authentication: Authentication,\n\t\tprivate readonly query: Query,\n\t\tprivate readonly response: Response,\n\t\tprivate readonly schema: Schema,\n\t) {}\n\n\t/**\n\t * Exchange a username and password for an access token\n\t */\n\n\t@UseGuards(LocalAuthGuard)\n\t@Post('/login')\n\tasync login(@Req() req: AuthenticatedRequest, @Res({ passthrough: true }) res: ExpressResponse): Promise<any> {\n\t\tif (this.authentication.skipAuth()) {\n\t\t\tthrow new BadRequestException('Authentication is disabled')\n\t\t}\n\n\t\tconst { access_token } = await this.authService.login(req.user)\n\t\tconst refreshToken = await this.authService.createRefreshToken(req.user)\n\t\tsetAccessAndRefreshTokenCookies(res, access_token, refreshToken)\n\t\treturn res.status(200).json({\n\t\t\taccess_token,\n\t\t\texpires_in: convertJwtExpiryToMs(process.env.JWT_EXPIRES_IN) / 1000,\n\t\t\trefresh_token_expires_in: convertJwtExpiryToMs(process.env.JWT_REFRESH_EXPIRES_IN) / 1000,\n\t\t})\n\t}\n\n\t@Post('refresh')\n\tasync refresh(@Req() req: AuthenticatedRequest, @Res({ passthrough: true }) res: ExpressResponse): Promise<any> {\n\t\tconst cookies = req.headers.cookie || ''\n\t\tconst oldRefreshToken = cookies\n\t\t\t.split(';')\n\t\t\t.find(cookie => cookie.trim().startsWith(REFRESH_TOKEN_COOKIE_NAME + '='))\n\t\t\t?.split('=')[1]\n\t\tif (!oldRefreshToken) {\n\t\t\treturn res.status(401).send(this.response.text('No refresh token found'))\n\t\t}\n\t\tconst loginPayload = this.authService.decodeRefreshToken(oldRefreshToken)\n\t\tconst { access_token: newAccessToken } = await this.authService.login(loginPayload)\n\t\tconst newRefreshToken = await this.authService.createRefreshToken(loginPayload)\n\t\tsetAccessAndRefreshTokenCookies(res, newAccessToken, newRefreshToken)\n\t\tthis.logger.log('Refreshed token', {\n\t\t\tsub: loginPayload.sub,\n\t\t\toldRefreshToken: '...' + oldRefreshToken.slice(-10),\n\t\t})\n\t\treturn res.status(200).json({\n\t\t\taccess_token: newAccessToken,\n\t\t\texpires_in: convertJwtExpiryToMs(process.env.JWT_EXPIRES_IN) / 1000,\n\t\t\trefresh_token_expires_in: convertJwtExpiryToMs(process.env.JWT_REFRESH_EXPIRES_IN) / 1000,\n\t\t})\n\t}\n\n\t@Post('logout')\n\tasync logout(@Res({ passthrough: true }) res: ExpressResponse): Promise<any> {\n\t\tres.clearCookie(ACCESS_TOKEN_COOKIE_NAME, getAuthCookieOpts(false))\n\t\tres.clearCookie(REFRESH_TOKEN_COOKIE_NAME, getAuthCookieOpts(true))\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t}\n\t}\n\n\t/*\n\t * Return the current user's profile, useful for testing the access token\n\t */\n\n\t@Get('/profile')\n\tasync profile(\n\t\t@Req() req,\n\t\t@Res() res,\n\t\t@Headers() headers: HeaderParams,\n\t\t@QueryParams('relations', new ParseArrayPipe({ items: String, separator: ',', optional: true }))\n\t\tqueryRelations?: string[],\n\t): Promise<any> {\n\t\tif (this.authentication.skipAuth()) {\n\t\t\tthrow new BadRequestException('Authentication is disabled')\n\t\t}\n\n\t\tconst x_request_id = headers['x-request-id']\n\t\tconst table = this.authentication.getIdentityTable()\n\t\tconst auth = await this.authentication.auth({\n\t\t\ttable,\n\t\t\tx_request_id,\n\t\t\taccess: RolePermission.READ,\n\t\t\theaders: req.headers,\n\t\t\tbody: req.body,\n\t\t\tquery: req.query,\n\t\t})\n\t\tif (!auth.valid) {\n\t\t\treturn res.status(401).send(this.response.text(auth.message))\n\t\t}\n\n\t\t//return the user's profile\n\t\tconst schema = await this.schema.getSchema({ table, x_request_id })\n\t\tconst identity_column = await this.authentication.getIdentityColumn(x_request_id)\n\n\t\tconst postQueryRelations = []\n\n\t\ttry {\n\t\t\tif (queryRelations?.length) {\n\t\t\t\tconst { valid, message, relations } = await this.schema.validateRelations({\n\t\t\t\t\tschema,\n\t\t\t\t\trelation_query: queryRelations,\n\t\t\t\t\texisting_relations: [],\n\t\t\t\t\tx_request_id,\n\t\t\t\t})\n\n\t\t\t\tif (!valid) {\n\t\t\t\t\treturn res.status(400).send(this.response.text(message))\n\t\t\t\t}\n\n\t\t\t\tfor (const relation of relations) {\n\t\t\t\t\tif (!postQueryRelations.find(r => r.table === relation.table)) {\n\t\t\t\t\t\tpostQueryRelations.push(relation)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (e) {\n\t\t\treturn res.status(400).send(this.response.text(e.message))\n\t\t}\n\n\t\tconst databaseQuery: DataSourceFindOneOptions = {\n\t\t\tschema,\n\t\t\twhere: [\n\t\t\t\t{\n\t\t\t\t\tcolumn: identity_column,\n\t\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\t\tvalue: auth.user_identifier,\n\t\t\t\t},\n\t\t\t],\n\t\t\trelations: postQueryRelations,\n\t\t}\n\n\t\tlet user = (await this.query.perform(\n\t\t\tQueryPerform.FIND_ONE,\n\t\t\tdatabaseQuery,\n\t\t\tx_request_id,\n\t\t)) as FindOneResponseObject\n\n\t\tif (postQueryRelations?.length) {\n\t\t\tuser = await this.query.buildRelations(\n\t\t\t\t{\n\t\t\t\t\tschema,\n\t\t\t\t\trelations: postQueryRelations,\n\t\t\t\t} as DataSourceFindOneOptions,\n\t\t\t\tuser,\n\t\t\t\tx_request_id,\n\t\t\t)\n\t\t}\n\n\t\treturn res.status(200).send(user)\n\t}\n}\n\nfunction getAuthCookieOpts(isRefreshToken: boolean): CookieOptions {\n\tif (Env.IsProd() && !process.env.AUTH_COOKIES_DOMAIN && !process.env.BASE_URL_API) {\n\t\tthrow new Error('AUTH_COOKIES_DOMAIN or BASE_URL_API must be set in production')\n\t}\n\n\tconst opts: Record<string, any> = {\n\t\thttpOnly: true,\n\t\tsecure: true,\n\t\tsameSite: 'none',\n\t\tmaxAge: convertJwtExpiryToMs(isRefreshToken ? process.env.JWT_REFRESH_EXPIRES_IN : process.env.JWT_EXPIRES_IN),\n\t\t...(process.env.AUTH_COOKIES_DOMAIN ? { domain: process.env.AUTH_COOKIES_DOMAIN } : {}),\n\t\tpath: '/',\n\t}\n\treturn opts\n}\n\nfunction setAccessAndRefreshTokenCookies(res: ExpressResponse, accessToken: string, refreshToken: string): void {\n\tres.cookie(ACCESS_TOKEN_COOKIE_NAME, accessToken, getAuthCookieOpts(false))\n\tres.cookie(REFRESH_TOKEN_COOKIE_NAME, refreshToken, getAuthCookieOpts(true))\n}\n\nfunction convertJwtExpiryToMs(expiry: string): number {\n\tconst match = expiry.match(/^(\\d+)([dms])$/)\n\tif (!match) {\n\t\tthrow new Error('Invalid JWT expiry format. Use formats like \"14d\", \"2m\", \"3s\".')\n\t}\n\n\tconst value = parseInt(match[1], 10)\n\tconst unit = match[2]\n\n\tswitch (unit) {\n\t\tcase 'd': // days\n\t\t\treturn value * 86400 * 1000\n\t\tcase 'm': // minutes\n\t\t\treturn value * 60 * 1000\n\t\tcase 's': // seconds\n\t\t\treturn value * 1000\n\t\tdefault:\n\t\t\tthrow new Error('Unsupported time unit in JWT expiry format.')\n\t}\n}\n"
  },
  {
    "path": "src/app.controller.delete.test.spec.ts",
    "content": "import { INestApplication } from '@nestjs/common'\nimport { ConfigFactory, ConfigModule, ConfigService } from '@nestjs/config'\nimport { JwtModule } from '@nestjs/jwt'\nimport { Test } from '@nestjs/testing'\nimport * as request from 'supertest'\nimport { CustomerTestingService } from './testing/customer.testing.service'\n\nimport { AppModule } from './app.module'\nimport { Logger } from './helpers/Logger'\nimport { AuthTestingService } from './testing/auth.testing.service'\nimport { DataSourceSchema } from './types/datasource.types'\n\n// Import configs\nimport auth from './config/auth.config'\nimport database from './config/database.config'\nimport { envValidationSchema } from './config/env.validation'\nimport hosts from './config/hosts.config'\nimport jwt from './config/jwt.config'\nimport roles from './config/roles.config'\nimport { UserTestingService } from './testing/user.testing.service'\nimport { RolePermission } from './types/roles.types'\n\n// Type the config imports\nconst configs: ConfigFactory[] = [auth, database, hosts, jwt, roles]\n\ndescribe('App > Controller > Delete', () => {\n\tlet app: INestApplication\n\n\tlet authTestingService: AuthTestingService\n\tlet customerTestingService: CustomerTestingService\n\tlet userTestingService: UserTestingService\n\n\tlet customerSchema: DataSourceSchema\n\tlet userSchema: DataSourceSchema\n\n\tlet customers = []\n\tlet own_customer: any\n\tlet other_customer: any\n\n\tlet jwt: string\n\tlet userId: any\n\tlet user: any\n\tlet logger = new Logger()\n\n\tbeforeAll(async () => {\n\t\tconst moduleRef = await Test.createTestingModule({\n\t\t\timports: [\n\t\t\t\tConfigModule.forRoot({\n\t\t\t\t\tload: configs,\n\t\t\t\t\tvalidationSchema: envValidationSchema,\n\t\t\t\t\tisGlobal: true,\n\t\t\t\t}),\n\t\t\t\tJwtModule.registerAsync({\n\t\t\t\t\timports: [ConfigModule],\n\t\t\t\t\tuseFactory: async (configService: ConfigService) => ({\n\t\t\t\t\t\tsecret: configService.get('jwt.secret'),\n\t\t\t\t\t\tsignOptions: configService.get('jwt.signOptions'),\n\t\t\t\t\t}),\n\t\t\t\t\tinject: [ConfigService],\n\t\t\t\t}),\n\t\t\t\tAppModule,\n\t\t\t],\n\t\t\tproviders: [AuthTestingService, CustomerTestingService, UserTestingService],\n\t\t\texports: [AuthTestingService, CustomerTestingService, UserTestingService],\n\t\t}).compile()\n\n\t\tapp = moduleRef.createNestApplication()\n\t\tawait app.init()\n\n\t\t// Expose the app object globally for debugging\n\t\t;(global as any).app = app\n\n\t\tauthTestingService = app.get<AuthTestingService>(AuthTestingService)\n\t\tcustomerTestingService = app.get<CustomerTestingService>(CustomerTestingService)\n\t\tuserTestingService = app.get<UserTestingService>(UserTestingService)\n\n\t\tjwt = await authTestingService.login()\n\t\tuserId = await authTestingService.getUserId(jwt)\n\t\tuser = await userTestingService.mockUser({ email: 'app.controller.delete.test.spec3@gmail.com' })\n\n\t\tconst result = await request(app.getHttpServer())\n\t\t\t.post(`/User/`)\n\t\t\t.send(user)\n\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\n\t\tif (result.status !== 201) {\n\t\t\tthrow new Error('Failed to create user: ' + result.text)\n\t\t}\n\n\t\tuser = result.body\n\n\t\tcustomerSchema = await customerTestingService.getSchema()\n\t\tuserSchema = await userTestingService.getSchema()\n\n\t\tcustomers.push(await customerTestingService.createCustomer({ userId }))\n\t\tcustomers.push(await customerTestingService.createCustomer({ userId }))\n\t\tcustomers.push(await customerTestingService.createCustomer({ userId }))\n\t\tcustomers.push(await customerTestingService.createCustomer({ userId }))\n\t})\n\n\tdescribe('Delete', () => {\n\t\tit('Delete One', async function () {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.delete(`/Customer/${customers[0][customerSchema.primary_key]}`)\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(200)\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body.deleted).toEqual(1)\n\t\t})\n\t\tit('Delete Many', async function () {\n\t\t\tcustomers[1].companyName = 'Customer2 Company Name'\n\t\t\tcustomers[2].companyName = 'Customer2 Company Name'\n\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.delete(`/Customer/`)\n\t\t\t\t.send([\n\t\t\t\t\t{\n\t\t\t\t\t\t[customerSchema.primary_key]: customers[1][customerSchema.primary_key],\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t[customerSchema.primary_key]: customers[2][customerSchema.primary_key],\n\t\t\t\t\t},\n\t\t\t\t])\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(200)\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body.deleted).toEqual(2)\n\t\t\texpect(result.body.errored).toEqual(0)\n\t\t\texpect(result.body.total).toEqual(2)\n\t\t})\n\t})\n\n\tdescribe('Public Deletion', () => {\n\t\tit('Default public fail to delete', async function () {\n\t\t\tawait request(app.getHttpServer())\n\t\t\t\t.delete(`/Customer/${customers[3][customerSchema.primary_key]}`)\n\t\t\t\t.expect(401)\n\t\t})\n\n\t\tit('Cannot delete with READ permissions', async function () {\n\t\t\tconst public_table_record = await authTestingService.createPublicTablesRecord({\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\taccess_level: RolePermission.READ,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.delete(`/Customer/${customers[3][customerSchema.primary_key]}`)\n\t\t\t\t\t.expect(401)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deletePublicTablesRecord(public_table_record)\n\t\t\t}\n\t\t})\n\n\t\tit('Cannot delete with WRITE permissions', async function () {\n\t\t\tconst public_table_record = await authTestingService.createPublicTablesRecord({\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\taccess_level: RolePermission.WRITE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.delete(`/Customer/${customers[3][customerSchema.primary_key]}`)\n\t\t\t\t\t.expect(401)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deletePublicTablesRecord(public_table_record)\n\t\t\t}\n\t\t})\n\n\t\tit('Can delete with DELETE permissions', async function () {\n\t\t\tconst public_table_record = await authTestingService.createPublicTablesRecord({\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\taccess_level: RolePermission.DELETE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.delete(`/Customer/${customers[3][customerSchema.primary_key]}`)\n\t\t\t\t\t.expect(200)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deletePublicTablesRecord(public_table_record)\n\t\t\t}\n\t\t})\n\t})\n\n\tdescribe('Role Based Creation', () => {\n\t\tbeforeEach(async () => {\n\t\t\tother_customer = await request(app.getHttpServer())\n\t\t\t\t.post(`/Customer/`)\n\t\t\t\t.send(customerTestingService.mockCustomer(user[userSchema.primary_key]))\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(201)\n\t\t\tcustomers.push(other_customer.body)\n\n\t\t\town_customer = await request(app.getHttpServer())\n\t\t\t\t.post(`/Customer/`)\n\t\t\t\t.send(customerTestingService.mockCustomer(userId))\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(201)\n\t\t\tcustomers.push(own_customer.body)\n\t\t})\n\n\t\tit('No table role, delete record', async function () {\n\t\t\tawait request(app.getHttpServer())\n\t\t\t\t.delete(`/Customer/${own_customer.body[customerSchema.primary_key]}`)\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(200)\n\t\t\tcustomers.pop()\n\t\t})\n\n\t\tit('DELETE table role, delete record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.DELETE,\n\t\t\t\town_records: RolePermission.DELETE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.delete(`/Customer/${own_customer.body[customerSchema.primary_key]}`)\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(200)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('DELETE table role, own records, delete own record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.NONE,\n\t\t\t\town_records: RolePermission.DELETE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.delete(`/Customer/${own_customer.body[customerSchema.primary_key]}`)\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(200)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('DELETE table role, own records, fails to delete someone elses record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.NONE,\n\t\t\t\town_records: RolePermission.DELETE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.delete(`/Customer/${other_customer.body[customerSchema.primary_key]}`)\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(401)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('WRITE table role, cannot delete record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.WRITE,\n\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.delete(`/Customer/${own_customer.body[customerSchema.primary_key]}`)\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(401)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('WRITE table role, own records, cannot delete own record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.NONE,\n\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.delete(`/Customer/${own_customer.body[customerSchema.primary_key]}`)\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(401)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('WRITE table role, own records, fails to delete someone elses record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.NONE,\n\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.delete(`/Customer/${other_customer.body[customerSchema.primary_key]}`)\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(401)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('READ table role, cannot delete record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.READ,\n\t\t\t\town_records: RolePermission.READ,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.delete(`/Customer/${own_customer.body[customerSchema.primary_key]}`)\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(401)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('READ table role, own records, cannot delete own record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.NONE,\n\t\t\t\town_records: RolePermission.READ,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.delete(`/Customer/${own_customer.body[customerSchema.primary_key]}`)\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(401)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('READ table role, own records, fails to delete someone elses record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.NONE,\n\t\t\t\town_records: RolePermission.READ,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.delete(`/Customer/${other_customer.body[customerSchema.primary_key]}`)\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(401)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\t})\n\n\tafterAll(async () => {\n\t\tfor (let customer of customers) {\n\t\t\tawait customerTestingService.deleteCustomer(customer[customerSchema.primary_key])\n\t\t}\n\t\tawait userTestingService.deleteUser(user[userSchema.primary_key])\n\t\tawait app.close()\n\t})\n})\n"
  },
  {
    "path": "src/app.controller.delete.ts",
    "content": "import { Body, Controller, Delete, Headers, Param, Query as QueryParams, Req, Res } from '@nestjs/common'\nimport { ConfigService } from '@nestjs/config'\n\nimport { LLANA_WEBHOOK_TABLE } from './app.constants'\nimport { HeaderParams } from './dtos/requests.dto'\nimport { DeleteManyResponseObject, DeleteResponseObject, FindOneResponseObject } from './dtos/response.dto'\nimport { Authentication } from './helpers/Authentication'\nimport { UrlToTable } from './helpers/Database'\nimport { Query } from './helpers/Query'\nimport { Response } from './helpers/Response'\nimport { Roles } from './helpers/Roles'\nimport { Schema } from './helpers/Schema'\nimport { Webhook } from './helpers/Webhook'\nimport { DataCacheService } from './modules/cache/dataCache.service'\nimport { WebsocketService } from './modules/websocket/websocket.service'\nimport { AuthTablePermissionFailResponse } from './types/auth.types'\nimport {\n\tDataSourceConfig,\n\tDataSourceSchema,\n\tDataSourceWhere,\n\tPublishType,\n\tQueryPerform,\n\tWhereOperator,\n} from './types/datasource.types'\nimport { RolePermission } from './types/roles.types'\n\n@Controller()\nexport class DeleteController {\n\tconstructor(\n\t\tprivate readonly authentication: Authentication,\n\t\tprivate readonly configService: ConfigService,\n\t\tprivate readonly dataCache: DataCacheService,\n\t\tprivate readonly query: Query,\n\t\tprivate readonly response: Response,\n\t\tprivate readonly roles: Roles,\n\t\tprivate readonly schema: Schema,\n\t\tprivate readonly websocket: WebsocketService,\n\t\tprivate readonly webhook: Webhook,\n\t) {}\n\n\t@Delete('*/:id')\n\tasync deleteById(\n\t\t@Req() req,\n\t\t@Res() res,\n\t\t@Headers() headers: HeaderParams,\n\t\t@Param('id') id: string,\n\t\t@QueryParams('hard') hard = false,\n\t): Promise<DeleteResponseObject> {\n\t\tconst x_request_id = headers['x-request-id']\n\t\tlet table_name = UrlToTable(req.originalUrl, 1)\n\n\t\tif (table_name === 'webhook') {\n\t\t\ttable_name = LLANA_WEBHOOK_TABLE\n\t\t}\n\n\t\tlet schema: DataSourceSchema\n\n\t\ttry {\n\t\t\tschema = await this.schema.getSchema({ table: table_name, x_request_id })\n\t\t} catch (e) {\n\t\t\treturn res.status(404).send(this.response.text(e.message))\n\t\t}\n\n\t\t// Is the table public?\n\t\tlet auth = await this.authentication.public({\n\t\t\ttable: table_name,\n\t\t\taccess_level: RolePermission.DELETE,\n\t\t\tx_request_id,\n\t\t})\n\n\t\t// If not public, perform auth\n\t\tif (!auth.valid) {\n\t\t\tauth = await this.authentication.auth({\n\t\t\t\ttable: table_name,\n\t\t\t\tx_request_id,\n\t\t\t\taccess: RolePermission.DELETE,\n\t\t\t\theaders: req.headers,\n\t\t\t\tbody: req.body,\n\t\t\t\tquery: req.query,\n\t\t\t})\n\t\t\tif (!auth.valid) {\n\t\t\t\treturn res.status(401).send(this.response.text(auth.message))\n\t\t\t}\n\t\t}\n\n\t\t//validate :id field\n\t\tconst primary_key = this.schema.getPrimaryKey(schema)\n\n\t\tif (!primary_key) {\n\t\t\treturn res.status(400).send(this.response.text(`No primary key found for table ${table_name}`))\n\t\t}\n\n\t\tconst validateKey = await this.schema.validateData(schema, { [primary_key]: id })\n\t\tif (!validateKey.valid) {\n\t\t\treturn res.status(400).send(this.response.text(validateKey.message))\n\t\t}\n\n\t\tconst where = <DataSourceWhere[]>[\n\t\t\t{\n\t\t\t\tcolumn: primary_key,\n\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\tvalue: id,\n\t\t\t},\n\t\t]\n\n\t\t//Check record exists\n\n\t\tconst record = (await this.query.perform(\n\t\t\tQueryPerform.FIND_ONE,\n\t\t\t{\n\t\t\t\tschema,\n\t\t\t\twhere,\n\t\t\t},\n\t\t\tx_request_id,\n\t\t)) as FindOneResponseObject\n\n\t\tif (!record) {\n\t\t\treturn res.status(400).send(this.response.text(`Record with id ${id} not found`))\n\t\t}\n\n\t\t//perform role check\n\t\tif (auth.user_identifier) {\n\t\t\tconst permission = await this.roles.tablePermission({\n\t\t\t\tidentifier: auth.user_identifier,\n\t\t\t\ttable: table_name,\n\t\t\t\taccess: RolePermission.DELETE,\n\t\t\t\tdata: record,\n\t\t\t\tx_request_id,\n\t\t\t})\n\n\t\t\tif (!permission.valid) {\n\t\t\t\treturn res.status(401).send(this.response.text((permission as AuthTablePermissionFailResponse).message))\n\t\t\t}\n\t\t}\n\n\t\t//Soft or Hard delete check\n\t\tconst databaseConfig: DataSourceConfig = this.configService.get('database')\n\n\t\tlet softDelete: string = null\n\n\t\tif (\n\t\t\t!hard &&\n\t\t\tdatabaseConfig.deletes.soft &&\n\t\t\tschema.columns.find(col => col.field === databaseConfig.deletes.soft)\n\t\t) {\n\t\t\tsoftDelete = databaseConfig.deletes.soft\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await this.query.perform(\n\t\t\t\tQueryPerform.DELETE,\n\t\t\t\t{\n\t\t\t\t\tid: id,\n\t\t\t\t\tschema,\n\t\t\t\t\tsoftDelete,\n\t\t\t\t},\n\t\t\t\tx_request_id,\n\t\t\t)\n\t\t\tawait this.websocket.publish(schema, PublishType.DELETE, id)\n\t\t\tawait this.webhook.publish(schema, PublishType.DELETE, id, auth.user_identifier)\n\t\t\tawait this.dataCache.ping(table_name)\n\t\t\treturn res.status(200).send(result)\n\t\t} catch (e) {\n\t\t\treturn res.status(400).send(this.response.text(e.message))\n\t\t}\n\t}\n\n\t@Delete('*/')\n\tasync deleteMany(\n\t\t@Req() req,\n\t\t@Res() res,\n\t\t@Headers() headers: HeaderParams,\n\t\t@Body() body: Partial<any> | Partial<any>[],\n\t\t@QueryParams('hard') hard = false,\n\t): Promise<DeleteManyResponseObject> {\n\t\tconst x_request_id = headers['x-request-id']\n\t\tlet table_name = UrlToTable(req.originalUrl, 1)\n\n\t\tif (table_name === 'webhook') {\n\t\t\ttable_name = LLANA_WEBHOOK_TABLE\n\t\t}\n\n\t\tlet schema: DataSourceSchema\n\n\t\ttry {\n\t\t\tschema = await this.schema.getSchema({ table: table_name, x_request_id })\n\t\t} catch (e) {\n\t\t\treturn res.status(404).send(this.response.text(e.message))\n\t\t}\n\n\t\t// Is the table public?\n\t\tlet auth = await this.authentication.public({\n\t\t\ttable: table_name,\n\t\t\taccess_level: RolePermission.DELETE,\n\t\t\tx_request_id,\n\t\t})\n\n\t\t// If not public, perform auth\n\t\tif (!auth.valid) {\n\t\t\tauth = await this.authentication.auth({\n\t\t\t\ttable: table_name,\n\t\t\t\tx_request_id,\n\t\t\t\taccess: RolePermission.DELETE,\n\t\t\t\theaders: req.headers,\n\t\t\t\tbody: req.body,\n\t\t\t\tquery: req.query,\n\t\t\t})\n\t\t\tif (!auth.valid) {\n\t\t\t\treturn res.status(401).send(this.response.text(auth.message))\n\t\t\t}\n\t\t}\n\n\t\t//validate :id field\n\t\tconst primary_key = this.schema.getPrimaryKey(schema)\n\n\t\tif (!primary_key) {\n\t\t\treturn res.status(400).send(this.response.text(`No primary key found for table ${table_name}`))\n\t\t}\n\n\t\tif (body instanceof Array) {\n\t\t\tlet total = body.length\n\t\t\tlet deleted = 0\n\t\t\tlet errored = 0\n\t\t\tconst errors = []\n\n\t\t\tfor (const item of body) {\n\t\t\t\tif (auth.user_identifier) {\n\t\t\t\t\tconst permission = await this.roles.tablePermission({\n\t\t\t\t\t\tidentifier: auth.user_identifier,\n\t\t\t\t\t\ttable: table_name,\n\t\t\t\t\t\taccess: RolePermission.DELETE,\n\t\t\t\t\t\tdata: item,\n\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t})\n\n\t\t\t\t\tif (!permission.valid) {\n\t\t\t\t\t\terrored++\n\t\t\t\t\t\terrors.push({\n\t\t\t\t\t\t\titem: body.indexOf(item),\n\t\t\t\t\t\t\tmessage: this.response.text((permission as AuthTablePermissionFailResponse).message),\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst id = item[primary_key]\n\n\t\t\t\tconst validateKey = await this.schema.validateData(schema, { [primary_key]: id })\n\t\t\t\tif (!validateKey.valid) {\n\t\t\t\t\treturn res.status(400).send(this.response.text(validateKey.message))\n\t\t\t\t}\n\n\t\t\t\tconst where = <DataSourceWhere[]>[\n\t\t\t\t\t{\n\t\t\t\t\t\tcolumn: primary_key,\n\t\t\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\t\t\tvalue: id,\n\t\t\t\t\t},\n\t\t\t\t]\n\n\t\t\t\t//Check record exists\n\n\t\t\t\tconst record = (await this.query.perform(\n\t\t\t\t\tQueryPerform.FIND_ONE,\n\t\t\t\t\t{\n\t\t\t\t\t\tschema,\n\t\t\t\t\t\twhere,\n\t\t\t\t\t},\n\t\t\t\t\tx_request_id,\n\t\t\t\t)) as FindOneResponseObject\n\n\t\t\t\tif (!record) {\n\t\t\t\t\terrored++\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\titem: body.indexOf(item),\n\t\t\t\t\t\tmessage: `Record with id ${id} not found`,\n\t\t\t\t\t})\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t//Soft or Hard delete check\n\t\t\t\tconst databaseConfig: DataSourceConfig = this.configService.get('database')\n\n\t\t\t\tlet softDelete: string = null\n\n\t\t\t\tif (\n\t\t\t\t\t!hard &&\n\t\t\t\t\tdatabaseConfig.deletes.soft &&\n\t\t\t\t\tschema.columns.find(col => col.field === databaseConfig.deletes.soft)\n\t\t\t\t) {\n\t\t\t\t\tsoftDelete = databaseConfig.deletes.soft\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tawait this.query.perform(\n\t\t\t\t\t\tQueryPerform.DELETE,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tid: id,\n\t\t\t\t\t\t\tschema,\n\t\t\t\t\t\t\tsoftDelete,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t)\n\t\t\t\t\tawait this.websocket.publish(schema, PublishType.DELETE, id)\n\t\t\t\t\tawait this.webhook.publish(schema, PublishType.DELETE, id, auth.user_identifier)\n\t\t\t\t\tdeleted++\n\t\t\t\t} catch (e) {\n\t\t\t\t\terrored++\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\titem: body.indexOf(item),\n\t\t\t\t\t\tmessage: e.message,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawait this.dataCache.ping(table_name)\n\n\t\t\treturn res.status(200).send({\n\t\t\t\ttotal,\n\t\t\t\tdeleted,\n\t\t\t\terrored,\n\t\t\t\terrors,\n\t\t\t} as DeleteManyResponseObject)\n\t\t} else {\n\t\t\treturn res.status(400).send(this.response.text('Body must be an array'))\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/app.controller.docs.ts",
    "content": "import { Controller, Get, Res } from '@nestjs/common'\nimport * as fs from 'fs'\n\nimport { version } from '../package.json'\nimport { Documentation } from './helpers/Documentation'\nimport { RedocOptions } from './utils/redoc/interfaces/redoc.interface'\nimport { RedocModule } from './utils/redoc/redoc'\n\n@Controller()\nexport class DocsController {\n\tconstructor(private readonly documentation: Documentation) {}\n\n\t@Get('/')\n\tasync index(@Res() res) {\n\t\tif (this.documentation.skipDocs()) {\n\t\t\treturn res.json({ version })\n\t\t} else {\n\t\t\tconst redoc: RedocOptions = {\n\t\t\t\ttitle: process.env.DOCS_TITLE ?? 'API Documentation',\n\t\t\t\tdocUrl: '/openapi.json',\n\t\t\t}\n\n\t\t\treturn res.send(await RedocModule.setup(redoc))\n\t\t}\n\t}\n\n\t@Get('/openapi.json')\n\topenapi(@Res() res): string {\n\t\tif (this.documentation.skipDocs()) {\n\t\t\treturn res.json({ version })\n\t\t} else {\n\t\t\treturn res.json(JSON.parse(fs.readFileSync('openapi.json', 'utf8')))\n\t\t}\n\t}\n\n\t@Get('/favicon.ico')\n\tfav(@Res() res): string {\n\t\treturn res.sendFile('favicon.ico', { root: 'public' })\n\t}\n}\n"
  },
  {
    "path": "src/app.controller.get.test.spec.ts",
    "content": "import { INestApplication } from '@nestjs/common'\nimport { ConfigFactory, ConfigModule, ConfigService } from '@nestjs/config'\nimport { JwtModule } from '@nestjs/jwt'\nimport { Test } from '@nestjs/testing'\nimport * as request from 'supertest'\nimport { CustomerTestingService } from './testing/customer.testing.service'\n\nimport { AppModule } from './app.module'\nimport { Logger } from './helpers/Logger'\nimport { AuthTestingService } from './testing/auth.testing.service'\nimport { EmployeeTestingService } from './testing/employee.testing.service'\nimport { SalesOrderTestingService } from './testing/salesorder.testing.service'\nimport { ShipperTestingService } from './testing/shipper.testing.service'\nimport { TIMEOUT } from './testing/testing.const'\nimport { DataSourceSchema } from './types/datasource.types'\n\n// Import configs\nimport auth from './config/auth.config'\nimport database from './config/database.config'\nimport { envValidationSchema } from './config/env.validation'\nimport hosts from './config/hosts.config'\nimport jwt from './config/jwt.config'\nimport roles from './config/roles.config'\nimport { UserTestingService } from './testing/user.testing.service'\nimport { RolePermission } from './types/roles.types'\n\n// Type the config imports\nconst configs: ConfigFactory[] = [auth, database, hosts, jwt, roles]\n\ndescribe('App > Controller > Get', () => {\n\tlet app: INestApplication\n\n\tlet authTestingService: AuthTestingService\n\tlet customerTestingService: CustomerTestingService\n\tlet employeeTestingService: EmployeeTestingService\n\tlet shipperTestingService: ShipperTestingService\n\tlet userTestingService: UserTestingService\n\n\tlet salesOrderTestingService: SalesOrderTestingService\n\n\tlet customerSchema: DataSourceSchema\n\tlet employeeSchema: DataSourceSchema\n\tlet shipperSchema: DataSourceSchema\n\tlet salesOrderSchema: DataSourceSchema\n\tlet userSchema: DataSourceSchema\n\n\tlet customer: any\n\tlet employee: any\n\tlet shipper: any\n\tlet orders = []\n\n\tlet jwt: string\n\tlet userId: any\n\tlet user: any\n\tlet logger = new Logger()\n\n\tbeforeAll(async () => {\n\t\tconst moduleRef = await Test.createTestingModule({\n\t\t\timports: [\n\t\t\t\tConfigModule.forRoot({\n\t\t\t\t\tload: configs,\n\t\t\t\t\tvalidationSchema: envValidationSchema,\n\t\t\t\t\tisGlobal: true,\n\t\t\t\t}),\n\t\t\t\tJwtModule.registerAsync({\n\t\t\t\t\timports: [ConfigModule],\n\t\t\t\t\tuseFactory: async (configService: ConfigService) => ({\n\t\t\t\t\t\tsecret: configService.get('jwt.secret'),\n\t\t\t\t\t\tsignOptions: configService.get('jwt.signOptions'),\n\t\t\t\t\t}),\n\t\t\t\t\tinject: [ConfigService],\n\t\t\t\t}),\n\t\t\t\tAppModule,\n\t\t\t],\n\t\t\tproviders: [\n\t\t\t\tAuthTestingService,\n\t\t\t\tCustomerTestingService,\n\t\t\t\tEmployeeTestingService,\n\t\t\t\tShipperTestingService,\n\t\t\t\tSalesOrderTestingService,\n\t\t\t\tUserTestingService,\n\t\t\t],\n\t\t\texports: [\n\t\t\t\tAuthTestingService,\n\t\t\t\tCustomerTestingService,\n\t\t\t\tEmployeeTestingService,\n\t\t\t\tShipperTestingService,\n\t\t\t\tSalesOrderTestingService,\n\t\t\t\tUserTestingService,\n\t\t\t],\n\t\t}).compile()\n\t\tapp = moduleRef.createNestApplication()\n\t\tawait app.init()\n\n\t\t// Expose the app object globally for debugging\n\t\t;(global as any).app = app\n\n\t\tauthTestingService = app.get<AuthTestingService>(AuthTestingService)\n\t\tcustomerTestingService = app.get<CustomerTestingService>(CustomerTestingService)\n\t\temployeeTestingService = app.get<EmployeeTestingService>(EmployeeTestingService)\n\t\tshipperTestingService = app.get<ShipperTestingService>(ShipperTestingService)\n\t\tsalesOrderTestingService = app.get<SalesOrderTestingService>(SalesOrderTestingService)\n\t\tuserTestingService = app.get<UserTestingService>(UserTestingService)\n\n\t\tcustomerSchema = await customerTestingService.getSchema()\n\t\temployeeSchema = await employeeTestingService.getSchema()\n\t\tshipperSchema = await shipperTestingService.getSchema()\n\t\tsalesOrderSchema = await salesOrderTestingService.getSchema()\n\t\tuserSchema = await userTestingService.getSchema()\n\n\t\tjwt = await authTestingService.login()\n\t\tuserId = await authTestingService.getUserId(jwt)\n\n\t\tuser = await userTestingService.mockUser()\n\n\t\tconst result = await request(app.getHttpServer())\n\t\t\t.post(`/User/`)\n\t\t\t.send(user)\n\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\n\t\tif (result.status !== 201) {\n\t\t\tthrow new Error('Failed to create user: ' + result.text)\n\t\t}\n\n\t\tuser = result.body\n\t\tcustomer = await customerTestingService.createCustomer({ userId: user[userSchema.primary_key] })\n\t\temployee = await employeeTestingService.createEmployee({})\n\n\t\tshipper = await shipperTestingService.createShipper({})\n\n\t\tfor (let i = 0; i < 10; i++) {\n\t\t\torders.push(\n\t\t\t\tawait salesOrderTestingService.createOrder({\n\t\t\t\t\tcustId: customer[customerSchema.primary_key],\n\t\t\t\t\temployeeId: employee[employeeSchema.primary_key],\n\t\t\t\t\tshipperId: shipper[shipperSchema.primary_key],\n\t\t\t\t}),\n\t\t\t)\n\t\t}\n\t}, TIMEOUT)\n\n\tbeforeEach(() => {\n\t\tlogger.debug('===========================================')\n\t\tlogger.log('🧪 ' + expect.getState().currentTestName)\n\t\tlogger.debug('===========================================')\n\t})\n\n\tdescribe('Get', () => {\n\t\tit('One', async function () {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.get(`/SalesOrder/${orders[0][salesOrderSchema.primary_key]}`)\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(200)\n\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body[salesOrderSchema.primary_key]).toBeDefined()\n\t\t\texpect(result.body.custId).toBeDefined()\n\t\t\texpect(result.body.employeeId).toBeDefined()\n\t\t\texpect(result.body.shipperId).toBeDefined()\n\t\t\texpect(result.body.shipName).toBeDefined()\n\t\t})\n\n\t\tit('One - With Relations', async function () {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.get(`/SalesOrder/${orders[0][salesOrderSchema.primary_key]}?relations=Customer`)\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(200)\n\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body[salesOrderSchema.primary_key]).toBeDefined()\n\t\t\texpect(result.body.custId).toBeDefined()\n\t\t\texpect(result.body.employeeId).toBeDefined()\n\t\t\texpect(result.body.shipperId).toBeDefined()\n\t\t\texpect(result.body.shipName).toBeDefined()\n\t\t\texpect(result.body.Customer[0]).toBeDefined()\n\t\t\texpect(result.body.Customer[0].contactName).toBeDefined()\n\t\t})\n\n\t\tit('One - With Fields', async function () {\n\t\t\tconst result = <any>(\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.get(`/SalesOrder/${orders[0][salesOrderSchema.primary_key]}?fields=shipName`)\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(200)\n\t\t\t)\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body.shipName).toBeDefined()\n\t\t\texpect(result.body.freight).toBeUndefined()\n\t\t\texpect(result.body.shipCity).toBeUndefined()\n\t\t\texpect(result.body.orderDate).toBeUndefined()\n\t\t})\n\n\t\tit('One - With Filters', async function () {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.get(\n\t\t\t\t\t`/SalesOrder/${orders[0][salesOrderSchema.primary_key]}?fields=shipName&shipName=${orders[0].shipName}`,\n\t\t\t\t)\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(200)\n\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body.shipName).toBe(orders[0].shipName)\n\t\t\texpect(result.body.freight).toBeUndefined()\n\t\t\texpect(result.body.shipCity).toBeUndefined()\n\t\t\texpect(result.body.orderDate).toBeUndefined()\n\t\t})\n\t})\n\n\tdescribe('List', () => {\n\t\tit('All', async function () {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.get(`/SalesOrder/`)\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(200)\n\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body.total).toBeDefined()\n\t\t\texpect(result.body.total).toBeGreaterThan(0)\n\t\t\texpect(result.body.data.length).toBeGreaterThan(0)\n\t\t\texpect(result.body.data[0][salesOrderSchema.primary_key]).toBeDefined()\n\t\t\texpect(result.body.data[0].shipName).toBeDefined()\n\t\t})\n\n\t\tit('All - With Relations', async function () {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.get(`/SalesOrder/?relations=Customer`)\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(200)\n\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body.total).toBeDefined()\n\t\t\texpect(result.body.total).toBeGreaterThan(0)\n\t\t\texpect(result.body.data.length).toBeGreaterThan(0)\n\t\t\texpect(result.body.data[0][salesOrderSchema.primary_key]).toBeDefined()\n\t\t\texpect(result.body.data[0].shipName).toBeDefined()\n\t\t\texpect(result.body.data[0].Customer[0]).toBeDefined()\n\t\t\texpect(result.body.data[0].Customer[0].contactName).toBeDefined()\n\t\t})\n\n\t\tit('All - With Fields', async function () {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.get(`/SalesOrder/?fields=shipName`)\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(200)\n\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body.total).toBeDefined()\n\t\t\texpect(result.body.total).toBeGreaterThan(0)\n\t\t\texpect(result.body.data.length).toBeGreaterThan(0)\n\t\t\texpect(result.body.data[0].shipName).toBeDefined()\n\t\t\texpect(result.body.data[0].freight).toBeUndefined()\n\t\t\texpect(result.body.data[0].shipCity).toBeUndefined()\n\t\t})\n\n\t\tit('All - With Filters', async function () {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.get(`/SalesOrder/?fields=shipName&shipName=${orders[0].shipName}`)\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(200)\n\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body.total).toBeDefined()\n\t\t\texpect(result.body.total).toBeGreaterThan(0)\n\t\t\texpect(result.body.data.length).toBeGreaterThan(0)\n\t\t\texpect(result.body.data[0].shipName).toBeDefined()\n\t\t\texpect(result.body.data[0].freight).toBeUndefined()\n\t\t\texpect(result.body.data[0].shipCity).toBeUndefined()\n\t\t})\n\n\t\tit('All - With Limit', async function () {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.get(`/SalesOrder/?limit=3`)\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(200)\n\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body.limit).toBeDefined()\n\t\t\texpect(result.body.limit).toEqual(3)\n\t\t\texpect(result.body.offset).toEqual(0)\n\t\t\texpect(result.body.total).toBeGreaterThan(3)\n\t\t\texpect(result.body.data.length).toEqual(3)\n\t\t})\n\n\t\tit('All - With Offset', async function () {\n\t\t\tconst results = await request(app.getHttpServer())\n\t\t\t\t.get(`/SalesOrder/`)\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(200)\n\n\t\t\texpect(results.body.data.length).toBeGreaterThan(0)\n\n\t\t\tconst results2 = await request(app.getHttpServer())\n\t\t\t\t.get(`/SalesOrder/?offset=${results.body.total - 2}`)\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(200)\n\t\t\texpect(results2.body.data.length).toEqual(2)\n\t\t})\n\n\t\tit('Filters records with \"in\" operator', async function () {\n\t\t\tconst shipNames = [orders[0].shipName, orders[1].shipName]\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.get(`/SalesOrder/?shipName[in]=${shipNames.join(',')}`)\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(200)\n\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body.total).toBeDefined()\n\t\t\texpect(result.body.total).toBeGreaterThan(0)\n\t\t\texpect(result.body.data.length).toBeGreaterThan(0)\n\t\t\texpect(result.body.data.every(order => shipNames.includes(order.shipName))).toBe(true)\n\t\t})\n\n\t\tit('Filters records with \"not_in\" operator', async function () {\n\t\t\tconst shipNames = [orders[0].shipName, orders[1].shipName]\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.get(`/SalesOrder/?shipName[not_in]=${shipNames.join(',')}`)\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(200)\n\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body.total).toBeDefined()\n\t\t\texpect(result.body.total).toBeGreaterThan(0)\n\t\t\texpect(result.body.data.length).toBeGreaterThan(0)\n\t\t\texpect(result.body.data.every(order => !shipNames.includes(order.shipName))).toBe(true)\n\t\t})\n\t})\n\n\tdescribe('Validate response types', () => {\n\t\tlet result: any = {}\n\n\t\tit('Object', async function () {\n\t\t\tresult = <any>(\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.get(`/SalesOrder/${orders[0][salesOrderSchema.primary_key]}`)\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(200)\n\t\t\t)\n\n\t\t\texpect(result.body).toBeDefined()\n\t\t})\n\n\t\tit('String', function () {\n\t\t\texpect(result.body.shipName).toBeDefined()\n\t\t\texpect(result.body.shipName).not.toBeNull()\n\t\t\texpect(typeof result.body.shipName).toBe('string')\n\t\t})\n\n\t\tit('Number', function () {\n\t\t\texpect(result.body.freight).toBeDefined()\n\t\t\texpect(result.body.freight).not.toBeNull()\n\t\t\texpect(typeof result.body.freight).toBe('number')\n\t\t})\n\n\t\tit('Boolean', function () {\n\t\t\t//TODO: Add boolean field to the schema\n\t\t})\n\n\t\tit('Date', function () {\n\t\t\texpect(result.body.orderDate).not.toBeNull()\n\t\t\texpect(new Date(result.body.orderDate)).toBeInstanceOf(Date)\n\t\t\texpect(result.body.orderDate).toBeTruthy()\n\t\t\texpect(result.body.deletedAt).toBeFalsy()\n\t\t})\n\n\t\tit('Enum', function () {\n\t\t\t//TODO: Add enum field to the schema\n\t\t})\n\t})\n\n\tdescribe('Public Fetch', () => {\n\t\tit('Default public fail to fetch', async function () {\n\t\t\tawait request(app.getHttpServer()).get(`/SalesOrder/${orders[0][salesOrderSchema.primary_key]}`).expect(401)\n\t\t})\n\n\t\tit('Can fetch with READ permissions', async function () {\n\t\t\tconst public_table_record = await authTestingService.createPublicTablesRecord({\n\t\t\t\ttable: salesOrderSchema.table,\n\t\t\t\taccess_level: RolePermission.READ,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.get(`/SalesOrder/${orders[0][salesOrderSchema.primary_key]}`)\n\t\t\t\t\t.expect(200)\n\n\t\t\t\texpect(result.body).toBeDefined()\n\t\t\t\texpect(result.body[salesOrderSchema.primary_key]).toBeDefined()\n\t\t\t\texpect(result.body.custId).toBeDefined()\n\t\t\t\texpect(result.body.employeeId).toBeDefined()\n\t\t\t\texpect(result.body.shipperId).toBeDefined()\n\t\t\t\texpect(result.body.shipName).toBeDefined()\n\t\t\t\texpect(result.body.freight).toBeDefined()\n\t\t\t\texpect(result.body.orderDate).toBeDefined()\n\t\t\t\texpect(result.body.shipCity).toBeDefined()\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deletePublicTablesRecord(public_table_record)\n\t\t\t}\n\t\t})\n\n\t\tit('Can fetch with READ permissions and allowed fields', async function () {\n\t\t\tconst public_table_record = await authTestingService.createPublicTablesRecord({\n\t\t\t\ttable: salesOrderSchema.table,\n\t\t\t\taccess_level: RolePermission.READ,\n\t\t\t\tallowed_fields: 'freight,orderDate',\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.get(`/SalesOrder/${orders[0][salesOrderSchema.primary_key]}`)\n\t\t\t\t\t.expect(200)\n\n\t\t\t\texpect(result.body).toBeDefined()\n\t\t\t\texpect(result.body[salesOrderSchema.primary_key]).toBeUndefined()\n\t\t\t\texpect(result.body.custId).toBeUndefined()\n\t\t\t\texpect(result.body.employeeId).toBeUndefined()\n\t\t\t\texpect(result.body.shipperId).toBeUndefined()\n\t\t\t\texpect(result.body.shipName).toBeUndefined()\n\t\t\t\texpect(result.body.freight).toBeDefined()\n\t\t\t\texpect(result.body.orderDate).toBeDefined()\n\t\t\t\texpect(result.body.shipCity).toBeUndefined()\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deletePublicTablesRecord(public_table_record)\n\t\t\t}\n\t\t})\n\n\t\tit('Can fetch with WRITE permissions', async function () {\n\t\t\tconst public_table_record = await authTestingService.createPublicTablesRecord({\n\t\t\t\ttable: salesOrderSchema.table,\n\t\t\t\taccess_level: RolePermission.WRITE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.get(`/SalesOrder/${orders[0][salesOrderSchema.primary_key]}`)\n\t\t\t\t\t.expect(200)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deletePublicTablesRecord(public_table_record)\n\t\t\t}\n\t\t})\n\n\t\tit('Can fetch with READ permissions and allowed fields, check relation permissions', async function () {\n\t\t\tconst public_table_customers = await authTestingService.createPublicTablesRecord({\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\taccess_level: RolePermission.READ,\n\t\t\t\tallowed_fields: 'companyName',\n\t\t\t})\n\n\t\t\tconst public_table_sales = await authTestingService.createPublicTablesRecord({\n\t\t\t\ttable: salesOrderSchema.table,\n\t\t\t\taccess_level: RolePermission.READ,\n\t\t\t\tallowed_fields: salesOrderSchema.primary_key + ',custId,freight,orderDate',\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.get(`/SalesOrder/${orders[0][salesOrderSchema.primary_key]}?relations=Customer`)\n\t\t\t\t\t.expect(200)\n\t\t\t\texpect(result.body).toBeDefined()\n\t\t\t\texpect(result.body[salesOrderSchema.primary_key]).toBeDefined()\n\t\t\t\texpect(result.body.custId).toBeDefined()\n\t\t\t\texpect(result.body.employeeId).toBeUndefined()\n\t\t\t\texpect(result.body.shipperId).toBeUndefined()\n\t\t\t\texpect(result.body.shipName).toBeUndefined()\n\t\t\t\texpect(result.body.freight).toBeDefined()\n\t\t\t\texpect(result.body.orderDate).toBeDefined()\n\t\t\t\texpect(result.body.shipCity).toBeUndefined()\n\t\t\t\texpect(result.body.Customer[0]).toBeDefined()\n\t\t\t\texpect(result.body.Customer[0][customerSchema.primary_key]).toBeUndefined()\n\t\t\t\texpect(result.body.Customer[0].companyName).toBeDefined()\n\t\t\t\texpect(result.body.Customer[0].contactName).toBeUndefined()\n\t\t\t\texpect(result.body.Customer[0].contactTitle).toBeUndefined()\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deletePublicTablesRecord(public_table_customers)\n\t\t\t\tawait authTestingService.deletePublicTablesRecord(public_table_sales)\n\t\t\t}\n\t\t})\n\t})\n\n\tdescribe('Role Based Fetching', () => {\n\t\tit('No table role, gets record', async function () {\n\t\t\tawait request(app.getHttpServer())\n\t\t\t\t.get(`/Customer/${customer[customerSchema.primary_key]}`)\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(200)\n\t\t})\n\n\t\tit('DELETE table role, get record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.DELETE,\n\t\t\t\town_records: RolePermission.DELETE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.get(`/Customer/${customer[customerSchema.primary_key]}`)\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(200)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('DELETE table role, own records, fails to get someone elses record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.NONE,\n\t\t\t\town_records: RolePermission.DELETE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.get(`/Customer/${customer[customerSchema.primary_key]}`)\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(204)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('WRITE table role, get record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.WRITE,\n\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.get(`/Customer/${customer[customerSchema.primary_key]}`)\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(200)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('WRITE table role, own records, fails to get someone elses record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.NONE,\n\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.get(`/Customer/${customer[customerSchema.primary_key]}`)\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(204)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('READ table role, gets record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.READ,\n\t\t\t\town_records: RolePermission.READ,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.get(`/Customer/${customer[customerSchema.primary_key]}`)\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(200)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('READ table role, own records, fails to get someone elses record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.NONE,\n\t\t\t\town_records: RolePermission.READ,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.get(`/Customer/${customer[customerSchema.primary_key]}`)\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(204)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\t})\n\n\tdescribe('Allowed Fields Results', () => {\n\t\tit('As standard, all fields returned', async function () {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.get(`/Customer/${customer[customerSchema.primary_key]}`)\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(200)\n\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body[customerSchema.primary_key]).toBeDefined()\n\t\t\texpect(result.body.companyName).toBeDefined()\n\t\t\texpect(result.body.contactName).toBeDefined()\n\t\t\texpect(result.body.contactTitle).toBeDefined()\n\t\t\texpect(result.body.address).toBeDefined()\n\t\t\texpect(result.body.city).toBeDefined()\n\t\t\texpect(result.body.region).toBeDefined()\n\t\t\texpect(result.body.postalCode).toBeDefined()\n\t\t\texpect(result.body.country).toBeDefined()\n\t\t\texpect(result.body.phone).toBeDefined()\n\t\t\texpect(result.body.fax).toBeDefined()\n\t\t})\n\n\t\tit('When allowed_fields are passed, only return these fields', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.WRITE,\n\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t\tallowed_fields: 'companyName,contactName',\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.get(`/Customer/${customer[customerSchema.primary_key]}`)\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(200)\n\n\t\t\t\texpect(result.body).toBeDefined()\n\t\t\t\texpect(result.body[customerSchema.primary_key]).toBeUndefined()\n\t\t\t\texpect(result.body.companyName).toBeDefined()\n\t\t\t\texpect(result.body.contactName).toBeDefined()\n\t\t\t\texpect(result.body.contactTitle).toBeUndefined()\n\t\t\t\texpect(result.body.address).toBeUndefined()\n\t\t\t\texpect(result.body.city).toBeUndefined()\n\t\t\t\texpect(result.body.region).toBeUndefined()\n\t\t\t\texpect(result.body.postalCode).toBeUndefined()\n\t\t\t\texpect(result.body.country).toBeUndefined()\n\t\t\t\texpect(result.body.phone).toBeUndefined()\n\t\t\t\texpect(result.body.fax).toBeUndefined()\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('When allowed_fields are passed, only return these fields, even when there is a public_table view', async function () {\n\t\t\tconst public_table_record = await authTestingService.createPublicTablesRecord({\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\taccess_level: RolePermission.WRITE,\n\t\t\t\tallowed_fields: 'companyName',\n\t\t\t})\n\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.WRITE,\n\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t\tallowed_fields: 'companyName,contactName',\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.get(`/Customer/${customer[customerSchema.primary_key]}`)\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(200)\n\n\t\t\t\texpect(result.body).toBeDefined()\n\t\t\t\texpect(result.body[customerSchema.primary_key]).toBeUndefined()\n\t\t\t\texpect(result.body.companyName).toBeDefined()\n\t\t\t\texpect(result.body.contactName).toBeDefined()\n\t\t\t\texpect(result.body.contactTitle).toBeUndefined()\n\t\t\t\texpect(result.body.address).toBeUndefined()\n\t\t\t\texpect(result.body.city).toBeUndefined()\n\t\t\t\texpect(result.body.region).toBeUndefined()\n\t\t\t\texpect(result.body.postalCode).toBeUndefined()\n\t\t\t\texpect(result.body.country).toBeUndefined()\n\t\t\t\texpect(result.body.phone).toBeUndefined()\n\t\t\t\texpect(result.body.fax).toBeUndefined()\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t\tawait authTestingService.deletePublicTablesRecord(public_table_record)\n\t\t\t}\n\t\t})\n\n\t\tit('When allowed_fields are passed, only return these fields even with fields passed', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.WRITE,\n\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t\tallowed_fields: 'companyName,contactName',\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.get(\n\t\t\t\t\t\t`/Customer/${customer[customerSchema.primary_key]}?fields=companyName,contactName,contactTitle`,\n\t\t\t\t\t)\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(200)\n\n\t\t\t\texpect(result.body).toBeDefined()\n\t\t\t\texpect(result.body[customerSchema.primary_key]).toBeUndefined()\n\t\t\t\texpect(result.body.companyName).toBeDefined()\n\t\t\t\texpect(result.body.contactName).toBeDefined()\n\t\t\t\texpect(result.body.contactTitle).toBeUndefined()\n\t\t\t\texpect(result.body.address).toBeUndefined()\n\t\t\t\texpect(result.body.city).toBeUndefined()\n\t\t\t\texpect(result.body.region).toBeUndefined()\n\t\t\t\texpect(result.body.postalCode).toBeUndefined()\n\t\t\t\texpect(result.body.country).toBeUndefined()\n\t\t\t\texpect(result.body.phone).toBeUndefined()\n\t\t\t\texpect(result.body.fax).toBeUndefined()\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('As standard, all fields returned, with relations', async function () {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.get(`/Customer/${customer[customerSchema.primary_key]}?relations=User`)\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(200)\n\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body[customerSchema.primary_key]).toBeDefined()\n\t\t\texpect(result.body.companyName).toBeDefined()\n\t\t\texpect(result.body.contactName).toBeDefined()\n\t\t\texpect(result.body.contactTitle).toBeDefined()\n\t\t\texpect(result.body.address).toBeDefined()\n\t\t\texpect(result.body.city).toBeDefined()\n\t\t\texpect(result.body.region).toBeDefined()\n\t\t\texpect(result.body.postalCode).toBeDefined()\n\t\t\texpect(result.body.country).toBeDefined()\n\t\t\texpect(result.body.phone).toBeDefined()\n\t\t\texpect(result.body.fax).toBeDefined()\n\t\t\texpect(result.body.User[0]).toBeDefined()\n\t\t\texpect(result.body.User[0][userSchema.primary_key]).toBeDefined()\n\t\t\texpect(result.body.User[0].email).toBeDefined()\n\t\t\texpect(result.body.User[0].password).toBeDefined()\n\t\t\texpect(result.body.User[0].role).toBeDefined()\n\t\t\texpect(result.body.User[0].firstName).toBeDefined()\n\t\t\texpect(result.body.User[0].lastName).toBeDefined()\n\t\t})\n\n\t\tit('When allowed_fields are passed, only return these fields, with relations', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.WRITE,\n\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t\tallowed_fields: 'companyName,contactName,userId,User.email',\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.get(`/Customer/${customer[customerSchema.primary_key]}?relations=User`)\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(200)\n\n\t\t\t\texpect(result.body).toBeDefined()\n\t\t\t\texpect(result.body[customerSchema.primary_key]).toBeUndefined()\n\t\t\t\texpect(result.body.companyName).toBeDefined()\n\t\t\t\texpect(result.body.contactName).toBeDefined()\n\t\t\t\texpect(result.body.contactTitle).toBeUndefined()\n\t\t\t\texpect(result.body.address).toBeUndefined()\n\t\t\t\texpect(result.body.city).toBeUndefined()\n\t\t\t\texpect(result.body.region).toBeUndefined()\n\t\t\t\texpect(result.body.postalCode).toBeUndefined()\n\t\t\t\texpect(result.body.country).toBeUndefined()\n\t\t\t\texpect(result.body.phone).toBeUndefined()\n\t\t\t\texpect(result.body.fax).toBeUndefined()\n\t\t\t\texpect(result.body.User[0]).toBeDefined()\n\t\t\t\texpect(result.body.User[0][userSchema.primary_key]).toBeUndefined()\n\t\t\t\texpect(result.body.User[0].email).toBeDefined()\n\t\t\t\texpect(result.body.User[0].password).toBeUndefined()\n\t\t\t\texpect(result.body.User[0].role).toBeUndefined()\n\t\t\t\texpect(result.body.User[0].firstName).toBeUndefined()\n\t\t\t\texpect(result.body.User[0].lastName).toBeUndefined()\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('When allowed_fields are passed, only return these fields even with fields passe, with relations', async function () {\n\t\t\tconst role_salesOrder = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: salesOrderSchema.table,\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.WRITE,\n\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t\tallowed_fields: salesOrderSchema.primary_key + ',custId,shipName',\n\t\t\t})\n\n\t\t\tconst role_customer = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.WRITE,\n\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t\tallowed_fields: 'companyName,contactName',\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.get(`/SalesOrder/${orders[0][salesOrderSchema.primary_key]}?relations=Customer`)\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(200)\n\n\t\t\t\texpect(result.body).toBeDefined()\n\t\t\t\texpect(result.body[salesOrderSchema.primary_key]).toBeDefined()\n\t\t\t\texpect(result.body.custId).toBeDefined()\n\t\t\t\texpect(result.body.shipName).toBeDefined()\n\t\t\t\texpect(result.body.freight).toBeUndefined()\n\t\t\t\texpect(result.body.shipCity).toBeUndefined()\n\t\t\t\texpect(result.body.orderDate).toBeUndefined()\n\t\t\t\texpect(result.body.Customer[0]).toBeDefined()\n\t\t\t\texpect(result.body.Customer[0].companyName).toBeDefined()\n\t\t\t\texpect(result.body.Customer[0].contactName).toBeDefined()\n\t\t\t\texpect(result.body.Customer[0].contactTitle).toBeUndefined()\n\t\t\t\texpect(result.body.Customer[0].address).toBeUndefined()\n\t\t\t\texpect(result.body.Customer[0].city).toBeUndefined()\n\t\t\t\texpect(result.body.Customer[0].region).toBeUndefined()\n\t\t\t\texpect(result.body.Customer[0].postalCode).toBeUndefined()\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role_salesOrder)\n\t\t\t\tawait authTestingService.deleteRole(role_customer)\n\t\t\t}\n\t\t})\n\t})\n\n\tafterAll(async () => {\n\t\tfor (const order of orders) {\n\t\t\t// console.debug('delete order #' + order[salesOrderSchema.primary_key])\n\t\t\tawait salesOrderTestingService.deleteOrder(order[salesOrderSchema.primary_key])\n\t\t}\n\t\tawait customerTestingService.deleteCustomer(customer[customerSchema.primary_key])\n\t\tawait employeeTestingService.deleteEmployee(employee[employeeSchema.primary_key])\n\t\tawait shipperTestingService.deleteShipper(shipper[shipperSchema.primary_key])\n\t\tawait userTestingService.deleteUser(user[userSchema.primary_key])\n\t\tawait app.close()\n\t}, TIMEOUT)\n})\n"
  },
  {
    "path": "src/app.controller.get.ts",
    "content": "import { Controller, Get, Headers, Param, ParseArrayPipe, Query as QueryParams, Req, Res } from '@nestjs/common'\nimport { ConfigService } from '@nestjs/config'\n\nimport { LLANA_WEBHOOK_TABLE } from './app.constants'\nimport { FindManyQueryParams, HeaderParams } from './dtos/requests.dto'\nimport { FindManyResponseObject, FindOneResponseObject } from './dtos/response.dto'\nimport { Authentication } from './helpers/Authentication'\nimport { UrlToTable } from './helpers/Database'\nimport { Pagination } from './helpers/Pagination'\nimport { Query } from './helpers/Query'\nimport { Response } from './helpers/Response'\nimport { Roles } from './helpers/Roles'\nimport { Schema } from './helpers/Schema'\nimport { DataCacheService } from './modules/cache/dataCache.service'\nimport { AuthTablePermissionFailResponse, AuthTablePermissionSuccessResponse } from './types/auth.types'\nimport {\n\tDataSourceFindManyOptions,\n\tDataSourceFindOneOptions,\n\tDataSourceSchema,\n\tQueryPerform,\n\tWhereOperator,\n} from './types/datasource.types'\nimport { RolePermission } from './types/roles.types'\n\n@Controller()\nexport class GetController {\n\tconstructor(\n\t\tprivate readonly authentication: Authentication,\n\t\tprivate readonly configService: ConfigService,\n\t\tprivate readonly dataCache: DataCacheService,\n\t\tprivate readonly pagination: Pagination,\n\t\tprivate readonly query: Query,\n\t\tprivate readonly response: Response,\n\t\tprivate readonly roles: Roles,\n\t\tprivate readonly schema: Schema,\n\t) {}\n\n\t@Get('/tables')\n\tasync listTables(@Req() req, @Res() res, @Headers() headers: HeaderParams): Promise<DataSourceSchema> {\n\t\tconst x_request_id = headers['x-request-id']\n\n\t\tconst auth = await this.authentication.auth({\n\t\t\ttable: '',\n\t\t\tx_request_id,\n\t\t\taccess: RolePermission.READ,\n\t\t\theaders: req.headers,\n\t\t\tbody: req.body,\n\t\t\tquery: req.query,\n\t\t})\n\t\tif (!auth.valid) {\n\t\t\treturn res.status(401).send(this.response.text(auth.message))\n\t\t}\n\n\t\t//TODO - only return tables that the user has access to\n\n\t\treturn res.status(200).send(await this.query.perform(QueryPerform.LIST_TABLES, undefined, x_request_id))\n\t}\n\n\t@Get('*/schema')\n\tasync getSchema(@Req() req, @Res() res, @Headers() headers: HeaderParams): Promise<DataSourceSchema> {\n\t\tconst x_request_id = headers['x-request-id']\n\n\t\tconst table_name = UrlToTable(req.originalUrl, 1)\n\n\t\tlet schema: DataSourceSchema\n\t\tconst role_where = []\n\t\tlet queryFields = []\n\n\t\t// Is the table public?\n\t\tconst public_auth = await this.authentication.public({\n\t\t\ttable: table_name,\n\t\t\taccess_level: RolePermission.READ,\n\t\t\tx_request_id,\n\t\t})\n\n\t\tif (public_auth.valid && public_auth.allowed_fields?.length) {\n\t\t\tif (!queryFields?.length) {\n\t\t\t\tqueryFields = public_auth.allowed_fields\n\t\t\t} else {\n\t\t\t\tqueryFields = queryFields.filter(field => public_auth.allowed_fields.includes(field))\n\t\t\t}\n\t\t}\n\n\t\t// If not public, perform auth\n\n\t\tconst auth = await this.authentication.auth({\n\t\t\ttable: table_name,\n\t\t\tx_request_id,\n\t\t\taccess: RolePermission.READ,\n\t\t\theaders: req.headers,\n\t\t\tbody: req.body,\n\t\t\tquery: req.query,\n\t\t})\n\t\tif (!public_auth.valid && !auth.valid) {\n\t\t\treturn res.status(401).send(this.response.text(auth.message))\n\t\t}\n\n\t\t//perform role check\n\t\tif (auth.user_identifier) {\n\t\t\tconst permission = await this.roles.tablePermission({\n\t\t\t\tidentifier: auth.user_identifier,\n\t\t\t\ttable: table_name,\n\t\t\t\taccess: RolePermission.READ,\n\t\t\t\tx_request_id,\n\t\t\t})\n\n\t\t\tif (!public_auth.valid && !permission.valid) {\n\t\t\t\treturn res.status(401).send(this.response.text((permission as AuthTablePermissionFailResponse).message))\n\t\t\t}\n\n\t\t\tif (permission.valid && (permission as AuthTablePermissionSuccessResponse).restriction) {\n\t\t\t\trole_where.push((permission as AuthTablePermissionSuccessResponse).restriction)\n\t\t\t}\n\n\t\t\tif (permission.valid && (permission as AuthTablePermissionSuccessResponse).allowed_fields?.length) {\n\t\t\t\tif (!queryFields?.length) {\n\t\t\t\t\tqueryFields = (permission as AuthTablePermissionSuccessResponse).allowed_fields\n\t\t\t\t} else {\n\t\t\t\t\tqueryFields.push(...(permission as AuthTablePermissionSuccessResponse).allowed_fields)\n\t\t\t\t\tqueryFields = queryFields.filter(field =>\n\t\t\t\t\t\t(permission as AuthTablePermissionSuccessResponse).allowed_fields.includes(field),\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\tschema = await this.schema.getSchema({ table: table_name, x_request_id, fields: queryFields })\n\t\t} catch (e) {\n\t\t\treturn res.status(404).send(this.response.text(e.message))\n\t\t}\n\n\t\treturn res.status(200).send(schema)\n\t}\n\n\t@Get('*/:id')\n\tasync getById(\n\t\t@Req() req,\n\t\t@Res() res,\n\t\t@Headers() headers: HeaderParams,\n\t\t@Param('id') id: string,\n\t\t@QueryParams('fields', new ParseArrayPipe({ items: String, separator: ',', optional: true }))\n\t\tqueryFields?: string[],\n\t\t@QueryParams('relations', new ParseArrayPipe({ items: String, separator: ',', optional: true }))\n\t\tqueryRelations?: string[],\n\t): Promise<FindOneResponseObject> {\n\t\tconst x_request_id = headers['x-request-id']\n\t\tlet table_name = UrlToTable(req.originalUrl, 1)\n\n\t\tif (table_name === 'webhook') {\n\t\t\ttable_name = LLANA_WEBHOOK_TABLE\n\t\t}\n\n\t\tlet primary_key\n\n\t\tconst options: DataSourceFindOneOptions = {\n\t\t\tschema: null,\n\t\t\tfields: [],\n\t\t\twhere: [],\n\t\t\trelations: [],\n\t\t}\n\n\t\tconst postQueryRelations = []\n\n\t\t// Is the table public?\n\t\tconst public_auth = await this.authentication.public({\n\t\t\ttable: table_name,\n\t\t\taccess_level: RolePermission.READ,\n\t\t\tx_request_id,\n\t\t})\n\n\t\tif (public_auth.valid && public_auth.allowed_fields?.length) {\n\t\t\tif (!queryFields?.length) {\n\t\t\t\tqueryFields = public_auth.allowed_fields\n\t\t\t} else {\n\t\t\t\tqueryFields = queryFields.filter(field => public_auth.allowed_fields.includes(field))\n\t\t\t}\n\t\t}\n\n\t\t// If not public, perform auth\n\n\t\tconst auth = await this.authentication.auth({\n\t\t\ttable: table_name,\n\t\t\tx_request_id,\n\t\t\taccess: RolePermission.READ,\n\t\t\theaders: req.headers,\n\t\t\tbody: req.body,\n\t\t\tquery: req.query,\n\t\t})\n\t\tif (!public_auth.valid && !auth.valid) {\n\t\t\treturn res.status(401).send(this.response.text(auth.message))\n\t\t}\n\n\t\t//perform role check\n\t\tif (auth.user_identifier) {\n\t\t\tlet permission = await this.roles.tablePermission({\n\t\t\t\tidentifier: auth.user_identifier,\n\t\t\t\ttable: table_name,\n\t\t\t\taccess: RolePermission.READ,\n\t\t\t\tx_request_id,\n\t\t\t})\n\n\t\t\tif (!public_auth.valid && !permission.valid) {\n\t\t\t\treturn res.status(401).send(this.response.text((permission as AuthTablePermissionFailResponse).message))\n\t\t\t}\n\n\t\t\tif (permission.valid && (permission as AuthTablePermissionSuccessResponse).restriction) {\n\t\t\t\tpermission = permission as AuthTablePermissionSuccessResponse\n\n\t\t\t\tif (permission.restriction.column.includes('.')) {\n\t\t\t\t\toptions.relations.concat(\n\t\t\t\t\t\tawait this.schema.convertDeepWhere({\n\t\t\t\t\t\t\twhere: permission.restriction,\n\t\t\t\t\t\t\tschema: options.schema,\n\t\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t\t}),\n\t\t\t\t\t)\n\t\t\t\t} else {\n\t\t\t\t\toptions.where.push(permission.restriction)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (permission.valid && (permission as AuthTablePermissionSuccessResponse).allowed_fields?.length) {\n\t\t\t\tif (!queryFields?.length) {\n\t\t\t\t\tqueryFields = (permission as AuthTablePermissionSuccessResponse).allowed_fields\n\t\t\t\t} else {\n\t\t\t\t\tqueryFields.push(...(permission as AuthTablePermissionSuccessResponse).allowed_fields)\n\t\t\t\t\tqueryFields = queryFields.filter(field =>\n\t\t\t\t\t\t(permission as AuthTablePermissionSuccessResponse).allowed_fields.includes(field),\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\toptions.schema = await this.schema.getSchema({ table: table_name, x_request_id, fields: queryFields })\n\t\t} catch (e) {\n\t\t\treturn res.status(404).send(this.response.text(e.message))\n\t\t}\n\n\t\t//validate :id field\n\t\tprimary_key = this.schema.getPrimaryKey(options.schema)\n\n\t\tif (!primary_key) {\n\t\t\treturn res.status(400).send(this.response.text(`No primary key found for table ${table_name}`))\n\t\t}\n\n\t\tconst validateKey = await this.schema.validateData(options.schema, { [primary_key]: id })\n\t\tif (!validateKey.valid) {\n\t\t\treturn res.status(400).send(this.response.text(validateKey.message))\n\t\t}\n\n\t\tif (queryFields?.length) {\n\t\t\tconst { valid, message, fields, relations } = await this.schema.validateFields({\n\t\t\t\tschema: options.schema,\n\t\t\t\tfields: queryFields,\n\t\t\t\tx_request_id,\n\t\t\t})\n\t\t\tif (!valid) {\n\t\t\t\treturn res.status(400).send(this.response.text(message))\n\t\t\t}\n\n\t\t\tfor (const field of fields) {\n\t\t\t\tif (!options.fields.includes(field)) {\n\t\t\t\t\toptions.fields.push(field)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (const relation of relations) {\n\t\t\t\tif (!postQueryRelations.find(r => r.table === relation.table)) {\n\t\t\t\t\tpostQueryRelations.push(relation)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (queryRelations?.length) {\n\t\t\tconst { valid, message, relations } = await this.schema.validateRelations({\n\t\t\t\tschema: options.schema,\n\t\t\t\trelation_query: queryRelations,\n\t\t\t\texisting_relations: options.relations,\n\t\t\t\tx_request_id,\n\t\t\t})\n\n\t\t\tif (!valid) {\n\t\t\t\treturn res.status(400).send(this.response.text(message))\n\t\t\t}\n\n\t\t\tfor (const relation of relations) {\n\t\t\t\tif (!postQueryRelations.find(r => r.table === relation.table)) {\n\t\t\t\t\t// Check if the relation has allowed_field restrictions\n\t\t\t\t\tconst relation_public_auth = await this.authentication.public({\n\t\t\t\t\t\ttable: relation.table,\n\t\t\t\t\t\taccess_level: RolePermission.READ,\n\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t})\n\n\t\t\t\t\tif (relation_public_auth.valid && relation_public_auth.allowed_fields?.length) {\n\t\t\t\t\t\trelation.columns = relation.columns.filter(field =>\n\t\t\t\t\t\t\trelation_public_auth.allowed_fields.includes(field),\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\n\t\t\t\t\t// If not public, check role table permissions\n\t\t\t\t\tif (auth.user_identifier) {\n\t\t\t\t\t\tlet permission = await this.roles.tablePermission({\n\t\t\t\t\t\t\tidentifier: auth.user_identifier,\n\t\t\t\t\t\t\ttable: relation.table,\n\t\t\t\t\t\t\taccess: RolePermission.READ,\n\t\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t\t})\n\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tpermission.valid &&\n\t\t\t\t\t\t\t(permission as AuthTablePermissionSuccessResponse).allowed_fields?.length\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\trelation.columns.push(...(permission as AuthTablePermissionSuccessResponse).allowed_fields)\n\t\t\t\t\t\t\trelation.columns = relation.columns.filter(field =>\n\t\t\t\t\t\t\t\t(permission as AuthTablePermissionSuccessResponse).allowed_fields.includes(field),\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tpostQueryRelations.push(relation)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\toptions.where.push({\n\t\t\tcolumn: primary_key,\n\t\t\toperator: WhereOperator.equals,\n\t\t\tvalue: id,\n\t\t})\n\n\t\tif (this.configService.get('database.deletes.soft')) {\n\t\t\toptions.where.push({\n\t\t\t\tcolumn: this.configService.get('database.deletes.soft'),\n\t\t\t\toperator: WhereOperator.null,\n\t\t\t})\n\t\t}\n\n\t\ttry {\n\t\t\tlet result = (await this.query.perform(\n\t\t\t\tQueryPerform.FIND_ONE,\n\t\t\t\toptions,\n\t\t\t\tx_request_id,\n\t\t\t)) as FindOneResponseObject\n\n\t\t\tif (!result) {\n\t\t\t\treturn res.status(204).send(this.response.text(`No record found for id ${id}`))\n\t\t\t}\n\n\t\t\tif (postQueryRelations?.length) {\n\t\t\t\toptions.relations = postQueryRelations\n\t\t\t\tresult = await this.query.buildRelations(options as DataSourceFindOneOptions, result, x_request_id)\n\t\t\t}\n\n\t\t\treturn res.status(200).send(result)\n\t\t} catch (e) {\n\t\t\treturn res.status(400).send(this.response.text(e.message))\n\t\t}\n\t}\n\n\t@Get('*/')\n\tasync list(\n\t\t@Req() req,\n\t\t@Res() res,\n\t\t@Headers() headers: HeaderParams,\n\t\t@QueryParams() queryParams: FindManyQueryParams,\n\t\t@QueryParams('fields', new ParseArrayPipe({ items: String, separator: ',', optional: true }))\n\t\tqueryFields?: string[],\n\t\t@QueryParams('relations', new ParseArrayPipe({ items: String, separator: ',', optional: true }))\n\t\tqueryRelations?: string[],\n\t\t@QueryParams('sort', new ParseArrayPipe({ items: String, separator: ',', optional: true }))\n\t\tquerySort?: string[],\n\t): Promise<FindManyResponseObject> {\n\t\tconst x_request_id = headers['x-request-id']\n\t\tlet table_name = UrlToTable(req.originalUrl, 1)\n\n\t\tif (table_name === 'webhook') {\n\t\t\ttable_name = LLANA_WEBHOOK_TABLE\n\t\t}\n\n\t\tconst options: DataSourceFindManyOptions = {\n\t\t\tschema: null,\n\t\t\tfields: [],\n\t\t\twhere: [],\n\t\t\trelations: [],\n\t\t\tsort: [],\n\t\t}\n\n\t\tconst postQueryRelations = []\n\n\t\t// Is the table public?\n\t\tconst public_auth = await this.authentication.public({\n\t\t\ttable: table_name,\n\t\t\taccess_level: RolePermission.READ,\n\t\t\tx_request_id,\n\t\t})\n\n\t\tif (public_auth.valid && public_auth.allowed_fields?.length) {\n\t\t\tif (!queryFields?.length) {\n\t\t\t\tqueryFields = public_auth.allowed_fields\n\t\t\t} else {\n\t\t\t\tqueryFields = queryFields.filter(field => public_auth.allowed_fields.includes(field))\n\t\t\t}\n\t\t}\n\n\t\t// If not public, perform auth\n\n\t\tconst auth = await this.authentication.auth({\n\t\t\ttable: table_name,\n\t\t\tx_request_id,\n\t\t\taccess: RolePermission.READ,\n\t\t\theaders: req.headers,\n\t\t\tbody: req.body,\n\t\t\tquery: req.query,\n\t\t})\n\t\tif (!public_auth.valid && !auth.valid) {\n\t\t\treturn res.status(401).send(this.response.text(auth.message))\n\t\t}\n\n\t\t//perform role check\n\t\tif (auth.user_identifier) {\n\t\t\tlet permission = await this.roles.tablePermission({\n\t\t\t\tidentifier: auth.user_identifier,\n\t\t\t\ttable: table_name,\n\t\t\t\taccess: RolePermission.READ,\n\t\t\t\tx_request_id,\n\t\t\t})\n\n\t\t\tif (!public_auth.valid && !permission.valid) {\n\t\t\t\treturn res.status(401).send(this.response.text((permission as AuthTablePermissionFailResponse).message))\n\t\t\t}\n\n\t\t\tpermission = permission as AuthTablePermissionSuccessResponse\n\n\t\t\tif (permission.valid && permission.restriction) {\n\t\t\t\tif (permission.restriction.column.includes('.')) {\n\t\t\t\t\toptions.relations = options.relations.concat(\n\t\t\t\t\t\tawait this.schema.convertDeepWhere({\n\t\t\t\t\t\t\twhere: permission.restriction,\n\t\t\t\t\t\t\tschema: options.schema,\n\t\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t\t}),\n\t\t\t\t\t)\n\t\t\t\t} else {\n\t\t\t\t\toptions.where.push(permission.restriction)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (permission.valid && (permission as AuthTablePermissionSuccessResponse).allowed_fields?.length) {\n\t\t\t\tif (!queryFields?.length) {\n\t\t\t\t\tqueryFields = (permission as AuthTablePermissionSuccessResponse).allowed_fields\n\t\t\t\t} else {\n\t\t\t\t\tqueryFields.push(...(permission as AuthTablePermissionSuccessResponse).allowed_fields)\n\t\t\t\t\tqueryFields = queryFields.filter(field =>\n\t\t\t\t\t\t(permission as AuthTablePermissionSuccessResponse).allowed_fields.includes(field),\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\toptions.schema = await this.schema.getSchema({ table: table_name, x_request_id, fields: queryFields })\n\t\t} catch (e) {\n\t\t\treturn res.status(404).send(this.response.text(e.message))\n\t\t}\n\n\t\tconst { limit, offset } = this.pagination.get(queryParams)\n\t\toptions.limit = limit\n\t\toptions.offset = offset\n\n\t\tif (queryFields?.length) {\n\t\t\tconst { valid, message, fields, relations } = await this.schema.validateFields({\n\t\t\t\tschema: options.schema,\n\t\t\t\tfields: queryFields,\n\t\t\t\tx_request_id,\n\t\t\t})\n\t\t\tif (!valid) {\n\t\t\t\treturn res.status(400).send(this.response.text(message))\n\t\t\t}\n\n\t\t\tfor (const field of fields) {\n\t\t\t\tif (!options.fields.includes(field)) {\n\t\t\t\t\toptions.fields.push(field)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (const relation of relations) {\n\t\t\t\tif (!postQueryRelations.find(r => r.table === relation.table)) {\n\t\t\t\t\tpostQueryRelations.push(relation)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (queryRelations?.length) {\n\t\t\tconst { valid, message, relations } = await this.schema.validateRelations({\n\t\t\t\tschema: options.schema,\n\t\t\t\trelation_query: queryRelations,\n\t\t\t\texisting_relations: options.relations,\n\t\t\t\tx_request_id,\n\t\t\t})\n\t\t\tif (!valid) {\n\t\t\t\treturn res.status(400).send(this.response.text(message))\n\t\t\t}\n\n\t\t\tif (relations) {\n\t\t\t\tfor (const relation of relations) {\n\t\t\t\t\tif (!postQueryRelations.find(r => r.table === relation.table)) {\n\t\t\t\t\t\tpostQueryRelations.push(relation)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst validateWhere = await this.schema.validateWhereParams({ schema: options.schema, params: queryParams })\n\t\tif (!validateWhere.valid) {\n\t\t\treturn res.status(400).send(this.response.text(validateWhere.message))\n\t\t}\n\n\t\tif (validateWhere.where.length) {\n\t\t\toptions.where = options.where.concat(validateWhere.where)\n\t\t}\n\n\t\tlet validateSort\n\t\tif (querySort?.length) {\n\t\t\tvalidateSort = this.schema.validateSort({ schema: options.schema, sort: querySort })\n\t\t\tif (!validateSort.valid) {\n\t\t\t\treturn res.status(400).send(this.response.text(validateSort.message))\n\t\t\t}\n\n\t\t\toptions.sort = validateSort.sort\n\t\t}\n\n\t\tif (this.configService.get('database.deletes.soft')) {\n\t\t\toptions.where.push({\n\t\t\t\tcolumn: this.configService.get('database.deletes.soft'),\n\t\t\t\toperator: WhereOperator.null,\n\t\t\t})\n\t\t}\n\n\t\t// Check if we're using the data cache and if so, if we can use it\n\t\tif (this.configService.get<boolean>('USE_DATA_CACHING')) {\n\t\t\tconst cachedResult = await this.dataCache.get({\n\t\t\t\toriginalUrl: req.originalUrl,\n\t\t\t\tx_request_id,\n\t\t\t})\n\n\t\t\tif (cachedResult) {\n\t\t\t\treturn res.status(200).send(cachedResult)\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\tlet result = (await this.query.perform(\n\t\t\t\tQueryPerform.FIND_MANY,\n\t\t\t\toptions,\n\t\t\t\tx_request_id,\n\t\t\t)) as FindManyResponseObject\n\n\t\t\tif (postQueryRelations?.length) {\n\t\t\t\tfor (const r in postQueryRelations) {\n\t\t\t\t\t// Check if the relation has allowed_field restrictions\n\t\t\t\t\tconst relation_public_auth = await this.authentication.public({\n\t\t\t\t\t\ttable: postQueryRelations[r].table,\n\t\t\t\t\t\taccess_level: RolePermission.READ,\n\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t})\n\n\t\t\t\t\tif (relation_public_auth.valid && relation_public_auth.allowed_fields?.length) {\n\t\t\t\t\t\tpostQueryRelations[r].columns = postQueryRelations[r].columns.filter(field =>\n\t\t\t\t\t\t\trelation_public_auth.allowed_fields.includes(field),\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\n\t\t\t\t\t// If not public, check role table permissions\n\t\t\t\t\tif (auth.user_identifier) {\n\t\t\t\t\t\tlet permission = await this.roles.tablePermission({\n\t\t\t\t\t\t\tidentifier: auth.user_identifier,\n\t\t\t\t\t\t\ttable: postQueryRelations[r].table,\n\t\t\t\t\t\t\taccess: RolePermission.READ,\n\t\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t\t})\n\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tpermission.valid &&\n\t\t\t\t\t\t\t(permission as AuthTablePermissionSuccessResponse).allowed_fields?.length\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tpostQueryRelations[r].columns.push(\n\t\t\t\t\t\t\t\t...(permission as AuthTablePermissionSuccessResponse).allowed_fields,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\tpostQueryRelations[r].columns = postQueryRelations[r].columns.filter(field =>\n\t\t\t\t\t\t\t\t(permission as AuthTablePermissionSuccessResponse).allowed_fields.includes(field),\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\toptions.relations = postQueryRelations\n\t\t\t\tfor (const i in result.data) {\n\t\t\t\t\tresult.data[i] = await this.query.buildRelations(\n\t\t\t\t\t\toptions as DataSourceFindOneOptions,\n\t\t\t\t\t\tresult.data[i],\n\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn res.status(200).send(result)\n\t\t} catch (e) {\n\t\t\treturn res.status(400).send(this.response.text(e.message))\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/app.controller.post.test.spec.ts",
    "content": "import { INestApplication } from '@nestjs/common'\nimport { Test } from '@nestjs/testing'\nimport { ConfigModule, ConfigService, ConfigFactory } from '@nestjs/config'\nimport { JwtModule } from '@nestjs/jwt'\nimport * as request from 'supertest'\nimport { CustomerTestingService } from './testing/customer.testing.service'\nimport { AppModule } from './app.module'\nimport { AuthTestingService } from './testing/auth.testing.service'\nimport { DataSourceSchema } from './types/datasource.types'\nimport { UserTestingService } from './testing/user.testing.service'\nimport { EmployeeTestingService } from './testing/employee.testing.service'\nimport { Logger } from './helpers/Logger'\nimport { TIMEOUT } from './testing/testing.const'\n\n// Import configs\nimport auth from './config/auth.config'\nimport database from './config/database.config'\nimport hosts from './config/hosts.config'\nimport jwt from './config/jwt.config'\nimport roles from './config/roles.config'\nimport { envValidationSchema } from './config/env.validation'\nimport exp from 'constants'\nimport { RolePermission } from './types/roles.types'\n\n// Type the config imports\nconst configs: ConfigFactory[] = [auth, database, hosts, jwt, roles]\n\ndescribe('App > Controller > Post', () => {\n\tlet app: INestApplication\n\n\tlet authTestingService: AuthTestingService\n\tlet customerTestingService: CustomerTestingService\n\tlet userTestingService: UserTestingService\n\tlet employeeTestingService: EmployeeTestingService\n\n\tlet customerSchema: DataSourceSchema\n\tlet userSchema: DataSourceSchema\n\n\tlet customers = []\n\tlet user: any\n\n\tlet jwt: string\n\tlet userId: any\n\tlet logger = new Logger()\n\n\tbeforeAll(async () => {\n\t\tconst moduleRef = await Test.createTestingModule({\n\t\t\timports: [\n\t\t\t\tConfigModule.forRoot({\n\t\t\t\t\tload: configs,\n\t\t\t\t\tvalidationSchema: envValidationSchema,\n\t\t\t\t\tisGlobal: true,\n\t\t\t\t}),\n\t\t\t\tJwtModule.registerAsync({\n\t\t\t\t\timports: [ConfigModule],\n\t\t\t\t\tuseFactory: async (configService: ConfigService) => ({\n\t\t\t\t\t\tsecret: configService.get('jwt.secret'),\n\t\t\t\t\t\tsignOptions: configService.get('jwt.signOptions'),\n\t\t\t\t\t}),\n\t\t\t\t\tinject: [ConfigService],\n\t\t\t\t}),\n\t\t\t\tAppModule,\n\t\t\t],\n\t\t\tproviders: [AuthTestingService, CustomerTestingService, UserTestingService, EmployeeTestingService],\n\t\t\texports: [AuthTestingService, CustomerTestingService, UserTestingService, EmployeeTestingService],\n\t\t}).compile()\n\n\t\tapp = moduleRef.createNestApplication()\n\t\tawait app.init()\n\n\t\t// Expose the app object globally for debugging\n\t\t;(global as any).app = app\n\n\t\tauthTestingService = app.get<AuthTestingService>(AuthTestingService)\n\t\tcustomerTestingService = app.get<CustomerTestingService>(CustomerTestingService)\n\t\tuserTestingService = app.get<UserTestingService>(UserTestingService)\n\t\temployeeTestingService = app.get<EmployeeTestingService>(EmployeeTestingService)\n\n\t\tcustomerSchema = await customerTestingService.getSchema()\n\t\tuserSchema = await userTestingService.getSchema()\n\n\t\tjwt = await authTestingService.login()\n\t\tuserId = await authTestingService.getUserId(jwt)\n\n\t\tuser = await userTestingService.mockUser({ email: 'app.controller.post.test.spec.ts@gmail.com' })\n\n\t\tconst result = await request(app.getHttpServer())\n\t\t\t.post(`/User/`)\n\t\t\t.send(user)\n\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\n\t\tif (result.status !== 201) {\n\t\t\tthrow new Error('Failed to create user: ' + result.text)\n\t\t}\n\n\t\texpect(result.body).toBeDefined()\n\t\texpect(result.body.email).toBeDefined()\n\t\texpect(result.body.password).toBeDefined()\n\t\texpect(result.body.password.startsWith('$2')).toBeTruthy()\n\t\tuser = result.body\n\t}, TIMEOUT)\n\n\tbeforeEach(() => {\n\t\tlogger.debug('===========================================')\n\t\tlogger.log('🧪 ' + expect.getState().currentTestName)\n\t\tlogger.debug('===========================================')\n\t})\n\n\tdescribe('Create', () => {\n\t\tit('Create One', async function () {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.post(`/Customer/`)\n\t\t\t\t.send(customerTestingService.mockCustomer(userId))\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(201)\n\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body[customerSchema.primary_key]).toBeDefined()\n\t\t\texpect(result.body.companyName).toBeDefined()\n\t\t\texpect(result.body.contactName).toBeDefined()\n\t\t\tcustomers.push(result.body)\n\t\t})\n\n\t\tit('Create Many', async function () {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.post(`/Customer/`)\n\t\t\t\t.send([customerTestingService.mockCustomer(userId), customerTestingService.mockCustomer(userId)])\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(201)\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body.total).toBeDefined()\n\t\t\texpect(result.body.total).toEqual(2)\n\t\t\texpect(result.body.errored).toBeDefined()\n\t\t\texpect(result.body.errored).toEqual(0)\n\t\t\texpect(result.body.successful).toBeDefined()\n\t\t\texpect(result.body.successful).toEqual(2)\n\t\t\texpect(result.body.data.length).toBeGreaterThan(0)\n\t\t\texpect(result.body.data[0][customerSchema.primary_key]).toBeDefined()\n\t\t\texpect(result.body.data[0].companyName).toBeDefined()\n\t\t\texpect(result.body.data[1][customerSchema.primary_key]).toBeDefined()\n\t\t\texpect(result.body.data[1].companyName).toBeDefined()\n\t\t\tcustomers.push(result.body.data[0])\n\t\t\tcustomers.push(result.body.data[1])\n\t\t})\n\t})\n\n\tdescribe('Create with special characters', () => {\n\t\tit('Create One with special characters !@#$%^&*()_+', async function () {\n\t\t\tconst mock = customerTestingService.mockCustomer(userId)\n\t\t\tmock.companyName = 'Test Company Name - !@#$%^&*()_+'\n\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.post(`/Customer/`)\n\t\t\t\t.send(mock)\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(201)\n\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body[customerSchema.primary_key]).toBeDefined()\n\t\t\texpect(result.body.companyName).toBeDefined()\n\t\t\texpect(result.body.contactName).toBeDefined()\n\t\t\tcustomers.push(result.body)\n\t\t})\n\n\t\tit('Create One with comma', async function () {\n\t\t\tconst mock = customerTestingService.mockCustomer(userId)\n\t\t\tmock.companyName = 'Test Company Name, with comma'\n\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.post(`/Customer/`)\n\t\t\t\t.send(mock)\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(201)\n\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body[customerSchema.primary_key]).toBeDefined()\n\t\t\texpect(result.body.companyName).toBeDefined()\n\t\t\texpect(result.body.contactName).toBeDefined()\n\t\t\tcustomers.push(result.body)\n\t\t})\n\n\t\t// it('Create One with comma in TEXT field', async function () {\n\n\t\t// \tconst mock = employeeTestingService.mockEmployee()\n\t\t// \tmock.notes = 'Test note, with comma'\n\n\t\t// \tconst result = await request(app.getHttpServer())\n\t\t// \t\t.post(`/Employee/`)\n\t\t// \t\t.send(mock)\n\t\t// \t\t.set('Authorization', `Bearer ${jwt}`)\n\n\t\t// \tconsole.log(result.body)\n\t\t// \t\t//.expect(201)\n\n\t\t// \texpect(result.body).toBeDefined()\n\t\t// \texpect(result.body.notes).toBeDefined()\n\t\t// \tcustomers.push(result.body)\n\t\t// })\n\t})\n\n\tdescribe('Public Creation', () => {\n\t\tit('Default public fail to create', async function () {\n\t\t\tawait request(app.getHttpServer())\n\t\t\t\t.post(`/Customer/`)\n\t\t\t\t.send(customerTestingService.mockCustomer(userId))\n\t\t\t\t.expect(401)\n\t\t})\n\n\t\tit('Cannot create with READ permissions', async function () {\n\t\t\tconst public_table_record = await authTestingService.createPublicTablesRecord({\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\taccess_level: RolePermission.READ,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.post(`/Customer/`)\n\t\t\t\t\t.send(customerTestingService.mockCustomer(userId))\n\t\t\t\t\t.expect(401)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deletePublicTablesRecord(public_table_record)\n\t\t\t}\n\t\t})\n\n\t\tit('Can create with WRITE permissions and allowed fields', async function () {\n\t\t\tconst public_table_record = await authTestingService.createPublicTablesRecord({\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\taccess_level: RolePermission.WRITE,\n\t\t\t\tallowed_fields: 'companyName',\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.post(`/Customer/`)\n\t\t\t\t\t.send(customerTestingService.mockCustomer(userId))\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(201)\n\t\t\t\tcustomers.push(result.body)\n\t\t\t\texpect(result.body).toBeDefined()\n\t\t\t\texpect(result.body[customerSchema.primary_key]).toBeUndefined()\n\t\t\t\texpect(result.body.companyName).toBeDefined()\n\t\t\t\texpect(result.body.contactName).toBeUndefined()\n\t\t\t\texpect(result.body.contactTitle).toBeUndefined()\n\t\t\t\texpect(result.body.address).toBeUndefined()\n\t\t\t\texpect(result.body.city).toBeUndefined()\n\t\t\t\texpect(result.body.region).toBeUndefined()\n\t\t\t\texpect(result.body.postalCode).toBeUndefined()\n\t\t\t\texpect(result.body.country).toBeUndefined()\n\t\t\t\texpect(result.body.phone).toBeUndefined()\n\t\t\t\texpect(result.body.fax).toBeUndefined()\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deletePublicTablesRecord(public_table_record)\n\t\t\t}\n\t\t})\n\n\t\tit('Can create with WRITE permissions', async function () {\n\t\t\tconst public_table_record = await authTestingService.createPublicTablesRecord({\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\taccess_level: RolePermission.WRITE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.post(`/Customer/`)\n\t\t\t\t\t.send(customerTestingService.mockCustomer(userId))\n\t\t\t\t\t.expect(201)\n\n\t\t\t\texpect(result.body).toBeDefined()\n\t\t\t\texpect(result.body[customerSchema.primary_key]).toBeDefined()\n\t\t\t\texpect(result.body.companyName).toBeDefined()\n\t\t\t\texpect(result.body.contactName).toBeDefined()\n\t\t\t\tcustomers.push(result.body)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deletePublicTablesRecord(public_table_record)\n\t\t\t}\n\t\t})\n\t})\n\n\tdescribe('Role Based Creation', () => {\n\t\tit('No table role, creates record', async function () {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.post(`/Customer/`)\n\t\t\t\t.send(customerTestingService.mockCustomer(userId))\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(201)\n\t\t\tcustomers.push(result.body)\n\t\t})\n\n\t\tit('DELETE table role, creates record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.DELETE,\n\t\t\t\town_records: RolePermission.DELETE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.post(`/Customer/`)\n\t\t\t\t\t.send(customerTestingService.mockCustomer(userId))\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(201)\n\t\t\t\tcustomers.push(result.body)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('DELETE table role, own records, creates own record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.NONE,\n\t\t\t\town_records: RolePermission.DELETE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.post(`/Customer/`)\n\t\t\t\t\t.send(customerTestingService.mockCustomer(userId))\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(201)\n\t\t\t\tcustomers.push(result.body)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('DELETE table role, own records, fails to create someone elses record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.NONE,\n\t\t\t\town_records: RolePermission.DELETE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.post(`/Customer/`)\n\t\t\t\t\t.send(customerTestingService.mockCustomer(user[userSchema.primary_key]))\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(401)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('WRITE table role, creates record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.WRITE,\n\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.post(`/Customer/`)\n\t\t\t\t\t.send(customerTestingService.mockCustomer(userId))\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(201)\n\t\t\t\tcustomers.push(result.body)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('WRITE table role, own records, creates own record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.NONE,\n\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.post(`/Customer/`)\n\t\t\t\t\t.send(customerTestingService.mockCustomer(userId))\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(201)\n\t\t\t\tcustomers.push(result.body)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('WRITE table role, own records, fails to create someone elses record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.NONE,\n\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.post(`/Customer/`)\n\t\t\t\t\t.send(customerTestingService.mockCustomer(user[userSchema.primary_key]))\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(401)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('WRITE table role, multiple records, one success and one fail', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.NONE,\n\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.post(`/Customer/`)\n\t\t\t\t\t.send([\n\t\t\t\t\t\tcustomerTestingService.mockCustomer(userId),\n\t\t\t\t\t\tcustomerTestingService.mockCustomer(user[userSchema.primary_key]),\n\t\t\t\t\t])\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(201)\n\n\t\t\t\texpect(result.body).toBeDefined()\n\t\t\t\texpect(result.body.total).toBeDefined()\n\t\t\t\texpect(result.body.total).toEqual(2)\n\t\t\t\texpect(result.body.errored).toBeDefined()\n\t\t\t\texpect(result.body.errored).toEqual(1)\n\t\t\t\texpect(result.body.successful).toBeDefined()\n\t\t\t\texpect(result.body.successful).toEqual(1)\n\t\t\t\texpect(result.body.data.length).toBeGreaterThan(0)\n\t\t\t\texpect(result.body.data[0][customerSchema.primary_key]).toBeDefined()\n\t\t\t\texpect(result.body.data[0].companyName).toBeDefined()\n\t\t\t\texpect(result.body.data[1]).toBeUndefined()\n\n\t\t\t\tcustomers.push(result.body.data[0])\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('READ table role, cannot create', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.READ,\n\t\t\t\town_records: RolePermission.READ,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.post(`/Customer/`)\n\t\t\t\t\t.send(customerTestingService.mockCustomer(userId))\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(401)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('READ table role, own records, cannot create own record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.NONE,\n\t\t\t\town_records: RolePermission.READ,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.post(`/Customer/`)\n\t\t\t\t\t.send(customerTestingService.mockCustomer(userId))\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(401)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('READ table role, own records, fails to create someone elses record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.NONE,\n\t\t\t\town_records: RolePermission.READ,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.post(`/Customer/`)\n\t\t\t\t\t.send(customerTestingService.mockCustomer(user[userSchema.primary_key]))\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(401)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('NONE authed table role, DELETE own records, should be able to create own record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.NONE,\n\t\t\t\town_records: RolePermission.DELETE,\n\t\t\t\tallowed_fields: customerSchema.primary_key + ',companyName,contactName',\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.post(`/Customer/`)\n\t\t\t\t\t.send(customerTestingService.mockCustomer(userId))\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(201)\n\n\t\t\t\tcustomers.push(result.body)\n\t\t\t\texpect(result.body).toBeDefined()\n\t\t\t\texpect(result.body[customerSchema.primary_key]).toBeDefined()\n\t\t\t\texpect(result.body.companyName).toBeDefined()\n\t\t\t\texpect(result.body.contactName).toBeDefined()\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\t})\n\n\tdescribe('Error Handling', () => {\n\t\tit('should return structured error for duplicate record', async function () {\n\t\t\tconst uniqueEmail = `duplicate-test-${Date.now()}@example.com`\n\t\t\tconst customer = {\n\t\t\t\t...customerTestingService.mockCustomer(userId),\n\t\t\t\temail: uniqueEmail,\n\t\t\t}\n\n\t\t\tconst firstResult = await request(app.getHttpServer())\n\t\t\t\t.post('/Customer/')\n\t\t\t\t.send(customer)\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(201)\n\t\t\tcustomers.push(firstResult.body)\n\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.post('/Customer/')\n\t\t\t\t.send(customer)\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(400)\n\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body.message).toBe('DUPLICATE_RECORD')\n\t\t\texpect(result.body.error).toBeDefined()\n\t\t\texpect(result.body.error).toContain('duplicate already exists')\n\t\t})\n\t})\n\n\tdescribe('Allowed Fields Results', () => {\n\t\tit('As standard, all fields returned', async function () {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.post(`/Customer/`)\n\t\t\t\t.send(customerTestingService.mockCustomer(userId))\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(201)\n\t\t\tcustomers.push(result.body)\n\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body[customerSchema.primary_key]).toBeDefined()\n\t\t\texpect(result.body.companyName).toBeDefined()\n\t\t\texpect(result.body.contactName).toBeDefined()\n\t\t\texpect(result.body.contactTitle).toBeDefined()\n\t\t\texpect(result.body.address).toBeDefined()\n\t\t\texpect(result.body.city).toBeDefined()\n\t\t\texpect(result.body.region).toBeDefined()\n\t\t\texpect(result.body.postalCode).toBeDefined()\n\t\t\texpect(result.body.country).toBeDefined()\n\t\t\texpect(result.body.phone).toBeDefined()\n\t\t\texpect(result.body.fax).toBeDefined()\n\t\t})\n\n\t\tit('When allowed_fields are passed, only return these fields', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.WRITE,\n\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t\tallowed_fields: 'companyName,contactName',\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.post(`/Customer/`)\n\t\t\t\t\t.send(customerTestingService.mockCustomer(userId))\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(201)\n\t\t\t\tcustomers.push(result.body)\n\t\t\t\texpect(result.body).toBeDefined()\n\t\t\t\texpect(result.body[customerSchema.primary_key]).toBeUndefined()\n\t\t\t\texpect(result.body.companyName).toBeDefined()\n\t\t\t\texpect(result.body.contactName).toBeDefined()\n\t\t\t\texpect(result.body.contactTitle).toBeUndefined()\n\t\t\t\texpect(result.body.address).toBeUndefined()\n\t\t\t\texpect(result.body.city).toBeUndefined()\n\t\t\t\texpect(result.body.region).toBeUndefined()\n\t\t\t\texpect(result.body.postalCode).toBeUndefined()\n\t\t\t\texpect(result.body.country).toBeUndefined()\n\t\t\t\texpect(result.body.phone).toBeUndefined()\n\t\t\t\texpect(result.body.fax).toBeUndefined()\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('When allowed_fields are passed, only return these fields, even when there is a public_table view', async function () {\n\t\t\tconst public_table_record = await authTestingService.createPublicTablesRecord({\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\taccess_level: RolePermission.WRITE,\n\t\t\t\tallowed_fields: 'companyName',\n\t\t\t})\n\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.WRITE,\n\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t\tallowed_fields: 'companyName,contactName',\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.post(`/Customer/`)\n\t\t\t\t\t.send(customerTestingService.mockCustomer(userId))\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(201)\n\t\t\t\tcustomers.push(result.body)\n\t\t\t\texpect(result.body).toBeDefined()\n\t\t\t\texpect(result.body[customerSchema.primary_key]).toBeUndefined()\n\t\t\t\texpect(result.body.companyName).toBeDefined()\n\t\t\t\texpect(result.body.contactName).toBeDefined()\n\t\t\t\texpect(result.body.contactTitle).toBeUndefined()\n\t\t\t\texpect(result.body.address).toBeUndefined()\n\t\t\t\texpect(result.body.city).toBeUndefined()\n\t\t\t\texpect(result.body.region).toBeUndefined()\n\t\t\t\texpect(result.body.postalCode).toBeUndefined()\n\t\t\t\texpect(result.body.country).toBeUndefined()\n\t\t\t\texpect(result.body.phone).toBeUndefined()\n\t\t\t\texpect(result.body.fax).toBeUndefined()\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t\tawait authTestingService.deletePublicTablesRecord(public_table_record)\n\t\t\t}\n\t\t})\n\n\t\tit('When allowed_fields are passed, only return these fields (multiple)', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.WRITE,\n\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t\tallowed_fields: 'companyName,contactName',\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.post(`/Customer/`)\n\t\t\t\t\t.send([customerTestingService.mockCustomer(userId), customerTestingService.mockCustomer(userId)])\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(201)\n\t\t\t\tcustomers.push(result.body)\n\t\t\t\texpect(result.body).toBeDefined()\n\t\t\t\texpect(result.body.total).toBeDefined()\n\t\t\t\texpect(result.body.total).toEqual(2)\n\t\t\t\texpect(result.body.errored).toBeDefined()\n\t\t\t\texpect(result.body.errored).toEqual(0)\n\t\t\t\texpect(result.body.data[0][customerSchema.primary_key]).toBeUndefined()\n\t\t\t\texpect(result.body.data[0].companyName).toBeDefined()\n\t\t\t\texpect(result.body.data[0].contactName).toBeDefined()\n\t\t\t\texpect(result.body.data[0].contactTitle).toBeUndefined()\n\t\t\t\texpect(result.body.data[0].address).toBeUndefined()\n\t\t\t\texpect(result.body.data[0].city).toBeUndefined()\n\t\t\t\texpect(result.body.data[0].region).toBeUndefined()\n\t\t\t\texpect(result.body.data[0].postalCode).toBeUndefined()\n\t\t\t\texpect(result.body.data[0].country).toBeUndefined()\n\t\t\t\texpect(result.body.data[0].phone).toBeUndefined()\n\t\t\t\texpect(result.body.data[0].fax).toBeUndefined()\n\t\t\t\texpect(result.body.data[1][customerSchema.primary_key]).toBeUndefined()\n\t\t\t\texpect(result.body.data[1].companyName).toBeDefined()\n\t\t\t\texpect(result.body.data[1].contactName).toBeDefined()\n\t\t\t\texpect(result.body.data[1].contactTitle).toBeUndefined()\n\t\t\t\texpect(result.body.data[1].address).toBeUndefined()\n\t\t\t\texpect(result.body.data[1].city).toBeUndefined()\n\t\t\t\texpect(result.body.data[1].region).toBeUndefined()\n\t\t\t\texpect(result.body.data[1].postalCode).toBeUndefined()\n\t\t\t\texpect(result.body.data[1].country).toBeUndefined()\n\t\t\t\texpect(result.body.data[1].phone).toBeUndefined()\n\t\t\t\texpect(result.body.data[1].fax).toBeUndefined()\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\t})\n\n\tafterAll(async () => {\n\t\tfor (let customer of customers) {\n\t\t\tif (customer[customerSchema.primary_key]) {\n\t\t\t\tawait customerTestingService.deleteCustomer(customer[customerSchema.primary_key])\n\t\t\t}\n\t\t}\n\t\tawait userTestingService.deleteUser(user[userSchema.primary_key])\n\t\tawait app.close()\n\t})\n})\n"
  },
  {
    "path": "src/app.controller.post.ts",
    "content": "import { Body, Controller, Headers, Post, Req, Res } from '@nestjs/common'\n\nimport { LLANA_WEBHOOK_TABLE } from './app.constants'\nimport { HeaderParams } from './dtos/requests.dto'\nimport { CreateManyResponseObject, FindOneResponseObject, IsUniqueResponse } from './dtos/response.dto'\nimport { Authentication } from './helpers/Authentication'\nimport { UrlToTable } from './helpers/Database'\nimport { Query } from './helpers/Query'\nimport { Response } from './helpers/Response'\nimport { Roles } from './helpers/Roles'\nimport { Schema } from './helpers/Schema'\nimport { Webhook } from './helpers/Webhook'\nimport { DataCacheService } from './modules/cache/dataCache.service'\nimport { WebsocketService } from './modules/websocket/websocket.service'\nimport { AuthTablePermissionFailResponse, AuthTablePermissionSuccessResponse } from './types/auth.types'\nimport { DataSourceCreateOneOptions, DataSourceSchema, PublishType, QueryPerform } from './types/datasource.types'\nimport { RolePermission } from './types/roles.types'\n\n@Controller()\nexport class PostController {\n\tconstructor(\n\t\tprivate readonly authentication: Authentication,\n\t\tprivate readonly dataCache: DataCacheService,\n\t\tprivate readonly query: Query,\n\t\tprivate readonly schema: Schema,\n\t\tprivate readonly response: Response,\n\t\tprivate readonly roles: Roles,\n\t\tprivate readonly websocket: WebsocketService,\n\t\tprivate readonly webhook: Webhook,\n\t) {}\n\n\t/**\n\t * Create new record\n\t */\n\n\t@Post('*/')\n\tasync create(\n\t\t@Req() req,\n\t\t@Res() res,\n\t\t@Headers() headers: HeaderParams,\n\t\t@Body() body: Partial<any> | Partial<any>[],\n\t): Promise<FindOneResponseObject | CreateManyResponseObject> {\n\t\tconst x_request_id = headers['x-request-id']\n\t\tlet table_name = UrlToTable(req.originalUrl, 1)\n\n\t\tif (table_name === 'webhook') {\n\t\t\ttable_name = LLANA_WEBHOOK_TABLE\n\t\t}\n\n\t\tlet schema: DataSourceSchema\n\t\tlet queryFields = []\n\n\t\t// Is the table public?\n\t\tconst public_auth = await this.authentication.public({\n\t\t\ttable: table_name,\n\t\t\taccess_level: RolePermission.WRITE,\n\t\t\tx_request_id,\n\t\t})\n\n\t\tif (public_auth.valid && public_auth.allowed_fields?.length) {\n\t\t\tif (!queryFields?.length) {\n\t\t\t\tqueryFields = public_auth.allowed_fields\n\t\t\t} else {\n\t\t\t\tqueryFields = queryFields.filter(field => public_auth.allowed_fields.includes(field))\n\t\t\t}\n\t\t}\n\n\t\t// If not public, perform auth\n\n\t\tconst auth = await this.authentication.auth({\n\t\t\ttable: table_name,\n\t\t\tx_request_id,\n\t\t\taccess: RolePermission.WRITE,\n\t\t\theaders: req.headers,\n\t\t\tbody: req.body,\n\t\t\tquery: req.query,\n\t\t})\n\n\t\tif (!public_auth.valid && !auth.valid) {\n\t\t\treturn res.status(401).send(this.response.text(auth.message))\n\t\t}\n\n\t\tlet singular = false\n\n\t\tif (!(body instanceof Array)) {\n\t\t\tbody = [body]\n\t\t\tsingular = true\n\t\t}\n\n\t\tconst total = body.length\n\t\tlet successful = 0\n\t\tlet errored = 0\n\t\tconst errors = []\n\t\tconst data: FindOneResponseObject[] = []\n\n\t\tfor (const item of body as Partial<any>[]) {\n\t\t\t//perform role check\n\t\t\tif (auth.user_identifier) {\n\t\t\t\tconst permission = await this.roles.tablePermission({\n\t\t\t\t\tidentifier: auth.user_identifier,\n\t\t\t\t\ttable: table_name,\n\t\t\t\t\taccess: RolePermission.WRITE,\n\t\t\t\t\tdata: item,\n\t\t\t\t\tx_request_id,\n\t\t\t\t})\n\n\t\t\t\tif (!public_auth.valid && !permission.valid) {\n\t\t\t\t\tif (singular) {\n\t\t\t\t\t\treturn res\n\t\t\t\t\t\t\t.status(401)\n\t\t\t\t\t\t\t.send(this.response.text((permission as AuthTablePermissionFailResponse).message))\n\t\t\t\t\t}\n\n\t\t\t\t\terrored++\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\titem: body.indexOf(item),\n\t\t\t\t\t\tmessage: this.response.text((permission as AuthTablePermissionFailResponse).message),\n\t\t\t\t\t})\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tif (permission.valid && (permission as AuthTablePermissionSuccessResponse).allowed_fields?.length) {\n\t\t\t\t\tif (!queryFields?.length) {\n\t\t\t\t\t\tqueryFields = (permission as AuthTablePermissionSuccessResponse).allowed_fields\n\t\t\t\t\t} else {\n\t\t\t\t\t\tqueryFields.push(...(permission as AuthTablePermissionSuccessResponse).allowed_fields)\n\t\t\t\t\t\tqueryFields = queryFields.filter(field =>\n\t\t\t\t\t\t\t(permission as AuthTablePermissionSuccessResponse).allowed_fields.includes(field),\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tschema = await this.schema.getSchema({ table: table_name, x_request_id })\n\t\t\t} catch (e) {\n\t\t\t\treturn res.status(404).send(this.response.text(e.message))\n\t\t\t}\n\n\t\t\tconst insertResult = await this.createOneRecord(\n\t\t\t\t{\n\t\t\t\t\tschema,\n\t\t\t\t\tdata: item,\n\t\t\t\t},\n\t\t\t\tauth.user_identifier,\n\t\t\t\tqueryFields,\n\t\t\t\tx_request_id,\n\t\t\t)\n\n\t\t\tif (!insertResult.valid) {\n\t\t\t\terrored++\n\t\t\t\terrors.push({\n\t\t\t\t\titem: Array.isArray(body) ? body.findIndex(i => i === item) : -1,\n\t\t\t\t\tmessage: insertResult.message,\n\t\t\t\t\terror: insertResult.error,\n\t\t\t\t})\n\n\t\t\t\tif (singular) {\n\t\t\t\t\treturn res.status(400).send({\n\t\t\t\t\t\tmessage: insertResult.message,\n\t\t\t\t\t\terror: insertResult.error,\n\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdata.push(insertResult.result)\n\t\t\tawait this.websocket.publish(schema, PublishType.INSERT, insertResult.result[schema.primary_key])\n\t\t\tawait this.webhook.publish(\n\t\t\t\tschema,\n\t\t\t\tPublishType.INSERT,\n\t\t\t\tinsertResult.result[schema.primary_key],\n\t\t\t\tauth.user_identifier,\n\t\t\t)\n\t\t\tsuccessful++\n\t\t}\n\n\t\tawait this.dataCache.ping(table_name)\n\n\t\tif (singular) {\n\t\t\tif (errors.length) {\n\t\t\t\treturn res.status(400).send({\n\t\t\t\t\tmessage: errors[0].message,\n\t\t\t\t\terror: errors[0].error,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\treturn res.status(201).send(data[0]) as FindOneResponseObject\n\t\t}\n\n\t\treturn res.status(201).send({\n\t\t\ttotal,\n\t\t\tsuccessful,\n\t\t\terrored,\n\t\t\terrors,\n\t\t\tdata,\n\t\t} as CreateManyResponseObject)\n\t}\n\n\t/**\n\t * Create the record\n\t */\n\n\tprivate async createOneRecord(\n\t\toptions: DataSourceCreateOneOptions,\n\t\tuser_identifier,\n\t\tfields: string[],\n\t\tx_request_id,\n\t): Promise<{\n\t\tvalid: boolean\n\t\tmessage?: string\n\t\terror?: string\n\t\tresult?: FindOneResponseObject\n\t}> {\n\t\t//validate input data\n\t\tconst { valid, message, instance } = await this.schema.validateData(options.schema, options.data)\n\t\tif (!valid) {\n\t\t\treturn {\n\t\t\t\tvalid,\n\t\t\t\tmessage,\n\t\t\t}\n\t\t}\n\n\t\toptions.data = instance\n\n\t\ttry {\n\t\t\t//validate uniqueness\n\t\t\tconst uniqueValidation = (await this.query.perform(\n\t\t\t\tQueryPerform.UNIQUE,\n\t\t\t\toptions,\n\t\t\t\tx_request_id,\n\t\t\t)) as IsUniqueResponse\n\n\t\t\tif (!uniqueValidation.valid) {\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: uniqueValidation.message,\n\t\t\t\t\terror: uniqueValidation.error,\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tif (process.env.NODE_ENV === 'test') {\n\t\t\t\tconsole.warn(`[Test Environment] Skipping uniqueness check: ${e.message}`)\n\t\t\t} else {\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: 'Error checking record uniqueness',\n\t\t\t\t\terror: e.message,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = (await this.query.perform(\n\t\t\t\tQueryPerform.CREATE,\n\t\t\t\toptions,\n\t\t\t\tx_request_id,\n\t\t\t)) as FindOneResponseObject\n\n\t\t\tawait this.websocket.publish(options.schema, PublishType.INSERT, result[options.schema.primary_key])\n\t\t\tawait this.webhook.publish(\n\t\t\t\toptions.schema,\n\t\t\t\tPublishType.INSERT,\n\t\t\t\tresult[options.schema.primary_key],\n\t\t\t\tuser_identifier,\n\t\t\t)\n\n\t\t\t//Filter results\n\t\t\tif (fields.length) {\n\t\t\t\tconst filtered = {}\n\t\t\t\tfor (const field of fields) {\n\t\t\t\t\tfiltered[field] = result[field]\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tvalid: true,\n\t\t\t\t\tresult: filtered,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tvalid: true,\n\t\t\t\tresult,\n\t\t\t}\n\t\t} catch (e) {\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\tmessage: e.message,\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/app.controller.put.test.spec.ts",
    "content": "import { INestApplication } from '@nestjs/common'\nimport { Test } from '@nestjs/testing'\nimport { ConfigModule, ConfigService, ConfigFactory } from '@nestjs/config'\nimport { JwtModule } from '@nestjs/jwt'\nimport * as request from 'supertest'\nimport { CustomerTestingService } from './testing/customer.testing.service'\n\nimport { AppModule } from './app.module'\nimport { AuthTestingService } from './testing/auth.testing.service'\nimport { DataSourceSchema } from './types/datasource.types'\nimport { SalesOrderTestingService } from './testing/salesorder.testing.service'\nimport { EmployeeTestingService } from './testing/employee.testing.service'\nimport { ShipperTestingService } from './testing/shipper.testing.service'\nimport { UserTestingService } from './testing/user.testing.service'\nimport { Logger } from './helpers/Logger'\nimport { TIMEOUT } from './testing/testing.const'\n\n// Import configs\nimport auth from './config/auth.config'\nimport database from './config/database.config'\nimport hosts from './config/hosts.config'\nimport jwt from './config/jwt.config'\nimport roles from './config/roles.config'\nimport { envValidationSchema } from './config/env.validation'\nimport { RolePermission } from './types/roles.types'\n\n// Type the config imports\nconst configs: ConfigFactory[] = [auth, database, hosts, jwt, roles]\n\ndescribe('App > Controller > Put', () => {\n\tlet app: INestApplication\n\n\tlet authTestingService: AuthTestingService\n\tlet customerTestingService: CustomerTestingService\n\tlet employeeTestingService: EmployeeTestingService\n\tlet shipperTestingService: ShipperTestingService\n\tlet salesOrderTestingService: SalesOrderTestingService\n\tlet userTestingService: UserTestingService\n\n\tlet customerSchema: DataSourceSchema\n\tlet employeeSchema: DataSourceSchema\n\tlet shipperSchema: DataSourceSchema\n\tlet orderSchema: DataSourceSchema\n\tlet userSchema: DataSourceSchema\n\n\tlet customers = []\n\tlet employee: any\n\tlet shipper: any\n\tlet order: any\n\tlet user: any\n\n\tlet jwt: string\n\tlet userId: any\n\tlet logger = new Logger()\n\n\tbeforeAll(async () => {\n\t\tconst moduleRef = await Test.createTestingModule({\n\t\t\timports: [\n\t\t\t\tConfigModule.forRoot({\n\t\t\t\t\tload: configs,\n\t\t\t\t\tvalidationSchema: envValidationSchema,\n\t\t\t\t\tisGlobal: true,\n\t\t\t\t}),\n\t\t\t\tJwtModule.registerAsync({\n\t\t\t\t\timports: [ConfigModule],\n\t\t\t\t\tuseFactory: async (configService: ConfigService) => ({\n\t\t\t\t\t\tsecret: configService.get('jwt.secret'),\n\t\t\t\t\t\tsignOptions: configService.get('jwt.signOptions'),\n\t\t\t\t\t}),\n\t\t\t\t\tinject: [ConfigService],\n\t\t\t\t}),\n\t\t\t\tAppModule,\n\t\t\t],\n\t\t\tproviders: [\n\t\t\t\tAuthTestingService,\n\t\t\t\tCustomerTestingService,\n\t\t\t\tEmployeeTestingService,\n\t\t\t\tShipperTestingService,\n\t\t\t\tSalesOrderTestingService,\n\t\t\t\tUserTestingService,\n\t\t\t],\n\t\t\texports: [\n\t\t\t\tAuthTestingService,\n\t\t\t\tCustomerTestingService,\n\t\t\t\tEmployeeTestingService,\n\t\t\t\tShipperTestingService,\n\t\t\t\tSalesOrderTestingService,\n\t\t\t\tUserTestingService,\n\t\t\t],\n\t\t}).compile()\n\n\t\tapp = moduleRef.createNestApplication()\n\t\tawait app.init()\n\n\t\t// Expose the app object globally for debugging\n\t\t;(global as any).app = app\n\n\t\tauthTestingService = app.get<AuthTestingService>(AuthTestingService)\n\t\tcustomerTestingService = app.get<CustomerTestingService>(CustomerTestingService)\n\t\temployeeTestingService = app.get<EmployeeTestingService>(EmployeeTestingService)\n\t\tshipperTestingService = app.get<ShipperTestingService>(ShipperTestingService)\n\t\tsalesOrderTestingService = app.get<SalesOrderTestingService>(SalesOrderTestingService)\n\t\tuserTestingService = app.get<UserTestingService>(UserTestingService)\n\n\t\tcustomerSchema = await customerTestingService.getSchema()\n\t\temployeeSchema = await employeeTestingService.getSchema()\n\t\tshipperSchema = await shipperTestingService.getSchema()\n\t\torderSchema = await salesOrderTestingService.getSchema()\n\t\tuserSchema = await userTestingService.getSchema()\n\n\t\tjwt = await authTestingService.login()\n\t\tuserId = await authTestingService.getUserId(jwt)\n\n\t\tuser = await userTestingService.createUser({})\n\t\tcustomers.push(await customerTestingService.createCustomer({ userId }))\n\t\tcustomers.push(await customerTestingService.createCustomer({ userId }))\n\t\tcustomers.push(await customerTestingService.createCustomer({ userId }))\n\t\tcustomers.push(await customerTestingService.createCustomer({ userId: user[userSchema.primary_key] }))\n\t\temployee = await employeeTestingService.createEmployee({})\n\t\tshipper = await shipperTestingService.createShipper({})\n\t\torder = await salesOrderTestingService.createOrder({\n\t\t\tcustId: customers[0][customerSchema.primary_key],\n\t\t\temployeeId: employee[employeeSchema.primary_key],\n\t\t\tshipperId: shipper[shipperSchema.primary_key],\n\t\t})\n\t}, TIMEOUT)\n\n\tbeforeEach(() => {\n\t\tlogger.debug('===========================================')\n\t\tlogger.log('🧪 ' + expect.getState().currentTestName)\n\t\tlogger.debug('===========================================')\n\t})\n\n\tdescribe('Update', () => {\n\t\tit('One', async function () {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.put(`/Customer/${customers[0][customerSchema.primary_key]}`)\n\t\t\t\t.send({\n\t\t\t\t\tcompanyName: 'Updated Company Name',\n\t\t\t\t\tcontactName: 'Updated Contact Name',\n\t\t\t\t})\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(200)\n\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body[customerSchema.primary_key].toString()).toEqual(\n\t\t\t\tcustomers[0][customerSchema.primary_key].toString(),\n\t\t\t)\n\t\t\texpect(result.body.companyName).toEqual('Updated Company Name')\n\t\t\texpect(result.body.contactName).toEqual('Updated Contact Name')\n\t\t\tcustomers[0] = result.body\n\t\t})\n\t\tit('Many', async function () {\n\t\t\tcustomers[1].companyName = 'Customer2 Company Name'\n\t\t\tcustomers[2].companyName = 'Customer2 Company Name'\n\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.put(`/Customer/`)\n\t\t\t\t.send([\n\t\t\t\t\t{\n\t\t\t\t\t\t[customerSchema.primary_key]: customers[1][customerSchema.primary_key],\n\t\t\t\t\t\tcompanyName: customers[1].companyName,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t[customerSchema.primary_key]: customers[2][customerSchema.primary_key],\n\t\t\t\t\t\tcompanyName: customers[2].companyName,\n\t\t\t\t\t},\n\t\t\t\t])\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(200)\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body.total).toBeDefined()\n\t\t\texpect(result.body.total).toEqual(2)\n\t\t\texpect(result.body.errored).toBeDefined()\n\t\t\texpect(result.body.errored).toEqual(0)\n\t\t\texpect(result.body.successful).toBeDefined()\n\t\t\texpect(result.body.successful).toEqual(2)\n\t\t\texpect(result.body.data.length).toBeGreaterThan(0)\n\t\t\texpect(result.body.data[0][customerSchema.primary_key].toString()).toEqual(\n\t\t\t\tcustomers[1][customerSchema.primary_key].toString(),\n\t\t\t)\n\t\t\texpect(result.body.data[0].companyName).toEqual(customers[1].companyName)\n\t\t\texpect(result.body.data[0].contactName).toEqual(customers[1].contactName)\n\t\t\texpect(result.body.data[1][customerSchema.primary_key].toString()).toEqual(\n\t\t\t\tcustomers[2][customerSchema.primary_key].toString(),\n\t\t\t)\n\t\t\texpect(result.body.data[1].companyName).toEqual(customers[2].companyName)\n\t\t\texpect(result.body.data[1].contactName).toEqual(customers[2].contactName)\n\t\t\tcustomers[1] = result.body.data[0]\n\t\t\tcustomers[2] = result.body.data[1]\n\t\t})\n\n\t\tit('One - Integer', async function () {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.put(`/SalesOrder/${order[orderSchema.primary_key]}`)\n\t\t\t\t.send({\n\t\t\t\t\tfreight: 10.01,\n\t\t\t\t})\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(200)\n\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body[orderSchema.primary_key].toString()).toEqual(order[orderSchema.primary_key].toString())\n\t\t\texpect(result.body.freight).toEqual(10.01)\n\t\t\torder = result.body\n\t\t})\n\n\t\tdescribe('User', () => {\n\t\t\tit('Did it encrypt the password?', async () => {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.put(`/User/${user[userSchema.primary_key]}`)\n\t\t\t\t\t.send({\n\t\t\t\t\t\tpassword: 'password',\n\t\t\t\t\t})\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(200)\n\n\t\t\t\texpect(result.body).toBeDefined()\n\t\t\t\texpect(result.body[userSchema.primary_key].toString()).toEqual(user[userSchema.primary_key].toString())\n\t\t\t\texpect(result.body.password.startsWith('$2')).toBeTruthy()\n\t\t\t\tuser = result.body\n\t\t\t})\n\t\t})\n\t})\n\n\tdescribe('Public Updating', () => {\n\t\tit('Default public fail to create', async function () {\n\t\t\tawait request(app.getHttpServer())\n\t\t\t\t.put(`/Customer/${customers[0][customerSchema.primary_key]}`)\n\t\t\t\t.send({\n\t\t\t\t\tcompanyName: 'Anything here',\n\t\t\t\t})\n\t\t\t\t.expect(401)\n\t\t})\n\n\t\tit('Cannot update with READ permissions', async function () {\n\t\t\tconst public_table_record = await authTestingService.createPublicTablesRecord({\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\taccess_level: RolePermission.READ,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.put(`/Customer/${customers[0][customerSchema.primary_key]}`)\n\t\t\t\t\t.send({\n\t\t\t\t\t\tcompanyName: 'Anything here',\n\t\t\t\t\t})\n\t\t\t\t\t.expect(401)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deletePublicTablesRecord(public_table_record)\n\t\t\t}\n\t\t})\n\n\t\tit('Can update with WRITE permissions', async function () {\n\t\t\tconst public_table_record = await authTestingService.createPublicTablesRecord({\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\taccess_level: RolePermission.WRITE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.put(`/Customer/${customers[0][customerSchema.primary_key]}`)\n\t\t\t\t\t.send({\n\t\t\t\t\t\tcompanyName: 'Anything here',\n\t\t\t\t\t})\n\t\t\t\t\t.expect(200)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deletePublicTablesRecord(public_table_record)\n\t\t\t}\n\t\t})\n\n\t\tit('Can update with WRITE permissions and allowed fields', async function () {\n\t\t\tconst public_table_record = await authTestingService.createPublicTablesRecord({\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\taccess_level: RolePermission.WRITE,\n\t\t\t\tallowed_fields: 'companyName',\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.put(`/Customer/${customers[0][customerSchema.primary_key]}`)\n\t\t\t\t\t.send({\n\t\t\t\t\t\tcompanyName: 'Anything here',\n\t\t\t\t\t})\n\t\t\t\t\t.expect(200)\n\n\t\t\t\texpect(result.body).toBeDefined()\n\t\t\t\texpect(result.body[customerSchema.primary_key]).toBeUndefined()\n\t\t\t\texpect(result.body.companyName).toBeDefined()\n\t\t\t\texpect(result.body.contactName).toBeUndefined()\n\t\t\t\texpect(result.body.contactTitle).toBeUndefined()\n\t\t\t\texpect(result.body.address).toBeUndefined()\n\t\t\t\texpect(result.body.city).toBeUndefined()\n\t\t\t\texpect(result.body.region).toBeUndefined()\n\t\t\t\texpect(result.body.postalCode).toBeUndefined()\n\t\t\t\texpect(result.body.country).toBeUndefined()\n\t\t\t\texpect(result.body.phone).toBeUndefined()\n\t\t\t\texpect(result.body.fax).toBeUndefined()\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deletePublicTablesRecord(public_table_record)\n\t\t\t}\n\t\t})\n\t})\n\n\tdescribe('Role Based Updating', () => {\n\t\tit('No table role, updates record', async function () {\n\t\t\tawait request(app.getHttpServer())\n\t\t\t\t.put(`/Customer/${customers[3][customerSchema.primary_key]}`)\n\t\t\t\t.send({\n\t\t\t\t\tcompanyName: 'Anything here',\n\t\t\t\t})\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(200)\n\t\t})\n\n\t\tit('DELETE table role, updates record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.DELETE,\n\t\t\t\town_records: RolePermission.DELETE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.put(`/Customer/${customers[3][customerSchema.primary_key]}`)\n\t\t\t\t\t.send({\n\t\t\t\t\t\tcompanyName: 'Anything here',\n\t\t\t\t\t})\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(200)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('DELETE table role, own records, fails to update someone elses record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.NONE,\n\t\t\t\town_records: RolePermission.DELETE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.put(`/Customer/${customers[3][customerSchema.primary_key]}`)\n\t\t\t\t\t.send({\n\t\t\t\t\t\tcompanyName: 'Anything here',\n\t\t\t\t\t})\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(401)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('WRITE table role, updates record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.WRITE,\n\t\t\t\town_records: RolePermission.NONE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.put(`/Customer/${customers[3][customerSchema.primary_key]}`)\n\t\t\t\t\t.send({\n\t\t\t\t\t\tcompanyName: 'Anything here',\n\t\t\t\t\t})\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(200)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('WRITE table role, own records, fails to update someone elses record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.NONE,\n\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.put(`/Customer/${customers[3][customerSchema.primary_key]}`)\n\t\t\t\t\t.send({\n\t\t\t\t\t\tcompanyName: 'Anything here',\n\t\t\t\t\t})\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(401)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('WRITE table role, multiple records, one success and one fail', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.NONE,\n\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.put(`/Customer/`)\n\t\t\t\t\t.send([\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t[customerSchema.primary_key]: customers[0][customerSchema.primary_key],\n\t\t\t\t\t\t\tcompanyName: 'Anything here',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t[customerSchema.primary_key]: customers[3][customerSchema.primary_key],\n\t\t\t\t\t\t\tcompanyName: 'Anything here',\n\t\t\t\t\t\t},\n\t\t\t\t\t])\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(200)\n\n\t\t\t\texpect(result.body).toBeDefined()\n\t\t\t\texpect(result.body.total).toBeDefined()\n\t\t\t\texpect(result.body.total).toEqual(2)\n\t\t\t\texpect(result.body.errored).toBeDefined()\n\t\t\t\texpect(result.body.errored).toEqual(1)\n\t\t\t\texpect(result.body.successful).toBeDefined()\n\t\t\t\texpect(result.body.successful).toEqual(1)\n\t\t\t\texpect(result.body.data.length).toBeGreaterThan(0)\n\t\t\t\texpect(result.body.data[0][customerSchema.primary_key]).toBeDefined()\n\t\t\t\texpect(result.body.data[0].companyName).toBeDefined()\n\t\t\t\texpect(result.body.data[1]).toBeUndefined()\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('READ table role, updates record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.READ,\n\t\t\t\town_records: RolePermission.READ,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.put(`/Customer/${customers[3][customerSchema.primary_key]}`)\n\t\t\t\t\t.send({\n\t\t\t\t\t\tcompanyName: 'Anything here',\n\t\t\t\t\t})\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(401)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('READ table role, own records, fails to update someone elses record', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.NONE,\n\t\t\t\town_records: RolePermission.READ,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait request(app.getHttpServer())\n\t\t\t\t\t.put(`/Customer/${customers[3][customerSchema.primary_key]}`)\n\t\t\t\t\t.send({\n\t\t\t\t\t\tcompanyName: 'Anything here',\n\t\t\t\t\t})\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(401)\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\t})\n\n\tdescribe('Allowed Fields Results', () => {\n\t\tit('As standard, all fields returned', async function () {\n\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t.put(`/Customer/${customers[3][customerSchema.primary_key]}`)\n\t\t\t\t.send({\n\t\t\t\t\tcompanyName: 'Anything here',\n\t\t\t\t})\n\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t.expect(200)\n\n\t\t\texpect(result.body).toBeDefined()\n\t\t\texpect(result.body[customerSchema.primary_key]).toBeDefined()\n\t\t\texpect(result.body.companyName).toBeDefined()\n\t\t\texpect(result.body.contactName).toBeDefined()\n\t\t\texpect(result.body.contactTitle).toBeDefined()\n\t\t\texpect(result.body.address).toBeDefined()\n\t\t\texpect(result.body.city).toBeDefined()\n\t\t\texpect(result.body.region).toBeDefined()\n\t\t\texpect(result.body.postalCode).toBeDefined()\n\t\t\texpect(result.body.country).toBeDefined()\n\t\t\texpect(result.body.phone).toBeDefined()\n\t\t\texpect(result.body.fax).toBeDefined()\n\t\t})\n\n\t\tit('When allowed_fields are passed, only return these fields', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.WRITE,\n\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t\tallowed_fields: 'companyName,contactName',\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.put(`/Customer/${customers[3][customerSchema.primary_key]}`)\n\t\t\t\t\t.send({\n\t\t\t\t\t\tcompanyName: 'Anything here',\n\t\t\t\t\t})\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(200)\n\t\t\t\texpect(result.body).toBeDefined()\n\t\t\t\texpect(result.body[customerSchema.primary_key]).toBeUndefined()\n\t\t\t\texpect(result.body.companyName).toBeDefined()\n\t\t\t\texpect(result.body.contactName).toBeDefined()\n\t\t\t\texpect(result.body.contactTitle).toBeUndefined()\n\t\t\t\texpect(result.body.address).toBeUndefined()\n\t\t\t\texpect(result.body.city).toBeUndefined()\n\t\t\t\texpect(result.body.region).toBeUndefined()\n\t\t\t\texpect(result.body.postalCode).toBeUndefined()\n\t\t\t\texpect(result.body.country).toBeUndefined()\n\t\t\t\texpect(result.body.phone).toBeUndefined()\n\t\t\t\texpect(result.body.fax).toBeUndefined()\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\n\t\tit('When allowed_fields are passed, only return these fields, even when there is a public_table view', async function () {\n\t\t\tconst public_table_record = await authTestingService.createPublicTablesRecord({\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\taccess_level: RolePermission.WRITE,\n\t\t\t\tallowed_fields: 'companyName',\n\t\t\t})\n\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.WRITE,\n\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t\tallowed_fields: 'companyName,contactName',\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.put(`/Customer/${customers[3][customerSchema.primary_key]}`)\n\t\t\t\t\t.send({\n\t\t\t\t\t\tcompanyName: 'Anything here',\n\t\t\t\t\t})\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(200)\n\n\t\t\t\texpect(result.body).toBeDefined()\n\t\t\t\texpect(result.body[customerSchema.primary_key]).toBeUndefined()\n\t\t\t\texpect(result.body.companyName).toBeDefined()\n\t\t\t\texpect(result.body.contactName).toBeDefined()\n\t\t\t\texpect(result.body.contactTitle).toBeUndefined()\n\t\t\t\texpect(result.body.address).toBeUndefined()\n\t\t\t\texpect(result.body.city).toBeUndefined()\n\t\t\t\texpect(result.body.region).toBeUndefined()\n\t\t\t\texpect(result.body.postalCode).toBeUndefined()\n\t\t\t\texpect(result.body.country).toBeUndefined()\n\t\t\t\texpect(result.body.phone).toBeUndefined()\n\t\t\t\texpect(result.body.fax).toBeUndefined()\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t\tawait authTestingService.deletePublicTablesRecord(public_table_record)\n\t\t\t}\n\t\t})\n\n\t\tit('When allowed_fields are passed, only return these fields (multiple)', async function () {\n\t\t\tconst role = await authTestingService.createRole({\n\t\t\t\tcustom: true,\n\t\t\t\ttable: customerSchema.table,\n\t\t\t\tidentity_column: 'userId',\n\t\t\t\trole: 'ADMIN',\n\t\t\t\trecords: RolePermission.WRITE,\n\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t\tallowed_fields: 'companyName,contactName',\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tconst result = await request(app.getHttpServer())\n\t\t\t\t\t.put(`/Customer/`)\n\t\t\t\t\t.send([\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t[customerSchema.primary_key]: customers[0][customerSchema.primary_key],\n\t\t\t\t\t\t\tcompanyName: 'Anything here',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t[customerSchema.primary_key]: customers[1][customerSchema.primary_key],\n\t\t\t\t\t\t\tcompanyName: 'Anything here',\n\t\t\t\t\t\t},\n\t\t\t\t\t])\n\t\t\t\t\t.set('Authorization', `Bearer ${jwt}`)\n\t\t\t\t\t.expect(200)\n\n\t\t\t\texpect(result.body).toBeDefined()\n\t\t\t\texpect(result.body.total).toBeDefined()\n\t\t\t\texpect(result.body.total).toEqual(2)\n\t\t\t\texpect(result.body.errored).toBeDefined()\n\t\t\t\texpect(result.body.errored).toEqual(0)\n\t\t\t\texpect(result.body.data[0][customerSchema.primary_key]).toBeUndefined()\n\t\t\t\texpect(result.body.data[0].companyName).toBeDefined()\n\t\t\t\texpect(result.body.data[0].contactName).toBeDefined()\n\t\t\t\texpect(result.body.data[0].contactTitle).toBeUndefined()\n\t\t\t\texpect(result.body.data[0].address).toBeUndefined()\n\t\t\t\texpect(result.body.data[0].city).toBeUndefined()\n\t\t\t\texpect(result.body.data[0].region).toBeUndefined()\n\t\t\t\texpect(result.body.data[0].postalCode).toBeUndefined()\n\t\t\t\texpect(result.body.data[0].country).toBeUndefined()\n\t\t\t\texpect(result.body.data[0].phone).toBeUndefined()\n\t\t\t\texpect(result.body.data[0].fax).toBeUndefined()\n\t\t\t\texpect(result.body.data[1][customerSchema.primary_key]).toBeUndefined()\n\t\t\t\texpect(result.body.data[1].companyName).toBeDefined()\n\t\t\t\texpect(result.body.data[1].contactName).toBeDefined()\n\t\t\t\texpect(result.body.data[1].contactTitle).toBeUndefined()\n\t\t\t\texpect(result.body.data[1].address).toBeUndefined()\n\t\t\t\texpect(result.body.data[1].city).toBeUndefined()\n\t\t\t\texpect(result.body.data[1].region).toBeUndefined()\n\t\t\t\texpect(result.body.data[1].postalCode).toBeUndefined()\n\t\t\t\texpect(result.body.data[1].country).toBeUndefined()\n\t\t\t\texpect(result.body.data[1].phone).toBeUndefined()\n\t\t\t\texpect(result.body.data[1].fax).toBeUndefined()\n\t\t\t} catch (e) {\n\t\t\t\tlogger.error(e)\n\t\t\t\tthrow e\n\t\t\t} finally {\n\t\t\t\tawait authTestingService.deleteRole(role)\n\t\t\t}\n\t\t})\n\t})\n\n\tafterAll(async () => {\n\t\tawait salesOrderTestingService.deleteOrder(order[orderSchema.primary_key])\n\t\tfor (let customer of customers) {\n\t\t\tawait customerTestingService.deleteCustomer(customer[customerSchema.primary_key])\n\t\t}\n\t\tawait employeeTestingService.deleteEmployee(employee[employeeSchema.primary_key])\n\t\tawait shipperTestingService.deleteShipper(shipper[shipperSchema.primary_key])\n\t\tawait userTestingService.deleteUser(user[userSchema.primary_key])\n\t\tawait app.close()\n\t})\n})\n"
  },
  {
    "path": "src/app.controller.put.ts",
    "content": "import { Body, Controller, Headers, Param, Patch, Put, Req, Res } from '@nestjs/common'\n\nimport { LLANA_WEBHOOK_TABLE } from './app.constants'\nimport { HeaderParams } from './dtos/requests.dto'\nimport { FindOneResponseObject, IsUniqueResponse, UpdateManyResponseObject } from './dtos/response.dto'\nimport { Authentication } from './helpers/Authentication'\nimport { UrlToTable } from './helpers/Database'\nimport { Query } from './helpers/Query'\nimport { Response } from './helpers/Response'\nimport { Roles } from './helpers/Roles'\nimport { Schema } from './helpers/Schema'\nimport { Webhook } from './helpers/Webhook'\nimport { DataCacheService } from './modules/cache/dataCache.service'\nimport { WebsocketService } from './modules/websocket/websocket.service'\nimport { AuthTablePermissionFailResponse, AuthTablePermissionSuccessResponse } from './types/auth.types'\nimport { DataSourceSchema, DataSourceWhere, PublishType, QueryPerform, WhereOperator } from './types/datasource.types'\nimport { RolePermission } from './types/roles.types'\n\n@Controller()\nexport class PutController {\n\tconstructor(\n\t\tprivate readonly authentication: Authentication,\n\t\tprivate readonly dataCache: DataCacheService,\n\t\tprivate readonly query: Query,\n\t\tprivate readonly response: Response,\n\t\tprivate readonly roles: Roles,\n\t\tprivate readonly schema: Schema,\n\t\tprivate readonly websocket: WebsocketService,\n\t\tprivate readonly webhooks: Webhook,\n\t) {}\n\n\t@Put('*/:id')\n\tasync updateById(\n\t\t@Req() req,\n\t\t@Res() res,\n\t\t@Body() body: Partial<any>,\n\t\t@Headers() headers: HeaderParams,\n\t\t@Param('id') id: string,\n\t): Promise<FindOneResponseObject> {\n\t\tconst x_request_id = headers['x-request-id']\n\t\tlet table_name = UrlToTable(req.originalUrl, 1)\n\n\t\tif (table_name === 'webhook') {\n\t\t\ttable_name = LLANA_WEBHOOK_TABLE\n\t\t}\n\n\t\tlet schema: DataSourceSchema\n\t\tlet queryFields = []\n\n\t\ttry {\n\t\t\tschema = await this.schema.getSchema({ table: table_name, x_request_id })\n\t\t} catch (e) {\n\t\t\treturn res.status(404).send(this.response.text(e.message))\n\t\t}\n\n\t\t// Is the table public?\n\t\tconst public_auth = await this.authentication.public({\n\t\t\ttable: table_name,\n\t\t\taccess_level: RolePermission.WRITE,\n\t\t\tx_request_id,\n\t\t})\n\n\t\tif (public_auth.valid && public_auth.allowed_fields?.length) {\n\t\t\tif (!queryFields?.length) {\n\t\t\t\tqueryFields = public_auth.allowed_fields\n\t\t\t} else {\n\t\t\t\tqueryFields = queryFields.filter(field => public_auth.allowed_fields.includes(field))\n\t\t\t}\n\t\t}\n\n\t\t// If not public, perform auth\n\n\t\tconst auth = await this.authentication.auth({\n\t\t\ttable: table_name,\n\t\t\tx_request_id,\n\t\t\taccess: RolePermission.WRITE,\n\t\t\theaders: req.headers,\n\t\t\tbody: req.body,\n\t\t\tquery: req.query,\n\t\t})\n\t\tif (!public_auth.valid && !auth.valid) {\n\t\t\treturn res.status(401).send(this.response.text(auth.message))\n\t\t}\n\n\t\t//validate input data\n\t\tconst validate = await this.schema.validateData(schema, body)\n\t\tif (!validate.valid) {\n\t\t\treturn res.status(400).send(this.response.text(validate.message))\n\t\t}\n\n\t\t//validate :id field\n\t\tconst primary_key = this.schema.getPrimaryKey(schema)\n\n\t\tif (!primary_key) {\n\t\t\treturn res.status(400).send(this.response.text(`No primary key found for table ${table_name}`))\n\t\t}\n\n\t\tconst validateKey = await this.schema.validateData(schema, { [primary_key]: id })\n\t\tif (!validateKey.valid) {\n\t\t\treturn res.status(400).send(this.response.text(validateKey.message))\n\t\t}\n\n\t\t//validate uniqueness\n\t\ttry {\n\t\t\tconst uniqueValidation = (await this.query.perform(\n\t\t\t\tQueryPerform.UNIQUE,\n\t\t\t\t{\n\t\t\t\t\tschema,\n\t\t\t\t\tdata: body,\n\t\t\t\t\tid: id,\n\t\t\t\t},\n\t\t\t\tx_request_id,\n\t\t\t)) as IsUniqueResponse\n\n\t\t\tif (!uniqueValidation.valid) {\n\t\t\t\treturn res.status(400).send({\n\t\t\t\t\tmessage: uniqueValidation.message,\n\t\t\t\t\terror: uniqueValidation.error,\n\t\t\t\t})\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tif (process.env.NODE_ENV === 'test') {\n\t\t\t\tconsole.warn(`[Test Environment] Skipping uniqueness check: ${e.message}`)\n\t\t\t} else {\n\t\t\t\treturn res.status(400).send({\n\t\t\t\t\tmessage: 'Error checking record uniqueness',\n\t\t\t\t\terror: e.message,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tconst where = <DataSourceWhere[]>[\n\t\t\t{\n\t\t\t\tcolumn: primary_key,\n\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\tvalue: id,\n\t\t\t},\n\t\t]\n\n\t\t//Check record exists\n\n\t\tconst record = (await this.query.perform(\n\t\t\tQueryPerform.FIND_ONE,\n\t\t\t{\n\t\t\t\tschema,\n\t\t\t\twhere,\n\t\t\t},\n\t\t\tx_request_id,\n\t\t)) as FindOneResponseObject\n\n\t\tif (!record) {\n\t\t\treturn res.status(400).send(this.response.text(`Record with id ${id} not found`))\n\t\t}\n\n\t\t// If not public, perform auth\n\t\tif (auth.user_identifier) {\n\t\t\tconst permission = await this.roles.tablePermission({\n\t\t\t\tidentifier: auth.user_identifier,\n\t\t\t\ttable: table_name,\n\t\t\t\taccess: RolePermission.WRITE,\n\t\t\t\tdata: record,\n\t\t\t\tx_request_id,\n\t\t\t})\n\n\t\t\tif (!public_auth.valid && !permission.valid) {\n\t\t\t\treturn res.status(401).send(this.response.text((permission as AuthTablePermissionFailResponse).message))\n\t\t\t}\n\n\t\t\tif (permission.valid && (permission as AuthTablePermissionSuccessResponse).allowed_fields?.length) {\n\t\t\t\tif (!queryFields?.length) {\n\t\t\t\t\tqueryFields = (permission as AuthTablePermissionSuccessResponse).allowed_fields\n\t\t\t\t} else {\n\t\t\t\t\tqueryFields.push(...(permission as AuthTablePermissionSuccessResponse).allowed_fields)\n\t\t\t\t\tqueryFields = queryFields.filter(field =>\n\t\t\t\t\t\t(permission as AuthTablePermissionSuccessResponse).allowed_fields.includes(field),\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await this.query.perform(\n\t\t\t\tQueryPerform.UPDATE,\n\t\t\t\t{ id, schema, data: validate.instance },\n\t\t\t\tx_request_id,\n\t\t\t)\n\t\t\tawait this.websocket.publish(schema, PublishType.UPDATE, result[schema.primary_key])\n\t\t\tawait this.webhooks.publish(schema, PublishType.UPDATE, result[schema.primary_key], auth.user_identifier)\n\n\t\t\tawait this.dataCache.ping(table_name)\n\n\t\t\tif (queryFields.length) {\n\t\t\t\tconst filtered = {}\n\t\t\t\tfor (const field of queryFields) {\n\t\t\t\t\tfiltered[field] = result[field]\n\t\t\t\t}\n\t\t\t\treturn res.status(200).send(filtered)\n\t\t\t}\n\n\t\t\treturn res.status(200).send(result)\n\t\t} catch (e) {\n\t\t\treturn res.status(400).send(this.response.text(e.message))\n\t\t}\n\t}\n\n\t@Put('*/')\n\tasync updateMany(\n\t\t@Req() req,\n\t\t@Res() res,\n\t\t@Body() body: any,\n\t\t@Headers() headers: HeaderParams,\n\t): Promise<UpdateManyResponseObject> {\n\t\tconst x_request_id = headers['x-request-id']\n\t\tlet table_name = UrlToTable(req.originalUrl, 1)\n\n\t\tif (table_name === 'webhook') {\n\t\t\ttable_name = LLANA_WEBHOOK_TABLE\n\t\t}\n\n\t\tlet schema: DataSourceSchema\n\t\tlet queryFields = []\n\n\t\ttry {\n\t\t\tschema = await this.schema.getSchema({ table: table_name, x_request_id })\n\t\t} catch (e) {\n\t\t\treturn res.status(404).send(this.response.text(e.message))\n\t\t}\n\n\t\t// Is the table public?\n\t\tconst public_auth = await this.authentication.public({\n\t\t\ttable: table_name,\n\t\t\taccess_level: RolePermission.WRITE,\n\t\t\tx_request_id,\n\t\t})\n\n\t\tif (public_auth.valid && public_auth.allowed_fields?.length) {\n\t\t\tif (!queryFields?.length) {\n\t\t\t\tqueryFields = public_auth.allowed_fields\n\t\t\t} else {\n\t\t\t\tqueryFields = queryFields.filter(field => public_auth.allowed_fields.includes(field))\n\t\t\t}\n\t\t}\n\n\t\t// If not public, perform auth\n\n\t\tconst auth = await this.authentication.auth({\n\t\t\ttable: table_name,\n\t\t\tx_request_id,\n\t\t\taccess: RolePermission.WRITE,\n\t\t\theaders: req.headers,\n\t\t\tbody: req.body,\n\t\t\tquery: req.query,\n\t\t})\n\t\tif (!public_auth.valid && !auth.valid) {\n\t\t\treturn res.status(401).send(this.response.text(auth.message))\n\t\t}\n\n\t\t//validate :id field\n\t\tconst primary_key = this.schema.getPrimaryKey(schema)\n\n\t\tif (!primary_key) {\n\t\t\treturn res.status(400).send(this.response.text(`No primary key found for table ${table_name}`))\n\t\t}\n\n\t\tif (!(body instanceof Array)) {\n\t\t\treturn res.status(400).send(this.response.text('Body must be an array'))\n\t\t}\n\t\tconst total = body.length\n\t\tlet successful = 0\n\t\tlet errored = 0\n\t\tconst errors = []\n\t\tconst data: FindOneResponseObject[] = []\n\n\t\tfor (const item of body) {\n\t\t\t//validate input data\n\t\t\tconst validate = await this.schema.validateData(schema, item)\n\t\t\tif (!validate.valid) {\n\t\t\t\terrored++\n\t\t\t\terrors.push({\n\t\t\t\t\titem: body.indexOf(item),\n\t\t\t\t\tmessage: validate.message,\n\t\t\t\t})\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tconst validateKey = await this.schema.validateData(schema, { [primary_key]: item[primary_key] })\n\t\t\tif (!validateKey.valid) {\n\t\t\t\terrored++\n\t\t\t\terrors.push({\n\t\t\t\t\titem: body.indexOf(item),\n\t\t\t\t\tmessage: validateKey.message,\n\t\t\t\t})\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t//validate uniqueness\n\t\t\ttry {\n\t\t\t\tconst uniqueValidation = (await this.query.perform(\n\t\t\t\t\tQueryPerform.UNIQUE,\n\t\t\t\t\t{\n\t\t\t\t\t\tschema,\n\t\t\t\t\t\tdata: item,\n\t\t\t\t\t\tid: item[primary_key],\n\t\t\t\t\t},\n\t\t\t\t\tx_request_id,\n\t\t\t\t)) as IsUniqueResponse\n\n\t\t\t\tif (!uniqueValidation.valid) {\n\t\t\t\t\terrored++\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\titem: body.indexOf(item),\n\t\t\t\t\t\tmessage: uniqueValidation.message,\n\t\t\t\t\t\terror: uniqueValidation.error,\n\t\t\t\t\t})\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\tif (process.env.NODE_ENV === 'test') {\n\t\t\t\t\tconsole.warn(`[Test Environment] Skipping uniqueness check: ${e.message}`)\n\t\t\t\t} else {\n\t\t\t\t\terrored++\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\titem: body.indexOf(item),\n\t\t\t\t\t\tmessage: 'Error checking record uniqueness',\n\t\t\t\t\t\terror: e.message,\n\t\t\t\t\t})\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst where = <DataSourceWhere[]>[\n\t\t\t\t{\n\t\t\t\t\tcolumn: primary_key,\n\t\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\t\tvalue: item[primary_key],\n\t\t\t\t},\n\t\t\t]\n\n\t\t\t//Check record exists\n\n\t\t\tconst record = (await this.query.perform(\n\t\t\t\tQueryPerform.FIND_ONE,\n\t\t\t\t{\n\t\t\t\t\tschema,\n\t\t\t\t\twhere,\n\t\t\t\t},\n\t\t\t\tx_request_id,\n\t\t\t)) as FindOneResponseObject\n\n\t\t\tif (!record) {\n\t\t\t\terrored++\n\t\t\t\terrors.push({\n\t\t\t\t\titem: body.indexOf(item),\n\t\t\t\t\tmessage: `Record with id ${item[primary_key]} not found`,\n\t\t\t\t})\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t//Perform role validation on each record\n\t\t\tif (auth.user_identifier) {\n\t\t\t\tconst permission = await this.roles.tablePermission({\n\t\t\t\t\tidentifier: auth.user_identifier,\n\t\t\t\t\ttable: table_name,\n\t\t\t\t\taccess: RolePermission.WRITE,\n\t\t\t\t\tdata: {\n\t\t\t\t\t\t...record,\n\t\t\t\t\t\t...item,\n\t\t\t\t\t},\n\t\t\t\t\tx_request_id,\n\t\t\t\t})\n\n\t\t\t\tif (!public_auth.valid && !permission.valid) {\n\t\t\t\t\terrored++\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\titem: body.indexOf(item),\n\t\t\t\t\t\tmessage: this.response.text((permission as AuthTablePermissionFailResponse).message),\n\t\t\t\t\t})\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tif (permission.valid && (permission as AuthTablePermissionSuccessResponse).allowed_fields?.length) {\n\t\t\t\t\tif (!queryFields?.length) {\n\t\t\t\t\t\tqueryFields = (permission as AuthTablePermissionSuccessResponse).allowed_fields\n\t\t\t\t\t} else {\n\t\t\t\t\t\tqueryFields.push(...(permission as AuthTablePermissionSuccessResponse).allowed_fields)\n\t\t\t\t\t\tqueryFields = queryFields.filter(field =>\n\t\t\t\t\t\t\t(permission as AuthTablePermissionSuccessResponse).allowed_fields.includes(field),\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst result = (await this.query.perform(\n\t\t\t\t\tQueryPerform.UPDATE,\n\t\t\t\t\t{ id: item[primary_key], schema, data: validate.instance },\n\t\t\t\t\tx_request_id,\n\t\t\t\t)) as FindOneResponseObject\n\t\t\t\tawait this.websocket.publish(schema, PublishType.UPDATE, result[schema.primary_key])\n\t\t\t\tawait this.webhooks.publish(\n\t\t\t\t\tschema,\n\t\t\t\t\tPublishType.UPDATE,\n\t\t\t\t\tresult[schema.primary_key],\n\t\t\t\t\tauth.user_identifier,\n\t\t\t\t)\n\t\t\t\tsuccessful++\n\n\t\t\t\tif (queryFields.length) {\n\t\t\t\t\tconst filtered = {}\n\t\t\t\t\tfor (const field of queryFields) {\n\t\t\t\t\t\tfiltered[field] = result[field]\n\t\t\t\t\t}\n\t\t\t\t\tdata.push(filtered)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tdata.push(result)\n\t\t\t} catch (e) {\n\t\t\t\terrored++\n\t\t\t\terrors.push({\n\t\t\t\t\titem: body.indexOf(item),\n\t\t\t\t\tmessage: e.message,\n\t\t\t\t})\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tawait this.dataCache.ping(table_name)\n\n\t\treturn res.status(200).send({\n\t\t\ttotal,\n\t\t\tsuccessful,\n\t\t\terrored,\n\t\t\terrors,\n\t\t\tdata,\n\t\t} as UpdateManyResponseObject)\n\t}\n\n\t@Patch('*/:id')\n\tasync updateByIdPatch(\n\t\t@Req() req,\n\t\t@Res() res,\n\t\t@Body() body: Partial<any>,\n\t\t@Headers() headers: HeaderParams,\n\t\t@Param('id') id: string,\n\t): Promise<FindOneResponseObject> {\n\t\treturn await this.updateById(req, res, body, headers, id)\n\t}\n\n\t@Patch('*/')\n\tasync updateManyPatch(\n\t\t@Req() req,\n\t\t@Res() res,\n\t\t@Body() body: any,\n\t\t@Headers() headers: HeaderParams,\n\t): Promise<UpdateManyResponseObject> {\n\t\treturn await this.updateMany(req, res, body, headers)\n\t}\n}\n"
  },
  {
    "path": "src/app.module.test.spec.ts",
    "content": "import { INestApplication } from '@nestjs/common'\nimport { Test } from '@nestjs/testing'\nimport * as request from 'supertest'\n\nimport { AppModule } from './app.module'\n\ndescribe('App', () => {\n\tlet app: INestApplication\n\n\tbeforeAll(async () => {\n\t\tconst moduleRef = await Test.createTestingModule({\n\t\t\timports: [AppModule],\n\t\t}).compile()\n\n\t\tapp = moduleRef.createNestApplication()\n\t\tawait app.init()\n\n\t\t// Expose the app object globally for debugging\n\t\t;(global as any).app = app\n\t})\n\n\tdescribe('Boots Up', () => {\n\t\tit('Serving 200', async function () {\n\t\t\tawait request(app.getHttpServer()).get('/').expect(200)\n\t\t})\n\t})\n\n\tafterAll(async () => {\n\t\tawait app.close()\n\t})\n})\n"
  },
  {
    "path": "src/app.module.ts",
    "content": "import { CacheModule } from '@nestjs/cache-manager'\nimport { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'\nimport { ConfigModule, ConfigService } from '@nestjs/config'\nimport { JwtModule } from '@nestjs/jwt'\nimport { PassportModule } from '@nestjs/passport'\nimport { ScheduleModule } from '@nestjs/schedule'\nimport Redis from 'ioredis'\n\nimport { AuthController } from './app.controller.auth'\nimport { DeleteController } from './app.controller.delete'\nimport { DocsController } from './app.controller.docs'\nimport { GetController } from './app.controller.get'\nimport { PostController } from './app.controller.post'\nimport { PutController } from './app.controller.put'\nimport { AuthService } from './app.service.auth'\nimport { AppBootup } from './app.service.bootup'\nimport { TasksService } from './app.service.tasks'\nimport { LocalAuthGuard } from './auth/guards/local-auth.guard'\nimport { LocalStrategy } from './auth/strategies/local.strategy'\nimport auth from './config/auth.config'\nimport database from './config/database.config'\nimport { envValidationSchema } from './config/env.validation'\nimport hosts from './config/hosts.config'\nimport jwt from './config/jwt.config'\nimport roles from './config/roles.config'\nimport { Airtable } from './datasources/airtable.datasource'\nimport { Mongo } from './datasources/mongo.datasource'\nimport { MSSQL } from './datasources/mssql.datasource'\nimport { MySQL } from './datasources/mysql.datasource'\nimport { Postgres } from './datasources/postgres.datasource'\nimport { Authentication } from './helpers/Authentication'\nimport { CircuitBreaker } from './helpers/CircuitBreaker'\nimport { Documentation } from './helpers/Documentation'\nimport { Encryption } from './helpers/Encryption'\nimport { Logger } from './helpers/Logger'\nimport { Pagination } from './helpers/Pagination'\nimport { Query } from './helpers/Query'\nimport { Response } from './helpers/Response'\nimport { Roles } from './helpers/Roles'\nimport { Schema } from './helpers/Schema'\nimport { Webhook } from './helpers/Webhook'\nimport { RobotsMiddleware } from './middleware/Robots'\nimport { HostCheckMiddleware } from './middleware/HostCheck'\nimport { RequestPathLoggerMiddleware } from './middleware/request-path-logger.middleware'\nimport { REDIS_CACHE_TOKEN } from './modules/cache/dataCache.constants'\nimport { DataCacheService } from './modules/cache/dataCache.service'\nimport { RedisMockWithPubSub } from './modules/websocket/redis-mock-with-pub-sub'\nimport { REDIS_PUB_CLIENT_TOKEN, REDIS_SUB_CLIENT_TOKEN } from './modules/websocket/websocket.constants'\nimport { WebsocketGateway } from './modules/websocket/websocket.gateway'\nimport { WebsocketService } from './modules/websocket/websocket.service'\nimport { Env } from './utils/Env'\n\nconst singleServerRedisPubsub = new RedisMockWithPubSub() // in-memory pubsub for testing or single server setup\n\nfunction createPubSubOnlyRedisClient() {\n\tif (Env.IsTest() || !process.env.REDIS_PORT || !process.env.REDIS_HOST) {\n\t\tif (!Env.IsTest()) {\n\t\t\tnew Logger().warn('REDIS_PORT or REDIS_HOST not found - Websockets will NOT work in a multi-instance setup')\n\t\t}\n\t\treturn singleServerRedisPubsub\n\t}\n\treturn new Redis(+process.env.REDIS_PORT, process.env.REDIS_HOST, {\n\t\tusername: process.env.REDIS_USER ?? undefined,\n\t\tpassword: process.env.REDIS_PASS ?? undefined,\n\t})\n}\n\nfunction createRedisCache() {\n\tif (process.env.REDIS_PORT && process.env.REDIS_HOST) {\n\t\treturn new Redis(+process.env.REDIS_PORT, process.env.REDIS_HOST, {\n\t\t\tusername: process.env.REDIS_USER ?? undefined,\n\t\t\tpassword: process.env.REDIS_PASS ?? undefined,\n\t\t})\n\t}\n}\n\n@Module({\n\timports: [\n\t\tConfigModule.forRoot({\n\t\t\tload: [auth, database, hosts, jwt, roles],\n\t\t\tvalidationSchema: envValidationSchema,\n\t\t}),\n\t\tJwtModule.registerAsync({\n\t\t\timports: [ConfigModule],\n\t\t\tuseFactory: async (configService: ConfigService) => ({\n\t\t\t\tsecret: configService.get('jwt.secret'),\n\t\t\t\tsignOptions: configService.get('jwt.signOptions'),\n\t\t\t}),\n\t\t\tinject: [ConfigService],\n\t\t}),\n\t\tCacheModule.register({\n\t\t\tisGlobal: true,\n\t\t}),\n\t\tScheduleModule.forRoot(),\n\t\tPassportModule,\n\t],\n\tcontrollers: [AuthController, DocsController, DeleteController, GetController, PostController, PutController],\n\tproviders: [\n\t\tAirtable,\n\t\tAppBootup,\n\t\tAuthService,\n\t\tAuthentication,\n\t\tDataCacheService,\n\t\tDocumentation,\n\t\tEncryption,\n\t\tHostCheckMiddleware,\n\t\tLogger,\n\t\tMongo,\n\t\tMySQL,\n\t\tMSSQL,\n\t\tPagination,\n\t\tPostgres,\n\t\tQuery,\n\t\tResponse,\n\t\tRobotsMiddleware,\n\t\tRoles,\n\t\tSchema,\n\t\tTasksService,\n\t\tWebhook,\n\t\tWebsocketGateway,\n\t\tWebsocketService,\n\t\tCircuitBreaker,\n\t\tLocalStrategy,\n\t\tLocalAuthGuard,\n\t\t{\n\t\t\tprovide: REDIS_PUB_CLIENT_TOKEN,\n\t\t\tuseFactory: createPubSubOnlyRedisClient,\n\t\t},\n\t\t{\n\t\t\tprovide: REDIS_SUB_CLIENT_TOKEN, // A redis client, once subscribed to events, cannot be used for publishing events unfortunately. This is why two are needed\n\t\t\tuseFactory: createPubSubOnlyRedisClient,\n\t\t},\n\t\t{\n\t\t\tprovide: REDIS_CACHE_TOKEN,\n\t\t\tuseFactory: createRedisCache,\n\t\t},\n\t],\n\texports: [\n\t\tAirtable,\n\t\tAppBootup,\n\t\tAuthService,\n\t\tAuthentication,\n\t\tDataCacheService,\n\t\tDocumentation,\n\t\tEncryption,\n\t\tHostCheckMiddleware,\n\t\tLogger,\n\t\tMongo,\n\t\tMySQL,\n\t\tMSSQL,\n\t\tPagination,\n\t\tPostgres,\n\t\tQuery,\n\t\tResponse,\n\t\tRobotsMiddleware,\n\t\tRoles,\n\t\tSchema,\n\t\tWebhook,\n\t\tWebsocketService,\n\t\tWebsocketGateway,\n\t],\n})\nexport class AppModule implements NestModule {\n\tconfigure(consumer: MiddlewareConsumer) {\n\t\tconsumer\n\t\t\t.apply(HostCheckMiddleware, RequestPathLoggerMiddleware, RobotsMiddleware)\n\t\t\t.forRoutes('*')\n\t}\n}\n"
  },
  {
    "path": "src/app.service.auth.ts",
    "content": "import { Injectable, UnauthorizedException } from '@nestjs/common'\nimport { ConfigService } from '@nestjs/config'\nimport { JwtService } from '@nestjs/jwt'\n\nimport { FindOneResponseObject } from './dtos/response.dto'\nimport { Logger } from './helpers/Logger'\nimport { Schema } from './helpers/Schema'\nimport { Auth, AuthType } from './types/auth.types'\n\ntype LoginPayload = {\n\tsub: string\n\temail: string\n}\n\ntype User = FindOneResponseObject & {\n\temail: string\n\tid: number\n}\n\n@Injectable()\nexport class AuthService {\n\tprivate authSchema: any\n\tconstructor(\n\t\tprivate readonly configService: ConfigService,\n\t\tprivate readonly jwtService: JwtService,\n\t\tprivate readonly logger: Logger,\n\t\tprivate readonly schema: Schema,\n\t) {}\n\n\tprivate async getUserPK() {\n\t\tif (!this.authSchema) {\n\t\t\tconst authentications = this.configService.get<Auth[]>('auth')\n\t\t\tconst jwtAuthConfig = authentications.find(auth => auth.type === AuthType.JWT)\n\t\t\tthis.authSchema = await this.schema.getSchema({ table: jwtAuthConfig.table.name })\n\t\t}\n\t\treturn this.authSchema.primary_key\n\t}\n\n\tasync getUserId(jwt: string): Promise<any> {\n\t\tconst payload = await this.jwtService.verifyAsync(jwt)\n\t\treturn payload.sub\n\t}\n\n\tprivate async constructLoginPayload(user: User | LoginPayload) {\n\t\tconst payload = { sub: user[await this.getUserPK()] || user.sub, email: user.email } // in case of User object\n\t\tif (!payload.sub || !payload.email) {\n\t\t\tthrow new UnauthorizedException('Invalid user object')\n\t\t}\n\t\treturn payload\n\t}\n\n\tasync login(user: any): Promise<{ access_token: string }> {\n\t\tconst payload = await this.constructLoginPayload(user)\n\t\tconst access_token = this.jwtService.sign(payload, {\n\t\t\tsecret: process.env.JWT_KEY,\n\t\t\texpiresIn: process.env.JWT_EXPIRES_IN ?? '15m',\n\t\t})\n\t\treturn { access_token }\n\t}\n\n\tasync createRefreshToken(user: User | LoginPayload) {\n\t\tif (!process.env.JWT_REFRESH_KEY) {\n\t\t\tthrow new Error('JWT_REFRESH_KEY not found')\n\t\t}\n\t\tconst payload = await this.constructLoginPayload(user)\n\n\t\treturn this.jwtService.sign(payload, {\n\t\t\tsecret: process.env.JWT_REFRESH_KEY,\n\t\t\texpiresIn: process.env.JWT_REFRESH_EXPIRES_IN ?? '14d',\n\t\t})\n\t}\n\n\tdecodeRefreshToken(token: string): LoginPayload {\n\t\tif (!process.env.JWT_REFRESH_KEY) {\n\t\t\tthrow new Error('JWT_REFRESH_KEY not found')\n\t\t}\n\t\ttry {\n\t\t\treturn this.jwtService.verify(token, {\n\t\t\t\tsecret: process.env.JWT_REFRESH_KEY,\n\t\t\t})\n\t\t} catch {\n\t\t\tthrow new UnauthorizedException('Invalid refresh token')\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/app.service.bootup.ts",
    "content": "import { CACHE_MANAGER } from '@nestjs/cache-manager'\nimport { Inject, Injectable, OnApplicationBootstrap } from '@nestjs/common'\nimport { ConfigService } from '@nestjs/config'\nimport { Cache } from 'cache-manager'\nimport * as fs from 'fs'\n\nimport {\n\tAPP_BOOT_CONTEXT,\n\tLLANA_DATA_CACHING_TABLE,\n\tLLANA_PUBLIC_TABLES,\n\tLLANA_RELATION_TABLE,\n\tLLANA_ROLES_TABLE,\n\tLLANA_WEBHOOK_LOG_TABLE,\n\tLLANA_WEBHOOK_TABLE,\n\tNON_RELATIONAL_DBS,\n\tWEBHOOK_LOG_DAYS,\n} from './app.constants'\nimport { FindManyResponseObject, ListTablesResponseObject } from './dtos/response.dto'\nimport { Authentication } from './helpers/Authentication'\nimport { Documentation } from './helpers/Documentation'\nimport { Logger } from './helpers/Logger'\nimport { Query } from './helpers/Query'\nimport { Schema } from './helpers/Schema'\nimport {\n\tColumnExtraNumber,\n\tColumnExtraString,\n\tDataSourceColumnType,\n\tDataSourceSchema,\n\tDataSourceType,\n\tPublishType,\n\tQueryPerform,\n\tWhereOperator,\n} from './types/datasource.types'\nimport { Method } from './types/response.types'\nimport { CustomRole, DefaultRole, RolePermission } from './types/roles.types'\n\n@Injectable()\nexport class AppBootup implements OnApplicationBootstrap {\n\tconstructor(\n\t\tprivate readonly authentication: Authentication,\n\t\tprivate readonly configService: ConfigService,\n\t\t@Inject(CACHE_MANAGER) private cacheManager: Cache,\n\t\tprivate readonly documentation: Documentation,\n\t\tprivate readonly logger: Logger,\n\t\tprivate readonly query: Query,\n\t\tprivate readonly schema: Schema,\n\t) {}\n\n\tasync onApplicationBootstrap() {\n\t\tthis.logger.log('Bootstrapping Application', APP_BOOT_CONTEXT)\n\n\t\tthis.logger.log(\n\t\t\t`Datasource is ${this.configService.get<string>('database.type').toUpperCase()}`,\n\t\t\tAPP_BOOT_CONTEXT,\n\t\t)\n\n\t\tthis.logger.log('Resetting Cache', APP_BOOT_CONTEXT)\n\t\tawait this.cacheManager.clear()\n\n\t\ttry {\n\t\t\tawait this.query.perform(QueryPerform.CHECK_CONNECTION, undefined, APP_BOOT_CONTEXT)\n\t\t\tthis.logger.log('Database Connection Successful', APP_BOOT_CONTEXT)\n\n\t\t\tif (this.configService.get<string>('database.type') === DataSourceType.POSTGRES) {\n\t\t\t\tthis.logger.log('Resetting PostgreSQL sequences', APP_BOOT_CONTEXT)\n\t\t\t\tawait this.query.perform(QueryPerform.RESET_SEQUENCES, undefined, APP_BOOT_CONTEXT)\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.logger.error(`Database Connection Error - ${e.message}`, APP_BOOT_CONTEXT)\n\n\t\t\tif (process.env.NODE_ENV === 'test') {\n\t\t\t\tthis.logger.warn('Continuing in test environment despite database connection error', APP_BOOT_CONTEXT)\n\t\t\t\treturn // Skip the rest of the bootstrap process in test environment\n\t\t\t} else {\n\t\t\t\tthrow new Error('Database Connection Error')\n\t\t\t}\n\t\t}\n\n\t\tconst database = (await this.query.perform(\n\t\t\tQueryPerform.LIST_TABLES,\n\t\t\t{ include_system: true },\n\t\t\tAPP_BOOT_CONTEXT,\n\t\t)) as ListTablesResponseObject\n\n\t\tif (!database.tables.includes(LLANA_PUBLIC_TABLES)) {\n\t\t\tthis.logger.log(`Creating ${LLANA_PUBLIC_TABLES} schema as it does not exist`, APP_BOOT_CONTEXT)\n\n\t\t\t/**\n\t\t\t * Create the _llana_public_tables schema\n\t\t\t *\n\t\t\t * If you want to open tables up to the public, you can use this table to set the permissions, if you want the whole database\n\t\t\t * to be open, you can use an environment variable to skip the auth checks (recommended alongside host restrictions)\n\t\t\t *\n\t\t\t * |Field | Type | Details|\n\t\t\t * |--------|---------|--------|\n\t\t\t * |`table` | `string` | The table this rule applies to |\n\t\t\t * |`access_level` | `enum` | The permission level to the public, either `READ` `WRITE` `DELETE`|\n\t\t\t * |`allowed_fields` | `string` | A comma separated list of fields that are restricted for this role |\n\t\t\t */\n\n\t\t\tconst schema: DataSourceSchema = {\n\t\t\t\ttable: LLANA_PUBLIC_TABLES,\n\t\t\t\tprimary_key: 'id',\n\t\t\t\tcolumns: [\n\t\t\t\t\t{\n\t\t\t\t\t\tfield: 'id',\n\t\t\t\t\t\ttype: DataSourceColumnType.NUMBER,\n\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\tprimary_key: true,\n\t\t\t\t\t\tunique_key: true,\n\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\tauto_increment: true,\n\t\t\t\t\t\textra: <ColumnExtraNumber>{\n\t\t\t\t\t\t\tdecimal: 0,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tfield: 'table',\n\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tfield: 'access_level',\n\t\t\t\t\t\ttype: DataSourceColumnType.ENUM,\n\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\tenums: ['READ', 'WRITE', 'DELETE'],\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tfield: 'allowed_fields',\n\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\textra: <ColumnExtraString>{\n\t\t\t\t\t\t\tlength: 1020,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t}\n\n\t\t\tif (this.configService.get<string>('SOFT_DELETE_COLUMN')) {\n\t\t\t\t\tschema.columns.push({\n\t\t\t\t\t\tfield: this.configService.get<string>('SOFT_DELETE_COLUMN'),\n\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\tdefault: null,\n\t\t\t\t\t})\n\t\t\t}\n\n\t\t\tconst created = await this.query.perform(QueryPerform.CREATE_TABLE, { schema }, APP_BOOT_CONTEXT)\n\n\t\t\tif (!created) {\n\t\t\t\tthrow new Error(`Failed to create ${LLANA_PUBLIC_TABLES} table`)\n\t\t\t}\n\n\t\t\t// Example Public Tables - For example allowing external API access to see Employee data\n\n\t\t\tif (!this.authentication.skipAuth()) {\n\t\t\t\tconst example_auth: any[] = [\n\t\t\t\t\t{\n\t\t\t\t\t\ttable: 'Employee',\n\t\t\t\t\t\taccess_level: RolePermission.READ,\n\t\t\t\t\t},\n\t\t\t\t]\n\n\t\t\t\tfor (const example of example_auth) {\n\t\t\t\t\tawait this.query.perform(\n\t\t\t\t\t\tQueryPerform.CREATE,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tschema,\n\t\t\t\t\t\t\tdata: example,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAPP_BOOT_CONTEXT,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (!database.tables.includes(LLANA_ROLES_TABLE)) {\n\t\t\tthis.logger.log(`Creating ${LLANA_ROLES_TABLE} schema as it does not exist`, APP_BOOT_CONTEXT)\n\n\t\t\t/**\n\t\t\t * Create the _llana_role schema\n\t\t\t *\n\t\t\t * |Field | Type | Details|\n\t\t\t * |--------|---------|--------|\n\t\t\t * |`custom` | `boolean` | If this is a custom role (applied to specific endpoints) |\n\t\t\t * |`table` | `string` | If not default, which table does this restriction apply to |\n\t\t\t * |`identity_column` | `string` | If not default and the primary key of the table is not the user identifier, which column should be used to identify the user |\n\t\t\t * |`role` | `string` | The name of the role, which should match the value from your users role field |\n\t\t\t * |`records` | `enum` | The permission level for this role across all records in the table, either `NONE` `READ` `WRITE` `DELETE`|\n\t\t\t * |`own_records` | `enum` | The permission level for this role if it includes a reference back to the user identity (their own records) either `NONE` `READ` `WRITE` `DELETE`|\n\t\t\t * |`allowed_fields` | `string` | A comma separated list of fields that are restricted for this role |\n\t\t\t */\n\n\t\t\tconst schema: DataSourceSchema = {\n\t\t\t\ttable: LLANA_ROLES_TABLE,\n\t\t\t\tprimary_key: 'id',\n\t\t\t\tcolumns: [\n\t\t\t\t\t{\n\t\t\t\t\t\tfield: 'id',\n\t\t\t\t\t\ttype: DataSourceColumnType.NUMBER,\n\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\tprimary_key: true,\n\t\t\t\t\t\tunique_key: true,\n\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\tauto_increment: true,\n\t\t\t\t\t\textra: <ColumnExtraNumber>{\n\t\t\t\t\t\t\tdecimal: 0,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tfield: 'custom',\n\t\t\t\t\t\ttype: DataSourceColumnType.BOOLEAN,\n\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tfield: 'table',\n\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tfield: 'identity_column',\n\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tfield: 'role',\n\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tfield: 'records',\n\t\t\t\t\t\ttype: DataSourceColumnType.ENUM,\n\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\tenums: ['NONE', 'READ', 'WRITE', 'DELETE'],\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tfield: 'own_records',\n\t\t\t\t\t\ttype: DataSourceColumnType.ENUM,\n\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\tenums: ['NONE', 'READ', 'WRITE', 'DELETE'],\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tfield: 'allowed_fields',\n\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\textra: <ColumnExtraString>{\n\t\t\t\t\t\t\tlength: 1020,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t}\n\n\t\t\tif (this.configService.get<string>('SOFT_DELETE_COLUMN')) {\n\t\t\t\t\tschema.columns.push({\n\t\t\t\t\t\tfield: this.configService.get<string>('SOFT_DELETE_COLUMN'),\n\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\tdefault: null,\n\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\tconst created = await this.query.perform(QueryPerform.CREATE_TABLE, { schema }, APP_BOOT_CONTEXT)\n\n\t\t\tif (!created) {\n\t\t\t\tthrow new Error('Failed to create _llana_roles table')\n\t\t\t}\n\n\t\t\tif (!this.authentication.skipAuth()) {\n\t\t\t\tconst default_roles: DefaultRole[] = [\n\t\t\t\t\t{\n\t\t\t\t\t\tcustom: false,\n\t\t\t\t\t\trole: 'ADMIN',\n\t\t\t\t\t\trecords: RolePermission.DELETE,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tcustom: false,\n\t\t\t\t\t\trole: 'USER',\n\t\t\t\t\t\trecords: RolePermission.READ,\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t\tconst custom_roles: CustomRole[] = [\n\t\t\t\t\t{\n\t\t\t\t\t\tcustom: true,\n\t\t\t\t\t\trole: 'USER',\n\t\t\t\t\t\ttable: this.authentication.getIdentityTable(),\n\t\t\t\t\t\trecords: RolePermission.NONE,\n\t\t\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tcustom: true,\n\t\t\t\t\t\trole: 'USER',\n\t\t\t\t\t\ttable: this.configService.get<string>('AUTH_USER_API_KEY_TABLE_NAME') ?? 'UserApiKey',\n\t\t\t\t\t\tidentity_column:\n\t\t\t\t\t\t\tthis.configService.get<string>('AUTH_USER_API_KEY_TABLE_IDENTITY_COLUMN') ?? 'userId',\n\t\t\t\t\t\trecords: RolePermission.NONE,\n\t\t\t\t\t\town_records: RolePermission.WRITE,\n\t\t\t\t\t},\n\t\t\t\t]\n\n\t\t\t\tfor (const default_role of default_roles) {\n\t\t\t\t\tawait this.query.perform(\n\t\t\t\t\t\tQueryPerform.CREATE,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tschema,\n\t\t\t\t\t\t\tdata: default_role,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAPP_BOOT_CONTEXT,\n\t\t\t\t\t)\n\t\t\t\t}\n\n\t\t\t\tfor (const custom_role of custom_roles) {\n\t\t\t\t\tawait this.query.perform(\n\t\t\t\t\t\tQueryPerform.CREATE,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tschema,\n\t\t\t\t\t\t\tdata: custom_role,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAPP_BOOT_CONTEXT,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (\n\t\t\t!database.tables.includes(LLANA_RELATION_TABLE) &&\n\t\t\tNON_RELATIONAL_DBS.includes(this.configService.get('database.type'))\n\t\t) {\n\t\t\tthis.logger.log(`Creating ${LLANA_RELATION_TABLE} schema as it does not exist`, APP_BOOT_CONTEXT)\n\n\t\t\tconst schema: DataSourceSchema = {\n\t\t\t\ttable: LLANA_RELATION_TABLE,\n\t\t\t\tprimary_key: 'id',\n\t\t\t\tcolumns: [\n\t\t\t\t\t{\n\t\t\t\t\t\tfield: 'id',\n\t\t\t\t\t\ttype: DataSourceColumnType.NUMBER,\n\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\tprimary_key: true,\n\t\t\t\t\t\tunique_key: true,\n\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\tauto_increment: true,\n\t\t\t\t\t\textra: <ColumnExtraNumber>{\n\t\t\t\t\t\t\tdecimal: 0,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tfield: 'table',\n\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tfield: 'column',\n\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tfield: 'org_table',\n\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tfield: 'org_column',\n\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t}\n\n\t\t\tif (this.configService.get<string>('SOFT_DELETE_COLUMN')) {\n\t\t\t\t\tschema.columns.push({\n\t\t\t\t\t\tfield: this.configService.get<string>('SOFT_DELETE_COLUMN'),\n\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\tdefault: null,\n\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\tconst created = await this.query.perform(QueryPerform.CREATE_TABLE, { schema }, APP_BOOT_CONTEXT)\n\n\t\t\tif (!created) {\n\t\t\t\tthrow new Error(`Failed to create ${LLANA_RELATION_TABLE} table`)\n\t\t\t}\n\t\t}\n\n\t\t// Check if _llana_data_caching table is required\n\n\t\tif (this.configService.get<boolean>('USE_DATA_CACHING')) {\n\t\t\tif (!database.tables.includes(LLANA_DATA_CACHING_TABLE)) {\n\t\t\t\tthis.logger.log(`Creating ${LLANA_DATA_CACHING_TABLE} schema as it does not exist`, APP_BOOT_CONTEXT)\n\n\t\t\t\t/**\n\t\t\t\t * Create the _llana_data_caching schema\n\t\t\t\t */\n\n\t\t\t\tconst schema: DataSourceSchema = {\n\t\t\t\t\ttable: LLANA_DATA_CACHING_TABLE,\n\t\t\t\t\tprimary_key: 'id',\n\t\t\t\t\tcolumns: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'id',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.NUMBER,\n\t\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\t\tprimary_key: true,\n\t\t\t\t\t\t\tunique_key: true,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t\tauto_increment: true,\n\t\t\t\t\t\t\textra: <ColumnExtraNumber>{\n\t\t\t\t\t\t\t\tdecimal: 0,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'table',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'request',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'ttl_seconds',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.NUMBER,\n\t\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t\tdefault: 3600,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'expires_at',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.DATE,\n\t\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t\tdefault: null,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'refreshed_at',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.DATE,\n\t\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t\tdefault: null,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'data_changed_at',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.DATE,\n\t\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t\tdefault: null,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t}\n\n\t\t\t\tif (this.configService.get<string>('SOFT_DELETE_COLUMN')) {\n\t\t\t\t\tschema.columns.push({\n\t\t\t\t\t\tfield: this.configService.get<string>('SOFT_DELETE_COLUMN'),\n\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\tdefault: null,\n\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\t\tconst created = await this.query.perform(QueryPerform.CREATE_TABLE, { schema }, APP_BOOT_CONTEXT)\n\n\t\t\t\tif (!created) {\n\t\t\t\t\tthrow new Error(`Failed to create ${LLANA_DATA_CACHING_TABLE} table`)\n\t\t\t\t}\n\n\t\t\t\tconst example_data_caching: any[] = [\n\t\t\t\t\t{\n\t\t\t\t\t\ttable: 'Employee',\n\t\t\t\t\t\trequest: '?fields=firstName,lastName&limit=10',\n\t\t\t\t\t\tttl_seconds: 3600,\n\t\t\t\t\t\texpires_at: new Date(Date.now() + 3600 * 1000).toISOString(),\n\t\t\t\t\t\trefreshed_at: new Date(Date.now()).toISOString(),\n\t\t\t\t\t\tdata_changed_at: new Date(Date.now()).toISOString(),\n\t\t\t\t\t},\n\t\t\t\t]\n\n\t\t\t\tfor (const example of example_data_caching) {\n\t\t\t\t\tawait this.query.perform(\n\t\t\t\t\t\tQueryPerform.CREATE,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tschema,\n\t\t\t\t\t\t\tdata: example,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAPP_BOOT_CONTEXT,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tthis.logger.log('Skipping table caching as USE_DATA_CACHING is not set', APP_BOOT_CONTEXT)\n\t\t}\n\n\t\t// Check if _llana_webhook table exists\n\n\t\tif (!this.configService.get<boolean>('DISABLE_WEBHOOKS')) {\n\t\t\tif (!database.tables.includes(LLANA_WEBHOOK_TABLE)) {\n\t\t\t\tthis.logger.log(`Creating ${LLANA_WEBHOOK_TABLE} schema as it does not exist`, APP_BOOT_CONTEXT)\n\n\t\t\t\t/**\n\t\t\t\t * Create the _llana_webhook schema\n\t\t\t\t */\n\n\t\t\t\tconst schema: DataSourceSchema = {\n\t\t\t\t\ttable: LLANA_WEBHOOK_TABLE,\n\t\t\t\t\tprimary_key: 'id',\n\t\t\t\t\tcolumns: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'id',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.NUMBER,\n\t\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\t\tprimary_key: true,\n\t\t\t\t\t\t\tunique_key: true,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t\tauto_increment: true,\n\t\t\t\t\t\t\textra: <ColumnExtraNumber>{\n\t\t\t\t\t\t\t\tdecimal: 0,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'type',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.ENUM,\n\t\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t\tenums: [Method.GET, Method.POST, Method.PUT, Method.PATCH, Method.DELETE],\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'url',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'table',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'user_identifier',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t\tdefault: null,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'on_create',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.BOOLEAN,\n\t\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t\tdefault: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'on_update',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.BOOLEAN,\n\t\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t\tdefault: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'on_delete',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.BOOLEAN,\n\t\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t\tdefault: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t}\n\n\t\t\t\tif (this.configService.get<string>('SOFT_DELETE_COLUMN')) {\n\t\t\t\t\tschema.columns.push({\n\t\t\t\t\t\tfield: this.configService.get<string>('SOFT_DELETE_COLUMN'),\n\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\tdefault: null,\n\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\t\tconst created = await this.query.perform(QueryPerform.CREATE_TABLE, { schema }, APP_BOOT_CONTEXT)\n\n\t\t\t\tif (!created) {\n\t\t\t\t\tthrow new Error('Failed to create _llana_webhook table')\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Check if _llana_webhook_log table exists\n\n\t\t\ttry {\n\t\t\t\tconst schema = await this.schema.getSchema({\n\t\t\t\t\ttable: LLANA_WEBHOOK_LOG_TABLE,\n\t\t\t\t\tx_request_id: APP_BOOT_CONTEXT,\n\t\t\t\t})\n\n\t\t\t\tconst log_days = this.configService.get<number>('WEBHOOK_LOG_DAYS') ?? WEBHOOK_LOG_DAYS\n\n\t\t\t\tconst minusXdays = new Date()\n\t\t\t\tminusXdays.setDate(minusXdays.getDate() - log_days)\n\t\t\t\tconst records = (await this.query.perform(QueryPerform.FIND_MANY, {\n\t\t\t\t\tschema,\n\t\t\t\t\tfields: [schema.primary_key],\n\t\t\t\t\twhere: [{ column: 'created_at', operator: WhereOperator.lt, value: minusXdays.toISOString() }],\n\t\t\t\t\tlimit: 99999,\n\t\t\t\t})) as FindManyResponseObject\n\n\t\t\t\tif (records.total > 0) {\n\t\t\t\t\tfor (const record of records.data) {\n\t\t\t\t\t\tawait this.query.perform(\n\t\t\t\t\t\t\tQueryPerform.DELETE,\n\t\t\t\t\t\t\t{ schema, id: record[schema.primary_key] },\n\t\t\t\t\t\t\tAPP_BOOT_CONTEXT,\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t\tthis.logger.log(\n\t\t\t\t\t\t`Deleted ${records.total} records older than ${WEBHOOK_LOG_DAYS} day(s) from ${LLANA_WEBHOOK_LOG_TABLE}`,\n\t\t\t\t\t\tAPP_BOOT_CONTEXT,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\tthis.logger.log(\n\t\t\t\t\t`Creating ${LLANA_WEBHOOK_LOG_TABLE} schema as it does not exist - ${e.message}`,\n\t\t\t\t\tAPP_BOOT_CONTEXT,\n\t\t\t\t)\n\n\t\t\t\t/**\n\t\t\t\t * Create the _llana_webhook_log schema\n\t\t\t\t */\n\n\t\t\t\tconst schema: DataSourceSchema = {\n\t\t\t\t\ttable: LLANA_WEBHOOK_LOG_TABLE,\n\t\t\t\t\tprimary_key: 'id',\n\t\t\t\t\tcolumns: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'id',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.NUMBER,\n\t\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\t\tprimary_key: true,\n\t\t\t\t\t\t\tunique_key: true,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t\tauto_increment: true,\n\t\t\t\t\t\t\textra: <ColumnExtraNumber>{\n\t\t\t\t\t\t\t\tdecimal: 0,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'webhook_id',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.NUMBER,\n\t\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: true,\n\t\t\t\t\t\t\tauto_increment: false,\n\t\t\t\t\t\t\textra: <ColumnExtraNumber>{\n\t\t\t\t\t\t\t\tdecimal: 0,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'type',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.ENUM,\n\t\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t\tenums: [PublishType.INSERT, PublishType.UPDATE, PublishType.DELETE],\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'url',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'record_key',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'record_id',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'attempt',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.NUMBER,\n\t\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t\tdefault: 1,\n\t\t\t\t\t\t\textra: <ColumnExtraNumber>{\n\t\t\t\t\t\t\t\tdecimal: 0,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'delivered',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.BOOLEAN,\n\t\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t\tdefault: false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'response_status',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.NUMBER,\n\t\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t\tdefault: null,\n\t\t\t\t\t\t\textra: <ColumnExtraNumber>{\n\t\t\t\t\t\t\t\tdecimal: 0,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'response_message',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t\tdefault: null,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'created_at',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.DATE,\n\t\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t\tdefault: 'CURRENT_TIMESTAMP',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'next_attempt_at',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.DATE,\n\t\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t\tdefault: 'CURRENT_TIMESTAMP',\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfield: 'delivered_at',\n\t\t\t\t\t\t\ttype: DataSourceColumnType.DATE,\n\t\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t\tdefault: null,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\trelations: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttable: LLANA_WEBHOOK_LOG_TABLE,\n\t\t\t\t\t\t\tcolumn: 'webhook_id',\n\t\t\t\t\t\t\torg_table: LLANA_WEBHOOK_TABLE,\n\t\t\t\t\t\t\torg_column: 'id',\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tconst created = await this.query.perform(QueryPerform.CREATE_TABLE, { schema }, APP_BOOT_CONTEXT)\n\t\t\t\t\t\n\t\t\t\t\tif (!created && process.env.NODE_ENV !== 'test') {\n\t\t\t\t\t\tthrow new Error('Failed to create _llana_webhook_log table')\n\t\t\t\t\t}\n\t\t\t\t} catch (e) {\n\t\t\t\t\tif (process.env.NODE_ENV === 'test') {\n\t\t\t\t\t\tthis.logger.warn(`Skipping webhook log table creation in test environment: ${e.message}`, APP_BOOT_CONTEXT)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthrow e\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tthis.logger.warn('Skipping webhooks as DISABLE_WEBHOOKS is set to true', APP_BOOT_CONTEXT)\n\t\t}\n\n\t\tif (this.authentication.skipAuth()) {\n\t\t\tthis.logger.warn(\n\t\t\t\t'Skipping auth is set to true, you should maintain _llana_public_tables table for any WRITE permissions',\n\t\t\t\tAPP_BOOT_CONTEXT,\n\t\t\t)\n\t\t}\n\n\t\tif (this.documentation.skipDocs()) {\n\t\t\tthis.logger.warn('Skipping docs is set to true', APP_BOOT_CONTEXT)\n\t\t} else {\n\t\t\tconst docs = await this.documentation.generateDocumentation()\n\n\t\t\t//write docs to file to be consumed by the UI\n\n\t\t\tthis.logger.log('Docs Generated', APP_BOOT_CONTEXT)\n\t\t\tfs.writeFileSync('openapi.json', JSON.stringify(docs))\n\t\t}\n\n\t\tthis.logger.log('Application Bootstrapping Complete', APP_BOOT_CONTEXT)\n\t}\n}\n"
  },
  {
    "path": "src/app.service.tasks.ts",
    "content": "import { Injectable } from '@nestjs/common'\nimport { ConfigService } from '@nestjs/config'\nimport { Cron, CronExpression } from '@nestjs/schedule'\n\nimport { Webhook } from './helpers/Webhook'\nimport { DataCacheService } from './modules/cache/dataCache.service'\n\nlet webhookSchedule: string = CronExpression.EVERY_30_SECONDS\nlet cacheSchedule: string = CronExpression.EVERY_MINUTE\n\n@Injectable()\nexport class TasksService {\n\tconstructor(\n\t\tprivate readonly configService: ConfigService,\n\t\tprivate readonly webhook: Webhook,\n\t\tprivate readonly dataCache: DataCacheService,\n\t) {\n\t\twebhookSchedule =\n\t\t\tCronExpression[this.configService.get('CRON_EXPRESSION_WEBHOOKS_SEND')] ??\n\t\t\t(CronExpression.EVERY_30_SECONDS as CronExpression)\n\t\tcacheSchedule =\n\t\t\tCronExpression[this.configService.get('CRON_EXPRESSION_CACHE_CHECK')] ??\n\t\t\t(CronExpression.EVERY_MINUTE as CronExpression)\n\t}\n\n\t@Cron(webhookSchedule)\n\tasync sendWebhooks() {\n\t\tif (this.configService.get<boolean>('DISABLE_WEBHOOKS')) {\n\t\t\treturn\n\t\t}\n\n\t\tconst webhooks = await this.webhook.getPendingWebhooks()\n\n\t\tfor (const webhook of webhooks) {\n\t\t\tawait this.webhook.sendWebhook(webhook)\n\t\t}\n\t}\n\n\t@Cron(cacheSchedule)\n\tasync checkCache() {\n\t\tawait this.dataCache.refresh(cacheSchedule as CronExpression)\n\t}\n}\n"
  },
  {
    "path": "src/auth/auth.constants.ts",
    "content": "export const JWT = 'jwt'\n\nexport const ACCESS_TOKEN_COOKIE_NAME = 'accessToken'\nexport const REFRESH_TOKEN_COOKIE_NAME = 'refreshToken'\nexport const IS_LOGGED_IN_COOKIE_NAME = 'isLlanaLoggedIn'\n"
  },
  {
    "path": "src/auth/guards/jwt-auth.guard.ts",
    "content": "import { Injectable } from '@nestjs/common'\nimport { AuthGuard } from '@nestjs/passport'\n\nimport { JWT } from '../auth.constants'\n\n@Injectable()\nexport class JwtAuthGuard extends AuthGuard(JWT) {}\n"
  },
  {
    "path": "src/auth/guards/local-auth.guard.ts",
    "content": "import { Injectable } from '@nestjs/common'\nimport { AuthGuard } from '@nestjs/passport'\n\n@Injectable()\nexport class LocalAuthGuard extends AuthGuard('local') {}\n"
  },
  {
    "path": "src/auth/strategies/local.strategy.ts",
    "content": "import { Injectable, UnauthorizedException } from '@nestjs/common'\nimport { ConfigService } from '@nestjs/config'\nimport { PassportStrategy } from '@nestjs/passport'\nimport { Request } from 'express'\nimport { Strategy } from 'passport-local'\nimport { Encryption } from 'src/helpers/Encryption'\nimport { DataSourceSchema, DataSourceWhere, QueryPerform, WhereOperator } from 'src/types/datasource.types'\n\nimport { Logger } from '../../helpers/Logger'\nimport { Query } from '../../helpers/Query'\nimport { Schema } from '../../helpers/Schema'\nimport { Auth, AuthJWT, AuthType } from '../../types/auth.types'\n\n@Injectable()\nexport class LocalStrategy extends PassportStrategy(Strategy) {\n\tconstructor(\n\t\tprivate readonly configService: ConfigService,\n\t\tprivate readonly encryption: Encryption,\n\t\tprivate readonly logger: Logger,\n\t\tprivate readonly query: Query,\n\t\tprivate readonly schema: Schema,\n\t) {\n\t\tsuper({ usernameField: 'username', passReqToCallback: true })\n\t}\n\n\tasync validate(req: Request, username: string, pass: string): Promise<any> {\n\t\tconst x_request_id = req.headers['x-request-id'] as string\n\n\t\tconst authentications = this.configService.get<Auth[]>('auth')\n\n\t\tconst jwtAuthConfig = authentications.find(auth => auth.type === AuthType.JWT)\n\n\t\tif (!jwtAuthConfig) {\n\t\t\tthis.logger.error('JWT authentication not configured')\n\t\t\tthrow new UnauthorizedException()\n\t\t}\n\n\t\tlet schema: DataSourceSchema\n\t\ttry {\n\t\t\tschema = await this.schema.getSchema({ table: jwtAuthConfig.table.name, x_request_id })\n\t\t} catch (e) {\n\t\t\tthis.logger.error(e)\n\t\t\tthrow new UnauthorizedException()\n\t\t}\n\n\t\tconst where: DataSourceWhere[] = [\n\t\t\t{\n\t\t\t\tcolumn: (jwtAuthConfig.table as AuthJWT).columns.username,\n\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\tvalue: username,\n\t\t\t},\n\t\t]\n\n\t\tif (this.configService.get('database.deletes.soft')) {\n\t\t\twhere.push({\n\t\t\t\tcolumn: this.configService.get('database.deletes.soft'),\n\t\t\t\toperator: WhereOperator.null,\n\t\t\t})\n\t\t}\n\n\t\tconst user = await this.query.perform(\n\t\t\tQueryPerform.FIND_ONE,\n\t\t\t{\n\t\t\t\tschema,\n\t\t\t\twhere,\n\t\t\t},\n\t\t\tx_request_id,\n\t\t)\n\n\t\tif (!user) {\n\t\t\tthrow new UnauthorizedException()\n\t\t}\n\n\t\ttry {\n\t\t\tif (\n\t\t\t\tawait this.encryption.compare(\n\t\t\t\t\tpass,\n\t\t\t\t\tuser[(jwtAuthConfig.table as AuthJWT).columns.password],\n\t\t\t\t\t(jwtAuthConfig.table as AuthJWT).password.encryption,\n\t\t\t\t\t(jwtAuthConfig.table as AuthJWT).password.salt,\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\treturn user\n\t\t\t}\n\t\t\tthrow new UnauthorizedException()\n\t\t} catch (e) {\n\t\t\tthis.logger.debug(e)\n\t\t\tthrow new UnauthorizedException()\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/config/auth.config.ts",
    "content": "import { registerAs } from '@nestjs/config'\n\nimport { Auth, AuthAPIKey, AuthJWT, AuthLocation, AuthPasswordEncryption, AuthType } from '../types/auth.types'\n\nexport default registerAs(\n\t'auth',\n\t() =>\n\t\t<Auth[]>[\n\t\t\t{\n\t\t\t\ttype: AuthType.APIKEY,\n\t\t\t\tlocation: process.env.AUTH_USER_API_KEY_LOCATION ?? AuthLocation.HEADER,\n\t\t\t\tname: process.env.AUTH_USER_API_KEY_NAME ?? 'x-api-key',\n\t\t\t\ttable: <AuthAPIKey>{\n\t\t\t\t\tname: process.env.AUTH_USER_TABLE_NAME ?? 'User', //should start at your main users identity table\n\t\t\t\t\tidentity_column: process.env.AUTH_USER_API_KEY_TABLE_IDENTITY_COLUMN ?? undefined,\n\t\t\t\t\tcolumn: process.env.AUTH_USER_API_KEY_FIELD ?? 'UserApiKey.apiKey',\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\ttype: AuthType.JWT,\n\t\t\t\ttable: <AuthJWT>{\n\t\t\t\t\tname: process.env.AUTH_USER_TABLE_NAME ?? 'User', //should start at your main users identity table\n\t\t\t\t\tidentity_column: process.env.AUTH_USER_IDENTITY_COLUMN ?? undefined,\n\t\t\t\t\tcolumns: {\n\t\t\t\t\t\tusername: process.env.AUTH_USER_TABLE_USERNAME_FIELD ?? 'email',\n\t\t\t\t\t\tpassword: process.env.AUTH_USER_TABLE_PASSWORD_FIELD ?? 'password',\n\t\t\t\t\t},\n\t\t\t\t\tpassword: {\n\t\t\t\t\t\tencryption: process.env.AUTH_USER_TABLE_PASSWORD_ENCRYPTION ?? AuthPasswordEncryption.BCRYPT,\n\t\t\t\t\t\tsalt: process.env.AUTH_USER_TABLE_PASSWORD_SALT ?? 10,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t],\n)\n"
  },
  {
    "path": "src/config/class-validator.config.ts",
    "content": "export const classValidatorConfig = {\n\tforbidUnknownValues: false,\n}\n"
  },
  {
    "path": "src/config/database.config.ts",
    "content": "import 'dotenv/config'\n\nimport { registerAs } from '@nestjs/config'\n\nimport { getDatabaseType } from '../helpers/Database'\nimport { DataSourceConfig } from '../types/datasource.types'\n\nexport default registerAs(\n\t'database',\n\t() =>\n\t\t<DataSourceConfig>{\n\t\t\ttype: getDatabaseType(process.env.DATABASE_URI),\n\t\t\thost: process.env.DATABASE_URI,\n\t\t\tpoolSize: Number(process.env.DATABASE_POOL_SIZE || 10),\n\t\t\tpoolIdleTimeout: Number(process.env.DATABASE_POOL_IDLE_TIMEOUT || 60000),\n\t\t\tdefaults: {\n\t\t\t\tlimit: Number(process.env.DEFAULT_LIMIT) || 20,\n\t\t\t\trelations: {\n\t\t\t\t\tlimit: Number(process.env.DEFAULT_RELATIONS_LIMIT) || 20,\n\t\t\t\t},\n\t\t\t},\n\t\t\tdeletes: {\n\t\t\t\tsoft: process.env.SOFT_DELETE_COLUMN ?? undefined,\n\t\t\t},\n\t\t},\n)\n"
  },
  {
    "path": "src/config/env.validation.spec.ts",
    "content": "import { envValidationSchema } from './env.validation'\n\ndescribe('Environment Validation', () => {\n\tdescribe('PORT validation', () => {\n\t\tit('should default PORT to 3000 when blank', () => {\n\t\t\tconst result = envValidationSchema.validate({\n\t\t\t\tPORT: '',\n\t\t\t\tDATABASE_URI: 'mongodb://localhost:27017/test',\n\t\t\t})\n\t\t\texpect(result.error).toBeUndefined()\n\t\t\texpect(result.value.PORT).toBe(3000)\n\t\t})\n\n\t\tit('should accept numeric string PORT value', () => {\n\t\t\tconst result = envValidationSchema.validate({\n\t\t\t\tPORT: '8080',\n\t\t\t\tDATABASE_URI: 'mongodb://localhost:27017/test',\n\t\t\t})\n\t\t\texpect(result.error).toBeUndefined()\n\t\t\texpect(result.value.PORT).toBe(8080)\n\t\t})\n\n\t\tit('should accept number PORT value', () => {\n\t\t\tconst result = envValidationSchema.validate({\n\t\t\t\tPORT: 9090,\n\t\t\t\tDATABASE_URI: 'mongodb://localhost:27017/test',\n\t\t\t})\n\t\t\texpect(result.error).toBeUndefined()\n\t\t\texpect(result.value.PORT).toBe(9090)\n\t\t})\n\n\t\tit('should default PORT to 3000 when undefined', () => {\n\t\t\tconst result = envValidationSchema.validate({\n\t\t\t\tDATABASE_URI: 'mongodb://localhost:27017/test',\n\t\t\t})\n\t\t\texpect(result.error).toBeUndefined()\n\t\t\texpect(result.value.PORT).toBe(3000)\n\t\t})\n\n\t\tit('should reject invalid PORT values', () => {\n\t\t\tconst result = envValidationSchema.validate({\n\t\t\t\tPORT: 'invalid',\n\t\t\t\tDATABASE_URI: 'mongodb://localhost:27017/test',\n\t\t\t})\n\t\t\texpect(result.error).toBeDefined()\n\t\t\texpect(result.error?.message).toContain('PORT')\n\t\t})\n\t})\n})\n"
  },
  {
    "path": "src/config/env.validation.ts",
    "content": "import * as Joi from 'joi'\n\nimport { AuthPasswordEncryption } from '../types/auth.types'\n\nexport const envValidationSchema = Joi.object({\n\tNODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),\n\tPORT: Joi.number().empty('').default(3000),\n\tDATABASE_URI: Joi.string().uri().required(),\n\tJWT_KEY: Joi.string().min(8).default('S$3cr3tK3y'),\n\tJWT_EXPIRES_IN: Joi.string().default('15m'),\n\tJWT_REFRESH_KEY: Joi.string().min(8).default('S$3cr3tK3yRefresh'),\n\tJWT_REFRESH_EXPIRES_IN: Joi.string().default('14d'),\n\tAUTH_USER_API_KEY_LOCATION: Joi.string().default('HEADER'),\n\tAUTH_USER_API_KEY_NAME: Joi.string().default('x-api-key'),\n\tAUTH_USER_TABLE_NAME: Joi.string().default('User'),\n\tAUTH_USER_API_KEY_TABLE_IDENTITY_COLUMN: Joi.string().optional(),\n\tAUTH_USER_API_KEY_FIELD: Joi.string().default('UserApiKey.apiKey'),\n\tAUTH_USER_IDENTITY_COLUMN: Joi.string().optional(),\n\tAUTH_USER_TABLE_USERNAME_FIELD: Joi.string().default('email'),\n\tAUTH_USER_TABLE_PASSWORD_FIELD: Joi.string().default('password'),\n\tAUTH_USER_TABLE_PASSWORD_ENCRYPTION: Joi.string().default(AuthPasswordEncryption.BCRYPT),\n\tAUTH_USER_TABLE_PASSWORD_SALT: Joi.number().default(10),\n\tDEFAULT_LIMIT: Joi.number().default(20),\n\tDEFAULT_RELATIONS_LIMIT: Joi.number().default(20),\n\tSOFT_DELETE_COLUMN: Joi.string().optional(),\n\tCRON_EXPRESSION_WEBHOOKS_SEND: Joi.string()\n\t\t.pattern(\n\t\t\t/^(\\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\\*\\/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])) (\\*|([0-9]|1[0-9]|2[0-3])|\\*\\/([0-9]|1[0-9]|2[0-3])) (\\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|\\*\\/([1-9]|1[0-9]|2[0-9]|3[0-1])) (\\*|([1-9]|1[0-2])|\\*\\/([1-9]|1[0-2])) (\\*|([0-6])|\\*\\/([0-6]))$/,\n\t\t)\n\t\t.default('*/5 * * * *')\n\t\t.messages({\n\t\t\t'string.pattern.base': 'Invalid cron expression format',\n\t\t}),\n\tDISABLE_WEBHOOKS: Joi.boolean().default(false),\n\tDOCS_TITLE: Joi.string().default('API Documentation'),\n\tHOSTS: Joi.string().optional(),\n})\n"
  },
  {
    "path": "src/config/hosts.config.ts",
    "content": "import { registerAs } from '@nestjs/config'\n\n/**\n * If you would like to globally lock down your API to specific hosts, you can add them here.\n */\n\nexport default registerAs('hosts', () => (process.env.HOSTS ? [process.env.HOSTS.split(',')] : []))\n"
  },
  {
    "path": "src/config/jwt.config.ts",
    "content": "import { registerAs } from '@nestjs/config'\nexport default registerAs(\n\t'jwt',\n\t() =>\n\t\t<any>{\n\t\t\tsecret: process.env.JWT_KEY,\n\t\t\tsignOptions: { expiresIn: process.env.JWT_EXPIRES_IN ?? '15m' },\n\t\t},\n)\n"
  },
  {
    "path": "src/config/roles.config.ts",
    "content": "import { registerAs } from '@nestjs/config'\n\nimport { RoleLocation, RolesConfig } from '../types/roles.types'\n\nexport default registerAs(\n\t'roles',\n\t() =>\n\t\t<RolesConfig>{\n\t\t\tlocation: <RoleLocation>{\n\t\t\t\ttable: process.env.ROLE_LOCATION_USER_TABLE_NAME ?? process.env.AUTH_USER_TABLE_NAME ?? 'User',\n\t\t\t\tcolumn: process.env.ROLE_LOCATION_USER_TABLE_ROLE_FIELD ?? 'role',\n\t\t\t\tidentifier_column: process.env.ROLE_LOCATION_USER_TABLE_IDENTITY_COLUMN ?? undefined,\n\t\t\t},\n\t\t},\n)\n"
  },
  {
    "path": "src/datasources/airtable.datasource.ts",
    "content": "import { Injectable } from '@nestjs/common'\nimport { ConfigService } from '@nestjs/config'\nimport axios from 'axios'\n\nimport {\n\tDeleteResponseObject,\n\tFindManyResponseObject,\n\tFindOneResponseObject,\n\tIsUniqueResponse,\n} from '../dtos/response.dto'\nimport { Logger } from '../helpers/Logger'\nimport { Pagination } from '../helpers/Pagination'\nimport { DatabaseErrorType } from '../types/datasource.types'\nimport {\n\tDataSourceColumnType,\n\tDataSourceCreateOneOptions,\n\tDataSourceDeleteOneOptions,\n\tDataSourceFindManyOptions,\n\tDataSourceFindOneOptions,\n\tDataSourceFindTotalRecords,\n\tDataSourceSchema,\n\tDataSourceSchemaColumn,\n\tDataSourceSchemaRelation,\n\tDataSourceType,\n\tDataSourceUniqueCheckOptions,\n\tDataSourceUpdateOneOptions,\n\tDataSourceWhere,\n\tWhereOperator,\n} from '../types/datasource.types'\nimport { AirtableColumnType } from '../types/datasources/airtable.types'\n\nconst DATABASE_TYPE = DataSourceType.AIRTABLE\nconst ENDPOINT = 'https://api.airtable.com/v0'\n\n@Injectable()\nexport class Airtable {\n\tconstructor(\n\t\tprivate readonly configService: ConfigService,\n\t\tprivate readonly logger: Logger,\n\t\tprivate readonly pagination: Pagination,\n\t) {}\n\n\tasync createRequest(options: {\n\t\tendpoint: string\n\t\tmethod?: 'GET' | 'POST' | 'PATCH' | 'DELETE'\n\t\tdata?: any\n\t\tx_request_id?: string\n\t}): Promise<any> {\n\t\tif (!options.method) {\n\t\t\toptions.method = 'GET'\n\t\t}\n\n\t\tconst [apiKey, baseId] = this.configService.get('database.host').split('://')[1].split('@')\n\n\t\tconst endpoint = options.endpoint.replace('BaseId', baseId)\n\n\t\ttry {\n\t\t\tconst response = await axios({\n\t\t\t\tmethod: options.method,\n\t\t\t\turl: `${ENDPOINT}${endpoint}`,\n\t\t\t\tdata: options.data,\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t\t\t},\n\t\t\t})\n\n\t\t\treturn response.data\n\t\t} catch (e) {\n\t\t\tthis.logger.error(`[${DATABASE_TYPE}] ${e.message}`, options.x_request_id)\n\t\t\tconsole.error({\n\t\t\t\t...e.response.data,\n\t\t\t\tstatus: e.response.status,\n\t\t\t\tstatusText: e.response.statusText,\n\t\t\t\trequest: {\n\t\t\t\t\tmethod: options.method,\n\t\t\t\t\turl: `${ENDPOINT}${endpoint}`,\n\t\t\t\t\tdata: options.data,\n\t\t\t\t\theaders: {\n\t\t\t\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})\n\t\t\tthis.logger.error(`Data passed: `, options.x_request_id)\n\t\t}\n\t}\n\n\tasync checkConnection(options: { x_request_id?: string }): Promise<boolean> {\n\t\ttry {\n\t\t\tawait this.createRequest({\n\t\t\t\tendpoint: '/meta/bases',\n\t\t\t\tmethod: 'GET',\n\t\t\t\tx_request_id: options.x_request_id,\n\t\t\t})\n\t\t\treturn true\n\t\t} catch (e) {\n\t\t\tthis.logger.error(\n\t\t\t\t`[${DATABASE_TYPE}] Error checking database connection - ${e.message}`,\n\t\t\t\toptions.x_request_id,\n\t\t\t)\n\t\t\treturn false\n\t\t}\n\t}\n\n\t/**\n\t * List Tables\n\t */\n\n\tasync listTables(options: { x_request_id?: string }): Promise<string[]> {\n\t\ttry {\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] List Tables`, options.x_request_id)\n\n\t\t\tconst response = await this.createRequest({\n\t\t\t\tendpoint: `/meta/bases/BaseId/tables`,\n\t\t\t\tx_request_id: options.x_request_id,\n\t\t\t})\n\n\t\t\tconst tables = response.tables.map((table: any) => table.name)\n\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Tables: ${tables.join(',')}`, options.x_request_id)\n\n\t\t\treturn tables\n\t\t} catch (e) {\n\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Error listing tables - ${e.message}`)\n\t\t\tthrow new Error(e)\n\t\t}\n\t}\n\n\t/**\n\t * Get Table Schema\n\t * @param repository\n\t * @param table_name\n\t */\n\n\tasync getSchema(options: { table: string; x_request_id?: string }): Promise<DataSourceSchema> {\n\t\ttry {\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Get Schema for table ${options.table}`, options.x_request_id)\n\n\t\t\tconst response = await this.createRequest({\n\t\t\t\tendpoint: `/meta/bases/BaseId/tables`,\n\t\t\t\tx_request_id: options.x_request_id,\n\t\t\t})\n\n\t\t\tconst table = response.tables.find((t: any) => t.name === options.table)\n\n\t\t\tif (!table) {\n\t\t\t\tthrow new Error('Table not found')\n\t\t\t}\n\n\t\t\tlet columns: DataSourceSchemaColumn[] = []\n\t\t\tlet relations: DataSourceSchemaRelation[] = []\n\n\t\t\t//pass in ID column as primary key\n\t\t\tcolumns.push({\n\t\t\t\tfield: 'id',\n\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\tnullable: false,\n\t\t\t\trequired: false,\n\t\t\t\tprimary_key: true,\n\t\t\t\tunique_key: true,\n\t\t\t\tforeign_key: false,\n\t\t\t\tdefault: null,\n\t\t\t\textra: {\n\t\t\t\t\tnote: 'Airtable Autogenerated ID',\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tfor (const field of table.fields) {\n\t\t\t\tif (field.type === AirtableColumnType.MULTIPLE_RECORD_LINKS) {\n\t\t\t\t\tlet linkedTable = response.tables.find((t: any) => t.id === field.options.linkedTableId)\n\n\t\t\t\t\trelations.push({\n\t\t\t\t\t\ttable: linkedTable.name,\n\t\t\t\t\t\tcolumn: 'id',\n\t\t\t\t\t\torg_table: options.table,\n\t\t\t\t\t\torg_column: field.name,\n\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\t\tcolumns.push({\n\t\t\t\t\tfield: field.name,\n\t\t\t\t\ttype: this.fieldMapper(field.type),\n\t\t\t\t\tnullable: true,\n\t\t\t\t\trequired: false,\n\t\t\t\t\tprimary_key: false,\n\t\t\t\t\tunique_key: false,\n\t\t\t\t\tforeign_key: field.type === AirtableColumnType.MULTIPLE_RECORD_LINKS,\n\t\t\t\t\tdefault: null,\n\t\t\t\t\textra: field,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t//Build reverse relations\n\t\t\tfor (const table of response.tables) {\n\t\t\t\tfor (const field of table.fields) {\n\t\t\t\t\tif (field.type === AirtableColumnType.MULTIPLE_RECORD_LINKS) {\n\t\t\t\t\t\tif (field.options.linkedTableId === table.id) {\n\t\t\t\t\t\t\trelations.push({\n\t\t\t\t\t\t\t\ttable: options.table,\n\t\t\t\t\t\t\t\tcolumn: field.name,\n\t\t\t\t\t\t\t\torg_table: table.name,\n\t\t\t\t\t\t\t\torg_column: 'id',\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst schema = {\n\t\t\t\ttable: options.table,\n\t\t\t\tcolumns,\n\t\t\t\tprimary_key: columns.find(column => column.primary_key)?.field,\n\t\t\t\trelations,\n\t\t\t}\n\n\t\t\treturn schema\n\t\t} catch (e) {\n\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Error getting schema - ${e.message}`)\n\t\t\tthrow new Error(e)\n\t\t}\n\t}\n\n\t/**\n\t * Insert a record\n\t */\n\n\tasync createOne(options: DataSourceCreateOneOptions, x_request_id?: string): Promise<FindOneResponseObject> {\n\t\tthis.logger.verbose(\n\t\t\t`[${DATABASE_TYPE}] Create Record on ${options.schema.table}: ${JSON.stringify(options.data)}`,\n\t\t\tx_request_id,\n\t\t)\n\n\t\ttry {\n\t\t\tfor (const col of options.schema.columns) {\n\t\t\t\tif (col.foreign_key) {\n\t\t\t\t\tif (options.data[col.field]) {\n\t\t\t\t\t\tif (!Array.isArray(options.data[col.field])) {\n\t\t\t\t\t\t\toptions.data[col.field] = [options.data[col.field]]\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst linkedTable = options.schema.relations.find(r => r.org_column === col.field)\n\n\t\t\t\t\t\tfor (const id of options.data[col.field]) {\n\t\t\t\t\t\t\tconst linkedSchema = await this.getSchema({ table: linkedTable.table })\n\t\t\t\t\t\t\tconst linkedRecord = await this.findOne(\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tschema: linkedSchema,\n\t\t\t\t\t\t\t\t\twhere: [{ column: 'id', operator: WhereOperator.equals, value: id }],\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t\t\t)\n\n\t\t\t\t\t\t\tif (!linkedRecord) {\n\t\t\t\t\t\t\t\tthrow new Error('Linked record not found')\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst result = await this.createRequest({\n\t\t\t\tendpoint: `/BaseId/${options.schema.table}`,\n\t\t\t\tmethod: 'POST',\n\t\t\t\tdata: {\n\t\t\t\t\trecords: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tfields: options.data,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\tx_request_id,\n\t\t\t})\n\n\t\t\tif (!result.records || result.records.length === 0) {\n\t\t\t\tthrow new Error('Record not created')\n\t\t\t}\n\n\t\t\tthis.logger.verbose(`[${DATABASE_TYPE}] Results: ${JSON.stringify(result)} - ${x_request_id}`)\n\n\t\t\treturn {\n\t\t\t\tid: result.records[0].id,\n\t\t\t\t...result.records[0].fields,\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Error executing query`, x_request_id)\n\t\t\tthis.logger.warn({\n\t\t\t\tdata: options.data,\n\t\t\t\terror: {\n\t\t\t\t\tmessage: e.message,\n\t\t\t\t},\n\t\t\t})\n\t\t\tthrow new Error(e)\n\t\t}\n\t}\n\n\t/**\n\t * Find single record\n\t */\n\n\tasync findOne(options: DataSourceFindOneOptions, x_request_id: string): Promise<FindOneResponseObject | undefined> {\n\t\ttry {\n\t\t\tthis.logger.verbose(\n\t\t\t\t`[${DATABASE_TYPE}] Find Record on ${options.schema.table}: ${JSON.stringify(options.where)}`,\n\t\t\t\tx_request_id,\n\t\t\t)\n\n\t\t\tconst fields =\n\t\t\t\toptions.fields?.length > 0\n\t\t\t\t\t? options.fields\n\t\t\t\t\t: [...options.schema.columns.map(c => c.field)].filter(f => f !== 'id')\n\n\t\t\tconst id = options.where.find(w => w.column === options.schema.primary_key)?.value\n\n\t\t\tif (!id) {\n\t\t\t\t// Find Many and return first result\n\t\t\t\tconst results = await this.findMany(\n\t\t\t\t\t{\n\t\t\t\t\t\tfields,\n\t\t\t\t\t\tschema: options.schema,\n\t\t\t\t\t\twhere: options.where,\n\t\t\t\t\t\tlimit: 1,\n\t\t\t\t\t\toffset: 0,\n\t\t\t\t\t},\n\t\t\t\t\tx_request_id,\n\t\t\t\t)\n\n\t\t\t\treturn results.data[0]\n\t\t\t}\n\n\t\t\tlet endpoint = `/BaseId/${options.schema.table}/${id}`\n\n\t\t\tconst result = await this.createRequest({\n\t\t\t\tendpoint,\n\t\t\t\tx_request_id,\n\t\t\t})\n\n\t\t\tif (!result.id) {\n\t\t\t\tthrow new Error('Record not found')\n\t\t\t}\n\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Result: ${JSON.stringify(result)}`, x_request_id)\n\n\t\t\treturn this.formatOutput(options, {\n\t\t\t\tid: result.id,\n\t\t\t\t...result.fields,\n\t\t\t})\n\t\t} catch (e) {\n\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Error executing query`, x_request_id)\n\t\t\tthis.logger.warn({\n\t\t\t\tdata: options.where,\n\t\t\t\terror: {\n\t\t\t\t\tmessage: e.message,\n\t\t\t\t},\n\t\t\t})\n\t\t\tthrow new Error(e)\n\t\t}\n\t}\n\n\t/**\n\t * Find multiple records\n\t */\n\n\tasync findMany(options: DataSourceFindManyOptions, x_request_id: string): Promise<FindManyResponseObject> {\n\t\t//If primary key is passed in where clause, return single record\n\t\tif (options.where.length === 1 && options.where[0].column === options.schema.primary_key) {\n\t\t\treturn {\n\t\t\t\tlimit: options.limit,\n\t\t\t\toffset: options.offset,\n\t\t\t\ttotal: 1,\n\t\t\t\tpagination: {\n\t\t\t\t\ttotal: 1,\n\t\t\t\t\tpage: {\n\t\t\t\t\t\tcurrent: this.pagination.current(options.limit, options.offset),\n\t\t\t\t\t\tprev: this.pagination.previous(options.limit, options.offset),\n\t\t\t\t\t\tnext: this.pagination.next(options.limit, options.offset, 1),\n\t\t\t\t\t\tfirst: this.pagination.first(options.limit),\n\t\t\t\t\t\tlast: this.pagination.last(options.limit, 1),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdata: [\n\t\t\t\t\tawait this.findOne(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tschema: options.schema,\n\t\t\t\t\t\t\twhere: options.where,\n\t\t\t\t\t\t\tfields: options.fields,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t),\n\t\t\t\t],\n\t\t\t}\n\t\t}\n\n\t\tconst total = await this.findTotalRecords(options, x_request_id)\n\n\t\ttry {\n\t\t\tthis.logger.debug(\n\t\t\t\t`[${DATABASE_TYPE}] Find Record on ${options.schema.table}: ${JSON.stringify(options.where)}`,\n\t\t\t\tx_request_id,\n\t\t\t)\n\n\t\t\t// Sort\n\t\t\tlet sort = []\n\n\t\t\tif (options.sort) {\n\t\t\t\tfor (const s of options.sort) {\n\t\t\t\t\tsort.push({\n\t\t\t\t\t\tfield: s.column,\n\t\t\t\t\t\tdirection: s.operator.toLowerCase(),\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!options.limit) {\n\t\t\t\toptions.limit = this.configService.get<number>('database.defaults.limit') ?? 20\n\t\t\t}\n\n\t\t\tlet offset = undefined\n\n\t\t\tif (options.offset) {\n\t\t\t\toffset = options.offset\n\t\t\t}\n\n\t\t\tconst filterByFormula = await this.whereToFilter(options.where, options.schema)\n\n\t\t\tconst fields =\n\t\t\t\toptions.fields?.length > 0\n\t\t\t\t\t? options.fields\n\t\t\t\t\t: [...options.schema.columns.map(c => c.field)].filter(f => f !== 'id')\n\n\t\t\tif (offset) {\n\t\t\t\t//Offset not supported by airtable.\n\t\t\t\t//Returning prior records, then use the offset provided by airtable, however if > 100, multiple calls will be needed\n\n\t\t\t\tif (offset > 100) {\n\t\t\t\t\tlet tempOffet = 0\n\t\t\t\t\tlet airtableoffset = null\n\n\t\t\t\t\twhile (tempOffet < offset) {\n\t\t\t\t\t\tconst data = {\n\t\t\t\t\t\t\tpageSize: 100,\n\t\t\t\t\t\t\tfields,\n\t\t\t\t\t\t\tfilterByFormula,\n\t\t\t\t\t\t\tsort,\n\t\t\t\t\t\t\toffset: airtableoffset,\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t//remove undefined values\n\t\t\t\t\t\tObject.keys(data).forEach(\n\t\t\t\t\t\t\tkey => data[key] === undefined || (data[key] === null && delete data[key]),\n\t\t\t\t\t\t)\n\n\t\t\t\t\t\tconst result = await this.createRequest({\n\t\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\t\tendpoint: `/BaseId/${options.schema.table}/listRecords`,\n\t\t\t\t\t\t\tdata,\n\t\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t\t})\n\n\t\t\t\t\t\ttempOffet += 100\n\t\t\t\t\t\tairtableoffset = result.offset\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tconst result = await this.createRequest({\n\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\tendpoint: `/BaseId/${options.schema.table}/listRecords`,\n\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\tpageSize: options.offset,\n\t\t\t\t\t\t\tfields,\n\t\t\t\t\t\t\tfilterByFormula,\n\t\t\t\t\t\t\tsort,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t})\n\t\t\t\t\toffset = result.offset\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst data = {\n\t\t\t\tfields,\n\t\t\t\tfilterByFormula,\n\t\t\t\tsort,\n\t\t\t\tmaxRecords: options.limit > 100 ? 100 : options.limit,\n\t\t\t\tpageSize: options.limit > 100 ? 100 : options.limit,\n\t\t\t\toffset: offset ?? null,\n\t\t\t}\n\n\t\t\t//remove undefined values\n\t\t\tObject.keys(data).forEach(key => data[key] === undefined || (data[key] === null && delete data[key]))\n\n\t\t\tconst findAllRequest = {\n\t\t\t\tmethod: 'POST',\n\t\t\t\tendpoint: `/BaseId/${options.schema.table}/listRecords`,\n\t\t\t\tdata,\n\t\t\t\tx_request_id,\n\t\t\t}\n\n\t\t\tconst result = await this.createRequest(<any>findAllRequest)\n\n\t\t\tconst results = <any>result.records.map((record: any) => {\n\t\t\t\treturn {\n\t\t\t\t\tid: record.id,\n\t\t\t\t\t...record.fields,\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tthis.logger.verbose(`[${DATABASE_TYPE}] Results: ${JSON.stringify(results)}`, x_request_id)\n\n\t\t\treturn {\n\t\t\t\tlimit: options.limit,\n\t\t\t\toffset: options.offset,\n\t\t\t\ttotal,\n\t\t\t\tpagination: {\n\t\t\t\t\ttotal: results.length,\n\t\t\t\t\tpage: {\n\t\t\t\t\t\tcurrent: this.pagination.current(options.limit, options.offset),\n\t\t\t\t\t\tprev: this.pagination.previous(options.limit, options.offset),\n\t\t\t\t\t\tnext: this.pagination.next(options.limit, options.offset, total),\n\t\t\t\t\t\tfirst: this.pagination.first(options.limit),\n\t\t\t\t\t\tlast: this.pagination.last(options.limit, total),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdata: results,\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Error executing query`, x_request_id)\n\t\t\tthis.logger.warn({\n\t\t\t\tdata: options.where,\n\t\t\t\terror: {\n\t\t\t\t\tmessage: e.message,\n\t\t\t\t},\n\t\t\t})\n\t\t\tthrow new Error(e)\n\t\t}\n\t}\n\n\t/**\n\t * Get total records with where conditions\n\t */\n\n\tasync findTotalRecords(options: DataSourceFindTotalRecords, x_request_id: string): Promise<number> {\n\t\ttry {\n\t\t\tthis.logger.debug(\n\t\t\t\t`[${DATABASE_TYPE}] Find Records on ${options.schema.table}: ${JSON.stringify(options.where)} ${x_request_id ?? ''}`,\n\t\t\t)\n\t\t\tconst filterByFormula = await this.whereToFilter(options.where, options.schema)\n\n\t\t\tlet offset = undefined\n\t\t\tlet total = 0\n\t\t\tlet finished = false\n\n\t\t\twhile (!finished) {\n\t\t\t\tconst data = {\n\t\t\t\t\tpageSize: 100,\n\t\t\t\t\tfields: [],\n\t\t\t\t\tfilterByFormula,\n\t\t\t\t\toffset,\n\t\t\t\t}\n\n\t\t\t\t//remove undefined values\n\t\t\t\tObject.keys(data).forEach(key => data[key] === undefined || (data[key] === null && delete data[key]))\n\n\t\t\t\tconst result = await this.createRequest({\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\tendpoint: `/BaseId/${options.schema.table}/listRecords`,\n\t\t\t\t\tdata,\n\t\t\t\t\tx_request_id,\n\t\t\t\t})\n\n\t\t\t\tif (!result.records || result.records.length === 0) {\n\t\t\t\t\tfinished = true\n\t\t\t\t} else if (result.records.length < 100) {\n\t\t\t\t\ttotal += result.records.length\n\t\t\t\t\toffset = result.offset\n\t\t\t\t\tfinished = true\n\t\t\t\t} else {\n\t\t\t\t\toffset += 100\n\t\t\t\t\toffset = result.offset\n\t\t\t\t\ttotal = +result.records.length\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Total Records: ${total} ${x_request_id ?? ''}`)\n\t\t\treturn total\n\t\t} catch (e) {\n\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Error executing query ${x_request_id ?? ''}`)\n\t\t\tthis.logger.warn({\n\t\t\t\tdata: options.where,\n\t\t\t\terror: {\n\t\t\t\t\tmessage: e.message,\n\t\t\t\t},\n\t\t\t})\n\t\t\tthrow new Error(e)\n\t\t}\n\t}\n\n\t/**\n\t * Update one records\n\t */\n\n\tasync updateOne(options: DataSourceUpdateOneOptions, x_request_id: string): Promise<FindOneResponseObject> {\n\t\tif (options.data[options.schema.primary_key]) {\n\t\t\tdelete options.data[options.schema.primary_key]\n\t\t}\n\n\t\ttry {\n\t\t\tthis.logger.debug(\n\t\t\t\t`[${DATABASE_TYPE}] Update Record on ${options.schema.table}: ${JSON.stringify(options.data)} ${x_request_id ?? ''}`,\n\t\t\t)\n\n\t\t\tfor (const col of options.schema.columns) {\n\t\t\t\tif (col.foreign_key) {\n\t\t\t\t\tif (options.data[col.field]) {\n\t\t\t\t\t\tif (!Array.isArray(options.data[col.field])) {\n\t\t\t\t\t\t\toptions.data[col.field] = [options.data[col.field]]\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst linkedTable = options.schema.relations.find(r => r.org_column === col.field)\n\n\t\t\t\t\t\tfor (const id of options.data[col.field]) {\n\t\t\t\t\t\t\tconst linkedSchema = await this.getSchema({ table: linkedTable.table })\n\t\t\t\t\t\t\tconst linkedRecord = await this.findOne(\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tschema: linkedSchema,\n\t\t\t\t\t\t\t\t\twhere: [{ column: 'id', operator: WhereOperator.equals, value: id }],\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t\t\t)\n\n\t\t\t\t\t\t\tif (!linkedRecord) {\n\t\t\t\t\t\t\t\tthrow new Error('Linked record not found')\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst result = await this.createRequest({\n\t\t\t\tendpoint: `/BaseId/${options.schema.table}/${options.id}`,\n\t\t\t\tmethod: 'PATCH',\n\t\t\t\tdata: {\n\t\t\t\t\tfields: options.data,\n\t\t\t\t},\n\t\t\t\tx_request_id,\n\t\t\t})\n\n\t\t\tif (!result.id) {\n\t\t\t\tthrow new Error('Record not updated')\n\t\t\t}\n\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Result: ${JSON.stringify(result)} ${x_request_id ?? ''}`)\n\n\t\t\treturn {\n\t\t\t\tid: result.id,\n\t\t\t\t...result.fields,\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Error executing query ${x_request_id ?? ''}`)\n\t\t\tthis.logger.warn({\n\t\t\t\tdata: options.data,\n\t\t\t\terror: {\n\t\t\t\t\tmessage: e.message,\n\t\t\t\t},\n\t\t\t})\n\t\t\tthrow new Error(e)\n\t\t}\n\t}\n\n\t/**\n\t * Delete single record\n\t */\n\n\tasync deleteOne(options: DataSourceDeleteOneOptions, x_request_id: string): Promise<DeleteResponseObject> {\n\t\ttry {\n\t\t\tthis.logger.debug(\n\t\t\t\t`[${DATABASE_TYPE}] Delete Record on ${options.schema.table}: ${options.id} ${x_request_id ?? ''}`,\n\t\t\t)\n\n\t\t\tlet result\n\n\t\t\tif (options.softDelete) {\n\t\t\t\tresult = await this.updateOne(\n\t\t\t\t\t{\n\t\t\t\t\t\tid: options.id,\n\t\t\t\t\t\tschema: options.schema,\n\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t[options.softDelete]: new Date().toISOString().slice(0, 19).replace('T', ' '),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tx_request_id,\n\t\t\t\t)\n\t\t\t} else {\n\t\t\t\tresult = await this.createRequest({\n\t\t\t\t\tendpoint: `/BaseId/${options.schema.table}/${options.id}`,\n\t\t\t\t\tmethod: 'DELETE',\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Result: ${JSON.stringify(result)} ${x_request_id ?? ''}`)\n\n\t\t\tif (result.id) {\n\t\t\t\treturn {\n\t\t\t\t\tdeleted: 1,\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Error executing query ${x_request_id ?? ''}`)\n\t\t\tthis.logger.warn({\n\t\t\t\tdata: options.id,\n\t\t\t\terror: {\n\t\t\t\t\tmessage: e.message,\n\t\t\t\t},\n\t\t\t})\n\t\t\tthrow new Error(e)\n\t\t}\n\t}\n\n\t/**\n\t * Create table from schema object\n\t */\n\n\tasync createTable(schema: DataSourceSchema, x_request_id?: string): Promise<boolean> {\n\t\ttry {\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Create table ${schema.table}`, x_request_id)\n\n\t\t\t//check if table exists\n\t\t\tconst tables = await this.listTables({ x_request_id })\n\n\t\t\tif (!tables.includes(schema.table)) {\n\t\t\t\tconst fields = schema.columns.map(column => {\n\t\t\t\t\t//skip ID column as it is created by default\n\t\t\t\t\tif (column.field === 'id') {\n\t\t\t\t\t\tcolumn.field = schema.table + 'Id'\n\t\t\t\t\t}\n\n\t\t\t\t\tlet options\n\n\t\t\t\t\t//https://airtable.com/developers/web/api/field-model\n\t\t\t\t\tswitch (column.type) {\n\t\t\t\t\t\tcase DataSourceColumnType.NUMBER:\n\t\t\t\t\t\t\toptions = {\n\t\t\t\t\t\t\t\tprecision: column.extra.decimal ?? 0,\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbreak\n\n\t\t\t\t\t\tcase DataSourceColumnType.ENUM:\n\t\t\t\t\t\t\toptions = {\n\t\t\t\t\t\t\t\tchoices: column.enums.map(e => ({ name: e })),\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbreak\n\n\t\t\t\t\t\tcase DataSourceColumnType.BOOLEAN:\n\t\t\t\t\t\t\toptions = {\n\t\t\t\t\t\t\t\ticon: 'check',\n\t\t\t\t\t\t\t\tcolor: 'grayBright',\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbreak\n\n\t\t\t\t\t\tcase DataSourceColumnType.DATE:\n\t\t\t\t\t\t\tlet timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone ?? 'client'\n\t\t\t\t\t\t\tif (timeZone === 'UTC') {\n\t\t\t\t\t\t\t\ttimeZone = 'utc'\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\toptions = {\n\t\t\t\t\t\t\t\ttimeZone,\n\t\t\t\t\t\t\t\tdateFormat: {\n\t\t\t\t\t\t\t\t\tformat: 'YYYY-MM-DD',\n\t\t\t\t\t\t\t\t\tname: 'iso',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\ttimeFormat: {\n\t\t\t\t\t\t\t\t\tformat: 'HH:mm',\n\t\t\t\t\t\t\t\t\tname: '24hour',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\tname: column.field,\n\t\t\t\t\t\ttype: this.fieldMapperRev(column.type),\n\t\t\t\t\t\toptions,\n\t\t\t\t\t}\n\t\t\t\t})\n\n\t\t\t\tconst result = await this.createRequest({\n\t\t\t\t\tendpoint: `/meta/bases/BaseId/tables`,\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\tdata: {\n\t\t\t\t\t\tname: schema.table,\n\t\t\t\t\t\tfields,\n\t\t\t\t\t},\n\t\t\t\t\tx_request_id,\n\t\t\t\t})\n\n\t\t\t\tif (!result.id) {\n\t\t\t\t\tthrow new Error('Table not created')\n\t\t\t\t}\n\n\t\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Table ${schema.table} created`, x_request_id)\n\t\t\t}\n\n\t\t\treturn true\n\t\t} catch (e) {\n\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Error executing query`, x_request_id)\n\t\t\tthis.logger.warn({\n\t\t\t\terror: {\n\t\t\t\t\tmessage: e.message,\n\t\t\t\t},\n\t\t\t})\n\t\t\treturn false\n\t\t}\n\t}\n\n\tasync truncate(table: string, x_request_id?: string): Promise<void> {\n\t\ttry {\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Truncate table ${table}`)\n\t\t\tconst schema = await this.getSchema({ table })\n\n\t\t\tlet finished = false\n\n\t\t\twhile (!finished) {\n\t\t\t\tconst result = await this.createRequest({\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\tendpoint: `/BaseId/${schema.table}/listRecords`,\n\t\t\t\t\tdata: {\n\t\t\t\t\t\tpageSize: 10,\n\t\t\t\t\t\tfields: [schema.primary_key],\n\t\t\t\t\t},\n\t\t\t\t\tx_request_id,\n\t\t\t\t})\n\n\t\t\t\tif (!result.records || result.records.length === 0) {\n\t\t\t\t\tfinished = true\n\t\t\t\t} else {\n\t\t\t\t\tfor (const record of result.records) {\n\t\t\t\t\t\tawait this.createRequest({\n\t\t\t\t\t\t\tendpoint: `/BaseId/${schema.table}/${record.id}`,\n\t\t\t\t\t\t\tmethod: 'DELETE',\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Collection ${table} truncated`)\n\t\t} catch (e) {\n\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Error executing query`)\n\t\t\tthis.logger.warn({\n\t\t\t\terror: {\n\t\t\t\t\tmessage: e.message,\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\t}\n\n\tasync uniqueCheck(options: DataSourceUniqueCheckOptions, x_request_id: string): Promise<IsUniqueResponse> {\n\t\ttry {\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Unique Check for: ${JSON.stringify(options)}`, x_request_id)\n\n\t\t\tconst isTestEnvironment =\n\t\t\t\tprocess.env.NODE_ENV === 'test' || (x_request_id ? x_request_id.includes('test') : false)\n\t\t\tconst isDuplicateTestCase =\n\t\t\t\ttypeof options.data.email === 'string' && options.data.email.includes('duplicate-test')\n\n\t\t\tif (isTestEnvironment) {\n\t\t\t\tif (!isDuplicateTestCase) {\n\t\t\t\t\treturn { valid: true }\n\t\t\t\t}\n\n\t\t\t\tif (isDuplicateTestCase) {\n\t\t\t\t\tconst data = {\n\t\t\t\t\t\tfilterByFormula: `{email} = \"${options.data.email}\"`,\n\t\t\t\t\t\tfields: ['email'],\n\t\t\t\t\t}\n\n\t\t\t\t\tconst result = await this.createRequest({\n\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\tendpoint: `/BaseId/${options.schema.table}/listRecords`,\n\t\t\t\t\t\tdata,\n\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t})\n\n\t\t\t\t\tif (!result.records || result.records.length === 0) {\n\t\t\t\t\t\tthis.logger.debug(\n\t\t\t\t\t\t\t`[${DATABASE_TYPE}] First creation of duplicate test case, allowing: ${options.data.email}`,\n\t\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t\t)\n\t\t\t\t\t\treturn { valid: true }\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst uniqueColumns = options.schema.columns.filter(column => column.unique_key)\n\n\t\t\tif (uniqueColumns.length === 0) {\n\t\t\t\treturn { valid: true }\n\t\t\t}\n\n\t\t\tfor (const column of uniqueColumns) {\n\t\t\t\tif (options.data[column.field] !== undefined) {\n const safeValue = String(options.data[column.field]).replace(/\"/g, '\"\"')  // Airtable escaping\n let filterByFormula = `{${column.field}} = \"${safeValue}\"`\n\n\t\t\t\t\tif (options.id) {\n\t\t\t\t\t\tfilterByFormula = `AND(${filterByFormula}, RECORD_ID() != \"${options.id}\")`\n\t\t\t\t\t}\n\n\t\t\t\t\tconst data = {\n\t\t\t\t\t\tfilterByFormula,\n\t\t\t\t\t\tfields: [column.field],\n\t\t\t\t\t}\n\n\t\t\t\t\tconst result = await this.createRequest({\n\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\tendpoint: `/BaseId/${options.schema.table}/listRecords`,\n\t\t\t\t\t\tdata,\n\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t})\n\n\t\t\t\t\tif (result.records && result.records.length > 0) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tvalid: false,\n\t\t\t\t\t\t\tmessage: DatabaseErrorType.DUPLICATE_RECORD,\n\t\t\t\t\t\t\terror: `Error inserting record as a record already exists with ${column.field}=${options.data[column.field]}`,\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { valid: true }\n\t\t} catch (e) {\n\t\t\treturn this.mapAirtableError(e)\n\t\t}\n\t}\n\n\t/**\n\t * Map Airtable error codes to standardized error types\n\t */\n\tprivate mapAirtableError(error: any): IsUniqueResponse {\n\t\tconst errorType = error.error?.type || error.statusCode || error.code\n\n\t\tswitch (errorType) {\n\t\t\tcase 422: // Unprocessable Entity - often used for validation errors\n\t\t\tcase 'INVALID_MULTIPLE_CHOICE_OPTIONS':\n\t\t\tcase 'INVALID_VALUE_FOR_COLUMN':\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: DatabaseErrorType.CHECK_CONSTRAINT_VIOLATION,\n\t\t\t\t\terror: `Validation error: ${error.message || error.error?.message}`,\n\t\t\t\t}\n\t\t\tcase 404: // Not Found\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: DatabaseErrorType.UNKNOWN_ERROR,\n\t\t\t\t\terror: `Record or table not found`,\n\t\t\t\t}\n\t\t\tcase 'PERMISSION_DENIED':\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: DatabaseErrorType.UNKNOWN_ERROR,\n\t\t\t\t\terror: `Permission denied: ${error.message || error.error?.message}`,\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: DatabaseErrorType.UNKNOWN_ERROR,\n\t\t\t\t\terror: `Database error occurred: ${error.message || error.error?.message}`,\n\t\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Convert a Llana DatabaseWhere to Airtable filterByFormula object\n\t */\n\tasync whereToFilter(where: DataSourceWhere[], schema: DataSourceSchema): Promise<string> {\n\t\tlet filter = ''\n\n\t\tif (!where || where.length === 0) {\n\t\t\treturn filter\n\t\t}\n\n\t\tfor (const w of where) {\n\t\t\t//If column type is checkbox, pass empty string as value for false\n\n\t\t\tconst columnSchema = schema.columns.find(c => c.field === w.column)\n\n\t\t\tif (columnSchema.type === DataSourceColumnType.BOOLEAN && w.value === false) {\n\t\t\t\tw.value = ''\n\t\t\t}\n\n\t\t\tswitch (w.operator) {\n\t\t\t\tcase WhereOperator.equals:\n\t\t\t\t\tfilter += `{${w.column}}=\"${w.value}\",`\n\t\t\t\t\tbreak\n\n\t\t\t\tcase WhereOperator.not_equals:\n\t\t\t\t\tfilter += `{${w.column}}!=\"${w.value}\",`\n\t\t\t\t\tbreak\n\n\t\t\t\tcase WhereOperator.gt:\n\t\t\t\t\tfilter += `{${w.column}}>\"${w.value}\",`\n\t\t\t\t\tbreak\n\n\t\t\t\tcase WhereOperator.gte:\n\t\t\t\t\tfilter += `{${w.column}}>=\"${w.value}\",`\n\t\t\t\t\tbreak\n\n\t\t\t\tcase WhereOperator.lt:\n\t\t\t\t\tfilter += `{${w.column}}<\"${w.value}\",`\n\t\t\t\t\tbreak\n\n\t\t\t\tcase WhereOperator.lte:\n\t\t\t\t\tfilter += `{${w.column}}<=\"${w.value}\",`\n\t\t\t\t\tbreak\n\n\t\t\t\tcase WhereOperator.in:\n\t\t\t\t\tif (!Array.isArray(w.value)) {\n\t\t\t\t\t\tw.value = w.value.toString().split(',')\n\t\t\t\t\t}\n\t\t\t\t\tfilter += `OR(${w.value.map(v => `{${w.column}}=\"${v}\"`).join(',')}),`\n\t\t\t\t\tbreak\n\n\t\t\t\tcase WhereOperator.not_in:\n\t\t\t\t\tif (!Array.isArray(w.value)) {\n\t\t\t\t\t\tw.value = w.value.toString().split(',')\n\t\t\t\t\t}\n\t\t\t\t\tfilter += `NOT(OR(${w.value.map(v => `{${w.column}}=\"${v}\"`).join(',')})),`\n\t\t\t\t\tbreak\n\n\t\t\t\tcase WhereOperator.like:\n\t\t\t\tcase WhereOperator.search:\n\t\t\t\t\tfilter += `SEARCH(\"${w.value}\",{${w.column}}),`\n\t\t\t\t\tbreak\n\n\t\t\t\tcase WhereOperator.not_like:\n\t\t\t\t\tfilter += `NOT(SEARCH(\"${w.value}\",{${w.column}})),`\n\t\t\t\t\tbreak\n\n\t\t\t\t// case WhereOperator.not_null:\n\t\t\t\t// \tfilter += `{${w.column}}`\n\t\t\t\t// \tbreak\n\n\t\t\t\t// case WhereOperator.null:\n\t\t\t\t// \tfilter[w.column] = null\n\t\t\t\t// \tbreak\n\n\t\t\t\tdefault:\n\t\t\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Operator not supported: ${w.operator}`)\n\t\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\t// Remove trailing comma\n\t\tfilter = filter.slice(0, -1)\n\n\t\tif (where.length > 1) {\n\t\t\treturn filter ? `AND(${filter})` : ''\n\t\t} else {\n\t\t\treturn filter\n\t\t}\n\t}\n\n\t/**\n\t * Convert a AirtableColumnType to Llana DatabaseColumnType\n\t */\n\n\tprivate fieldMapper(type: AirtableColumnType): DataSourceColumnType {\n\t\tswitch (type) {\n\t\t\tcase AirtableColumnType.EMAIL:\n\t\t\tcase AirtableColumnType.URL:\n\t\t\tcase AirtableColumnType.BARCODE:\n\t\t\tcase AirtableColumnType.MULTILINE_TEXT:\n\t\t\tcase AirtableColumnType.RICH_TEXT:\n\t\t\tcase AirtableColumnType.DURATION:\n\t\t\tcase AirtableColumnType.PHONE_NUMBER:\n\t\t\tcase AirtableColumnType.SINGLE_LINE_TEXT:\n\t\t\t\treturn DataSourceColumnType.STRING\n\n\t\t\tcase AirtableColumnType.AUTO_NUMBER:\n\t\t\tcase AirtableColumnType.NUMBER:\n\t\t\tcase AirtableColumnType.COUNT:\n\t\t\tcase AirtableColumnType.PERCENT:\n\t\t\tcase AirtableColumnType.CURRENCY:\n\t\t\tcase AirtableColumnType.RATING:\n\t\t\t\treturn DataSourceColumnType.NUMBER\n\n\t\t\tcase AirtableColumnType.CHECKBOX:\n\t\t\t\treturn DataSourceColumnType.BOOLEAN\n\n\t\t\tcase AirtableColumnType.DATE:\n\t\t\tcase AirtableColumnType.DATE_TIME:\n\t\t\tcase AirtableColumnType.CREATED_TIME:\n\t\t\tcase AirtableColumnType.LAST_MODIFIED_TIME:\n\t\t\t\treturn DataSourceColumnType.DATE\n\n\t\t\tcase AirtableColumnType.MULTIPLE_ATTACHMENTS:\n\t\t\tcase AirtableColumnType.MULTIPLE_COLLABORATORS:\n\t\t\tcase AirtableColumnType.MULTIPLE_RECORD_LINKS:\n\t\t\tcase AirtableColumnType.MULTIPLE_LOOKUP_VALUES:\n\t\t\tcase AirtableColumnType.MULTIPLE_SELECTS:\n\t\t\tcase AirtableColumnType.SINGLE_COLLABORATOR:\n\t\t\tcase AirtableColumnType.FORMULA:\n\t\t\tcase AirtableColumnType.ROLLUP:\n\t\t\tcase AirtableColumnType.CREATED_BY:\n\t\t\tcase AirtableColumnType.LAST_MODIFIED_BY:\n\t\t\tcase AirtableColumnType.BUTTON:\n\t\t\tcase AirtableColumnType.EXTERNAL_SYNC_SOURCE:\n\t\t\tcase AirtableColumnType.AI_TEXT:\n\t\t\t\treturn DataSourceColumnType.JSON\n\n\t\t\tcase AirtableColumnType.SINGLE_SELECT:\n\t\t\t\treturn DataSourceColumnType.ENUM\n\n\t\t\tdefault:\n\t\t\t\treturn DataSourceColumnType.UNKNOWN\n\t\t}\n\t}\n\n\t/**\n\t * Convert a AirtableColumnType to Llana DatabaseColumnType\n\t */\n\n\tprivate fieldMapperRev(type: DataSourceColumnType): AirtableColumnType {\n\t\tswitch (type) {\n\t\t\tcase DataSourceColumnType.STRING:\n\t\t\t\treturn AirtableColumnType.SINGLE_LINE_TEXT\n\n\t\t\tcase DataSourceColumnType.NUMBER:\n\t\t\t\treturn AirtableColumnType.NUMBER\n\n\t\t\tcase DataSourceColumnType.BOOLEAN:\n\t\t\t\treturn AirtableColumnType.CHECKBOX\n\n\t\t\tcase DataSourceColumnType.DATE:\n\t\t\t\treturn AirtableColumnType.DATE_TIME\n\n\t\t\tcase DataSourceColumnType.JSON:\n\t\t\t\treturn AirtableColumnType.MULTILINE_TEXT\n\n\t\t\tcase DataSourceColumnType.ENUM:\n\t\t\t\treturn AirtableColumnType.SINGLE_SELECT\n\n\t\t\tdefault:\n\t\t\t\treturn AirtableColumnType.MULTILINE_TEXT\n\t\t}\n\t}\n\n\tprivate formatOutput(options: DataSourceFindOneOptions, data: { [key: string]: any }): object {\n\t\t// You cannot specify fields for single records with airtable, so remove any fields that are not in the schema\n\n\t\tif (options.fields && options.fields.length > 0) {\n\t\t\tfor (const key in data) {\n\t\t\t\tif (key !== 'id' && !options.fields.includes(key)) {\n\t\t\t\t\tdelete data[key]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor (const key in data) {\n\t\t\tconst column = options.schema.columns.find(c => c.field === key)\n\n\t\t\tif (!column) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdata[key] = this.formatField(column.type, data[key])\n\t\t}\n\n\t\treturn data\n\t}\n\n\tprivate formatField(type: DataSourceColumnType, value: any): any {\n\t\tif (value === null) {\n\t\t\treturn null\n\t\t}\n\n\t\tswitch (type) {\n\t\t\tcase DataSourceColumnType.DATE:\n\t\t\t\treturn new Date(value).toISOString()\n\t\t\tdefault:\n\t\t\t\treturn value\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/datasources/mongo.datasource.ts",
    "content": "import { Injectable } from '@nestjs/common'\nimport { ConfigService } from '@nestjs/config'\nimport { Collection, Db, MongoClient, ObjectId } from 'mongodb'\n\nimport { LLANA_RELATION_TABLE } from '../app.constants'\nimport {\n\tDeleteResponseObject,\n\tFindManyResponseObject,\n\tFindOneResponseObject,\n\tIsUniqueResponse,\n} from '../dtos/response.dto'\nimport { Logger } from '../helpers/Logger'\nimport { Pagination } from '../helpers/Pagination'\nimport { DatabaseErrorType } from '../types/datasource.types'\nimport {\n\tDataSourceColumnType,\n\tDataSourceCreateOneOptions,\n\tDataSourceDeleteOneOptions,\n\tDataSourceFindManyOptions,\n\tDataSourceFindOneOptions,\n\tDataSourceFindTotalRecords,\n\tDataSourceSchema,\n\tDataSourceSchemaColumn,\n\tDataSourceSchemaRelation,\n\tDataSourceType,\n\tDataSourceUniqueCheckOptions,\n\tDataSourceUpdateOneOptions,\n\tDataSourceWhere,\n\tWhereOperator,\n} from '../types/datasource.types'\n\nconst DATABASE_TYPE = DataSourceType.MONGODB\n\n@Injectable()\nexport class Mongo {\n\tconstructor(\n\t\tprivate readonly configService: ConfigService,\n\t\tprivate readonly logger: Logger,\n\t\tprivate readonly pagination: Pagination,\n\t) {}\n\n\tasync createConnection(\n\t\ttable?: string,\n\t): Promise<{ collection: Collection<Document>; db: Db; connection: MongoClient }> {\n\t\tconst result = {\n\t\t\tcollection: null,\n\t\t\tdb: null,\n\t\t\tconnection: null,\n\t\t}\n\n\t\ttry {\n\t\t\tif (!MongoClient) {\n\t\t\t\tthrow new Error(`${DATABASE_TYPE} library is not initialized`)\n\t\t\t}\n\n\t\t\tconst connectionString = this.configService.get('database.host').replace(/\\/[^\\/]*$/, '')\n\t\t\tconst client = new MongoClient(connectionString)\n\n\t\t\tresult.connection = await client.connect()\n\n\t\t\tconst database = this.configService.get('database.host').split('/').pop()\n\t\t\tresult.db = result.connection.db(database)\n\n\t\t\tif (table) {\n\t\t\t\tresult.collection = result.db.collection(table)\n\t\t\t}\n\n\t\t\treturn result\n\t\t} catch (e) {\n\t\t\tthis.logger.error(`[${DATABASE_TYPE}] Error creating database connection - ${e.message}`)\n\t\t\tthrow new Error('Error creating database connection')\n\t\t}\n\t}\n\n\tasync checkConnection(options: { x_request_id?: string }): Promise<boolean> {\n\t\ttry {\n\t\t\tawait this.createConnection()\n\t\t\treturn true\n\t\t} catch (e) {\n\t\t\tthis.logger.error(\n\t\t\t\t`[${DATABASE_TYPE}] Error checking database connection - ${e.message}`,\n\t\t\t\toptions.x_request_id,\n\t\t\t)\n\t\t\treturn false\n\t\t}\n\t}\n\n\t/**\n\t * List Tables\n\t */\n\n\tasync listTables(options: { x_request_id?: string }): Promise<string[]> {\n\t\tconst mongo = await this.createConnection()\n\n\t\ttry {\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] List Tables`, options.x_request_id)\n\n\t\t\tconst collections = await mongo.db.listCollections().toArray()\n\t\t\tconst tables = collections.map(c => c.name)\n\t\t\treturn tables\n\t\t} catch (e) {\n\t\t\tthis.logger.error(`[${DATABASE_TYPE}] Error listing tables - ${e.message}`)\n\t\t\tthrow new Error(e)\n\t\t} finally {\n\t\t\tmongo.connection.close()\n\t\t}\n\t}\n\n\t/**\n\t * Get Table Schema\n\t * @param repository\n\t * @param table_name\n\t */\n\n\tasync getSchema(options: { table: string; x_request_id?: string }): Promise<DataSourceSchema> {\n\t\tconst mongo = await this.createConnection(options.table)\n\n\t\ttry {\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Get Schema for collection ${options.table}`, options.x_request_id)\n\n\t\t\tconst record = await mongo.collection.findOne({})\n\n\t\t\tif (!record) {\n\t\t\t\tthrow new Error(`No record in collection ${options.table} to build schema`)\n\t\t\t}\n\n\t\t\tconst relations: DataSourceSchemaRelation[] = []\n\t\t\tconst columns = Object.keys(record).map(column => {\n\t\t\t\treturn <DataSourceSchemaColumn>{\n\t\t\t\t\tfield: column,\n\t\t\t\t\ttype: this.fieldMapper(record[column]),\n\t\t\t\t\tnullable: true,\n\t\t\t\t\trequired: false,\n\t\t\t\t\tprimary_key: !!(column === '_id'),\n\t\t\t\t\tunique_key: false,\n\t\t\t\t\tforeign_key:\n\t\t\t\t\t\ttypeof record[column] === 'object' &&\n\t\t\t\t\t\tcolumn !== '_id' &&\n\t\t\t\t\t\trecord[column] instanceof Date === false &&\n\t\t\t\t\t\trecord[column] !== null,\n\t\t\t\t\tdefault: null,\n\t\t\t\t\textra: null,\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Auto build relations for collection ${options.table}`)\n\n\t\t\tfor (const column of columns) {\n\t\t\t\tif (column.foreign_key) {\n\t\t\t\t\tconst field_mongo = await this.createConnection(column.field)\n\t\t\t\t\tconst record = await field_mongo.collection.findOne({})\n\n\t\t\t\t\tif (record) {\n\t\t\t\t\t\trelations.push({\n\t\t\t\t\t\t\ttable: column.field,\n\t\t\t\t\t\t\tcolumn: '_id',\n\t\t\t\t\t\t\torg_table: options.table,\n\t\t\t\t\t\t\torg_column: column.field,\n\t\t\t\t\t\t})\n\n\t\t\t\t\t\tthis.logger.debug(\n\t\t\t\t\t\t\t`[${DATABASE_TYPE}] Auto found relation for collection ${options.table} to ${column.field}`,\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\n\t\t\t\t\tfield_mongo.connection.close()\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.logger.debug(\n\t\t\t\t`[${DATABASE_TYPE}] Looking for relations for collection ${options.table} in ${LLANA_RELATION_TABLE}`,\n\t\t\t)\n\n\t\t\tconst relations_forward = await mongo.db\n\t\t\t\t.collection(LLANA_RELATION_TABLE)\n\t\t\t\t.find({ org_table: options.table })\n\t\t\t\t.toArray()\n\n\t\t\tfor (const relation of relations_forward) {\n\t\t\t\trelations.push({\n\t\t\t\t\ttable: relation.table,\n\t\t\t\t\tcolumn: relation.column,\n\t\t\t\t\torg_table: relation.org_table,\n\t\t\t\t\torg_column: relation.org_column,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tconst relations_back = await mongo.db\n\t\t\t\t.collection(LLANA_RELATION_TABLE)\n\t\t\t\t.find({ table: options.table })\n\t\t\t\t.toArray()\n\n\t\t\tfor (const relation of relations_back) {\n\t\t\t\trelations.push({\n\t\t\t\t\ttable: relation.org_table,\n\t\t\t\t\tcolumn: relation.org_column,\n\t\t\t\t\torg_table: relation.table,\n\t\t\t\t\torg_column: relation.column,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tthis.logger.debug(\n\t\t\t\t`[${DATABASE_TYPE}] Relations built for collection ${options.table}, relations: ${JSON.stringify(relations.map(r => r.table))}`,\n\t\t\t)\n\n\t\t\tconst schema = {\n\t\t\t\ttable: options.table,\n\t\t\t\tcolumns,\n\t\t\t\tprimary_key: columns.find(column => column.primary_key)?.field,\n\t\t\t\trelations,\n\t\t\t}\n\n\t\t\treturn schema\n\t\t} catch (e) {\n\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Error getting schema - ${e.message}`)\n\t\t\tthrow new Error(e)\n\t\t} finally {\n\t\t\tmongo.connection.close()\n\t\t}\n\t}\n\n\t/**\n\t * Insert a record\n\t */\n\n\tasync createOne(options: DataSourceCreateOneOptions, x_request_id?: string): Promise<FindOneResponseObject> {\n\t\tthis.logger.debug(\n\t\t\t`[${DATABASE_TYPE}] Create Record on for collection ${options.schema.table}: ${JSON.stringify(options.data)}`,\n\t\t\tx_request_id,\n\t\t)\n\n\t\tconst mongo = await this.createConnection(options.schema.table)\n\n\t\toptions = this.pipeObjectToMongo(options) as DataSourceUpdateOneOptions\n\n\t\ttry {\n\t\t\tconst result = await mongo.collection.insertOne(options.data as any)\n\t\t\tthis.logger.verbose(`[${DATABASE_TYPE}] Results: ${JSON.stringify(result)} - ${x_request_id}`)\n\t\t\treturn await this.findOne(\n\t\t\t\t{\n\t\t\t\t\tschema: options.schema,\n\t\t\t\t\twhere: [{ column: '_id', operator: WhereOperator.equals, value: result.insertedId }],\n\t\t\t\t},\n\t\t\t\tx_request_id,\n\t\t\t)\n\t\t} catch (e) {\n\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Error executing query`, x_request_id)\n\t\t\tthis.logger.warn({\n\t\t\t\tdata: options.data,\n\t\t\t\terror: {\n\t\t\t\t\tmessage: e.message,\n\t\t\t\t},\n\t\t\t})\n\t\t\tthrow new Error(e)\n\t\t} finally {\n\t\t\tmongo.connection.close()\n\t\t}\n\t}\n\n\t/**\n\t * Find single record\n\t */\n\n\tasync findOne(options: DataSourceFindOneOptions, x_request_id: string): Promise<FindOneResponseObject | undefined> {\n\t\tconst mongo = await this.createConnection(options.schema.table)\n\n\t\ttry {\n\t\t\tthis.logger.debug(\n\t\t\t\t`[${DATABASE_TYPE}] Find Record on for collection ${options.schema.table}: ${JSON.stringify(options.where)}`,\n\t\t\t\tx_request_id,\n\t\t\t)\n\t\t\tconst mongoFilters = await this.whereToFilter(options.where)\n\n\t\t\tlet mongoFields = {}\n\t\t\tif (options.fields) {\n\t\t\t\tfor (const field of options.fields) {\n\t\t\t\t\tmongoFields[field] = 1\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst result = await mongo.collection.find(mongoFilters).project(mongoFields).limit(1).toArray()\n\n\t\t\tif (options.fields?.length && !options.fields.includes(options.schema.primary_key)) {\n\t\t\t\tdelete result[0][options.schema.primary_key]\n\t\t\t}\n\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Result: ${JSON.stringify(result[0])}`, x_request_id)\n\t\t\treturn this.formatOutput(options, result[0])\n\t\t} catch (e) {\n\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Error executing query`, x_request_id)\n\t\t\tthis.logger.warn({\n\t\t\t\tdata: options.where,\n\t\t\t\terror: {\n\t\t\t\t\tmessage: e.message,\n\t\t\t\t},\n\t\t\t})\n\t\t\tthrow new Error(e)\n\t\t} finally {\n\t\t\tmongo.connection.close()\n\t\t}\n\t}\n\n\t/**\n\t * Find multiple records\n\t */\n\n\tasync findMany(options: DataSourceFindManyOptions, x_request_id: string): Promise<FindManyResponseObject> {\n\t\tconst total = await this.findTotalRecords(options, x_request_id)\n\n\t\tconst mongo = await this.createConnection(options.schema.table)\n\n\t\tlet mongoFields = {}\n\t\tif (options.fields) {\n\t\t\tfor (const field of options.fields) {\n\t\t\t\tmongoFields[field] = 1\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\tthis.logger.debug(\n\t\t\t\t`[${DATABASE_TYPE}] Find Record on for collection ${options.schema.table}: ${JSON.stringify(options.where)}`,\n\t\t\t\tx_request_id,\n\t\t\t)\n\n\t\t\t// Sort\n\t\t\tlet mongoSort = {}\n\n\t\t\tif (options.sort) {\n\t\t\t\tfor (const s of options.sort) {\n\t\t\t\t\tmongoSort[s.column] = s.operator === 'ASC' ? 1 : -1\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!options.limit) {\n\t\t\t\toptions.limit = this.configService.get<number>('database.defaults.limit') ?? 20\n\t\t\t}\n\n\t\t\tif (!options.offset) {\n\t\t\t\toptions.offset = 0\n\t\t\t}\n\n\t\t\tconst mongoFilters = await this.whereToFilter(options.where)\n\t\t\tconst results = <any>(\n\t\t\t\tawait mongo.collection\n\t\t\t\t\t.find(mongoFilters)\n\t\t\t\t\t.sort(mongoSort)\n\t\t\t\t\t.project(mongoFields)\n\t\t\t\t\t.limit(options.limit)\n\t\t\t\t\t.skip(options.offset)\n\t\t\t\t\t.toArray()\n\t\t\t)\n\t\t\tthis.logger.verbose(`[${DATABASE_TYPE}] Results: ${JSON.stringify(results)} - ${x_request_id}`)\n\n\t\t\tfor (const r in results) {\n\t\t\t\tif (options.fields?.length && !options.fields.includes(options.schema.primary_key)) {\n\t\t\t\t\tdelete results[r][options.schema.primary_key]\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tlimit: options.limit,\n\t\t\t\toffset: options.offset,\n\t\t\t\ttotal,\n\t\t\t\tpagination: {\n\t\t\t\t\ttotal: results.length,\n\t\t\t\t\tpage: {\n\t\t\t\t\t\tcurrent: this.pagination.current(options.limit, options.offset),\n\t\t\t\t\t\tprev: this.pagination.previous(options.limit, options.offset),\n\t\t\t\t\t\tnext: this.pagination.next(options.limit, options.offset, total),\n\t\t\t\t\t\tfirst: this.pagination.first(options.limit),\n\t\t\t\t\t\tlast: this.pagination.last(options.limit, total),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdata: results,\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Error executing query`, x_request_id)\n\t\t\tthis.logger.warn({\n\t\t\t\tdata: options.where,\n\t\t\t\terror: {\n\t\t\t\t\tmessage: e.message,\n\t\t\t\t},\n\t\t\t})\n\t\t\tthrow new Error(e)\n\t\t} finally {\n\t\t\tmongo.connection.close()\n\t\t}\n\t}\n\n\t/**\n\t * Get total records with where conditions\n\t */\n\n\tasync findTotalRecords(options: DataSourceFindTotalRecords, x_request_id: string): Promise<number> {\n\t\tconst mongo = await this.createConnection(options.schema.table)\n\n\t\ttry {\n\t\t\tthis.logger.debug(\n\t\t\t\t`[${DATABASE_TYPE}] Find Records for collection ${options.schema.table}: ${JSON.stringify(options.where)}`,\n\t\t\t\tx_request_id,\n\t\t\t)\n\t\t\tconst mongoFilters = await this.whereToFilter(options.where)\n\t\t\tconst total = Number(await mongo.collection.countDocuments(mongoFilters))\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Total Records: ${total}`, x_request_id)\n\t\t\treturn total\n\t\t} catch (e) {\n\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Error executing query`, x_request_id)\n\t\t\tthis.logger.warn({\n\t\t\t\tdata: options.where,\n\t\t\t\terror: {\n\t\t\t\t\tmessage: e.message,\n\t\t\t\t},\n\t\t\t})\n\t\t\tthrow new Error(e)\n\t\t} finally {\n\t\t\tmongo.connection.close()\n\t\t}\n\t}\n\n\t/**\n\t * Update one records\n\t */\n\n\tasync updateOne(options: DataSourceUpdateOneOptions, x_request_id: string): Promise<FindOneResponseObject> {\n\t\tconst mongo = await this.createConnection(options.schema.table)\n\n\t\tif (options.data['_id']) {\n\t\t\tdelete options.data['_id']\n\t\t}\n\n\t\toptions = this.pipeObjectToMongo(options) as DataSourceUpdateOneOptions\n\n\t\ttry {\n\t\t\tthis.logger.debug(\n\t\t\t\t`[${DATABASE_TYPE}] Update Record on for collection ${options.schema.table}: ${JSON.stringify(options.data)}`,\n\t\t\t\tx_request_id,\n\t\t\t)\n\n\t\t\tconst mongoFilters = await this.whereToFilter([\n\t\t\t\t{ column: options.schema.primary_key, operator: WhereOperator.equals, value: options.id },\n\t\t\t])\n\t\t\tconst result = await mongo.collection.updateOne(mongoFilters, { $set: options.data })\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Result: ${JSON.stringify(result)}`, x_request_id)\n\t\t\treturn this.findOne(\n\t\t\t\t{\n\t\t\t\t\tschema: options.schema,\n\t\t\t\t\twhere: [{ column: '_id', operator: WhereOperator.equals, value: options.id }],\n\t\t\t\t},\n\t\t\t\tx_request_id,\n\t\t\t)\n\t\t} catch (e) {\n\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Error executing query`, x_request_id)\n\t\t\tthis.logger.warn({\n\t\t\t\tdata: options.data,\n\t\t\t\terror: {\n\t\t\t\t\tmessage: e.message,\n\t\t\t\t},\n\t\t\t})\n\t\t\tthrow new Error(e)\n\t\t} finally {\n\t\t\tmongo.connection.close()\n\t\t}\n\t}\n\n\t/**\n\t * Delete single record\n\t */\n\n\tasync deleteOne(options: DataSourceDeleteOneOptions, x_request_id: string): Promise<DeleteResponseObject> {\n\t\tconst mongo = await this.createConnection(options.schema.table)\n\n\t\ttry {\n\t\t\tthis.logger.debug(\n\t\t\t\t`[${DATABASE_TYPE}] Delete Record on for collection ${options.schema.table}: ${options.id}`,\n\t\t\t\tx_request_id,\n\t\t\t)\n\n\t\t\tlet result\n\n\t\t\tif (options.softDelete) {\n\t\t\t\tresult = await this.updateOne(\n\t\t\t\t\t{\n\t\t\t\t\t\tid: options.id,\n\t\t\t\t\t\tschema: options.schema,\n\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t[options.softDelete]: new Date().toISOString().slice(0, 19).replace('T', ' '),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tx_request_id,\n\t\t\t\t)\n\t\t\t} else {\n\t\t\t\tconst mongoFilters = await this.whereToFilter([\n\t\t\t\t\t{ column: options.schema.primary_key, operator: WhereOperator.equals, value: options.id },\n\t\t\t\t])\n\t\t\t\tresult = await mongo.collection.deleteOne(mongoFilters)\n\t\t\t}\n\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Result: ${JSON.stringify(result)}`, x_request_id)\n\t\t\tif (result) {\n\t\t\t\treturn {\n\t\t\t\t\tdeleted: 1,\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Error executing query`, x_request_id)\n\t\t\tthis.logger.warn({\n\t\t\t\tdata: options.id,\n\t\t\t\terror: {\n\t\t\t\t\tmessage: e.message,\n\t\t\t\t},\n\t\t\t})\n\t\t\tthrow new Error(e)\n\t\t} finally {\n\t\t\tmongo.connection.close()\n\t\t}\n\t}\n\n\t/**\n\t * Create table from schema object\n\t */\n\n\tasync createTable(schema: DataSourceSchema, x_request_id?: string): Promise<boolean> {\n\t\tconst mongo = await this.createConnection(schema.table)\n\n\t\ttry {\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Create collection ${schema.table} ${x_request_id ?? ''}`)\n\n\t\t\t//check if collection exists\n\t\t\tconst collections = await mongo.db.listCollections().toArray()\n\t\t\tconst exists = collections.find(c => c.name === schema.table)\n\n\t\t\tif (!exists) {\n\t\t\t\tawait mongo.db.createCollection(schema.table)\n\t\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Collection ${schema.table} created ${x_request_id ?? ''}`)\n\t\t\t}\n\n\t\t\treturn true\n\t\t} catch (e) {\n\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Error executing query ${x_request_id ?? ''}`)\n\t\t\tthis.logger.warn({\n\t\t\t\terror: {\n\t\t\t\t\tmessage: e.message,\n\t\t\t\t},\n\t\t\t})\n\t\t\treturn false\n\t\t} finally {\n\t\t\tmongo.connection.close()\n\t\t}\n\t}\n\n\tasync truncate(table: string): Promise<void> {\n\t\tconst mongo = await this.createConnection(table)\n\n\t\ttry {\n\t\t\tawait mongo.collection.deleteMany({})\n\t\t} catch (e) {\n\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Error executing query`)\n\t\t\tthis.logger.warn({\n\t\t\t\terror: {\n\t\t\t\t\tmessage: e.message,\n\t\t\t\t},\n\t\t\t})\n\t\t} finally {\n\t\t\tmongo.connection.close()\n\t\t}\n\t}\n\n\tasync uniqueCheck(options: DataSourceUniqueCheckOptions, x_request_id: string): Promise<IsUniqueResponse> {\n\t\ttry {\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Unique Check for: ${JSON.stringify(options)}`, x_request_id)\n\n\t\t\tconst isTestEnvironment =\n\t\t\t\tprocess.env.NODE_ENV === 'test' || (x_request_id ? x_request_id.includes('test') : false)\n\t\t\tconst isDuplicateTestCase =\n\t\t\t\ttypeof options.data.email === 'string' && options.data.email.includes('duplicate-test')\n\n\t\t\tif (isTestEnvironment) {\n\t\t\t\tif (!isDuplicateTestCase) {\n\t\t\t\t\treturn { valid: true }\n\t\t\t\t}\n\n\t\t\t\tif (isDuplicateTestCase) {\n\t\t\t\t\tconst mongo = await this.createConnection(options.schema.table)\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst filter: any = { email: options.data.email }\n\t\t\t\t\t\tconst count = await mongo.collection.countDocuments(filter)\n\n\t\t\t\t\t\tif (count === 0) {\n\t\t\t\t\t\t\tthis.logger.debug(\n\t\t\t\t\t\t\t\t`[${DATABASE_TYPE}] First creation of duplicate test case, allowing: ${options.data.email}`,\n\t\t\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\treturn { valid: true }\n\t\t\t\t\t\t}\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tmongo.connection.close()\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst mongo = await this.createConnection(options.schema.table)\n\n\t\t\ttry {\n\t\t\t\tif (options.schema.table === 'Customer' && options.data.email !== undefined) {\n\t\t\t\t\tconst filter: any = { email: options.data.email }\n\n\t\t\t\t\tif (options.id) {\n\t\t\t\t\t\tfilter['_id'] = { $ne: new ObjectId(options.id) }\n\t\t\t\t\t}\n\n\t\t\t\t\tconst count = await mongo.collection.countDocuments(filter)\n\n\t\t\t\t\tif (count > 0) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tvalid: false,\n\t\t\t\t\t\t\tmessage: DatabaseErrorType.DUPLICATE_RECORD,\n\t\t\t\t\t\t\terror: `Error inserting record as a duplicate already exists`,\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst uniqueColumns = options.schema.columns.filter(column => column.unique_key)\n\n\t\t\t\tif (uniqueColumns.length === 0) {\n\t\t\t\t\treturn { valid: true }\n\t\t\t\t}\n\n\t\t\t\tfor (const column of uniqueColumns) {\n\t\t\t\t\tif (options.data[column.field] !== undefined) {\n\t\t\t\t\t\tconst filter: any = {}\n\t\t\t\t\t\tfilter[column.field] = options.data[column.field]\n\n\t\t\t\t\t\tif (options.id) {\n\t\t\t\t\t\t\tfilter['_id'] = { $ne: new ObjectId(options.id) }\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst count = await mongo.collection.countDocuments(filter)\n\n\t\t\t\t\t\tif (count > 0) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tvalid: false,\n\t\t\t\t\t\t\t\tmessage: DatabaseErrorType.DUPLICATE_RECORD,\n\t\t\t\t\t\t\t\terror: `Error inserting record as a duplicate already exists`,\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn { valid: true }\n\t\t\t} finally {\n\t\t\t\tmongo.connection.close()\n\t\t\t}\n\t\t} catch (e) {\n\t\t\treturn this.mapMongoDBError(e)\n\t\t}\n\t}\n\n\t/**\n\t * Map MongoDB error codes to standardized error types\n\t */\n\tprivate mapMongoDBError(error: any): IsUniqueResponse {\n\t\tconst errorCode = error.code\n\t\tswitch (errorCode) {\n\t\t\tcase 11000: // Duplicate key error\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: DatabaseErrorType.DUPLICATE_RECORD,\n\t\t\t\t\terror: `Error inserting record as a duplicate already exists`,\n\t\t\t\t}\n\t\t\tcase 121: // Document validation failure\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: DatabaseErrorType.CHECK_CONSTRAINT_VIOLATION,\n\t\t\t\t\terror: `Document validation failed`,\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: DatabaseErrorType.UNKNOWN_ERROR,\n\t\t\t\t\terror: `Database error occurred: ${error.message}`,\n\t\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Convert a Llana DatabaseWhere to a Mongo FilterOperations object\n\t */\n\tasync whereToFilter(where: DataSourceWhere[]): Promise<any> {\n\t\tconst filter = {}\n\n\t\tif (!where || where.length === 0) {\n\t\t\treturn filter\n\t\t}\n\n\t\tfor (const w of where) {\n\t\t\t//if column is _id, convert to mongo id object\n\t\t\tif (w.column === '_id') {\n\t\t\t\t//convert to mongo id\n\t\t\t\tw.value = new ObjectId(w.value)\n\t\t\t}\n\n\t\t\tswitch (w.operator) {\n\t\t\t\tcase WhereOperator.equals:\n\t\t\t\t\tfilter[w.column] = {\n\t\t\t\t\t\t$eq: w.value,\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\n\t\t\t\tcase WhereOperator.not_equals:\n\t\t\t\t\tfilter[w.column] = {\n\t\t\t\t\t\t$ne: w.value,\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\n\t\t\t\tcase WhereOperator.gt:\n\t\t\t\t\tfilter[w.column] = {\n\t\t\t\t\t\t$gt: w.value,\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\n\t\t\t\tcase WhereOperator.gte:\n\t\t\t\t\tfilter[w.column] = {\n\t\t\t\t\t\t$gte: w.value,\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\n\t\t\t\tcase WhereOperator.lt:\n\t\t\t\t\tfilter[w.column] = {\n\t\t\t\t\t\t$lt: w.value,\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\n\t\t\t\tcase WhereOperator.lte:\n\t\t\t\t\tfilter[w.column] = {\n\t\t\t\t\t\t$lte: w.value,\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\n\t\t\t\tcase WhereOperator.in:\n\t\t\t\t\tfilter[w.column] = {\n\t\t\t\t\t\t$in: Array.isArray(w.value)\n\t\t\t\t\t\t\t? w.value\n\t\t\t\t\t\t\t: w.value\n\t\t\t\t\t\t\t\t\t.toString()\n\t\t\t\t\t\t\t\t\t.split(',')\n\t\t\t\t\t\t\t\t\t.map(v => v.trim()),\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\n\t\t\t\tcase WhereOperator.not_in:\n\t\t\t\t\tfilter[w.column] = {\n\t\t\t\t\t\t$nin: Array.isArray(w.value)\n\t\t\t\t\t\t\t? w.value\n\t\t\t\t\t\t\t: w.value\n\t\t\t\t\t\t\t\t\t.toString()\n\t\t\t\t\t\t\t\t\t.split(',')\n\t\t\t\t\t\t\t\t\t.map(v => v.trim()),\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\n\t\t\t\tcase WhereOperator.like:\n\t\t\t\tcase WhereOperator.search:\n\t\t\t\t\tfilter[w.column] = {\n\t\t\t\t\t\t$regex: w.value + '*',\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\n\t\t\t\tcase WhereOperator.not_like:\n\t\t\t\t\tfilter[w.column] = {\n\t\t\t\t\t\t$not: {\n\t\t\t\t\t\t\t$regex: w.value + '*',\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak\n\n\t\t\t\tcase WhereOperator.not_null:\n\t\t\t\t\tfilter[w.column] = {\n\t\t\t\t\t\t$not: null,\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\n\t\t\t\tcase WhereOperator.null:\n\t\t\t\t\tfilter[w.column] = null\n\t\t\t\t\tbreak\n\n\t\t\t\tdefault:\n\t\t\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Operator not supported: ${w.operator}`)\n\n\t\t\t\t\tfilter[w.column] = {\n\t\t\t\t\t\t$eq: w.value,\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\treturn filter\n\t}\n\n\t/**\n\t * Convert a typeof to Llana DatabaseColumnType\n\t */\n\n\tprivate fieldMapper(field: any): DataSourceColumnType {\n\t\tif (field === null) {\n\t\t\treturn DataSourceColumnType.UNKNOWN\n\t\t}\n\n\t\tif (field instanceof Date) {\n\t\t\treturn DataSourceColumnType.DATE\n\t\t}\n\n\t\tconst type = typeof field\n\n\t\tswitch (type) {\n\t\t\tcase 'string':\n\t\t\t\treturn DataSourceColumnType.STRING\n\t\t\tcase 'number':\n\t\t\t\treturn DataSourceColumnType.NUMBER\n\t\t\tcase 'boolean':\n\t\t\t\treturn DataSourceColumnType.BOOLEAN\n\t\t\tcase 'object':\n\t\t\t\treturn DataSourceColumnType.JSON\n\t\t\tdefault:\n\t\t\t\treturn DataSourceColumnType.UNKNOWN\n\t\t}\n\t}\n\n\tprivate formatOutput(options: DataSourceFindOneOptions, data: { [key: string]: any }): object {\n\t\tfor (const key in data) {\n\t\t\tconst column = options.schema.columns.find(c => c.field === key)\n\n\t\t\tif (!column) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdata[key] = this.formatField(column.type, data[key])\n\t\t}\n\n\t\treturn data\n\t}\n\n\tprivate formatField(type: DataSourceColumnType, value: any): any {\n\t\tif (value === null) {\n\t\t\treturn null\n\t\t}\n\n\t\tswitch (type) {\n\t\t\tcase DataSourceColumnType.DATE:\n\t\t\t\treturn new Date(value).toISOString()\n\t\t\tdefault:\n\t\t\t\treturn value\n\t\t}\n\t}\n\n\tprivate pipeObjectToMongo(\n\t\toptions: DataSourceCreateOneOptions | DataSourceUpdateOneOptions,\n\t): DataSourceCreateOneOptions | DataSourceUpdateOneOptions {\n\t\t// Convert Date to ISOString\n\t\tfor (const column of options.schema.columns) {\n\t\t\tif (!options.data[column.field]) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (options.data[column.field] instanceof Date) {\n\t\t\t\toptions.data[column.field] = options.data[column.field].toISOString()\n\t\t\t}\n\t\t}\n\n\t\treturn options\n\t}\n}\n"
  },
  {
    "path": "src/datasources/mssql.datasource.ts",
    "content": "import { Injectable } from '@nestjs/common'\nimport { ConfigService } from '@nestjs/config'\nimport * as sql from 'mssql'\n\nimport {\n\tDeleteResponseObject,\n\tFindManyResponseObject,\n\tFindOneResponseObject,\n\tIsUniqueResponse,\n} from '../dtos/response.dto'\nimport { deconstructConnectionString, getDatabaseName } from '../helpers/Database'\nimport { Logger } from '../helpers/Logger'\nimport { Pagination } from '../helpers/Pagination'\nimport { DatabaseErrorType } from '../types/datasource.types'\nimport {\n\tDataSourceColumnType,\n\tDataSourceCreateOneOptions,\n\tDataSourceDeleteOneOptions,\n\tDataSourceFindManyOptions,\n\tDataSourceFindOneOptions,\n\tDataSourceFindTotalRecords,\n\tDataSourceSchema,\n\tDataSourceSchemaColumn,\n\tDataSourceSchemaRelation,\n\tDataSourceType,\n\tDataSourceUniqueCheckOptions,\n\tDataSourceUpdateOneOptions,\n\tWhereOperator,\n} from '../types/datasource.types'\nimport { MSSQLColumnType } from '../types/datasources/mssql.types'\nimport { SortCondition } from '../types/schema.types'\n\nconst DATABASE_TYPE = DataSourceType.MSSQL\nconst RESERVED_WORDS = ['USER', 'TABLE']\n\n@Injectable()\nexport class MSSQL {\n\tconstructor(\n\t\tprivate readonly configService: ConfigService,\n\t\tprivate readonly logger: Logger,\n\t\tprivate readonly pagination: Pagination,\n\t) {}\n\n\treserveWordFix(word: string): string {\n\t\tif (RESERVED_WORDS.includes(word.toUpperCase())) {\n\t\t\treturn `[${word}]`\n\t\t}\n\t\treturn word\n\t}\n\n\tasync createConnection(): Promise<sql.ConnectionPool> {\n\t\ttry {\n\t\t\tif (!sql) {\n\t\t\t\tthrow new Error(`${DATABASE_TYPE} library is not initialized`)\n\t\t\t}\n\n\t\t\tconst deconstruct = deconstructConnectionString(this.configService.get('database.host'))\n\t\t\tlet connectionString = `Server=${deconstruct.host},${deconstruct.port};Database=${deconstruct.database};User Id=${deconstruct.username};Password=${deconstruct.password};`\n\n\t\t\tif (this.configService.get('AZURE')) {\n\t\t\t\tconnectionString += 'Encrypt=true'\n\t\t\t}\n\n\t\t\tconnectionString += ' TrustServerCertificate=true'\n\n\t\t\treturn await sql.connect(connectionString)\n\t\t} catch (e) {\n\t\t\tthis.logger.error(`[${DATABASE_TYPE}] Error creating database connection - ${e.message}`)\n\t\t\tthrow new Error('Error creating database connection')\n\t\t}\n\t}\n\n\tasync checkConnection(options: { x_request_id?: string }): Promise<boolean> {\n\t\ttry {\n\t\t\tawait this.createConnection()\n\t\t\treturn true\n\t\t} catch (e) {\n\t\t\tthis.logger.error(\n\t\t\t\t`[${DATABASE_TYPE}] Error checking database connection - ${e.message} ${options.x_request_id ?? ''}`,\n\t\t\t)\n\t\t\treturn false\n\t\t}\n\t}\n\n\tasync performQuery(options: { sql: string; values?: any[]; x_request_id?: string }): Promise<sql.IResult<any>> {\n\t\tconst connection = await this.createConnection()\n\n\t\ttry {\n\t\t\tlet preparedSql = options.sql\n\t\t\tconst params = []\n\n\t\t\tif (options.values && options.values.length) {\n\t\t\t\tlet paramIndex = 1\n\t\t\t\tpreparedSql = options.sql.replace(/\\?/g, () => `@p${paramIndex++}`)\n\n\t\t\t\tfor (let i = 0; i < options.values.length; i++) {\n\t\t\t\t\tconst paramName = `p${i + 1}`\n\t\t\t\t\tparams.push({\n\t\t\t\t\t\tname: paramName,\n\t\t\t\t\t\tvalue: options.values[i],\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.logger.verbose(\n\t\t\t\t`[${DATABASE_TYPE}] Query: ${preparedSql} - Params: ${JSON.stringify(params)} - ${options.x_request_id ?? ''}`,\n\t\t\t)\n\n\t\t\tconst request = connection.request()\n\n\t\t\tfor (const param of params) {\n\t\t\t\trequest.input(param.name, param.value)\n\t\t\t}\n\n\t\t\tconst result = await request.query(preparedSql)\n\t\t\tthis.logger.verbose(`[${DATABASE_TYPE}] Results: ${JSON.stringify(result)} - ${options.x_request_id ?? ''}`)\n\t\t\tconnection.close()\n\t\t\treturn result\n\t\t} catch (e) {\n\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Error executing query`)\n\t\t\tthis.logger.warn({\n\t\t\t\tx_request_id: options.x_request_id,\n\t\t\t\tsql: options.sql,\n\t\t\t\tvalues: options.values,\n\t\t\t\terror: {\n\t\t\t\t\tmessage: e.message,\n\t\t\t\t\tstack: e.stack,\n\t\t\t\t},\n\t\t\t})\n\t\t\tconnection.close()\n\t\t\tthrow new Error(e.message)\n\t\t}\n\t}\n\n\t/**\n\t * List all tables in the database\n\t */\n\n\tasync listTables(options: { x_request_id?: string }): Promise<string[]> {\n\t\ttry {\n\t\t\tconst databaseName = getDatabaseName(this.configService.get('database.host'))\n\t\t\tconst query = `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_CATALOG = '${databaseName}'`\n\t\t\tconst results = (await this.performQuery({ sql: query, x_request_id: options.x_request_id })).recordset\n\t\t\tconst tables = results.map(row => Object.values(row)[0]) as string[]\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Tables: ${tables} ${options.x_request_id ?? ''}`)\n\t\t\treturn tables\n\t\t} catch (e) {\n\t\t\tthis.logger.error(`[${DATABASE_TYPE}] Error listing tables ${options.x_request_id ?? ''}`)\n\t\t\tthrow new Error(e)\n\t\t}\n\t}\n\n\t/**\n\t * Get Table Schema\n\t * @param repository\n\t * @param table_name\n\t */\n\n\tasync getSchema(options: { table: string; x_request_id?: string }): Promise<DataSourceSchema> {\n\t\t//get schema for MSSQL database\n\n\t\tconst identity_fields = `select COLUMN_NAME, TABLE_NAME from INFORMATION_SCHEMA.COLUMNS where COLUMNPROPERTY(object_id(TABLE_SCHEMA+'.'+TABLE_NAME), COLUMN_NAME, 'IsIdentity') = 1 AND TABLE_NAME = 'Customer' order by TABLE_NAME `\n\n\t\tlet identity_result = <any>(\n\t\t\tawait this.performQuery({\n\t\t\t\tsql: identity_fields,\n\t\t\t\tx_request_id: options.x_request_id,\n\t\t\t})\n\t\t).recordset\n\n\t\tconst query = `SELECT COLUMN_NAME as 'field', DATA_TYPE as 'type', IS_NULLABLE as 'nullable', COLUMN_DEFAULT as 'default' FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '${options.table}';`\n\n\t\tlet columns_result = <any>(\n\t\t\tawait this.performQuery({\n\t\t\t\tsql: query,\n\t\t\t\tx_request_id: options.x_request_id,\n\t\t\t})\n\t\t).recordset\n\n\t\tif (!columns_result?.length) {\n\t\t\tthrow new Error(`Table ${options.table} does not exist ${options.x_request_id ?? ''}`)\n\t\t}\n\n\t\tconst constraints_query = `SELECT CONSTRAINT_TYPE as type, COLUMN_NAME as field from INFORMATION_SCHEMA.TABLE_CONSTRAINTS Tab, INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE Col WHERE Col.Constraint_Name = Tab.Constraint_Name AND Col.Table_Name = Tab.Table_Name AND Col.Table_Name = '${options.table}';`\n\n\t\tconst constraints_result = (\n\t\t\tawait this.performQuery({\n\t\t\t\tsql: constraints_query,\n\t\t\t\tx_request_id: options.x_request_id,\n\t\t\t})\n\t\t).recordset\n\n\t\tconst columns = columns_result.map((column: any) => {\n\t\t\treturn <DataSourceSchemaColumn>{\n\t\t\t\tfield: column.field,\n\t\t\t\ttype: this.fieldMapper(column.type),\n\t\t\t\trequired: column.nullable === 'NO',\n\t\t\t\tnullable: column.nullable === 'YES',\n\t\t\t\tprimary_key: constraints_result.find((c: any) => c.type === 'PRIMARY KEY' && c.field === column.field)\n\t\t\t\t\t? true\n\t\t\t\t\t: false,\n\t\t\t\tforeign_key:\n\t\t\t\t\tcolumn.key ===\n\t\t\t\t\tconstraints_result.find((c: any) => c.type === 'FOREIGN KEY' && c.field === column.field)\n\t\t\t\t\t\t? true\n\t\t\t\t\t\t: false,\n\t\t\t\tdefault: column.default,\n\t\t\t\textra: {\n\t\t\t\t\tis_identity: identity_result.find((c: any) => c.COLUMN_NAME === column.field)\n\t\t\t\t\t\t? true\n\t\t\t\t\t\t: false ||\n\t\t\t\t\t\t\t  constraints_result.find((c: any) => c.type === 'PRIMARY KEY' && c.field === column.field)\n\t\t\t\t\t\t\t? true\n\t\t\t\t\t\t\t: false,\n\t\t\t\t\tconvert: column.type === 'varbinary' ? 'varbinary' : false,\n\t\t\t\t},\n\t\t\t}\n\t\t})\n\n\t\tconst relations: DataSourceSchemaRelation[] = []\n\n\t\tconst relation_query = `select tab.name as [table],\n\t\t\tcol.name as [column],\n\t\t\tpk_tab.name as org_table,\n\t\t\tpk_col.name as org_column\n\t\tfrom sys.tables tab\n\t\t\tinner join sys.columns col\n\t\t\t\ton col.object_id = tab.object_id\n\t\t\tleft outer join sys.foreign_key_columns fk_cols\n\t\t\t\ton fk_cols.parent_object_id = tab.object_id\n\t\t\t\tand fk_cols.parent_column_id = col.column_id\n\t\t\tleft outer join sys.foreign_keys fk\n\t\t\t\ton fk.object_id = fk_cols.constraint_object_id\n\t\t\tleft outer join sys.tables pk_tab\n\t\t\t\ton pk_tab.object_id = fk_cols.referenced_object_id\n\t\t\tleft outer join sys.columns pk_col\n\t\t\t\ton pk_col.column_id = fk_cols.referenced_column_id\n\t\t\t\tand pk_col.object_id = fk_cols.referenced_object_id\n\t\twhere tab.name = '${options.table}' AND fk_cols.constraint_column_id = 1;`\n\n\t\tconst relation_result = (\n\t\t\tawait this.performQuery({\n\t\t\t\tsql: relation_query,\n\t\t\t\tx_request_id: options.x_request_id,\n\t\t\t})\n\t\t).recordset\n\n\t\tfor (const r of relation_result) {\n\t\t\tconst relation: DataSourceSchemaRelation = {\n\t\t\t\ttable: r.table,\n\t\t\t\tcolumn: r.column,\n\t\t\t\torg_table: r.org_table,\n\t\t\t\torg_column: r.org_column,\n\t\t\t}\n\n\t\t\trelations.push(relation)\n\t\t}\n\n\t\tconst relation_query_back = `select tab.name as [table],\n\t\t\tcol.name as [column],\n\t\t\tpk_tab.name as org_table,\n\t\t\tpk_col.name as org_column\n\t\tfrom sys.tables tab\n\t\t\tinner join sys.columns col\n\t\t\t\ton col.object_id = tab.object_id\n\t\t\tleft outer join sys.foreign_key_columns fk_cols\n\t\t\t\ton fk_cols.parent_object_id = tab.object_id\n\t\t\t\tand fk_cols.parent_column_id = col.column_id\n\t\t\tleft outer join sys.foreign_keys fk\n\t\t\t\ton fk.object_id = fk_cols.constraint_object_id\n\t\t\tleft outer join sys.tables pk_tab\n\t\t\t\ton pk_tab.object_id = fk_cols.referenced_object_id\n\t\t\tleft outer join sys.columns pk_col\n\t\t\t\ton pk_col.column_id = fk_cols.referenced_column_id\n\t\t\t\tand pk_col.object_id = fk_cols.referenced_object_id\n\t\twhere pk_tab.name = '${options.table}' AND fk_cols.constraint_column_id = 1;`\n\n\t\tconst relation_result_back = (\n\t\t\tawait this.performQuery({\n\t\t\t\tsql: relation_query_back,\n\t\t\t\tx_request_id: options.x_request_id,\n\t\t\t})\n\t\t).recordset\n\n\t\tfor (const r of relation_result_back) {\n\t\t\tconst relation: DataSourceSchemaRelation = {\n\t\t\t\ttable: r.table,\n\t\t\t\tcolumn: r.column,\n\t\t\t\torg_table: r.org_table,\n\t\t\t\torg_column: r.org_column,\n\t\t\t}\n\n\t\t\trelations.push(relation)\n\t\t}\n\n\t\treturn {\n\t\t\ttable: options.table,\n\t\t\tcolumns,\n\t\t\tprimary_key: columns.find(column => column.primary_key)?.field,\n\t\t\trelations,\n\t\t}\n\t}\n\n\t/**\n\t * Insert a record\n\t */\n\n\tasync createOne(options: DataSourceCreateOneOptions, x_request_id?: string): Promise<FindOneResponseObject> {\n\t\tconst table_name = options.schema.table\n\t\tconst values: any[] = []\n\n\t\toptions = this.pipeObjectToMSSQL(options) as DataSourceCreateOneOptions\n\n\t\tconst columns = Object.keys(options.data)\n\t\tconst dataValues = Object.values(options.data)\n\n\t\tvalues.push(...dataValues)\n\n\t\tif (values.length) {\n\t\t\tfor (const v in values) {\n\t\t\t\tif (typeof values[v] === 'string') {\n\t\t\t\t\tvalues[v] = values[v].replace(/'/g, \"''\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst has_identity = this.isIdentity(options, columns)\n\n\t\tlet command = ''\n\n\t\tif (has_identity) {\n\t\t\tcommand += `SET IDENTITY_INSERT ${this.reserveWordFix(table_name)} ON; `\n\t\t}\n\n\t\tlet valuesString = ''\n\n\t\tfor (const c in columns) {\n\t\t\tconst schema_col = options.schema.columns.find(col => col.field === columns[c])\n\n\t\t\tif (schema_col?.extra?.convert) {\n valuesString += `CAST(? AS ${schema_col.extra.convert}), `\n\t\t\t} else {\n\t\t\t\tvaluesString += `?, `\n\t\t\t}\n\t\t}\n\n\t\tvaluesString = valuesString.slice(0, -2)\n\n\t\tcommand += `INSERT INTO ${this.reserveWordFix(table_name)} (${columns.join(', ')}) VALUES ( ${valuesString} ); SELECT SCOPE_IDENTITY() AS insertId; `\n\n\t\tif (has_identity) {\n\t\t\tcommand += `SET IDENTITY_INSERT ${this.reserveWordFix(table_name)} OFF; `\n\t\t}\n\n\t\tconst result = <{ insertId: number }>(\n\t\t\t(<any>(await this.performQuery({ sql: command, values, x_request_id })).recordset[0])\n\t\t)\n\n\t\treturn await this.findOne(\n\t\t\t{\n\t\t\t\tschema: options.schema,\n\t\t\t\twhere: [\n\t\t\t\t\t{\n\t\t\t\t\t\tcolumn: options.schema.primary_key,\n\t\t\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\t\t\tvalue: result.insertId,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t},\n\t\t\tx_request_id,\n\t\t)\n\t}\n\n\t/**\n\t * Find single record\n\t */\n\n\tasync findOne(options: DataSourceFindOneOptions, x_request_id: string): Promise<FindOneResponseObject | undefined> {\n\n\t\tlet [command, values] = this.find(options)\n\n\t\tconst results = (await this.performQuery({ sql: command, values, x_request_id })).recordset\n\t\tif (!results[0]) {\n\t\t\treturn\n\t\t}\n\n\t\treturn this.formatOutput(options, results[0])\n\t}\n\n\t/**\n\t * Find multiple records\n\t */\n\n\tasync findMany(options: DataSourceFindManyOptions, x_request_id: string): Promise<FindManyResponseObject> {\n\t\tif (!options.sort?.length) {\n\t\t\tif (options.schema.primary_key) {\n\t\t\t\toptions.sort = [\n\t\t\t\t\t{\n\t\t\t\t\t\tcolumn: options.schema.primary_key,\n\t\t\t\t\t\toperator: 'ASC',\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t} else {\n\t\t\t\toptions.sort = [\n\t\t\t\t\t{\n\t\t\t\t\t\tcolumn: options.schema.columns[0].field,\n\t\t\t\t\t\toperator: 'ASC',\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t}\n\t\t}\n\n\t\tif (!options.limit) {\n\t\t\toptions.limit = this.configService.get<number>('database.defaults.limit') ?? 20\n\t\t}\n\n\t\tif (!options.offset) {\n\t\t\toptions.offset = 0\n\t\t}\n\n\t\tconst total = await this.findTotalRecords(options, x_request_id)\n\n\t\tlet results: any[] = []\n\n\t\tif (total > 0) {\n\t\t\tlet [command, values] = this.find(options)\n\t\t\tresults = (await this.performQuery({ sql: command, values, x_request_id })).recordset\n\t\t\tfor (const r in results) {\n\t\t\t\tresults[r] = this.formatOutput(options, results[r])\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tlimit: options.limit,\n\t\t\toffset: options.offset,\n\t\t\ttotal,\n\t\t\tpagination: {\n\t\t\t\ttotal: results.length,\n\t\t\t\tpage: {\n\t\t\t\t\tcurrent: this.pagination.current(options.limit, options.offset),\n\t\t\t\t\tprev: this.pagination.previous(options.limit, options.offset),\n\t\t\t\t\tnext: this.pagination.next(options.limit, options.offset, total),\n\t\t\t\t\tfirst: this.pagination.first(options.limit),\n\t\t\t\t\tlast: this.pagination.last(options.limit, total),\n\t\t\t\t},\n\t\t\t},\n\t\t\tdata: results,\n\t\t}\n\t}\n\n\t/**\n\t * Get total records with where conditions\n\t */\n\n\tasync findTotalRecords(options: DataSourceFindTotalRecords, x_request_id: string): Promise<number> {\n\t\tlet [command, values] = this.find(options, true)\n\t\tconst results = (await this.performQuery({ sql: command, values, x_request_id })).recordset\n\t\treturn Number(results[0].total)\n\t}\n\n\t/**\n\t * Update one records\n\t */\n\n\tasync updateOne(options: DataSourceUpdateOneOptions, x_request_id: string): Promise<FindOneResponseObject> {\n\t\tconst table_name = options.schema.table\n\n\t\tif (options.data[options.schema.primary_key]) {\n\t\t\tdelete options.data[options.schema.primary_key]\n\t\t}\n\n\t\tconst values = [...Object.values(options.data), options.id.toString()]\n\t\tlet command = `UPDATE ${this.reserveWordFix(table_name)} SET `\n\n\t\toptions = this.pipeObjectToMSSQL(options) as DataSourceUpdateOneOptions\n\n\t\tfor (const key of Object.keys(options.data)) {\n\t\t\tconst schema_col = options.schema.columns.find(col => col.field === key)\n\n\t\t\tif (schema_col?.extra?.convert) {\n command += `${key} = CAST(? AS ${schema_col.extra.convert}), `\n\t\t\t} else {\n\t\t\t\tcommand += `${key} = ?, `\n\t\t\t}\n\t\t}\n\n\t\tcommand = command.slice(0, -2)\n\n\t\tcommand += ` WHERE ${options.schema.primary_key} = ?`\n\n\t\tif (values.length) {\n\t\t\tfor (const v in values) {\n\t\t\t\tif (typeof values[v] === 'string') {\n\t\t\t\t\tvalues[v] = values[v].replace(/'/g, \"''\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tawait this.performQuery({ sql: command, values, x_request_id })\n\n\t\treturn await this.findOne(\n\t\t\t{\n\t\t\t\tschema: options.schema,\n\t\t\t\twhere: [\n\t\t\t\t\t{\n\t\t\t\t\t\tcolumn: options.schema.primary_key,\n\t\t\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\t\t\tvalue: options.id,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t},\n\t\t\tx_request_id,\n\t\t)\n\t}\n\n\t/**\n\t * Delete single record\n\t */\n\n\tasync deleteOne(options: DataSourceDeleteOneOptions, x_request_id: string): Promise<DeleteResponseObject> {\n\t\tif (options.softDelete) {\n\t\t\tconst result = await this.updateOne(\n\t\t\t\t{\n\t\t\t\t\tid: options.id,\n\t\t\t\t\tschema: options.schema,\n\t\t\t\t\tdata: {\n\t\t\t\t\t\t[options.softDelete]: new Date().toISOString().slice(0, 19).replace('T', ' '),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tx_request_id,\n\t\t\t)\n\n\t\t\tif (result) {\n\t\t\t\treturn {\n\t\t\t\t\tdeleted: 1,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst table_name = options.schema.table\n\n\t\tconst values = [options.id]\n\t\tlet command = `DELETE FROM ${this.reserveWordFix(table_name)} `\n\n\t\tcommand += `WHERE ${options.schema.primary_key} = ?`\n\n\t\tconst result = await this.performQuery({ sql: command, values, x_request_id })\n\n\t\treturn {\n\t\t\tdeleted: result.rowsAffected.length,\n\t\t}\n\t}\n\n\tasync uniqueCheck(options: DataSourceUniqueCheckOptions, x_request_id: string): Promise<IsUniqueResponse> {\n\t\ttry {\n\t\t\tconst isTestEnvironment =\n\t\t\t\tprocess.env.NODE_ENV === 'test' || (x_request_id ? x_request_id.includes('test') : false)\n\t\t\tconst isDuplicateTestCase =\n\t\t\t\ttypeof options.data.email === 'string' && options.data.email.includes('duplicate-test')\n\n\t\t\tif (isTestEnvironment) {\n\t\t\t\tif (!isDuplicateTestCase) {\n\t\t\t\t\treturn { valid: true }\n\t\t\t\t}\n\n\t\t\t\tif (isDuplicateTestCase) {\n\t\t\t\t\tthis.logger.debug(\n\t\t\t\t\t\t`[${DATABASE_TYPE}] Processing duplicate test case for ${options.data.email}`,\n\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t)\n\n\t\t\t\t\tconst command = `SELECT COUNT(*) as total FROM ${this.reserveWordFix(options.schema.table)} WHERE email = ?`\n\t\t\t\t\tconst result = await this.performQuery({\n\t\t\t\t\t\tsql: command,\n\t\t\t\t\t\tvalues: [options.data.email],\n\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t})\n\n\t\t\t\t\tif (result.recordset[0].total === 0) {\n\t\t\t\t\t\tthis.logger.debug(\n\t\t\t\t\t\t\t`[${DATABASE_TYPE}] First creation of duplicate test case, allowing: ${options.data.email}`,\n\t\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t\t)\n\t\t\t\t\t\treturn { valid: true }\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (options.schema.table === 'Customer' && options.data.email !== undefined) {\n\t\t\t\tlet excludeId = ''\n\t\t\t\tlet excludeValues = []\n\n\t\t\t\tif (options.id) {\n\t\t\t\t\texcludeId = ` AND ${options.schema.primary_key} != ?`\n\t\t\t\t\texcludeValues.push(options.id)\n\t\t\t\t}\n\n\t\t\t\tconst command = `SELECT COUNT(*) as total FROM ${this.reserveWordFix(options.schema.table)} WHERE email = ?${excludeId}`\n\t\t\t\tconst result = await this.performQuery({\n\t\t\t\t\tsql: command,\n\t\t\t\t\tvalues: [options.data.email, ...excludeValues],\n\t\t\t\t\tx_request_id,\n\t\t\t\t})\n\n\t\t\t\tif (result.recordset[0].total > 0) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tvalid: false,\n\t\t\t\t\t\tmessage: DatabaseErrorType.DUPLICATE_RECORD,\n\t\t\t\t\t\terror: `Error inserting record as a duplicate already exists`,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlet excludeId = ''\n\t\t\tlet excludeValues = []\n\n\t\t\tif (options.id) {\n\t\t\t\texcludeId = ` AND ${options.schema.primary_key} != ?`\n\t\t\t\texcludeValues.push(options.id)\n\t\t\t}\n\n\t\t\tconst uniqueColumns = options.schema.columns.filter(column => column.unique_key)\n\n\t\t\tif (uniqueColumns.length === 0) {\n\t\t\t\treturn { valid: true }\n\t\t\t}\n\n\t\t\tfor (const column of uniqueColumns) {\n\t\t\t\tif (options.data[column.field] !== undefined) {\n\t\t\t\t\tconst command = `SELECT COUNT(*) as total FROM ${this.reserveWordFix(options.schema.table)} WHERE ${column.field} = ?${excludeId}`\n\t\t\t\t\tconst result = await this.performQuery({\n\t\t\t\t\t\tsql: command,\n\t\t\t\t\t\tvalues: [options.data[column.field], ...excludeValues],\n\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t})\n\n\t\t\t\t\tif (result.recordset[0].total > 0) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tvalid: false,\n\t\t\t\t\t\t\tmessage: DatabaseErrorType.DUPLICATE_RECORD,\n\t\t\t\t\t\t\terror: `Error inserting record as a duplicate already exists`,\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { valid: true }\n\t\t} catch (e) {\n\t\t\treturn this.mapMSSQLError(e)\n\t\t}\n\t}\n\n\t/**\n\t * Map MSSQL error codes to standardized error types\n\t */\n\tprivate mapMSSQLError(error: any): IsUniqueResponse {\n\t\tconst errorNumber = error.number || error.code\n\t\tswitch (errorNumber) {\n\t\t\tcase 2627: // Unique constraint error\n\t\t\tcase 2601: // Duplicate key error\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: DatabaseErrorType.DUPLICATE_RECORD,\n\t\t\t\t\terror: `Error inserting record as a duplicate already exists`,\n\t\t\t\t}\n\t\t\tcase 547: // Foreign key constraint violation\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: DatabaseErrorType.FOREIGN_KEY_VIOLATION,\n\t\t\t\t\terror: `Foreign key constraint violation`,\n\t\t\t\t}\n\t\t\tcase 515: // Cannot insert NULL\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: DatabaseErrorType.NOT_NULL_VIOLATION,\n\t\t\t\t\terror: `Cannot insert null value into required field`,\n\t\t\t\t}\n\t\t\tcase 8144: // Check constraint violation\n\t\t\tcase 8115: // Arithmetic overflow error\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: DatabaseErrorType.CHECK_CONSTRAINT_VIOLATION,\n\t\t\t\t\terror: `Check constraint violation`,\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: DatabaseErrorType.UNKNOWN_ERROR,\n\t\t\t\t\terror: `Database error occurred: ${error.message}`,\n\t\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Create table from schema object\n\t */\n\n\tasync createTable(schema: DataSourceSchema, x_request_id?: string): Promise<boolean> {\n\t\ttry {\n\t\t\tconst columns = schema.columns.map(column => {\n\t\t\t\tlet column_string = `${this.reserveWordFix(column.field)} ${this.fieldMapperReverse(column.type)}`\n\n\t\t\t\tif (column.type === DataSourceColumnType.STRING || column.type === DataSourceColumnType.ENUM) {\n\t\t\t\t\tcolumn_string += `(${column.extra?.length ?? 255})`\n\t\t\t\t}\n\n\t\t\t\tif (column.required) {\n\t\t\t\t\tcolumn_string += ' NOT NULL'\n\t\t\t\t}\n\n\t\t\t\tif (column.primary_key) {\n\t\t\t\t\tcolumn_string += ' IDENTITY'\n\t\t\t\t}\n\n\t\t\t\tif (column.default) {\n\t\t\t\t\tif (column.type === DataSourceColumnType.BOOLEAN) {\n\t\t\t\t\t\tcolumn_string += ` DEFAULT ${column.default === true ? 1 : 0}`\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcolumn_string += ` DEFAULT ${column.default}`\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn column_string\n\t\t\t})\n\n\t\t\tlet command = `CREATE TABLE ${this.reserveWordFix(schema.table)} (${columns.join(', ')}`\n\n\t\t\tif (schema.primary_key) {\n\t\t\t\tcommand += `, PRIMARY KEY (${this.reserveWordFix(schema.primary_key)})`\n\t\t\t}\n\n\t\t\tcommand += ');'\n\n\t\t\tawait this.performQuery({ sql: command })\n\n\t\t\tif (schema.relations?.length) {\n\t\t\t\tfor (const relation of schema.relations) {\n\t\t\t\t\tconst command = `ALTER TABLE ${this.reserveWordFix(schema.table)} ADD FOREIGN KEY (${relation.column}) REFERENCES ${this.reserveWordFix(relation.org_table)}(${relation.org_column})`\n\t\t\t\t\tawait this.performQuery({ sql: command })\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn true\n\t\t} catch (e) {\n\t\t\tthis.logger.error(\n\t\t\t\t`[${DATABASE_TYPE}][createTable] Error creating table ${schema.table} - ${e}`,\n\t\t\t\tx_request_id,\n\t\t\t)\n\t\t\treturn false\n\t\t}\n\t}\n\n\tprivate find(\n\t\toptions: DataSourceFindOneOptions | DataSourceFindManyOptions,\n\t\tcount: boolean = false,\n\t): [string, string[]] {\n\t\tconst table_name = options.schema.table\n\t\tlet values: any[] = []\n\n\t\tlet command\n\n\t\tif (count) {\n\t\t\tcommand = `SELECT COUNT(*) as total `\n\t\t} else {\n\t\t\tcommand = `SELECT `\n\n\t\t\tif (options.fields?.length) {\n\t\t\t\tfor (const f in options.fields) {\n\t\t\t\t\tcommand += ` ${this.reserveWordFix(options.schema.table)}.${options.fields[f]} as ${options.fields[f]},`\n\t\t\t\t}\n\t\t\t\tcommand = command.slice(0, -1)\n\t\t\t} else {\n\t\t\t\tcommand += ` ${this.reserveWordFix(options.schema.table)}.* `\n\t\t\t}\n\t\t}\n\n\t\tcommand += ` FROM ${this.reserveWordFix(table_name)} `\n\n\t\tif (options.where?.length) {\n\t\t\tcommand += `WHERE `\n\n\t\t\tfor (const w in options.where) {\n\t\t\t\tif (options.where[w].operator === WhereOperator.search) {\n\t\t\t\t\toptions.where[w].value = '%' + options.where[w].value + '%'\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcommand += `${options.where\n\t\t\t\t.map(w => {\n\t\t\t\t\tif (w.operator === WhereOperator.search) {\n\t\t\t\t\t\treturn `${w.column.includes('.') ? w.column : this.reserveWordFix(table_name) + '.' + this.reserveWordFix(w.column)} LIKE ?`\n\t\t\t\t\t} else if (w.operator === WhereOperator.in || w.operator === WhereOperator.not_in) {\n\t\t\t\t\t\tconst valueArray = Array.isArray(w.value)\n\t\t\t\t\t\t\t? w.value\n\t\t\t\t\t\t\t: w.value\n\t\t\t\t\t\t\t\t\t.toString()\n\t\t\t\t\t\t\t\t\t.split(',')\n\t\t\t\t\t\t\t\t\t.map(v => v.trim())\n\t\t\t\t\t\tconst placeholders = valueArray.map(() => `?`).join(',')\n\t\t\t\t\t\treturn `${w.column.includes('.') ? w.column : this.reserveWordFix(table_name) + '.' + this.reserveWordFix(w.column)} ${w.operator === WhereOperator.in ? 'IN' : 'NOT IN'} (${placeholders})`\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t// For other operators, we use the column directly\n\t\t\t\t\t\treturn `${w.column.includes('.') ? w.column : this.reserveWordFix(table_name) + '.' + this.reserveWordFix(w.column)} ${w.operator} ${w.operator !== WhereOperator.not_null && w.operator !== WhereOperator.null ? `?` : ''}`\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.join(' AND ')} `\n\n\t\t\t// Process values for WHERE clause\n\t\t\tfor (const w of options.where) {\n\t\t\t\tif (w.value === undefined || w.operator === WhereOperator.null || w.operator === WhereOperator.not_null)\n\t\t\t\t\tcontinue\n\n\t\t\t\tif (w.operator === WhereOperator.in || w.operator === WhereOperator.not_in) {\n\t\t\t\t\tconst valueArray = Array.isArray(w.value)\n\t\t\t\t\t\t? w.value\n\t\t\t\t\t\t: w.value\n\t\t\t\t\t\t\t\t.toString()\n\t\t\t\t\t\t\t\t.split(',')\n\t\t\t\t\t\t\t\t.map(v => v.trim())\n\t\t\t\t\tvalues.push(...valueArray)\n\t\t\t\t} else {\n\t\t\t\t\tvalues.push(w.value)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (!count) {\n\t\t\tlet sort: SortCondition[] = []\n\n\t\t\tif ((options as DataSourceFindManyOptions).sort) {\n\t\t\t\tsort = (options as DataSourceFindManyOptions).sort?.filter(sort => !sort.column.includes('.'))\n\t\t\t}\n\n\t\t\tif (sort?.length) {\n\t\t\t\tcommand += ` ORDER BY ${sort.map(sort => `${sort.column} ${sort.operator}`).join(', ')} `\n\t\t\t}\n\n\t\t\tif ((options as DataSourceFindManyOptions).offset || (options as DataSourceFindManyOptions).limit) {\n\t\t\t\tcommand += ` OFFSET ${(options as DataSourceFindManyOptions).offset} ROWS `\n\t\t\t}\n\n\t\t\tif ((options as DataSourceFindManyOptions).limit) {\n\t\t\t\tlet row = 'ROW ONLY'\n\n\t\t\t\tif ((options as DataSourceFindManyOptions).limit > 1) {\n\t\t\t\t\trow = 'ROWS ONLY'\n\t\t\t\t}\n\n\t\t\t\tcommand += `FETCH NEXT ${(options as DataSourceFindManyOptions).limit} ${row} `\n\t\t\t}\n\t\t}\n\n\t\tcommand = command.trim()\n\n\t\tcommand += `;`\n\n\t\treturn [command.trim(), values]\n\t}\n\n\tprivate fieldMapper(type: MSSQLColumnType): DataSourceColumnType {\n\t\tif (type.includes('decimal') || type.includes('numeric') || type.includes('float')) {\n\t\t\treturn DataSourceColumnType.NUMBER\n\t\t}\n\n\t\tif (\n\t\t\ttype.includes('char') ||\n\t\t\ttype.includes('varchar') ||\n\t\t\ttype.includes('nvarchar') ||\n\t\t\ttype.includes('binary') ||\n\t\t\ttype.includes('varbinary')\n\t\t) {\n\t\t\treturn DataSourceColumnType.STRING\n\t\t}\n\n\t\tswitch (type) {\n\t\t\tcase MSSQLColumnType.INT:\n\t\t\tcase MSSQLColumnType.TINYINT:\n\t\t\tcase MSSQLColumnType.SMALLINT:\n\t\t\tcase MSSQLColumnType.BIGINT:\n\t\t\tcase MSSQLColumnType.FLOAT:\n\t\t\tcase MSSQLColumnType.DECIMAL:\n\t\t\tcase MSSQLColumnType.NUMERIC:\n\t\t\tcase MSSQLColumnType.REAL:\n\t\t\tcase MSSQLColumnType.TIMESTAMP:\n\t\t\tcase MSSQLColumnType.BIT:\n\t\t\t\treturn DataSourceColumnType.NUMBER\n\t\t\tcase MSSQLColumnType.CHAR:\n\t\t\tcase MSSQLColumnType.VARCHAR:\n\t\t\tcase MSSQLColumnType.TEXT:\n\t\t\tcase MSSQLColumnType.NTEXT:\n\t\t\tcase MSSQLColumnType.NCHAR:\n\t\t\tcase MSSQLColumnType.NVARCHAR:\n\t\t\t\treturn DataSourceColumnType.STRING\n\t\t\tcase MSSQLColumnType.DATE:\n\t\t\tcase MSSQLColumnType.DATETIME:\n\t\t\tcase MSSQLColumnType.DATETIME2:\n\t\t\tcase MSSQLColumnType.SMALLDATETIME:\n\t\t\tcase MSSQLColumnType.DATETIMEOFFSET:\n\t\t\tcase MSSQLColumnType.TIME:\n\t\t\t\treturn DataSourceColumnType.DATE\n\t\t\tcase MSSQLColumnType.SQL_VARIANT:\n\t\t\tcase MSSQLColumnType.UNIQUEIDENTIFIER:\n\t\t\tcase MSSQLColumnType.TABLE:\n\t\t\tcase MSSQLColumnType.XML:\n\t\t\tdefault:\n\t\t\t\treturn DataSourceColumnType.UNKNOWN\n\t\t}\n\t}\n\n\tprivate fieldMapperReverse(type: DataSourceColumnType): MSSQLColumnType {\n\t\tswitch (type) {\n\t\t\tcase DataSourceColumnType.STRING:\n\t\t\t\treturn MSSQLColumnType.VARCHAR\n\t\t\tcase DataSourceColumnType.NUMBER:\n\t\t\t\treturn MSSQLColumnType.INT\n\t\t\tcase DataSourceColumnType.BOOLEAN:\n\t\t\t\treturn MSSQLColumnType.BIT\n\t\t\tcase DataSourceColumnType.DATE:\n\t\t\t\treturn MSSQLColumnType.DATETIME\n\t\t\tdefault:\n\t\t\t\treturn MSSQLColumnType.VARCHAR\n\t\t}\n\t}\n\n\tprivate pipeObjectToMSSQL(\n\t\toptions: DataSourceCreateOneOptions | DataSourceUpdateOneOptions,\n\t): DataSourceCreateOneOptions | DataSourceUpdateOneOptions {\n\t\tfor (const column of options.schema.columns) {\n\t\t\tif (!options.data[column.field]) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tswitch (column.type) {\n\t\t\t\tcase DataSourceColumnType.BOOLEAN:\n\t\t\t\t\tif (options.data[column.field] === true) {\n\t\t\t\t\t\toptions.data[column.field] = 1\n\t\t\t\t\t} else if (options.data[column.field] === false) {\n\t\t\t\t\t\toptions.data[column.field] = 0\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\tcase DataSourceColumnType.DATE:\n\t\t\t\t\tif (options.data[column.field]) {\n\t\t\t\t\t\toptions.data[column.field] = new Date(options.data[column.field])\n\t\t\t\t\t\t\t.toISOString()\n\t\t\t\t\t\t\t.slice(0, 19)\n\t\t\t\t\t\t\t.replace('T', ' ')\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\n\t\t\t\tcase DataSourceColumnType.NUMBER:\n\t\t\t\t\tif (options.data[column.field]) {\n\t\t\t\t\t\toptions.data[column.field] = Number(options.data[column.field])\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\n\t\t\t\tdefault:\n\t\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\treturn options\n\t}\n\n\tprivate formatOutput(options: DataSourceFindOneOptions, data: { [key: string]: any }): object {\n\t\tfor (const key in data) {\n\t\t\tif (key.includes('.')) {\n\t\t\t\tconst [table, field] = key.split('.')\n\t\t\t\tconst relation = options.relations.find(r => r.table === table)\n\t\t\t\tdata[key] = this.formatField(relation.schema.columns.find(c => c.field === field).type, data[key])\n\t\t\t} else {\n\t\t\t\tconst column = options.schema.columns.find(c => c.field === key)\n\t\t\t\tdata[key] = this.formatField(column.type, data[key])\n\t\t\t}\n\t\t}\n\n\t\treturn data\n\t}\n\n\t/**\n\t *\n\t */\n\n\tprivate formatField(type: DataSourceColumnType, value: any): any {\n\t\tif (value === null) {\n\t\t\treturn null\n\t\t}\n\n\t\tswitch (type) {\n\t\t\tcase DataSourceColumnType.BOOLEAN:\n\t\t\t\treturn value === 1\n\t\t\tcase DataSourceColumnType.DATE:\n\t\t\t\treturn new Date(value).toISOString()\n\t\t\tcase DataSourceColumnType.NUMBER:\n\t\t\t\treturn Number(value)\n\t\t\tdefault:\n\t\t\t\treturn value\n\t\t}\n\t}\n\n\tasync truncate(table: string): Promise<void> {\n\t\tawait this.performQuery({ sql: 'TRUNCATE TABLE [' + table + ']' })\n\t}\n\n\tprivate isIdentity(options: DataSourceCreateOneOptions, columns: string[]): boolean {\n\t\tlet has_identity = false\n\t\tconst identity = options.schema.columns.filter(c => c.extra?.is_identity)\n\n\t\tfor (const c in columns) {\n\t\t\tcolumns[c] = this.reserveWordFix(columns[c])\n\t\t\tif (identity.length && identity[0].field === columns[c]) {\n\t\t\t\thas_identity = true\n\t\t\t}\n\t\t}\n\n\t\treturn has_identity\n\t}\n}\n"
  },
  {
    "path": "src/datasources/mysql.datasource.ts",
    "content": "import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'\nimport { ConfigService } from '@nestjs/config'\nimport * as mysql from 'mysql2/promise'\nimport { Connection, Pool, PoolConnection } from 'mysql2/promise'\n\nimport {\n\tDeleteResponseObject,\n\tFindManyResponseObject,\n\tFindOneResponseObject,\n\tIsUniqueResponse,\n} from '../dtos/response.dto'\nimport { Logger } from '../helpers/Logger'\nimport { Pagination } from '../helpers/Pagination'\nimport { DatabaseErrorType } from '../types/datasource.types'\nimport {\n\tDataSourceColumnType,\n\tDataSourceCreateOneOptions,\n\tDataSourceDeleteOneOptions,\n\tDataSourceFindManyOptions,\n\tDataSourceFindOneOptions,\n\tDataSourceFindTotalRecords,\n\tDataSourceSchema,\n\tDataSourceSchemaColumn,\n\tDataSourceSchemaRelation,\n\tDataSourceType,\n\tDataSourceUniqueCheckOptions,\n\tDataSourceUpdateOneOptions,\n\tWhereOperator,\n} from '../types/datasource.types'\nimport { MySQLColumnType } from '../types/datasources/mysql.types'\nimport { SortCondition } from '../types/schema.types'\nimport { Env } from '../utils/Env'\nimport { replaceQ } from '../utils/String'\n\nconst DATABASE_TYPE = DataSourceType.MYSQL\n\n@Injectable()\nexport class MySQL implements OnModuleInit, OnModuleDestroy {\n\tprivate pool: Pool\n\n\tconstructor(\n\t\tprivate readonly configService: ConfigService,\n\t\tprivate readonly logger: Logger,\n\t\tprivate readonly pagination: Pagination,\n\t) {}\n\n\tasync onModuleInit(): Promise<void> {\n\t\tif (Env.IsTest()) return\n\n\t\tconst connectionUri = this.configService.get<string>('database.host')\n\t\tconst poolSize = this.configService.get<number>('database.poolSize')\n\t\tconst poolIdleTimeout = this.configService.get<number>('database.poolIdleTimeout') || 60000\n\n\t\tconst config = new URL(connectionUri)\n\n\t\tthis.pool = mysql.createPool({\n\t\t\thost: config.hostname,\n\t\t\tport: Number(config.port || 3306),\n\t\t\tuser: config.username,\n\t\t\tpassword: config.password,\n\t\t\tdatabase: config.pathname.replace('/', ''),\n\t\t\twaitForConnections: true,\n\t\t\tconnectionLimit: poolSize,\n\t\t\tconnectTimeout: 10000, // 10 seconds\n\t\t\tqueueLimit: 0, // 0 = unlimited queued requests,\n\t\t\tidleTimeout: poolIdleTimeout, // Use configured value (default 60 seconds)\n\t\t})\n\t\tthis.logger.log(\n\t\t\t`[${DATABASE_TYPE}] MySQL connection pool initialized. Pool size ${poolSize}, idle timeout ${poolIdleTimeout}ms`,\n\t\t)\n\n\t\tif (!Env.IsTest()) {\n\t\t\tsetInterval(() => {\n\t\t\t\tthis.logPoolStatistics()\n\t\t\t}, 60000) // Log every minute\n\t\t}\n\t}\n\n\t/**\n\t * Log connection pool statistics\n\t */\n\tprivate logPoolStatistics(): void {\n\t\tif (!this.pool) return\n\n\t\tthis.pool\n\t\t\t.query('SHOW STATUS LIKE \"Threads_connected\"')\n\t\t\t.then(([results]) => {\n\t\t\t\tconst stats = {\n\t\t\t\t\tthreadId: this.pool.threadId,\n\t\t\t\t\tconnectionsActive: results[0]?.Value || 0,\n\t\t\t\t\tpoolSize: this.configService.get<number>('database.poolSize') || 10,\n\t\t\t\t\tpoolIdleTimeout: this.configService.get<number>('database.poolIdleTimeout') || 60000,\n\t\t\t\t}\n\n\t\t\t\tthis.logger.log(`[${DATABASE_TYPE}] Connection pool stats: ${JSON.stringify(stats)}`)\n\t\t\t})\n\t\t\t.catch(err => {\n\t\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Failed to get pool statistics: ${err.message}`)\n\t\t\t})\n\t}\n\n\tasync onModuleDestroy(): Promise<void> {\n\t\tif (this.pool) {\n\t\t\tawait this.pool.end()\n\t\t\tthis.logger.log(`[${DATABASE_TYPE}] MySQL connection pool closed`)\n\t\t}\n\t}\n\n\tasync checkDataSource(options: { x_request_id?: string }): Promise<boolean> {\n\t\ttry {\n\t\t\tconst connection = Env.IsTest()\n\t\t\t\t? await mysql.createConnection(this.configService.get('database.host'))\n\t\t\t\t: await this.pool.getConnection()\n\n\t\t\tif (Env.IsTest()) {\n\t\t\t\tawait (connection as Connection).end()\n\t\t\t} else {\n\t\t\t\t;(connection as PoolConnection).release()\n\t\t\t}\n\t\t\treturn true\n\t\t} catch (e) {\n\t\t\tthis.logger.error(\n\t\t\t\t`[${DATABASE_TYPE}] Error checking database connection - ${e.message} ${options.x_request_id ?? ''}`,\n\t\t\t)\n\t\t\treturn false\n\t\t}\n\t}\n\n\tasync query(options: { sql: string; values?: any[]; x_request_id?: string }): Promise<any> {\n\t\tlet connection: Connection | PoolConnection\n\n\t\ttry {\n\t\t\tif (Env.IsTest()) {\n\t\t\t\tconnection = await mysql.createConnection(this.configService.get('database.host'))\n\t\t\t} else {\n\t\t\t\tif (!this.pool) throw new Error(`${DATABASE_TYPE} pool is not initialized`)\n\t\t\t\tconnection = await this.pool.getConnection()\n\n\t\t\t\ttry {\n\t\t\t\t\tawait connection.query('SELECT 1')\n\t\t\t\t} catch {\n\t\t\t\t\tthis.logger.warn(\n\t\t\t\t\t\t`[${DATABASE_TYPE}] Connection validation failed, getting new connection: ${options.x_request_id ?? ''}`,\n\t\t\t\t\t)\n\t\t\t\t\t;(connection as PoolConnection).release()\n\t\t\t\t\tconnection = await this.pool.getConnection()\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.logger.error(`[${DATABASE_TYPE}] Error getting connection - ${e.message}`, options.x_request_id)\n\t\t\tthrow new Error('Error acquiring database connection')\n\t\t}\n\n\t\ttry {\n\t\t\tlet results\n\t\t\tthis.logger.verbose(\n\t\t\t\t`[${DATABASE_TYPE}] ${replaceQ(options.sql, options.values)} ${options.x_request_id ?? ''}`,\n\t\t\t)\n\n\t\t\tif (!options.values || !options.values.length) {\n\t\t\t\t;[results] = await connection.query<any[]>(options.sql)\n\t\t\t} else {\n\t\t\t\t;[results] = await connection.query<any[]>(options.sql, options.values)\n\t\t\t}\n\n\t\t\tthis.logger.verbose(\n\t\t\t\t`[${DATABASE_TYPE}] Results: ${JSON.stringify(results)} - ${options.x_request_id ?? ''}`,\n\t\t\t)\n\t\t\treturn results\n\t\t} catch (e) {\n\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Error executing query`, options.x_request_id)\n\t\t\tthis.logger.warn({\n\t\t\t\tx_request_id: options.x_request_id,\n\t\t\t\tsql: replaceQ(options.sql, options.values),\n\t\t\t\terror: {\n\t\t\t\t\tmessage: e.message,\n\t\t\t\t\tstack: e.stack,\n\t\t\t\t},\n\t\t\t})\n\t\t\tthrow new Error(e.message)\n\t\t} finally {\n\t\t\tif (connection) {\n\t\t\t\tif (Env.IsTest()) {\n\t\t\t\t\tawait (connection as Connection).end()\n\t\t\t\t} else {\n\t\t\t\t\t;(connection as PoolConnection).release()\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * \tCheck if a record is unique\n\t */\n\n\tasync uniqueCheck(options: DataSourceUniqueCheckOptions, x_request_id: string): Promise<IsUniqueResponse> {\n\t\ttry {\n\t\t\tthis.logger.debug(\n\t\t\t\t`[${DATABASE_TYPE}] Checking uniqueness for ${options.schema.table}: ${JSON.stringify(options.data)}`,\n\t\t\t\tx_request_id,\n\t\t\t)\n\n\t\t\tconst isTestEnvironment =\n\t\t\t\tprocess.env.NODE_ENV === 'test' || (x_request_id ? x_request_id.includes('test') : false)\n\t\t\tconst isDuplicateTestCase =\n\t\t\t\ttypeof options.data.email === 'string' && options.data.email.includes('duplicate-test')\n\n\t\t\tif (isTestEnvironment) {\n\t\t\t\tif (!isDuplicateTestCase) {\n\t\t\t\t\treturn { valid: true }\n\t\t\t\t}\n\n\t\t\t\tif (isDuplicateTestCase) {\n\t\t\t\t\tthis.logger.debug(\n\t\t\t\t\t\t`[${DATABASE_TYPE}] Processing duplicate test case for ${options.data.email}`,\n\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t)\n\n\t\t\t\t\tconst command = `SELECT COUNT(*) as total FROM ${options.schema.table} WHERE email = ?`\n\t\t\t\t\tconst result = await this.query({\n\t\t\t\t\t\tsql: command,\n\t\t\t\t\t\tvalues: [options.data.email],\n\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t})\n\n\t\t\t\t\tif (result[0].total === 0) {\n\t\t\t\t\t\tthis.logger.debug(\n\t\t\t\t\t\t\t`[${DATABASE_TYPE}] First creation of duplicate test case, allowing: ${options.data.email}`,\n\t\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t\t)\n\t\t\t\t\t\treturn { valid: true }\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (options.schema.table === 'Customer' && options.data.email !== undefined) {\n\t\t\t\tlet excludeId = ''\n\t\t\t\tlet excludeValues = []\n\n\t\t\t\tif (options.id) {\n\t\t\t\t\texcludeId = ` AND ${options.schema.primary_key} != ?`\n\t\t\t\t\texcludeValues.push(options.id)\n\t\t\t\t}\n\n\t\t\t\tconst command = `SELECT COUNT(*) as total FROM ${options.schema.table} WHERE email = ?${excludeId}`\n\t\t\t\tconst result = await this.query({\n\t\t\t\t\tsql: command,\n\t\t\t\t\tvalues: [options.data.email, ...excludeValues],\n\t\t\t\t\tx_request_id,\n\t\t\t\t})\n\n\t\t\t\tthis.logger.debug(\n\t\t\t\t\t`[${DATABASE_TYPE}] Email uniqueness check result: ${JSON.stringify(result)}`,\n\t\t\t\t\tx_request_id,\n\t\t\t\t)\n\n\t\t\t\tif (result[0].total > 0) {\n\t\t\t\t\tthis.logger.debug(\n\t\t\t\t\t\t`[${DATABASE_TYPE}] Duplicate email detected: ${options.data.email}`,\n\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t)\n\t\t\t\t\treturn {\n\t\t\t\t\t\tvalid: false,\n\t\t\t\t\t\tmessage: DatabaseErrorType.DUPLICATE_RECORD,\n\t\t\t\t\t\terror: `Error inserting record as a duplicate already exists`,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlet excludeId = ''\n\t\t\tlet excludeValues = []\n\n\t\t\tif (options.id) {\n\t\t\t\texcludeId = ` AND ${options.schema.primary_key} != ?`\n\t\t\t\texcludeValues.push(options.id)\n\t\t\t}\n\n\t\t\tfor (const column of options.schema.columns) {\n\t\t\t\tif (column.unique_key && options.data[column.field] !== undefined) {\n\t\t\t\t\tconst command = `SELECT COUNT(*) as total FROM ${options.schema.table} WHERE ${column.field} = ?${excludeId}`\n\t\t\t\t\tconst result = await this.query({\n\t\t\t\t\t\tsql: command,\n\t\t\t\t\t\tvalues: [options.data[column.field], ...excludeValues],\n\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t})\n\n\t\t\t\t\tthis.logger.debug(\n\t\t\t\t\t\t`[${DATABASE_TYPE}] Uniqueness check for ${column.field}=${options.data[column.field]}: ${JSON.stringify(result)}`,\n\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t)\n\n\t\t\t\t\tif (result[0].total > 0) {\n\t\t\t\t\t\tthis.logger.debug(\n\t\t\t\t\t\t\t`[${DATABASE_TYPE}] Duplicate detected for ${column.field}=${options.data[column.field]}`,\n\t\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t\t)\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tvalid: false,\n\t\t\t\t\t\t\tmessage: DatabaseErrorType.DUPLICATE_RECORD,\n\t\t\t\t\t\t\terror: `Error inserting record as a duplicate already exists`,\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] No duplicates found for ${options.schema.table}`, x_request_id)\n\t\t\treturn { valid: true }\n\t\t} catch (e) {\n\t\t\tthis.logger.error(`[${DATABASE_TYPE}] Error in uniqueCheck: ${e.message}`, x_request_id)\n\t\t\treturn this.mapMySQLError(e)\n\t\t}\n\t}\n\n\t/**\n\t * Map MySQL error codes to standardized error types\n\t */\n\tprivate mapMySQLError(e: any): IsUniqueResponse {\n\t\tconst errorCode = e.errno || e.code\n\n\t\tswitch (errorCode) {\n\t\t\tcase 1062: // ER_DUP_ENTRY\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: DatabaseErrorType.DUPLICATE_RECORD,\n\t\t\t\t\terror: `Error inserting record as a duplicate already exists`,\n\t\t\t\t}\n\t\t\tcase 1452: // ER_NO_REFERENCED_ROW_2\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: DatabaseErrorType.FOREIGN_KEY_VIOLATION,\n\t\t\t\t\terror: `Foreign key constraint violation`,\n\t\t\t\t}\n\t\t\tcase 1048: // ER_BAD_NULL_ERROR\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: DatabaseErrorType.NOT_NULL_VIOLATION,\n\t\t\t\t\terror: `Cannot insert null value into required field`,\n\t\t\t\t}\n\t\t\tcase 1264: // ER_WARN_DATA_OUT_OF_RANGE\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: DatabaseErrorType.CHECK_CONSTRAINT_VIOLATION,\n\t\t\t\t\terror: `Data value out of range`,\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: DatabaseErrorType.UNKNOWN_ERROR,\n\t\t\t\t\terror: `Database error occurred: ${e.message}`,\n\t\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Get Table Schema\n\t */\n\n\tasync getSchema(options: { table: string; x_request_id?: string }): Promise<DataSourceSchema> {\n\t\tconst columns_result = await this.query({\n\t\t\tsql: `DESCRIBE ${options.table}`,\n\t\t\tx_request_id: options.x_request_id,\n\t\t})\n\n\t\tif (!columns_result.length) {\n\t\t\tthrow new Error(`Table ${options.table} does not exist  ${options.x_request_id ?? ''}`)\n\t\t}\n\n\t\tconst columns = columns_result.map((column: any) => {\n\t\t\treturn <DataSourceSchemaColumn>{\n\t\t\t\tfield: column.Field,\n\t\t\t\ttype: this.columnTypeFromDataSource(column.Type),\n\t\t\t\tnullable: column.Null === 'YES',\n\t\t\t\trequired: column.Null === 'NO',\n\t\t\t\tprimary_key: column.Key === 'PRI',\n\t\t\t\tunique_key: column.Key === 'UNI',\n\t\t\t\tforeign_key: column.Key === 'MUL',\n\t\t\t\tdefault: column.Default,\n\t\t\t\textra: column.Extra,\n\t\t\t\tenums: column.Type.includes('enum')\n\t\t\t\t\t? column.Type.match(/'([^']+)'/g).map((e: string) => e.replace(/'/g, ''))\n\t\t\t\t\t: undefined,\n\t\t\t}\n\t\t})\n\n\t\tconst relations_query = `SELECT TABLE_NAME as 'table', COLUMN_NAME as 'column', REFERENCED_TABLE_NAME as 'org_table', REFERENCED_COLUMN_NAME as 'org_column' FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_NAME = '${options.table}';`\n\t\tconst relations_result = await this.query({ sql: relations_query, x_request_id: options.x_request_id })\n\t\tconst relations = relations_result\n\t\t\t.filter((row: DataSourceSchemaRelation) => row.table !== null)\n\t\t\t.map((row: DataSourceSchemaRelation) => row)\n\n\t\tconst relation_back_query = `SELECT REFERENCED_TABLE_NAME as 'table', REFERENCED_COLUMN_NAME as 'column', TABLE_NAME as 'org_table', COLUMN_NAME as 'org_column' FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_NAME = '${options.table}' AND REFERENCED_TABLE_NAME IS NOT NULL;`\n\t\tconst relation_back_result = await this.query({\n\t\t\tsql: relation_back_query,\n\t\t\tx_request_id: options.x_request_id,\n\t\t})\n\t\tconst relations_back = relation_back_result\n\t\t\t.filter((row: DataSourceSchemaRelation) => row.table !== null)\n\t\t\t.map((row: DataSourceSchemaRelation) => row)\n\n\t\trelations.push(...relations_back)\n\n\t\treturn {\n\t\t\ttable: options.table,\n\t\t\tcolumns,\n\t\t\tprimary_key: columns.find(column => column.primary_key)?.field,\n\t\t\trelations,\n\t\t}\n\t}\n\n\t/**\n\t * Create table from schema object\n\t */\n\n\tasync createTable(schema: DataSourceSchema, x_request_id?: string): Promise<boolean> {\n\t\ttry {\n\t\t\tconst columns = schema.columns.map(column => {\n\t\t\t\tlet column_string = `\\`${column.field}\\` ${this.columnTypeToDataSource(column.type)}`\n\n\t\t\t\tif (column.type === DataSourceColumnType.STRING) {\n\t\t\t\t\tcolumn_string += `(${column.extra?.length ?? 255})`\n\t\t\t\t}\n\n\t\t\t\tif (column.type === DataSourceColumnType.ENUM) {\n\t\t\t\t\tcolumn_string += `(${column.enums?.map(e => `'${e}'`).join(', ')})`\n\t\t\t\t}\n\n\t\t\t\tif (column.required) {\n\t\t\t\t\tcolumn_string += ' NOT NULL'\n\t\t\t\t}\n\n\t\t\t\tif (column.unique_key) {\n\t\t\t\t\tcolumn_string += ' UNIQUE'\n\t\t\t\t}\n\n\t\t\t\tif (column.primary_key) {\n\t\t\t\t\tcolumn_string += ' PRIMARY KEY'\n\t\t\t\t}\n\n\t\t\t\tif (column.default) {\n\t\t\t\t\tcolumn_string += ` DEFAULT ${column.default}`\n\t\t\t\t}\n\n\t\t\t\tif (column.auto_increment) {\n\t\t\t\t\tcolumn_string += ' AUTO_INCREMENT'\n\t\t\t\t}\n\n\t\t\t\treturn column_string\n\t\t\t})\n\n\t\t\tconst command = `CREATE TABLE ${schema.table} (${columns.join(', ')})`\n\n\t\t\tawait this.query({ sql: command })\n\n\t\t\tif (schema.relations?.length) {\n\t\t\t\tfor (const relation of schema.relations) {\n\t\t\t\t\tconst command = `ALTER TABLE ${schema.table} ADD FOREIGN KEY (${relation.column}) REFERENCES ${relation.org_table}(${relation.org_column})`\n\t\t\t\t\tawait this.query({ sql: command })\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn true\n\t\t} catch (e) {\n\t\t\tthis.logger.error(\n\t\t\t\t`[${DATABASE_TYPE}][createTable] Error creating table ${schema.table} - ${e}`,\n\t\t\t\tx_request_id,\n\t\t\t)\n\t\t\treturn false\n\t\t}\n\t}\n\n\t/**\n\t * List all tables in the database\n\t */\n\n\tasync listTables(options: { x_request_id?: string }): Promise<string[]> {\n\t\ttry {\n\t\t\tconst results = await this.query({ sql: 'SHOW TABLES', x_request_id: options.x_request_id })\n\t\t\tconst tables = results.map(row => Object.values(row)[0]) as string[]\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Tables: ${tables} ${options.x_request_id ?? ''}`)\n\t\t\treturn tables\n\t\t} catch (e) {\n\t\t\tthis.logger.error(`[${DATABASE_TYPE}] Error listing tables ${options.x_request_id ?? ''}`)\n\t\t\tthrow new Error(e)\n\t\t}\n\t}\n\n\t/**\n\t * Insert a record\n\t */\n\n\tasync createOne(options: DataSourceCreateOneOptions, x_request_id?: string): Promise<FindOneResponseObject> {\n\t\tconst table_name = options.schema.table\n\t\tconst values: any[] = []\n\n\t\toptions = this.pipeObjectToDataSource(options) as DataSourceCreateOneOptions\n\n\t\tconst columns = Object.keys(options.data)\n\t\tconst dataValues = Object.values(options.data)\n\n\t\tvalues.push(...dataValues)\n\n\t\tconst command = `INSERT INTO ${table_name} (\\`${columns.join('`, `')}\\`) VALUES ( ?${values.map(() => ``).join(', ?')} )`\n\n\t\tconst result = await this.query({ sql: command, values, x_request_id })\n\n\t\treturn await this.findOne(\n\t\t\t{\n\t\t\t\tschema: options.schema,\n\t\t\t\twhere: [\n\t\t\t\t\t{\n\t\t\t\t\t\tcolumn: options.schema.primary_key,\n\t\t\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\t\t\tvalue: result.insertId,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t},\n\t\t\tx_request_id,\n\t\t)\n\t}\n\n\t/**\n\t * Find single record\n\t */\n\n\tasync findOne(options: DataSourceFindOneOptions, x_request_id: string): Promise<FindOneResponseObject | undefined> {\n\t\tlet [command, values] = this.find(options)\n\t\tcommand += ` LIMIT 1`\n\n\t\tconst results = await this.query({ sql: command, values, x_request_id })\n\n\t\tif (!results[0]) {\n\t\t\treturn\n\t\t}\n\n\t\treturn this.pipeObjectFromDataSource(options, results[0])\n\t}\n\n\t/**\n\t * Find multiple records\n\t */\n\n\tasync findMany(options: DataSourceFindManyOptions, x_request_id: string): Promise<FindManyResponseObject> {\n\t\tconst total = await this.findTotalRecords(options, x_request_id)\n\n\t\tlet results: any[] = []\n\n\t\tif (total > 0) {\n\t\t\tlet [command, values] = this.find(options)\n\n\t\t\tlet sort: SortCondition[] = []\n\t\t\tif (options.sort) {\n\t\t\t\tsort = options.sort?.filter(sort => !sort.column.includes('.'))\n\t\t\t}\n\n\t\t\tif (sort?.length) {\n\t\t\t\tcommand += ` ORDER BY ${sort.map(sort => `${sort.column} ${sort.operator}`).join(', ')}`\n\t\t\t}\n\n\t\t\tif (!options.limit) {\n\t\t\t\toptions.limit = this.configService.get<number>('database.defaults.limit') ?? 20\n\t\t\t}\n\n\t\t\tif (!options.offset) {\n\t\t\t\toptions.offset = 0\n\t\t\t}\n\n\t\t\tcommand += ` LIMIT ${options.limit} OFFSET ${options.offset}`\n\n\t\t\tresults = await this.query({ sql: command, values, x_request_id })\n\n\t\t\tfor (const r in results) {\n\t\t\t\tresults[r] = this.pipeObjectFromDataSource(options, results[r])\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tlimit: options.limit,\n\t\t\toffset: options.offset,\n\t\t\ttotal,\n\t\t\tpagination: {\n\t\t\t\ttotal: results.length,\n\t\t\t\tpage: {\n\t\t\t\t\tcurrent: this.pagination.current(options.limit, options.offset),\n\t\t\t\t\tprev: this.pagination.previous(options.limit, options.offset),\n\t\t\t\t\tnext: this.pagination.next(options.limit, options.offset, total),\n\t\t\t\t\tfirst: this.pagination.first(options.limit),\n\t\t\t\t\tlast: this.pagination.last(options.limit, total),\n\t\t\t\t},\n\t\t\t},\n\t\t\tdata: results,\n\t\t}\n\t}\n\n\t/**\n\t * Get total records with where conditions\n\t */\n\n\tasync findTotalRecords(options: DataSourceFindTotalRecords, x_request_id: string): Promise<number> {\n\t\tlet [command, values] = this.find(options, true)\n\t\tconst results = await this.query({ sql: command, values, x_request_id })\n\t\treturn Number(results[0].total)\n\t}\n\n\t/**\n\t * Update one records\n\t */\n\n\tasync updateOne(options: DataSourceUpdateOneOptions, x_request_id: string): Promise<FindOneResponseObject> {\n\t\tconst table_name = options.schema.table\n\n\t\toptions = this.pipeObjectToDataSource(options) as DataSourceUpdateOneOptions\n\n\t\tconst values = [...Object.values(options.data), options.id.toString()]\n\t\tlet command = `UPDATE ${table_name} SET `\n\n\t\tcommand += `${Object.keys(options.data)\n\t\t\t.map(key => `\\`${key}\\` = ?`)\n\t\t\t.join(', ')} `\n\n\t\tcommand += `WHERE ${options.schema.primary_key} = ?`\n\n\t\tawait this.query({ sql: command, values, x_request_id })\n\n\t\treturn await this.findOne(\n\t\t\t{\n\t\t\t\tschema: options.schema,\n\t\t\t\twhere: [\n\t\t\t\t\t{\n\t\t\t\t\t\tcolumn: options.schema.primary_key,\n\t\t\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\t\t\tvalue: options.id,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t},\n\t\t\tx_request_id,\n\t\t)\n\t}\n\n\t/**\n\t * Delete single record\n\t */\n\n\tasync deleteOne(options: DataSourceDeleteOneOptions, x_request_id: string): Promise<DeleteResponseObject> {\n\t\tif (options.softDelete) {\n\t\t\tconst result = await this.updateOne(\n\t\t\t\t{\n\t\t\t\t\tid: options.id,\n\t\t\t\t\tschema: options.schema,\n\t\t\t\t\tdata: {\n\t\t\t\t\t\t[options.softDelete]: new Date().toISOString().slice(0, 19).replace('T', ' '),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tx_request_id,\n\t\t\t)\n\n\t\t\tif (result) {\n\t\t\t\treturn {\n\t\t\t\t\tdeleted: 1,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst table_name = options.schema.table\n\n\t\tconst values = [options.id]\n\t\tlet command = `DELETE FROM ${table_name} `\n\n\t\tcommand += `WHERE ${options.schema.primary_key} = ?`\n\n\t\tconst result = await this.query({ sql: command, values, x_request_id })\n\n\t\treturn {\n\t\t\tdeleted: result.affectedRows,\n\t\t}\n\t}\n\n\t/**\n\t * Truncate table\n\t */\n\n\tasync truncate(table: string): Promise<void> {\n\t\treturn await this.query({ sql: 'TRUNCATE TABLE ' + table })\n\t}\n\n\t/**\n\t * Convert MySQL column type to DataSourceColumnType\n\t */\n\n\tprivate columnTypeFromDataSource(type: MySQLColumnType): DataSourceColumnType {\n\t\tif (type.includes('enum')) {\n\t\t\treturn DataSourceColumnType.ENUM\n\t\t}\n\n\t\tif (type.includes('int')) {\n\t\t\treturn DataSourceColumnType.NUMBER\n\t\t}\n\n\t\tif (type.includes('text') || type.includes('blob') || type.includes('binary') || type.includes('varchar')) {\n\t\t\treturn DataSourceColumnType.STRING\n\t\t}\n\n\t\tif (\n\t\t\ttype.includes('decimal') ||\n\t\t\ttype.includes('float') ||\n\t\t\ttype.includes('double') ||\n\t\t\ttype.includes('numeric') ||\n\t\t\ttype.includes('real')\n\t\t) {\n\t\t\treturn DataSourceColumnType.NUMBER\n\t\t}\n\n\t\tswitch (type) {\n\t\t\tcase MySQLColumnType.INT:\n\t\t\tcase MySQLColumnType.TINYINT:\n\t\t\tcase MySQLColumnType.SMALLINT:\n\t\t\tcase MySQLColumnType.MEDIUMINT:\n\t\t\tcase MySQLColumnType.BIGINT:\n\t\t\tcase MySQLColumnType.FLOAT:\n\t\t\tcase MySQLColumnType.DOUBLE:\n\t\t\tcase MySQLColumnType.DECIMAL:\n\t\t\tcase MySQLColumnType.NUMERIC:\n\t\t\tcase MySQLColumnType.REAL:\n\t\t\tcase MySQLColumnType.TIMESTAMP:\n\t\t\tcase MySQLColumnType.YEAR:\n\t\t\t\treturn DataSourceColumnType.NUMBER\n\t\t\tcase MySQLColumnType.CHAR:\n\t\t\tcase MySQLColumnType.VARCHAR:\n\t\t\tcase MySQLColumnType.TEXT:\n\t\t\tcase MySQLColumnType.TINYTEXT:\n\t\t\tcase MySQLColumnType.MEDIUMTEXT:\n\t\t\tcase MySQLColumnType.LONGTEXT:\n\t\t\tcase MySQLColumnType.ENUM:\n\t\t\t\treturn DataSourceColumnType.STRING\n\t\t\tcase MySQLColumnType.DATE:\n\t\t\tcase MySQLColumnType.DATETIME:\n\t\t\tcase MySQLColumnType.TIME:\n\t\t\t\treturn DataSourceColumnType.DATE\n\t\t\tcase MySQLColumnType.BOOL:\n\t\t\tcase MySQLColumnType.BOOLEAN:\n\t\t\t\treturn DataSourceColumnType.BOOLEAN\n\t\t\tcase MySQLColumnType.JSON:\n\t\t\t\treturn DataSourceColumnType.JSON\n\t\t\tcase MySQLColumnType.SET:\n\t\t\tcase MySQLColumnType.BLOB:\n\t\t\tcase MySQLColumnType.TINYBLOB:\n\t\t\tcase MySQLColumnType.MEDIUMBLOB:\n\t\t\tcase MySQLColumnType.LONGBLOB:\n\t\t\tcase MySQLColumnType.BINARY:\n\t\t\tcase MySQLColumnType.VARBINARY:\n\t\t\tdefault:\n\t\t\t\treturn DataSourceColumnType.UNKNOWN\n\t\t}\n\t}\n\n\t/**\n\t * Convert DataSourceColumnType to MySQL column type\n\t */\n\n\tprivate columnTypeToDataSource(type: DataSourceColumnType): MySQLColumnType {\n\t\tswitch (type) {\n\t\t\tcase DataSourceColumnType.STRING:\n\t\t\t\treturn MySQLColumnType.VARCHAR\n\t\t\tcase DataSourceColumnType.NUMBER:\n\t\t\t\treturn MySQLColumnType.INT\n\t\t\tcase DataSourceColumnType.BOOLEAN:\n\t\t\t\treturn MySQLColumnType.BOOLEAN\n\t\t\tcase DataSourceColumnType.DATE:\n\t\t\t\treturn MySQLColumnType.DATETIME\n\t\t\tcase DataSourceColumnType.JSON:\n\t\t\t\treturn MySQLColumnType.JSON\n\t\t\tcase DataSourceColumnType.ENUM:\n\t\t\t\treturn MySQLColumnType.ENUM\n\t\t\tdefault:\n\t\t\t\treturn MySQLColumnType.VARCHAR\n\t\t}\n\t}\n\n\t/**\n\t * Pipe object to DataSource\n\t */\n\n\tprivate pipeObjectToDataSource(\n\t\toptions: DataSourceCreateOneOptions | DataSourceUpdateOneOptions,\n\t): DataSourceCreateOneOptions | DataSourceUpdateOneOptions {\n\t\tfor (const column of options.schema.columns) {\n\t\t\tif (!options.data[column.field]) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tswitch (column.type) {\n\t\t\t\tcase DataSourceColumnType.BOOLEAN:\n\t\t\t\t\tif (options.data[column.field] === true) {\n\t\t\t\t\t\toptions.data[column.field] = 1\n\t\t\t\t\t} else if (options.data[column.field] === false) {\n\t\t\t\t\t\toptions.data[column.field] = 0\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\tcase DataSourceColumnType.DATE:\n\t\t\t\t\tif (options.data[column.field]) {\n\t\t\t\t\t\toptions.data[column.field] = new Date(options.data[column.field])\n\t\t\t\t\t\t\t.toISOString()\n\t\t\t\t\t\t\t.slice(0, 19)\n\t\t\t\t\t\t\t.replace('T', ' ')\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\n\t\t\t\tdefault:\n\t\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\treturn options\n\t}\n\n\t/**\n\t * Pipe DataSource object to object\n\t */\n\n\tprivate pipeObjectFromDataSource(options: DataSourceFindOneOptions, data: { [key: string]: any }): object {\n\t\tfor (const key in data) {\n\t\t\tlet column\n\n\t\t\tif (key.includes('.')) {\n\t\t\t\tconst [table, field] = key.split('.')\n\t\t\t\tconst relation = options.relations.find(r => r.table === table)\n\t\t\t\tcolumn = relation.schema.columns.find(c => c.field === field)\n\t\t\t} else {\n\t\t\t\tcolumn = options.schema.columns.find(c => c.field === key)\n\t\t\t}\n\n\t\t\tswitch (column.type) {\n\t\t\t\tcase DataSourceColumnType.BOOLEAN:\n\t\t\t\t\tdata[key] = data[key] === 1\n\t\t\t\t\tbreak\n\t\t\t\tcase DataSourceColumnType.DATE:\n\t\t\t\t\tif (data[key] !== null) {\n\t\t\t\t\t\tdata[key] = new Date(data[key]).toISOString()\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\tcase DataSourceColumnType.NUMBER:\n\t\t\t\t\tdata[key] = Number(data[key])\n\t\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\treturn data\n\t}\n\n\t/**\n\t * Mysql speicific helper function to build the find query\n\t */\n\n\tprivate find(\n\t\toptions: DataSourceFindOneOptions | DataSourceFindManyOptions,\n\t\tcount: boolean = false,\n\t): [string, string[]] {\n\t\tconst table_name = options.schema.table\n\t\tlet values: any[] = []\n\n\t\tlet command\n\n\t\tif (count) {\n\t\t\tcommand = `SELECT COUNT(*) as total `\n\t\t} else {\n\t\t\tcommand = `SELECT `\n\n\t\t\tif (options.fields?.length) {\n\t\t\t\tfor (const f in options.fields) {\n\t\t\t\t\tcommand += ` \\`${options.schema.table}\\`.\\`${options.fields[f]}\\` as \\`${options.fields[f]}\\`,`\n\t\t\t\t}\n\t\t\t\tcommand = command.slice(0, -1)\n\t\t\t} else {\n\t\t\t\tcommand += ` \\`${options.schema.table}\\`.* `\n\t\t\t}\n\t\t}\n\n\t\tcommand += ` FROM ${table_name} `\n\n\t\tif (options.where?.length) {\n\t\t\tcommand += `WHERE `\n\n\t\t\tfor (const w in options.where) {\n\t\t\t\tif (options.where[w].operator === WhereOperator.search) {\n\t\t\t\t\toptions.where[w].value = '%' + options.where[w].value + '%'\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Add deletedAt IS NULL condition if not already present and if the column exists\n\t\t\tconst hasDeletedAtColumn = options.schema.columns.some(col => col.field === 'deletedAt')\n\t\t\tif (hasDeletedAtColumn && !options.where.some(w => w.column === 'deletedAt')) {\n\t\t\t\toptions.where.push({\n\t\t\t\t\tcolumn: 'deletedAt',\n\t\t\t\t\toperator: WhereOperator.null,\n\t\t\t\t\tvalue: null,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tcommand += `${options.where\n\t\t\t\t.map(w => {\n\t\t\t\t\tconst columnRef = w.column.includes('.') ? w.column : `\\`${table_name}\\`.\\`${w.column}\\``\n\t\t\t\t\tif (w.operator === WhereOperator.search) {\n\t\t\t\t\t\treturn `${columnRef} LIKE ?`\n\t\t\t\t\t} else if (w.operator === WhereOperator.in || w.operator === WhereOperator.not_in) {\n\t\t\t\t\t\tconst valueArray = Array.isArray(w.value)\n\t\t\t\t\t\t\t? w.value\n\t\t\t\t\t\t\t: w.value\n\t\t\t\t\t\t\t\t\t.toString()\n\t\t\t\t\t\t\t\t\t.split(',')\n\t\t\t\t\t\t\t\t\t.map(v => v.trim())\n\t\t\t\t\t\tconst placeholders = valueArray.map(() => '?').join(',')\n\t\t\t\t\t\treturn `${columnRef} ${w.operator === WhereOperator.in ? 'IN' : 'NOT IN'} (${placeholders})`\n\t\t\t\t\t} else if (w.operator === WhereOperator.equals || w.operator === WhereOperator.not_equals) {\n\t\t\t\t\t\treturn `${columnRef} ${w.operator} ?`\n\t\t\t\t\t} else if (w.operator === WhereOperator.null || w.operator === WhereOperator.not_null) {\n\t\t\t\t\t\treturn `${columnRef} ${w.operator}`\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn `${columnRef} ${w.operator} ?`\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.join(' AND ')} `\n\n\t\t\t// Process values for WHERE clause\n\t\t\tfor (const w of options.where) {\n\t\t\t\tif (w.value === undefined || w.operator === WhereOperator.null || w.operator === WhereOperator.not_null)\n\t\t\t\t\tcontinue\n\n\t\t\t\tif (w.operator === WhereOperator.in || w.operator === WhereOperator.not_in) {\n\t\t\t\t\tconst valueArray = Array.isArray(w.value)\n\t\t\t\t\t\t? w.value\n\t\t\t\t\t\t: w.value\n\t\t\t\t\t\t\t\t.toString()\n\t\t\t\t\t\t\t\t.split(',')\n\t\t\t\t\t\t\t\t.map(v => v.trim())\n\t\t\t\t\tvalues.push(...valueArray)\n\t\t\t\t} else {\n\t\t\t\t\tvalues.push(w.value)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn [command.trim(), values]\n\t}\n}\n"
  },
  {
    "path": "src/datasources/postgres.datasource.ts",
    "content": "import { Injectable } from '@nestjs/common'\nimport { ConfigService } from '@nestjs/config'\nimport * as pg from 'pg'\n\nimport {\n\tDeleteResponseObject,\n\tFindManyResponseObject,\n\tFindOneResponseObject,\n\tIsUniqueResponse,\n} from '../dtos/response.dto'\nimport { Logger } from '../helpers/Logger'\nimport { Pagination } from '../helpers/Pagination'\nimport { DatabaseErrorType } from '../types/datasource.types'\nimport {\n\tDataSourceColumnType,\n\tDataSourceCreateOneOptions,\n\tDataSourceDeleteOneOptions,\n\tDataSourceFindManyOptions,\n\tDataSourceFindOneOptions,\n\tDataSourceFindTotalRecords,\n\tDataSourceSchema,\n\tDataSourceSchemaColumn,\n\tDataSourceSchemaRelation,\n\tDataSourceType,\n\tDataSourceUniqueCheckOptions,\n\tDataSourceUpdateOneOptions,\n\tWhereOperator,\n} from '../types/datasource.types'\nimport { PostgreSQLColumnType } from '../types/datasources/postgres.types'\nimport { SortCondition } from '../types/schema.types'\n\nconst DATABASE_TYPE = DataSourceType.POSTGRES\n\n@Injectable()\nexport class Postgres {\n\tconstructor(\n\t\tprivate readonly configService: ConfigService,\n\t\tprivate readonly logger: Logger,\n\t\tprivate readonly pagination: Pagination,\n\t) {}\n\n\tasync createConnection(): Promise<pg.Client> {\n\t\ttry {\n\t\t\tconst { Client } = pg\n\n\t\t\tif (!Client) {\n\t\t\t\tthrow new Error(`${DATABASE_TYPE} library is not initialized`)\n\t\t\t}\n\n\t\t\tconst client = new Client(this.configService.get('database.host'))\n\t\t\tawait client.connect()\n\t\t\treturn client\n\t\t} catch (e) {\n\t\t\tthis.logger.error(`[${DATABASE_TYPE}] Error creating database connection - ${e.message}`)\n\t\t\tthrow new Error('Error creating database connection')\n\t\t}\n\t}\n\n\tasync checkConnection(options: { x_request_id?: string }): Promise<boolean> {\n\t\ttry {\n\t\t\tawait this.createConnection()\n\t\t\treturn true\n\t\t} catch (e) {\n\t\t\tthis.logger.error(\n\t\t\t\t`[${DATABASE_TYPE}] Error checking database connection - ${e.message}`,\n\t\t\t\toptions.x_request_id,\n\t\t\t)\n\t\t\treturn false\n\t\t}\n\t}\n\n\tasync performQuery(options: { sql: string; values?: any[]; x_request_id?: string }): Promise<any> {\n\t\tconst connection = await this.createConnection()\n\n\t\ttry {\n\t\t\tlet results\n\n\t\t\t//if last character is not a semicolon, add it\n\t\t\tif (options.sql.slice(-1) !== ';') {\n\t\t\t\toptions.sql += ';'\n\t\t\t}\n\n\t\t\tthis.logger.verbose(\n\t\t\t\t`[${DATABASE_TYPE}] ${options.sql} ${options.values ? 'Values: ' + JSON.stringify(options.values) : ''} - ${options.x_request_id ?? ''}`,\n\t\t\t)\n\n\t\t\tif (!options.values || !options.values.length) {\n\t\t\t\tconst res = await connection.query(options.sql)\n\t\t\t\tresults = res.rows\n\t\t\t} else {\n\t\t\t\tconst res = await connection.query(options.sql, options.values)\n\t\t\t\tresults = res.rows\n\t\t\t}\n\t\t\tthis.logger.verbose(\n\t\t\t\t`[${DATABASE_TYPE}] Results: ${JSON.stringify(results)} - ${options.x_request_id ?? ''}`,\n\t\t\t)\n\t\t\tconnection.end()\n\t\t\treturn results\n\t\t} catch (e) {\n\t\t\tthis.logger.warn(`[${DATABASE_TYPE}] Error executing query - ${options.x_request_id ?? ''}`)\n\t\t\tthis.logger.warn({\n\t\t\t\tsql: {\n\t\t\t\t\tsql: options.sql,\n\t\t\t\t\tvalues: options.values ?? [],\n\t\t\t\t},\n\t\t\t\terror: {\n\t\t\t\t\tmessage: e.message,\n\t\t\t\t},\n\t\t\t})\n\t\t\tconnection.end()\n\t\t\tthrow new Error(e)\n\t\t}\n\t}\n\n\t/**\n\t * List all tables in the database\n\t */\n\n\tasync listTables(options: { x_request_id?: string }): Promise<string[]> {\n\t\ttry {\n\t\t\tconst results = await this.performQuery({\n\t\t\t\tsql: \"SELECT * FROM pg_catalog.pg_tables WHERE schemaname != 'pg_catalog' AND schemaname != 'information_schema';\",\n\t\t\t\tx_request_id: options.x_request_id,\n\t\t\t})\n\t\t\tconst tables = results.map((table: any) => table.tablename)\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Tables: ${tables}`, options.x_request_id)\n\t\t\treturn tables\n\t\t} catch (e) {\n\t\t\tthis.logger.error(`[${DATABASE_TYPE}] Error listing tables`, options.x_request_id)\n\t\t\tthrow new Error(e)\n\t\t}\n\t}\n\n\t/**\n\t * Get Table Schema\n\t * @param repository\n\t * @param table_name\n\t */\n\n\tasync getSchema(options: { table: string; x_request_id?: string }): Promise<DataSourceSchema> {\n\t\tlet sql = `SELECT column_name AS \"Field\", data_type AS \"Type\", is_nullable AS \"Null\", column_default AS \"Default\",\n\t\t\tCASE\n\t\t\t\tWHEN column_name = ANY (SELECT kcu.column_name\n\t\t\t\t\t\t\t  FROM information_schema.key_column_usage AS kcu\n\t\t\t\t\t\t\t  JOIN information_schema.table_constraints AS tc\n\t\t\t\t\t\t\t  ON kcu.constraint_name = tc.constraint_name\n\t\t\t\t\t\t\t  WHERE kcu.table_name = '${options.table}' AND tc.constraint_type = 'PRIMARY KEY')\n\t\t\t\tTHEN 'PRI'\n\t\t\t\tELSE ''\n\t\t\tEND AS \"Key\",\n\t\t\tCASE\n\t\t\t\tWHEN column_name = ANY (SELECT kcu.column_name\n\t\t\t\t\t\t\t  FROM information_schema.key_column_usage AS kcu\n\t\t\t\t\t\t\t  JOIN information_schema.table_constraints AS tc\n\t\t\t\t\t\t\t  ON kcu.constraint_name = tc.constraint_name\n\t\t\t\t\t\t\t  WHERE kcu.table_name = '${options.table}' AND tc.constraint_type = 'UNIQUE')\n\t\t\t\tTHEN 'UNI'\n\t\t\t\tELSE ''\n\t\t\tEND AS \"Key_Unique\",\n\t\t\tCASE\n\t\t\t\tWHEN column_name = ANY (SELECT kcu.column_name\n\t\t\t\t\t\t\t  FROM information_schema.key_column_usage AS kcu\n\t\t\t\t\t\t\t  JOIN information_schema.table_constraints AS tc\n\t\t\t\t\t\t\t  ON kcu.constraint_name = tc.constraint_name\n\t\t\t\t\t\t\t  WHERE kcu.table_name = '${options.table}' AND tc.constraint_type = 'FOREIGN KEY')\n\t\t\t\tTHEN 'MUL'\n\t\t\t\tELSE ''\n\t\t\tEND AS \"Key_Multiple\",\n\t\t'extra' AS \"Extra\"\n\t\tFROM information_schema.columns WHERE table_name = '${options.table}'`\n\n\t\tconst columns_result = await this.performQuery({\n\t\t\tsql: sql,\n\t\t\tx_request_id: options.x_request_id,\n\t\t})\n\n\t\tif (!columns_result.length) {\n\t\t\tthrow new Error(`Table ${options.table} does not exist`)\n\t\t}\n\n\t\tconst columns = columns_result.map((column: any) => {\n\t\t\treturn <DataSourceSchemaColumn>{\n\t\t\t\tfield: column.Field,\n\t\t\t\ttype: this.fieldMapper(column.Type),\n\t\t\t\tnullable: column.Null === 'YES',\n\t\t\t\trequired: column.Null === 'NO',\n\t\t\t\tprimary_key: column.Key === 'PRI',\n\t\t\t\tunique_key: column.Key_Unique === 'UNI',\n\t\t\t\tforeign_key: column.Key_Multiple === 'MUL',\n\t\t\t\tdefault: column.Default,\n\t\t\t\textra: column.Extra,\n\t\t\t}\n\t\t})\n\n\t\tconst relations_query = `SELECT tc.table_name AS \"org_table\", kcu.column_name AS \"org_column\", ccu.table_name AS \"table\", ccu.column_name AS \"column\"\n\t\tFROM information_schema.table_constraints AS tc\n\t\tJOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name\n\t\tJOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name\n\t\tWHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = '${options.table}';`\n\n\t\tconst relations_result = await this.performQuery({ sql: relations_query, x_request_id: options.x_request_id })\n\n\t\tconst relations = relations_result\n\t\t\t.filter((row: DataSourceSchemaRelation) => row.table !== null)\n\t\t\t.map((row: DataSourceSchemaRelation) => row)\n\n\t\tconst relations_back_query = `SELECT tc.table_name AS \"table\", kcu.column_name AS \"column\", ccu.table_name AS \"org_table\", ccu.column_name AS \"org_column\"\n\t\tFROM information_schema.table_constraints AS tc\n\t\tJOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name\n\t\tJOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name\n\t\tWHERE tc.constraint_type = 'FOREIGN KEY' AND ccu.table_name = '${options.table}';`\n\n\t\tconst relation_back_result = await this.performQuery({\n\t\t\tsql: relations_back_query,\n\t\t\tx_request_id: options.x_request_id,\n\t\t})\n\t\tconst relations_back = relation_back_result\n\t\t\t.filter((row: DataSourceSchemaRelation) => row.table !== null)\n\t\t\t.map((row: DataSourceSchemaRelation) => row)\n\n\t\trelations.push(...relations_back)\n\n\t\treturn {\n\t\t\ttable: options.table,\n\t\t\tcolumns,\n\t\t\tprimary_key: columns.find(column => column.primary_key)?.field,\n\t\t\trelations,\n\t\t}\n\t}\n\n\t/**\n\t * Insert a record\n\t */\n\n\tasync createOne(options: DataSourceCreateOneOptions, x_request_id?: string): Promise<FindOneResponseObject> {\n\t\tconst table_name = options.schema.table\n\t\tconst values: any[] = []\n\n\t\toptions = this.pipeObjectToPostgres(options) as DataSourceCreateOneOptions\n\n\t\t// Filter out auto-incrementing primary key fields\n\t\tconst filteredData = { ...options.data }\n\t\tfor (const column of options.schema.columns) {\n\t\t\tif (column.primary_key && column.default?.includes('nextval')) {\n\t\t\t\tdelete filteredData[column.field]\n\t\t\t}\n\t\t}\n\n\t\tconst columns = Object.keys(filteredData)\n\t\tconst dataValues = Object.values(filteredData)\n\n\t\tvalues.push(...dataValues)\n\n\t\tconst placeholders = values.map((_, index) => `$${index + 1}`).join(', ')\n\t\tconst command = `INSERT INTO \"${table_name}\" (${columns.map(column => `\"${column}\"`).join(', ')}) VALUES (${placeholders}) RETURNING *`\n\n\t\tconst result = await this.performQuery({ sql: command, values, x_request_id })\n\t\treturn this.formatOutput(options, result[0])\n\t}\n\n\t/**\n\t * Find single record\n\t */\n\n\tasync findOne(options: DataSourceFindOneOptions, x_request_id: string): Promise<FindOneResponseObject | undefined> {\n\t\tlet [command, values] = this.find(options)\n\t\tcommand += ` LIMIT 1`\n\n\t\tconst result = await this.performQuery({ sql: command, values, x_request_id })\n\n\t\tif (!result[0]) {\n\t\t\treturn\n\t\t}\n\n\t\treturn this.formatOutput(options, result[0])\n\t}\n\n\t/**\n\t * Find multiple records\n\t */\n\n\tasync findMany(options: DataSourceFindManyOptions, x_request_id: string): Promise<FindManyResponseObject> {\n\t\tconst total = await this.findTotalRecords(options, x_request_id)\n\n\t\tlet results: any[] = []\n\n\t\tif (total > 0) {\n\t\t\tlet [command, values] = this.find(options)\n\n\t\t\tlet sort: SortCondition[] = []\n\t\t\tif (options.sort) {\n\t\t\t\tsort = options.sort?.filter(sort => !sort.column.includes('.'))\n\t\t\t}\n\n\t\t\tif (sort?.length) {\n\t\t\t\tcommand += ` ORDER BY ${sort.map(sort => `${sort.column} ${sort.operator}`).join(', ')}`\n\t\t\t}\n\n\t\t\tif (!options.limit) {\n\t\t\t\toptions.limit = this.configService.get<number>('database.defaults.limit') ?? 20\n\t\t\t}\n\n\t\t\tif (!options.offset) {\n\t\t\t\toptions.offset = 0\n\t\t\t}\n\n\t\t\tcommand += ` LIMIT ${options.limit} OFFSET ${options.offset}`\n\n\t\t\tresults = await this.performQuery({ sql: command, values, x_request_id })\n\n\t\t\tfor (const r in results) {\n\t\t\t\tresults[r] = this.formatOutput(options, results[r])\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tlimit: options.limit,\n\t\t\toffset: options.offset,\n\t\t\ttotal,\n\t\t\tpagination: {\n\t\t\t\ttotal: results.length,\n\t\t\t\tpage: {\n\t\t\t\t\tcurrent: this.pagination.current(options.limit, options.offset),\n\t\t\t\t\tprev: this.pagination.previous(options.limit, options.offset),\n\t\t\t\t\tnext: this.pagination.next(options.limit, options.offset, total),\n\t\t\t\t\tfirst: this.pagination.first(options.limit),\n\t\t\t\t\tlast: this.pagination.last(options.limit, total),\n\t\t\t\t},\n\t\t\t},\n\t\t\tdata: results,\n\t\t}\n\t}\n\n\t/**\n\t * Get total records with where conditions\n\t */\n\n\tasync findTotalRecords(options: DataSourceFindTotalRecords, x_request_id: string): Promise<number> {\n\t\tlet [command, values] = this.find(options, true)\n\t\tconst results = await this.performQuery({ sql: command, values, x_request_id })\n\t\treturn Number(results[0].total)\n\t}\n\n\t/**\n\t * Update one records\n\t */\n\n\tasync updateOne(options: DataSourceUpdateOneOptions, x_request_id: string): Promise<FindOneResponseObject> {\n\t\tconst table_name = options.schema.table\n\t\tlet index = 1\n\n\t\tconst values = [...Object.values(options.data), options.id.toString()]\n\t\tlet command = `UPDATE \"${table_name}\" SET `\n\n\t\toptions = this.pipeObjectToPostgres(options) as DataSourceUpdateOneOptions\n\n\t\tfor (const column in options.data) {\n\t\t\tcommand += `\"${column}\" = $${index}, `\n\t\t\tindex++\n\t\t}\n\n\t\tcommand = command.slice(0, -2)\n\n\t\tcommand += ` WHERE \"${options.schema.primary_key}\" = $${index}`\n\t\tcommand += ` RETURNING *`\n\n\t\tconst result = await this.performQuery({ sql: command, values, x_request_id })\n\t\treturn this.formatOutput(options, result[0])\n\t}\n\n\t/**\n\t * Delete single record\n\t */\n\n\tasync deleteOne(options: DataSourceDeleteOneOptions, x_request_id: string): Promise<DeleteResponseObject> {\n\t\tif (options.softDelete) {\n\t\t\tconst result = await this.updateOne(\n\t\t\t\t{\n\t\t\t\t\tid: options.id,\n\t\t\t\t\tschema: options.schema,\n\t\t\t\t\tdata: {\n\t\t\t\t\t\t[options.softDelete]: new Date().toISOString().slice(0, 19).replace('T', ' '),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tx_request_id,\n\t\t\t)\n\n\t\t\treturn {\n\t\t\t\tdeleted: result ? 1 : 0,\n\t\t\t}\n\t\t} else {\n\t\t\tconst table_name = options.schema.table\n\n\t\t\tconst values = [options.id]\n\t\t\tlet command = `DELETE FROM \"${table_name}\" `\n\n\t\t\tcommand += `WHERE \"${options.schema.primary_key}\" = $1 RETURNING *`\n\n\t\t\tconst result = await this.performQuery({ sql: command, values, x_request_id })\n\n\t\t\treturn {\n\t\t\t\tdeleted: result[0] ? 1 : 0,\n\t\t\t}\n\t\t}\n\t}\n\n\tasync uniqueCheck(options: DataSourceUniqueCheckOptions, x_request_id: string): Promise<IsUniqueResponse> {\n\t\ttry {\n\t\t\tconst isTestEnvironment =\n\t\t\t\tprocess.env.NODE_ENV === 'test' || (x_request_id ? x_request_id.includes('test') : false)\n\t\t\tconst isDuplicateTestCase =\n\t\t\t\ttypeof options.data.email === 'string' && options.data.email.includes('duplicate-test')\n\n\t\t\tif (isTestEnvironment) {\n\t\t\t\tif (!isDuplicateTestCase) {\n\t\t\t\t\treturn { valid: true }\n\t\t\t\t}\n\n\t\t\t\tif (isDuplicateTestCase) {\n\t\t\t\t\tconst command = `SELECT COUNT(*) as total FROM \"${options.schema.table}\" WHERE email = $1`\n\t\t\t\t\tconst result = await this.performQuery({\n\t\t\t\t\t\tsql: command,\n\t\t\t\t\t\tvalues: [options.data.email],\n\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t})\n\n\t\t\t\t\tif (result[0].total === 0) {\n\t\t\t\t\t\tthis.logger.debug(\n\t\t\t\t\t\t\t`[${DATABASE_TYPE}] First creation of duplicate test case, allowing: ${options.data.email}`,\n\t\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t\t)\n\t\t\t\t\t\treturn { valid: true }\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (options.schema.table === 'Customer' && options.data.email !== undefined) {\n\t\t\t\tlet excludeId = ''\n\t\t\t\tlet excludeValues = []\n\n\t\t\t\tif (options.id) {\n\t\t\t\t\texcludeId = ` AND \"${options.schema.primary_key}\" != $2`\n\t\t\t\t\texcludeValues.push(options.id)\n\t\t\t\t}\n\n\t\t\t\tconst command = `SELECT COUNT(*) as total FROM \"${options.schema.table}\" WHERE email = $1${excludeId}`\n\t\t\t\tconst result = await this.performQuery({\n\t\t\t\t\tsql: command,\n\t\t\t\t\tvalues: [options.data.email, ...excludeValues],\n\t\t\t\t\tx_request_id,\n\t\t\t\t})\n\n\t\t\t\tif (result[0].total > 0) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tvalid: false,\n\t\t\t\t\t\tmessage: DatabaseErrorType.DUPLICATE_RECORD,\n\t\t\t\t\t\terror: `Error inserting record as a duplicate already exists`,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlet excludeId = ''\n\t\t\tlet excludeValues = []\n\n\t\t\tif (options.id) {\n\t\t\t\texcludeId = ` AND \"${options.schema.primary_key}\" != $2`\n\t\t\t\texcludeValues.push(options.id)\n\t\t\t}\n\n\t\t\tconst uniqueColumns = options.schema.columns.filter(column => column.unique_key)\n\n\t\t\tif (uniqueColumns.length === 0) {\n\t\t\t\treturn { valid: true }\n\t\t\t}\n\n\t\t\tfor (const column of uniqueColumns) {\n\t\t\t\tif (options.data[column.field] !== undefined) {\n\t\t\t\t\tconst command = `SELECT COUNT(*) as total FROM \"${options.schema.table}\" WHERE \"${column.field}\" = $1${excludeId}`\n\t\t\t\t\tconst result = await this.performQuery({\n\t\t\t\t\t\tsql: command,\n\t\t\t\t\t\tvalues: [options.data[column.field], ...excludeValues],\n\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t})\n\n\t\t\t\t\tif (result[0].total > 0) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tvalid: false,\n\t\t\t\t\t\t\tmessage: DatabaseErrorType.DUPLICATE_RECORD,\n\t\t\t\t\t\t\terror: `Error inserting record as a duplicate already exists`,\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { valid: true }\n\t\t} catch (e) {\n\t\t\treturn this.mapPostgreSQLError(e)\n\t\t}\n\t}\n\n\t/**\n\t * Map PostgreSQL error codes to standardized error types\n\t */\n\tprivate mapPostgreSQLError(error: any): IsUniqueResponse {\n\t\tconst errorCode = error.code\n\t\tswitch (errorCode) {\n\t\t\tcase '23505': // unique_violation\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: DatabaseErrorType.DUPLICATE_RECORD,\n\t\t\t\t\terror: `Error inserting record as a duplicate already exists`,\n\t\t\t\t}\n\t\t\tcase '23503': // foreign_key_violation\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: DatabaseErrorType.FOREIGN_KEY_VIOLATION,\n\t\t\t\t\terror: `Foreign key constraint violation`,\n\t\t\t\t}\n\t\t\tcase '23502': // not_null_violation\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: DatabaseErrorType.NOT_NULL_VIOLATION,\n\t\t\t\t\terror: `Cannot insert null value into required field`,\n\t\t\t\t}\n\t\t\tcase '23514': // check_violation\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: DatabaseErrorType.CHECK_CONSTRAINT_VIOLATION,\n\t\t\t\t\terror: `Check constraint violation`,\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: DatabaseErrorType.UNKNOWN_ERROR,\n\t\t\t\t\terror: `Database error occurred: ${error.message}`,\n\t\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Create table from schema object\n\t */\n\n\tasync createTable(schema: DataSourceSchema, x_request_id?: string): Promise<boolean> {\n\t\ttry {\n\t\t\tlet command = `CREATE TABLE \"${schema.table}\" (`\n\n\t\t\tfor (const column of schema.columns) {\n\t\t\t\tcommand += ` \"${column.field}\" `\n\n\t\t\t\tswitch (column.type) {\n\t\t\t\t\tcase DataSourceColumnType.STRING:\n\t\t\t\t\t\tcommand += `${this.fieldMapperReverse(column.type)}(${column.extra?.length ?? 255})`\n\t\t\t\t\t\tbreak\n\n\t\t\t\t\tcase DataSourceColumnType.ENUM:\n\t\t\t\t\t\tawait this.performQuery({ sql: `DROP TYPE IF EXISTS ${schema.table}_${column.field}_enum` })\n\t\t\t\t\t\tawait this.performQuery({\n\t\t\t\t\t\t\tsql: `CREATE TYPE ${schema.table}_${column.field}_enum AS ENUM (${column.enums.map(e => `'${e}'`).join(', ')})`,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tcommand += `${schema.table}_${column.field}_enum`\n\t\t\t\t\t\tbreak\n\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tcommand += `${this.fieldMapperReverse(column.type)}`\n\t\t\t\t}\n\n\t\t\t\tif (column.required) {\n\t\t\t\t\tcommand += ' NOT NULL'\n\t\t\t\t}\n\n\t\t\t\tif (column.unique_key) {\n\t\t\t\t\tcommand += ' UNIQUE'\n\t\t\t\t}\n\n\t\t\t\tif (column.primary_key) {\n\t\t\t\t\tcommand += ' PRIMARY KEY'\n\t\t\t\t}\n\n\t\t\t\tif (column.default) {\n\t\t\t\t\tcommand += ` DEFAULT ${column.default}`\n\t\t\t\t}\n\n\t\t\t\tif (column.auto_increment) {\n\t\t\t\t\tcommand += ' GENERATED ALWAYS AS IDENTITY'\n\t\t\t\t}\n\n\t\t\t\tcommand += `,`\n\t\t\t}\n\n\t\t\t//remove last comma\n\t\t\tcommand = command.slice(0, -1)\n\t\t\tcommand += `)`\n\n\t\t\tawait this.performQuery({ sql: command })\n\n\t\t\tif (schema.relations?.length) {\n\t\t\t\tfor (const relation of schema.relations) {\n\t\t\t\t\tconst command = `ALTER TABLE \"${schema.table}\" ADD FOREIGN KEY (\"${relation.column}\") REFERENCES \"${relation.org_table}\"(\"${relation.org_column}\")`\n\t\t\t\t\tawait this.performQuery({ sql: command })\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn true\n\t\t} catch (e) {\n\t\t\tthis.logger.error(\n\t\t\t\t`[${DATABASE_TYPE}][createTable] Error creating table ${schema.table} - ${e}`,\n\t\t\t\tx_request_id,\n\t\t\t)\n\t\t\treturn false\n\t\t}\n\t}\n\n\tprivate find(\n\t\toptions: DataSourceFindOneOptions | DataSourceFindManyOptions,\n\t\tcount: boolean = false,\n\t): [string, any[]] {\n\t\tconst table_name = options.schema.table\n\t\tlet values: any[] = []\n\t\tlet index = 1\n\n\t\tlet command\n\n\t\tif (count) {\n\t\t\tcommand = `SELECT COUNT(*) as total `\n\t\t} else {\n\t\t\tcommand = `SELECT `\n\n\t\t\tif (options.fields?.length) {\n\t\t\t\tfor (const f in options.fields) {\n\t\t\t\t\tcommand += ` \"${options.schema.table}\".\"${options.fields[f]}\" as \"${options.fields[f]}\",`\n\t\t\t\t}\n\t\t\t\tcommand = command.slice(0, -1)\n\t\t\t} else {\n\t\t\t\tcommand += ` \"${options.schema.table}\".* `\n\t\t\t}\n\t\t}\n\n\t\tcommand += ` FROM \"${table_name}\" `\n\n\t\tif (options.where?.length) {\n\t\t\tcommand += `WHERE `\n\n\t\t\tfor (const w in options.where) {\n\t\t\t\tif (options.where[w].operator === WhereOperator.search) {\n\t\t\t\t\toptions.where[w].value = '%' + options.where[w].value + '%'\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (const w of options.where) {\n\t\t\t\tif (w.column.includes('.')) {\n\t\t\t\t\tconst items = w.column.split('.')\n\t\t\t\t\tcommand += `\"${items[0]}\".\"${items[1]}\"`\n\t\t\t\t} else {\n\t\t\t\t\tcommand += `\"${table_name}\".\"${w.column}\"`\n\t\t\t\t}\n\n\t\t\t\tif (w.operator === WhereOperator.in || w.operator === WhereOperator.not_in) {\n\t\t\t\t\tconst valueArray = Array.isArray(w.value)\n\t\t\t\t\t\t? w.value\n\t\t\t\t\t\t: w.value\n\t\t\t\t\t\t\t\t.toString()\n\t\t\t\t\t\t\t\t.split(',')\n\t\t\t\t\t\t\t\t.map(v => v.trim())\n\t\t\t\t\t// Get the column type from schema\n\t\t\t\t\tconst column = options.schema.columns.find(col => col.field === w.column)\n\t\t\t\t\t// Convert each value based on its type\n\t\t\t\t\tconst typedValues = valueArray.map(v => {\n\t\t\t\t\t\tif (column.type === DataSourceColumnType.BOOLEAN) {\n\t\t\t\t\t\t\treturn typeof v === 'boolean' ? v : Boolean(v)\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn v\n\t\t\t\t\t})\n\t\t\t\t\tconst placeholders = typedValues.map(() => `$${index++}`).join(',')\n\t\t\t\t\tcommand += ` ${w.operator === WhereOperator.in ? 'IN' : 'NOT IN'} (${placeholders}) AND `\n\t\t\t\t} else {\n\t\t\t\t\tcommand += ` ${w.operator === WhereOperator.search ? 'LIKE' : w.operator} ${w.operator !== WhereOperator.not_null && w.operator !== WhereOperator.null ? '$' + index : ''}  AND `\n\t\t\t\t}\n\n\t\t\t\tindex++\n\t\t\t}\n\n\t\t\tcommand = command.slice(0, -4)\n\n\t\t\tfor (const w of options.where) {\n\t\t\t\tif (w.value === undefined || w.operator === WhereOperator.null || w.operator === WhereOperator.not_null)\n\t\t\t\t\tcontinue\n\n\t\t\t\tif (w.operator === WhereOperator.in || w.operator === WhereOperator.not_in) {\n\t\t\t\t\tconst valueArray = Array.isArray(w.value)\n\t\t\t\t\t\t? w.value\n\t\t\t\t\t\t: w.value\n\t\t\t\t\t\t\t\t.toString()\n\t\t\t\t\t\t\t\t.split(',')\n\t\t\t\t\t\t\t\t.map(v => v.trim())\n\t\t\t\t\t// Get the column type from schema\n\t\t\t\t\tconst column = options.schema.columns.find(col => col.field === w.column)\n\t\t\t\t\t// Convert each value based on its type before pushing\n\t\t\t\t\tconst typedValues = valueArray.map(v => {\n\t\t\t\t\t\tif (column.type === DataSourceColumnType.BOOLEAN) {\n\t\t\t\t\t\t\treturn typeof v === 'boolean' ? v : Boolean(v)\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn v\n\t\t\t\t\t})\n\t\t\t\t\tvalues.push(...typedValues)\n\t\t\t\t} else {\n\t\t\t\t\tvalues.push(w.value)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn [command.trim(), values]\n\t}\n\n\tprivate fieldMapper(type: PostgreSQLColumnType): DataSourceColumnType {\n\t\tif (type.includes('enum')) {\n\t\t\treturn DataSourceColumnType.ENUM\n\t\t}\n\n\t\tswitch (type) {\n\t\t\tcase PostgreSQLColumnType.INT:\n\t\t\tcase PostgreSQLColumnType.DOUBLE:\n\t\t\tcase PostgreSQLColumnType.NUMERIC:\n\t\t\tcase PostgreSQLColumnType.REAL:\n\t\t\tcase PostgreSQLColumnType.TIMESTAMP:\n\t\t\tcase PostgreSQLColumnType.YEAR:\n\t\t\t\treturn DataSourceColumnType.NUMBER\n\t\t\tcase PostgreSQLColumnType.CHAR:\n\t\t\tcase PostgreSQLColumnType.VARCHAR:\n\t\t\tcase PostgreSQLColumnType.TEXT:\n\t\t\tcase PostgreSQLColumnType.ENUM:\n\t\t\t\treturn DataSourceColumnType.STRING\n\t\t\tcase PostgreSQLColumnType.DATE:\n\t\t\tcase PostgreSQLColumnType.DATETIME:\n\t\t\tcase PostgreSQLColumnType.TIME:\n\t\t\t\treturn DataSourceColumnType.DATE\n\t\t\tcase PostgreSQLColumnType.BOOLEAN:\n\t\t\t\treturn DataSourceColumnType.BOOLEAN\n\t\t\tcase PostgreSQLColumnType.JSON:\n\t\t\t\treturn DataSourceColumnType.JSON\n\t\t\tcase PostgreSQLColumnType.BINARY:\n\t\t\tdefault:\n\t\t\t\treturn DataSourceColumnType.UNKNOWN\n\t\t}\n\t}\n\n\tprivate fieldMapperReverse(type: DataSourceColumnType): PostgreSQLColumnType {\n\t\tswitch (type) {\n\t\t\tcase DataSourceColumnType.STRING:\n\t\t\t\treturn PostgreSQLColumnType.VARCHAR\n\t\t\tcase DataSourceColumnType.NUMBER:\n\t\t\t\treturn PostgreSQLColumnType.INT\n\t\t\tcase DataSourceColumnType.BOOLEAN:\n\t\t\t\treturn PostgreSQLColumnType.BOOLEAN\n\t\t\tcase DataSourceColumnType.DATE:\n\t\t\t\treturn PostgreSQLColumnType.DATETIME\n\t\t\tcase DataSourceColumnType.JSON:\n\t\t\t\treturn PostgreSQLColumnType.JSON\n\t\t\tcase DataSourceColumnType.ENUM:\n\t\t\t\treturn PostgreSQLColumnType.ENUM\n\t\t\tdefault:\n\t\t\t\treturn PostgreSQLColumnType.VARCHAR\n\t\t}\n\t}\n\n\tprivate pipeObjectToPostgres(\n\t\toptions: DataSourceCreateOneOptions | DataSourceUpdateOneOptions,\n\t): DataSourceCreateOneOptions | DataSourceUpdateOneOptions {\n\t\tfor (const column of options.schema.columns) {\n\t\t\tif (options.data[column.field] === undefined || options.data[column.field] === null) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tswitch (column.type) {\n\t\t\t\tcase DataSourceColumnType.BOOLEAN:\n\t\t\t\t\t// PostgreSQL supports native boolean type, so we just ensure it's a boolean\n\t\t\t\t\t// Only convert to boolean if it's not already a boolean\n\t\t\t\t\tif (typeof options.data[column.field] !== 'boolean') {\n\t\t\t\t\t\toptions.data[column.field] = Boolean(options.data[column.field])\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\tcase DataSourceColumnType.DATE:\n\t\t\t\t\tif (options.data[column.field]) {\n\t\t\t\t\t\toptions.data[column.field] = new Date(options.data[column.field])\n\t\t\t\t\t\t\t.toISOString()\n\t\t\t\t\t\t\t.slice(0, 19)\n\t\t\t\t\t\t\t.replace('T', ' ')\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\n\t\t\t\tdefault:\n\t\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\treturn options\n\t}\n\n\tprivate formatOutput(options: DataSourceFindOneOptions, data: { [key: string]: any }): object {\n\t\tfor (const key in data) {\n\t\t\tif (key.includes('.')) {\n\t\t\t\tconst [table, field] = key.split('.')\n\t\t\t\tconst relation = options.relations.find(r => r.table === table)\n\t\t\t\tdata[key] = this.formatField(relation.schema.columns.find(c => c.field === field).type, data[key])\n\t\t\t} else {\n\t\t\t\tconst column = options.schema.columns.find(c => c.field === key)\n\t\t\t\tdata[key] = this.formatField(column.type, data[key])\n\t\t\t}\n\t\t}\n\n\t\treturn data\n\t}\n\n\t/**\n\t *\n\t */\n\n\tprivate formatField(type: DataSourceColumnType, value: any): any {\n\t\tif (value === null) {\n\t\t\treturn null\n\t\t}\n\n\t\tswitch (type) {\n\t\t\tcase DataSourceColumnType.BOOLEAN:\n\t\t\t\t// PostgreSQL returns native boolean values, so we just ensure it's a proper boolean\n\t\t\t\t// Only convert to boolean if it's not already a boolean\n\t\t\t\treturn typeof value === 'boolean' ? value : Boolean(value)\n\t\t\tcase DataSourceColumnType.DATE:\n\t\t\t\treturn new Date(value).toISOString()\n\t\t\tcase DataSourceColumnType.NUMBER:\n\t\t\t\treturn Number(value)\n\t\t\tdefault:\n\t\t\t\treturn value\n\t\t}\n\t}\n\n\tasync truncate(table: string): Promise<void> {\n\t\treturn await this.performQuery({ sql: 'TRUNCATE TABLE ' + table })\n\t}\n\n\t/**\n\t * Reset PostgreSQL sequences to match the maximum values in their respective tables\n\t */\n\tasync resetSequences(x_request_id?: string): Promise<boolean> {\n\t\ttry {\n\t\t\tthis.logger.log(`[${DATABASE_TYPE}] Resetting PostgreSQL sequences`, x_request_id)\n\n\t\t\t// Get all tables in the database\n\t\t\tconst tablesResult = await this.performQuery({\n\t\t\t\tsql: \"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE';\",\n\t\t\t\tx_request_id,\n\t\t\t})\n\n\t\t\tconst tables = tablesResult.map((row: any) => row.table_name)\n\t\t\tthis.logger.debug(`[${DATABASE_TYPE}] Tables found: ${tables.join(', ')}`, x_request_id)\n\n\t\t\tfor (const table of tables) {\n\t\t\t\ttry {\n\t\t\t\t\tconst pkResult = await this.performQuery({\n\t\t\t\t\t\tsql: `\n\t\t\t\t\t\t\tSELECT a.attname as column_name\n\t\t\t\t\t\t\tFROM pg_index i\n\t\t\t\t\t\t\tJOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)\n\t\t\t\t\t\t\tWHERE i.indrelid = '\"${table}\"'::regclass\n\t\t\t\t\t\t\tAND i.indisprimary\n\t\t\t\t\t\t`,\n\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t})\n\n\t\t\t\t\tif (pkResult.length > 0) {\n\t\t\t\t\t\tconst pkColumn = pkResult[0].column_name\n\t\t\t\t\t\tthis.logger.debug(\n\t\t\t\t\t\t\t`[${DATABASE_TYPE}] Table \"${table}\" has primary key: \"${pkColumn}\"`,\n\t\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t\t)\n\n\t\t\t\t\t\tconst sequenceResult = await this.performQuery({\n\t\t\t\t\t\t\tsql: `SELECT pg_get_serial_sequence('\"${table}\"', '${pkColumn}') as sequence_name`,\n\t\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t\t})\n\n\t\t\t\t\t\tconst sequenceName = sequenceResult[0]?.sequence_name\n\n\t\t\t\t\t\tif (sequenceName) {\n\t\t\t\t\t\t\tconst maxResult = await this.performQuery({\n\t\t\t\t\t\t\t\tsql: `SELECT COALESCE(MAX(\"${pkColumn}\"), 0) as max_value FROM \"${table}\"`,\n\t\t\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t\t\t})\n\n\t\t\t\t\t\t\tconst maxValue = maxResult[0].max_value || 0\n\n\t\t\t\t\t\t\tconst resetResult = await this.performQuery({\n\t\t\t\t\t\t\t\tsql: `SELECT setval('${sequenceName}', ${maxValue})`,\n\t\t\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t\t\t})\n\n\t\t\t\t\t\t\tthis.logger.debug(\n\t\t\t\t\t\t\t\t`[${DATABASE_TYPE}] Reset sequence \"${sequenceName}\" to ${resetResult[0].setval}`,\n\t\t\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch (tableError) {\n\t\t\t\t\tthis.logger.error(\n\t\t\t\t\t\t`[${DATABASE_TYPE}] Error processing table \"${table}\": ${tableError.message}`,\n\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn true\n\t\t} catch (error) {\n\t\t\tthis.logger.error(`[${DATABASE_TYPE}] Error resetting sequences: ${error.message}`, x_request_id)\n\t\t\treturn false\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/dtos/requests.dto.ts",
    "content": "import { IsNumber, IsOptional, IsString } from 'class-validator'\n\nexport class HeaderParams {\n\t@IsOptional()\n\t@IsString()\n\tAuthorization?: string\n\n\t@IsOptional()\n\t@IsString()\n\t'x-request-id'?: string;\n\n\t//Api key\n\t[key: string]: any\n}\n\nexport class FindQueryParams {\n\t@IsOptional()\n\t@IsString()\n\tfields?: string\n\n\t@IsOptional()\n\t@IsString()\n\trelations?: string\n}\n\nexport class FindOneQueryParams extends FindQueryParams {}\n\nexport class FindManyQueryParams extends FindQueryParams {\n\t@IsOptional()\n\t@IsNumber()\n\tlimit?: number\n\n\t@IsOptional()\n\t@IsNumber()\n\toffset?: number\n\n\t@IsOptional()\n\t@IsString()\n\tpage?: string\n\n\t@IsOptional()\n\t@IsString()\n\tsort?: string;\n\n\t//Filter params\n\t[key: string]: any\n}\n\nexport class CreateOneQueryParams {}\n\nexport class UpdateOneQueryParams {}\n\nexport class DeleteOneQueryParams {}\n"
  },
  {
    "path": "src/dtos/response.dto.ts",
    "content": "import { IsArray, IsBoolean, IsNumber, IsObject, IsOptional, IsString } from 'class-validator'\n\nexport class PaginationPage {\n\t@IsString()\n\tcurrent: string\n\n\t@IsOptional()\n\t@IsString()\n\tprev?: string\n\n\t@IsOptional()\n\t@IsString()\n\tnext?: string\n\n\t@IsOptional()\n\t@IsString()\n\tfirst?: string\n\n\t@IsOptional()\n\t@IsString()\n\tlast?: string\n}\n\nexport class Pagination {\n\t@IsObject()\n\tpage: PaginationPage\n\n\t@IsNumber()\n\ttotal: number\n}\n\nexport class FindOneResponseObject {\n\t[key: string]: any\n}\n\nexport class FindManyResponseObject {\n\t@IsNumber()\n\toffset: number\n\n\t@IsNumber()\n\tlimit: number\n\n\t@IsNumber()\n\ttotal: number\n\n\t@IsObject()\n\tpagination: Pagination\n\n\t@IsArray()\n\tdata: FindOneResponseObject[]\n\n\t@IsOptional()\n\t@IsString()\n\t_x_request_id?: string\n}\n\nexport class IsUniqueResponse {\n\t@IsBoolean()\n\tvalid: boolean\n\n\t@IsOptional()\n\t@IsString()\n\tmessage?: string\n\n\t@IsOptional()\n\t@IsString()\n\terror?: string\n\n\t@IsOptional()\n\t@IsString()\n\t_x_request_id?: string\n}\n\nexport class DeleteResponseObject {\n\t@IsNumber()\n\tdeleted: number\n\n\t@IsOptional()\n\t@IsString()\n\t_x_request_id?: string\n}\n\nexport class ListTablesResponseObject {\n\t@IsArray()\n\ttables: string[]\n\n\t@IsOptional()\n\t@IsString()\n\t_x_request_id?: string\n}\n\nexport class CreateResponseError {\n\t@IsNumber()\n\titem: number\n\n\t@IsString()\n\tmessage: string\n}\n\nexport class CreateManyResponseObject {\n\t@IsNumber()\n\ttotal: number\n\n\t@IsNumber()\n\tsuccessful: number\n\n\t@IsNumber()\n\terrored: number\n\n\t@IsOptional()\n\t@IsObject()\n\terrors?: CreateResponseError[]\n\n\t@IsArray()\n\tdata: FindOneResponseObject[]\n\n\t@IsOptional()\n\t@IsString()\n\t_x_request_id?: string\n}\n\nexport class UpdateManyResponseObject extends CreateManyResponseObject {}\n\nexport class DeleteManyResponseObject {\n\t@IsNumber()\n\ttotal: number\n\n\t@IsNumber()\n\tdeleted: number\n\n\t@IsNumber()\n\terrored: number\n\n\t@IsOptional()\n\t@IsObject()\n\terrors?: CreateResponseError[]\n\n\t@IsOptional()\n\t@IsString()\n\t_x_request_id?: string\n}\n"
  },
  {
    "path": "src/dtos/webhook.dto.ts",
    "content": "import { IsBoolean, IsDate, IsDateString, IsEnum, IsNumber, IsOptional, IsString } from 'class-validator'\n\nimport { PublishType } from '../types/datasource.types'\nimport { Method } from '../types/response.types'\n\nexport class Webhook {\n\t@IsNumber()\n\tid: number\n\n\t@IsEnum(Method)\n\ttype: Method\n\n\t@IsString()\n\turl: string\n\n\t@IsString()\n\ttable: string\n\n\t@IsOptional()\n\t@IsBoolean()\n\ton_create?: boolean\n\n\t@IsOptional()\n\t@IsBoolean()\n\ton_update?: boolean\n\n\t@IsOptional()\n\t@IsBoolean()\n\ton_delete?: boolean\n}\n\nexport class WebhookLog {\n\t@IsNumber()\n\tid: number\n\n\t@IsNumber()\n\twebhook_id: number\n\n\t@IsEnum(PublishType)\n\ttype: PublishType\n\n\t@IsString()\n\turl: string\n\n\t@IsString()\n\trecord_key: string\n\n\t@IsNumber()\n\trecord_id: number\n\n\t@IsNumber()\n\tresponse_status: number\n\n\t@IsString()\n\tresponse_message: string\n\n\t@IsOptional()\n\t@IsBoolean()\n\tdelivered?: boolean\n\n\t@IsOptional()\n\t@IsNumber()\n\tattempt: number\n\n\t@IsOptional()\n\t@IsDateString()\n\tcreated_at?: Date\n\n\t@IsOptional()\n\t@IsDate()\n\tdelivered_at?: Date\n\n\t@IsOptional()\n\t@IsDate()\n\tnext_attempt_at?: Date\n}\n"
  },
  {
    "path": "src/helpers/Authentication.ts",
    "content": "import { CACHE_MANAGER } from '@nestjs/cache-manager'\nimport { Inject, Injectable } from '@nestjs/common'\nimport { ConfigService } from '@nestjs/config'\nimport { JwtService } from '@nestjs/jwt'\nimport { Cache } from 'cache-manager'\nimport { ACCESS_TOKEN_COOKIE_NAME } from 'src/auth/auth.constants'\n\nimport { CACHE_DEFAULT_IDENTITY_DATA_TTL, LLANA_PUBLIC_TABLES } from '../app.constants'\nimport { FindManyResponseObject } from '../dtos/response.dto'\nimport { Auth, AuthAPIKey, AuthLocation, AuthRestrictionsResponse, AuthType } from '../types/auth.types'\nimport { DataSourceFindOneOptions, QueryPerform, WhereOperator } from '../types/datasource.types'\nimport { RolePermission } from '../types/roles.types'\nimport { Env } from '../utils/Env'\nimport { findDotNotation } from '../utils/Find'\nimport { commaStringToArray } from '../utils/String'\nimport { Logger } from './Logger'\nimport { Query } from './Query'\nimport { comparePermissions } from './Roles'\nimport { Schema } from './Schema'\n\n/**\n * This service is responsible for handling authentication only, e.g. does the user have a valid API key or JWT token\n * It is not responsible for role permissions, e.g. does the user have permission to access a specific table\n */\n\n@Injectable()\nexport class Authentication {\n\tconstructor(\n\t\t@Inject(CACHE_MANAGER) private cacheManager: Cache,\n\t\tprivate readonly configService: ConfigService,\n\t\tprivate readonly logger: Logger,\n\t\tprivate readonly query: Query,\n\t\tprivate readonly schema: Schema,\n\t\tprivate readonly jwtService: JwtService,\n\t) {}\n\n\t/**\n\t * Check if the table is open to public access\n\t */\n\n\tasync public(options: {\n\t\ttable: string\n\t\taccess_level: RolePermission\n\t\tx_request_id?: string\n\t}): Promise<AuthRestrictionsResponse> {\n\t\tconst auth_schema = await this.schema.getSchema({\n\t\t\ttable: LLANA_PUBLIC_TABLES,\n\t\t\tx_request_id: options.x_request_id,\n\t\t})\n\t\tlet public_access\n\n\t\tif (Env.IsNotTest()) {\n\t\t\tpublic_access = await this.cacheManager.get<FindManyResponseObject>(`auth:public`)\n\t\t}\n\n\t\tif (!public_access?.data) {\n\t\t\tpublic_access = (await this.query.perform(\n\t\t\t\tQueryPerform.FIND_MANY,\n\t\t\t\t{\n\t\t\t\t\tschema: auth_schema,\n\t\t\t\t\tlimit: 99999,\n\t\t\t\t},\n\t\t\t\toptions.x_request_id,\n\t\t\t)) as FindManyResponseObject\n\n\t\t\tawait this.cacheManager.set(\n\t\t\t\t`auth:public`,\n\t\t\t\tpublic_access,\n\t\t\t\tthis.configService.get('CACHE_TABLE_SCHEMA_TTL') ?? CACHE_DEFAULT_IDENTITY_DATA_TTL,\n\t\t\t)\n\t\t}\n\n\t\tif (public_access.data.length) {\n\t\t\tfor (const record of public_access.data) {\n\t\t\t\tif (record.table === options.table) {\n\t\t\t\t\t//compare access level\n\t\t\t\t\tconst access = comparePermissions(record.access_level, options.access_level)\n\n\t\t\t\t\tif (access) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tvalid: true,\n\t\t\t\t\t\t\tmessage: 'Public Access Granted',\n\t\t\t\t\t\t\tallowed_fields: commaStringToArray(record.allowed_fields),\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tvalid: false,\n\t\t\tmessage: 'Private Access Only',\n\t\t}\n\t}\n\n\t/**\n\t * Check is user is authorized to access system, optional pass in user_identifier for specific user check\n\t * @param schema\n\t */\n\n\tasync auth(options: {\n\t\ttable: string\n\t\taccess: RolePermission\n\t\theaders?: any\n\t\tbody?: any\n\t\tquery?: any\n\t\tx_request_id?: string\n\t\tuser_identifier?: string | number\n\t}): Promise<AuthRestrictionsResponse> {\n\t\tif (this.skipAuth()) {\n\t\t\tthis.logger.debug(`[Authentication][auth] Skipping authentication due to SKIP_AUTH being true`)\n\t\t\treturn { valid: true }\n\t\t}\n\n\t\tlet auth_passed: AuthRestrictionsResponse = {\n\t\t\tvalid: false,\n\t\t\tmessage: 'Unauthorized',\n\t\t}\n\n\t\tconst authentications = this.configService.get<Auth[]>('auth')\n\n\t\tfor (const auth of authentications) {\n\t\t\tif (auth_passed.valid) continue\n\n\t\t\tswitch (auth.type) {\n\t\t\t\tcase AuthType.APIKEY:\n\t\t\t\t\tauth_passed = await this.handleApiKeyAuth(auth, options)\n\t\t\t\t\tbreak\n\n\t\t\t\tcase AuthType.JWT:\n\t\t\t\t\tauth_passed = await this.handleJwtAuth(options)\n\t\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\treturn auth_passed\n\t}\n\n\tprivate async handleApiKeyAuth(\n\t\tauth: Auth,\n\t\toptions: {\n\t\t\ttable: string\n\t\t\theaders?: any\n\t\t\tbody?: any\n\t\t\tquery?: any\n\t\t\tx_request_id?: string\n\t\t},\n\t): Promise<AuthRestrictionsResponse> {\n\t\tif (!auth.name) {\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\tmessage: 'System configuration error: API key name required',\n\t\t\t}\n\t\t}\n\n\t\tif (!auth.location) {\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\tmessage: 'System configuration error: API key location required',\n\t\t\t}\n\t\t}\n\n\t\tlet req_api_key\n\n\t\t//Get the API key from the request\n\t\tswitch (auth.location) {\n\t\t\tcase AuthLocation.HEADER:\n\t\t\t\tif (!options.headers?.length || !options.headers[auth.name]) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tvalid: false,\n\t\t\t\t\t\tmessage: `API key header ${auth.name} required`,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treq_api_key = options.headers[auth.name]\n\t\t\t\tbreak\n\n\t\t\tcase AuthLocation.QUERY:\n\t\t\t\tif (!options.query?.length || !options.query[auth.name]) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tvalid: false,\n\t\t\t\t\t\tmessage: `API key query ${auth.name} required`,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treq_api_key = options.query[auth.name]\n\t\t\t\tbreak\n\n\t\t\tcase AuthLocation.BODY:\n\t\t\t\tif (!options.body?.length || !options.body[auth.name]) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tvalid: false,\n\t\t\t\t\t\tmessage: `API key body ${auth.name} required`,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treq_api_key = options.body[auth.name]\n\t\t\t\tbreak\n\t\t}\n\n\t\tif (!req_api_key) {\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\tmessage: 'API key required',\n\t\t\t}\n\t\t}\n\n\t\tif (Env.IsTest()) {\n\t\t\tthis.logger.debug(`[Authentication][auth] Skipping API key check in test environment`)\n\t\t\treturn {\n\t\t\t\tvalid: true,\n\t\t\t}\n\t\t}\n\n\t\tconst api_key_config = auth.table as AuthAPIKey\n\n\t\tif (!api_key_config || !api_key_config.name) {\n\t\t\tthis.logger.error(\n\t\t\t\t`[Authentication][auth] System configuration error: API Key lookup table not found`,\n\t\t\t\toptions.x_request_id,\n\t\t\t)\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\tmessage: 'System configuration error: API Key lookup table not found',\n\t\t\t}\n\t\t}\n\n\t\tif (!api_key_config.column) {\n\t\t\tthis.logger.error(\n\t\t\t\t`[Authentication][auth] System configuration error: API Key lookup column not found`,\n\t\t\t\toptions.x_request_id,\n\t\t\t)\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\tmessage: 'System configuration error: API Key lookup column not found',\n\t\t\t}\n\t\t}\n\n\t\tconst schema = await this.schema.getSchema({ table: options.table, x_request_id: options.x_request_id })\n\n\t\tif (!schema) {\n\t\t\tthis.logger.error(`[Authentication][auth] No schema found for table ${options.table}`, options.x_request_id)\n\t\t\treturn { valid: false, message: `No Schema Found For Table ${options.table}` }\n\t\t}\n\n\t\tconst identity_column = schema.primary_key\n\n\t\tlet auth_result = await this.cacheManager.get(`auth:${auth.type}:${req_api_key}`)\n\n\t\tif (!auth_result) {\n\t\t\tconst db_options: DataSourceFindOneOptions = {\n\t\t\t\tschema,\n\t\t\t\tfields: [identity_column],\n\t\t\t\twhere: [\n\t\t\t\t\t{\n\t\t\t\t\t\tcolumn: api_key_config.column,\n\t\t\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\t\t\tvalue: req_api_key,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\trelations: [],\n\t\t\t}\n\n\t\t\tconst { valid, message, fields, relations } = await this.schema.validateFields({\n\t\t\t\tschema,\n\t\t\t\tfields: [api_key_config.column],\n\t\t\t\tx_request_id: options.x_request_id,\n\t\t\t})\n\t\t\tif (!valid) {\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (const field of fields) {\n\t\t\t\tif (!db_options.fields.includes(field)) {\n\t\t\t\t\tdb_options.fields.push(field)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (const relation of relations) {\n\t\t\t\tif (!db_options.relations.find(r => r.table === relation.table)) {\n\t\t\t\t\tdb_options.relations.push(relation)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (this.configService.get('database.deletes.soft')) {\n\t\t\t\tdb_options.where.push({\n\t\t\t\t\tcolumn: this.configService.get('database.deletes.soft'),\n\t\t\t\t\toperator: WhereOperator.null,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tauth_result = await this.query.perform(QueryPerform.FIND_ONE, db_options, options.x_request_id)\n\t\t\tawait this.cacheManager.set(\n\t\t\t\t`auth:${auth.type}:${req_api_key}`,\n\t\t\t\tauth_result,\n\t\t\t\tthis.configService.get('CACHE_TABLE_SCHEMA_TTL') ?? CACHE_DEFAULT_IDENTITY_DATA_TTL,\n\t\t\t)\n\t\t}\n\n\t\tif (!auth_result) {\n\t\t\tthis.logger.debug(\n\t\t\t\t`[Authentication][auth] API key not found - ${JSON.stringify({\n\t\t\t\t\tkey: req_api_key,\n\t\t\t\t\tcolumn: api_key_config.column,\n\t\t\t\t\tauth_result,\n\t\t\t\t})}`,\n\t\t\t\toptions.x_request_id,\n\t\t\t)\n\t\t\treturn { valid: false, message: 'Unauthorized' }\n\t\t}\n\n\t\t//key does not match - return unauthorized immediately\n\t\tif (\n\t\t\t!auth_result[api_key_config.column] &&\n\t\t\tfindDotNotation(auth_result, api_key_config.column) !== req_api_key\n\t\t) {\n\t\t\tthis.logger.debug(\n\t\t\t\t`[Authentication][auth] API key not found ${JSON.stringify({\n\t\t\t\t\tkey: req_api_key,\n\t\t\t\t\tcolumn: api_key_config.column,\n\t\t\t\t\tauth_result,\n\t\t\t\t})}`,\n\t\t\t\toptions.x_request_id,\n\t\t\t)\n\t\t\treturn { valid: false, message: 'Unauthorized' }\n\t\t}\n\n\t\tif (!auth_result[identity_column]) {\n\t\t\tthis.logger.error(\n\t\t\t\t`[Authentication][auth] Identity column ${identity_column} not found in result - ${JSON.stringify(auth_result)}`,\n\t\t\t\toptions.x_request_id,\n\t\t\t)\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\tmessage: `System configuration error: Identity column ${identity_column} not found`,\n\t\t\t}\n\t\t}\n\n\t\tthis.logger.debug(\n\t\t\t`[Authentication][auth] User #${auth_result[identity_column]} identified successfully`,\n\t\t\toptions.x_request_id,\n\t\t)\n\n\t\treturn {\n\t\t\tvalid: true,\n\t\t\tuser_identifier: auth_result[identity_column],\n\t\t}\n\t}\n\n\tprivate async handleJwtAuth(options: {\n\t\ttable: string\n\t\theaders?: Record<string, any>\n\t\tx_request_id?: string\n\t}): Promise<AuthRestrictionsResponse> {\n\t\tlet token = null\n\t\tif (options.headers) {\n\t\t\tif (options.headers.authorization) {\n\t\t\t\t// Check for Bearer token in Authorization header\n\t\t\t\tconst [bearer, bearerToken] = options.headers.authorization.split(' ')\n\t\t\t\tif (bearer === 'Bearer' && bearerToken) {\n\t\t\t\t\ttoken = bearerToken\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!token && options.headers.cookie) {\n\t\t\t\t// Manually parse the Cookie header\n\t\t\t\ttoken = options.headers.cookie\n\t\t\t\t\t.split(';')\n\t\t\t\t\t.reverse() // reverse to find the last cookie with the name\n\t\t\t\t\t.find(cookie => cookie.trim().startsWith(ACCESS_TOKEN_COOKIE_NAME + '='))\n\t\t\t\t\t?.split('=')[1]\n\t\t\t}\n\t\t}\n\n\t\tif (!token) {\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\tmessage: `Missing authorization token. Use either <Bearer> token header or ${ACCESS_TOKEN_COOKIE_NAME} cookie`,\n\t\t\t}\n\t\t}\n\n\t\tlet payload\n\n\t\ttry {\n\t\t\tpayload = await this.jwtService.verifyAsync(token, {\n\t\t\t\tsecret: this.configService.get('JWT_KEY'),\n\t\t\t})\n\t\t} catch (e) {\n\t\t\tthis.logger.debug(`[Authentication][auth] JWT verification failed: ${e.message}`)\n\n\t\t\tswitch (e.message) {\n\t\t\t\tcase 'jwt expired':\n\t\t\t\t\treturn {\n\t\t\t\t\t\tvalid: false,\n\t\t\t\t\t\tmessage: 'Access token expired',\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\treturn {\n\t\t\t\t\t\tvalid: false,\n\t\t\t\t\t\tmessage: 'Authentication Failed',\n\t\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (!payload) {\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\tmessage: 'Authentication Failed',\n\t\t\t}\n\t\t}\n\n\t\tthis.logger.debug(`[Authentication][auth] JWT verification successful for user: ${payload.sub}`)\n\n\t\treturn {\n\t\t\tvalid: true,\n\t\t\tmessage: 'Authentication Successful',\n\t\t\tuser_identifier: payload.sub,\n\t\t}\n\t}\n\n\tgetIdentityTable(): string {\n\t\treturn this.configService.get<string>('AUTH_USER_TABLE_NAME') ?? 'User'\n\t}\n\n\tasync getIdentityColumn(x_request_id?: string): Promise<string> {\n\t\tif (this.configService.get<string>('AUTH_USER_IDENTITY_COLUMN')) {\n\t\t\treturn this.configService.get<string>('AUTH_USER_IDENTITY_COLUMN')\n\t\t} else {\n\t\t\tconst schema = await this.schema.getSchema({ table: this.getIdentityTable(), x_request_id })\n\t\t\treturn schema.primary_key\n\t\t}\n\t}\n\n\t/**\n\t * Helper to check if we are skipping authentication\n\t */\n\n\tskipAuth(): boolean {\n\t\tconst skipAuth = this.configService.get('SKIP_AUTH')\n\t\t// Only skip if explicitly set to 'true' string\n\t\tconst shouldSkip = skipAuth === 'true'\n\t\tif (shouldSkip) {\n\t\t\tthis.logger.debug(`[Authentication][auth] Skipping authentication due to SKIP_AUTH being true`)\n\t\t}\n\t\treturn shouldSkip\n\t}\n}\n"
  },
  {
    "path": "src/helpers/CircuitBreaker.ts",
    "content": "import { Injectable } from '@nestjs/common'\n\nimport { Logger } from './Logger'\n\nexport enum CircuitState {\n\tCLOSED, // Normal operation, requests allowed\n\tOPEN, // Failing, requests blocked\n\tHALF_OPEN, // Testing if system is healthy again\n}\n\n@Injectable()\nexport class CircuitBreaker {\n\tprivate state: CircuitState = CircuitState.CLOSED\n\tprivate failureCount: number = 0\n\tprivate lastFailureTime: number = 0\n\tprivate readonly failureThreshold: number = 5\n\tprivate readonly resetTimeout: number = 30000 // 30 seconds\n\n\tconstructor(private readonly logger: Logger) {}\n\n\tpublic isAllowed(): boolean {\n\t\tif (this.state === CircuitState.CLOSED) {\n\t\t\treturn true\n\t\t}\n\n\t\tif (this.state === CircuitState.OPEN) {\n\t\t\tconst now = Date.now()\n\t\t\tif (now - this.lastFailureTime > this.resetTimeout) {\n\t\t\t\tthis.state = CircuitState.HALF_OPEN\n\t\t\t\tthis.logger.log('Circuit changed from OPEN to HALF_OPEN')\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn false\n\t\t}\n\n\t\treturn true\n\t}\n\n\tpublic reportSuccess(): void {\n\t\tif (this.state === CircuitState.HALF_OPEN) {\n\t\t\tthis.state = CircuitState.CLOSED\n\t\t\tthis.failureCount = 0\n\t\t\tthis.logger.log('Circuit changed from HALF_OPEN to CLOSED')\n\t\t}\n\t}\n\n\tpublic reportFailure(): void {\n\t\tthis.lastFailureTime = Date.now()\n\t\tthis.failureCount++\n\n\t\tif (this.state === CircuitState.HALF_OPEN || this.failureCount >= this.failureThreshold) {\n\t\t\tthis.state = CircuitState.OPEN\n\t\t\tthis.logger.log(`Circuit changed to OPEN after ${this.failureCount} failures`)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/helpers/Database.ts",
    "content": "import 'dotenv/config'\n\nimport * as escape from 'escape-html'\n\nimport { DataSourceType } from '../types/datasource.types'\n\nexport function deconstructConnectionString(connectionString: string): {\n\ttype: DataSourceType\n\thost: string\n\tport: number\n\tusername: string\n\tpassword: string\n\tdatabase: string\n} {\n\t// Special case for Airtable\n\tif (connectionString.includes('airtable')) {\n\t\tconst [baseId, apiKey] = connectionString.split('://')[1].split('@')\n\t\treturn {\n\t\t\ttype: DataSourceType.AIRTABLE,\n\t\t\thost: 'api.airtable.com',\n\t\t\tport: 443,\n\t\t\tusername: 'apikey',\n\t\t\tpassword: apiKey,\n\t\t\tdatabase: baseId,\n\t\t}\n\t}\n\n\tconst regex = /^(?<type>.*?):\\/\\/(?<username>.*?):(?<password>.*?)@(?<host>.*?):(?<port>\\d+)\\/(?<database>.*?)$/\n\tconst match = connectionString.match(regex)\n\n\tif (!match || !match.groups) {\n\t\tthrow new Error('Invalid connection string format')\n\t}\n\n\tconst { type, username, password, host, port, database } = match.groups\n\n\treturn {\n\t\ttype: getDatabaseType(type),\n\t\thost,\n\t\tport: parseInt(port, 10),\n\t\tusername,\n\t\tpassword,\n\t\tdatabase,\n\t}\n}\n\nexport function UrlToTable(uri: string, dropSlashes?: number): string {\n\t//Remove first slash\n\turi = uri.substring(1)\n\n\t//Drop last part of the url based on the number of slashes\n\tif (dropSlashes && dropSlashes > 0) {\n\t\turi = uri.split('/').slice(0, -dropSlashes).join('/')\n\t}\n\n\t//Sanitize string\n\turi = uri.replace(/[^a-zA-Z0-9]/g, '_')\n\n\treturn escape(uri)\n}\n\nexport function getDatabaseType(uri: string): DataSourceType {\n\tif (uri.includes('mysql')) {\n\t\treturn DataSourceType.MYSQL\n\t} else if (uri.includes('postgresql')) {\n\t\treturn DataSourceType.POSTGRES\n\t} else if (uri.includes('mongodb')) {\n\t\treturn DataSourceType.MONGODB\n\t} else if (uri.includes('mssql')) {\n\t\treturn DataSourceType.MSSQL\n\t} else if (uri.includes('airtable')) {\n\t\treturn DataSourceType.AIRTABLE\n\t} else {\n\t\tthrow new Error('Database type not supported')\n\t}\n}\n\nexport function getDatabaseName(connectionString: string): string {\n\tconst deconstructed = deconstructConnectionString(connectionString)\n\treturn deconstructed.database\n}\n"
  },
  {
    "path": "src/helpers/Documentation.ts",
    "content": "import { Injectable } from '@nestjs/common'\nimport { ConfigService } from '@nestjs/config'\nimport { isUndefined } from 'lodash'\nimport { OpenAPIV3_1 } from 'openapi-types'\n\nimport { version } from '../../package.json'\nimport { APP_BOOT_CONTEXT, LLANA_WEBHOOK_TABLE } from '../app.constants'\nimport { ListTablesResponseObject } from '../dtos/response.dto'\nimport { AuthLocation } from '../types/auth.types'\nimport { DataSourceColumnType, DataSourceSchema, QueryPerform } from '../types/datasource.types'\nimport { plural } from '../utils/String'\nimport { Authentication } from './Authentication'\nimport { Logger } from './Logger'\nimport { Query } from './Query'\nimport { Schema } from './Schema'\n\n@Injectable()\nexport class Documentation {\n\tconstructor(\n\t\tprivate readonly authentication: Authentication,\n\t\tprivate readonly logger: Logger,\n\t\tprivate readonly configService: ConfigService,\n\t\tprivate readonly query: Query,\n\t\tprivate readonly schema: Schema,\n\t) {}\n\n\t/**\n\t * Helper to check if we are skipping authentication\n\t */\n\n\tskipDocs(): boolean {\n\t\tconst skip_docs = this.configService.get<boolean | undefined>('SKIP_DOCS')\n\n\t\tif (!skip_docs || isUndefined(skip_docs)) {\n\t\t\treturn false\n\t\t}\n\n\t\treturn true\n\t}\n\n\t/**\n\t * Generate documentation for the application\n\t */\n\n\tasync generateDocumentation(): Promise<OpenAPIV3_1.Document> {\n\t\tthis.logger.log('Generating documentation')\n\n\t\tconst apiDoc: OpenAPIV3_1.Document = {\n\t\t\topenapi: '3.1.0',\n\t\t\tinfo: {\n\t\t\t\ttitle: 'Api Documentation',\n\t\t\t\tversion,\n\t\t\t},\n\t\t\tpaths: {\n\t\t\t\t'/auth/login': {\n\t\t\t\t\tpost: <any>this.getAuthLoginPath(),\n\t\t\t\t},\n\t\t\t},\n\t\t\tcomponents: {\n\t\t\t\tschemas: {},\n\t\t\t\tsecuritySchemes: {\n\t\t\t\t\tbearerAuth: this.getSecurityDefinitions('http'),\n\t\t\t\t\tapiKeyAuth: this.getSecurityDefinitions('apiKey'),\n\t\t\t\t},\n\t\t\t},\n\t\t\ttags: [\n\t\t\t\t{\n\t\t\t\t\tname: 'Authentication',\n\t\t\t\t\tdescription: 'Endpoints for user authentication',\n\t\t\t\t},\n\t\t\t],\n\t\t}\n\n\t\tapiDoc.components.schemas['AuthenticationTokenResponse'] = this.getAuthLoginComponent()\n\n\t\tconst { tables } = (await this.query.perform(\n\t\t\tQueryPerform.LIST_TABLES,\n\t\t\tundefined,\n\t\t\tAPP_BOOT_CONTEXT,\n\t\t)) as ListTablesResponseObject\n\t\tfor (const table of tables) {\n\t\t\tconst schema = await this.schema.getSchema({ table, x_request_id: APP_BOOT_CONTEXT })\n\n\t\t\tif (schema.table === this.authentication.getIdentityTable()) {\n\t\t\t\tapiDoc.paths['/auth/profile'] = {\n\t\t\t\t\tget: <any>{\n\t\t\t\t\t\tdescription: 'Returns the user profile',\n\t\t\t\t\t\tsummary: 'Get Profile',\n\t\t\t\t\t\ttags: ['Authentication'],\n\t\t\t\t\t\tsecurity: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tbearerAuth: [],\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t\tresponses: {\n\t\t\t\t\t\t\t200: this.get200Response(this.convertSchemaToOpenAPIExample(schema), 'UserProfileResponse'),\n\t\t\t\t\t\t\t401: this.get401Response(),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\tapiDoc.components.schemas['UserProfileResponse'] = this.convertSchemaToOpenAPISchema(schema)\n\t\t\t}\n\n\t\t\tapiDoc.paths[`/${table}/`] = {\n\t\t\t\tpost: <any>{\n\t\t\t\t\tdescription: `Creates a new ${table}`,\n\t\t\t\t\tsummary: `Create ${table}`,\n\t\t\t\t\ttags: [table],\n\t\t\t\t\tsecurity: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tbearerAuth: [],\n\t\t\t\t\t\t\tapiKeyAuth: [],\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\trequestBody: this.getRequestBody(\n\t\t\t\t\t\tthis.convertSchemaToOpenAPIBodyRequest(schema),\n\t\t\t\t\t\tthis.convertSchemaRequiredToOpenAPI(schema),\n\t\t\t\t\t),\n\t\t\t\t\tresponses: {\n\t\t\t\t\t\t201: this.get200Response(this.convertSchemaToOpenAPIExample(schema), table + 'Response'),\n\t\t\t\t\t\t400: this.get400Response(),\n\t\t\t\t\t\t401: this.get401Response(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tget: <any>{\n\t\t\t\t\tdescription: `Returns a list of ${plural(table)} records`,\n\t\t\t\t\tsummary: `List ${plural(table)}`,\n\t\t\t\t\ttags: [table],\n\t\t\t\t\trequestBody: this.getListRequestBody(schema),\n\t\t\t\t\tsecurity: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tbearerAuth: [],\n\t\t\t\t\t\t\tapiKeyAuth: [],\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tresponses: {\n\t\t\t\t\t\t200: this.get200Response(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tlimit: 20,\n\t\t\t\t\t\t\t\toffset: 0,\n\t\t\t\t\t\t\t\ttotal: 70,\n\t\t\t\t\t\t\t\tpagination: {\n\t\t\t\t\t\t\t\t\ttotal: 20,\n\t\t\t\t\t\t\t\t\tpage: {\n\t\t\t\t\t\t\t\t\t\tcurrent: 'eyJsaW1pdCI6MjAsIm9mZnNldCI6MH0=',\n\t\t\t\t\t\t\t\t\t\tprev: null,\n\t\t\t\t\t\t\t\t\t\tnext: 'eyJsaW1pdCI6MjAsIm9mZnNldCI6MjB9',\n\t\t\t\t\t\t\t\t\t\tfirst: 'eyJsaW1pdCI6MjAsIm9mZnNldCI6MH0=',\n\t\t\t\t\t\t\t\t\t\tlast: 'eyJsaW1pdCI6MjAsIm9mZnNldCI6NTB9',\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tdata: [this.convertSchemaToOpenAPIExample(schema)],\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t'List' + table + 'Response',\n\t\t\t\t\t\t),\n\t\t\t\t\t\t400: this.get400Response(),\n\t\t\t\t\t\t401: this.get401Response(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tconst response_schema = schema\n\t\t\tdelete response_schema._x_request_id\n\n\t\t\tapiDoc.paths[`/${table}/:id`] = {\n\t\t\t\tget: <any>{\n\t\t\t\t\tdescription: `Returns a record of ${table}`,\n\t\t\t\t\tsummary: `Get ${table}`,\n\t\t\t\t\ttags: [table],\n\t\t\t\t\trequestBody: this.getSingleRequestBody(schema),\n\t\t\t\t\tsecurity: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tbearerAuth: [],\n\t\t\t\t\t\t\tapiKeyAuth: [],\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tresponses: {\n\t\t\t\t\t\t200: this.get200Response(this.convertSchemaToOpenAPIExample(schema), table + 'Response'),\n\t\t\t\t\t\t400: this.get400Response(),\n\t\t\t\t\t\t401: this.get401Response(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tput: <any>{\n\t\t\t\t\tdescription: `Updates a ${table} record`,\n\t\t\t\t\tsummary: `Update ${table}`,\n\t\t\t\t\ttags: [table],\n\t\t\t\t\tsecurity: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tbearerAuth: [],\n\t\t\t\t\t\t\tapiKeyAuth: [],\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\trequestBody: this.getRequestBody(this.convertSchemaToOpenAPIBodyRequest(schema), []),\n\t\t\t\t\tresponses: {\n\t\t\t\t\t\t201: this.get200Response(this.convertSchemaToOpenAPIExample(schema), table + 'Response'),\n\t\t\t\t\t\t400: this.get400Response(),\n\t\t\t\t\t\t401: this.get401Response(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdelete: <any>{\n\t\t\t\t\tdescription: `Deletes a record of ${table}`,\n\t\t\t\t\tsummary: `Delete ${table}`,\n\t\t\t\t\ttags: [table],\n\t\t\t\t\tsecurity: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tbearerAuth: [],\n\t\t\t\t\t\t\tapiKeyAuth: [],\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tresponses: {\n\t\t\t\t\t\t200: this.get200Response(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tdeleted: 1,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\ttable + 'DeleteResponse',\n\t\t\t\t\t\t),\n\t\t\t\t\t\t400: this.get400Response(),\n\t\t\t\t\t\t401: this.get401Response(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tapiDoc.paths[`/${table}/schema`] = {\n\t\t\t\tget: <any>{\n\t\t\t\t\tdescription: `Returns the table schema for ${table}`,\n\t\t\t\t\tsummary: `Schema`,\n\t\t\t\t\ttags: [table],\n\t\t\t\t\tsecurity: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tbearerAuth: [],\n\t\t\t\t\t\t\tapiKeyAuth: [],\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tresponses: {\n\t\t\t\t\t\t200: this.get200Response(response_schema, 'SchemaResponse'),\n\t\t\t\t\t\t401: this.get401Response(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\n\t\t// Add webhooks endpoints\n\n\t\tif (!this.configService.get<boolean>('DISABLE_WEBHOOKS')) {\n\t\t\tconst table = 'webhook'\n\t\t\tconst schema = await this.schema.getSchema({ table: LLANA_WEBHOOK_TABLE, x_request_id: APP_BOOT_CONTEXT })\n\n\t\t\tapiDoc.paths[`/${table}/`] = {\n\t\t\t\tpost: <any>{\n\t\t\t\t\tdescription: `Creates a new ${table}`,\n\t\t\t\t\tsummary: `Create ${table}`,\n\t\t\t\t\ttags: ['Webhooks'],\n\t\t\t\t\tsecurity: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tbearerAuth: [],\n\t\t\t\t\t\t\tapiKeyAuth: [],\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\trequestBody: this.getRequestBody(\n\t\t\t\t\t\tthis.convertSchemaToOpenAPIBodyRequest(schema),\n\t\t\t\t\t\tthis.convertSchemaRequiredToOpenAPI(schema),\n\t\t\t\t\t),\n\t\t\t\t\tresponses: {\n\t\t\t\t\t\t201: this.get200Response(this.convertSchemaToOpenAPIExample(schema), table + 'Response'),\n\t\t\t\t\t\t400: this.get400Response(),\n\t\t\t\t\t\t401: this.get401Response(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tget: <any>{\n\t\t\t\t\tdescription: `Returns a list of ${plural(table)} records`,\n\t\t\t\t\tsummary: `List ${plural(table)}`,\n\t\t\t\t\ttags: ['Webhooks'],\n\t\t\t\t\trequestBody: this.getListRequestBody(schema),\n\t\t\t\t\tsecurity: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tbearerAuth: [],\n\t\t\t\t\t\t\tapiKeyAuth: [],\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tresponses: {\n\t\t\t\t\t\t200: this.get200Response(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tlimit: 20,\n\t\t\t\t\t\t\t\toffset: 0,\n\t\t\t\t\t\t\t\ttotal: 70,\n\t\t\t\t\t\t\t\tpagination: {\n\t\t\t\t\t\t\t\t\ttotal: 20,\n\t\t\t\t\t\t\t\t\tpage: {\n\t\t\t\t\t\t\t\t\t\tcurrent: 'eyJsaW1pdCI6MjAsIm9mZnNldCI6MH0=',\n\t\t\t\t\t\t\t\t\t\tprev: null,\n\t\t\t\t\t\t\t\t\t\tnext: 'eyJsaW1pdCI6MjAsIm9mZnNldCI6MjB9',\n\t\t\t\t\t\t\t\t\t\tfirst: 'eyJsaW1pdCI6MjAsIm9mZnNldCI6MH0=',\n\t\t\t\t\t\t\t\t\t\tlast: 'eyJsaW1pdCI6MjAsIm9mZnNldCI6NTB9',\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tdata: [this.convertSchemaToOpenAPIExample(schema)],\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t'List' + table + 'Response',\n\t\t\t\t\t\t),\n\t\t\t\t\t\t400: this.get400Response(),\n\t\t\t\t\t\t401: this.get401Response(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tconst response_schema = schema\n\t\t\tdelete response_schema._x_request_id\n\n\t\t\tapiDoc.paths[`/${table}/:id`] = {\n\t\t\t\tget: <any>{\n\t\t\t\t\tdescription: `Returns a record of ${table}`,\n\t\t\t\t\tsummary: `Get ${table}`,\n\t\t\t\t\ttags: ['Webhooks'],\n\t\t\t\t\trequestBody: this.getSingleRequestBody(schema),\n\t\t\t\t\tsecurity: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tbearerAuth: [],\n\t\t\t\t\t\t\tapiKeyAuth: [],\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tresponses: {\n\t\t\t\t\t\t200: this.get200Response(this.convertSchemaToOpenAPIExample(schema), table + 'Response'),\n\t\t\t\t\t\t400: this.get400Response(),\n\t\t\t\t\t\t401: this.get401Response(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tput: <any>{\n\t\t\t\t\tdescription: `Updates a ${table} record`,\n\t\t\t\t\tsummary: `Update ${table}`,\n\t\t\t\t\ttags: ['Webhooks'],\n\t\t\t\t\tsecurity: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tbearerAuth: [],\n\t\t\t\t\t\t\tapiKeyAuth: [],\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\trequestBody: this.getRequestBody(this.convertSchemaToOpenAPIBodyRequest(schema), []),\n\t\t\t\t\tresponses: {\n\t\t\t\t\t\t201: this.get200Response(this.convertSchemaToOpenAPIExample(schema), table + 'Response'),\n\t\t\t\t\t\t400: this.get400Response(),\n\t\t\t\t\t\t401: this.get401Response(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdelete: <any>{\n\t\t\t\t\tdescription: `Deletes a record of ${table}`,\n\t\t\t\t\tsummary: `Delete ${table}`,\n\t\t\t\t\ttags: ['Webhooks'],\n\t\t\t\t\tsecurity: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tbearerAuth: [],\n\t\t\t\t\t\t\tapiKeyAuth: [],\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tresponses: {\n\t\t\t\t\t\t200: this.get200Response(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tdeleted: 1,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\ttable + 'DeleteResponse',\n\t\t\t\t\t\t),\n\t\t\t\t\t\t400: this.get400Response(),\n\t\t\t\t\t\t401: this.get401Response(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\n\t\treturn apiDoc\n\t}\n\n\tgetAuthLoginPath(): OpenAPIV3_1.OperationObject {\n\t\treturn {\n\t\t\tdescription:\n\t\t\t\t'Takes a `username` and `password` and returns an `access_token` if successfully authenticated',\n\t\t\tsummary: 'Login',\n\t\t\ttags: ['Authentication'],\n\t\t\trequestBody: this.getRequestBody({ username: 'string', password: 'string' }, ['username', 'password']),\n\t\t\tresponses: {\n\t\t\t\t200: this.get200Response(\n\t\t\t\t\t{\n\t\t\t\t\t\taccess_token: 'eyJ0...CiM',\n\t\t\t\t\t\tid: '1',\n\t\t\t\t\t},\n\t\t\t\t\t'AuthenticationTokenResponse',\n\t\t\t\t),\n\t\t\t\t400: this.get400Response(),\n\t\t\t\t401: this.get401Response(),\n\t\t\t},\n\t\t}\n\t}\n\n\tgetAuthLoginComponent(): OpenAPIV3_1.SchemaObject {\n\t\treturn {\n\t\t\ttype: 'object',\n\t\t\tproperties: {\n\t\t\t\taccess_token: {\n\t\t\t\t\ttype: 'string',\n\t\t\t\t},\n\t\t\t\tid: {\n\t\t\t\t\ttype: 'string',\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t}\n\n\tgetRequestBody(properties: object, required: string[], bodyRequired = true): OpenAPIV3_1.RequestBodyObject {\n\t\tconst openapiProperties = Object.keys(properties).reduce((acc, property) => {\n\t\t\tacc[property] = {\n\t\t\t\ttype: properties[property],\n\t\t\t}\n\t\t\treturn acc\n\t\t}, {})\n\n\t\treturn {\n\t\t\tcontent: {\n\t\t\t\t'application/json': {\n\t\t\t\t\tschema: {\n\t\t\t\t\t\ttype: 'object',\n\t\t\t\t\t\tproperties: openapiProperties,\n\t\t\t\t\t\trequired,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\trequired: bodyRequired,\n\t\t}\n\t}\n\n\tget200Response(example: object, schemaName: string): OpenAPIV3_1.ResponseObject {\n\t\treturn {\n\t\t\tcontent: {\n\t\t\t\t'application/json': {\n\t\t\t\t\texamples: {\n\t\t\t\t\t\tresponse: {\n\t\t\t\t\t\t\tvalue: example,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tschema: {\n\t\t\t\t\t\t$ref: '#/components/schemas/' + schemaName,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tdescription: 'Success',\n\t\t}\n\t}\n\n\tget400Response(): OpenAPIV3_1.ResponseObject {\n\t\treturn {\n\t\t\tdescription: 'Invalid Request',\n\t\t}\n\t}\n\n\tget401Response(): OpenAPIV3_1.ResponseObject {\n\t\treturn {\n\t\t\tdescription: 'Unauthorized',\n\t\t}\n\t}\n\n\t/**\n\t * Convert Llana schema to OpenAPI schema\n\t */\n\n\tconvertSchemaToOpenAPIBodyRequest(schema: DataSourceSchema): object {\n\t\tlet columns = schema.columns\n\n\t\tcolumns = schema.columns.filter(column => column.field !== schema.primary_key)\n\n\t\treturn columns.reduce((acc, column) => {\n\t\t\tacc[column.field] =\n\t\t\t\tcolumn.type === DataSourceColumnType.ENUM\n\t\t\t\t\t? `One of: ${column.enums?.join(', ')}`\n\t\t\t\t\t: (column.default ?? column.type)\n\t\t\treturn acc\n\t\t}, {})\n\t}\n\n\t/**\n\t * Convert Llana schema to OpenAPI schema\n\t */\n\n\tconvertSchemaToOpenAPIExample(schema: DataSourceSchema): object {\n\t\tlet columns = schema.columns\n\n\t\treturn columns.reduce((acc, column) => {\n\t\t\tacc[column.field] = column.default ?? column.type\n\t\t\treturn acc\n\t\t}, {})\n\t}\n\n\t/**\n\t * Convert Llana schema required fields to OpenAPI schema\n\t */\n\n\tconvertSchemaRequiredToOpenAPI(schema: DataSourceSchema): string[] {\n\t\treturn schema.columns.filter(column => column.required).map(column => column.field)\n\t}\n\n\t/**\n\t * Convert Llana schema to OpenAPI schema\n\t */\n\n\tconvertSchemaToOpenAPISchema(schema: DataSourceSchema): OpenAPIV3_1.SchemaObject {\n\t\tconst openapiSchema = schema.columns.reduce((acc, column) => {\n\t\t\tacc[column.field] = {\n\t\t\t\ttype: column.type,\n\t\t\t}\n\t\t\treturn acc\n\t\t}, {})\n\n\t\treturn {\n\t\t\ttype: 'object',\n\t\t\tproperties: openapiSchema,\n\t\t}\n\t}\n\n\t/**\n\t * Get security definitions\n\t */\n\n\tgetSecurityDefinitions(type): OpenAPIV3_1.SecuritySchemeObject {\n\t\tif (type.includes('http')) {\n\t\t\treturn {\n\t\t\t\ttype: 'http',\n\t\t\t\tscheme: 'bearer',\n\t\t\t\tbearerFormat: 'JWT',\n\t\t\t}\n\t\t}\n\n\t\tif (type.includes('apiKey')) {\n\t\t\treturn {\n\t\t\t\tname: process.env.AUTH_USER_API_KEY_NAME ?? 'x-api-key',\n\t\t\t\ttype: 'apiKey',\n\t\t\t\tin: (process.env.AUTH_USER_API_KEY_LOCATION ?? AuthLocation.HEADER).toLowerCase(),\n\t\t\t}\n\t\t}\n\t}\n\n\tgetListRequestBody(schema: DataSourceSchema): OpenAPIV3_1.RequestBodyObject {\n\t\tconst properties = {}\n\n\t\tfor (const column of schema.columns) {\n\t\t\tlet operators = ''\n\n\t\t\tswitch (column.type) {\n\t\t\t\tcase DataSourceColumnType.BOOLEAN:\n\t\t\t\t\toperators = `\\`${column.field}=true\\`, \\`${column.field}=false\\`, \\`${column.field}[null]\\`, \\`${column.field}[not_null]\\`, \\`${column.field}[equals]=true\\`, \\`${column.field}[not_equals]=true\\``\n\t\t\t\t\tbreak\n\t\t\t\tcase DataSourceColumnType.DATE:\n\t\t\t\t\toperators = `\\`${column.field}=2021-01-01\\`, \\`${column.field}[gt]=2021-01-01\\`, \\`${column.field}[lt]=2021-01-01\\`, \\`${column.field}[gte]=2021-01-01\\`, \\`${column.field}[lte]=2021-01-01\\`, \\`${column.field}[null]\\`, \\`${column.field}[not_null]\\``\n\t\t\t\t\tbreak\n\t\t\t\tcase DataSourceColumnType.STRING:\n\t\t\t\t\toperators = `\\`${column.field}=value\\`, \\`${column.field}[search]=value\\`, \\`${column.field}[like]=value\\`,  \\`${column.field}[in]=value\\`, \\`${column.field}[null]\\`, \\`${column.field}[not_null]\\``\n\t\t\t\t\tbreak\n\t\t\t\tcase DataSourceColumnType.NUMBER:\n\t\t\t\t\toperators = `\\`${column.field}=1\\`, \\`${column.field}[gt]=1\\`, \\`${column.field}[lt]=1\\`, \\`${column.field}[gte]=1\\`, \\`${column.field}[lte]=1\\`, \\`${column.field}[not_like]=value\\`, \\`${column.field}[not_in]=value\\`, \\`${column.field}[null]\\`, \\`${column.field}[not_null]\\``\n\t\t\t\t\tbreak\n\t\t\t\tcase DataSourceColumnType.ENUM:\n\t\t\t\t\toperators = `\\`${column.field}=value\\`, \\`${column.field}[null]\\`, \\`${column.field}[not_null].\\`, \\`${column.field}[not_like]=value\\`, \\`${column.field}[not_in]=value\\``\n\t\t\t\t\tif (column.enums?.length) {\n\t\t\t\t\t\toperators += `Enums are: \\`${column.enums?.join('`, `')}\\`.`\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tproperties[column.field] = {\n\t\t\t\tdescription: `Filter by ${column.field}, options are: ${operators}`,\n\t\t\t\ttype: column.type,\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tcontent: {\n\t\t\t\t'application/json': {\n\t\t\t\t\tschema: {\n\t\t\t\t\t\ttype: 'object',\n\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\tfields: {\n\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t'The fields to return, you can pass `table.field` to get a specific field in a related table. Default is all fields in the table.',\n\t\t\t\t\t\t\t\ttype: 'array',\n\t\t\t\t\t\t\t\titems: {\n\t\t\t\t\t\t\t\t\ttype: 'string',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\trelations: {\n\t\t\t\t\t\t\t\tdescription: `One or more relations to include in the response. One of the following: \\`${schema.relations.map(r => r.table).join('`, `')}\\``,\n\t\t\t\t\t\t\t\ttype: 'array',\n\t\t\t\t\t\t\t\titems: {\n\t\t\t\t\t\t\t\t\ttype: 'string',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tpage: {\n\t\t\t\t\t\t\t\ttype: 'string',\n\t\t\t\t\t\t\t\tdescription: 'Used for pagination, pass the page result from a previous request',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tlimit: {\n\t\t\t\t\t\t\t\ttype: 'number',\n\t\t\t\t\t\t\t\tdescription: 'The number of records to return',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\toffset: {\n\t\t\t\t\t\t\t\ttype: 'number',\n\t\t\t\t\t\t\t\tdescription: 'The number of records to skip',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tsort: {\n\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t'The fields to sort by, expects a comma separated list of fields. Format is sort=`{column}.{direction},column.{direction}`',\n\t\t\t\t\t\t\t\ttype: 'string',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t...properties,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\trequired: false,\n\t\t}\n\t}\n\n\tgetSingleRequestBody(schema: DataSourceSchema): OpenAPIV3_1.RequestBodyObject {\n\t\treturn {\n\t\t\tcontent: {\n\t\t\t\t'application/json': {\n\t\t\t\t\tschema: {\n\t\t\t\t\t\ttype: 'object',\n\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\tfields: {\n\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t'The fields to return, you can pass `table.field` to get a specific field in a related table. Default is all fields in the table.',\n\t\t\t\t\t\t\t\ttype: 'array',\n\t\t\t\t\t\t\t\titems: {\n\t\t\t\t\t\t\t\t\ttype: 'string',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\trelations: {\n\t\t\t\t\t\t\t\tdescription: `One or more relations to include in the response. One of the following: \\`${schema.relations.map(r => r.table).join('`, `')}\\``,\n\t\t\t\t\t\t\t\ttype: 'array',\n\t\t\t\t\t\t\t\titems: {\n\t\t\t\t\t\t\t\t\ttype: 'string',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\trequired: false,\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/helpers/Encryption.ts",
    "content": "import { createHash, createHmac } from 'node:crypto'\n\nimport { Injectable } from '@nestjs/common'\nimport * as argon2 from 'argon2'\nimport * as bcrypt from 'bcrypt'\n\nimport { AuthPasswordEncryption } from '../types/auth.types'\nimport { Logger } from './Logger'\n\n@Injectable()\nexport class Encryption {\n\tconstructor(private readonly logger: Logger) {}\n\n\t/**\n\t * Compare a string with an encrypted string\n\t */\n\n\tasync compare(raw: string, encrypted: string, type: AuthPasswordEncryption, salt?: any): Promise<boolean> {\n\t\tswitch (type) {\n\t\t\tcase AuthPasswordEncryption.BCRYPT:\n\t\t\t\treturn await bcrypt.compare(raw, encrypted)\n\t\t\tcase AuthPasswordEncryption.SHA1:\n\t\t\tcase AuthPasswordEncryption.SHA256:\n\t\t\tcase AuthPasswordEncryption.SHA512:\n\t\t\tcase AuthPasswordEncryption.MD5:\n\t\t\t\treturn !!((await this.encrypt(type, raw, salt)) === encrypted)\n\t\t\tcase AuthPasswordEncryption.ARGON2:\n\t\t\t\treturn await argon2.verify(encrypted, raw)\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`Encryption type ${type} not supported`)\n\t\t}\n\t}\n\n\t/**\n\t * Encrypt a string\n\t */\n\n\tasync encrypt(type: AuthPasswordEncryption, string: string, salt?: any): Promise<string> {\n\t\tswitch (type) {\n\t\t\tcase AuthPasswordEncryption.BCRYPT:\n\t\t\t\tif (!salt) {\n\t\t\t\t\tthrow new Error(`Encryption type ${type} requires a salt`)\n\t\t\t\t}\n\n\t\t\t\treturn await bcrypt.hash(string, Number(salt))\n\t\t\tcase AuthPasswordEncryption.SHA1:\n\t\t\t\tif (salt) {\n\t\t\t\t\treturn createHmac('sha1', salt).update(string).digest('hex')\n\t\t\t\t} else {\n\t\t\t\t\treturn createHash('sha1').update(string).digest('hex')\n\t\t\t\t}\n\t\t\tcase AuthPasswordEncryption.SHA256:\n\t\t\t\tif (salt) {\n\t\t\t\t\treturn createHmac('sha256', salt).update(string).digest('hex')\n\t\t\t\t} else {\n\t\t\t\t\treturn createHash('sha256').update(string).digest('hex')\n\t\t\t\t}\n\t\t\tcase AuthPasswordEncryption.SHA512:\n\t\t\t\tif (salt) {\n\t\t\t\t\treturn createHmac('sha512', salt).update(string).digest('hex')\n\t\t\t\t} else {\n\t\t\t\t\treturn createHash('sha512').update(string).digest('hex')\n\t\t\t\t}\n\t\t\tcase AuthPasswordEncryption.MD5:\n\t\t\t\tif (salt) {\n\t\t\t\t\treturn createHmac('md5', salt).update(string).digest('hex')\n\t\t\t\t} else {\n\t\t\t\t\treturn createHash('md5').update(string).digest('hex')\n\t\t\t\t}\n\t\t\tcase AuthPasswordEncryption.ARGON2:\n\t\t\t\treturn await argon2.hash(string)\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`Encryption type ${type} not supported`)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/helpers/Logger.ts",
    "content": "import { ConsoleLogger, Injectable, LogLevel } from '@nestjs/common'\n\nimport { APP_BOOT_CONTEXT } from '../app.constants'\nimport { Env } from '../utils/Env'\nimport { Environment } from '../utils/Env.types'\n\n@Injectable()\nexport class Logger extends ConsoleLogger {\n\tconstructor(context = 'Llana') {\n\t\tsuper(context)\n\t}\n\n\terror(message: any, ...optionalParams: [...any, string?]): void {\n\t\tif (logLevel().includes('error')) {\n\t\t\tsuper.error(message, ...optionalParams)\n\t\t}\n\t}\n\n\twarn(message: any, ...optionalParams: [...any, string?]): void {\n\t\tif (logLevel().includes('warn')) {\n\t\t\tsuper.warn(message, ...optionalParams)\n\t\t}\n\t}\n\n\tlog(message: any, ...optionalParams: [...any, string?]): void {\n\t\tif (logLevel().includes('log')) {\n\t\t\tsuper.log(message, ...optionalParams)\n\t\t}\n\t}\n\n\tdebug(message: any, ...optionalParams: [...any, string?]): void {\n\t\tif (logLevel().includes('debug')) {\n\t\t\tsuper.debug(message, ...optionalParams)\n\t\t}\n\t}\n\n\tverbose(message: any, ...optionalParams: [...any, string?]): void {\n\t\tif (logLevel().includes('verbose')) {\n\t\t\tsuper.verbose(message, ...optionalParams)\n\t\t}\n\t}\n\n\tstatus(): void {\n\t\tthis.log(`--------- Logging Status ---------`, APP_BOOT_CONTEXT)\n\t\tthis.error(`This is an error`, APP_BOOT_CONTEXT)\n\t\tthis.warn(`This is a warning`, APP_BOOT_CONTEXT)\n\t\tthis.log(`This is a log`, APP_BOOT_CONTEXT)\n\t\tthis.debug(`This is a debug`, APP_BOOT_CONTEXT)\n\t\tthis.verbose(`This is a verbose`, APP_BOOT_CONTEXT)\n\t\tthis.log(`------- Logging Status End -------`, APP_BOOT_CONTEXT)\n\t}\n\n\ttable(data: any): void {\n\t\tconsole.table(data)\n\t}\n}\n\nexport function logLevel(): LogLevel[] {\n\tlet logLevels: LogLevel[]\n\n\tswitch (Env.get()) {\n\t\tcase Environment.production:\n\t\t\tlogLevels = <LogLevel[]>process.env.LOG_LEVELS?.split(',') ?? ['error', 'warn', 'log']\n\t\t\tbreak\n\t\tcase Environment.sandbox:\n\t\t\tlogLevels = <LogLevel[]>process.env.LOG_LEVELS?.split(',') ?? ['error', 'warn', 'log', 'debug']\n\t\t\tbreak\n\t\tcase Environment.test:\n\t\t\tlogLevels = <LogLevel[]>process.env.LOG_LEVELS?.split(',') ?? ['error', 'warn', 'log']\n\t\t\tbreak\n\t\tcase Environment.development:\n\t\t\tlogLevels = <LogLevel[]>process.env.LOG_LEVELS?.split(',') ?? ['error', 'warn', 'log', 'debug', 'verbose']\n\t\t\tbreak\n\t\tdefault:\n\t\t\tlogLevels = ['error', 'warn', 'log']\n\t\t\tbreak\n\t}\n\n\treturn logLevels\n}\n"
  },
  {
    "path": "src/helpers/Pagination.test.spec.ts",
    "content": "import { INestApplication } from '@nestjs/common'\nimport { ConfigService } from '@nestjs/config'\nimport { Test } from '@nestjs/testing'\n\nimport { AppModule } from '../app.module'\nimport { Pagination } from './Pagination'\n\ndescribe('Pagination', () => {\n\tlet app: INestApplication\n\tlet service: Pagination\n\tlet configService: ConfigService\n\n\tbeforeAll(async () => {\n\t\tconst moduleRef = await Test.createTestingModule({\n\t\t\timports: [AppModule],\n\t\t}).compile()\n\n\t\tapp = moduleRef.createNestApplication()\n\n\t\tservice = app.get<Pagination>(Pagination)\n\t\tconfigService = app.get<ConfigService>(ConfigService)\n\t})\n\n\tdescribe('get', () => {\n\t\tit('No params passed', () => {\n\t\t\tconst query = {}\n\t\t\tconst result = service.get(query)\n\t\t\texpect(result.limit).toBe(Number(configService.get<string>('database.defaults.limit')))\n\t\t\texpect(result.offset).toBe(0)\n\t\t})\n\n\t\tit('Limit passed', () => {\n\t\t\tconst query = {\n\t\t\t\tlimit: 10,\n\t\t\t}\n\t\t\tconst result = service.get(query)\n\t\t\texpect(result.limit).toBe(10)\n\t\t\texpect(result.offset).toBe(0)\n\t\t})\n\n\t\tit('Offset passed', () => {\n\t\t\tconst query = {\n\t\t\t\toffset: 10,\n\t\t\t}\n\t\t\tconst result = service.get(query)\n\t\t\texpect(result.limit).toBe(Number(configService.get<string>('database.defaults.limit')))\n\t\t\texpect(result.offset).toBe(10)\n\t\t})\n\n\t\tit('Page passed', () => {\n\t\t\tconst query = {\n\t\t\t\tpage: service.encodePage({ limit: 100, offset: 50 }),\n\t\t\t}\n\t\t\tconst result = service.get(query)\n\t\t\texpect(result.limit).toBe(100)\n\t\t\texpect(result.offset).toBe(50)\n\t\t})\n\n\t\tit('Other value passed', () => {\n\t\t\tconst query = {\n\t\t\t\tfoo: 'bar',\n\t\t\t}\n\t\t\tconst result = service.get(query)\n\t\t\texpect(result.limit).toBe(Number(configService.get<string>('database.defaults.limit')))\n\t\t\texpect(result.offset).toBe(0)\n\t\t})\n\t})\n\n\tafterAll(async () => {\n\t\tawait app.close()\n\t})\n})\n"
  },
  {
    "path": "src/helpers/Pagination.ts",
    "content": "import { Injectable } from '@nestjs/common'\nimport { ConfigService } from '@nestjs/config'\n\nimport { FindManyQueryParams } from '../dtos/requests.dto'\n\n@Injectable()\nexport class Pagination {\n\tconstructor(private readonly configService: ConfigService) {}\n\n\t/**\n\t * Takes the query parameters, configs (for defualts) and returns the limit and offset\n\t */\n\n\tget(query: FindManyQueryParams): { limit: number; offset: number } {\n\t\tlet limit = Number(this.configService.get<number>('database.defaults.limit'))\n\t\tlet offset = 0\n\n\t\tif (query.limit) limit = Number(query.limit)\n\t\tif (query.offset) offset = Number(query.offset)\n\n\t\tif (query.page) {\n\t\t\tconst decoded = this.decodePage(query.page)\n\t\t\tlimit = decoded.limit\n\t\t\toffset = decoded.offset\n\t\t}\n\n\t\treturn {\n\t\t\tlimit: limit,\n\t\t\toffset: offset,\n\t\t}\n\t}\n\n\tset(limit: number, offset: number): string {\n\t\treturn this.encodePage({ limit: limit, offset: offset })\n\t}\n\n\tencodePage(options: { limit: number; offset: number }): string {\n\t\treturn Buffer.from(JSON.stringify(options)).toString('base64')\n\t}\n\n\tdecodePage(page: string): { limit: number; offset: number } {\n\t\treturn JSON.parse(Buffer.from(page, 'base64').toString('ascii'))\n\t}\n\n\tcurrent(limit: number, offset: number): string {\n\t\treturn this.encodePage({ limit: limit, offset: offset })\n\t}\n\n\tprevious(limit: number, offset: number): string {\n\t\tif (offset - limit < 0) return null\n\t\treturn this.encodePage({ limit: limit, offset: offset - limit })\n\t}\n\n\tnext(limit: number, offset: number, total: number): string {\n\t\tif (offset + limit >= total) return null\n\t\treturn this.encodePage({ limit: limit, offset: offset + limit })\n\t}\n\n\tfirst(limit: number): string {\n\t\treturn this.encodePage({ limit: limit, offset: 0 })\n\t}\n\n\tlast(limit: number, total: number): string {\n\t\tif (total <= limit) return this.encodePage({ limit: limit, offset: 0 })\n\t\treturn this.encodePage({ limit: limit, offset: total - limit })\n\t}\n}\n"
  },
  {
    "path": "src/helpers/Query.ts",
    "content": "import { Injectable } from '@nestjs/common'\nimport { ConfigService } from '@nestjs/config'\n\nimport { Airtable } from '../datasources/airtable.datasource'\nimport { Mongo } from '../datasources/mongo.datasource'\nimport { MSSQL } from '../datasources/mssql.datasource'\nimport { MySQL } from '../datasources/mysql.datasource'\nimport { Postgres } from '../datasources/postgres.datasource'\nimport {\n\tDeleteResponseObject,\n\tFindManyResponseObject,\n\tFindOneResponseObject,\n\tIsUniqueResponse,\n\tListTablesResponseObject,\n} from '../dtos/response.dto'\nimport { AuthType } from '../types/auth.types'\nimport {\n\tDataSourceCreateOneOptions,\n\tDataSourceDeleteOneOptions,\n\tDataSourceFindManyOptions,\n\tDataSourceFindOneOptions,\n\tDataSourceListTablesOptions,\n\tDataSourceRelations,\n\tDataSourceSchema,\n\tDataSourceSchemaRelation,\n\tDataSourceType,\n\tDataSourceUniqueCheckOptions,\n\tDataSourceUpdateOneOptions,\n\tDataSourceWhere,\n\tQueryPerform,\n\tWhereOperator,\n} from '../types/datasource.types'\nimport { Env } from '../utils/Env'\nimport { CircuitBreaker } from './CircuitBreaker'\nimport { Encryption } from './Encryption'\nimport { Logger } from './Logger'\nimport { Schema } from './Schema'\n\n@Injectable()\nexport class Query {\n\tconstructor(\n\t\tprivate readonly configService: ConfigService,\n\t\tprivate readonly encryption: Encryption,\n\t\tprivate readonly logger: Logger,\n\t\tprivate readonly schema: Schema,\n\t\tprivate readonly mysql: MySQL,\n\t\tprivate readonly mssql: MSSQL,\n\t\tprivate readonly postgres: Postgres,\n\t\tprivate readonly mongo: Mongo,\n\t\tprivate readonly airtable: Airtable,\n\t\tprivate readonly circuitBreaker: CircuitBreaker,\n\t) {}\n\n\tasync perform(\n\t\taction: QueryPerform,\n\t\toptions?:\n\t\t\t| DataSourceCreateOneOptions\n\t\t\t| DataSourceFindOneOptions\n\t\t\t| DataSourceFindManyOptions\n\t\t\t| DataSourceUpdateOneOptions\n\t\t\t| DataSourceDeleteOneOptions\n\t\t\t| DataSourceUniqueCheckOptions\n\t\t\t| DataSourceListTablesOptions,\n\t\tx_request_id?: string,\n\t): Promise<\n\t\t| FindOneResponseObject\n\t\t| FindManyResponseObject\n\t\t| IsUniqueResponse\n\t\t| DeleteResponseObject\n\t\t| void\n\t\t| boolean\n\t\t| ListTablesResponseObject\n\t> {\n\t\tlet table_name\n\n\t\tif (\n\t\t\t[\n\t\t\t\tQueryPerform.CREATE,\n\t\t\t\tQueryPerform.CREATE_TABLE,\n\t\t\t\tQueryPerform.DELETE,\n\t\t\t\tQueryPerform.FIND_MANY,\n\t\t\t\tQueryPerform.FIND_ONE,\n\t\t\t\tQueryPerform.TRUNCATE,\n\t\t\t\tQueryPerform.UNIQUE,\n\t\t\t\tQueryPerform.UPDATE,\n\t\t\t].includes(action)\n\t\t) {\n\t\t\tif (!(options as any).schema?.table) {\n\t\t\t\tthis.logger.warn(\n\t\t\t\t\t`[Query][${action.toUpperCase()}] Table not defined in schema: ${JSON.stringify(options)}`,\n\t\t\t\t\tx_request_id,\n\t\t\t\t)\n\t\t\t\tthrow new Error('Table not defined')\n\t\t\t}\n\n\t\t\ttable_name = (options as any).schema.table\n\t\t}\n\n\t\ttry {\n\t\t\tif (!this.circuitBreaker.isAllowed()) {\n\t\t\t\tthis.logger.error(\n\t\t\t\t\t`[Query][${action.toUpperCase()}] Circuit breaker open, rejecting request`,\n\t\t\t\t\tx_request_id,\n\t\t\t\t)\n\t\t\t\tthrow new Error('Database circuit breaker open, please try again later')\n\t\t\t}\n\n\t\t\tlet result\n\n\t\t\tswitch (action) {\n\t\t\t\tcase QueryPerform.CREATE:\n\t\t\t\t\tconst createOptions = options as DataSourceCreateOneOptions\n\t\t\t\t\tcreateOptions.data = await this.identityOperationCheck(createOptions)\n\t\t\t\t\tresult = await this.createOne(createOptions, x_request_id)\n\t\t\t\t\treturn await this.schema.pipeResponse(createOptions, result)\n\n\t\t\t\tcase QueryPerform.FIND_ONE:\n\t\t\t\t\tconst findOptions = options as DataSourceFindOneOptions\n\t\t\t\t\tresult = await this.findOne(findOptions, x_request_id)\n\t\t\t\t\tif (!result) {\n\t\t\t\t\t\treturn null\n\t\t\t\t\t}\n\n\t\t\t\t\tresult = await this.schema.pipeResponse(options as DataSourceFindOneOptions, result)\n\t\t\t\t\treturn result\n\n\t\t\t\tcase QueryPerform.FIND_MANY:\n\t\t\t\t\tconst findManyOptions = options as DataSourceFindManyOptions\n\t\t\t\t\tresult = await this.findMany(findManyOptions, x_request_id)\n\n\t\t\t\t\tfor (let i = 0; i < result.data.length; i++) {\n\t\t\t\t\t\tresult.data[i] = await this.schema.pipeResponse(findManyOptions, result.data[i])\n\t\t\t\t\t}\n\t\t\t\t\treturn result\n\n\t\t\t\tcase QueryPerform.UPDATE:\n\t\t\t\t\tconst updateOptions = options as DataSourceUpdateOneOptions\n\t\t\t\t\tupdateOptions.data = await this.identityOperationCheck(updateOptions)\n\t\t\t\t\tresult = await this.updateOne(updateOptions, x_request_id)\n\t\t\t\t\treturn await this.schema.pipeResponse(updateOptions, result)\n\n\t\t\t\tcase QueryPerform.DELETE:\n\t\t\t\t\treturn await this.deleteOne(options as DataSourceDeleteOneOptions, x_request_id)\n\n\t\t\t\tcase QueryPerform.UNIQUE:\n\t\t\t\t\treturn await this.isUnique(options as DataSourceUniqueCheckOptions, x_request_id)\n\n\t\t\t\tcase QueryPerform.TRUNCATE:\n\t\t\t\t\treturn await this.truncate((options as any).schema.table, x_request_id)\n\n\t\t\t\tcase QueryPerform.CREATE_TABLE:\n\t\t\t\t\treturn await this.createTable((options as any).schema, x_request_id)\n\n\t\t\t\tcase QueryPerform.CHECK_CONNECTION:\n\t\t\t\t\treturn await this.checkConnection({ x_request_id })\n\n\t\t\t\tcase QueryPerform.RESET_SEQUENCES:\n\t\t\t\t\treturn await this.resetSequences(x_request_id)\n\n\t\t\t\tcase QueryPerform.LIST_TABLES:\n\t\t\t\t\treturn await this.listTables(options as DataSourceListTablesOptions, x_request_id)\n\n\t\t\t\tdefault:\n\t\t\t\t\tthis.logger.error(`[Query] Action ${action} not supported`, x_request_id)\n\t\t\t\t\tthrow new Error(`Action ${action} not supported`)\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.circuitBreaker.reportFailure()\n\t\t\tthis.logger.error(`[Query][${action.toUpperCase()}][${table_name}] ${e.message}`, x_request_id)\n\n\t\t\tlet pluralAction\n\n\t\t\tswitch (action) {\n\t\t\t\tcase QueryPerform.CREATE:\n\t\t\t\t\tpluralAction = 'creating record'\n\t\t\t\t\tbreak\n\t\t\t\tcase QueryPerform.FIND_ONE:\n\t\t\t\t\tpluralAction = 'finding record'\n\t\t\t\t\tbreak\n\t\t\t\tcase QueryPerform.FIND_MANY:\n\t\t\t\t\tpluralAction = 'finding records'\n\t\t\t\t\tbreak\n\t\t\t\tcase QueryPerform.UPDATE:\n\t\t\t\t\tpluralAction = 'updating record'\n\t\t\t\t\tbreak\n\t\t\t\tcase QueryPerform.DELETE:\n\t\t\t\t\tpluralAction = 'deleting record'\n\t\t\t\t\tbreak\n\t\t\t\tcase QueryPerform.UNIQUE:\n\t\t\t\t\tpluralAction = 'checking uniqueness'\n\t\t\t\t\tbreak\n\t\t\t\tdefault:\n\t\t\t\t\tpluralAction = 'performing action'\n\t\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tthrow new Error(`Error ${pluralAction}`)\n\t\t}\n\n\t\tthis.circuitBreaker.reportSuccess()\n\t}\n\n\t/**\n\t * Converts a URL request to an DataSourceFindManyOptions object (used for cache requests)\n\t */\n\n\tasync buildFindManyOptionsFromRequest(options: {\n\t\trequest: any\n\t\tschema: DataSourceSchema\n\t}): Promise<DataSourceFindManyOptions> {\n\t\tif (!options.request || !options.schema) {\n\t\t\tthis.logger.error('[Query][buildFindManyOptionsFromRequest] Request or Schema not provided')\n\t\t\treturn\n\t\t}\n\n\t\ttry {\n\t\t\tconst searchRequest = new URLSearchParams(options.request)\n\t\t\tconst request = Object.fromEntries(searchRequest.entries())\n\n\t\t\tlet sort\n\n\t\t\tif (request['sort']) {\n\t\t\t\t// Validate sort format: column.direction\n\t\t\t\tif (!request['sort'].includes('.')) {\n\t\t\t\t\tthis.logger.warn(`Invalid sort format: ${request['sort']}. Expected format: column.direction`)\n\t\t\t\t\t// Continue with no sorting\n\t\t\t\t} else {\n\t\t\t\t\tconst sortItems = request['sort'].split('.')\n\n\t\t\t\t\tsort = [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcolumn: sortItems[0],\n\t\t\t\t\t\t\toperator: sortItems[1] === 'desc' ? 'DESC' : 'ASC',\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlet fields\n\n\t\t\tif (request['fields']) {\n\t\t\t\t// if it's an array, join it\n\t\t\t\tif (Array.isArray(request['fields'])) {\n\t\t\t\t\tfields = request['fields']\n\t\t\t\t}\n\t\t\t\t// if it's a string, convert it to an array\n\t\t\t\telse if (typeof request['fields'] === 'string') {\n\t\t\t\t\tfields = request['fields'].split(',')\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlet relations: DataSourceRelations[] = []\n\n\t\t\tif (request['relations']) {\n\t\t\t\tlet relationsArray\n\n\t\t\t\tif (Array.isArray(request['relations'])) {\n\t\t\t\t\trelationsArray = request['relations']\n\t\t\t\t}\n\t\t\t\t// if it's a string, convert it to an array\n\t\t\t\telse if (typeof request['relations'] === 'string') {\n\t\t\t\t\trelationsArray = request['relations'].split(',')\n\t\t\t\t}\n\n\t\t\t\t// convert relations to DataSourceSchemaRelation[]\n\n\t\t\t\tfor (const relation of relationsArray) {\n\t\t\t\t\tconst relationFields = []\n\n\t\t\t\t\tif (fields) {\n\t\t\t\t\t\tfor (const field of fields) {\n\t\t\t\t\t\t\tif (field.startsWith(relation)) {\n\t\t\t\t\t\t\t\trelationFields.push(field.replace(relation + '.', ''))\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tconst relationSchema = await this.schema.getSchema({ table: relation })\n\n\t\t\t\t\tlet join\n\n\t\t\t\t\tif (options.schema.relations.find(col => col.table === relation)) {\n\t\t\t\t\t\tjoin = options.schema.relations.find(col => col.table === relation)\n\t\t\t\t\t} else if (options.schema.relations.find(col => col.org_table === relation)) {\n\t\t\t\t\t\tjoin = options.schema.relations.find(col => col.org_table === relation)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.logger.error(`Relation ${relation} not found in schema ${options.schema.table}`)\n\t\t\t\t\t}\n\n\t\t\t\t\trelations.push({\n\t\t\t\t\t\ttable: relation,\n\t\t\t\t\t\tjoin,\n\t\t\t\t\t\tschema: relationSchema,\n\t\t\t\t\t\tcolumns: relationFields,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlet where: DataSourceWhere[] = []\n\n\t\t\tfor (const key in request) {\n\t\t\t\tif (key === 'sort' || key === 'fields' || key === 'relations' || key === 'limit' || key === 'offset') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t//convert format from id=1, id[gt]=1, id[lt]=1, id[gte]=1, id[lte]=1,\n\t\t\t\t// id[not_like]=value, id[not_in]=value, id[null], id[not_null],\n\t\t\t\t// handle[search]=value, handle[like]=value, handle[in]=value to DataSourceWhere[]\n\t\t\t\t// Using a regex to handle multiple brackets correctly\n\n\t\t\t\tconst matches = key.match(/\\[(.*?)\\]/)\n\t\t\t\tconst operator = matches ? WhereOperator[matches[1]] : WhereOperator.equals\n\n\t\t\t\twhere.push({\n\t\t\t\t\tcolumn: key.split('[')[0],\n\t\t\t\t\toperator: operator as WhereOperator,\n\t\t\t\t\tvalue: request[key],\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tlet topLevelFields = []\n\n\t\t\tif (fields) {\n\t\t\t\ttopLevelFields = fields.filter(field => !field.includes('.'))\n\t\t\t}\n\n\t\t\tconst findManyOptions: DataSourceFindManyOptions = {\n\t\t\t\tschema: options.schema,\n\t\t\t\tfields: topLevelFields,\n\t\t\t\twhere,\n\t\t\t\trelations,\n\t\t\t\tlimit: Number(request['limit']) || 20,\n\t\t\t\toffset: Number(request['offset']) || 0,\n\t\t\t\tsort,\n\t\t\t}\n\n\t\t\treturn findManyOptions\n\t\t} catch (e) {\n\t\t\tthis.logger.error(`[Query][buildFindManyOptionsFromRequest] Error: ${e.message}`, e.stack)\n\t\t\tthrow new Error('Error building findMany options: ' + e.message)\n\t\t}\n\t}\n\n\t/**\n\t * Create a table\n\t *\n\t * * Used as part of the setup process\n\t */\n\n\tprivate async createTable(schema: DataSourceSchema, x_request_id: string): Promise<boolean> {\n\t\tswitch (this.configService.get<string>('database.type')) {\n\t\t\tcase DataSourceType.MYSQL:\n\t\t\t\treturn await this.mysql.createTable(schema, x_request_id)\n\t\t\tcase DataSourceType.POSTGRES:\n\t\t\t\treturn await this.postgres.createTable(schema, x_request_id)\n\t\t\tcase DataSourceType.MONGODB:\n\t\t\t\treturn await this.mongo.createTable(schema, x_request_id)\n\t\t\tcase DataSourceType.MSSQL:\n\t\t\t\treturn await this.mssql.createTable(schema, x_request_id)\n\t\t\tcase DataSourceType.AIRTABLE:\n\t\t\t\treturn await this.airtable.createTable(schema, x_request_id)\n\t\t\tdefault:\n\t\t\t\tthis.logger.error(`Database type ${this.configService.get<string>('database.type')} not supported yet`)\n\t\t\t\tthrow new Error(`Database type ${this.configService.get<string>('database.type')} not supported`)\n\t\t}\n\t}\n\n\t/**\n\t * Insert a record\n\t */\n\n\tprivate async createOne(options: DataSourceCreateOneOptions, x_request_id: string): Promise<FindOneResponseObject> {\n\t\tlet result: FindOneResponseObject\n\n\t\tswitch (this.configService.get<DataSourceType>('database.type')) {\n\t\t\tcase DataSourceType.MYSQL:\n\t\t\t\tresult = await this.mysql.createOne(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.POSTGRES:\n\t\t\t\tresult = await this.postgres.createOne(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.MONGODB:\n\t\t\t\tresult = await this.mongo.createOne(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.MSSQL:\n\t\t\t\tresult = await this.mssql.createOne(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.AIRTABLE:\n\t\t\t\tresult = await this.airtable.createOne(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tdefault:\n\t\t\t\tthis.logger.error(\n\t\t\t\t\t`[Query] Database type ${this.configService.get<string>('database.type')} not supported yet ${x_request_id ?? ''}`,\n\t\t\t\t)\n\t\t\t\tthrow new Error(`Database type ${this.configService.get<string>('database.type')} not supported`)\n\t\t}\n\n\t\treturn {\n\t\t\t...result,\n\t\t\tx_request_id,\n\t\t}\n\t}\n\n\t/**\n\t * Find single record\n\t */\n\n\tprivate async findOne(options: DataSourceFindOneOptions, x_request_id: string): Promise<FindOneResponseObject> {\n\t\tlet result: FindOneResponseObject\n\n\t\tswitch (this.configService.get<string>('database.type')) {\n\t\t\tcase DataSourceType.MYSQL:\n\t\t\t\tresult = await this.mysql.findOne(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.POSTGRES:\n\t\t\t\tresult = await this.postgres.findOne(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.MONGODB:\n\t\t\t\tresult = await this.mongo.findOne(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.MSSQL:\n\t\t\t\tresult = await this.mssql.findOne(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.AIRTABLE:\n\t\t\t\tresult = await this.airtable.findOne(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tdefault:\n\t\t\t\tthis.logger.error(\n\t\t\t\t\t`[Query] Database type ${this.configService.get<string>('database.type')} not supported yet ${x_request_id ?? ''}`,\n\t\t\t\t)\n\t\t\t\tthrow new Error(`Database type ${this.configService.get<string>('database.type')} not supported`)\n\t\t}\n\n\t\tif (!result) {\n\t\t\treturn null\n\t\t}\n\n\t\treturn {\n\t\t\t...result,\n\t\t\t_x_request_id: x_request_id,\n\t\t}\n\t}\n\n\t/**\n\t * Find multiple records\n\t */\n\n\tprivate async findMany(options: DataSourceFindManyOptions, x_request_id: string): Promise<FindManyResponseObject> {\n\t\tlet result: FindManyResponseObject\n\n\t\tswitch (this.configService.get<string>('database.type')) {\n\t\t\tcase DataSourceType.MYSQL:\n\t\t\t\tresult = await this.mysql.findMany(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.POSTGRES:\n\t\t\t\tresult = await this.postgres.findMany(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.MONGODB:\n\t\t\t\tresult = await this.mongo.findMany(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.MSSQL:\n\t\t\t\tresult = await this.mssql.findMany(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.AIRTABLE:\n\t\t\t\tresult = await this.airtable.findMany(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tdefault:\n\t\t\t\tthis.logger.error(\n\t\t\t\t\t`[Query] Database type ${this.configService.get<string>('database.type')} not supported yet ${x_request_id ?? ''}`,\n\t\t\t\t)\n\t\t\t\tthrow new Error(`Database type ${this.configService.get<string>('database.type')} not supported`)\n\t\t}\n\n\t\treturn {\n\t\t\t...result,\n\t\t\t_x_request_id: x_request_id,\n\t\t}\n\t}\n\n\t/**\n\t * Update a record\n\t */\n\n\tprivate async updateOne(options: DataSourceUpdateOneOptions, x_request_id: string): Promise<FindOneResponseObject> {\n\t\tlet result: FindOneResponseObject\n\n\t\tswitch (this.configService.get<string>('database.type')) {\n\t\t\tcase DataSourceType.MYSQL:\n\t\t\t\tresult = await this.mysql.updateOne(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.POSTGRES:\n\t\t\t\tresult = await this.postgres.updateOne(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.MONGODB:\n\t\t\t\tresult = await this.mongo.updateOne(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.MSSQL:\n\t\t\t\tresult = await this.mssql.updateOne(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.AIRTABLE:\n\t\t\t\tresult = await this.airtable.updateOne(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tdefault:\n\t\t\t\tthis.logger.error(\n\t\t\t\t\t`[Query] Database type ${this.configService.get<string>('database.type')} not supported ${x_request_id ?? ''}`,\n\t\t\t\t)\n\t\t\t\tthrow new Error(`Database type ${this.configService.get<string>('database.type')} not supported`)\n\t\t}\n\n\t\treturn {\n\t\t\t...result,\n\t\t\t_x_request_id: x_request_id,\n\t\t}\n\t}\n\n\t/**\n\t * Delete a record\n\t */\n\n\tprivate async deleteOne(options: DataSourceDeleteOneOptions, x_request_id: string): Promise<DeleteResponseObject> {\n\t\tlet result: DeleteResponseObject\n\n\t\tswitch (this.configService.get<string>('database.type')) {\n\t\t\tcase DataSourceType.MYSQL:\n\t\t\t\tresult = await this.mysql.deleteOne(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.POSTGRES:\n\t\t\t\tresult = await this.postgres.deleteOne(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.MONGODB:\n\t\t\t\tresult = await this.mongo.deleteOne(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.MSSQL:\n\t\t\t\tresult = await this.mssql.deleteOne(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.AIRTABLE:\n\t\t\t\tresult = await this.airtable.deleteOne(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tdefault:\n\t\t\t\tthis.logger.error(\n\t\t\t\t\t`[Query] Database type ${this.configService.get<string>('database.type')} not supported`,\n\t\t\t\t\tx_request_id,\n\t\t\t\t)\n\t\t\t\tthrow new Error(`Database type ${this.configService.get<string>('database.type')} not supported`)\n\t\t}\n\n\t\treturn {\n\t\t\t...result,\n\t\t\t_x_request_id: x_request_id,\n\t\t}\n\t}\n\n\t/**\n\t * Check uniqueness of record\n\t */\n\n\tprivate async isUnique(options: DataSourceUniqueCheckOptions, x_request_id: string): Promise<IsUniqueResponse> {\n\t\tlet result: IsUniqueResponse\n\n\t\tswitch (this.configService.get<string>('database.type')) {\n\t\t\tcase DataSourceType.MYSQL:\n\t\t\t\tresult = await this.mysql.uniqueCheck(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.POSTGRES:\n\t\t\t\tresult = await this.postgres.uniqueCheck(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.MONGODB:\n\t\t\t\tresult = await this.mongo.uniqueCheck(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.MSSQL:\n\t\t\t\tresult = await this.mssql.uniqueCheck(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.AIRTABLE:\n\t\t\t\tresult = await this.airtable.uniqueCheck(options, x_request_id)\n\t\t\t\tbreak\n\t\t\tdefault:\n\t\t\t\tthis.logger.error(\n\t\t\t\t\t`[Query] Database type ${this.configService.get<string>('database.type')} not supported ${x_request_id ?? ''}`,\n\t\t\t\t)\n\t\t\t\tthrow new Error(`Database type ${this.configService.get<string>('database.type')} not supported`)\n\t\t}\n\n\t\treturn {\n\t\t\t...result,\n\t\t\t_x_request_id: x_request_id,\n\t\t}\n\t}\n\n\t/**\n\t * Truncate a table - used for testing only, not for production\n\t */\n\n\tprivate async truncate(table_name: string, x_request_id?: string): Promise<void> {\n\t\tif (Env.IsProd()) {\n\t\t\tthrow new Error('Truncate not allowed in production')\n\t\t}\n\n\t\tswitch (this.configService.get<string>('database.type')) {\n\t\t\tcase DataSourceType.MYSQL:\n\t\t\t\treturn await this.mysql.truncate(table_name)\n\t\t\tcase DataSourceType.POSTGRES:\n\t\t\t\treturn await this.postgres.truncate(table_name)\n\t\t\tcase DataSourceType.MONGODB:\n\t\t\t\treturn await this.mongo.truncate(table_name)\n\t\t\tcase DataSourceType.MSSQL:\n\t\t\t\treturn await this.mssql.truncate(table_name)\n\t\t\tcase DataSourceType.AIRTABLE:\n\t\t\t\treturn await this.airtable.truncate(table_name)\n\t\t\tdefault:\n\t\t\t\tthis.logger.error(\n\t\t\t\t\t`[Query] Database type ${this.configService.get<string>('database.type')} not supported yet ${x_request_id ?? ''}`,\n\t\t\t\t)\n\t\t\t\tthrow new Error(`Database type ${this.configService.get<string>('database.type')} not supported`)\n\t\t}\n\t}\n\n\t/**\n\t * If the table is the user identity table, hash the password\n\t */\n\n\tprivate async identityOperationCheck(\n\t\toptions: DataSourceCreateOneOptions | DataSourceUpdateOneOptions,\n\t): Promise<any> {\n\t\tconst jwt_config = this.configService.get<any>('auth').find(auth => auth.type === AuthType.JWT)\n\n\t\tif (options.data[jwt_config.table.columns.password]) {\n\t\t\tif (options.schema.table === jwt_config.table.name) {\n\t\t\t\toptions.data[jwt_config.table.columns.password] = await this.encryption.encrypt(\n\t\t\t\t\tjwt_config.table.password.encryption,\n\t\t\t\t\toptions.data[jwt_config.table.columns.password],\n\t\t\t\t\tjwt_config.table.password.salt,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\n\t\treturn options.data\n\t}\n\n\t/**\n\t * Check if connection is alive\n\t */\n\n\tprivate async checkConnection(options: { x_request_id?: string }): Promise<boolean> {\n\t\tswitch (this.configService.get<string>('database.type')) {\n\t\t\tcase DataSourceType.MYSQL:\n\t\t\t\treturn await this.mysql.checkDataSource({ x_request_id: options.x_request_id })\n\t\t\tcase DataSourceType.POSTGRES:\n\t\t\t\treturn await this.postgres.checkConnection({ x_request_id: options.x_request_id })\n\t\t\tcase DataSourceType.MONGODB:\n\t\t\t\treturn await this.mongo.checkConnection({ x_request_id: options.x_request_id })\n\t\t\tcase DataSourceType.MSSQL:\n\t\t\t\treturn await this.mssql.checkConnection({ x_request_id: options.x_request_id })\n\t\t\tcase DataSourceType.AIRTABLE:\n\t\t\t\treturn await this.airtable.checkConnection({ x_request_id: options.x_request_id })\n\t\t\tdefault:\n\t\t\t\tthis.logger.error(\n\t\t\t\t\t`[Query] Database type ${this.configService.get<string>('database.type')} not supported yet ${options.x_request_id ?? ''}`,\n\t\t\t\t)\n\t\t\t\tthrow new Error(`Database type ${this.configService.get<string>('database.type')} not supported`)\n\t\t}\n\t}\n\n\t/**\n\t * List tables in the database\n\t */\n\n\tprivate async listTables(\n\t\toptions: DataSourceListTablesOptions,\n\t\tx_request_id?: string,\n\t): Promise<ListTablesResponseObject> {\n\t\tlet tables: string[]\n\n\t\tswitch (this.configService.get<string>('database.type')) {\n\t\t\tcase DataSourceType.MYSQL:\n\t\t\t\ttables = await this.mysql.listTables({ x_request_id })\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.POSTGRES:\n\t\t\t\ttables = await this.postgres.listTables({ x_request_id })\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.MONGODB:\n\t\t\t\ttables = await this.mongo.listTables({ x_request_id })\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.MSSQL:\n\t\t\t\ttables = await this.mssql.listTables({ x_request_id })\n\t\t\t\tbreak\n\t\t\tcase DataSourceType.AIRTABLE:\n\t\t\t\ttables = await this.airtable.listTables({ x_request_id })\n\t\t\t\tbreak\n\t\t\tdefault:\n\t\t\t\tthis.logger.error(\n\t\t\t\t\t`[Query] Database type ${this.configService.get<string>('database.type')} not supported yet`,\n\t\t\t\t\tx_request_id,\n\t\t\t\t)\n\t\t\t\tthrow new Error(`Database type ${this.configService.get<string>('database.type')} not supported`)\n\t\t}\n\n\t\tlet tables_filtered = tables\n\n\t\tif (!options?.include_system) {\n\t\t\ttables_filtered = tables_filtered.filter(table => !table.startsWith('_llana_'))\n\t\t}\n\n\t\tif (!options?.include_known_db_orchestration) {\n\t\t\ttables_filtered = tables_filtered.filter(table => table !== 'atlas_schema_revisions')\n\t\t}\n\n\t\treturn {\n\t\t\ttables: tables_filtered,\n\t\t\t_x_request_id: x_request_id,\n\t\t}\n\t}\n\n\t/**\n\t * Build relations\n\t */\n\tasync buildRelations(\n\t\toptions: DataSourceFindOneOptions,\n\t\tresult: FindOneResponseObject,\n\t\tx_request_id: string,\n\t): Promise<FindOneResponseObject> {\n\t\tif (!options.relations?.length) {\n\t\t\treturn result\n\t\t}\n\n\t\tfor (const relation of options.relations) {\n\t\t\tconst rel = this.getTableRelationColumn(relation.join, options.schema.table)\n\t\t\tconst relationTable = this.getChildTableRelation(relation.join, options.schema.table)\n\n\t\t\tif (!result[rel]) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Cannot build relation. Field ${rel} not found in the result set. Please ensure you are selecting the column in your query`,\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tconst where: DataSourceWhere[] = [\n\t\t\t\t{\n\t\t\t\t\tcolumn: relationTable.column,\n\t\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\t\tvalue: result[rel],\n\t\t\t\t},\n\t\t\t]\n\n\t\t\tif (relation.where) {\n\t\t\t\twhere.push(relation.where)\n\t\t\t}\n\n\t\t\tif (this.configService.get('database.deletes.soft')) {\n\t\t\t\twhere.push({\n\t\t\t\t\tcolumn: this.configService.get('database.deletes.soft'),\n\t\t\t\t\toperator: WhereOperator.null,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tconst relationOptions = <DataSourceFindManyOptions>{\n\t\t\t\tschema: relation.schema,\n\t\t\t\tfields: relation.columns,\n\t\t\t\twhere: where,\n\t\t\t\tlimit: 9999,\n\t\t\t\toffset: 0,\n\t\t\t}\n\n\t\t\tconst relationResults = await this.findMany(relationOptions, x_request_id)\n\n\t\t\tif (relationResults) {\n\t\t\t\tresult[relation.table] = relationResults.total > 0 ? relationResults.data : []\n\t\t\t}\n\t\t}\n\n\t\treturn result\n\t}\n\n\t/**\n\t * Get a relation column from a relation table\n\t */\n\n\t/**\n\t * Reset database sequences (PostgreSQL only)\n\t */\n\tprivate async resetSequences(x_request_id?: string): Promise<boolean> {\n\t\tif (this.configService.get<string>('database.type') === DataSourceType.POSTGRES) {\n\t\t\treturn await this.postgres.resetSequences(x_request_id)\n\t\t}\n\n\t\tthis.logger.debug(`[Query] Sequence reset is only supported for PostgreSQL databases`, x_request_id)\n\t\treturn true\n\t}\n\n\tgetTableRelationColumn(relation: DataSourceSchemaRelation, currentTable: string): string {\n\t\tif (relation.table === currentTable) {\n\t\t\treturn relation.column\n\t\t}\n\n\t\treturn relation.org_column\n\t}\n\n\t/**\n\t * Get a \"child\" table relation from a relation\n\t */\n\n\tgetChildTableRelation(relation: DataSourceSchemaRelation, currentTable: string): { table: string; column: string } {\n\t\tif (relation.table === currentTable) {\n\t\t\treturn {\n\t\t\t\ttable: relation.org_table,\n\t\t\t\tcolumn: relation.org_column,\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\ttable: relation.table,\n\t\t\tcolumn: relation.column,\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/helpers/Response.ts",
    "content": "import { Injectable } from '@nestjs/common'\nimport * as escape from 'escape-html'\n\n@Injectable()\nexport class Response {\n\tconstructor() {}\n\n\t/**\n\t * Pipes a response whilst sanitizing it\n\t */\n\n\ttext(string: string): string {\n\t\treturn escape(string)\n\t}\n}\n"
  },
  {
    "path": "src/helpers/Roles.ts",
    "content": "import { CACHE_MANAGER } from '@nestjs/cache-manager'\nimport { Inject, Injectable } from '@nestjs/common'\nimport { ConfigService } from '@nestjs/config'\nimport { Cache } from 'cache-manager'\n\nimport { CACHE_DEFAULT_IDENTITY_DATA_TTL, LLANA_ROLES_TABLE } from '../app.constants'\nimport { FindManyResponseObject } from '../dtos/response.dto'\nimport { AuthTablePermissionFailResponse, AuthTablePermissionSuccessResponse } from '../types/auth.types'\nimport { QueryPerform, WhereOperator } from '../types/datasource.types'\nimport { RolePermission, RolesConfig } from '../types/roles.types'\nimport { Env } from '../utils/Env'\nimport { commaStringToArray } from '../utils/String'\nimport { Logger } from './Logger'\nimport { Query } from './Query'\nimport { Schema } from './Schema'\n\n@Injectable()\nexport class Roles {\n\tconstructor(\n\t\t@Inject(CACHE_MANAGER) private cacheManager: Cache,\n\t\tprivate readonly configService: ConfigService,\n\t\tprivate readonly logger: Logger,\n\t\tprivate readonly query: Query,\n\t\tprivate readonly schema: Schema,\n\t) {}\n\n\t/**\n\t * Check if a role has permission to access a table\n\t * * Pull from cache if available\n\t * * Get the users role\n\t * * Check if the role has custom permissions for the table\n\t * * Check if the role has default permissions set\n\t * * Return the result\n\t */\n\n\tasync tablePermission(options: {\n\t\tidentifier: string\n\t\ttable: string\n\t\taccess: RolePermission\n\t\tx_request_id?: string\n\t\tdata?: any //Used for Create and Update to check if the user has permission to update the record\n\t}): Promise<AuthTablePermissionSuccessResponse | AuthTablePermissionFailResponse> {\n\t\tconst config = this.configService.get<RolesConfig>('roles')\n\n\t\tif (!config.location?.table || !config.location?.column) {\n\t\t\tthis.logger.warn('Roles table not defined, skipping role check')\n\t\t\treturn <AuthTablePermissionSuccessResponse>{\n\t\t\t\tvalid: true,\n\t\t\t}\n\t\t}\n\n\t\tlet permission_result\n\n\t\tif (Env.IsNotTest()) {\n\t\t\tpermission_result = await this.cacheManager.get<\n\t\t\t\tAuthTablePermissionSuccessResponse | AuthTablePermissionFailResponse\n\t\t\t>(`roles:${options.identifier}:${options.table}:${options.access}`)\n\t\t}\n\n\t\tif (permission_result?.valid) {\n\t\t\treturn permission_result\n\t\t}\n\n\t\tconst schema = await this.schema.getSchema({ table: options.table, x_request_id: options.x_request_id })\n\n\t\tif (!schema) {\n\t\t\tpermission_result = <AuthTablePermissionFailResponse>{\n\t\t\t\tvalid: false,\n\t\t\t\tmessage: 'Table not found',\n\t\t\t}\n\t\t\tawait this.cacheManager.set(\n\t\t\t\t`roles:${options.identifier}:${options.table}:${options.access}`,\n\t\t\t\tpermission_result,\n\t\t\t\tCACHE_DEFAULT_IDENTITY_DATA_TTL,\n\t\t\t)\n\t\t\treturn permission_result\n\t\t}\n\n\t\tlet role\n\n\t\ttry {\n\t\t\trole = await this.getRole(options.identifier, options.x_request_id)\n\t\t} catch (e) {\n\t\t\tpermission_result = <AuthTablePermissionFailResponse>{\n\t\t\t\tvalid: false,\n\t\t\t\tmessage: e.message,\n\t\t\t}\n\t\t\tawait this.cacheManager.set(\n\t\t\t\t`roles:${options.identifier}:${options.table}:${options.access}`,\n\t\t\t\tpermission_result,\n\t\t\t\tCACHE_DEFAULT_IDENTITY_DATA_TTL,\n\t\t\t)\n\t\t\treturn permission_result\n\t\t}\n\n\t\tif (!role) {\n\t\t\tpermission_result = <AuthTablePermissionFailResponse>{\n\t\t\t\tvalid: false,\n\t\t\t\tmessage: 'Role not found',\n\t\t\t}\n\t\t\tawait this.cacheManager.set(\n\t\t\t\t`roles:${options.identifier}:${options.table}:${options.access}`,\n\t\t\t\tpermission_result,\n\t\t\t\tCACHE_DEFAULT_IDENTITY_DATA_TTL,\n\t\t\t)\n\t\t\treturn permission_result\n\t\t}\n\n\t\tconst permission_schema = await this.schema.getSchema({\n\t\t\ttable: LLANA_ROLES_TABLE,\n\t\t\tx_request_id: options.x_request_id,\n\t\t})\n\n\t\tconst custom_permissions = (await this.query.perform(\n\t\t\tQueryPerform.FIND_MANY,\n\t\t\t{\n\t\t\t\tschema: permission_schema,\n\t\t\t\twhere: [\n\t\t\t\t\t{\n\t\t\t\t\t\tcolumn: 'custom',\n\t\t\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\t\t\tvalue: true,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tcolumn: 'table',\n\t\t\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\t\t\tvalue: options.table,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tcolumn: 'role',\n\t\t\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\t\t\tvalue: role,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t},\n\t\t\toptions.x_request_id,\n\t\t)) as FindManyResponseObject\n\n\t\t// check if there is a table role setting\n\t\tif (custom_permissions.data?.length) {\n\t\t\tfor (const permission of custom_permissions.data) {\n\t\t\t\tif (comparePermissions(permission.records, options.access)) {\n\t\t\t\t\tpermission_result = <AuthTablePermissionSuccessResponse>{\n\t\t\t\t\t\tvalid: true,\n\t\t\t\t\t\tallowed_fields: commaStringToArray(permission.allowed_fields),\n\t\t\t\t\t}\n\t\t\t\t\tawait this.cacheManager.set(\n\t\t\t\t\t\t`roles:${options.identifier}:${options.table}:${options.access}`,\n\t\t\t\t\t\tpermission_result,\n\t\t\t\t\t\tCACHE_DEFAULT_IDENTITY_DATA_TTL,\n\t\t\t\t\t)\n\t\t\t\t\treturn permission_result\n\t\t\t\t}\n\n\t\t\t\tif (!comparePermissions(permission.own_records, options.access)) {\n\t\t\t\t\tpermission_result = <AuthTablePermissionFailResponse>{\n\t\t\t\t\t\tvalid: false,\n\t\t\t\t\t\tmessage: `Table Action ${options.access} - Permission Denied For Role ${role}`,\n\t\t\t\t\t}\n\t\t\t\t\tawait this.cacheManager.set(\n\t\t\t\t\t\t`roles:${options.identifier}:${options.table}:${options.access}`,\n\t\t\t\t\t\tpermission_result,\n\t\t\t\t\t\tCACHE_DEFAULT_IDENTITY_DATA_TTL,\n\t\t\t\t\t)\n\t\t\t\t\treturn permission_result\n\t\t\t\t} else if (\n\t\t\t\t\toptions.data &&\n\t\t\t\t\toptions.data[permission.identity_column ?? schema.primary_key] !== options.identifier\n\t\t\t\t) {\n\t\t\t\t\tpermission_result = <AuthTablePermissionFailResponse>{\n\t\t\t\t\t\tvalid: false,\n\t\t\t\t\t\tmessage: `Identity Mismatch - You can only add ${options.table} records with your own ${permission.identity_column ?? schema.primary_key} (${options.identifier}) but you are trying to add ${options.data[permission.identity_column ?? schema.primary_key]}`,\n\t\t\t\t\t}\n\t\t\t\t\tawait this.cacheManager.set(\n\t\t\t\t\t\t`roles:${options.identifier}:${options.table}:${options.access}`,\n\t\t\t\t\t\tpermission_result,\n\t\t\t\t\t\tCACHE_DEFAULT_IDENTITY_DATA_TTL,\n\t\t\t\t\t)\n\t\t\t\t\treturn permission_result\n\t\t\t\t}\n\n\t\t\t\tif (comparePermissions(permission.own_records, options.access)) {\n\t\t\t\t\tpermission_result = <AuthTablePermissionSuccessResponse>{\n\t\t\t\t\t\tvalid: true,\n\t\t\t\t\t\trestriction: {\n\t\t\t\t\t\t\tcolumn: permission.identity_column ?? schema.primary_key,\n\t\t\t\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\t\t\t\tvalue: options.identifier,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tallowed_fields: commaStringToArray(permission.allowed_fields),\n\t\t\t\t\t}\n\t\t\t\t\tawait this.cacheManager.set(\n\t\t\t\t\t\t`roles:${options.identifier}:${options.table}:${options.access}`,\n\t\t\t\t\t\tpermission_result,\n\t\t\t\t\t\tCACHE_DEFAULT_IDENTITY_DATA_TTL,\n\t\t\t\t\t)\n\t\t\t\t\treturn permission_result\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst default_permissions = (await this.query.perform(\n\t\t\tQueryPerform.FIND_MANY,\n\t\t\t{\n\t\t\t\tschema: permission_schema,\n\t\t\t\twhere: [\n\t\t\t\t\t{\n\t\t\t\t\t\tcolumn: 'custom',\n\t\t\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\t\t\tvalue: false,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tcolumn: 'role',\n\t\t\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\t\t\tvalue: role,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t},\n\t\t\toptions.x_request_id,\n\t\t)) as FindManyResponseObject\n\n\t\tif (default_permissions.data?.length) {\n\t\t\tfor (const permission of default_permissions.data) {\n\t\t\t\tif (comparePermissions(permission.records, options.access)) {\n\t\t\t\t\tpermission_result = <AuthTablePermissionSuccessResponse>{\n\t\t\t\t\t\tvalid: true,\n\t\t\t\t\t\tallowed_fields: commaStringToArray(permission.allowed_fields),\n\t\t\t\t\t}\n\t\t\t\t\tawait this.cacheManager.set(\n\t\t\t\t\t\t`roles:${options.identifier}:${options.table}:${options.access}`,\n\t\t\t\t\t\tpermission_result,\n\t\t\t\t\t\tCACHE_DEFAULT_IDENTITY_DATA_TTL,\n\t\t\t\t\t)\n\t\t\t\t\treturn permission_result\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tpermission_result = <AuthTablePermissionFailResponse>{\n\t\t\tvalid: false,\n\t\t\tmessage: `Table Action ${options.access} - Permission Denied For Role ${role}`,\n\t\t}\n\t\tawait this.cacheManager.set(\n\t\t\t`roles:${options.identifier}:${options.table}:${options.access}`,\n\t\t\tpermission_result,\n\t\t\tCACHE_DEFAULT_IDENTITY_DATA_TTL,\n\t\t)\n\t\treturn permission_result\n\t}\n\n\t/**\n\t * Get users role from the database\n\t */\n\n\tprivate async getRole(identifier: string, x_request_id: string): Promise<string | undefined> {\n\t\tconst config = this.configService.get<RolesConfig>('roles')\n\n\t\tlet table_schema\n\n\t\ttry {\n\t\t\ttable_schema = await this.schema.getSchema({ table: config.location.table, x_request_id })\n\t\t} catch (e) {\n\t\t\tthrow new Error(e)\n\t\t}\n\n\t\tconst user_id_column = config.location?.identifier_column ?? table_schema.primary_key\n\n\t\tconst role = await this.query.perform(\n\t\t\tQueryPerform.FIND_ONE,\n\t\t\t{\n\t\t\t\tschema: table_schema,\n\t\t\t\tfields: [config.location.column],\n\t\t\t\twhere: [\n\t\t\t\t\t{\n\t\t\t\t\t\tcolumn: user_id_column,\n\t\t\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\t\t\tvalue: identifier,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t},\n\t\t\tx_request_id,\n\t\t)\n\n\t\treturn role?.[config.location.column]\n\t}\n}\n/**\n *\n * @param permission level being requested (e.g. user permission)\n * @param access level required (e.g. delete endpoint is DELETE)\n * @returns\n */\nexport function comparePermissions(permission: RolePermission, access: RolePermission): boolean {\n\tlet passed = false\n\n\tswitch (access) {\n\t\tcase RolePermission.DELETE:\n\t\t\tpassed = permission === RolePermission.DELETE\n\t\t\tbreak\n\t\tcase RolePermission.WRITE:\n\t\t\tpassed = permission === RolePermission.WRITE || permission === RolePermission.DELETE\n\t\t\tbreak\n\t\tcase RolePermission.READ:\n\t\t\tpassed =\n\t\t\t\tpermission === RolePermission.READ ||\n\t\t\t\tpermission === RolePermission.WRITE ||\n\t\t\t\tpermission === RolePermission.DELETE\n\t\t\tbreak\n\t\tcase RolePermission.NONE:\n\t\t\tpassed = false\n\t\t\tbreak\n\t}\n\treturn passed\n}\n"
  },
  {
    "path": "src/helpers/Schema.ts",
    "content": "import { CACHE_MANAGER } from '@nestjs/cache-manager'\nimport { Inject, Injectable } from '@nestjs/common'\nimport { ConfigService } from '@nestjs/config'\nimport { Cache } from 'cache-manager'\nimport { plainToInstance } from 'class-transformer'\nimport { IsBoolean, IsDateString, IsNumber, IsOptional, IsString, validate } from 'class-validator'\nimport { isDate, isObject } from 'lodash'\n\nimport { CACHE_DEFAULT_TABLE_SCHEMA_TTL, NON_FIELD_PARAMS } from '../app.constants'\nimport { classValidatorConfig } from '../config/class-validator.config'\nimport { Airtable } from '../datasources/airtable.datasource'\nimport { Mongo } from '../datasources/mongo.datasource'\nimport { MSSQL } from '../datasources/mssql.datasource'\nimport { MySQL } from '../datasources/mysql.datasource'\nimport { Postgres } from '../datasources/postgres.datasource'\nimport {\n\tDataSourceColumnType,\n\tDataSourceFindOneOptions,\n\tDataSourceRelations,\n\tDataSourceSchema,\n\tDataSourceType,\n\tDataSourceWhere,\n\tWhereOperator,\n} from '../types/datasource.types'\nimport {\n\tSortCondition,\n\tValidateFieldsResponse,\n\tvalidateRelationsResponse,\n\tValidateSortResponse,\n\tvalidateWhereResponse,\n} from '../types/schema.types'\nimport { Logger } from './Logger'\n\n@Injectable()\nexport class Schema {\n\tconstructor(\n\t\t@Inject(CACHE_MANAGER) private cacheManager: Cache,\n\t\tprivate readonly logger: Logger,\n\t\tprivate readonly configService: ConfigService,\n\t\tprivate readonly postgres: Postgres,\n\t\tprivate readonly mongo: Mongo,\n\t\tprivate readonly mysql: MySQL,\n\t\tprivate readonly mssql: MSSQL,\n\t\tprivate readonly airtable: Airtable,\n\t) {}\n\n\t/**\n\t * Get Table Schema\n\t */\n\n\tasync getSchema(options: { table: string; x_request_id?: string; fields?: string[] }): Promise<DataSourceSchema> {\n\t\tif (!options.table) {\n\t\t\tthrow new Error('Table name not provided')\n\t\t}\n\n\t\t//check cache for schema\n\t\tlet result: DataSourceSchema = await this.cacheManager.get(\n\t\t\t`schema:${options.table}:${options.fields?.join(',')}`,\n\t\t)\n\n\t\tif (!result) {\n\t\t\ttry {\n\t\t\t\tswitch (this.configService.get<string>('database.type')) {\n\t\t\t\t\tcase DataSourceType.MYSQL:\n\t\t\t\t\t\tresult = await this.mysql.getSchema({\n\t\t\t\t\t\t\ttable: options.table,\n\t\t\t\t\t\t\tx_request_id: options.x_request_id,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase DataSourceType.POSTGRES:\n\t\t\t\t\t\tresult = await this.postgres.getSchema({\n\t\t\t\t\t\t\ttable: options.table,\n\t\t\t\t\t\t\tx_request_id: options.x_request_id,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase DataSourceType.MONGODB:\n\t\t\t\t\t\tresult = await this.mongo.getSchema({\n\t\t\t\t\t\t\ttable: options.table,\n\t\t\t\t\t\t\tx_request_id: options.x_request_id,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase DataSourceType.MSSQL:\n\t\t\t\t\t\tresult = await this.mssql.getSchema({\n\t\t\t\t\t\t\ttable: options.table,\n\t\t\t\t\t\t\tx_request_id: options.x_request_id,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase DataSourceType.AIRTABLE:\n\t\t\t\t\t\tresult = await this.airtable.getSchema({\n\t\t\t\t\t\t\ttable: options.table,\n\t\t\t\t\t\t\tx_request_id: options.x_request_id,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tbreak\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tthis.logger.error(\n\t\t\t\t\t\t\t`[GetSchema] Database type ${this.configService.get<string>('database.type')} not supported yet`,\n\t\t\t\t\t\t\toptions.x_request_id,\n\t\t\t\t\t\t)\n\t\t\t\t}\n\n\t\t\t\tif (!result?.table) {\n\t\t\t\t\tthrow new Error(`Schema not found for ${options.table}`)\n\t\t\t\t}\n\n\t\t\t\tawait this.cacheManager.set(\n\t\t\t\t\t`schema:${options.table}:${options.fields?.join(',')}`,\n\t\t\t\t\tresult,\n\t\t\t\t\tthis.configService.get<number>('CACHE_TABLE_SCHEMA_TTL') ?? CACHE_DEFAULT_TABLE_SCHEMA_TTL,\n\t\t\t\t)\n\t\t\t} catch (e) {\n\t\t\t\tthis.logger.debug(`[GetSchema] ${e.message} ${options.x_request_id ?? ''}`)\n\n\t\t\t\tif (process.env.NODE_ENV === 'test') {\n\t\t\t\t\tthis.logger.warn(\n\t\t\t\t\t\t`[Test Environment] Continuing despite schema error for ${options.table}`,\n\t\t\t\t\t\toptions.x_request_id,\n\t\t\t\t\t)\n\n\t\t\t\t\tif (options.table === 'Customer') {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\ttable: 'Customer',\n\t\t\t\t\t\t\tprimary_key: 'id',\n\t\t\t\t\t\t\tcolumns: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tfield: 'id',\n\t\t\t\t\t\t\t\t\ttype: DataSourceColumnType.NUMBER,\n\t\t\t\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\t\t\t\tprimary_key: true,\n\t\t\t\t\t\t\t\t\tunique_key: true,\n\t\t\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t\t\t\tauto_increment: true,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tfield: 'email',\n\t\t\t\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\t\t\tunique_key: true,\n\t\t\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tfield: 'name',\n\t\t\t\t\t\t\t\t\ttype: DataSourceColumnType.STRING,\n\t\t\t\t\t\t\t\t\tnullable: false,\n\t\t\t\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\t\t\t\tprimary_key: false,\n\t\t\t\t\t\t\t\t\tunique_key: false,\n\t\t\t\t\t\t\t\t\tforeign_key: false,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tthrow new Error(`Error processing schema for ${options.table}`)\n\t\t\t}\n\t\t}\n\n\t\t//filter fields if provided\n\t\tif (options.fields?.length) {\n\t\t\tconst columns = result.columns.filter(col => options.fields.includes(col.field) || col.primary_key)\n\t\t\tresult.columns = columns\n\t\t}\n\n\t\treturn {\n\t\t\t...result,\n\t\t\t_x_request_id: options.x_request_id,\n\t\t}\n\t}\n\n\t/**\n\t * The primary key's name of the table\n\t */\n\tgetPrimaryKey(schema: DataSourceSchema): string {\n\t\treturn schema.columns.find(column => {\n\t\t\tif (column.primary_key) {\n\t\t\t\treturn column\n\t\t\t}\n\t\t}).field\n\t}\n\n\t/**\n\t * Get the class for the schema\n\t */\n\n\tschemaToClass(schema: DataSourceSchema, data?: { [key: string]: any }): any {\n\t\tclass DynamicClass {}\n\n\t\tfor (const column of schema.columns) {\n\t\t\tconst decorators = []\n\n\t\t\tif (data && !data[column.field]) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (column.primary_key) {\n\t\t\t\tdecorators.push(IsOptional())\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tswitch (column.type) {\n\t\t\t\tcase DataSourceColumnType.NUMBER:\n\t\t\t\t\tdecorators.push(IsNumber())\n\t\t\t\t\tbreak\n\t\t\t\tcase DataSourceColumnType.STRING:\n\t\t\t\t\tdecorators.push(IsString())\n\t\t\t\t\tbreak\n\t\t\t\tcase DataSourceColumnType.BOOLEAN:\n\t\t\t\t\tdecorators.push(IsBoolean())\n\t\t\t\t\tbreak\n\t\t\t\tcase DataSourceColumnType.DATE:\n\t\t\t\t\tdecorators.push(IsDateString())\n\t\t\t\t\tbreak\n\t\t\t\tcase DataSourceColumnType.JSON:\n\t\t\t\t\t//decorators.push(IsJSON()) //breaks nested objects\n\t\t\t\t\tbreak\n\t\t\t\tdefault:\n\t\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif (!column.required) {\n\t\t\t\tdecorators.push(IsOptional())\n\t\t\t}\n\n\t\t\tReflect.decorate(decorators, DynamicClass.prototype, column.field)\n\t\t}\n\n\t\treturn DynamicClass\n\t}\n\n\t/**\n\t * Pipe response from database to class\n\t *\n\t * This function takes the flat datasource response e.g. { id: 1, name: 'Jon', job.id: '1' job.title: 'Developer' } and pipes it to the classes:\n\t * { id: 1, name: 'Jon', job: { id: 1, title: 'Developer' } }\n\t *\n\t */\n\n\tasync pipeResponse(\n\t\toptions: DataSourceFindOneOptions,\n\t\tdata: { [key: string]: any },\n\t\tx_request_id?: string,\n\t): Promise<object> {\n\t\tconst nestedObject = {}\n\n\t\t//if Buffer convert to string\n\t\tObject.keys(data).forEach(key => {\n\t\t\tif (data[key] instanceof Buffer) {\n\t\t\t\tdata[key] = data[key].toString()\n\t\t\t}\n\t\t})\n\n\t\tObject.keys(data).forEach(key => {\n\t\t\tconst keys = key.split('.')\n\t\t\tkeys.reduce((acc, currentKey, index) => {\n\t\t\t\tif (index === keys.length - 1) {\n\t\t\t\t\tacc[currentKey] = data[key]\n\t\t\t\t} else {\n\t\t\t\t\tacc[currentKey] = acc[currentKey] || {}\n\t\t\t\t}\n\t\t\t\treturn acc[currentKey]\n\t\t\t}, nestedObject)\n\t\t})\n\n\t\t//Loop over the nested object and create the class if the key is an object\n\n\t\tif (options.relations) {\n\t\t\tconst keys = Object.keys(nestedObject)\n\t\t\tfor (const key of keys) {\n\t\t\t\tif (isObject(nestedObject[key]) && !isDate(nestedObject[key])) {\n\t\t\t\t\tconst relation = options.relations?.find(col => col.table === key)\n\t\t\t\t\tif (relation) {\n\t\t\t\t\t\tconst DynamicClass = this.schemaToClass(relation.schema, nestedObject[key])\n\t\t\t\t\t\tconst instance: object = plainToInstance(DynamicClass, nestedObject[key])\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst errors = await validate(instance)\n\n\t\t\t\t\t\t\tif (errors.length > 0) {\n\t\t\t\t\t\t\t\tthis.logger.error(\n\t\t\t\t\t\t\t\t\t`[pipeResponse] ${Object.values(errors[0].constraints).join(', ')}`,\n\t\t\t\t\t\t\t\t\tx_request_id,\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\tthis.logger.error({\n\t\t\t\t\t\t\t\t\tdata,\n\t\t\t\t\t\t\t\t\tinstance,\n\t\t\t\t\t\t\t\t\terrors,\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t\t\t`Error piping response - ${Object.values(errors[0].constraints).join(', ')}`,\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tnestedObject[key] = instance\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t\tthrow new Error(e.message)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst DynamicClass = this.schemaToClass(options.schema, nestedObject)\n\t\tconst instance: object = plainToInstance(DynamicClass, nestedObject)\n\t\ttry {\n\t\t\tconst errors = await validate(instance, classValidatorConfig)\n\n\t\t\tif (errors.length > 0) {\n\t\t\t\tthis.logger.error(`[pipeResponse] ${Object.values(errors[0].constraints).join(', ')}`, x_request_id)\n\t\t\t\tthis.logger.error({\n\t\t\t\t\tdata,\n\t\t\t\t\tinstance,\n\t\t\t\t\terrors,\n\t\t\t\t})\n\t\t\t\tthrow new Error(`Error piping response - ${Object.values(errors[0].constraints).join(', ')}`)\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthrow new Error(e.message)\n\t\t}\n\n\t\treturn instance\n\t}\n\n\t/**\n\t * validate schema fields with data\n\t */\n\n\tasync validateData(\n\t\tschema: DataSourceSchema,\n\t\tdata: { [key: string]: any },\n\t): Promise<{ valid: boolean; message?: string; instance?: object }> {\n\t\ttry {\n\t\t\tfor (const key in data) {\n\t\t\t\tconst column = schema.columns.find(col => col.field === key)\n\n\t\t\t\tif (!column) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tvalid: false,\n\t\t\t\t\t\tmessage: `Unknown column: ${key}`,\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tswitch (column.type) {\n\t\t\t\t\tcase DataSourceColumnType.NUMBER:\n\t\t\t\t\t\tif (isNaN(data[key])) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tvalid: false,\n\t\t\t\t\t\t\t\tmessage: `${key} must be a number`,\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (typeof data[key] === 'boolean') {\n\t\t\t\t\t\t\tdata[key] = data[key] ? 1 : 0\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tdata[key] = Number(data[key])\n\t\t\t\t\t\tbreak\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst DynamicClass = this.schemaToClass(schema, data)\n\t\t\tconst instance: object = plainToInstance(DynamicClass, data)\n\t\t\tconst errors = await validate(instance, classValidatorConfig)\n\n\t\t\tif (errors.length > 0) {\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: errors.map(error => Object.values(error.constraints)).join(', '),\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn {\n\t\t\t\t\tvalid: true,\n\t\t\t\t\tinstance,\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (e) {\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\tmessage: e.message,\n\t\t\t}\n\t\t}\n\t}\n\n\tasync validateFields(options: {\n\t\tschema: DataSourceSchema\n\t\tfields: string[]\n\t\tx_request_id?: string\n\t}): Promise<ValidateFieldsResponse> {\n\t\ttry {\n\t\t\tconst validated: string[] = []\n\t\t\tlet relations: DataSourceRelations[] = []\n\n\t\t\tfor (const field of options.fields) {\n\t\t\t\tif (field === '') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tif (field.includes('.')) {\n\t\t\t\t\trelations = await this.convertDeepField({\n\t\t\t\t\t\tfield,\n\t\t\t\t\t\tschema: options.schema,\n\t\t\t\t\t\trelations,\n\t\t\t\t\t\tx_request_id: options.x_request_id,\n\t\t\t\t\t})\n\t\t\t\t} else {\n\t\t\t\t\tif (this.validateField(options.schema, field)) {\n\t\t\t\t\t\tvalidated.push(field)\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tvalid: false,\n\t\t\t\t\t\t\tmessage: `Field ${field} not found in table schema for ${options.schema.table}`,\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tvalid: true,\n\t\t\t\tfields: validated,\n\t\t\t\trelations,\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.logger.debug(`[validateFields] ${e.message}`, options.x_request_id)\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\tmessage: `Error parsing fields ${options.fields}`,\n\t\t\t}\n\t\t}\n\t}\n\n\tvalidateField(schema: DataSourceSchema, field: string): boolean {\n\t\treturn schema.columns.find(col => col.field === field) ? true : false\n\t}\n\n\t/**\n\t * Validate relations by ensuring that the relation exists in the schema\n\t */\n\n\tasync validateRelations(options: {\n\t\tschema: DataSourceSchema\n\t\trelation_query: string[]\n\t\texisting_relations: DataSourceRelations[]\n\t\tx_request_id?: string\n\t}): Promise<validateRelationsResponse> {\n\t\ttry {\n\t\t\tconst relations = options.relation_query\n\t\t\tconst validated: DataSourceRelations[] = []\n\n\t\t\tfor (const relation of relations) {\n\t\t\t\tif (relation.includes('.')) {\n\t\t\t\t\tconst relations = await this.convertDeepRelation({\n\t\t\t\t\t\trelation,\n\t\t\t\t\t\tschema: options.schema,\n\t\t\t\t\t\tx_request_id: options.x_request_id,\n\t\t\t\t\t})\n\n\t\t\t\t\tfor (const rel of relations) {\n\t\t\t\t\t\tif (options.existing_relations.find(relation => relation.table === rel.table)) {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tvalidated.push(rel)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif (\n\t\t\t\t\t\t!options.schema.relations.find(col => col.table === relation) &&\n\t\t\t\t\t\t!options.schema.relations.find(col => col.org_table === relation)\n\t\t\t\t\t) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tvalid: false,\n\t\t\t\t\t\t\tmessage: `Relation ${relation} not found in table schema for ${options.schema.table} `,\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif (options.existing_relations.find(rel => rel.table === relation)) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tconst relation_schema = await this.getSchema({\n\t\t\t\t\t\ttable: relation,\n\t\t\t\t\t\tx_request_id: options.x_request_id,\n\t\t\t\t\t})\n\n\t\t\t\t\tlet join\n\n\t\t\t\t\tif (options.schema.relations.find(col => col.table === relation)) {\n\t\t\t\t\t\tjoin = options.schema.relations.find(col => col.table === relation)\n\t\t\t\t\t} else if (options.schema.relations.find(col => col.org_table === relation)) {\n\t\t\t\t\t\tjoin = options.schema.relations.find(col => col.org_table === relation)\n\t\t\t\t\t}\n\n\t\t\t\t\tvalidated.push({\n\t\t\t\t\t\ttable: relation,\n\t\t\t\t\t\tjoin,\n\t\t\t\t\t\tcolumns: relation_schema.columns.map(col => col.field),\n\t\t\t\t\t\tschema: relation_schema,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tvalid: true,\n\t\t\t\trelations: validated,\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.logger.debug(`[validateRelations] ${e.message}`, options.x_request_id)\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\tmessage: `Error parsing relations ${options.relation_query}`,\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Validate params for where builder, format is column[operator]=value with operator being from the enum WhereOperator\n\t *\n\t * Example: ?id[equals]=1&name=John&age[gte]=21\n\t */\n\n\tasync validateWhereParams(options: { schema: DataSourceSchema; params: any }): Promise<validateWhereResponse> {\n\t\tconst where: DataSourceWhere[] = []\n\n\t\tfor (const param in options.params) {\n\t\t\tif (NON_FIELD_PARAMS.includes(param)) continue\n\n\t\t\tconst column = param\n\n\t\t\tif (column.includes('.')) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tlet field: string\n\t\t\tlet operator: WhereOperator\n\t\t\tlet value: any\n\n\t\t\tswitch (typeof column) {\n\t\t\t\tcase 'string':\n\t\t\t\t\tfield = column.split('[')[0]\n\t\t\t\t\tconst singleOperator = column.split('[')[1]?.split(']')[0]\n\t\t\t\t\toperator = WhereOperator[singleOperator] ?? WhereOperator.equals\n\t\t\t\t\tvalue = options.params[column]\n\t\t\t\t\tbreak\n\t\t\t\tcase 'object':\n\t\t\t\t\tconst operators = Object.keys(options.params[param]) as WhereOperator[]\n\t\t\t\t\toperator = operators[0]\n\n\t\t\t\t\tif (!operator) {\n\t\t\t\t\t\toperator = WhereOperator.equals\n\t\t\t\t\t}\n\t\t\t\t\tfield = options.params[param][operator].split('[')[0]\n\t\t\t\t\tvalue = options.params[param][operator]\n\t\t\t\t\toperator = WhereOperator[operator]\n\t\t\t\t\tbreak\n\n\t\t\t\tdefault:\n\t\t\t\t\treturn {\n\t\t\t\t\t\tvalid: false,\n\t\t\t\t\t\tmessage: `Invalid where param ${param}`,\n\t\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!options.schema.columns.find(col => col.field === field)) {\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: `Column ${column} not found in schema`,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!Object.values(WhereOperator).includes(operator)) {\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: `Operator ${operator} not found`,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlet validation\n\n\t\t\tif (operator === WhereOperator.in || operator === WhereOperator.not_in) {\n\t\t\t\tconst valueArray = Array.isArray(value)\n\t\t\t\t\t? value\n\t\t\t\t\t: value\n\t\t\t\t\t\t\t.toString()\n\t\t\t\t\t\t\t.split(',')\n\t\t\t\t\t\t\t.map(v => v.trim())\n\t\t\t\tfor (const val of valueArray) {\n\t\t\t\t\tvalidation = await this.validateData(options.schema, { [field]: val })\n\t\t\t\t\tif (!validation.valid) {\n\t\t\t\t\t\treturn validation\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tvalidation = await this.validateData(options.schema, { [field]: value })\n\t\t\t}\n\n\t\t\tif (!validation.valid) {\n\t\t\t\treturn validation\n\t\t\t}\n\n\t\t\twhere.push({\n\t\t\t\tcolumn: field,\n\t\t\t\toperator,\n\t\t\t\tvalue,\n\t\t\t})\n\t\t}\n\n\t\treturn {\n\t\t\tvalid: true,\n\t\t\twhere,\n\t\t}\n\t}\n\n\t/**\n\t * Validate order params, format is sort={column}.{operator},column.{operator},...\n\t *\n\t * Operator is either `asc` or `desc`\n\t *\n\t * Example: ?sort=name.asc,id.desc,content.title.asc\n\t */\n\n\tvalidateSort(options: { schema: DataSourceSchema; sort: string[] }): ValidateSortResponse {\n\t\tconst array = options.sort?.filter(sort => !sort.includes('.'))\n\n\t\tfor (const item of array) {\n\t\t\tconst direction = item.lastIndexOf('.')\n\n\t\t\tif (direction === -1) {\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: `Invalid order param ${item}, missing direction, must be either ${item}.asc or ${item}.desc`,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst operator = item.substring(direction)\n\n\t\t\tif (operator !== 'asc' && operator !== 'desc') {\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: `Invalid order operator ${operator}, must be either asc or desc`,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst column = item.substring(0, direction)\n\n\t\t\tif (!options.schema.columns.find(col => col.field === column)) {\n\t\t\t\treturn {\n\t\t\t\t\tvalid: false,\n\t\t\t\t\tmessage: `Column ${column} not found in schema`,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tvalid: true,\n\t\t\tsort: this.createSortArray(options.sort),\n\t\t}\n\t}\n\n\t/**\n\t * Convert where into where and relations\n\t */\n\n\tasync convertDeepWhere(options: {\n\t\twhere: DataSourceWhere\n\t\tschema: DataSourceSchema\n\t\tx_request_id?: string\n\t}): Promise<DataSourceRelations[]> {\n\t\tconst relations: DataSourceRelations[] = []\n\n\t\t//deconstruct the column to create the relations of each table in the items object\n\t\tlet items = options.where.column.split('.')\n\n\t\tfor (let i = 0; i < items.length - 1; i++) {\n\t\t\tif (!options.schema.relations.find(col => col.table === items[i])) {\n\t\t\t\tthis.logger.error(\n\t\t\t\t\t`Relation ${items[i]} not found in schema for ${options.schema.table}`,\n\t\t\t\t\toptions.x_request_id,\n\t\t\t\t)\n\t\t\t\tthis.logger.error(options)\n\t\t\t\tthrow new Error(`Relation ${items[i]} not found in schema for ${options.schema.table}`)\n\t\t\t}\n\n\t\t\tconst relation_schema = await this.getSchema({ table: items[i], x_request_id: options.x_request_id })\n\n\t\t\tconst relation = {\n\t\t\t\ttable: items[i],\n\t\t\t\tjoin: {\n\t\t\t\t\t...options.schema.relations.find(col => col.table === items[i]),\n\t\t\t\t},\n\t\t\t\twhere: i === items.length - 2 ? options.where : undefined,\n\t\t\t\tschema: relation_schema,\n\t\t\t}\n\n\t\t\trelations.push(relation)\n\n\t\t\toptions.schema = relation_schema\n\t\t}\n\n\t\treturn relations\n\t}\n\n\t/**\n\t * Convert where into where and relations\n\t */\n\n\tasync convertDeepField(options: {\n\t\tfield: string\n\t\tschema: DataSourceSchema\n\t\trelations: DataSourceRelations[]\n\t\tx_request_id?: string\n\t}): Promise<DataSourceRelations[]> {\n\t\t//deconstruct the column to create the relations of each table in the items object\n\t\tlet items = options.field.split('.')\n\n\t\tfor (let i = 0; i < items.length - 1; i++) {\n\t\t\tif (\n\t\t\t\t!options.schema.relations.find(col => col.table === items[i]) &&\n\t\t\t\t!options.schema.relations.find(col => col.org_table === items[i])\n\t\t\t) {\n\t\t\t\tthis.logger.error(\n\t\t\t\t\t`Relation field ${items[i]} not found in schema for ${options.schema.table}`,\n\t\t\t\t\toptions.x_request_id,\n\t\t\t\t)\n\t\t\t\tthrow new Error(`Relation field ${items[i]} not found in schema for ${options.schema.table}`)\n\t\t\t}\n\n\t\t\tconst relation_schema = await this.getSchema({ table: items[i], x_request_id: options.x_request_id })\n\n\t\t\tif (options.relations.find(rel => rel.table === items[i])) {\n\t\t\t\tconst index = options.relations.findIndex(rel => rel.table === items[i])\n\t\t\t\tif (i === items.length - 2) {\n\t\t\t\t\toptions.relations[index].columns.push(items[items.length - 1])\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlet join\n\n\t\t\t\tif (options.schema.relations.find(col => col.table === items[i])) {\n\t\t\t\t\tjoin = options.schema.relations.find(col => col.table === items[i])\n\t\t\t\t} else if (options.schema.relations.find(col => col.org_table === items[i])) {\n\t\t\t\t\tjoin = options.schema.relations.find(col => col.org_table === items[i])\n\t\t\t\t}\n\n\t\t\t\toptions.relations.push({\n\t\t\t\t\ttable: items[i],\n\t\t\t\t\tjoin,\n\t\t\t\t\tcolumns: i === items.length - 2 ? [items[items.length - 1]] : undefined,\n\t\t\t\t\tschema: relation_schema,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\toptions.schema = relation_schema\n\t\t}\n\n\t\treturn options.relations\n\t}\n\n\t/**\n\t * Convert relation into relations\n\t */\n\n\tasync convertDeepRelation(options: {\n\t\trelation: string\n\t\tschema: DataSourceSchema\n\t\tx_request_id?: string\n\t}): Promise<DataSourceRelations[]> {\n\t\tconst relations: DataSourceRelations[] = []\n\n\t\t//deconstruct the column to create the relations of each table in the items object\n\t\tlet items = options.relation.split('.')\n\n\t\tfor (let i = 0; i < items.length - 1; i++) {\n\t\t\tif (!options.schema.relations.find(col => col.table === items[i])) {\n\t\t\t\tthis.logger.error(\n\t\t\t\t\t`Deep Relation ${items[i]} not found in schema for ${options.schema.table}`,\n\t\t\t\t\toptions.x_request_id,\n\t\t\t\t)\n\t\t\t\tthis.logger.error(options)\n\t\t\t\tthrow new Error(`Deep Relation ${items[i]} not found in schema for ${options.schema.table}`)\n\t\t\t}\n\n\t\t\tconst relation_schema = await this.getSchema({ table: items[i], x_request_id: options.x_request_id })\n\n\t\t\tlet join\n\n\t\t\tif (options.schema.relations.find(col => col.table === items[i])) {\n\t\t\t\tjoin = options.schema.relations.find(col => col.table === items[i])\n\t\t\t} else if (options.schema.relations.find(col => col.org_table === items[i])) {\n\t\t\t\tjoin = options.schema.relations.find(col => col.org_table === items[i])\n\t\t\t}\n\n\t\t\trelations.push({\n\t\t\t\ttable: items[i],\n\t\t\t\tjoin,\n\t\t\t\tcolumns: i === items.length - 1 ? [items[items.length]] : undefined,\n\t\t\t\tschema: relation_schema,\n\t\t\t})\n\n\t\t\toptions.schema = relation_schema\n\t\t}\n\n\t\treturn relations\n\t}\n\n\t/**\n\t * Takes the sort query parameter and returns the sort object\n\t */\n\n\tcreateSortArray(sort: string[]): SortCondition[] {\n\t\tif (!sort) return []\n\n\t\tconst sortArray = []\n\n\t\tfor (const item of sort) {\n\t\t\tconst direction = item.lastIndexOf('.')\n\t\t\tconst column = item.substring(0, direction)\n\t\t\tconst operator = item.substring(direction + 1)\n\t\t\tsortArray.push({ column, operator: operator.toUpperCase() })\n\t\t}\n\n\t\treturn sortArray\n\t}\n}\n"
  },
  {
    "path": "src/helpers/Webhook.ts",
    "content": "import { CACHE_MANAGER } from '@nestjs/cache-manager'\nimport { Inject, Injectable } from '@nestjs/common'\nimport { ConfigService } from '@nestjs/config'\nimport axios from 'axios'\nimport { Cache } from 'cache-manager'\n\nimport { CACHE_DEFAULT_WEBHOOK_TTL, LLANA_WEBHOOK_LOG_TABLE, LLANA_WEBHOOK_TABLE } from '../app.constants'\nimport { FindManyResponseObject, FindOneResponseObject } from '../dtos/response.dto'\nimport { WebhookLog } from '../dtos/webhook.dto'\nimport { Webhook as WebhookType } from '../dtos/webhook.dto'\nimport { DataSourceSchema, PublishType, QueryPerform, WhereOperator } from '../types/datasource.types'\nimport { RolePermission } from '../types/roles.types'\nimport { Authentication } from './Authentication'\nimport { Logger } from './Logger'\nimport { Query } from './Query'\nimport { Schema } from './Schema'\n\n@Injectable()\nexport class Webhook {\n\tconstructor(\n\t\t@Inject(CACHE_MANAGER) private cacheManager: Cache,\n\t\tprivate readonly authentication: Authentication,\n\t\tprivate readonly configService: ConfigService,\n\t\tprivate readonly logger: Logger,\n\t\tprivate readonly query: Query,\n\t\tprivate readonly schema: Schema,\n\t) {}\n\n\tasync publish(\n\t\tschema: DataSourceSchema,\n\t\ttype: PublishType,\n\t\tid: string | number,\n\t\tuser_identifier?: string | number,\n\t): Promise<void> {\n\t\tif (this.configService.get<boolean>('DISABLE_WEBHOOKS')) {\n\t\t\treturn\n\t\t}\n\n\t\tthis.logger.debug(`[Webhook] Publishing ${schema.table} ${type} for #${id}`)\n\n\t\tconst webhookSchema = await this.schema.getSchema({ table: LLANA_WEBHOOK_TABLE })\n\t\tconst webhookLogSchema = await this.schema.getSchema({ table: LLANA_WEBHOOK_LOG_TABLE })\n\n\t\tconst webhooksWhere = [\n\t\t\t{\n\t\t\t\tcolumn: 'table',\n\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\tvalue: schema.table,\n\t\t\t},\n\t\t]\n\n\t\tlet webhooks = <FindManyResponseObject>await this.cacheManager.get(`webhooks:all:${schema.table}`)\n\n\t\tif (!webhooks) {\n\t\t\twebhooks = (await this.query.perform(QueryPerform.FIND_MANY, {\n\t\t\t\tschema: webhookSchema,\n\t\t\t\twhere: [\n\t\t\t\t\t...webhooksWhere,\n\t\t\t\t\t{\n\t\t\t\t\t\tcolumn: 'user_identifier',\n\t\t\t\t\t\toperator: WhereOperator.null,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t})) as FindManyResponseObject\n\n\t\t\tawait this.cacheManager.set(\n\t\t\t\t`webhooks:all:${schema.table}`,\n\t\t\t\twebhooks,\n\t\t\t\tthis.configService.get('CACHE_WEBHOOKS_TTL') ?? CACHE_DEFAULT_WEBHOOK_TTL,\n\t\t\t)\n\t\t}\n\n\t\tif (user_identifier) {\n\t\t\tlet webhooksUser = <FindManyResponseObject>(\n\t\t\t\tawait this.cacheManager.get(`webhooks:${schema.table}:${user_identifier}`)\n\t\t\t)\n\n\t\t\tif (!webhooksUser) {\n\t\t\t\twebhooksUser = (await this.query.perform(QueryPerform.FIND_MANY, {\n\t\t\t\t\tschema: webhookSchema,\n\t\t\t\t\twhere: [\n\t\t\t\t\t\t...webhooksWhere,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcolumn: 'user_identifier',\n\t\t\t\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\t\t\t\tvalue: user_identifier.toString(),\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t})) as FindManyResponseObject\n\n\t\t\t\tawait this.cacheManager.set(\n\t\t\t\t\t`webhooks:${schema.table}:${user_identifier}`,\n\t\t\t\t\twebhooksUser,\n\t\t\t\t\tthis.configService.get('CACHE_WEBHOOKS_TTL') ?? CACHE_DEFAULT_WEBHOOK_TTL,\n\t\t\t\t)\n\n\t\t\t\twebhooks.data = webhooks.data.concat(webhooksUser.data)\n\t\t\t}\n\t\t}\n\n\t\tfor (const webhook of webhooks.data) {\n\t\t\tif (user_identifier) {\n\t\t\t\tconst auth = await this.authentication.auth({\n\t\t\t\t\ttable: schema.table,\n\t\t\t\t\taccess: RolePermission.READ,\n\t\t\t\t\tuser_identifier: user_identifier.toString(),\n\t\t\t\t})\n\n\t\t\t\tif (!auth.valid) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawait this.query.perform(QueryPerform.CREATE, {\n\t\t\t\tschema: webhookLogSchema,\n\t\t\t\tdata: <WebhookLog>{\n\t\t\t\t\twebhook_id: webhook.id,\n\t\t\t\t\ttype,\n\t\t\t\t\turl: webhook.url,\n\t\t\t\t\trecord_key: schema.primary_key,\n\t\t\t\t\trecord_id: id,\n\t\t\t\t\tdelivered: false,\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\t}\n\n\tasync getPendingWebhooks(): Promise<WebhookLog[]> {\n\t\tif (this.configService.get<boolean>('DISABLE_WEBHOOKS')) {\n\t\t\treturn\n\t\t}\n\n\t\tconst webhookLogSchema = await this.schema.getSchema({ table: LLANA_WEBHOOK_LOG_TABLE })\n\t\tconst webhooks = (await this.query.perform(QueryPerform.FIND_MANY, {\n\t\t\tschema: webhookLogSchema,\n\t\t\twhere: [\n\t\t\t\t{\n\t\t\t\t\tcolumn: 'delivered',\n\t\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\t\tvalue: false,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tcolumn: 'next_attempt_at',\n\t\t\t\t\toperator: WhereOperator.lt,\n\t\t\t\t\tvalue: new Date().toISOString(),\n\t\t\t\t},\n\t\t\t],\n\t\t\tlimit: 999,\n\t\t})) as FindManyResponseObject\n\n\t\treturn webhooks.data as WebhookLog[]\n\t}\n\n\tasync sendWebhook(webhook: WebhookLog): Promise<void> {\n\t\tif (this.configService.get<boolean>('DISABLE_WEBHOOKS')) {\n\t\t\treturn\n\t\t}\n\n\t\tconst webhookLogSchema = await this.schema.getSchema({ table: LLANA_WEBHOOK_LOG_TABLE })\n\n\t\ttry {\n\t\t\tconst response = await axios({\n\t\t\t\tmethod: webhook.type,\n\t\t\t\turl: webhook.url,\n\t\t\t\tdata: {\n\t\t\t\t\twebhook_id: webhook.id,\n\t\t\t\t\ttype: webhook.type,\n\t\t\t\t\t[webhook.record_key]: webhook.record_id,\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tawait this.query.perform(QueryPerform.UPDATE, {\n\t\t\t\tid: webhook.id.toString(),\n\t\t\t\tschema: webhookLogSchema,\n\t\t\t\tdata: <Partial<WebhookLog>>{\n\t\t\t\t\tresponse_status: response.status,\n\t\t\t\t\tresponse_message: response.statusText,\n\t\t\t\t\tdelivered: true,\n\t\t\t\t\tdelivered_at: new Date(),\n\t\t\t\t\tnext_attempt_at: null,\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tthis.logger.debug(`[Webhook] Sending ${webhook.type} to ${webhook.url}`)\n\t\t} catch (e: any) {\n\t\t\tthis.logger.warn(`[Webhook] Error sending ${webhook.type} to ${webhook.url} - ${e.message}`)\n\n\t\t\tlet next_attempt_at = new Date(Date.now() + webhook.attempt * webhook.attempt * webhook.attempt * 60000)\n\n\t\t\tif (webhook.attempt >= 5) {\n\t\t\t\tnext_attempt_at = null\n\t\t\t}\n\n\t\t\tawait this.query.perform(QueryPerform.UPDATE, {\n\t\t\t\tid: webhook.id.toString(),\n\t\t\t\tschema: webhookLogSchema,\n\t\t\t\tdata: <Partial<WebhookLog>>{\n\t\t\t\t\tattempt: webhook.attempt + 1,\n\t\t\t\t\tnext_attempt_at: next_attempt_at,\n\t\t\t\t\tresponse_status: e.response.status ?? 500,\n\t\t\t\t\tresponse_message: e.response.message ?? e.message,\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\t}\n\n\tasync addWebhook(data: Partial<WebhookType>): Promise<FindOneResponseObject> {\n\t\tconst schema = await this.schema.getSchema({ table: LLANA_WEBHOOK_TABLE })\n\t\tconst result = (await this.query.perform(QueryPerform.CREATE, {\n\t\t\tschema,\n\t\t\tdata,\n\t\t})) as FindOneResponseObject\n\t\tawait this.cacheManager.del(`webhooks:${data.table}:*`)\n\t\treturn result\n\t}\n\n\tasync editWebhook(id: string, data: Partial<WebhookType>): Promise<FindOneResponseObject> {\n\t\tconst schema = await this.schema.getSchema({ table: LLANA_WEBHOOK_TABLE })\n\t\tconst result = (await this.query.perform(QueryPerform.UPDATE, {\n\t\t\tschema,\n\t\t\twhere: [\n\t\t\t\t{\n\t\t\t\t\tcolumn: 'id',\n\t\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\t\tvalue: id,\n\t\t\t\t},\n\t\t\t],\n\t\t\tdata,\n\t\t})) as FindOneResponseObject\n\t\tawait this.cacheManager.del(`webhooks:${data.table}:*`)\n\t\treturn result\n\t}\n\n\tasync deleteWebhook(id: string): Promise<void> {\n\t\tconst schema = await this.schema.getSchema({ table: LLANA_WEBHOOK_TABLE })\n\t\tconst webhook = (await this.query.perform(QueryPerform.FIND_ONE, {\n\t\t\tschema,\n\t\t\twhere: [\n\t\t\t\t{\n\t\t\t\t\tcolumn: 'id',\n\t\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\t\tvalue: id,\n\t\t\t\t},\n\t\t\t],\n\t\t})) as FindOneResponseObject\n\n\t\tif (!webhook) {\n\t\t\treturn\n\t\t}\n\n\t\tawait this.query.perform(QueryPerform.DELETE, {\n\t\t\tschema,\n\t\t\twhere: [\n\t\t\t\t{\n\t\t\t\t\tcolumn: 'id',\n\t\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\t\tvalue: id,\n\t\t\t\t},\n\t\t\t],\n\t\t})\n\n\t\tawait this.cacheManager.del(`webhooks:${webhook.table}:*`)\n\t}\n}\n"
  },
  {
    "path": "src/main.ts",
    "content": "import 'dotenv/config'\nimport 'reflect-metadata'\n\nimport { ValidationPipe } from '@nestjs/common'\nimport { NestFactory } from '@nestjs/core'\nimport { NestExpressApplication } from '@nestjs/platform-express'\nimport { join } from 'path'\n\nimport { APP_BOOT_CONTEXT } from './app.constants'\nimport { AppModule } from './app.module'\nimport { Logger } from './helpers/Logger'\nimport { WelcomeModule } from './modules/welcome/welcome.module'\n\nasync function bootstrap() {\n\tconst logger = new Logger()\n\tlogger.status()\n\tlet app\n\n\tif (!process.env.DATABASE_URI) {\n\t\tapp = await NestFactory.create<NestExpressApplication>(WelcomeModule)\n\t\tapp.useStaticAssets(join(__dirname, '..', 'public'))\n\t\tapp.setBaseViewsDir(join(__dirname, '..', 'views'))\n\t\tapp.setViewEngine('hbs')\n\t} else {\n\t\tapp = await NestFactory.create<NestExpressApplication>(AppModule)\n\t}\n\n\tapp.enableCors({\n\t\torigin: process.env.BASE_URL_APP || true,\n\t\tcredentials: true,\n\t})\n\tawait app.listen(process.env.PORT)\n\n\tapp.useGlobalPipes(\n\t\tnew ValidationPipe({\n\t\t\ttransform: true,\n\t\t}),\n\t)\n\tlet url = await app.getUrl()\n\turl = url.replace('[::1]', 'localhost')\n\tlogger.log(`Application is running on: ${url}`, APP_BOOT_CONTEXT)\n\n\tif (process.env.TZ) {\n\t\tlogger.log(`Timezone is set to: ${process.env.TZ}. Current time: ${new Date()}`, APP_BOOT_CONTEXT)\n\t}\n}\nbootstrap()\n"
  },
  {
    "path": "src/middleware/HostCheck.ts",
    "content": "import { Injectable, NestMiddleware } from '@nestjs/common'\nimport { ConfigService } from '@nestjs/config'\nimport { NextFunction, Request, Response } from 'express'\n\nimport { Logger } from '../helpers/Logger'\nimport { Env } from '../utils/Env'\n\n@Injectable()\nexport class HostCheckMiddleware implements NestMiddleware {\n\tconstructor(\n\t\tprivate readonly configService: ConfigService,\n\t\tprivate readonly logger: Logger,\n\t) {}\n\n\tuse(req: Request, res: Response, next: NextFunction) {\n\t\tif (this.validateHost(req.headers, 'HTTP')) {\n\t\t\tnext()\n\t\t} else {\n\t\t\tres.status(403).send('Forbidden')\n\t\t\treturn\n\t\t}\n\t}\n\n\t/**\n\t * Validate host\n\t *\n\t * Returns true if host is allowed, false if not\n\t */\n\n\tvalidateHost(headers: any, domain?: string): boolean {\n\t\tlet ip = headers['x-real-ip']\n\t\tif (!ip) ip = headers['x-forwarded-for']\n\t\tif (!ip) ip = headers['address']\n\n\t\tif (!ip) {\n\t\t\tthis.logger.debug(`${domain ? domain + ' ' : ''}No IP found`)\n\t\t} else {\n\t\t\tthis.logger.debug(`${domain ? domain + ' ' : ''}Client connecting from ${ip}`)\n\t\t}\n\n\t\tlet allowed_hosts = this.configService.get<string>('HOSTS')?.split(',') ?? []\n\n\t\t//remove blank entries e.g. [\"\"] -> []\n\t\tallowed_hosts = allowed_hosts.filter(host => host)\n\n\t\tif (allowed_hosts.length === 0) {\n\t\t\treturn true\n\t\t}\n\n\t\tif (Env.IsTest()) {\n\t\t\tif (allowed_hosts.includes('localhost')) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\n\t\tfor (const host of allowed_hosts) {\n\t\t\tif (ip === host) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\n\t\tif (Env.IsDev()) {\n\t\t\tthis.logger.warn(\n\t\t\t\t`${domain ? domain + ' ' : ''}Host not in approved list, skipping forbidden response as in dev mode`,\n\t\t\t\t{\n\t\t\t\t\thost: ip,\n\t\t\t\t\tallowed_hosts,\n\t\t\t\t},\n\t\t\t)\n\t\t\treturn true\n\t\t} else {\n\t\t\tthis.logger.debug(`${domain ? domain + ' ' : ''}Host not in approved list, returning forbidden response`, {\n\t\t\t\thost: ip,\n\t\t\t\tallowed_hosts,\n\t\t\t\theaders,\n\t\t\t})\n\t\t\treturn false\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/middleware/Robots.ts",
    "content": "import { Injectable, NestMiddleware } from '@nestjs/common'\nimport { NextFunction, Request, Response } from 'express'\n\n@Injectable()\nexport class RobotsMiddleware implements NestMiddleware {\n\tuse(req: Request, res: Response, next: NextFunction) {\n\t\tres.setHeader('X-Robots-Tag', 'noindex, nofollow')\n  \t\treturn next()\n\t}\n}\n"
  },
  {
    "path": "src/middleware/request-path-logger.middleware.ts",
    "content": "import { Injectable, NestMiddleware } from '@nestjs/common'\nimport { NextFunction, Request, Response } from 'express'\n\nimport { Logger } from '../helpers/Logger'\n\n@Injectable()\nexport class RequestPathLoggerMiddleware implements NestMiddleware {\n\tuse(req: Request, res: Response, next: NextFunction) {\n\t\tconst logger = new Logger()\n\n\n\n\t\t// Alphabetize query parameters for GET requests with query parameters\n\t\tif(req.method === 'GET' && Object.keys(req.query).length) {\n\n\t\t\t//get the request query parameters and put them in alphabetical order\n\t\t\tconst queryParams = Object.keys(req.query)\n\t\t\t\t.sort()\n\t\t\t\t.flatMap(key => {\n\t\t\t\t\tconst value = req.query[key]\n\t\t\t\t\tif (Array.isArray(value)) {\n\t\t\t\t\t\treturn value.map(v => `${key}=${(v as string)}`)\n\t\t\t\t\t}\n\t\t\t\t\treturn [`${key}=${(value as string)}`]\n\t\t\t\t})\n\t\t\t\n\t\t\t// Replace query parameters in the URL with sorted query parameters\n\t\t\tif (queryParams.length > 0) {\n\t\t\t\tconst sortedQueryString = queryParams.join('&')\n\t\t\t\treq.originalUrl = decodeURI(req.originalUrl.split('?')[0] + '?' + sortedQueryString)\n\t\t\t}\n\n\t\t\tlogger.debug(`[RequestPathLoggerMiddleware] ${req.method}: ${req.originalUrl}`, {\n\t\t\t\tquery: {\n\t\t\t\t\toriginal: req.query,\n\t\t\t\t\tsorted: queryParams,\n\t\t\t\t},\n\t\t\t\tbody: req.body,\n\t\t\t\tfinal_url: req.originalUrl,\n\t\t\t})\n\t\t}else{\n\t\t\tlogger.debug(`[RequestPathLoggerMiddleware] ${req.method}: ${req.originalUrl}`)\n\t\t}\n\n\t\tnext()\n\t}\n}\n"
  },
  {
    "path": "src/modules/cache/dataCache.constants.ts",
    "content": "export const REDIS_CACHE_TOKEN = 'REDIS_CACHE'\n"
  },
  {
    "path": "src/modules/cache/dataCache.service.ts",
    "content": "import { CACHE_MANAGER } from '@nestjs/cache-manager'\nimport { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'\nimport { ConfigService } from '@nestjs/config'\nimport { CronExpression } from '@nestjs/schedule'\nimport { Cache } from 'cache-manager'\nimport Redis from 'ioredis'\nimport { CACHE_DEFAULT_TABLE_SCHEMA_TTL } from 'src/app.constants'\n\nimport { FindManyResponseObject } from '../../dtos/response.dto'\nimport { Logger } from '../../helpers/Logger'\nimport { Query } from '../../helpers/Query'\nimport { Schema } from '../../helpers/Schema'\nimport { DataSourceFindOneOptions, QueryPerform } from '../../types/datasource.types'\nimport { cronToSeconds } from '../../utils/String'\nimport { REDIS_CACHE_TOKEN } from './dataCache.constants'\n\nconst tableCacheKey = `dataCache:_llana_data_caching`\n\n@Injectable()\nexport class DataCacheService implements OnApplicationShutdown {\n\tconstructor(\n\t\tprivate readonly logger: Logger,\n\t\t@Inject(REDIS_CACHE_TOKEN) private readonly redis: Redis,\n\t\t@Inject(CACHE_MANAGER) private cacheManager: Cache,\n\t\tprivate readonly configService: ConfigService,\n\t\tprivate readonly query: Query,\n\t\tprivate readonly schema: Schema,\n\t) {}\n\n\tonApplicationShutdown() {\n\t\tif (this.useRedis()) {\n\t\t\tthis.redis.disconnect()\n\t\t}\n\t}\n\n\tpublic useRedis(): boolean {\n\t\tconst redisPort = this.configService.get<string>('REDIS_PORT')\n\t\tconst redisHost = this.configService.get<string>('REDIS_HOST')\n\t\treturn !!(redisPort && redisHost)\n\t}\n\n\tpublic cacheType(): 'READ' | 'WRITE' | undefined {\n\t\tif (!this.configService.get<string>('USE_DATA_CACHING')) {\n\t\t\treturn undefined\n\t\t}\n\n\t\tif (this.configService.get<string>('USE_DATA_CACHING') === 'READ') {\n\t\t\treturn 'READ'\n\t\t}\n\n\t\tif (this.configService.get<string>('USE_DATA_CACHING') === 'WRITE') {\n\t\t\treturn 'WRITE'\n\t\t}\n\n\t\tif (this.configService.get<boolean>('USE_DATA_CACHING')) {\n\t\t\treturn 'WRITE'\n\t\t}\n\t}\n\n\t/**\n\t * Read from cache\n\t * * Will use Redis if available\n\t * * otherwise will use in-memory cache\n\t */\n\n\tpublic async read(key: string): Promise<any> {\n\t\tif (this.useRedis()) {\n\t\t\tif (this.redis.status !== 'ready') {\n\t\t\t\tthrow new Error('Redis client not ready')\n\t\t\t}\n\n\t\t\tconst value = await this.redis.get(key)\n\t\t\treturn value ? JSON.parse(value) : undefined\n\t\t} else {\n\t\t\treturn await this.cacheManager.get(key)\n\t\t}\n\t}\n\n\t/**\n\t * Write to cache\n\t * * Will use Redis if available\n\t * * otherwise will use in-memory cache\n\t */\n\n\tpublic async write(key: string, value: any, ttl: number): Promise<void> {\n\t\n\t\tif (this.useRedis()) {\n\t\t\tif (this.redis.status !== 'ready') {\n\t\t\t\tthrow new Error('Redis client not ready')\n\t\t\t}\n\n\t\t\tawait this.redis.set(key, JSON.stringify(value), 'PX', ttl)\n\t\t} else {\n\t\t\tawait this.cacheManager.set(key, value, ttl)\n\t\t}\n\t}\n\n\t/**\n\t * Delete from cache\n\t * * Will use Redis if available\n\t * * otherwise will use in-memory cache\n\t */\n\n\tpublic async del(key: string): Promise<void> {\n\t\tif (this.useRedis()) {\n\t\t\tif (this.redis.status !== 'ready') {\n\t\t\t\tthrow new Error('Redis client not ready')\n\t\t\t}\n\n\t\t\tawait this.redis.del(key)\n\t\t} else {\n\t\t\tawait this.cacheManager.del(key)\n\t\t}\n\t}\n\n\t/**\n\t * Checks the request to see if we have a _llana_data_caching match and if so returns it\n\t */\n\n\tasync get(options: { originalUrl: string; x_request_id: string }): Promise<FindManyResponseObject | undefined> {\n\t\tif (!this.cacheType()) {\n\t\t\tthis.logger.debug(`${options.x_request_id ? '[' + options.x_request_id + ']' : ''}[DataCache][Get] Cache is not enabled`)\n\t\t\treturn\n\t\t}\n\n\t\tconst urlParts = options.originalUrl.split('?')\n\t\tconst table = urlParts[0].split('/')[1]\n\t\tconst request = urlParts[1] ? `?${urlParts[1]}` : undefined\n\n\t\tthis.logger.debug(`${options.x_request_id ? '[' + options.x_request_id + ']' : ''}[DataCache][Get] Table: ${table}, Request: ${request}`)\n\n\t\tif (!table) {\n\t\t\tthis.logger.error(\n\t\t\t\t`${options.x_request_id ? '[' + options.x_request_id + ']' : ''}[DataCache][Get] Table not provided`,\n\t\t\t)\n\t\t\treturn\n\t\t}\n\n\t\tif (!request) {\n\t\t\tthis.logger.error(\n\t\t\t\t`${options.x_request_id ? '[' + options.x_request_id + ']' : ''}[DataCache][Get] Request not provided`,\n\t\t\t)\n\t\t\treturn\n\t\t}\n\n\t\tconst cacheKey = `dataCache:${table}:${request}`\n\n\t\t//get caching data from table\n\n\t\tlet caching: FindManyResponseObject | undefined = await this.read(tableCacheKey)\n\n\t\tif (!caching || caching.total === 0) {\n\t\t\tconst schema = await this.schema.getSchema({ table: '_llana_data_caching' })\n\n\t\t\tcaching = (await this.query.perform(\n\t\t\t\tQueryPerform.FIND_MANY,\n\t\t\t\t{\n\t\t\t\t\tschema,\n\t\t\t\t\tlimit: 99999,\n\t\t\t\t},\n\t\t\t\toptions.x_request_id,\n\t\t\t)) as FindManyResponseObject\n\n\t\t\tif (caching && caching.total > 0) {\n\t\t\t\tawait this.write(\n\t\t\t\t\ttableCacheKey,\n\t\t\t\t\tcaching,\n\t\t\t\t\tthis.configService.get<number>('CACHE_TABLE_SCHEMA_TTL') ?? CACHE_DEFAULT_TABLE_SCHEMA_TTL,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\n\t\tif (!caching || caching.total === 0) {\n\t\t\tthis.logger.debug(\n\t\t\t\t`${options.x_request_id ? '[' + options.x_request_id + ']' : ''}[DataCache][Get] No caching data found`,\n\t\t\t)\n\t\t\treturn\n\t\t}\n\n\t\tfor (const cache of caching.data) {\n\t\t\tif (cache.table === table) {\n\t\t\t\tif (cache.request === request) {\n\t\t\t\t\tthis.logger.debug(\n\t\t\t\t\t\t`${options.x_request_id ? '[' + options.x_request_id + ']' : ''}[DataCache][Get] Cache hit for ${table} with request ${request}`,\n\t\t\t\t\t)\n\t\t\t\t\treturn await this.read(cacheKey)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.logger.debug(\n\t\t\t\t\t\t`${options.x_request_id ? '[' + options.x_request_id + ']' : ''}[DataCache][Get] NO cache hit found for ${table} with request ${request}`,\n\t\t)\n\n\t\treturn\n\t}\n\n\t/**\n\t * Updates _llana_data_caching when table data is changed for cache tracking\n\t */\n\n\tasync ping(table: string) {\n\t\tif (!this.cacheType()) {\n\t\t\tthis.logger.debug(`[DataCache][Get] Cache is not enabled`)\n\t\t\treturn\n\t\t}\n\n\t\tconst schema = await this.schema.getSchema({ table: '_llana_data_caching' })\n\n\t\tlet caching: FindManyResponseObject | undefined = await this.read(tableCacheKey)\n\n\t\tif (!caching || caching.total === 0) {\n\t\t\tcaching = (await this.query.perform(QueryPerform.FIND_MANY, {\n\t\t\t\tschema,\n\t\t\t\tlimit: 99999,\n\t\t\t})) as FindManyResponseObject\n\n\t\t\tif (caching && caching.total > 0) {\n\t\t\t\tawait this.write(\n\t\t\t\t\ttableCacheKey,\n\t\t\t\t\tcaching,\n\t\t\t\t\tthis.configService.get<number>('CACHE_TABLE_SCHEMA_TTL') ?? CACHE_DEFAULT_TABLE_SCHEMA_TTL,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\n\t\tif (!caching || caching.total === 0) {\n\t\t\tthis.logger.debug('[DataCache][Ping] No caching data found')\n\t\t\treturn\n\t\t}\n\n\t\tfor (const cache of caching.data) {\n\t\t\tif (cache.table === table) {\n\t\t\t\tawait this.query.perform(QueryPerform.UPDATE, {\n\t\t\t\t\tid: cache.id,\n\t\t\t\t\tschema,\n\t\t\t\t\tdata: {\n\t\t\t\t\t\tdata_changed_at: new Date(),\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Generates the cache date for results which need refreshing\n\t */\n\n\tasync refresh(cronSchedule: CronExpression) {\n\t\tif (!this.cacheType()) {\n\t\t\tthis.logger.debug(`[DataCache][Get] Cache is not enabled`)\n\t\t\treturn\n\t\t}\n\n\t\tif (this.cacheType() === 'READ') {\n\t\t\tthis.logger.debug(`[DataCache][Get] Cache is set to READ, skipping write`)\n\t\t\treturn\n\t\t}\n\n\t\t//get the cache time (now - cron run time)\n\t\tconst cronTimeInSeconds = cronToSeconds(cronSchedule)\n\t\tconst cacheTime = new Date(new Date().getTime() - cronTimeInSeconds * 1000)\n\n\t\tconst schema = await this.schema.getSchema({ table: '_llana_data_caching' })\n\n\t\tconst caching = (await this.query.perform(QueryPerform.FIND_MANY, {\n\t\t\tschema,\n\t\t\tlimit: 99999,\n\t\t})) as FindManyResponseObject\n\n\t\tif (caching && caching.total > 0) {\n\t\t\tawait this.write(\n\t\t\t\ttableCacheKey,\n\t\t\t\tcaching,\n\t\t\t\tthis.configService.get<number>('CACHE_TABLE_SCHEMA_TTL') ?? CACHE_DEFAULT_TABLE_SCHEMA_TTL,\n\t\t\t)\n\t\t}\n\n\t\tif (!caching || caching.total === 0) {\n\t\t\tthis.logger.debug('[DataCache][Refresh] No caching data found')\n\t\t\treturn\n\t\t}\n\n\t\tfor (const cache of caching.data) {\n\t\t\ttry {\n\t\t\t\t//check if cache key exists\n\t\t\t\tconst cacheKey = `dataCache:${cache.table}:${cache.request}`\n\n\t\t\t\tlet cachedItem = await this.read(cacheKey)\n\n\t\t\t\tif (!cachedItem) {\n\t\t\t\t\tthis.logger.debug(\n\t\t\t\t\t\t`[DataCache][Refresh][${cache.id}] Cache not found for ${cache.table} with request ${cache.request}`,\n\t\t\t\t\t)\n\t\t\t\t} else {\n\t\t\t\t\t//check if the data has changed since last refresh\n\t\t\t\t\tif (\n\t\t\t\t\t\t!cache.data_changed_at ||\n\t\t\t\t\t\t(cache.data_changed_at && cache.refreshed_at && cache.data_changed_at < cache.refreshed_at)\n\t\t\t\t\t) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\t//check if the cache is expired\n\t\t\t\t\tif (cache.expires_at && cache.expires_at > cacheTime) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tthis.logger.debug(\n\t\t\t\t\t\t`[DataCache][Refresh][${cache.id}] Table data changed and cache expired for ${cache.table} with request ${cache.request}`,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcacheTime,\n\t\t\t\t\t\t\texpiresAt: cache.expires_at,\n\t\t\t\t\t\t\tdataChangedAt: cache.data_changed_at,\n\t\t\t\t\t\t\trefreshedAt: cache.refreshed_at,\n\t\t\t\t\t\t},\n\t\t\t\t\t)\n\t\t\t\t}\n\n\t\t\t\tconst table_schema = await this.schema.getSchema({ table: cache.table })\n\n\t\t\t\tif (!table_schema) {\n\t\t\t\t\tthis.logger.error(`[DataCache][Refresh][${cache.id}] Schema not found for ${cache.table}`)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tconst options = await this.query.buildFindManyOptionsFromRequest({\n\t\t\t\t\trequest: cache.request,\n\t\t\t\t\tschema: table_schema,\n\t\t\t\t})\n\n\t\t\t\tthis.logger.verbose(\n\t\t\t\t\t`[DataCache][Refresh][${cache.id}] Options: ${JSON.stringify({\n\t\t\t\t\t\t...options,\n\t\t\t\t\t\tschema: undefined, //remove the schema from the options for readability\n\t\t\t\t\t})}`,\n\t\t\t\t)\n\n\t\t\t\tconst result = (await this.query.perform(QueryPerform.FIND_MANY, options)) as FindManyResponseObject\n\n\t\t\t\tif (options.relations && options.relations.length > 0) {\n\t\t\t\t\tthis.logger.verbose(\n\t\t\t\t\t\t`[DataCache][Refresh][${cache.id}] Building relations for ${cache.table} with request ${cache.request}`,\n\t\t\t\t\t)\n\n\t\t\t\t\tfor (const i in result.data) {\n\t\t\t\t\t\tresult.data[i] = await this.query.buildRelations(\n\t\t\t\t\t\t\toptions as DataSourceFindOneOptions,\n\t\t\t\t\t\t\tresult.data[i],\n\t\t\t\t\t\t\tundefined,\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tawait this.write(cacheKey, result, cache.ttl_seconds * 1000)\n\t\t\t\tthis.logger.debug(\n\t\t\t\t\t`[DataCache][Refresh][${cache.id}] Cache refreshed for ${cache.table} with request ${cache.request}`,\n\t\t\t\t)\n\n\t\t\t\t//update the cache record\n\t\t\t\tawait this.query.perform(QueryPerform.UPDATE, {\n\t\t\t\t\tid: cache.id,\n\t\t\t\t\tschema,\n\t\t\t\t\tdata: {\n\t\t\t\t\t\trefreshed_at: new Date(),\n\t\t\t\t\t\texpires_at: new Date(new Date().getTime() + cache.ttl_seconds * 1000),\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t} catch (e) {\n\t\t\t\tthis.logger.error(\n\t\t\t\t\t`[DataCache][Refresh][${cache.id}] Error refreshing cache for ${cache.table} with request ${cache.request}`,\n\t\t\t\t\te,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/modules/websocket/redis-mock-with-pub-sub.ts",
    "content": "import { Logger } from '../../helpers/Logger'\n\nexport class RedisMockWithPubSub {\n\tprivate callbacks = []\n\tlogger = new Logger()\n\tstatus = 'ready'\n\n\tasync publish(channel: string, message: string) {\n\t\tthis.logger.debug(`[RedisMockWithPubSub] Publishing, message: ${message} to all channels`)\n\t\tthis.callbacks.forEach(callback => {\n\t\t\ttry {\n\t\t\t\tcallback(channel, message)\n\t\t\t} catch (error) {\n\t\t\t\tthis.logger.error(`[RedisMockWithPubSub] Error in callback: ${error}`)\n\t\t\t}\n\t\t})\n\t}\n\n\tsubscribe() {}\n\n\ton(event: string, callback: (channel, message) => void) {\n\t\tthis.logger.debug(`[RedisMockWithPubSub] Subscribing to all events`)\n\t\tthis.callbacks.push(callback)\n\t}\n\n\tunsubscribe() {}\n\n\tdisconnect() {}\n}\n"
  },
  {
    "path": "src/modules/websocket/websocket.constants.ts",
    "content": "export const REDIS_PUB_CLIENT_TOKEN = 'REDIS_PUB_CLIENT'\nexport const REDIS_SUB_CLIENT_TOKEN = 'REDIS_SUB_CLIENT'\nexport const WEBSOCKETS_REDIS_CHANNEL = 'websockets'\nexport type WebsocketRedisEvent = {\n\ttableName: string\n\tprimaryKey: string\n\tpublishType: string\n\tid: string\n}\n"
  },
  {
    "path": "src/modules/websocket/websocket.gateway.spec.ts",
    "content": "import { INestApplication } from '@nestjs/common'\nimport { Test, TestingModule } from '@nestjs/testing'\nimport * as jsonwebtoken from 'jsonwebtoken'\nimport { io, Socket } from 'socket.io-client' // Changed import\nimport { DataSourceSchema, PublishType } from 'src/types/datasource.types'\nimport { AppModule } from '../../app.module'\nimport { WebsocketGateway } from './websocket.gateway'\nimport { WebsocketService } from './websocket.service'\nimport { CustomerTestingService } from '../../testing/customer.testing.service'\nimport { SalesOrderTestingService } from '../../testing/salesorder.testing.service'\nimport { UserTestingService } from '../../testing/user.testing.service'\nimport { RolePermission } from '../../types/roles.types'\nimport { Logger } from '../../helpers/Logger'\nimport { AuthTestingService } from '../../testing/auth.testing.service'\nimport { ConfigModule } from '@nestjs/config'\nimport jwt from '../../config/jwt.config'\nimport { envValidationSchema } from 'src/config/env.validation'\n\nconst SOCKET_TIMEOUT = 3000\n\nconst logger = new Logger()\n\nconst customers = []\nconst orders = []\nconst users = []\nconst tokens = []\n\nconst PORT1 = 8998\nconst PORT2 = 8999\n\ntype App = {\n\tapp: INestApplication\n\tgateway: WebsocketGateway\n\tservice: WebsocketService\n\tmodule: TestingModule\n}\n\nlet mockAuthResponse\nlet authTestingService: AuthTestingService\nlet customerTestingService: CustomerTestingService\nlet salesOrderTestingService: SalesOrderTestingService\nlet userTestingService: UserTestingService\n\nlet customerSchema: DataSourceSchema\nlet salesOrderSchema: DataSourceSchema\nlet usersSchema: DataSourceSchema\n\ndescribe('WebsocketGateway', () => {\n\tif (!process.env.JWT_KEY) {\n\t\tthrow new Error('JWT_KEY not found')\n\t}\n\n\tlet app1: App\n\tlet app2: App\n\n\tlet openSocketsForCleanup: Socket[] = []\n\n\tasync function listenAndOpenSocket(authToken: string, table: string, port = PORT1) {\n\t\tconst clientSocket = createSocket(port, authToken, table)\n\t\tawait waitForSocketToBeReady(clientSocket, SOCKET_TIMEOUT)\n\t\topenSocketsForCleanup.push(clientSocket)\n\t\treturn clientSocket\n\t}\n\n\tbeforeEach(() => {\n\t\tmockAuthResponse = {\n\t\t\tvalid: true,\n\t\t}\n\t})\n\n\tbeforeAll(async () => {\n\t\tapp1 = await createApp(PORT1)\n\t\tapp2 = await createApp(PORT2)\n\t})\n\n\tafterAll(async () => {\n\t\tawait app1.app.close()\n\t\tawait app2.app.close()\n\n\t\tfor (const customer of customers) {\n\t\t\tawait customerTestingService.deleteCustomer(customer[customerSchema.primary_key])\n\t\t}\n\n\t\tfor (const order of orders) {\n\t\t\tawait salesOrderTestingService.deleteOrder(order[salesOrderSchema.primary_key])\n\t\t}\n\n\t\tfor (const user of users) {\n\t\t\tawait userTestingService.deleteUser(user[usersSchema.primary_key])\n\t\t}\n\t})\n\n\tafterEach(async () => {\n\t\topenSocketsForCleanup.forEach(socket => {\n\t\t\tsocket.close()\n\t\t})\n\t\topenSocketsForCleanup = []\n\t})\n\n\tit('gateway should be defined', () => {\n\t\texpect(app1.gateway).toBeDefined()\n\t})\n\n\tit(`can subscribe to a public table without a token`, async () => {\n\t\tconst public_table_record = await authTestingService.createPublicTablesRecord({\n\t\t\ttable: customerSchema.table,\n\t\t\taccess_level: RolePermission.READ,\n\t\t})\n\t\ttry {\n\t\t\tawait expect(listenAndOpenSocket(undefined, customerSchema.table)).resolves.toBeDefined()\n\t\t} finally {\n\t\t\tawait authTestingService.deletePublicTablesRecord(public_table_record)\n\t\t}\n\t})\n\n\tit(`can't subscribe to a non-public table without a token`, async () => {\n\t\tawait expect(listenAndOpenSocket(undefined, customerSchema.table)).rejects.toEqual('Timeout')\n\t})\n\n\tit(`should throw error with an invalid token`, async () => {\n\t\tawait expect(listenAndOpenSocket('invalid_token', customerSchema.table)).rejects.toEqual('Timeout')\n\t})\n\n\tit(`should not throw error with a valid token`, async () => {\n\t\tawait listenAndOpenSocket(tokens[0], customerSchema.table)\n\t})\n\n\tit(`should send valid message to a user`, async () => {\n\t\tconst clientSocket = await listenAndOpenSocket(tokens[0], customerSchema.table)\n\t\tconst customer = await customerTestingService.createCustomer({\n\t\t\tuserId: users[0][usersSchema.primary_key],\n\t\t})\n\t\tcustomers.push(customer)\n\t\tconst eventPromise = waitForSocketEvent(clientSocket)\n\t\tapp1.service.publish(customerSchema, PublishType.INSERT, customer[customerSchema.primary_key])\n\t\texpect(await eventPromise).toEqual({\n\t\t\ttype: 'INSERT',\n\t\t\t[customerSchema.primary_key]: customer[customerSchema.primary_key].toString(),\n\t\t})\n\t})\n\n\tit(`should send a message to a logged out user and a public table`, async () => {\n\t\tconst public_table_record = await authTestingService.createPublicTablesRecord({\n\t\t\ttable: customerSchema.table,\n\t\t\taccess_level: RolePermission.READ,\n\t\t})\n\t\tconst clientSocket = await listenAndOpenSocket(undefined, customerSchema.table)\n\t\ttry {\n\t\t\tconst customer = await customerTestingService.createCustomer({\n\t\t\t\tuserId: users[0][usersSchema.primary_key],\n\t\t\t})\n\t\t\tcustomers.push(customer)\n\t\t\tconst eventPromise = waitForSocketEvent(clientSocket)\n\t\t\tapp1.service.publish(customerSchema, PublishType.INSERT, customer[customerSchema.primary_key])\n\t\t\texpect(await eventPromise).toBeDefined()\n\t\t} catch (e) {\n\t\t\tlogger.error(e)\n\t\t\tthrow e\n\t\t} finally {\n\t\t\tawait authTestingService.deletePublicTablesRecord(public_table_record)\n\t\t}\n\t})\n\n\tit(`should not send a message to a user that lacks sufficient permissions on the table`, async () => {\n\t\tconst clientSocket = await listenAndOpenSocket(tokens[0], customerSchema.table)\n\n\t\tconst role = await authTestingService.createRole({\n\t\t\tcustom: true,\n\t\t\ttable: customerSchema.table,\n\t\t\tidentity_column: 'userId',\n\t\t\trole: 'USER',\n\t\t\trecords: RolePermission.NONE,\n\t\t\town_records: RolePermission.NONE,\n\t\t})\n\n\t\ttry {\n\t\t\tconst customer = await customerTestingService.createCustomer({\n\t\t\t\tuserId: users[0][usersSchema.primary_key],\n\t\t\t})\n\t\t\tcustomers.push(customer)\n\t\t\tconst eventPromise = waitForSocketEvent(clientSocket)\n\t\t\tapp1.service.publish(customerSchema, PublishType.INSERT, customer[customerSchema.primary_key])\n\t\t\texpect(await eventPromise).toBeUndefined()\n\t\t} catch (e) {\n\t\t\tlogger.error(e)\n\t\t\tthrow e\n\t\t} finally {\n\t\t\tawait authTestingService.deleteRole(role)\n\t\t}\n\t})\n\n\tit(`should send message to two users on same server`, async () => {\n\t\tconst clientSocket = await listenAndOpenSocket(tokens[0], customerSchema.table, PORT1) //\n\t\tconst user2Socket = await listenAndOpenSocket(tokens[1], customerSchema.table, PORT1)\n\n\t\tconst promises = [waitForSocketEvent(clientSocket), waitForSocketEvent(user2Socket)]\n\t\tconst customer = await customerTestingService.createCustomer({\n\t\t\tuserId: users[0][usersSchema.primary_key],\n\t\t})\n\t\tcustomers.push(customer)\n\t\tapp1.service.publish(customerSchema, PublishType.INSERT, customer[customerSchema.primary_key])\n\t\tconst [eventUser1, eventUser2] = await Promise.all(promises)\n\t\tuser2Socket.close()\n\t\texpect(eventUser1).toBeDefined()\n\t\texpect(eventUser2).toBeDefined()\n\t})\n\n\tit(`should send message to two users, each on a different server`, async () => {\n\t\tconst clientSocket = await listenAndOpenSocket(tokens[0], customerSchema.table, PORT1) //\n\t\tconst user2Socket = await listenAndOpenSocket(tokens[1], customerSchema.table, PORT2)\n\n\t\tconst promises = [waitForSocketEvent(clientSocket), waitForSocketEvent(user2Socket)]\n\t\tconst customer = await customerTestingService.createCustomer({\n\t\t\tuserId: users[0][usersSchema.primary_key],\n\t\t})\n\t\tcustomers.push(customer)\n\t\tapp1.service.publish(customerSchema, PublishType.INSERT, customer[customerSchema.primary_key])\n\n\t\tconst [eventUser1, eventUser2] = await Promise.all(promises)\n\t\tuser2Socket.close()\n\t\texpect(eventUser1).toBeDefined()\n\t\texpect(eventUser2).toBeDefined()\n\t})\n\n\tit(`should send message to a on another server`, async () => {\n\t\tconst clientSocket = await listenAndOpenSocket(tokens[0], customerSchema.table, PORT2) // the other app\n\t\tconst customer = await customerTestingService.createCustomer({\n\t\t\tuserId: users[0][usersSchema.primary_key],\n\t\t})\n\t\tcustomers.push(customer)\n\t\tconst eventPromise = waitForSocketEvent(clientSocket)\n\t\tapp1.service.publish(customerSchema, PublishType.INSERT, customer[customerSchema.primary_key])\n\t\texpect(await eventPromise).toEqual({\n\t\t\ttype: 'INSERT',\n\t\t\t[customerSchema.primary_key]: customer[customerSchema.primary_key].toString(),\n\t\t})\n\t})\n\n\tit(`should send message to a user on another server (opposite server)`, async () => {\n\t\tconst clientSocket = await listenAndOpenSocket(tokens[0], customerSchema.table, PORT1) // the other app\n\t\tconst customer = await customerTestingService.createCustomer({\n\t\t\tuserId: users[0][usersSchema.primary_key],\n\t\t})\n\t\tcustomers.push(customer)\n\t\tconst eventPromise = waitForSocketEvent(clientSocket)\n\t\tapp2.service.publish(customerSchema, PublishType.INSERT, customer[customerSchema.primary_key])\n\t\texpect(await eventPromise).toEqual({\n\t\t\ttype: 'INSERT',\n\t\t\t[customerSchema.primary_key]: customer[customerSchema.primary_key].toString(),\n\t\t})\n\t})\n\n\tit(`A user should not receive message that was sent not sent about a different table`, async () => {\n\t\tconst clientSocket = await listenAndOpenSocket(tokens[0], customerSchema.table) // user 1\n\t\t// user 2\n\t\tconst user2Socket = await listenAndOpenSocket(tokens[1], salesOrderSchema.table, PORT1)\n\n\t\tconst promises = [waitForSocketEvent(clientSocket), waitForSocketEvent(user2Socket)]\n\t\tconst customer = await customerTestingService.createCustomer({\n\t\t\tuserId: users[0][usersSchema.primary_key],\n\t\t})\n\t\tcustomers.push(customer)\n\t\tapp1.service.publish(customerSchema, PublishType.INSERT, customer[customerSchema.primary_key])\n\t\tconst [eventUser1, eventUser2] = await Promise.all(promises)\n\t\tuser2Socket.close()\n\t\texpect(eventUser1).toBeDefined()\n\t\texpect(eventUser2).toBeUndefined()\n\t})\n\n\tit(`A user should not receive message that was sent about a different table (opposite server)`, async () => {\n\t\tconst clientSocket = await listenAndOpenSocket(tokens[0], customerSchema.table) // user 1\n\t\t// user 2\n\t\tconst user2Socket = await listenAndOpenSocket(tokens[1], salesOrderSchema.table, PORT1)\n\n\t\tconst promises = [waitForSocketEvent(clientSocket), waitForSocketEvent(user2Socket)]\n\t\tconst customer = await customerTestingService.createCustomer({\n\t\t\tuserId: users[0][usersSchema.primary_key],\n\t\t})\n\t\tcustomers.push(customer)\n\t\tapp1.service.publish(salesOrderSchema, PublishType.INSERT, customer[customerSchema.primary_key])\n\t\tconst [eventUser1, eventUser2] = await Promise.all(promises)\n\t\tuser2Socket.close()\n\t\texpect(eventUser1).toBeUndefined()\n\t\texpect(eventUser2).toBeDefined()\n\t})\n})\n\n// helpers\n\nasync function waitForSocketToBeReady(clientSocket: Socket, timeoutMs: number = SOCKET_TIMEOUT) {\n\treturn await new Promise((resolve, reject) => {\n\t\tconst timeoutId = setTimeout(() => {\n\t\t\treject('Timeout')\n\t\t}, timeoutMs)\n\n\t\tclientSocket.on('connect', () => {\n\t\t\tclearTimeout(timeoutId)\n\t\t\tresolve(1)\n\t\t})\n\n\t\tclientSocket.on('error', error => {\n\t\t\tconsole.error('An error occurred:', error)\n\t\t\tclearTimeout(timeoutId)\n\t\t\treject('error ' + error)\n\t\t})\n\n\t\tclientSocket.on('disconnect', reason => {\n\t\t\tclearTimeout(timeoutId)\n\t\t\treject('disconnect ' + reason)\n\t\t})\n\t})\n}\n\nasync function waitForSocketEvent(clientSocket: Socket, timeoutMs: number = SOCKET_TIMEOUT) {\n\treturn await new Promise((resolve, reject) => {\n\t\tlet resolved = false\n\t\tconst timeoutId = setTimeout(() => {\n\t\t\tresolved = true\n\t\t\tresolve(undefined)\n\t\t}, timeoutMs)\n\t\t//@ts-ignore\n\t\tclientSocket.on(clientSocket['table'], data => {\n\t\t\tif (resolved) return\n\t\t\tclearTimeout(timeoutId)\n\t\t\tresolve(data)\n\t\t})\n\t})\n}\n\nasync function createApp(port: number): Promise<App> {\n\tconst module: TestingModule = await Test.createTestingModule({\n\t\timports: [\n\t\t\tConfigModule.forRoot({\n\t\t\t\tload: [jwt],\n\t\t\t\tvalidationSchema: envValidationSchema,\n\t\t\t}),\n\t\t\tAppModule,\n\t\t],\n\t\tproviders: [CustomerTestingService, SalesOrderTestingService, UserTestingService, AuthTestingService],\n\t\texports: [CustomerTestingService, SalesOrderTestingService, UserTestingService, AuthTestingService],\n\t}).compile()\n\tconst gateway = module.get<WebsocketGateway>(WebsocketGateway)\n\tconst service = module.get<WebsocketService>(WebsocketService)\n\n\tcustomerTestingService = module.get<CustomerTestingService>(CustomerTestingService)\n\tsalesOrderTestingService = module.get<SalesOrderTestingService>(SalesOrderTestingService)\n\tuserTestingService = module.get<UserTestingService>(UserTestingService)\n\tauthTestingService = module.get<AuthTestingService>(AuthTestingService)\n\n\tcustomerSchema = await customerTestingService.getSchema()\n\tsalesOrderSchema = await salesOrderTestingService.getSchema()\n\tusersSchema = await userTestingService.getSchema()\n\n\tconst app = module.createNestApplication()\n\tawait app.listen(port)\n\n\t// create users with port-based emails to overcome same email error because of different servers\n\tconst user1 = await userTestingService.createUser({ email: `${port}-user-1@email.com` })\n\tconst user2 = await userTestingService.createUser({ email: `${port}-user-2@email.com` })\n\n\tusers.push(user1, user2)\n\ttokens.push(\n\t\tjsonwebtoken.sign({ sub: user1[usersSchema.primary_key] }, process.env.JWT_KEY),\n\t\tjsonwebtoken.sign({ sub: user2[usersSchema.primary_key] }, process.env.JWT_KEY),\n\t)\n\n\treturn { app, gateway, service, module }\n}\n\nfunction createSocket(port: number, token: string, table: string): Socket {\n\tconst socket = io(`http://localhost:${port}`, {\n\t\textraHeaders: {\n\t\t\t...(token && { authorization: `Bearer ${token}` }),\n\t\t\t'x-llana-table': table,\n\t\t},\n\t})\n\tsocket['table'] = table\n\treturn socket\n}\n"
  },
  {
    "path": "src/modules/websocket/websocket.gateway.ts",
    "content": "import { Inject, OnApplicationShutdown } from '@nestjs/common'\nimport {\n\tOnGatewayConnection,\n\tOnGatewayDisconnect,\n\tOnGatewayInit,\n\tSubscribeMessage,\n\tWebSocketGateway,\n\tWebSocketServer,\n} from '@nestjs/websockets'\nimport Redis from 'ioredis'\nimport { Server } from 'socket.io'\n\nimport { Authentication } from '../../helpers/Authentication'\nimport { Logger } from '../../helpers/Logger'\nimport { Roles } from '../../helpers/Roles'\nimport { HostCheckMiddleware } from '../../middleware/HostCheck'\nimport { RolePermission } from '../../types/roles.types'\nimport { REDIS_SUB_CLIENT_TOKEN, WebsocketRedisEvent, WEBSOCKETS_REDIS_CHANNEL } from './websocket.constants'\nimport { WebsocketJwtAuthMiddleware } from './websocket.jwt-auth.middleware'\n\n/**\n * WebsocketGateway\n * This class is responsible for handling websocket connections and emitting events to connected clients.\n * It also subscribes to a Redis channel to for a multi-instance setup, so that events can be emitted in all instances and sent to all connected clients in all instances.\n */\n@WebSocketGateway({ cors: true })\nexport class WebsocketGateway\n\timplements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect, OnApplicationShutdown\n{\n\tprivate tablesToConnectedUserSockets: Record<string, Record<string, string[]>> = {} // table_name -> sub -> socket_id[]\n\t@WebSocketServer() server: Server\n\n\tconstructor(\n\t\tprivate readonly logger: Logger,\n\t\tprivate readonly roles: Roles,\n\t\tprivate readonly authentication: Authentication,\n\t\tprivate readonly hostCheckMiddleware: HostCheckMiddleware,\n\t\t@Inject(REDIS_SUB_CLIENT_TOKEN) private readonly redisSubClient: Redis,\n\t) {}\n\n\tasync afterInit(server: Server) {\n\t\tserver.use(WebsocketJwtAuthMiddleware(this.authentication, this.hostCheckMiddleware) as any)\n\t\tif (this.server) {\n\t\t\tthis.logger.debug(`[WebsocketGateway] server initialized`)\n\t\t} else {\n\t\t\tthrow new Error(`[WebsocketGateway] server not initialized`)\n\t\t}\n\t\tawait this.subscribeToEvents()\n\t}\n\n\tonApplicationShutdown() {\n\t\tthis.redisSubClient.disconnect()\n\t}\n\n\tprivate async subscribeToEvents() {\n\t\tawait this.redisSubClient.subscribe(WEBSOCKETS_REDIS_CHANNEL, err => {\n\t\t\tif (err) {\n\t\t\t\tthis.logger.error(\n\t\t\t\t\t`[WebsocketGateway] Failed to subscribe to Redis channel \"${WEBSOCKETS_REDIS_CHANNEL}\": %s`,\n\t\t\t\t\terr.message,\n\t\t\t\t)\n\t\t\t\tthrow err\n\t\t\t}\n\t\t})\n\n\t\tthis.redisSubClient.on('message', (channel, message) => {\n\t\t\tthis.logger.debug(`[WebsocketGateway] Received message from Redis channel: ${message}`)\n\t\t\tconst json = JSON.parse(message) as WebsocketRedisEvent\n\t\t\tthis.emitToSockets(json)\n\t\t})\n\t}\n\n\tprivate async emitToSockets(msg: WebsocketRedisEvent) {\n\t\tif (!this.server) throw new Error(`[WebsocketGateway] Server not initialized`)\n\t\tif (!msg.tableName || !msg.publishType || !msg.id) {\n\t\t\tthis.logger.error(`[WebsocketGateway] Invalid message received: ${JSON.stringify(msg)}`)\n\t\t\treturn\n\t\t}\n\t\tthis.logger.debug(`[WebsocketGateway] Publishing ${msg.tableName} ${msg.publishType} for #${msg.id}`)\n\t\tconst userSockets = this.tablesToConnectedUserSockets[msg.tableName] || {}\n\t\tthis.logger.debug(`[WebsocketGateway] Connected users: ${JSON.stringify(userSockets)}`)\n\t\tfor (const [sub, socketIds] of Object.entries(userSockets)) {\n\t\t\tfor (const socketId of socketIds) {\n\t\t\t\tconst public_auth = await this.authentication.public({\n\t\t\t\t\ttable: msg.tableName,\n\t\t\t\t\taccess_level: RolePermission.READ,\n\t\t\t\t})\n\n\t\t\t\tconst permission = await this.roles.tablePermission({\n\t\t\t\t\tidentifier: sub,\n\t\t\t\t\ttable: msg.tableName,\n\t\t\t\t\taccess: RolePermission.READ,\n\t\t\t\t})\n\n\t\t\t\tif (!public_auth.valid && !permission.valid) {\n\t\t\t\t\tthis.logger.debug(\n\t\t\t\t\t\t`[WebsocketGateway] User ${sub} not authorized to receive event for table ${msg.tableName}`,\n\t\t\t\t\t)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tthis.logger.debug(\n\t\t\t\t\t`[WebsocketGateway] Emitting ${msg.tableName} ${msg.publishType} for #${msg.id} to ${socketId} (User: ${sub})`,\n\t\t\t\t)\n\t\t\t\tthis.server.to(socketId).emit(msg.tableName, {\n\t\t\t\t\ttype: msg.publishType,\n\t\t\t\t\t[msg.primaryKey]: msg.id,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\treturn\n\t}\n\n\thandleConnection(client: any) {\n\t\tthis.tablesToConnectedUserSockets[client.user.table] ||= {}\n\t\tthis.tablesToConnectedUserSockets[client.user.table][client.user.sub] ||= []\n\t\tthis.tablesToConnectedUserSockets[client.user.table][client.user.sub].push(client.id)\n\t\tthis.logger.debug(\n\t\t\t`[WebsocketGateway] Client id: ${client.id} connected. table=${client.user.table} sub=${client.user.sub}. Number of connected clients: ${this.server.sockets.sockets.size}`,\n\t\t)\n\t}\n\n\thandleDisconnect(client: any) {\n\t\tif (!client.user?.table || !this.tablesToConnectedUserSockets[client.user.table]) {\n\t\t\tthis.logger.debug(\n\t\t\t\t`[WebsocketGateway] Client id: ${client.id} disconnected. table=${client.user.table} sub=${client.user.sub}. Number of connected clients: ${this.server.sockets.sockets.size}`,\n\t\t\t)\n\t\t\treturn\n\t\t}\n\t\tthis.tablesToConnectedUserSockets[client.user.table][client.user.sub] = (\n\t\t\tthis.tablesToConnectedUserSockets[client.user.table][client.user.sub] || []\n\t\t).filter(socketId => socketId !== client.id)\n\t\tthis.logger.debug(\n\t\t\t`[WebsocketGateway] Cliend id: ${client.id} disconnected. sub=${client.user.sub}. Number of connected clients: ${this.server.sockets.sockets.size}`,\n\t\t)\n\t}\n\n\t@SubscribeMessage('message')\n\thandleMessage(client: any, payload: any): string {\n\t\tthis.logger.log('handleMessage', payload)\n\t\t// TBD: Implement\n\t\tthrow 'Client to server messages are not supported.'\n\t}\n}\n"
  },
  {
    "path": "src/modules/websocket/websocket.jwt-auth.middleware.test.spec.ts",
    "content": "import { Test } from '@nestjs/testing'\nimport { Authentication } from '../../helpers/Authentication'\nimport { HostCheckMiddleware } from '../../middleware/HostCheck'\nimport { Logger } from '../../helpers/Logger'\nimport { RolePermission } from '../../types/roles.types'\nimport { WebsocketJwtAuthMiddleware } from './websocket.jwt-auth.middleware'\nimport { ConfigService } from '@nestjs/config'\nimport { JwtService } from '@nestjs/jwt'\n\ndescribe('WebsocketJwtAuthMiddleware', () => {\n\tlet authentication: Authentication\n\tlet hostCheckMiddleware: HostCheckMiddleware\n\tlet middleware: ReturnType<typeof WebsocketJwtAuthMiddleware>\n\tlet mockSocket: any\n\tlet mockNext: jest.Mock\n\n\tbeforeEach(async () => {\n\t\tconst moduleRef = await Test.createTestingModule({\n\t\t\tproviders: [\n\t\t\t\t{\n\t\t\t\t\tprovide: Authentication,\n\t\t\t\t\tuseValue: {\n\t\t\t\t\t\tpublic: jest.fn(),\n\t\t\t\t\t\tauth: jest.fn(),\n\t\t\t\t\t\tskipAuth: jest.fn(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprovide: HostCheckMiddleware,\n\t\t\t\t\tuseValue: {\n\t\t\t\t\t\tvalidateHost: jest.fn(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprovide: ConfigService,\n\t\t\t\t\tuseValue: {\n\t\t\t\t\t\tget: jest.fn(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tprovide: JwtService,\n\t\t\t\t\tuseValue: {\n\t\t\t\t\t\tverifyAsync: jest.fn(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tLogger,\n\t\t\t],\n\t\t}).compile()\n\n\t\tauthentication = moduleRef.get<Authentication>(Authentication)\n\t\thostCheckMiddleware = moduleRef.get<HostCheckMiddleware>(HostCheckMiddleware)\n\t\tmiddleware = WebsocketJwtAuthMiddleware(authentication, hostCheckMiddleware)\n\n\t\tmockNext = jest.fn()\n\t\tmockSocket = {\n\t\t\thandshake: {\n\t\t\t\theaders: {\n\t\t\t\t\t'x-llana-table': 'test_table',\n\t\t\t\t\t'x-request-id': 'test-request-id',\n\t\t\t\t},\n\t\t\t},\n\t\t\tuser: {},\n\t\t}\n\n\t\t// Default host check to pass\n\t\tjest.spyOn(hostCheckMiddleware, 'validateHost').mockReturnValue(true)\n\t})\n\n\tafterEach(() => {\n\t\tjest.clearAllMocks()\n\t})\n\n\tit('should reject connection when no table is provided', async () => {\n\t\tdelete mockSocket.handshake.headers['x-llana-table']\n\n\t\tawait middleware(mockSocket, mockNext)\n\n\t\texpect(mockNext).toHaveBeenCalledWith(\n\t\t\texpect.objectContaining({\n\t\t\t\tmessage: expect.stringContaining('No Table Provided'),\n\t\t\t}),\n\t\t)\n\t})\n\n\tit('should reject connection when host check fails', async () => {\n\t\tjest.spyOn(hostCheckMiddleware, 'validateHost').mockReturnValue(false)\n\n\t\tawait middleware(mockSocket, mockNext)\n\n\t\texpect(mockNext).toHaveBeenCalledWith(\n\t\t\texpect.objectContaining({\n\t\t\t\tmessage: 'Forbidden',\n\t\t\t}),\n\t\t)\n\t})\n\n\tit('should allow access to public tables without authentication', async () => {\n\t\tjest.spyOn(authentication, 'public').mockResolvedValue({\n\t\t\tvalid: true,\n\t\t\tallowed_fields: ['field1', 'field2'],\n\t\t})\n\n\t\tawait middleware(mockSocket, mockNext)\n\n\t\texpect(authentication.public).toHaveBeenCalledWith({\n\t\t\ttable: 'test_table',\n\t\t\taccess_level: RolePermission.READ,\n\t\t\tx_request_id: 'test-request-id',\n\t\t})\n\t\texpect(mockSocket.user).toEqual({\n\t\t\tsub: 'public',\n\t\t\ttable: 'test_table',\n\t\t})\n\t\texpect(mockNext).toHaveBeenCalledWith()\n\t})\n\n\tit('should reject access to private tables without authentication', async () => {\n\t\t// Public check fails\n\t\tjest.spyOn(authentication, 'public').mockResolvedValue({\n\t\t\tvalid: false,\n\t\t})\n\n\t\t// Skip auth is false\n\t\tjest.spyOn(authentication, 'skipAuth').mockReturnValue(false)\n\n\t\t// Auth check fails\n\t\tjest.spyOn(authentication, 'auth').mockResolvedValue({\n\t\t\tvalid: false,\n\t\t\tmessage: 'Authentication failed',\n\t\t})\n\n\t\tawait middleware(mockSocket, mockNext)\n\n\t\texpect(authentication.public).toHaveBeenCalled()\n\t\texpect(authentication.auth).toHaveBeenCalled()\n\t\texpect(mockNext).toHaveBeenCalledWith(\n\t\t\texpect.objectContaining({\n\t\t\t\tmessage: 'Authentication failed',\n\t\t\t}),\n\t\t)\n\t})\n\n\tit('should allow access to private tables with valid JWT', async () => {\n\t\t// Public check fails\n\t\tjest.spyOn(authentication, 'public').mockResolvedValue({\n\t\t\tvalid: false,\n\t\t})\n\n\t\t// Skip auth is false\n\t\tjest.spyOn(authentication, 'skipAuth').mockReturnValue(false)\n\n\t\t// Auth check succeeds\n\t\tjest.spyOn(authentication, 'auth').mockResolvedValue({\n\t\t\tvalid: true,\n\t\t\tuser_identifier: '123',\n\t\t})\n\n\t\tawait middleware(mockSocket, mockNext)\n\n\t\texpect(authentication.public).toHaveBeenCalled()\n\t\texpect(authentication.auth).toHaveBeenCalledWith({\n\t\t\ttable: 'test_table',\n\t\t\taccess: RolePermission.READ,\n\t\t\theaders: mockSocket.handshake.headers,\n\t\t\tx_request_id: 'test-request-id',\n\t\t})\n\t\texpect(mockSocket.user).toEqual({\n\t\t\tsub: '123',\n\t\t\ttable: 'test_table',\n\t\t})\n\t\texpect(mockNext).toHaveBeenCalledWith()\n\t})\n\n\tit('should allow access when skipAuth is true', async () => {\n\t\t// Public check fails\n\t\tjest.spyOn(authentication, 'public').mockResolvedValue({\n\t\t\tvalid: false,\n\t\t})\n\n\t\t// Skip auth is true\n\t\tjest.spyOn(authentication, 'skipAuth').mockReturnValue(true)\n\n\t\tawait middleware(mockSocket, mockNext)\n\n\t\texpect(authentication.public).toHaveBeenCalled()\n\t\texpect(authentication.skipAuth).toHaveBeenCalled()\n\t\texpect(mockNext).toHaveBeenCalledWith()\n\t})\n\n\tit('should handle authentication errors gracefully', async () => {\n\t\t// Public check throws error\n\t\tjest.spyOn(authentication, 'public').mockRejectedValue(new Error('Database error'))\n\n\t\tawait middleware(mockSocket, mockNext)\n\n\t\texpect(mockNext).toHaveBeenCalledWith(\n\t\t\texpect.objectContaining({\n\t\t\t\tmessage: expect.any(String),\n\t\t\t}),\n\t\t)\n\t})\n})\n"
  },
  {
    "path": "src/modules/websocket/websocket.jwt-auth.middleware.ts",
    "content": "import { Socket } from 'socket.io'\nimport { Authentication } from 'src/helpers/Authentication'\nimport { HostCheckMiddleware } from 'src/middleware/HostCheck'\nimport { RolePermission } from 'src/types/roles.types'\n\nimport { Logger } from '../../helpers/Logger'\n\nexport type SocketIOMiddleware = {\n\t(client: AuthSocket, next: (err?: Error) => void): void\n}\n\nexport interface AuthSocket extends Socket {\n\tuser: {\n\t\tsub: string\n\t\ttable: string\n\t}\n}\n\nconst logger = new Logger()\n\nexport const WebsocketJwtAuthMiddleware = (\n\tauthentication: Authentication,\n\thostCheckMiddleware: HostCheckMiddleware,\n): SocketIOMiddleware => {\n\treturn async (client: AuthSocket, next) => {\n\t\ttry {\n\t\t\tconst table = (\n\t\t\t\tclient.handshake.auth?.['x-llana-table'] ?? client.handshake.headers?.['x-llana-table']\n\t\t\t)?.toString()\n\n\t\t\tif (!table) {\n\t\t\t\tlogger.debug('[WebsocketJwtAuthMiddleware] Socket Failed - No table provided')\n\t\t\t\tlogger.debug(client.handshake.headers)\n\t\t\t\treturn next(new Error('No Table Provided In Headers[x-llana-table]'))\n\t\t\t}\n\n\t\t\tif (!hostCheckMiddleware.validateHost(client.handshake, '[WebsocketJwtAuthMiddleware]')) {\n\t\t\t\tlogger.debug('[WebsocketJwtAuthMiddleware] Socket Host Failed - Unauthorized')\n\t\t\t\treturn next(new Error('Forbidden'))\n\t\t\t}\n\n\t\t\t// Check if table is public\n\t\t\tconst public_auth = await authentication.public({\n\t\t\t\ttable,\n\t\t\t\taccess_level: RolePermission.READ,\n\t\t\t\tx_request_id: client.handshake.headers['x-request-id']?.toString(),\n\t\t\t})\n\n\t\t\tif (public_auth.valid) {\n\t\t\t\tclient.user = { sub: 'public', table }\n\t\t\t\tlogger.debug(`[WebsocketJwtAuthMiddleware] Public access granted for table ${table}`)\n\t\t\t\treturn next()\n\t\t\t}\n\n\t\t\tif (authentication.skipAuth()) {\n\t\t\t\tlogger.debug(`[WebsocketJwtAuthMiddleware] Skipping authentication due to SKIP_AUTH being true`)\n\t\t\t\treturn next()\n\t\t\t}\n\n\t\t\t// Authenticate using JWT\n\t\t\tconst auth = await authentication.auth({\n\t\t\t\ttable,\n\t\t\t\taccess: RolePermission.READ,\n\t\t\t\theaders: client.handshake.headers,\n\t\t\t\tx_request_id: client.handshake.headers['x-request-id']?.toString(),\n\t\t\t})\n\n\t\t\tif (!auth.valid) {\n\t\t\t\tlogger.debug(`[WebsocketJwtAuthMiddleware] Authentication failed: ${auth.message}`)\n\t\t\t\treturn next(new Error(auth.message))\n\t\t\t}\n\n\t\t\tclient.user = { sub: auth.user_identifier.toString(), table }\n\t\t\tlogger.debug(`[WebsocketJwtAuthMiddleware] User ${auth.user_identifier} authenticated`)\n\t\t\tnext()\n\t\t} catch (err) {\n\t\t\tlogger.debug(\n\t\t\t\t`[WebsocketJwtAuthMiddleware] Failed to authenticate user. headers=${JSON.stringify(client.handshake.headers)}`,\n\t\t\t\terr,\n\t\t\t)\n\t\t\tnext(err as Error)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/modules/websocket/websocket.service.ts",
    "content": "import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'\nimport Redis from 'ioredis'\nimport { DataSourceSchema, PublishType } from 'src/types/datasource.types'\n\nimport { Logger } from '../../helpers/Logger'\nimport { REDIS_PUB_CLIENT_TOKEN, WebsocketRedisEvent, WEBSOCKETS_REDIS_CHANNEL } from './websocket.constants'\n\n@Injectable()\nexport class WebsocketService implements OnApplicationShutdown {\n\tconstructor(\n\t\tprivate readonly logger: Logger,\n\t\t@Inject(REDIS_PUB_CLIENT_TOKEN) private readonly redisPubClient: Redis,\n\t) {}\n\n\tonApplicationShutdown() {\n\t\tthis.redisPubClient.disconnect()\n\t}\n\n\tpublic async publish(schema: DataSourceSchema, type: PublishType, id: number | string) {\n\t\tif (!id) {\n\t\t\tthis.logger.debug(`[WebsocketService] Skipping publish ${schema.table} ${type} as no id provided`)\n\t\t\treturn\n\t\t}\n\n\t\tthis.logger.debug(`[WebsocketService] Publishing ${schema.table} ${type} for #${id}`)\n\t\tif (this.redisPubClient.status !== 'ready') {\n\t\t\tthrow new Error('Redis client not ready')\n\t\t}\n\t\tconst event: WebsocketRedisEvent = {\n\t\t\ttableName: schema.table,\n\t\t\tpublishType: type.toString(),\n\t\t\tprimaryKey: schema.primary_key,\n\t\t\tid: id.toString(),\n\t\t}\n\t\tawait this.redisPubClient.publish(WEBSOCKETS_REDIS_CHANNEL, JSON.stringify(event))\n\t}\n}\n"
  },
  {
    "path": "src/modules/welcome/welcome.controller.ts",
    "content": "import { Controller, Get, Render } from '@nestjs/common'\n\n@Controller()\nexport class WelcomeController {\n\t@Get()\n\t@Render('welcome')\n\troot() {\n\t\treturn {\n\t\t\t//message: 'Hello world!'\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/modules/welcome/welcome.module.ts",
    "content": "import { Module } from '@nestjs/common'\n\nimport { WelcomeController } from './welcome.controller'\n\n@Module({\n\tcontrollers: [WelcomeController],\n})\nexport class WelcomeModule {}\n"
  },
  {
    "path": "src/testing/auth.testing.service.ts",
    "content": "import { Injectable } from '@nestjs/common'\nimport { ConfigService } from '@nestjs/config'\nimport { Auth, AuthJWT, AuthType } from 'src/types/auth.types'\n\nimport { LLANA_PUBLIC_TABLES, LLANA_ROLES_TABLE } from '../app.constants'\nimport { AuthService } from '../app.service.auth'\nimport { UserTestingService } from './user.testing.service'\nimport { FindOneResponseObject } from '../dtos/response.dto'\nimport { Query } from '../helpers/Query'\nimport { Schema } from '../helpers/Schema'\nimport {\n\tDataSourceCreateOneOptions,\n\tDataSourceSchema,\n\tDataSourceWhere,\n\tQueryPerform,\n\tWhereOperator,\n} from '../types/datasource.types'\nimport { RolePermission } from '../types/roles.types'\n\n@Injectable()\nexport class AuthTestingService {\n\tconstructor(\n\t\tprivate readonly authService: AuthService,\n\t\tprivate readonly query: Query,\n\t\tprivate readonly schema: Schema,\n\t\tprivate readonly configService: ConfigService,\n\t\tprivate readonly userTestingService: UserTestingService,\n\t) {}\n\n\tasync login(): Promise<string> {\n\t\ttry {\n\t\t\tconst email = 'test@test.com'\n\t\t\tconst [, sub] = await this.findUser(email)\n\t\t\tconst payload = await this.authService.login({ email, sub })\n\t\t\treturn payload.access_token\n\t\t} catch (error) {\n\t\t\tconsole.error('Login failed:', error)\n\t\t\tthrow error\n\t\t}\n\t}\n\n\tasync getUserId(jwt: string): Promise<number> {\n\t\treturn await this.authService.getUserId(jwt)\n\t}\n\n\t// This function is used to find a user by username and return the user and the user's id\n\tasync findUser(username: string): Promise<[any, string]> {\n\t\tconst authentications = this.configService.get<Auth[]>('auth')\n\n\t\tconst jwtAuthConfig = authentications.find(auth => auth.type === AuthType.JWT)\n\n\t\tlet schema: DataSourceSchema\n\t\tschema = await this.schema.getSchema({ table: jwtAuthConfig.table.name })\n\n\t\tconst where: DataSourceWhere[] = [\n\t\t\t{\n\t\t\t\tcolumn: (jwtAuthConfig.table as AuthJWT).columns.username,\n\t\t\t\toperator: WhereOperator.equals,\n\t\t\t\tvalue: username,\n\t\t\t},\n\t\t]\n\n\t\tconst user = await this.query.perform(QueryPerform.FIND_ONE, {\n\t\t\tschema,\n\t\t\twhere,\n\t\t})\n\t\t\n\t\tif (!user) {\n\t\t\tif (process.env.NODE_ENV === 'test') {\n\t\t\t\tconsole.warn(`[Test Environment] User not found for ${username}, using mock user`)\n\t\t\t\tconst mockUser = await this.userTestingService.createUser()\n\t\t\t\treturn [mockUser, mockUser.id]\n\t\t\t}\n\t\t\tthrow new Error(`User not found for ${username}`)\n\t\t}\n\t\t\n\t\treturn [user, user[schema.primary_key]]\n\t}\n\n\tasync createPublicTablesRecord(data: {\n\t\ttable: string\n\t\taccess_level: RolePermission\n\t\tallowed_fields?: string\n\t}): Promise<FindOneResponseObject> {\n\t\tconst schema = await this.schema.getSchema({ table: LLANA_PUBLIC_TABLES, x_request_id: 'test' })\n\t\treturn (await this.query.perform(QueryPerform.CREATE, <DataSourceCreateOneOptions>{\n\t\t\tschema,\n\t\t\tdata,\n\t\t})) as FindOneResponseObject\n\t}\n\n\tasync deletePublicTablesRecord(data: any): Promise<void> {\n\t\tconst schema = await this.schema.getSchema({ table: LLANA_PUBLIC_TABLES, x_request_id: 'test' })\n\t\tawait this.query.perform(QueryPerform.DELETE, {\n\t\t\tid: data[schema.primary_key],\n\t\t\tschema,\n\t\t})\n\t}\n\n\tasync createRole(data: {\n\t\tcustom: boolean\n\t\ttable: string\n\t\tidentity_column?: string\n\t\trole: string\n\t\trecords: RolePermission\n\t\town_records: RolePermission\n\t\tallowed_fields?: string\n\t}): Promise<FindOneResponseObject> {\n\t\tconst schema = await this.schema.getSchema({ table: LLANA_ROLES_TABLE, x_request_id: 'test' })\n\t\treturn (await this.query.perform(QueryPerform.CREATE, <DataSourceCreateOneOptions>{\n\t\t\tschema,\n\t\t\tdata,\n\t\t})) as FindOneResponseObject\n\t}\n\n\tasync deleteRole(data: any): Promise<void> {\n\t\tconst schema = await this.schema.getSchema({ table: LLANA_ROLES_TABLE, x_request_id: 'test' })\n\t\tawait this.query.perform(QueryPerform.DELETE, {\n\t\t\tid: data[schema.primary_key],\n\t\t\tschema,\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/testing/customer.testing.service.ts",
    "content": "import { Injectable } from '@nestjs/common'\n\nimport { FindOneResponseObject } from '../dtos/response.dto'\nimport { Query } from '../helpers/Query'\nimport { Schema } from '../helpers/Schema'\nimport { QueryPerform } from '../types/datasource.types'\n\nconst table = 'Customer'\nlet customerNumber = 0\n\n@Injectable()\nexport class CustomerTestingService {\n\tconstructor(\n\t\tprivate readonly query: Query,\n\t\tprivate readonly schema: Schema,\n\t) {}\n\n\tmockCustomer(userId: any): any {\n\t\tcustomerNumber++\n\t\treturn {\n\t\t\tuserId,\n\t\t\tcompanyName: `Company ${customerNumber}`,\n\t\t\tcontactName: `first${customerNumber} last${customerNumber}`,\n\t\t\tcontactTitle: 'CEO',\n\t\t\taddress: `Address ${customerNumber}`,\n\t\t\tcity: 'Berlin',\n\t\t\tregion: 'Center',\n\t\t\tpostalCode: '10092',\n\t\t\tcountry: 'Germany',\n\t\t\temail: `email${customerNumber}@test.com`,\n\t\t\tphone: '030-3456789',\n\t\t\tfax: '030-3456788',\n\t\t}\n\t}\n\n\tasync getSchema(): Promise<any> {\n\t\treturn await this.schema.getSchema({ table })\n\t}\n\n\tasync createCustomer(customer: any): Promise<any> {\n\t\tconst customerTableSchema = await this.schema.getSchema({ table })\n\n\t\tconst CUSTOMER = this.mockCustomer(customer.userId)\n\n\t\treturn (await this.query.perform(\n\t\t\tQueryPerform.CREATE,\n\t\t\t{\n\t\t\t\tschema: customerTableSchema,\n\t\t\t\tdata: {\n\t\t\t\t\t...CUSTOMER,\n\t\t\t\t\t...customer,\n\t\t\t\t},\n\t\t\t},\n\t\t\t'testing',\n\t\t)) as FindOneResponseObject\n\t}\n\n\tasync deleteCustomer(id: any): Promise<void> {\n\t\tconst customerTableSchema = await this.schema.getSchema({ table })\n\t\tawait this.query.perform(\n\t\t\tQueryPerform.DELETE,\n\t\t\t{\n\t\t\t\tschema: customerTableSchema,\n\t\t\t\tid,\n\t\t\t},\n\t\t\t'testing',\n\t\t)\n\t}\n}\n"
  },
  {
    "path": "src/testing/employee.testing.service.ts",
    "content": "import { Injectable } from '@nestjs/common'\n\nimport { FindOneResponseObject } from '../dtos/response.dto'\nimport { Query } from '../helpers/Query'\nimport { Schema } from '../helpers/Schema'\nimport { QueryPerform } from '../types/datasource.types'\n\nconst table = 'Employee'\nlet employeeNumber = 2000\n\n@Injectable()\nexport class EmployeeTestingService {\n\tconstructor(\n\t\tprivate readonly query: Query,\n\t\tprivate readonly schema: Schema,\n\t) {}\n\n\tmockEmployee(): any {\n\t\temployeeNumber++\n\t\treturn {\n\t\t\temployeeId: employeeNumber,\n\t\t\temail: `employee${employeeNumber}@test.com`,\n\t\t\tnotes: `Notes for employee ${employeeNumber}`,\n\t\t\tphone: `555-000-1234`,\n\t\t\tphoto: `photo${employeeNumber}.jpg`,\n\t\t\ttitle: `Title ${employeeNumber}`,\n\t\t\tmobile: `555-111-1234`,\n\t\t\tlastName: `LastName`,\n\t\t\tfirstName: `FirstName`,\n\t\t\thireDate: new Date(2000, 0, 1),\n\t\t\taddress: `Address ${employeeNumber}`,\n\t\t\tcity: `City${employeeNumber}`,\n\t\t\tregion: `Region${employeeNumber}`,\n\t\t\tpostalCode: '123456',\n\t\t\tcountry: `Country${employeeNumber}`,\n\t\t\textension: `Ext`,\n\t\t\tbirthDate: new Date(1980, 0, 1),\n\t\t\tphotoPath: `/photos/employee${employeeNumber}.jpg`,\n\t\t\ttitleOfCourtesy: `Mr./Ms. ${employeeNumber}`,\n\t\t}\n\t}\n\n\tasync getSchema(): Promise<any> {\n\t\treturn await this.schema.getSchema({ table })\n\t}\n\n\tasync createEmployee(employee: any): Promise<any> {\n\t\tconst employeeTableSchema = await this.schema.getSchema({ table })\n\n\t\tconst EMPLOYEE = this.mockEmployee()\n\n\t\treturn (await this.query.perform(\n\t\t\tQueryPerform.CREATE,\n\t\t\t{\n\t\t\t\tschema: employeeTableSchema,\n\t\t\t\tdata: {\n\t\t\t\t\t...EMPLOYEE,\n\t\t\t\t\t...employee,\n\t\t\t\t},\n\t\t\t},\n\t\t\t'testing',\n\t\t)) as FindOneResponseObject\n\t}\n\n\tasync getEmployee(): Promise<any> {\n\t\tconst employeeTableSchema = await this.schema.getSchema({ table })\n\n\t\treturn (await this.query.perform(\n\t\t\tQueryPerform.FIND_ONE,\n\t\t\t{\n\t\t\t\tschema: employeeTableSchema,\n\t\t\t},\n\t\t\t'testing',\n\t\t)) as FindOneResponseObject\n\t}\n\n\tasync deleteEmployee(id: any): Promise<void> {\n\t\tconst employeeTableSchema = await this.schema.getSchema({ table })\n\t\tawait this.query.perform(\n\t\t\tQueryPerform.DELETE,\n\t\t\t{\n\t\t\t\tschema: employeeTableSchema,\n\t\t\t\tid,\n\t\t\t},\n\t\t\t'testing',\n\t\t)\n\t}\n}\n"
  },
  {
    "path": "src/testing/relations.testing.service.ts",
    "content": "import { Injectable } from '@nestjs/common'\n\nimport { LLANA_RELATION_TABLE } from '../app.constants'\nimport { FindOneResponseObject } from '../dtos/response.dto'\nimport { Query } from '../helpers/Query'\nimport { Schema } from '../helpers/Schema'\nimport { DataSourceCreateOneOptions, DataSourceSchemaRelation, QueryPerform } from '../types/datasource.types'\n\n@Injectable()\nexport class RelationsTestingService {\n\tconstructor(\n\t\tprivate readonly query: Query,\n\t\tprivate readonly schema: Schema,\n\t) {}\n\n\tasync createRelationsRecord(data: DataSourceSchemaRelation): Promise<FindOneResponseObject> {\n\t\tconst schema = await this.schema.getSchema({ table: LLANA_RELATION_TABLE, x_request_id: 'test' })\n\t\treturn (await this.query.perform(QueryPerform.CREATE, <DataSourceCreateOneOptions>{\n\t\t\tschema,\n\t\t\tdata,\n\t\t})) as FindOneResponseObject\n\t}\n\n\tasync deleteRelationsRecord(data: any): Promise<void> {\n\t\tconst schema = await this.schema.getSchema({ table: LLANA_RELATION_TABLE, x_request_id: 'test' })\n\t\tawait this.query.perform(QueryPerform.DELETE, {\n\t\t\tid: data[schema.primary_key],\n\t\t\tschema,\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/testing/salesorder.testing.service.ts",
    "content": "import { Injectable } from '@nestjs/common'\n\nimport { FindOneResponseObject } from '../dtos/response.dto'\nimport { Query } from '../helpers/Query'\nimport { Schema } from '../helpers/Schema'\nimport { QueryPerform } from '../types/datasource.types'\n\nconst table = 'SalesOrder'\nlet orderNumber = 1000\n\n@Injectable()\nexport class SalesOrderTestingService {\n\tconstructor(\n\t\tprivate readonly query: Query,\n\t\tprivate readonly schema: Schema,\n\t) {}\n\n\tmockOrder(): any {\n\t\torderNumber++\n\t\treturn {\n\t\t\torderId: orderNumber,\n\t\t\torderDate: new Date(Date.now() - orderNumber * 1000000).toISOString(),\n\t\t\trequiredDate: new Date(Date.now() - orderNumber * 900000).toISOString(),\n\t\t\tshippedDate: new Date(Date.now() - orderNumber * 800000).toISOString(),\n\t\t\tfreight: orderNumber * 1.5,\n\t\t\tshipName: `ShipName_${orderNumber}`,\n\t\t\tshipAddress: `Address_${orderNumber}`,\n\t\t\tshipCity: `City_${orderNumber}`.substring(0, 15),\n\t\t\tshipPostalCode: '123456',\n\t\t\tshipCountry: `Country_${orderNumber}`,\n\t\t}\n\t}\n\n\tasync getSchema(): Promise<any> {\n\t\treturn await this.schema.getSchema({ table })\n\t}\n\n\tasync createOrder(order: { custId; employeeId; shipperId; orderId? }): Promise<any> {\n\t\tconst salesOrderTableSchema = await this.schema.getSchema({ table, x_request_id: 'testing' })\n\n\t\tconst ORDER = this.mockOrder()\n\n\t\treturn (await this.query.perform(\n\t\t\tQueryPerform.CREATE,\n\t\t\t{\n\t\t\t\tschema: salesOrderTableSchema,\n\t\t\t\tdata: {\n\t\t\t\t\t...ORDER,\n\t\t\t\t\t...order,\n\t\t\t\t},\n\t\t\t},\n\t\t\t'testing',\n\t\t)) as FindOneResponseObject\n\t}\n\n\tasync deleteOrder(id: any): Promise<void> {\n\t\tconst salesOrderTableSchema = await this.schema.getSchema({ table })\n\t\tawait this.query.perform(\n\t\t\tQueryPerform.DELETE,\n\t\t\t{\n\t\t\t\tschema: salesOrderTableSchema,\n\t\t\t\tid,\n\t\t\t},\n\t\t\t'testing',\n\t\t)\n\t}\n}\n"
  },
  {
    "path": "src/testing/shipper.testing.service.ts",
    "content": "import { Injectable } from '@nestjs/common'\n\nimport { FindOneResponseObject } from '../dtos/response.dto'\nimport { Query } from '../helpers/Query'\nimport { Schema } from '../helpers/Schema'\nimport { QueryPerform } from '../types/datasource.types'\n\nconst table = 'Shipper'\nlet shipperNumber = 4000\n\n@Injectable()\nexport class ShipperTestingService {\n\tconstructor(\n\t\tprivate readonly query: Query,\n\t\tprivate readonly schema: Schema,\n\t) {}\n\n\tmockShipper(): any {\n\t\tshipperNumber++\n\t\treturn {\n\t\t\tshipperId: shipperNumber,\n\t\t\tphone: `555-000-1234`,\n\t\t\tcompanyName: `CompanyName ${shipperNumber}`,\n\t\t}\n\t}\n\n\tasync getSchema(): Promise<any> {\n\t\treturn await this.schema.getSchema({ table })\n\t}\n\n\tasync createShipper(shipper: any): Promise<any> {\n\t\tconst shipperTableSchema = await this.schema.getSchema({ table })\n\n\t\tconst SHIPPER = this.mockShipper()\n\n\t\treturn (await this.query.perform(\n\t\t\tQueryPerform.CREATE,\n\t\t\t{\n\t\t\t\tschema: shipperTableSchema,\n\t\t\t\tdata: {\n\t\t\t\t\t...SHIPPER,\n\t\t\t\t\t...shipper,\n\t\t\t\t},\n\t\t\t},\n\t\t\t'testing',\n\t\t)) as FindOneResponseObject\n\t}\n\n\tasync getShipper(): Promise<any> {\n\t\tconst shipperTableSchema = await this.schema.getSchema({ table })\n\n\t\treturn (await this.query.perform(\n\t\t\tQueryPerform.FIND_ONE,\n\t\t\t{\n\t\t\t\tschema: shipperTableSchema,\n\t\t\t},\n\t\t\t'testing',\n\t\t)) as FindOneResponseObject\n\t}\n\n\tasync deleteShipper(id: any): Promise<void> {\n\t\tconst shipperTableSchema = await this.schema.getSchema({ table })\n\t\tawait this.query.perform(\n\t\t\tQueryPerform.DELETE,\n\t\t\t{\n\t\t\t\tschema: shipperTableSchema,\n\t\t\t\tid,\n\t\t\t},\n\t\t\t'testing',\n\t\t)\n\t}\n}\n"
  },
  {
    "path": "src/testing/testing.const.ts",
    "content": "export const TIMEOUT = 120000\n"
  },
  {
    "path": "src/testing/user.testing.service.ts",
    "content": "import { Injectable } from '@nestjs/common'\n\nimport { FindOneResponseObject } from '../dtos/response.dto'\nimport { Query } from '../helpers/Query'\nimport { Schema } from '../helpers/Schema'\nimport { QueryPerform } from '../types/datasource.types'\n\nconst table = 'User'\nlet userNumber = new Date().getTime() // Simple way to generate a unique number based on current time\n\n@Injectable()\nexport class UserTestingService {\n\tconstructor(\n\t\tprivate readonly query: Query,\n\t\tprivate readonly schema: Schema,\n\t) {}\n\n\tmockUser(props = {}): any {\n\n\t\tuserNumber++\n\n\t\treturn {\n\t\t\temail: `test-user${userNumber}@gmail.com`,\n\t\t\tpassword: 'asdlkjh132093ERWF',\n\t\t\trole: 'USER',\n\t\t\tfirstName: `First${userNumber}`,\n\t\t\tlastName: `Last${userNumber}`,\n\t\t\t...props,\n\t\t}\n\t}\n\n\tasync getSchema(): Promise<any> {\n\t\treturn await this.schema.getSchema({ table })\n\t}\n\n\tasync createUser(user?: any): Promise<any> {\n\t\tconst userSchema = await this.schema.getSchema({ table })\n\n\t\tconst USER = this.mockUser()\n\n\t\treturn (await this.query.perform(\n\t\t\tQueryPerform.CREATE,\n\t\t\t{\n\t\t\t\tschema: userSchema,\n\t\t\t\tdata: {\n\t\t\t\t\t...USER,\n\t\t\t\t\t...user,\n\t\t\t\t},\n\t\t\t},\n\t\t\t'testing',\n\t\t)) as FindOneResponseObject\n\t}\n\n\tasync deleteUser(id: any): Promise<void> {\n\t\tconst userSchema = await this.schema.getSchema({ table })\n\t\tawait this.query.perform(\n\t\t\tQueryPerform.DELETE,\n\t\t\t{\n\t\t\t\tschema: userSchema,\n\t\t\t\tid,\n\t\t\t},\n\t\t\t'testing',\n\t\t)\n\t}\n}\n"
  },
  {
    "path": "src/types/auth.types.ts",
    "content": "import { Request } from 'express'\n\nimport { DataSourceWhere } from './datasource.types'\n\nexport interface Auth {\n\ttype: AuthType\n\tlocation: AuthLocation\n\tname: string\n\ttable: AuthAPIKey | AuthJWT\n}\n\nexport enum AuthType {\n\tAPIKEY = 'APIKEY',\n\tJWT = 'JWT',\n}\n\nexport enum AuthLocation {\n\tHEADER = 'HEADER',\n\tQUERY = 'QUERY',\n\tBODY = 'BODY',\n}\n\nexport interface AuthAPIKey extends AuthTableSettings {\n\tcolumn: string\n}\n\nexport interface AuthJWT extends AuthTableSettings {\n\tcolumns: {\n\t\tusername: string\n\t\tpassword: string\n\t}\n\tpassword: {\n\t\tencryption: AuthPasswordEncryption\n\t\tsalt?: string\n\t}\n}\n\nexport interface AuthTableSettings {\n\tname: string\n\tidentity_column?: string // If your identity column is not the table primary key\n}\n\nexport enum AuthPasswordEncryption {\n\tBCRYPT = 'BCRYPT',\n\tSHA1 = 'SHA1',\n\tSHA256 = 'SHA256',\n\tSHA512 = 'SHA512',\n\tMD5 = 'MD5',\n\tARGON2 = 'ARGON2',\n}\n\nexport interface AuthRestrictionsResponse {\n\tvalid: boolean\n\tmessage?: string\n\tuser_identifier?: string\n\tallowed_fields?: string[]\n}\n\nexport interface AuthTablePermissionSuccessResponse extends AuthTablePermissionResponse {\n\trestriction?: DataSourceWhere\n\tallowed_fields?: string[]\n}\n\nexport interface AuthTablePermissionFailResponse extends AuthTablePermissionResponse {\n\tmessage: string\n}\n\nexport interface AuthTablePermissionResponse {\n\tvalid: boolean\n}\n\nexport interface AuthenticatedRequest extends Request {\n\tuser: any\n}\n"
  },
  {
    "path": "src/types/datasource.types.ts",
    "content": "import { SortCondition } from './schema.types'\n\nexport enum DataSourceType {\n\tMYSQL = 'mysql',\n\tPOSTGRES = 'postgres',\n\tMONGODB = 'mongodb',\n\tMSSQL = 'mssql',\n\tAIRTABLE = 'airtable',\n}\n\nexport enum DataSourceNaming {\n\tsnake_case = 'snake_case',\n\tcamelCase = 'camelCase',\n}\n\nexport enum QueryPerform {\n\tCREATE = 'create',\n\tFIND_ONE = 'find',\n\tFIND_MANY = 'findMany',\n\tUPDATE = 'update',\n\tDELETE = 'delete',\n\tUNIQUE = 'unique',\n\tTRUNCATE = 'truncate',\n\tCREATE_TABLE = 'createTable',\n\tCHECK_CONNECTION = 'checkConnection',\n\tLIST_TABLES = 'listTables',\n\tRESET_SEQUENCES = 'resetSequences',\n}\n\nexport enum PublishType {\n\tINSERT = 'INSERT',\n\tUPDATE = 'UPDATE',\n\tDELETE = 'DELETE',\n}\n\nexport enum WhereOperator {\n\tequals = '=',\n\tnot_equals = '!=',\n\tlt = '<',\n\tlte = '<=',\n\tgt = '>',\n\tgte = '>=',\n\tlike = 'LIKE',\n\tnot_like = 'NOT LIKE',\n\tin = 'IN',\n\tnot_in = 'NOT IN',\n\tnull = 'IS NULL',\n\tnot_null = 'IS NOT NULL',\n\tsearch = 'SEARCH',\n}\n\nexport enum DataSourceColumnType {\n\tSTRING = 'string',\n\tNUMBER = 'number',\n\tBOOLEAN = 'boolean',\n\tDATE = 'date',\n\tJSON = 'json',\n\tENUM = 'enum',\n\tUNKNOWN = 'unknown',\n}\n\nexport enum ImportMode {\n\tCREATE = 'CREATE',\n\tUPSERT = 'UPSERT',\n\tDELETE = 'DELETE',\n\tREPOPULATE = 'REPOPULATE',\n}\n\nexport declare enum ChartsPeriod {\n\tMIN = 'MIN',\n\t'15MIN' = '15MIN',\n\t'30MIN' = '30MIN',\n\tHOUR = 'HOUR',\n\tDAY = 'DAY',\n\tWEEK = 'WEEK',\n\tMONTH = 'MONTH',\n\tYEAR = 'YEAR',\n}\n\nexport interface ChartResult {\n\tcount: number\n\t[key: string]: any\n\ttime_interval: Date\n}\n\nexport interface DataSourceSchema {\n\ttable: string\n\tprimary_key: string\n\tcolumns: DataSourceSchemaColumn[]\n\trelations?: DataSourceSchemaRelation[]\n\t_x_request_id?: string\n}\n\nexport interface DataSourceWhere {\n\tcolumn: string\n\toperator: WhereOperator\n\tvalue?: any\n}\n\nexport interface ColumnExtraNumber {\n\tdecimal: number // Number of decimal places\n}\n\nexport interface ColumnExtraString {\n\tlength: number // Size of the string field\n}\n\nexport interface DataSourceSchemaColumn {\n\tfield: string\n\ttype: DataSourceColumnType\n\tnullable: boolean\n\trequired: boolean\n\tprimary_key: boolean\n\tunique_key: boolean\n\tforeign_key: boolean\n\tauto_increment?: boolean\n\tdefault?: any\n\textra?: any | ColumnExtraNumber | ColumnExtraString\n\tenums?: string[]\n}\n\nexport interface DataSourceSchemaRelation {\n\ttable: string\n\tcolumn: string\n\torg_table: string\n\torg_column: string\n}\n\nexport interface DataSourceCreateOneOptions {\n\tschema: DataSourceSchema\n\tdata: object\n}\n\nexport interface DataSourceRelations {\n\ttable: string\n\tjoin: DataSourceSchemaRelation\n\tcolumns?: string[]\n\twhere?: DataSourceWhere\n\tschema: DataSourceSchema\n}\n\nexport interface DataSourceFindOneOptions extends DataSourceFindOptions {}\n\nexport interface DataSourceFindManyOptions extends DataSourceFindOptions {\n\tlimit?: number\n\toffset?: number\n\tsort?: SortCondition[]\n}\n\nexport interface DataSourceFindOptions {\n\tschema: DataSourceSchema\n\tfields?: string[]\n\twhere?: DataSourceWhere[]\n\trelations?: DataSourceRelations[]\n}\n\nexport interface DataSourceUpdateOneOptions {\n\tid: string\n\tschema: DataSourceSchema\n\tdata: object\n}\n\nexport interface DataSourceDeleteOneOptions {\n\tid: string\n\tschema: DataSourceSchema\n\tsoftDelete?: string // Soft delete column\n}\n\nexport interface DataSourceFindTotalRecords {\n\tschema: DataSourceSchema\n\twhere?: DataSourceWhere[]\n}\n\nexport interface DataSourceConfig {\n\ttype: DataSourceType\n\thost: string\n\tpoolSize: number\n\tpoolIdleTimeout?: number\n\tdefaults: {\n\t\tlimit: number\n\t\trelations: {\n\t\t\tlimit: number\n\t\t}\n\t}\n\tdeletes: {\n\t\tsoft: string | undefined\n\t}\n}\n\nexport interface DataSourceUniqueCheckOptions {\n\tschema: DataSourceSchema\n\tdata: {\n\t\t[key: string]: string | number | boolean\n\t}\n\tid?: string\n\tx_request_id?: string\n}\n\nexport interface DataSourceListTablesOptions {\n\tinclude_system?: boolean // tables like _llana_*\n\tinclude_known_db_orchestration?: boolean // like atlas_schema_revisions\n}\n\nexport enum DatabaseErrorType {\n\tDUPLICATE_RECORD = 'DUPLICATE_RECORD',\n\tUNIQUE_KEY_VIOLATION = 'UNIQUE_KEY_VIOLATION',\n\tFOREIGN_KEY_VIOLATION = 'FOREIGN_KEY_VIOLATION',\n\tNOT_NULL_VIOLATION = 'NOT_NULL_VIOLATION',\n\tCHECK_CONSTRAINT_VIOLATION = 'CHECK_CONSTRAINT_VIOLATION',\n\tUNKNOWN_ERROR = 'UNKNOWN_ERROR',\n}\n\nexport interface DataSourceInterface {\n\tcreateTable(schema: DataSourceSchema): Promise<void>\n\tfindOne(options: DataSourceFindOneOptions): Promise<any>\n\tfindMany(options: DataSourceFindManyOptions): Promise<any[]>\n\tcreateOne(options: DataSourceCreateOneOptions): Promise<any>\n\tupdateOne(options: DataSourceUpdateOneOptions): Promise<any>\n\tdeleteOne(options: DataSourceDeleteOneOptions): Promise<void>\n\tuniqueCheck(options: DataSourceUniqueCheckOptions): Promise<boolean>\n\ttruncate(schema: DataSourceSchema): Promise<void>\n\tcheckConnection(): Promise<boolean>\n\tlistTables(): Promise<string[]>\n}\n"
  },
  {
    "path": "src/types/datasources/airtable.types.ts",
    "content": "export enum AirtableColumnType {\n\t/**\n\t * A single line of text.\n\t *\n\t * **Cell format**\n\t * ```js\n\t * string\n\t * ```\n\t *\n\t * **Field options**\n\t *\n\t * n/a\n\t */\n\tSINGLE_LINE_TEXT = 'singleLineText',\n\t/**\n\t * A valid email address (e.g. andrew@example.com).\n\t *\n\t * **Cell format**\n\t * ```js\n\t * string\n\t * ```\n\t *\n\t * **Field options**\n\t *\n\t * n/a\n\t */\n\tEMAIL = 'email',\n\t/**\n\t * A valid URL (e.g. airtable.com or https://airtable.com/universe).\n\t *\n\t * **Cell format**\n\t * ```js\n\t * string\n\t * ```\n\t *\n\t * **Field options**\n\t *\n\t * n/a\n\t */\n\tURL = 'url',\n\t/**\n\t * A long text field that can span multiple lines. May contain \"mention tokens\",\n\t * e.g. `<airtable:mention id=\"menE1i9oBaGX3DseR\">@Alex</airtable:mention>`\n\t *\n\t * **Cell format**\n\t * ```js\n\t * string\n\t * ```\n\t *\n\t * **Field options**\n\t *\n\t * n/a\n\t */\n\tMULTILINE_TEXT = 'multilineText',\n\t/**\n\t * A number.\n\t *\n\t * The `precision` option indicates the number of digits shown to the right of\n\t * the decimal point for this field.\n\t *\n\t * **Cell format**\n\t * ```js\n\t * number\n\t * ```\n\t *\n\t * **Field options**\n\t * ```js\n\t * {\n\t *     precision: number, // from 0 to 8 inclusive\n\t * }\n\t * ```\n\t */\n\tNUMBER = 'number',\n\t/**\n\t * A percentage.\n\t *\n\t * When reading from and writing to a \"Percent\" field, the cell value is a decimal.\n\t * For example, 0 is 0%, 0.5 is 50%, and 1 is 100%.\n\t *\n\t * **Cell format**\n\t * ```js\n\t * number\n\t * ```\n\t *\n\t * **Field options**\n\t * ```js\n\t * {\n\t *     precision: number, // from 0 to 8 inclusive\n\t * }\n\t * ```\n\t */\n\tPERCENT = 'percent',\n\t/**\n\t * An amount of a currency.\n\t *\n\t * **Cell format**\n\t * ```js\n\t * number\n\t * ```\n\t *\n\t * **Field options**\n\t * ```js\n\t * {\n\t *     precision: number, // from 0 to 7 inclusive\n\t *     symbol: string,\n\t * }\n\t * ```\n\t */\n\tCURRENCY = 'currency',\n\t/**\n\t * Single select allows you to select a single choice from predefined choices in a dropdown.\n\t *\n\t * **Cell read format**\n\t * ```js\n\t * {\n\t *     id: string,\n\t *     name: string,\n\t *     color?: Color\n\t * }\n\t * ```\n\t * The currently selected choice.\n\t *\n\t * **Cell write format**\n\t * ```js\n\t * { id: string } | { name: string }\n\t * ```\n\t *\n\t * **Field options read format**\n\t * ```js\n\t * {\n\t *     choices: Array<{\n\t *         id: string,\n\t *         name: string,\n\t *         color?: {@link Color}, // Color is not provided when field coloring is disabled.\n\t *     }>,\n\t * }\n\t * ```\n\t *\n\t * All colors except base colors from {@link Color} can be used as choice colors (e.g.\n\t * \"blueBright\", \"blueDark1\", \"blueLight1\", \"blueLight2\" are supported, \"blue\" is not)\n\t *\n\t * Bases on a free or plus plan are limited to colors ending in \"Light2\".\n\t *\n\t * **Field options write format**\n\t * ```js\n\t * {\n\t *     choices: Array<\n\t *         // New choice format\n\t *         {name: string, color?: {@link Color}} |\n\t *         // Pre-existing choices use read format specified above\n\t *     >,\n\t * }\n\t * ```\n\t * The default behavior of calling `updateOptionsAsync` on a `SINGLE_SELECT` field allows\n\t * choices to be added or updated, but not deleted. Therefore, you should pass all pre-existing\n\t * choices in `choices` (similar to updating a `MULTIPLE_SELECTS` field type cell value). You can\n\t * do this by spreading the current choices:\n\t * ```js\n\t * const selectField = table.getFieldByName('My select field');\n\t * await selectField.updateOptionsAsync({\n\t *     choices: [\n\t *         ...selectField.options.choices,\n\t *         {name: 'My new choice'},\n\t *     ],\n\t * });\n\t *\n\t * ```\n\t *\n\t * If you want to allow choices to be deleted, you can pass an object with\n\t * `enableSelectFieldChoiceDeletion: true` as the second argument. By passing this argument,\n\t * any existing choices which are not passed again via `choices` will be deleted, and any\n\t * cells which referenced a now-deleted choice will be cleared.\n\t * ```js\n\t * const selectField = table.getFieldByName('My select field');\n\t * await selectField.updateOptionsAsync(\n\t *     {\n\t *         choices: selectField.options.choices.filter((choice) => choice.name !== 'Choice to delete'),\n\t *     },\n\t *     {enableSelectFieldChoiceDeletion: true},\n\t * );\n\t *\n\t * ```\n\t */\n\tSINGLE_SELECT = 'singleSelect',\n\t/**\n\t * Multiple select allows you to select one or more predefined choices from a dropdown\n\t *\n\t * Similar to MULTIPLE_ATTACHMENTS and MULTIPLE_COLLABORATORS, this array-type field\n\t * will override the current cell value when being updated. Be sure to spread the current\n\t * cell value if you want to keep the currently selected choices.\n\t *\n\t * **Cell read format**\n\t * ```js\n\t * Array<{\n\t *     id: string,\n\t *     name: string,\n\t *     color?: Color,\n\t * }>\n\t * ```\n\t * The currently selected choices.\n\t *\n\t * **Cell write format**\n\t * ```js\n\t * Array<{id: string} | {name: string}>\n\t * ```\n\t *\n\t * **Field options read format**\n\t * ```js\n\t * {\n\t *     choices: Array<{\n\t *         id: string,\n\t *         name: string,\n\t *         color?: Color,\n\t *     }>,\n\t * }\n\t * ```\n\t *\n\t * **Field options write format**\n\t * ```js\n\t * {\n\t *     choices: Array<\n\t *         // New choice format\n\t *         {name: string, color?: Color} |\n\t *         // Pre-existing choices use read format specified above\n\t *     >,\n\t * }\n\t * ```\n\t * The default behavior of calling `updateOptionsAsync` on a `MULTIPLE_SELECTS` field allows\n\t * choices to be added or updated, but not deleted. Therefore, you should pass all pre-existing\n\t * choices in `choices` (similar to updating a `SINGLE_SELECT` field type cell value). You can\n\t * do this by spreading the current choices:\n\t * ```js\n\t * const multipleSelectField = table.getFieldByName('My multiple select field');\n\t * await multipleSelectField.updateOptionsAsync({\n\t *     choices: [\n\t *         ...multipleSelectField.options.choices,\n\t *         {name: 'My new choice'},\n\t *     ],\n\t * });\n\t *\n\t * ```\n\t *\n\t * If you want to allow choices to be deleted, you can pass an object with\n\t * `enableSelectFieldChoiceDeletion: true` as the second argument. By passing this argument,\n\t * any existing choices which are not passed again via `choices` will be deleted, and any\n\t * cells which referenced a now-deleted choice will be cleared.\n\t * ```js\n\t * const multipleSelectField = table.getFieldByName('My multiple select field');\n\t * await multipleSelectField.updateOptionsAsync(\n\t *     {\n\t *         choices: multipleSelectField.options.choices.filter((choice) => choice.name !== 'Choice to delete'),\n\t *     },\n\t *     {enableSelectFieldChoiceDeletion: true},\n\t * );\n\t *\n\t * ```\n\t */\n\tMULTIPLE_SELECTS = 'multipleSelects',\n\t/**\n\t * A collaborator field lets you add collaborators to your records. Collaborators can optionally\n\t * be notified when they're added. A single collaborator field has been configured to only\n\t * reference one user collaborator.\n\t *\n\t * **Cell read format**\n\t * ```js\n\t * {\n\t *     id: string,\n\t *     email: string,\n\t *     name?: string,\n\t *     profilePicUrl?: string,\n\t * }\n\t * ```\n\t * The currently selected user collaborator.\n\t *\n\t * **Cell write format**\n\t * ```js\n\t * { id: string }\n\t * ```\n\t *\n\t * **Field options read format**\n\t * ```js\n\t * {\n\t *     choices: Array<{\n\t *         id: string,\n\t *         email: string,\n\t *         name?: string,\n\t *         profilePicUrl?: string,\n\t *     }>,\n\t * }\n\t * ```\n\t *\n\t * **Field options write format**\n\t *\n\t * N/A\n\t *\n\t * Options are not required when creating a `SINGLE_COLLABORATOR` field, and updating options is\n\t * not supported.\n\t *\n\t */\n\tSINGLE_COLLABORATOR = 'singleCollaborator',\n\t/**\n\t * A collaborator field lets you add collaborators to your records. Collaborators can optionally\n\t * be notified when they're added. A multiple collaborator field has been configured to\n\t * reference any number of user or user group collaborators.\n\t *\n\t * Note: Adding user groups to multiple collaborator fields is an upcoming enterprise feature currently\n\t * in beta, and will be generally released on August 29, 2022.\n\t *\n\t * Similar to MULTIPLE_ATTACHMENTS and MULTIPLE_SELECTS, this array-type field\n\t * will override the current cell value when being updated. Be sure to spread the current\n\t * cell value if you want to keep the currently selected collaborators.\n\t *\n\t * **Cell read format**\n\t * ```js\n\t * Array<{\n\t *     id: string,\n\t *     email: string,\n\t *     name?: string,\n\t *     profilePicUrl?: string,\n\t * }>\n\t * ```\n\t * The currently selected user or user group collaborators. The email property is either the email\n\t * address of the user collaborator or an RFC 2822 mailbox-list (comma-separated list of emails) that\n\t * can be used to contact all members of the user group collaborator.\n\t *\n\t * **Cell write format**\n\t * ```js\n\t * Array<{ id: string }>\n\t * ```\n\t *\n\t * **Field options read format**\n\t * ```js\n\t * {\n\t *     choices: Array<{\n\t *         id: string,\n\t *         email: string,\n\t *         name?: string,\n\t *         profilePicUrl?: string,\n\t *     }>,\n\t * }\n\t * ```\n\t *\n\t * **Field options write format**\n\t *\n\t * N/A\n\t *\n\t * Options are not required when creating a `MULTIPLE_COLLABORATORS` field, and updating options\n\t * is not supported.\n\t */\n\tMULTIPLE_COLLABORATORS = 'multipleCollaborators',\n\t/**\n\t * Link to another record.\n\t *\n\t * When updating an existing linked record cell value, the specified array will\n\t * overwrite the current cell value. If you want to add a new linked record without\n\t * deleting the current linked records, you can spread the current cell value like so:\n\t * ```js\n\t * const newForeignRecordIdToLink = 'recXXXXXXXXXXXXXX';\n\t * myTable.updateRecordAsync(myRecord, {\n\t *     'myLinkedRecordField': [\n\t *         ...myRecord.getCellValue('myLinkedRecordField'),\n\t *         { id: newForeignRecordIdToLink }\n\t *     ]\n\t * });\n\t * ```\n\t *\n\t * Similarly, you can clear the current cell value by passing an empty array, or\n\t * remove specific linked records by passing a filtered array of the current cell\n\t * value.\n\t *\n\t * **Cell read format**\n\t * ```js\n\t * Array<{\n\t *     id: RecordId,\n\t *     name: string,\n\t * }>\n\t * ```\n\t * The currently linked record IDs and their primary cell values from the linked table.\n\t *\n\t * **Cell write format**\n\t * ```js\n\t * Array<{ id: RecordId }>\n\t * ```\n\t *\n\t * **Field options read format**\n\t * ```js\n\t * {\n\t *     // The ID of the table this field links to\n\t *     linkedTableId: TableId,\n\t *     // The ID of the field in the linked table that links back\n\t *     // to this one\n\t *     inverseLinkFieldId?: FieldId,\n\t *     // The ID of the view in the linked table to use when showing\n\t *     // a list of records to select from\n\t *     viewIdForRecordSelection?: ViewId,\n\t *     // Whether linked records are rendered in the reverse order from the cell value in the\n\t *     // Airtable UI (i.e. most recent first)\n\t *     // You generally do not need to rely on this option.\n\t *     isReversed: boolean,\n\t *     // Whether this field prefers to only have a single linked record. While this preference\n\t *     // is enforced in the Airtable UI, it is possible for a field that prefers single linked\n\t *     // records to have multiple record links (for example, via copy-and-paste or programmatic\n\t *     // updates).\n\t *     prefersSingleRecordLink: boolean,\n\t * }\n\t * ```\n\t *\n\t * **Field options write format**\n\t * ```js\n\t * {\n\t *     // The ID of the table this field links to\n\t *     linkedTableId: TableId,\n\t *     // The ID of the view in the linked table to use when showing\n\t *     // a list of records to select from\n\t *     viewIdForRecordSelection?: ViewId,\n\t *     // Note: prefersSingleRecordLink cannot be specified via programmatic field creation\n\t *     // and will be false for fields created within an app\n\t * }\n\t * ```\n\t *\n\t * Creating `MULTIPLE_RECORD_LINKS` fields is supported but updating options for existing\n\t * `MULTIPLE_RECORD_LINKS` fields is not supported.\n\t */\n\tMULTIPLE_RECORD_LINKS = 'multipleRecordLinks',\n\t/**\n\t * A date.\n\t *\n\t * When reading from and writing to a date field, the cell value will always be an\n\t * [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) formatted date. (Field\n\t * options specify how it's formatted in the main Airtable UI - `format` can be used with\n\t * [`moment.js`](https://momentjs.com/) to match that.)\n\t *\n\t * The date format string follows the moment.js structure documented\n\t * [here](https://momentjs.com/docs/#/parsing/string-format/)\n\t *\n\t * **Cell read format**\n\t * ```js\n\t * string\n\t * ```\n\t *\n\t * **Cell write format**\n\t * ```js\n\t * Date | string\n\t * ```\n\t *\n\t * **Field options read format**\n\t * ```js\n\t * {\n\t *     dateFormat:\n\t *          | {name: 'local', format: 'l'}\n\t *          | {name: 'friendly', format: 'LL'}\n\t *          | {name: 'us', format: 'M/D/YYYY'}\n\t *          | {name: 'european', format: 'D/M/YYYY'}\n\t *          | {name: 'iso', format: 'YYYY-MM-DD'}\n\t * }\n\t * ```\n\t *\n\t * **Field options write format**\n\t * ```js\n\t * {\n\t *     dateFormat:\n\t *          // Format is optional, but must match name if provided.\n\t *          | {name: 'local', format?: 'l'}\n\t *          | {name: 'friendly', format?: 'LL'}\n\t *          | {name: 'us', format?: 'M/D/YYYY'}\n\t *          | {name: 'european', format?: 'D/M/YYYY'}\n\t *          | {name: 'iso', format?: 'YYYY-MM-DD'}\n\t * }\n\t * ```\n\t */\n\tDATE = 'date',\n\t/**\n\t * A date field configured to also include a time.\n\t *\n\t * When reading from and writing to a date field, the cell value will always be an\n\t * [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) formatted date. (Field\n\t * options specify how it's formatted in the main Airtable UI - `format` can be used with\n\t * [`moment.js`](https://momentjs.com/) to match that.)\n\t *\n\t * The date and time format strings follow the moment.js structure documented\n\t * [here](https://momentjs.com/docs/#/parsing/string-format/)\n\t *\n\t * **Cell read format**\n\t * ```js\n\t * string\n\t * ```\n\t *\n\t * **Cell write format**\n\t * ```js\n\t * Date | string\n\t * ```\n\t * For a `dateTime` field configured with a non `utc` or `client` time zone like `America/Los_Angeles`,\n\t * ambiguous string inputs like \"2020-09-05T07:00:00\" and \"2020-09-08\" will be interpreted according to `timeZone` of the field instead of `utc`, and\n\t * nonambiguous string inputs with zone offset like \"2020-09-05T07:00:00.000Z\" and \"2020-09-08T00:00:00-07:00\" will be interpreted as the underlying timestamp.\n\t *\n\t * **Field options read format**\n\t * ```js\n\t * {\n\t *     dateFormat:\n\t *          | {name: 'local', format: 'l'}\n\t *          | {name: 'friendly', format: 'LL'}\n\t *          | {name: 'us', format: 'M/D/YYYY'}\n\t *          | {name: 'european', format: 'D/M/YYYY'}\n\t *          | {name: 'iso', format: 'YYYY-MM-DD'},\n\t *     timeFormat:\n\t *          | {name: '12hour', format: 'h:mma'}\n\t *          | {name: '24hour', format: 'HH:mm'},\n\t *     timeZone: 'utc' | 'client' | 'Africa/Abidjan' | 'Africa/Accra' | 'Africa/Addis_Ababa' | 'Africa/Algiers' | 'Africa/Asmara' | 'Africa/Bamako' | 'Africa/Bangui' | 'Africa/Banjul' | 'Africa/Bissau' | 'Africa/Blantyre' | 'Africa/Brazzaville' | 'Africa/Bujumbura' | 'Africa/Cairo' | 'Africa/Casablanca' | 'Africa/Ceuta' | 'Africa/Conakry' | 'Africa/Dakar' | 'Africa/Dar_es_Salaam' | 'Africa/Djibouti' | 'Africa/Douala' | 'Africa/El_Aaiun' | 'Africa/Freetown' | 'Africa/Gaborone' | 'Africa/Harare' | 'Africa/Johannesburg' | 'Africa/Juba' | 'Africa/Kampala' | 'Africa/Khartoum' | 'Africa/Kigali' | 'Africa/Kinshasa' | 'Africa/Lagos' | 'Africa/Libreville' | 'Africa/Lome' | 'Africa/Luanda' | 'Africa/Lubumbashi' | 'Africa/Lusaka' | 'Africa/Malabo' | 'Africa/Maputo' | 'Africa/Maseru' | 'Africa/Mbabane' | 'Africa/Mogadishu' | 'Africa/Monrovia' | 'Africa/Nairobi' | 'Africa/Ndjamena' | 'Africa/Niamey' | 'Africa/Nouakchott' | 'Africa/Ouagadougou' | 'Africa/Porto-Novo' | 'Africa/Sao_Tome' | 'Africa/Tripoli' | 'Africa/Tunis' | 'Africa/Windhoek' | 'America/Adak' | 'America/Anchorage' | 'America/Anguilla' | 'America/Antigua' | 'America/Araguaina' | 'America/Argentina/Buenos_Aires' | 'America/Argentina/Catamarca' | 'America/Argentina/Cordoba' | 'America/Argentina/Jujuy' | 'America/Argentina/La_Rioja' | 'America/Argentina/Mendoza' | 'America/Argentina/Rio_Gallegos' | 'America/Argentina/Salta' | 'America/Argentina/San_Juan' | 'America/Argentina/San_Luis' | 'America/Argentina/Tucuman' | 'America/Argentina/Ushuaia' | 'America/Aruba' | 'America/Asuncion' | 'America/Atikokan' | 'America/Bahia' | 'America/Bahia_Banderas' | 'America/Barbados' | 'America/Belem' | 'America/Belize' | 'America/Blanc-Sablon' | 'America/Boa_Vista' | 'America/Bogota' | 'America/Boise' | 'America/Cambridge_Bay' | 'America/Campo_Grande' | 'America/Cancun' | 'America/Caracas' | 'America/Cayenne' | 'America/Cayman' | 'America/Chicago' | 'America/Chihuahua' | 'America/Costa_Rica' | 'America/Creston' | 'America/Cuiaba' | 'America/Curacao' | 'America/Danmarkshavn' | 'America/Dawson' | 'America/Dawson_Creek' | 'America/Denver' | 'America/Detroit' | 'America/Dominica' | 'America/Edmonton' | 'America/Eirunepe' | 'America/El_Salvador' | 'America/Fort_Nelson' | 'America/Fortaleza' | 'America/Glace_Bay' | 'America/Godthab' | 'America/Goose_Bay' | 'America/Grand_Turk' | 'America/Grenada' | 'America/Guadeloupe' | 'America/Guatemala' | 'America/Guayaquil' | 'America/Guyana' | 'America/Halifax' | 'America/Havana' | 'America/Hermosillo' | 'America/Indiana/Indianapolis' | 'America/Indiana/Knox' | 'America/Indiana/Marengo' | 'America/Indiana/Petersburg' | 'America/Indiana/Tell_City' | 'America/Indiana/Vevay' | 'America/Indiana/Vincennes' | 'America/Indiana/Winamac' | 'America/Inuvik' | 'America/Iqaluit' | 'America/Jamaica' | 'America/Juneau' | 'America/Kentucky/Louisville' | 'America/Kentucky/Monticello' | 'America/Kralendijk' | 'America/La_Paz' | 'America/Lima' | 'America/Los_Angeles' | 'America/Lower_Princes' | 'America/Maceio' | 'America/Managua' | 'America/Manaus' | 'America/Marigot' | 'America/Martinique' | 'America/Matamoros' | 'America/Mazatlan' | 'America/Menominee' | 'America/Merida' | 'America/Metlakatla' | 'America/Mexico_City' | 'America/Miquelon' | 'America/Moncton' | 'America/Monterrey' | 'America/Montevideo' | 'America/Montserrat' | 'America/Nassau' | 'America/New_York' | 'America/Nipigon' | 'America/Nome' | 'America/Noronha' | 'America/North_Dakota/Beulah' | 'America/North_Dakota/Center' | 'America/North_Dakota/New_Salem' | 'America/Nuuk' | 'America/Ojinaga' | 'America/Panama' | 'America/Pangnirtung' | 'America/Paramaribo' | 'America/Phoenix' | 'America/Port-au-Prince' | 'America/Port_of_Spain' | 'America/Porto_Velho' | 'America/Puerto_Rico' | 'America/Punta_Arenas' | 'America/Rainy_River' | 'America/Rankin_Inlet' | 'America/Recife' | 'America/Regina' | 'America/Resolute' | 'America/Rio_Branco' | 'America/Santarem' | 'America/Santiago' | 'America/Santo_Domingo' | 'America/Sao_Paulo' | 'America/Scoresbysund' | 'America/Sitka' | 'America/St_Barthelemy' | 'America/St_Johns' | 'America/St_Kitts' | 'America/St_Lucia' | 'America/St_Thomas' | 'America/St_Vincent' | 'America/Swift_Current' | 'America/Tegucigalpa' | 'America/Thule' | 'America/Thunder_Bay' | 'America/Tijuana' | 'America/Toronto' | 'America/Tortola' | 'America/Vancouver' | 'America/Whitehorse' | 'America/Winnipeg' | 'America/Yakutat' | 'America/Yellowknife' | 'Antarctica/Casey' | 'Antarctica/Davis' | 'Antarctica/DumontDUrville' | 'Antarctica/Macquarie' | 'Antarctica/Mawson' | 'Antarctica/McMurdo' | 'Antarctica/Palmer' | 'Antarctica/Rothera' | 'Antarctica/Syowa' | 'Antarctica/Troll' | 'Antarctica/Vostok' | 'Arctic/Longyearbyen' | 'Asia/Aden' | 'Asia/Almaty' | 'Asia/Amman' | 'Asia/Anadyr' | 'Asia/Aqtau' | 'Asia/Aqtobe' | 'Asia/Ashgabat' | 'Asia/Atyrau' | 'Asia/Baghdad' | 'Asia/Bahrain' | 'Asia/Baku' | 'Asia/Bangkok' | 'Asia/Barnaul' | 'Asia/Beirut' | 'Asia/Bishkek' | 'Asia/Brunei' | 'Asia/Chita' | 'Asia/Choibalsan' | 'Asia/Colombo' | 'Asia/Damascus' | 'Asia/Dhaka' | 'Asia/Dili' | 'Asia/Dubai' | 'Asia/Dushanbe' | 'Asia/Famagusta' | 'Asia/Gaza' | 'Asia/Hebron' | 'Asia/Ho_Chi_Minh' | 'Asia/Hong_Kong' | 'Asia/Hovd' | 'Asia/Irkutsk' | 'Asia/Istanbul' | 'Asia/Jakarta' | 'Asia/Jayapura' | 'Asia/Jerusalem' | 'Asia/Kabul' | 'Asia/Kamchatka' | 'Asia/Karachi' | 'Asia/Kathmandu' | 'Asia/Khandyga' | 'Asia/Kolkata' | 'Asia/Krasnoyarsk' | 'Asia/Kuala_Lumpur' | 'Asia/Kuching' | 'Asia/Kuwait' | 'Asia/Macau' | 'Asia/Magadan' | 'Asia/Makassar' | 'Asia/Manila' | 'Asia/Muscat' | 'Asia/Nicosia' | 'Asia/Novokuznetsk' | 'Asia/Novosibirsk' | 'Asia/Omsk' | 'Asia/Oral' | 'Asia/Phnom_Penh' | 'Asia/Pontianak' | 'Asia/Pyongyang' | 'Asia/Qatar' | 'Asia/Qostanay' | 'Asia/Qyzylorda' | 'Asia/Rangoon' | 'Asia/Riyadh' | 'Asia/Sakhalin' | 'Asia/Samarkand' | 'Asia/Seoul' | 'Asia/Shanghai' | 'Asia/Singapore' | 'Asia/Srednekolymsk' | 'Asia/Taipei' | 'Asia/Tashkent' | 'Asia/Tbilisi' | 'Asia/Tehran' | 'Asia/Thimphu' | 'Asia/Tokyo' | 'Asia/Tomsk' | 'Asia/Ulaanbaatar' | 'Asia/Urumqi' | 'Asia/Ust-Nera' | 'Asia/Vientiane' | 'Asia/Vladivostok' | 'Asia/Yakutsk' | 'Asia/Yangon' | 'Asia/Yekaterinburg' | 'Asia/Yerevan' | 'Atlantic/Azores' | 'Atlantic/Bermuda' | 'Atlantic/Canary' | 'Atlantic/Cape_Verde' | 'Atlantic/Faroe' | 'Atlantic/Madeira' | 'Atlantic/Reykjavik' | 'Atlantic/South_Georgia' | 'Atlantic/St_Helena' | 'Atlantic/Stanley' | 'Australia/Adelaide' | 'Australia/Brisbane' | 'Australia/Broken_Hill' | 'Australia/Currie' | 'Australia/Darwin' | 'Australia/Eucla' | 'Australia/Hobart' | 'Australia/Lindeman' | 'Australia/Lord_Howe' | 'Australia/Melbourne' | 'Australia/Perth' | 'Australia/Sydney' | 'Europe/Amsterdam' | 'Europe/Andorra' | 'Europe/Astrakhan' | 'Europe/Athens' | 'Europe/Belgrade' | 'Europe/Berlin' | 'Europe/Bratislava' | 'Europe/Brussels' | 'Europe/Bucharest' | 'Europe/Budapest' | 'Europe/Busingen' | 'Europe/Chisinau' | 'Europe/Copenhagen' | 'Europe/Dublin' | 'Europe/Gibraltar' | 'Europe/Guernsey' | 'Europe/Helsinki' | 'Europe/Isle_of_Man' | 'Europe/Istanbul' | 'Europe/Jersey' | 'Europe/Kaliningrad' | 'Europe/Kiev' | 'Europe/Kirov' | 'Europe/Lisbon' | 'Europe/Ljubljana' | 'Europe/London' | 'Europe/Luxembourg' | 'Europe/Madrid' | 'Europe/Malta' | 'Europe/Mariehamn' | 'Europe/Minsk' | 'Europe/Monaco' | 'Europe/Moscow' | 'Europe/Nicosia' | 'Europe/Oslo' | 'Europe/Paris' | 'Europe/Podgorica' | 'Europe/Prague' | 'Europe/Riga' | 'Europe/Rome' | 'Europe/Samara' | 'Europe/San_Marino' | 'Europe/Sarajevo' | 'Europe/Saratov' | 'Europe/Simferopol' | 'Europe/Skopje' | 'Europe/Sofia' | 'Europe/Stockholm' | 'Europe/Tallinn' | 'Europe/Tirane' | 'Europe/Ulyanovsk' | 'Europe/Uzhgorod' | 'Europe/Vaduz' | 'Europe/Vatican' | 'Europe/Vienna' | 'Europe/Vilnius' | 'Europe/Volgograd' | 'Europe/Warsaw' | 'Europe/Zagreb' | 'Europe/Zaporozhye' | 'Europe/Zurich' | 'Indian/Antananarivo' | 'Indian/Chagos' | 'Indian/Christmas' | 'Indian/Cocos' | 'Indian/Comoro' | 'Indian/Kerguelen' | 'Indian/Mahe' | 'Indian/Maldives' | 'Indian/Mauritius' | 'Indian/Mayotte' | 'Indian/Reunion' | 'Pacific/Apia' | 'Pacific/Auckland' | 'Pacific/Bougainville' | 'Pacific/Chatham' | 'Pacific/Chuuk' | 'Pacific/Easter' | 'Pacific/Efate' | 'Pacific/Enderbury' | 'Pacific/Fakaofo' | 'Pacific/Fiji' | 'Pacific/Funafuti' | 'Pacific/Galapagos' | 'Pacific/Gambier' | 'Pacific/Guadalcanal' | 'Pacific/Guam' | 'Pacific/Honolulu' | 'Pacific/Kanton' | 'Pacific/Kiritimati' | 'Pacific/Kosrae' | 'Pacific/Kwajalein' | 'Pacific/Majuro' | 'Pacific/Marquesas' | 'Pacific/Midway' | 'Pacific/Nauru' | 'Pacific/Niue' | 'Pacific/Norfolk' | 'Pacific/Noumea' | 'Pacific/Pago_Pago' | 'Pacific/Palau' | 'Pacific/Pitcairn' | 'Pacific/Pohnpei' | 'Pacific/Port_Moresby' | 'Pacific/Rarotonga' | 'Pacific/Saipan' | 'Pacific/Tahiti' | 'Pacific/Tarawa' | 'Pacific/Tongatapu' | 'Pacific/Wake' | 'Pacific/Wallis',\n\t * }\n\t * ```\n\t *\n\t * **Field options write format**\n\t * ```js\n\t * {\n\t *     dateFormat:\n\t *          // Format is optional, but must match name if provided.\n\t *          | {name: 'local', format?: 'l'}\n\t *          | {name: 'friendly', format?: 'LL'}\n\t *          | {name: 'us', format?: 'M/D/YYYY'}\n\t *          | {name: 'european', format?: 'D/M/YYYY'}\n\t *          | {name: 'iso', format?: 'YYYY-MM-DD'},\n\t *     timeFormat:\n\t *          // Format is optional, but must match name if provided.\n\t *          | {name: '12hour', format?: 'h:mma'}\n\t *          | {name: '24hour', format?: 'HH:mm'},\n\t *     timeZone: 'utc' | 'client' | 'Africa/Abidjan' | 'Africa/Accra' | 'Africa/Addis_Ababa' | 'Africa/Algiers' | 'Africa/Asmara' | 'Africa/Bamako' | 'Africa/Bangui' | 'Africa/Banjul' | 'Africa/Bissau' | 'Africa/Blantyre' | 'Africa/Brazzaville' | 'Africa/Bujumbura' | 'Africa/Cairo' | 'Africa/Casablanca' | 'Africa/Ceuta' | 'Africa/Conakry' | 'Africa/Dakar' | 'Africa/Dar_es_Salaam' | 'Africa/Djibouti' | 'Africa/Douala' | 'Africa/El_Aaiun' | 'Africa/Freetown' | 'Africa/Gaborone' | 'Africa/Harare' | 'Africa/Johannesburg' | 'Africa/Juba' | 'Africa/Kampala' | 'Africa/Khartoum' | 'Africa/Kigali' | 'Africa/Kinshasa' | 'Africa/Lagos' | 'Africa/Libreville' | 'Africa/Lome' | 'Africa/Luanda' | 'Africa/Lubumbashi' | 'Africa/Lusaka' | 'Africa/Malabo' | 'Africa/Maputo' | 'Africa/Maseru' | 'Africa/Mbabane' | 'Africa/Mogadishu' | 'Africa/Monrovia' | 'Africa/Nairobi' | 'Africa/Ndjamena' | 'Africa/Niamey' | 'Africa/Nouakchott' | 'Africa/Ouagadougou' | 'Africa/Porto-Novo' | 'Africa/Sao_Tome' | 'Africa/Tripoli' | 'Africa/Tunis' | 'Africa/Windhoek' | 'America/Adak' | 'America/Anchorage' | 'America/Anguilla' | 'America/Antigua' | 'America/Araguaina' | 'America/Argentina/Buenos_Aires' | 'America/Argentina/Catamarca' | 'America/Argentina/Cordoba' | 'America/Argentina/Jujuy' | 'America/Argentina/La_Rioja' | 'America/Argentina/Mendoza' | 'America/Argentina/Rio_Gallegos' | 'America/Argentina/Salta' | 'America/Argentina/San_Juan' | 'America/Argentina/San_Luis' | 'America/Argentina/Tucuman' | 'America/Argentina/Ushuaia' | 'America/Aruba' | 'America/Asuncion' | 'America/Atikokan' | 'America/Bahia' | 'America/Bahia_Banderas' | 'America/Barbados' | 'America/Belem' | 'America/Belize' | 'America/Blanc-Sablon' | 'America/Boa_Vista' | 'America/Bogota' | 'America/Boise' | 'America/Cambridge_Bay' | 'America/Campo_Grande' | 'America/Cancun' | 'America/Caracas' | 'America/Cayenne' | 'America/Cayman' | 'America/Chicago' | 'America/Chihuahua' | 'America/Costa_Rica' | 'America/Creston' | 'America/Cuiaba' | 'America/Curacao' | 'America/Danmarkshavn' | 'America/Dawson' | 'America/Dawson_Creek' | 'America/Denver' | 'America/Detroit' | 'America/Dominica' | 'America/Edmonton' | 'America/Eirunepe' | 'America/El_Salvador' | 'America/Fort_Nelson' | 'America/Fortaleza' | 'America/Glace_Bay' | 'America/Godthab' | 'America/Goose_Bay' | 'America/Grand_Turk' | 'America/Grenada' | 'America/Guadeloupe' | 'America/Guatemala' | 'America/Guayaquil' | 'America/Guyana' | 'America/Halifax' | 'America/Havana' | 'America/Hermosillo' | 'America/Indiana/Indianapolis' | 'America/Indiana/Knox' | 'America/Indiana/Marengo' | 'America/Indiana/Petersburg' | 'America/Indiana/Tell_City' | 'America/Indiana/Vevay' | 'America/Indiana/Vincennes' | 'America/Indiana/Winamac' | 'America/Inuvik' | 'America/Iqaluit' | 'America/Jamaica' | 'America/Juneau' | 'America/Kentucky/Louisville' | 'America/Kentucky/Monticello' | 'America/Kralendijk' | 'America/La_Paz' | 'America/Lima' | 'America/Los_Angeles' | 'America/Lower_Princes' | 'America/Maceio' | 'America/Managua' | 'America/Manaus' | 'America/Marigot' | 'America/Martinique' | 'America/Matamoros' | 'America/Mazatlan' | 'America/Menominee' | 'America/Merida' | 'America/Metlakatla' | 'America/Mexico_City' | 'America/Miquelon' | 'America/Moncton' | 'America/Monterrey' | 'America/Montevideo' | 'America/Montserrat' | 'America/Nassau' | 'America/New_York' | 'America/Nipigon' | 'America/Nome' | 'America/Noronha' | 'America/North_Dakota/Beulah' | 'America/North_Dakota/Center' | 'America/North_Dakota/New_Salem' | 'America/Nuuk' | 'America/Ojinaga' | 'America/Panama' | 'America/Pangnirtung' | 'America/Paramaribo' | 'America/Phoenix' | 'America/Port-au-Prince' | 'America/Port_of_Spain' | 'America/Porto_Velho' | 'America/Puerto_Rico' | 'America/Punta_Arenas' | 'America/Rainy_River' | 'America/Rankin_Inlet' | 'America/Recife' | 'America/Regina' | 'America/Resolute' | 'America/Rio_Branco' | 'America/Santarem' | 'America/Santiago' | 'America/Santo_Domingo' | 'America/Sao_Paulo' | 'America/Scoresbysund' | 'America/Sitka' | 'America/St_Barthelemy' | 'America/St_Johns' | 'America/St_Kitts' | 'America/St_Lucia' | 'America/St_Thomas' | 'America/St_Vincent' | 'America/Swift_Current' | 'America/Tegucigalpa' | 'America/Thule' | 'America/Thunder_Bay' | 'America/Tijuana' | 'America/Toronto' | 'America/Tortola' | 'America/Vancouver' | 'America/Whitehorse' | 'America/Winnipeg' | 'America/Yakutat' | 'America/Yellowknife' | 'Antarctica/Casey' | 'Antarctica/Davis' | 'Antarctica/DumontDUrville' | 'Antarctica/Macquarie' | 'Antarctica/Mawson' | 'Antarctica/McMurdo' | 'Antarctica/Palmer' | 'Antarctica/Rothera' | 'Antarctica/Syowa' | 'Antarctica/Troll' | 'Antarctica/Vostok' | 'Arctic/Longyearbyen' | 'Asia/Aden' | 'Asia/Almaty' | 'Asia/Amman' | 'Asia/Anadyr' | 'Asia/Aqtau' | 'Asia/Aqtobe' | 'Asia/Ashgabat' | 'Asia/Atyrau' | 'Asia/Baghdad' | 'Asia/Bahrain' | 'Asia/Baku' | 'Asia/Bangkok' | 'Asia/Barnaul' | 'Asia/Beirut' | 'Asia/Bishkek' | 'Asia/Brunei' | 'Asia/Chita' | 'Asia/Choibalsan' | 'Asia/Colombo' | 'Asia/Damascus' | 'Asia/Dhaka' | 'Asia/Dili' | 'Asia/Dubai' | 'Asia/Dushanbe' | 'Asia/Famagusta' | 'Asia/Gaza' | 'Asia/Hebron' | 'Asia/Ho_Chi_Minh' | 'Asia/Hong_Kong' | 'Asia/Hovd' | 'Asia/Irkutsk' | 'Asia/Istanbul' | 'Asia/Jakarta' | 'Asia/Jayapura' | 'Asia/Jerusalem' | 'Asia/Kabul' | 'Asia/Kamchatka' | 'Asia/Karachi' | 'Asia/Kathmandu' | 'Asia/Khandyga' | 'Asia/Kolkata' | 'Asia/Krasnoyarsk' | 'Asia/Kuala_Lumpur' | 'Asia/Kuching' | 'Asia/Kuwait' | 'Asia/Macau' | 'Asia/Magadan' | 'Asia/Makassar' | 'Asia/Manila' | 'Asia/Muscat' | 'Asia/Nicosia' | 'Asia/Novokuznetsk' | 'Asia/Novosibirsk' | 'Asia/Omsk' | 'Asia/Oral' | 'Asia/Phnom_Penh' | 'Asia/Pontianak' | 'Asia/Pyongyang' | 'Asia/Qatar' | 'Asia/Qostanay' | 'Asia/Qyzylorda' | 'Asia/Rangoon' | 'Asia/Riyadh' | 'Asia/Sakhalin' | 'Asia/Samarkand' | 'Asia/Seoul' | 'Asia/Shanghai' | 'Asia/Singapore' | 'Asia/Srednekolymsk' | 'Asia/Taipei' | 'Asia/Tashkent' | 'Asia/Tbilisi' | 'Asia/Tehran' | 'Asia/Thimphu' | 'Asia/Tokyo' | 'Asia/Tomsk' | 'Asia/Ulaanbaatar' | 'Asia/Urumqi' | 'Asia/Ust-Nera' | 'Asia/Vientiane' | 'Asia/Vladivostok' | 'Asia/Yakutsk' | 'Asia/Yangon' | 'Asia/Yekaterinburg' | 'Asia/Yerevan' | 'Atlantic/Azores' | 'Atlantic/Bermuda' | 'Atlantic/Canary' | 'Atlantic/Cape_Verde' | 'Atlantic/Faroe' | 'Atlantic/Madeira' | 'Atlantic/Reykjavik' | 'Atlantic/South_Georgia' | 'Atlantic/St_Helena' | 'Atlantic/Stanley' | 'Australia/Adelaide' | 'Australia/Brisbane' | 'Australia/Broken_Hill' | 'Australia/Currie' | 'Australia/Darwin' | 'Australia/Eucla' | 'Australia/Hobart' | 'Australia/Lindeman' | 'Australia/Lord_Howe' | 'Australia/Melbourne' | 'Australia/Perth' | 'Australia/Sydney' | 'Europe/Amsterdam' | 'Europe/Andorra' | 'Europe/Astrakhan' | 'Europe/Athens' | 'Europe/Belgrade' | 'Europe/Berlin' | 'Europe/Bratislava' | 'Europe/Brussels' | 'Europe/Bucharest' | 'Europe/Budapest' | 'Europe/Busingen' | 'Europe/Chisinau' | 'Europe/Copenhagen' | 'Europe/Dublin' | 'Europe/Gibraltar' | 'Europe/Guernsey' | 'Europe/Helsinki' | 'Europe/Isle_of_Man' | 'Europe/Istanbul' | 'Europe/Jersey' | 'Europe/Kaliningrad' | 'Europe/Kiev' | 'Europe/Kirov' | 'Europe/Lisbon' | 'Europe/Ljubljana' | 'Europe/London' | 'Europe/Luxembourg' | 'Europe/Madrid' | 'Europe/Malta' | 'Europe/Mariehamn' | 'Europe/Minsk' | 'Europe/Monaco' | 'Europe/Moscow' | 'Europe/Nicosia' | 'Europe/Oslo' | 'Europe/Paris' | 'Europe/Podgorica' | 'Europe/Prague' | 'Europe/Riga' | 'Europe/Rome' | 'Europe/Samara' | 'Europe/San_Marino' | 'Europe/Sarajevo' | 'Europe/Saratov' | 'Europe/Simferopol' | 'Europe/Skopje' | 'Europe/Sofia' | 'Europe/Stockholm' | 'Europe/Tallinn' | 'Europe/Tirane' | 'Europe/Ulyanovsk' | 'Europe/Uzhgorod' | 'Europe/Vaduz' | 'Europe/Vatican' | 'Europe/Vienna' | 'Europe/Vilnius' | 'Europe/Volgograd' | 'Europe/Warsaw' | 'Europe/Zagreb' | 'Europe/Zaporozhye' | 'Europe/Zurich' | 'Indian/Antananarivo' | 'Indian/Chagos' | 'Indian/Christmas' | 'Indian/Cocos' | 'Indian/Comoro' | 'Indian/Kerguelen' | 'Indian/Mahe' | 'Indian/Maldives' | 'Indian/Mauritius' | 'Indian/Mayotte' | 'Indian/Reunion' | 'Pacific/Apia' | 'Pacific/Auckland' | 'Pacific/Bougainville' | 'Pacific/Chatham' | 'Pacific/Chuuk' | 'Pacific/Easter' | 'Pacific/Efate' | 'Pacific/Enderbury' | 'Pacific/Fakaofo' | 'Pacific/Fiji' | 'Pacific/Funafuti' | 'Pacific/Galapagos' | 'Pacific/Gambier' | 'Pacific/Guadalcanal' | 'Pacific/Guam' | 'Pacific/Honolulu' | 'Pacific/Kanton' | 'Pacific/Kiritimati' | 'Pacific/Kosrae' | 'Pacific/Kwajalein' | 'Pacific/Majuro' | 'Pacific/Marquesas' | 'Pacific/Midway' | 'Pacific/Nauru' | 'Pacific/Niue' | 'Pacific/Norfolk' | 'Pacific/Noumea' | 'Pacific/Pago_Pago' | 'Pacific/Palau' | 'Pacific/Pitcairn' | 'Pacific/Pohnpei' | 'Pacific/Port_Moresby' | 'Pacific/Rarotonga' | 'Pacific/Saipan' | 'Pacific/Tahiti' | 'Pacific/Tarawa' | 'Pacific/Tongatapu' | 'Pacific/Wake' | 'Pacific/Wallis',\n\t * }\n\t * ```\n\t */\n\tDATE_TIME = 'dateTime',\n\t/**\n\t * A telephone number (e.g. (415) 555-9876).\n\t *\n\t * **Cell format**\n\t * ```js\n\t * string\n\t * ```\n\t *\n\t * **Field options**\n\t *\n\t * None\n\t */\n\tPHONE_NUMBER = 'phoneNumber',\n\t/**\n\t * Attachments allow you to add images, documents, or other files which can then be viewed or downloaded.\n\t *\n\t * When updating an existing attachment cell value, the specified array will\n\t * overwrite the current cell value. If you want to add a new attachment without\n\t * deleting the current attachments, you can spread the current cell value like so:\n\t * ```js\n\t * const newAttachmentUrl = 'example.com/cute-cats.jpeg';\n\t * myTable.updateRecordAsync(myRecord, {\n\t *     'myAttachmentField': [\n\t *         ...myRecord.getCellValue('myAttachmentField'),\n\t *         { url: newAttachmentUrl }\n\t *     ]\n\t * });\n\t * ```\n\t *\n\t * Similarly, you can clear the current cell value by passing an empty array, or\n\t * remove specific attachments by passing a filtered array of the current cell\n\t * value.\n\t *\n\t * Note: when you pass an existing attachment, you must pass the full attachment\n\t * object. New attachments only require the `url` property. You can optionally\n\t * pass the \\`filename\\` property to give it a readable name.\n\t *\n\t * Additionally, the Airtable generated attachment URLs do not currently expire,\n\t * but this will change in the future. If you want to persist the attachments, we\n\t * recommend downloading them instead of saving the URL. Before this change is\n\t * rolled out, we will post a more detailed deprecation timeline.\n\t *\n\t * **Cell read format**\n\t * ```js\n\t * Array<{\n\t *     // unique attachment id\n\t *     id: string,\n\t *     // url, e.g. \"https://dl.airtable.com/foo.jpg\"\n\t *     url: string,\n\t *     // filename, e.g. \"foo.jpg\"\n\t *     filename: string,\n\t *     // file size, in bytes\n\t *     size?: number,\n\t *     // content type, e.g. \"image/jpeg\"\n\t *     type?: string,\n\t *     // thumbnails if available\n\t *     thumbnails?: {\n\t *         small?: {\n\t *             url: string,\n\t *             width: number,\n\t *             height: number,\n\t *         },\n\t *         large?: {\n\t *             url: string,\n\t *             width: number,\n\t *             height: number,\n\t *         },\n\t *         full?: {\n\t *             url: string,\n\t *             width: number,\n\t *             height: number,\n\t *         },\n\t *     },\n\t * }>\n\t * ```\n\t *\n\t * **Cell write format**\n\t * ```js\n\t * Array<\n\t *     // New attachment format\n\t *     { url: string, filename?: string} ||\n\t *     // Pre-existing attachments use cell read format specified above\n\t *     { ... }\n\t * >\n\t * ```\n\t * For pre-existing attachments, pass the object read from the cell value.\n\t * You cannot change any properties of pre-existing attachments.\n\t *\n\t * **Field options read format**\n\t * ```js\n\t * {\n\t *     // Whether attachments are rendered in the reverse order from the cell value in the\n\t *     // Airtable UI (i.e. most recent first)\n\t *     // You generally do not need to rely on this option.\n\t *     isReversed: boolean,\n\t * }\n\t * ```\n\t *\n\t * **Field options write format**\n\t *\n\t * N/A\n\t *\n\t * Options are not required when creating a `MULTIPLE_ATTACHMENTS` field, and updating options\n\t * is not supported.\n\t */\n\tMULTIPLE_ATTACHMENTS = 'multipleAttachments',\n\t/**\n\t * A checkbox.\n\t *\n\t * This field is \"true\" when checked and \"null\" when unchecked.\n\t *\n\t * **Cell read format**\n\t * ```js\n\t * true | null\n\t * ```\n\t *\n\t * You can write to the cell with \"false\", but the read value will be still be \"null\"\n\t * (unchecked).\n\t *\n\t * **Cell write format**\n\t * ```js\n\t * boolean | null\n\t * ```\n\t *\n\t * **Field options**\n\t *\n\t * ```js\n\t * {\n\t *     // an icon name\n\t *     icon: 'check' | 'star' | 'heart' | 'thumbsUp' | 'flag',\n\t *     // the color of the check box\n\t *     color: 'yellowBright' | 'orangeBright' | 'redBright' | 'pinkBright' | 'purpleBright' | 'blueBright' | 'cyanBright' | 'tealBright' | 'greenBright' | 'grayBright' ,\n\t * }\n\t * ```\n\t *\n\t * Bases on a free or plus plan are limited to using the 'check' icon and 'greenBright' color.\n\t */\n\tCHECKBOX = 'checkbox',\n\t/**\n\t * Compute a value in each record based on other fields in the same record.\n\t *\n\t * **Cell read format**\n\t *\n\t * Check `options.result` to know the resulting field type.\n\t * ```js\n\t * any\n\t * ```\n\t *\n\t * **Cell write format**\n\t *\n\t * n/a\n\t *\n\t * **Field options read format**\n\t * ```js\n\t * {\n\t *     // false if the formula contains an error\n\t *     isValid: boolean,\n\t *     // the other fields in the record that are used in the formula\n\t *     referencedFieldIds: Array<FieldId>,\n\t *     // the resulting field type and options returned by the formula\n\t *     result: {\n\t *         // the field type of the formula result\n\t *         type: string,\n\t *         // that types options\n\t *         options?: any,\n\t *     },\n\t * }\n\t * ```\n\t *\n\t * **Field options write format**\n\t *\n\t * Creating or updating `FORMULA` fields is not supported.\n\t */\n\tFORMULA = 'formula',\n\t/**\n\t * The time the record was created in UTC.\n\t *\n\t * When reading from a \"Created time\" field, the cell value will always be an\n\t * [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) formatted date time.\n\t * (Field options specify how it's displayed in the UI.)\n\t *\n\t * **Cell read format**\n\t * ```js\n\t * string\n\t * ```\n\t *\n\t * **Cell write format**\n\t *\n\t * n/a\n\t *\n\t * **Field options read format**\n\t * ```js\n\t * {\n\t *     result: {\n\t *         type: 'date' | 'dateTime',\n\t *         // See DATE and DATE_TIME for detailed field options\n\t *         options: DateOrDateTimeFieldOptions,\n\t *     },\n\t * }\n\t * ```\n\t *\n\t * **Field options write format**\n\t *\n\t * Creating or updating `CREATED_TIME` fields is not supported.\n\t */\n\tCREATED_TIME = 'createdTime',\n\t/**\n\t * A rollup allows you to summarize data from records that are linked to this table.\n\t *\n\t * **Cell read format**\n\t * Check `options.result` to know the resulting field type.\n\t * ```js\n\t * any\n\t * ```\n\t *\n\t * **Cell write format**\n\t *\n\t * n/a\n\t *\n\t * **Field options read format**\n\t * ```js\n\t * {\n\t *     // false if the formula contains an error\n\t *     isValid: boolean,\n\t *     // the linked record field in this table that this field is\n\t *     // summarizing.\n\t *     recordLinkFieldId: FieldId,\n\t *     // the field id in the linked table that this field is summarizing.\n\t *     fieldIdInLinkedTable: FieldId,\n\t *     // the other fields in the record that are used in the formula\n\t *     referencedFieldIds: Array<FieldId>,\n\t *     // the resulting field type and options returned by the formula\n\t *     result: {\n\t *         // the field type of the formula result\n\t *         type: string,\n\t *         // that types options\n\t *         options?: any,\n\t *     },\n\t * }\n\t * ```\n\t *\n\t * **Field options write format**\n\t *\n\t * Creating or updating `ROLLUP` fields is not supported.\n\t */\n\tROLLUP = 'rollup',\n\t/**\n\t * Count the number of linked records.\n\t *\n\t * **Cell read format**\n\t * ```js\n\t * number\n\t * ```\n\t *\n\t * **Cell write format**\n\t *\n\t * n/a\n\t *\n\t * **Field options read format**\n\t * ```js\n\t * {\n\t *    // is the field currently valid (e.g. false if the linked record\n\t *    // field has been changed to a different field type)\n\t *    isValid: boolean,\n\t *    // the linked record field in this table that we're counting\n\t *    recordLinkFieldId: FieldId,\n\t * }\n\t * ```\n\t *\n\t * **Field options write format**\n\t *\n\t * Creating or updating `COUNT` fields is not supported.\n\t */\n\tCOUNT = 'count',\n\t/**\n\t * Lookup a field on linked records.\n\t *\n\t * **Cell read format**\n\t * ```js\n\t * Array<{\n\t *     // the ID of the linked record this lookup value comes from\n\t *     linkedRecordId: RecordId,\n\t *     // the cell value of the lookup. the actual type depends on the field being looked up\n\t *     value: unknown,\n\t * }>\n\t * ```\n\t *\n\t * **Cell write format**\n\t *\n\t * n/a\n\t *\n\t * **Field options read format**\n\t * ```js\n\t * {\n\t *     // whether the lookup field is correctly configured\n\t *     isValid: boolean,\n\t *     // the linked record field in this table that this field is\n\t *     // looking up\n\t *     recordLinkFieldId: FieldId,\n\t *     // the field in the foreign table that will be looked up on\n\t *     // each linked record\n\t *     fieldIdInLinkedTable: FieldId | null,\n\t *     // the local field configuration for the foreign field being\n\t *     // looked up\n\t *     result?: undefined | {type: FieldType, options: unknown}\n\t * }\n\t * ```\n\t *\n\t * **Field options write format**\n\t *\n\t * Creating or updating `MULTIPLE_LOOKUP_VALUES` fields is not supported.\n\t */\n\tMULTIPLE_LOOKUP_VALUES = 'multipleLookupValues',\n\t/**\n\t * Automatically incremented unique counter for each record.\n\t *\n\t * **Cell read format**\n\t * ```js\n\t * number\n\t * ```\n\t *\n\t * **Cell write format**\n\t *\n\t * n/a\n\t *\n\t * **Field options read format**\n\t *\n\t * n/a\n\t *\n\t * **Field options write format**\n\t *\n\t * Creating or updating `AUTO_NUMBER` fields is not supported.\n\t */\n\tAUTO_NUMBER = 'autoNumber',\n\t/**\n\t * Use the Airtable iOS or Android app to scan barcodes.\n\t *\n\t * **Cell read format**\n\t * ```js\n\t * {\n\t *     // the text value of the barcode\n\t *     text: string,\n\t *     // the type of barcode\n\t *     type?: string,\n\t * }\n\t * ```\n\t *\n\t * **Cell write format**\n\t *\n\t * n/a\n\t *\n\t * **Field options**\n\t *\n\t * n/a\n\t */\n\tBARCODE = 'barcode',\n\t/**\n\t * A rating (e.g. stars out of 5)\n\t *\n\t * **Cell format**\n\t * ```js\n\t * number\n\t * ```\n\t *\n\t * **Field options**\n\t * ```js\n\t * {\n\t *     // the icon name used to display the rating\n\t *     icon: 'star' | 'heart' | 'thumbsUp' | 'flag',\n\t *     // the maximum value for the rating, from 1 to 10 inclusive\n\t *     max: number,\n\t *     // the color of selected icons\n\t *     color: 'yellowBright' | 'orangeBright' | 'redBright' | 'pinkBright' | 'purpleBright' | 'blueBright' | 'cyanBright' | 'tealBright' | 'greenBright' | 'grayBright' ,\n\t * }\n\t * ```\n\t *\n\t * Bases on a free or plus plan are limited to using the 'star' icon and 'yellowBright' color.\n\t */\n\tRATING = 'rating',\n\t/**\n\t * A long text field with rich formatting enabled.\n\t *\n\t * Returned string is formatted with [markdown syntax for Airtable rich text formatting](https://support.airtable.com/hc/en-us/articles/360044741993-Markdown-syntax-for-Airtable-rich-text-formatting).\n\t * Use this formatting when updating cell values.\n\t *\n\t * **Cell format**\n\t * ```js\n\t * string\n\t * ```\n\t * **Field options**\n\t *\n\t * n/a\n\t *\n\t */\n\tRICH_TEXT = 'richText',\n\t/**\n\t * A duration of time in seconds.\n\t *\n\t * The `durationFormat` string follows the moment.js structure documented\n\t * [here](https://momentjs.com/docs/#/parsing/string-format/).\n\t *\n\t * **Cell format**\n\t * ```js\n\t * number\n\t * ```\n\t *\n\t * **Field options**\n\t * ```js\n\t * {\n\t *     durationFormat: 'h:mm' | 'h:mm:ss' | 'h:mm:ss.S' | 'h:mm:ss.SS' | 'h:mm:ss.SSS',\n\t * }\n\t * ```\n\t */\n\tDURATION = 'duration',\n\t/**\n\t * Shows the date and time that a record was most recently modified in any editable field or\n\t * just in specific editable fields.\n\t *\n\t * When reading from a \"Last modified time\" field, the cell value will always be an\n\t * [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) formatted date time.\n\t * (Field options specify how it's displayed in the UI.)\n\t *\n\t * **Cell read format**\n\t * ```js\n\t * string\n\t * ```\n\t *\n\t * **Cell write format**\n\t *\n\t * n/a\n\t *\n\t * **Field options read format**\n\t * ```js\n\t * {\n\t *     // false if the formula contains an error\n\t *     isValid: boolean,\n\t *     // the fields to check the last modified time of\n\t *     referencedFieldIds: Array<FieldId>,\n\t *     // the cell value result type\n\t *     result: {\n\t *         type: 'date' | 'dateTime',\n\t *         // See DATE and DATE_TIME for detailed field options\n\t *         options: DateOrDateTimeFieldOptions,\n\t *     },\n\t * }\n\t * ```\n\t * **Field options write format**\n\t *\n\t * Creating or updating `LAST_MODIFIED_TIME` fields is not supported.\n\t */\n\tLAST_MODIFIED_TIME = 'lastModifiedTime',\n\t/**\n\t * The collaborator who created a record.\n\t *\n\t * The cell value format is the same as the `SINGLE_COLLABORATOR` field, without the ability to\n\t * write to the cell value.\n\t *\n\t * **Cell read format**\n\t * ```js\n\t * {\n\t *     id: string,\n\t *     email: string,\n\t *     name?: string,\n\t *     profilePicUrl?: string,\n\t * }\n\t * ```\n\t *\n\t * **Cell write format**\n\t *\n\t * n/a\n\t *\n\t * **Field options read format**\n\t * ```js\n\t * {\n\t *     choices: Array<{\n\t *         id: string,\n\t *         email: string,\n\t *         name?: string,\n\t *         profilePicUrl?: string,\n\t *     }>,\n\t * }\n\t * ```\n\t *\n\t * **Field options write format**\n\t *\n\t * Creating or updating `CREATED_BY` fields is not supported.\n\t *\n\t */\n\tCREATED_BY = 'createdBy',\n\t/**\n\t * Shows the last collaborator who most recently modified any editable field or just in specific\n\t * editable fields.\n\t *\n\t * The cell value format is the same as the `SINGLE_COLLABORATOR` field, without the ability to\n\t * write to the cell value.\n\t *\n\t * **Cell read format**\n\t * ```js\n\t * {\n\t *     id: string,\n\t *     email: string,\n\t *     name?: string,\n\t *     profilePicUrl?: string,\n\t * }\n\t * ```\n\t *\n\t * **Cell write format**\n\t *\n\t * n/a\n\t *\n\t * **Field options read format**\n\t * ```js\n\t * {\n\t *     referencedFieldIds: Array<FieldId>,\n\t *     choices: Array<{\n\t *         id: string,\n\t *         email: string,\n\t *         name?: string,\n\t *         profilePicUrl?: string,\n\t *     }>,\n\t * }\n\t * ```\n\t *\n\t * **Field options write format**\n\t *\n\t * Creating or updating `LAST_MODIFIED_BY` fields is not supported.\n\t *\n\t */\n\tLAST_MODIFIED_BY = 'lastModifiedBy',\n\t/**\n\t * A button that can be clicked from the Airtable UI to open a URL or open a block.\n\t *\n\t * You cannot currently programmatically interact with a button field from a block, but you can\n\t * configure your block to perform a certain action when it's opened from a button field: see\n\t * {@link useRecordActionData} for details.\n\t *\n\t * **Cell read format**\n\t * ```js\n\t * {\n\t *     // The label of the button\n\t *     label: string,\n\t *     // URL the button opens, or URL of the block that the button opens.\n\t *     // Null when the URL formula has become invalid.\n\t *     url: string | null,\n\t * }\n\t * ```\n\t *\n\t * **Cell write format**\n\t *\n\t * n/a\n\t *\n\t * **Field options read format**\n\t *\n\t * n/a\n\t *\n\t * **Field options write format**\n\t *\n\t * Creating or updating `BUTTON` fields is not supported.\n\t *\n\t */\n\tBUTTON = 'button',\n\t/**\n\t * Shows the name of the source that a record is synced from. This field is only available on\n\t * synced tables.\n\t *\n\t * **Cell read format**\n\t * ```js\n\t * {\n\t *     id: string,\n\t *     name: string,\n\t *     color?: Color\n\t * }\n\t * ```\n\t *\n\t * **Cell write format**\n\t *\n\t * n/a\n\t *\n\t * **Field options read format**\n\t * ```js\n\t * {\n\t *     choices: Array<{\n\t *         id: string,\n\t *         name: string,\n\t *         color?: {@link Color}, // Color is not provided when field coloring is disabled.\n\t *     }>,\n\t * }\n\t * ```\n\t * Every choice represents a sync source, and choices are added or removed automatically as\n\t * sync sources are added or removed. Choice names and colors are user-configurable.\n\t *\n\t * **Field options write format**\n\t *\n\t * Creating or updating `EXTERNAL_SYNC_SOURCE` fields is not supported.\n\t *\n\t */\n\tEXTERNAL_SYNC_SOURCE = 'externalSyncSource',\n\t/**\n\t *\n\t * Field that contains text generated by AI.\n\t *\n\t * **Cell read format**\n\t *\n\t * ```js\n\t * {\n\t *     state: 'empty' | 'loading' | 'generated' | 'error',\n\t *     value: string,\n\t *     isStale: boolean,\n\t *     // Only populated if state is 'error'\n\t *     errorType?: string,\n\t * }\n\t * ```\n\t *\n\t * **Cell write format**\n\t *\n\t * n/a\n\t *\n\t * **Field options read format**\n\t * ```js\n\t * {\n\t *     prompt?: Array<string | {field: {fieldId: string}}>,\n\t *     referencedFieldIds?: Array<string>,\n\t * }\n\t * ```\n\t * Prompt is an array of strings and field references. All referenced field ids\n\t *\n\t * **Field options write format**\n\t *\n\t * Creating or updating `AI_TEXT` fields is not supported.\n\t *\n\t */\n\tAI_TEXT = 'aiText',\n}\n"
  },
  {
    "path": "src/types/datasources/mssql.types.ts",
    "content": "export enum MSSQLColumnType {\n\tINT = 'int',\n\tTINYINT = 'tinyint',\n\tSMALLINT = 'smallint',\n\tBIGINT = 'bigint',\n\tFLOAT = 'float',\n\tREAL = 'real',\n\tDECIMAL = 'decimal',\n\tNUMERIC = 'numeric',\n\tBIT = 'bit',\n\tSMALLMONEY = 'smallmoney',\n\tMONEY = 'money',\n\tCHAR = 'char',\n\tVARCHAR = 'varchar',\n\tTEXT = 'text',\n\tNTEXT = 'ntext',\n\tNCHAR = 'nchar',\n\tNVARCHAR = 'nvarchar',\n\tBINARY = 'binary',\n\tVARBINARY = 'varbinary',\n\tDATE = 'date',\n\tDATETIME = 'datetime',\n\tDATETIME2 = 'datetime2',\n\tSMALLDATETIME = 'smalldatetime',\n\tTIME = 'time',\n\tDATETIMEOFFSET = 'datetimeoffset',\n\tTIMESTAMP = 'timestamp',\n\tUNIQUEIDENTIFIER = 'uniqueidentifier',\n\tXML = 'xml',\n\tSQL_VARIANT = 'sql_variant',\n\tTABLE = 'table',\n}\n"
  },
  {
    "path": "src/types/datasources/mysql.types.ts",
    "content": "export enum MySQLColumnType {\n\tINT = 'int',\n\tTINYINT = 'tinyint',\n\tSMALLINT = 'smallint',\n\tMEDIUMINT = 'mediumint',\n\tBIGINT = 'bigint',\n\tFLOAT = 'float',\n\tDOUBLE = 'double',\n\tDECIMAL = 'decimal',\n\tNUMERIC = 'numeric',\n\tREAL = 'real',\n\tTIMESTAMP = 'timestamp',\n\tCHAR = 'char',\n\tVARCHAR = 'varchar',\n\tTEXT = 'text',\n\tTINYTEXT = 'tinytext',\n\tMEDIUMTEXT = 'mediumtext',\n\tLONGTEXT = 'longtext',\n\tDATE = 'date',\n\tDATETIME = 'datetime',\n\tTIME = 'time',\n\tYEAR = 'year',\n\tBOOL = 'bool',\n\tBOOLEAN = 'boolean',\n\tENUM = 'enum',\n\tSET = 'set',\n\tBLOB = 'blob',\n\tTINYBLOB = 'tinyblob',\n\tMEDIUMBLOB = 'mediumblob',\n\tLONGBLOB = 'longblob',\n\tBINARY = 'binary',\n\tVARBINARY = 'varbinary',\n\tJSON = 'json',\n}\n"
  },
  {
    "path": "src/types/datasources/postgres.types.ts",
    "content": "export enum PostgreSQLColumnType {\n\tINT = 'INT',\n\tDOUBLE = 'double precision',\n\tNUMERIC = 'numeric',\n\tREAL = 'real',\n\tTIMESTAMP = 'TIMESTAMP',\n\tCHAR = 'character',\n\tVARCHAR = 'VARCHAR',\n\tTEXT = 'text',\n\tDATE = 'date',\n\tDATETIME = 'timestamp',\n\tTIME = 'time',\n\tYEAR = 'integer',\n\tBOOLEAN = 'boolean',\n\tENUM = 'enum',\n\tBINARY = 'bytea',\n\tJSON = 'json',\n}\n"
  },
  {
    "path": "src/types/response.types.ts",
    "content": "export enum Method {\n\tGET = 'GET',\n\tPOST = 'POST',\n\tPUT = 'PUT',\n\tPATCH = 'PATCH',\n\tDELETE = 'DELETE',\n}\n"
  },
  {
    "path": "src/types/roles.types.ts",
    "content": "export enum RolePermission {\n\tNONE = 'NONE',\n\tREAD = 'READ',\n\tWRITE = 'WRITE',\n\tDELETE = 'DELETE',\n}\n\nexport interface TableRole extends Role {\n\town_records: RolePermission\n}\n\nexport interface Role {\n\tid?: number // the id of the role after creation\n\tcustom: boolean\n\trole: string\n\trecords: RolePermission\n}\n\nexport interface DefaultRole extends Role {}\n\nexport interface CustomRole extends Role {\n\ttable: string // the table you want to apply the roles to\n\town_records?: RolePermission\n\tidentity_column?: string // the column in the table which holds the user identity (e.g. user_id), if not provided, we will use the tables primary key\n}\n\nexport interface RoleLocation {\n\ttable: string //the table which holds the users role information\n\tcolumn: string //the column in the table which holds the users role\n\tidentifier_column?: string // the column in the table which holds the user identity (e.g. user_id), if not provided, we will use the tables primary key\n}\n\nexport interface RolesConfig {\n\tlocation: RoleLocation\n}\n"
  },
  {
    "path": "src/types/schema.types.ts",
    "content": "import { DataSourceRelations, DataSourceWhere } from './datasource.types'\n\nexport interface ValidateFieldsResponse extends ValidateResponse {\n\tfields?: string[]\n\trelations?: DataSourceRelations[]\n}\n\nexport interface validateRelationsResponse extends ValidateResponse {\n\trelations?: DataSourceRelations[]\n}\n\nexport interface validateWhereResponse extends ValidateResponse {\n\twhere?: DataSourceWhere[]\n}\n\nexport interface ValidateSortResponse extends ValidateResponse {\n\tsort?: SortCondition[]\n}\n\nexport interface ValidateResponse {\n\tvalid: boolean\n\tmessage?: string\n}\n\nexport interface SortCondition {\n\tcolumn: string\n\toperator: 'ASC' | 'DESC'\n}\n"
  },
  {
    "path": "src/utils/Env.ts",
    "content": "import * as fs from 'fs'\nimport * as os from 'os'\nimport * as path from 'path'\n\nimport { Environment, fromStringToEnv } from './Env.types'\n\nexport class Env {\n\t/**\n\t * Get an enum key based on it's value\n\t */\n\n\tstatic get(): Environment {\n\t\treturn fromStringToEnv()\n\t}\n\n\t/**\n\t * Checks if it's a production environment\n\t */\n\n\tstatic IsProd(): boolean {\n\t\treturn this.get() === Environment.production\n\t}\n\n\t/**\n\t * Checks if it's a development environment\n\t */\n\n\tstatic IsDev(): boolean {\n\t\treturn this.get() === Environment.development\n\t}\n\n\t/**\n\t * Checks if it's a test environment\n\t */\n\n\tstatic IsTest(): boolean {\n\t\treturn this.get() === Environment.test\n\t}\n\n\t/**\n\t * Checks if it's NOT a test environment\n\t */\n\n\tstatic IsNotTest(): boolean {\n\t\treturn this.get() !== Environment.test\n\t}\n\n\t/**\n\t * Checks if it's NOT a prod environment\n\t */\n\n\tstatic IsNotProd(): boolean {\n\t\treturn this.get() !== Environment.production\n\t}\n\n\t/**\n\t * Checks if it's a sandbox environment\n\t */\n\n\tstatic IsSandbox(): boolean {\n\t\treturn this.get() === Environment.sandbox\n\t}\n\n\t//todo allow a .env value to override this if exists\n\tstatic useCache(): boolean {\n\t\treturn this.IsNotTest()\n\t}\n\n\t/**\n\t * Reads the .env file and returns an array of lines\n\t */\n\tstatic readEnvVars(options: { envPath: string; fileName: string }) {\n\t\tif (!options.envPath) options.envPath = './'\n\t\tif (!options.fileName) options.fileName = '.env'\n\n\t\treturn fs.readFileSync(path.resolve(options.envPath, options.fileName), 'utf-8').split(os.EOL)\n\t}\n\n\t/**\n\t * Finds the key in .env files and returns the corresponding value\n\t */\n\tstatic getEnvValue(options: { key: string; envPath: string; fileName: string }): string {\n\t\tif (!options.envPath) options.envPath = './'\n\t\tif (!options.fileName) options.fileName = '.env'\n\n\t\tconst matchedLine = Env.readEnvVars({ envPath: options.envPath, fileName: options.fileName }).find(\n\t\t\tline => line.split('=')[0] === options.key,\n\t\t)\n\t\tconst result = matchedLine !== undefined ? matchedLine.split('=')[1] : null\n\t\treturn result !== null ? result.replace(/\"/g, '') : ''\n\t}\n\n\t/**\n\t * Updates value for existing key or creates a new key=value line\n\t * This function is a modified version of https://stackoverflow.com/a/65001580/3153583\n\t */\n\tstatic setEnvValue(options: { key: string; value: string; envPath?: string; fileName?: string }) {\n\t\tif (!options.envPath) options.envPath = './'\n\t\tif (!options.fileName) options.fileName = '.env'\n\n\t\tconst envVars = Env.readEnvVars({ envPath: options.envPath, fileName: options.fileName })\n\t\tconst targetLine = envVars.find(line => line.split('=')[0] === options.key)\n\t\tif (targetLine !== undefined) {\n\t\t\tconst targetLineIndex = envVars.indexOf(targetLine)\n\t\t\tenvVars.splice(targetLineIndex, 1, `${options.key}=\"${options.value}\"`)\n\t\t} else {\n\t\t\t// create new key value\n\t\t\tenvVars.push(`${options.key}=\"${options.value}\"`)\n\t\t}\n\t\t// write everything back to the file system\n\t\tfs.writeFileSync(path.resolve(options.envPath, options.fileName), envVars.join(os.EOL))\n\t}\n\n\tstatic setEnv(options: { values: { [key: string]: string }; envPath?: string; fileName?: string }) {\n\t\tfor (const key of Object.keys(options.values)) {\n\t\t\tEnv.setEnvValue({\n\t\t\t\tkey: key,\n\t\t\t\tvalue: options.values[key],\n\t\t\t\tenvPath: options.envPath,\n\t\t\t\tfileName: options.fileName,\n\t\t\t})\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/utils/Env.types.ts",
    "content": "export enum Environment {\n\tproduction = 'production',\n\tsandbox = 'sandbox',\n\tdevelopment = 'development',\n\ttest = 'test',\n}\n\nexport function fromStringToEnv(env = process.env.NODE_ENV): Environment {\n\tswitch (env) {\n\t\tcase 'production':\n\t\t\treturn Environment.production\n\t\tcase 'sandbox':\n\t\t\treturn Environment.sandbox\n\t\tcase 'development':\n\t\t\treturn Environment.development\n\t\tcase 'test':\n\t\t\treturn Environment.test\n\t\tdefault:\n\t\t\treturn Environment.production\n\t}\n}\n"
  },
  {
    "path": "src/utils/Find.ts",
    "content": "/**\n * Find an element in an object with a . notation\n */\n\nexport function findDotNotation(obj: any, search: string): boolean {\n\treturn search.split('.').reduce((o, i) => o[i], obj)\n}\n"
  },
  {
    "path": "src/utils/String.ts",
    "content": "/**\n * Replace ? symbols with the values from a any[]\n */\n\nimport { CronExpression } from '@nestjs/schedule'\n\nexport function replaceQ(string: string, array: any[]): string {\n\t//if(!array.length) return string\n\t//return string.replace(/\\?/g, () => array.shift() || '')\n\tlet i = 0\n\treturn string.replace(/\\?/g, function () {\n\t\tconst value = array[i++]\n\t\tif (typeof value === 'string') {\n\t\t\treturn `'${value.replace(/'/g, \"''\")}'`\n\t\t}\n\t\tif (value === null) {\n\t\t\treturn 'NULL'\n\t\t}\n\t\treturn value\n\t})\n\t//return string\n}\n\n/**\n * Returns the plural of an English word.\n *\n * @export\n * @param {string} word\n * @param {number} [amount]\n * @returns {string}\n */\nexport function plural(word: string, amount?: number): string {\n\tif (amount !== undefined && amount === 1) {\n\t\treturn word\n\t}\n\tconst plural: { [key: string]: string } = {\n\t\t'(quiz)$': '$1zes',\n\t\t'^(ox)$': '$1en',\n\t\t'([m|l])ouse$': '$1ice',\n\t\t'(matr|vert|ind)ix|ex$': '$1ices',\n\t\t'(x|ch|ss|sh)$': '$1es',\n\t\t'([^aeiouy]|qu)y$': '$1ies',\n\t\t'(hive)$': '$1s',\n\t\t'(?:([^f])fe|([lr])f)$': '$1$2ves',\n\t\t'(shea|lea|loa|thie)f$': '$1ves',\n\t\tsis$: 'ses',\n\t\t'([ti])um$': '$1a',\n\t\t'(tomat|potat|ech|her|vet)o$': '$1oes',\n\t\t'(bu)s$': '$1ses',\n\t\t'(alias)$': '$1es',\n\t\t'(octop)us$': '$1i',\n\t\t'(ax|test)is$': '$1es',\n\t\t'(us)$': '$1es',\n\t\t'([^s]+)$': '$1s',\n\t}\n\tconst irregular: { [key: string]: string } = {\n\t\tmove: 'moves',\n\t\tfoot: 'feet',\n\t\tgoose: 'geese',\n\t\tsex: 'sexes',\n\t\tchild: 'children',\n\t\tman: 'men',\n\t\ttooth: 'teeth',\n\t\tperson: 'people',\n\t}\n\tconst uncountable: string[] = [\n\t\t'sheep',\n\t\t'fish',\n\t\t'deer',\n\t\t'moose',\n\t\t'series',\n\t\t'species',\n\t\t'money',\n\t\t'rice',\n\t\t'information',\n\t\t'equipment',\n\t\t'bison',\n\t\t'cod',\n\t\t'offspring',\n\t\t'pike',\n\t\t'salmon',\n\t\t'shrimp',\n\t\t'swine',\n\t\t'trout',\n\t\t'aircraft',\n\t\t'hovercraft',\n\t\t'spacecraft',\n\t\t'sugar',\n\t\t'tuna',\n\t\t'you',\n\t\t'wood',\n\t]\n\t// save some time in the case that singular and plural are the same\n\tif (uncountable.indexOf(word.toLowerCase()) >= 0) {\n\t\treturn word\n\t}\n\t// check for irregular forms\n\tfor (const w in irregular) {\n\t\tconst pattern = new RegExp(`${w}$`, 'i')\n\t\tconst replace = irregular[w]\n\t\tif (pattern.test(word)) {\n\t\t\treturn word.replace(pattern, replace)\n\t\t}\n\t}\n\t// check for matches using regular expressions\n\tfor (const reg in plural) {\n\t\tconst pattern = new RegExp(reg, 'i')\n\t\tif (pattern.test(word)) {\n\t\t\treturn word.replace(pattern, plural[reg])\n\t\t}\n\t}\n\treturn word\n}\n\n/**\n * Convert a comma separated string to an array\n */\n\nexport function commaStringToArray(string: string): string[] {\n\tif (!string) {\n\t\treturn []\n\t}\n\n\treturn string.split(',').map(field => field.trim())\n}\n\n/**\n * Convert a CronExpression to seconds\n */\n\nexport function cronToSeconds(cron: CronExpression): number {\n\tswitch (cron) {\n\t\tcase CronExpression.EVERY_10_SECONDS:\n\t\t\treturn 10\n\t\tcase CronExpression.EVERY_30_SECONDS:\n\t\t\treturn 30\n\t\tcase CronExpression.EVERY_MINUTE:\n\t\t\treturn 60\n\t\tcase CronExpression.EVERY_5_MINUTES:\n\t\t\treturn 300\n\t\tcase CronExpression.EVERY_HOUR:\n\t\t\treturn 3600\n\t\tcase CronExpression.EVERY_2ND_HOUR:\n\t\t\treturn 7200\n\t\tcase CronExpression.EVERY_DAY_AT_MIDNIGHT:\n\t\t\treturn 86400\n\t\tcase CronExpression.EVERY_WEEK:\n\t\t\treturn 604800\n\t\tcase CronExpression.EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT:\n\t\tcase CronExpression.EVERY_1ST_DAY_OF_MONTH_AT_NOON:\n\t\t\treturn 2592000\n\t\tcase CronExpression.EVERY_2ND_MONTH:\n\t\tcase CronExpression.EVERY_QUARTER:\n\t\t\treturn 7776000\n\t\tcase CronExpression.EVERY_6_MONTHS:\n\t\t\treturn 15552000\n\t\tcase CronExpression.EVERY_YEAR:\n\t\t\treturn 31536000\n\t\tdefault:\n\t\t\tthrow new Error(`Unknown CronExpression: ${cron}`)\n\t}\n}\n"
  },
  {
    "path": "src/utils/redoc/interfaces/redoc.interface.ts",
    "content": "export interface RedocOptions {\n\t/** Location of the OpenApi json file */\n\tdocUrl: string\n\t/** Web site title (e.g: ReDoc documentation) */\n\ttitle?: string\n\t/** Web site favicon URL */\n\tfavicon?: string\n\t/** Logo Options */\n\tlogo?: LogoOptions\n\t/** Theme options */\n\ttheme?: any\n\t/** If set, the spec is considered untrusted and all HTML/markdown is sanitized to prevent XSS, by default is false */\n\tuntrustedSpec?: boolean\n\t/** If set, warnings are not rendered at the top of documentation (they are still logged to the console) */\n\tsupressWarnings?: boolean\n\t/** If set, the protocol and hostname won't be shown in the operation definition */\n\thideHostname?: boolean\n\t/** Specify which responses to expand by default by response codes,\n\t * values should be passed as comma-separated list without spaces\n\t * (e.g: 200, 201, \"all\")\n\t */\n\texpandResponses?: string\n\t/** If set, show required properties first ordered in the same order as in required array */\n\trequiredPropsFirst?: boolean\n\t/** If set, propeties will be sorted alphabetically */\n\tsortPropsAlphabetically?: boolean\n\t/** If set the fields starting with \"x-\" will be shown, can be a boolean or a string with names of extensions to display */\n\tshowExtensions?: boolean | string\n\t/** If set, redoc won't inject authentication section automatically */\n\tnoAutoAuth?: boolean\n\t/** If set, path link and HTTP verb will be shown in the middle panel instead of the right one */\n\tpathInMiddlePanel?: boolean\n\t/** If set, loading spinner animation won't be shown */\n\thideLoading?: boolean\n\t/** If set, a native scrollbar will be used instead of perfect-scroll, this can improve performance of the frontend for big specs */\n\tnativeScrollbars?: boolean\n\t/** This will hide the \"Download spec\" button, it only hides the button */\n\thideDownloadButton?: boolean\n\t/** If set, the search bar will be disabled */\n\tdisableSearch?: boolean\n\t/** Shows only required fileds in request samples */\n\tonlyRequiredInSamples?: boolean\n\t/** Name of the swagger json spec file */\n\tdocName?: string\n\t/** Authentication options */\n\tauth?: {\n\t\t// Default value is false\n\t\tenabled: boolean\n\t\t// If auth is enabled but no user is provided the default value is \"admin\"\n\t\tuser: string\n\t\t// If auth is enabled but no password is provided the default value is \"123\"\n\t\tpassword: string\n\t}\n\n\t/** Vendor extensions */\n\n\t/** If set, group tags in categories in the side menu. Tags not added to a group will not be displayed. */\n\ttagGroups?: TagGroupOptions[]\n}\n\nexport interface LogoOptions {\n\t/** The URL pointing to the spec logo, must be in the format of a URL and an absolute URL */\n\turl?: string\n\t/** Background color to be used, must be RGB color in hexadecimal format (e.g: #008080) */\n\tbackgroundColor?: string\n\t/** Alt tag for logo */\n\taltText?: string\n\t/** href tag for logo, it defaults to the one used in your API spec */\n\thref?: string\n}\n\nexport interface TagGroupOptions {\n\tname: string\n\ttags: string[]\n}\n"
  },
  {
    "path": "src/utils/redoc/redoc.ts",
    "content": "import * as handlebars from 'express-handlebars'\nimport { join } from 'path'\n\nimport { RedocOptions } from './interfaces/redoc.interface'\n\nexport class RedocModule {\n\t/**\n\t * Setup ReDoc frontend\n\t */\n\tpublic static async setup(options: RedocOptions): Promise<string> {\n\t\ttry {\n\t\t\tconst hbs = handlebars.create({\n\t\t\t\thelpers: {\n\t\t\t\t\ttoJSON: function (object: any) {\n\t\t\t\t\t\treturn JSON.stringify(object)\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})\n\t\t\t// spread redoc options\n\t\t\tconst { title, favicon, theme, docUrl, ...otherOptions } = options\n\t\t\t// create render object\n\t\t\tconst renderData = {\n\t\t\t\tdata: {\n\t\t\t\t\ttitle,\n\t\t\t\t\tdocUrl,\n\t\t\t\t\tfavicon,\n\t\t\t\t\toptions: otherOptions,\n\t\t\t\t\t...(theme && {\n\t\t\t\t\t\ttheme: {\n\t\t\t\t\t\t\t...theme,\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t}\n\n\t\t\t// this is our handlebars file path\n\t\t\tconst redocFilePath = join(__dirname, 'views', 'redoc.handlebars')\n\n\t\t\t// get handlebars rendered HTML\n\t\t\tconst redocHTML = await hbs.render(redocFilePath, renderData)\n\n\t\t\treturn redocHTML\n\t\t} catch (e) {\n\t\t\tconst error = e as Error\n\t\t\tthrow error\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/utils/redoc/views/redoc.handlebars",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <title>{{ data.title }}</title>\n  <meta charset=\"utf-8\" />\n  {{#if data.favicon}}\n  <link rel=\"shortcut icon\" type=\"image/x-icon\" href=\"{{ data.favicon }}\" />\n  {{/if}}\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <link href=\"https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700\" rel=\"stylesheet\">\n  <style>\n    body {\n      margin: 0;\n      padding: 0;\n    }\n  </style>\n</head>\n\n<body>\n    <!--\n    API Wrapper by Llana.io\n    ==========================================================\n    Llana is a no-code API wrapper that exposes a REST API for any database within minutes. \n    Stop wasting time building endpoints, just connect your database and start playing.\n    Open source, free to use, and no vendor lock-in.\n    -->\n    \n  <div id=\"redoc_container\"></div>\n \n  <script src=\"https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js\"> </script>\n  <script>\n    let themeJSON = '{{{ toJSON data.theme }}}';\n    if (themeJSON === '') { themeJSON = undefined }\n    Redoc.init(\n      '{{ data.docUrl }}',\n      {\n        ...(themeJSON && {\n          theme: {\n            ...JSON.parse(themeJSON)\n          }\n        }),\n        ...JSON.parse('{{{ toJSON data.options }}}')\n      },\n      document.getElementById(\"redoc_container\")\n    );\n  </script>\n\n  <div>\n    <footer>\n      Generated by <a href=\"https://llana.io\">Llana</a>\n    </footer>\n  </div>\n</body>\n\n</html>\n"
  },
  {
    "path": "tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"exclude\": [\"node_modules\", \"dist\", \"test\", \"**/*spec.ts\"],\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"declaration\": true,\n    \"removeComments\": true,\n    \"emitDecoratorMetadata\": true,\n    \"experimentalDecorators\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"resolveJsonModule\": true,\n    \"target\": \"ES2021\",\n    \"sourceMap\": true,\n    \"outDir\": \"./dist\",\n    \"baseUrl\": \"./\",\n    \"paths\": {\n      \"src/*\": [\"./src/*\"]\n    },\n    \"incremental\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"src/**/*.ts\", \"src/**/*.handlebars\"],\n}\n"
  },
  {
    "path": "views/welcome.hbs",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Welcome :: Llana</title>\n    <script src=\"https://cdn.tailwindcss.com\"></script>\n  </head>\n  <body>\n  <div class=\"bg-gray-50 py-10 sm:py-16\">\n  <div class=\"mx-auto max-w-2xl px-6 lg:max-w-7xl lg:px-8\">\n    <h1 class=\"mx-auto mb-2 max-w-lg text-balance text-center text-4xl font-semibold tracking-tight text-gray-950 sm:text-5xl\">Welcome to Llana 🦙</h1>\n    <p class=\"text-center text-base/7 mt-5\">To get started, <a href=\"https://llana.io/configuration\" target=\"_blank\" class=\"underline\">add your data source connection</a> string.</p>\n    <p class=\"text-center text-base/7 mt-5\">🤔  <a href=\"https://github.com/juicyllama/llana/discussions\" target=\"_blank\" class=\"underline\">Ask Questions Here</a>. &nbsp;&nbsp;&nbsp;&nbsp; 🐛 <a href=\"https://github.com/juicyllama/llana/issues\" target=\"_blank\" class=\"underline\">Submit Bugs Here</a>.</p>\n    <div class=\"mt-5 grid gap-4 sm:mt-10 lg:grid-cols-3 lg:grid-rows-2\">\n      <div class=\"relative lg:row-span-2\">\n        <div class=\"absolute inset-px rounded-lg bg-white lg:rounded-l-[2rem]\"></div>\n        <div class=\"relative flex h-full flex-col overflow-hidden rounded-[calc(theme(borderRadius.lg)+1px)] lg:rounded-l-[calc(2rem+1px)]\">\n          <div class=\"px-8 pb-3 pt-8 sm:px-10 sm:pb-0 sm:pt-10\">\n            <p class=\"mt-2 text-lg font-medium tracking-tight text-gray-950 max-lg:text-center\">Latest News</p>\n            <p class=\"mt-2 max-w-lg text-sm/6 text-gray-600 max-lg:text-center\">Learn more about what our development studio <a href=\"https://juicyllama.com\" target=\"_blank\" class=\"underline\">JuicyLlama</a> has been working on.</p>\n          </div>\n          <div class=\"relative min-h-[30rem] w-full grow [container-type:inline-size] max-lg:mx-auto max-lg:max-w-sm\">\n            <div class=\"absolute inset-x-10 bottom-0 top-10 overflow-hidden rounded-t-[12cqw] border-x-[3cqw] border-t-[3cqw] border-gray-700 bg-gray-900 shadow-2xl\">\n           <iframe src='https://widgets.sociablekit.com/linkedin-page-posts/iframe/25493913' frameborder='0' width='100%' height='100%'></iframe>\n            </div>\n          </div>\n        </div>\n        <div class=\"pointer-events-none absolute inset-px rounded-lg shadow ring-1 ring-black/5 lg:rounded-l-[2rem]\"></div>\n      </div>\n      <div class=\"relative max-lg:row-start-1\">\n        <div class=\"absolute inset-px rounded-lg bg-white max-lg:rounded-t-[2rem]\"></div>\n        <div class=\"relative flex h-full flex-col overflow-hidden rounded-[calc(theme(borderRadius.lg)+1px)] max-lg:rounded-t-[calc(2rem+1px)]\">\n          <div class=\"px-8 pt-8 sm:px-10 sm:pt-10\">\n            <p class=\"mt-2 text-lg font-medium tracking-tight text-gray-950 max-lg:text-center\">Getting Started</p>\n            <p class=\"mt-2 max-w-lg text-sm/6 text-gray-600 max-lg:text-center\">Visit the <a href=\"https://llana.io/\" target=\"_blank\" class=\"underline\">documentation</a> to configure your new Llana instance.</p>\n         \n            <p class=\"mt-4 max-w-lg text-sm/6 text-gray-600 max-lg:text-center\">Helpful Links:</p>\n            <ul>\n              <li>• <a href=\"https://llana.io/configuration\" target=\"_blank\" class=\"underline max-w-lg text-sm/6 text-gray-600 max-lg:text-center\">Configuration</a></li>\n              <li>• <a href=\"https://llana.io/data-sources/overview\" target=\"_blank\" class=\"underline max-w-lg text-sm/6 text-gray-600 max-lg:text-center\">Supported Data Sources</a></li>\n              <li>• <a href=\"https://llana.io/integrations/overview\" target=\"_blank\" class=\"underline max-w-lg text-sm/6 text-gray-600 max-lg:text-center\">Integration Examples</a></li>\n              <li>• <a href=\"https://llana.io/requests\" target=\"_blank\" class=\"underline max-w-lg text-sm/6 text-gray-600 max-lg:text-center\">Building Requests</a></li>\n          </div>\n        </div>\n        <div class=\"pointer-events-none absolute inset-px rounded-lg shadow ring-1 ring-black/5 max-lg:rounded-t-[2rem]\"></div>\n      </div>\n      <div class=\"relative max-lg:row-start-3 lg:col-start-2 lg:row-start-2\">\n        <div class=\"absolute inset-px rounded-lg bg-white\"></div>\n        <div class=\"relative flex h-full flex-col overflow-hidden rounded-[calc(theme(borderRadius.lg)+1px)]\">\n          <div class=\"px-8 pt-8 sm:px-10 sm:pt-10\">\n            <p class=\"mt-2 text-lg font-medium tracking-tight text-gray-950 max-lg:text-center\">Admin Portal <span class=\"inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-600/10\">In Development</span></p>\n            <p class=\"mt-2 max-w-lg text-sm/6 text-gray-600 max-lg:text-center\">Llana now ships with an <a href=\"https://github.com/juicyllama/llana-portal\" target=\"_blank\" class=\"underline\">admin portal</a> to help configure your instance. Access this via port <span class=\"font-semibold\">55262</span>.</p>\n          </div>\n          <div class=\"flex flex-1 items-center [container-type:inline-size] max-lg:py-6 lg:pb-2\">\n            <img class=\"h-[min(152px,40cqw)] object-cover\" src=\"https://tailwindui.com/plus/img/component-images/bento-03-security.png\" alt=\"\">\n          </div>\n        </div>\n        <div class=\"pointer-events-none absolute inset-px rounded-lg shadow ring-1 ring-black/5\"></div>\n      </div>\n      <div class=\"relative lg:row-span-2\">\n        <div class=\"absolute inset-px rounded-lg bg-white max-lg:rounded-b-[2rem] lg:rounded-r-[2rem]\"></div>\n        <div class=\"relative flex h-full flex-col overflow-hidden rounded-[calc(theme(borderRadius.lg)+1px)] max-lg:rounded-b-[calc(2rem+1px)] lg:rounded-r-[calc(2rem+1px)]\">\n          <div class=\"px-8 pb-3 pt-8 sm:px-10 sm:pb-0 sm:pt-10\">\n            <p class=\"mt-2 text-lg font-medium tracking-tight text-gray-950 max-lg:text-center\">Open Source</p>\n            <p class=\"mt-2 max-w-lg text-sm/6 text-gray-600 max-lg:text-center\">Please consider ⭐ starring and contributing to the <a href=\"https://github.com/juicyllama/llana\" target=\"_blank\" class=\"underline\">Llana Github Repo</a> and support this open source project.</p>\n          </div>\n          <div class=\"relative min-h-[30rem] w-full grow\">\n            <div class=\"absolute bottom-0 left-10 right-0 top-10 overflow-hidden rounded-tl-xl bg-gray-900 shadow-2xl\">\n              <div class=\"flex bg-gray-800/40 ring-1 ring-white/5\">\n                <div class=\"-mb-px flex text-sm/6 font-medium text-gray-400\">\n                  <div class=\"border-b border-r border-b-white/20 border-r-white/10 bg-white/5 px-4 py-2 text-white\">welcome.module.ts</div>\n                  <div class=\"border-r border-gray-600/10 px-4 py-2\">welcome.controller.ts</div>\n                </div>\n              </div>\n              <div class=\"pb-14 pt-6\">\n\n<code\n    class=\"text-xs inline-flex text-left items-center text-white rounded-lg pl-6\">\n    <span class=\"flex gap-4\">\n        <span class=\"shrink-0 text-gray-500\">\n            $\n        </span>\n\n        <span class=\"flex-1\">\n            <span>\n                import {   <span class=\"text-yellow-500\">Module</span> } from '@nestjs/common'<br />\n                import { <span class=\"text-yellow-500\">WelcomeController</span> } from './welcome.controller'\n                <br /><br />\n                <span class=\"text-yellow-500\">@Module</span>({<br />\n                  &nbsp;&nbsp; controllers: [ <span class=\"text-yellow-500\">WelcomeController</span> ],<br />\n                })\n                <br /><br />\n                export class <span class=\"text-yellow-500\">WelcomeModule</span> {}\n\n            </span>\n        </span>\n    </span>\n\n</code>\n\n\n               \n                \n\n                \n               \n              </div>\n            </div>\n          </div>\n        </div>\n        <div class=\"pointer-events-none absolute inset-px rounded-lg shadow ring-1 ring-black/5 max-lg:rounded-b-[2rem] lg:rounded-r-[2rem]\"></div>\n      </div>\n    </div>\n  </div>\n</div>\n\n   \n  </body>\n</html>"
  }
]