├── .gitignore
├── .java-version
├── .scalafix.conf
├── .scalariform.conf
├── Jenkinsfile
├── app
├── DatabaseFlow.scala
├── assets
│ └── stylesheets
│ │ ├── _components.less
│ │ ├── _editor.less
│ │ ├── _font.less
│ │ ├── _fontmono.less
│ │ ├── _menu.less
│ │ ├── _query.less
│ │ ├── _queryplan.less
│ │ ├── _queryplannode.less
│ │ ├── _results.less
│ │ ├── _savequery.less
│ │ ├── _static.less
│ │ ├── _tabs.less
│ │ ├── databaseflow.less
│ │ ├── graphiql.less
│ │ └── mermaid.less
├── controllers
│ ├── BaseController.scala
│ ├── HomeController.scala
│ ├── MessagesController.scala
│ ├── admin
│ │ ├── ActivityController.scala
│ │ ├── AdminController.scala
│ │ ├── ResultCacheController.scala
│ │ ├── SandboxController.scala
│ │ ├── SettingsController.scala
│ │ ├── UserCreateController.scala
│ │ └── UserEditController.scala
│ ├── auth
│ │ ├── AuthenticationController.scala
│ │ └── RegistrationController.scala
│ ├── connection
│ │ ├── ConnectionSettingsController.scala
│ │ └── ConnectionTestController.scala
│ ├── graphql
│ │ ├── GraphQLController.scala
│ │ └── SchemaController.scala
│ ├── query
│ │ ├── ExportController.scala
│ │ ├── QueryController.scala
│ │ └── SharedResultController.scala
│ ├── schema
│ │ └── SchemaController.scala
│ └── user
│ │ ├── ProfileController.scala
│ │ └── UserActivityController.scala
├── models
│ ├── InternalMessages.scala
│ ├── auth
│ │ ├── AuthEnv.scala
│ │ └── AuthModule.scala
│ ├── connection
│ │ ├── ConnectionGraphQL.scala
│ │ └── ConnectionSettings.scala
│ ├── database
│ │ ├── Conversions.scala
│ │ ├── PoolSettings.scala
│ │ ├── Query.scala
│ │ ├── Queryable.scala
│ │ ├── Row.scala
│ │ ├── Statement.scala
│ │ └── Transaction.scala
│ ├── ddl
│ │ ├── CreateAuditRecordTable.scala
│ │ ├── CreateConnectionsTable.scala
│ │ ├── CreateGraphQLTable.scala
│ │ ├── CreatePasswordInfoTable.scala
│ │ ├── CreateQueryResultsTable.scala
│ │ ├── CreateSavedQueriesTable.scala
│ │ ├── CreateSettingsTable.scala
│ │ ├── CreateSharedResultTable.scala
│ │ ├── CreateTableStatement.scala
│ │ ├── CreateUsersTable.scala
│ │ └── DdlQueries.scala
│ ├── forms
│ │ ├── ConnectionForm.scala
│ │ └── GraphQLForm.scala
│ ├── graphql
│ │ ├── ArrayGraphQL.scala
│ │ ├── ColumnGraphQL.scala
│ │ ├── ColumnNotNullGraphQL.scala
│ │ ├── ColumnNullableGraphQL.scala
│ │ ├── CommonGraphQL.scala
│ │ ├── ConnectionGraphQLSchema.scala
│ │ ├── DateTimeSchema.scala
│ │ ├── ForeignKeyGraphQL.scala
│ │ └── GraphQLContext.scala
│ ├── parse
│ │ └── StatementParser.scala
│ ├── queries
│ │ ├── BaseQueries.scala
│ │ ├── QueryTranslations.scala
│ │ ├── audit
│ │ │ ├── AuditRecordQueries.scala
│ │ │ └── AuditReportQueries.scala
│ │ ├── auth
│ │ │ ├── PasswordInfoQueries.scala
│ │ │ └── UserQueries.scala
│ │ ├── column
│ │ │ └── ColumnDetailQueries.scala
│ │ ├── connection
│ │ │ └── ConnectionSettingsQueries.scala
│ │ ├── dynamic
│ │ │ ├── ColumnValueParser.scala
│ │ │ ├── DeleteRowStatement.scala
│ │ │ ├── DynamicQuery.scala
│ │ │ ├── InsertRowStatement.scala
│ │ │ └── UpdateRowStatement.scala
│ │ ├── export
│ │ │ ├── CsvExportQuery.scala
│ │ │ └── SqlExportQuery.scala
│ │ ├── query
│ │ │ └── SavedQueryQueries.scala
│ │ ├── result
│ │ │ ├── CachedResultQueries.scala
│ │ │ ├── CreateResultTable.scala
│ │ │ ├── InsertResultRow.scala
│ │ │ ├── SharedResultQueries.scala
│ │ │ └── SharedResultRow.scala
│ │ └── settings
│ │ │ └── SettingQueries.scala
│ ├── result
│ │ ├── CachedResult.scala
│ │ ├── CachedResultInsert.scala
│ │ ├── CachedResultQuery.scala
│ │ ├── CachedResultTransform.scala
│ │ ├── QueryResultGraphQL.scala
│ │ ├── QueryResultRow.scala
│ │ └── ResultQueryHelper.scala
│ ├── sandbox
│ │ └── SandboxTask.scala
│ ├── schema
│ │ ├── SchemaGraphQL.scala
│ │ └── SchemaModelGraphQL.scala
│ ├── settings
│ │ ├── ExportModel.scala
│ │ ├── Setting.scala
│ │ └── SettingKey.scala
│ └── user
│ │ ├── ProfileData.scala
│ │ ├── RegistrationData.scala
│ │ ├── Role.scala
│ │ ├── User.scala
│ │ ├── UserForms.scala
│ │ └── UserProfile.scala
├── services
│ ├── audit
│ │ └── AuditRecordService.scala
│ ├── config
│ │ ├── ConfigFileService.scala
│ │ └── DatabaseConfig.scala
│ ├── connection
│ │ └── ConnectionSettingsService.scala
│ ├── database
│ │ ├── DatabaseConnection.scala
│ │ ├── DatabaseConnectionService.scala
│ │ ├── DatabaseRegistry.scala
│ │ ├── DatabaseWorkerPool.scala
│ │ ├── MasterDdl.scala
│ │ ├── SampleDatabaseService.scala
│ │ ├── core
│ │ │ ├── CoreDatabase.scala
│ │ │ ├── MasterDatabase.scala
│ │ │ └── ResultCacheDatabase.scala
│ │ ├── ssl
│ │ │ ├── ClientSideCertSslSockets.scala
│ │ │ ├── SslInit.scala
│ │ │ ├── SslParams.scala
│ │ │ └── SslSettings.scala
│ │ └── transaction
│ │ │ ├── TransactionManager.scala
│ │ │ └── TransactionProvider.scala
│ ├── explore
│ │ ├── ExploreFetcherHelper.scala
│ │ ├── ExploreHasIdHelper.scala
│ │ ├── ExploreService.scala
│ │ ├── ExploreTableHelper.scala
│ │ └── ExploreViewHelper.scala
│ ├── graphql
│ │ └── GraphQLService.scala
│ ├── plan
│ │ ├── PlanParseService.scala
│ │ ├── h2
│ │ │ └── H2ParseService.scala
│ │ ├── mysql
│ │ │ ├── MySqlParseKeys.scala
│ │ │ ├── MySqlParseService.scala
│ │ │ └── MySqlQueryBlockParser.scala
│ │ ├── oracle
│ │ │ ├── OracleParseHelper.scala
│ │ │ └── OracleParseService.scala
│ │ └── postgres
│ │ │ ├── PostgresNodeParser.scala
│ │ │ ├── PostgresParseHelper.scala
│ │ │ ├── PostgresParseKeys.scala
│ │ │ └── PostgresParseService.scala
│ ├── query
│ │ ├── ParameterService.scala
│ │ ├── PlanExecutionService.scala
│ │ ├── ProcedureService.scala
│ │ ├── QueryCheckService.scala
│ │ ├── QueryExecutionService.scala
│ │ ├── QueryResultRowService.scala
│ │ ├── QuerySaveService.scala
│ │ ├── RowDataHelper.scala
│ │ ├── RowDataService.scala
│ │ ├── RowUpdateService.scala
│ │ ├── SavedQueryService.scala
│ │ ├── SharedResultService.scala
│ │ └── SimpleQueryService.scala
│ ├── result
│ │ ├── CachedResultActor.scala
│ │ ├── CachedResultService.scala
│ │ └── ChartDataService.scala
│ ├── schema
│ │ ├── JdbcHelper.scala
│ │ ├── MermaidChartService.scala
│ │ ├── MetadataColumns.scala
│ │ ├── MetadataEnums.scala
│ │ ├── MetadataIdentifiers.scala
│ │ ├── MetadataIndexes.scala
│ │ ├── MetadataKeys.scala
│ │ ├── MetadataProcedures.scala
│ │ ├── MetadataTables.scala
│ │ ├── MetadataTimezone.scala
│ │ ├── MetadataViews.scala
│ │ ├── SchemaHelper.scala
│ │ ├── SchemaRefreshService.scala
│ │ └── SchemaService.scala
│ ├── settings
│ │ └── SettingsService.scala
│ ├── socket
│ │ ├── DetailHelper.scala
│ │ ├── RequestMessageHelper.scala
│ │ ├── SocketService.scala
│ │ ├── StartHelper.scala
│ │ └── TransactionHelper.scala
│ ├── supervisor
│ │ └── ActorSupervisor.scala
│ └── user
│ │ ├── PasswordInfoService.scala
│ │ ├── UserSearchService.scala
│ │ └── UserService.scala
├── util
│ ├── ApplicationContext.scala
│ ├── ApplicationHelper.scala
│ ├── Configuration.scala
│ ├── DateUtils.scala
│ ├── ExceptionUtils.scala
│ ├── FutureUtils.scala
│ ├── JdbcUtils.scala
│ ├── JsonUtils.scala
│ ├── Logging.scala
│ ├── PasswordEncryptUtils.scala
│ ├── SlugUtils.scala
│ ├── cache
│ │ ├── CacheService.scala
│ │ └── UserCache.scala
│ └── web
│ │ ├── DataOutputFormatter.scala
│ │ ├── ErrorHandler.scala
│ │ ├── FormUtils.scala
│ │ ├── LoggingFilter.scala
│ │ ├── MessageFrameFormatter.scala
│ │ ├── RequestHandler.scala
│ │ └── WebFilters.scala
└── views
│ ├── activity
│ ├── listRow.scala.html
│ ├── listScript.scala.html
│ └── listTable.scala.html
│ ├── admin
│ ├── cache
│ │ └── results.scala.html
│ ├── index.scala.html
│ ├── sandbox
│ │ ├── list.scala.html
│ │ ├── metrics.scala.html
│ │ └── run.scala.html
│ ├── settings.scala.html
│ ├── status.scala.html
│ ├── systemActivity.scala.html
│ └── user
│ │ ├── create.scala.html
│ │ ├── edit.scala.html
│ │ ├── list.scala.html
│ │ └── view.scala.html
│ ├── auth
│ ├── register.scala.html
│ └── signin.scala.html
│ ├── components
│ ├── formErrors.scala.html
│ ├── motd.scala.html
│ ├── permissionsPanel.scala.html
│ └── userDropdown.scala.html
│ ├── connection
│ ├── form.scala.html
│ ├── list.scala.html
│ └── permissions.scala.html
│ ├── error
│ ├── badRequest.scala.html
│ ├── notFound.scala.html
│ └── serverError.scala.html
│ ├── graphql
│ ├── graphiql.scala.html
│ └── voyager.scala.html
│ ├── index.scala.html
│ ├── layout
│ ├── admin.scala.html
│ ├── basic.scala.html
│ ├── materialize.scala.html
│ ├── simple.scala.html
│ └── themeStyles.scala.html
│ ├── maintenance.scala.html
│ ├── modal
│ ├── columnModal.scala.html
│ ├── confirmModal.scala.html
│ ├── exportModal.scala.html
│ ├── planNodeModal.scala.html
│ ├── reconnectModal.scala.html
│ ├── rowDetailModal.scala.html
│ ├── rowUpdateModal.scala.html
│ ├── saveQueryModal.scala.html
│ └── sharedResultModal.scala.html
│ ├── profile
│ ├── changePassword.scala.html
│ ├── userActivity.scala.html
│ └── view.scala.html
│ ├── query
│ ├── main.scala.html
│ ├── navbar.scala.html
│ └── sidenav.scala.html
│ ├── result
│ ├── export.scala.html
│ ├── list.scala.html
│ ├── options.scala.html
│ ├── title.scala.html
│ ├── viewChart.scala.html
│ └── viewData.scala.html
│ └── schema
│ └── mermaid.scala.html
├── bin
├── docker.sh
└── publish.sh
├── build.sbt
├── charting
└── src
│ └── main
│ └── scala
│ ├── Charting.scala
│ ├── models
│ ├── charting
│ │ ├── ChartColumn.scala
│ │ ├── ChartSettings.scala
│ │ ├── ChartType.scala
│ │ └── options
│ │ │ ├── BarChartOptions.scala
│ │ │ ├── BoxPlotOptions.scala
│ │ │ ├── BubbleChart3DOptions.scala
│ │ │ ├── BubbleChartOptions.scala
│ │ │ ├── ChartOptions.scala
│ │ │ ├── HistogramOptions.scala
│ │ │ ├── LineChartOptions.scala
│ │ │ ├── PieChartOptions.scala
│ │ │ ├── ScatterPlot3DOptions.scala
│ │ │ └── ScatterPlotOptions.scala
│ └── template
│ │ └── ChartOptionsTemplate.scala
│ ├── services
│ └── charting
│ │ ├── ChartRenderService.scala
│ │ ├── ChartSettingsService.scala
│ │ ├── ChartingService.scala
│ │ └── ChartingTests.scala
│ └── util
│ └── LogUtils.scala
├── client
└── src
│ └── main
│ └── scala
│ ├── DatabaseFlowApp.scala
│ ├── NetworkHelper.scala
│ ├── ResponseMessageHelper.scala
│ ├── models
│ └── template
│ │ ├── FeedbackTemplate.scala
│ │ ├── HelpTemplate.scala
│ │ ├── HistoryTemplate.scala
│ │ ├── ModelListTemplate.scala
│ │ ├── ProgressTemplate.scala
│ │ ├── SidenavTemplate.scala
│ │ ├── StaticPanelTemplate.scala
│ │ ├── column
│ │ ├── ColumnTemplate.scala
│ │ ├── TableColumnDetailTemplate.scala
│ │ └── ViewColumnDetailTemplate.scala
│ │ ├── proc
│ │ ├── ProcedureCallTemplate.scala
│ │ └── ProcedureDetailTemplate.scala
│ │ ├── query
│ │ ├── QueryEditorTemplate.scala
│ │ ├── QueryErrorTemplate.scala
│ │ ├── QueryFilterTemplate.scala
│ │ ├── QueryParametersTemplate.scala
│ │ ├── QueryPlanNodeDetailTemplate.scala
│ │ ├── QueryPlanTemplate.scala
│ │ ├── QueryResultsTemplate.scala
│ │ └── StatementResultsTemplate.scala
│ │ ├── results
│ │ ├── ChartResultTemplate.scala
│ │ ├── DataCellTemplate.scala
│ │ ├── DataFilterTemplate.scala
│ │ └── DataTableTemplate.scala
│ │ ├── tbl
│ │ ├── RowDetailTemplate.scala
│ │ ├── RowUpdateTemplate.scala
│ │ ├── TableDetailTemplate.scala
│ │ ├── TableForeignKeyDetailTemplate.scala
│ │ └── TableIndexDetailTemplate.scala
│ │ ├── typ
│ │ └── EnumDetailTemplate.scala
│ │ └── view
│ │ └── ViewDetailTemplate.scala
│ ├── services
│ ├── InitService.scala
│ ├── ModelResultsService.scala
│ ├── NavigationService.scala
│ ├── NotificationService.scala
│ ├── ShortcutService.scala
│ ├── TextChangeService.scala
│ └── query
│ │ ├── ChartService.scala
│ │ ├── QueryAppendService.scala
│ │ ├── QueryErrorService.scala
│ │ ├── QueryEventHandlers.scala
│ │ ├── QueryPlanService.scala
│ │ ├── QueryResultService.scala
│ │ ├── RowCountService.scala
│ │ ├── StatementResultService.scala
│ │ └── TransactionService.scala
│ ├── ui
│ ├── FeedbackManager.scala
│ ├── HelpManager.scala
│ ├── HistoryManager.scala
│ ├── ProgressManager.scala
│ ├── UserManager.scala
│ ├── WorkspaceManager.scala
│ ├── editor
│ │ ├── EditorCreationHelper.scala
│ │ └── EditorManager.scala
│ ├── metadata
│ │ ├── EnumUpdates.scala
│ │ ├── MetadataManager.scala
│ │ ├── ModelFilterManager.scala
│ │ ├── ModelListManager.scala
│ │ ├── ProcedureUpdates.scala
│ │ ├── TableUpdates.scala
│ │ └── ViewUpdates.scala
│ ├── modal
│ │ ├── ColumnDetailManager.scala
│ │ ├── ConfirmManager.scala
│ │ ├── PlanNodeDetailManager.scala
│ │ ├── QueryExportFormManager.scala
│ │ ├── ReconnectManager.scala
│ │ ├── RowDetailManager.scala
│ │ ├── RowUpdateManager.scala
│ │ ├── SavedQueryFormManager.scala
│ │ └── SharedResultFormManager.scala
│ ├── query
│ │ ├── AdHocQueryManager.scala
│ │ ├── EnumManager.scala
│ │ ├── FilterManager.scala
│ │ ├── ParameterChangeManager.scala
│ │ ├── ParameterManager.scala
│ │ ├── ProcedureManager.scala
│ │ ├── QueryCheckManager.scala
│ │ ├── QueryManager.scala
│ │ ├── RowDataManager.scala
│ │ ├── SavedQueryChangeManager.scala
│ │ ├── SavedQueryManager.scala
│ │ ├── SharedResultManager.scala
│ │ ├── SqlManager.scala
│ │ ├── TableDetailHelper.scala
│ │ ├── TableManager.scala
│ │ ├── ViewDetailHelper.scala
│ │ └── ViewManager.scala
│ ├── search
│ │ ├── SearchFilterFields.scala
│ │ ├── SearchFilterManager.scala
│ │ └── SearchManager.scala
│ └── tabs
│ │ ├── TabManager.scala
│ │ └── TabSelectionManager.scala
│ └── util
│ ├── KeyboardShortcut.scala
│ ├── LogHelper.scala
│ ├── Messages.scala
│ ├── NetworkMessage.scala
│ ├── NetworkSocket.scala
│ ├── ScriptLoader.scala
│ └── TemplateHelper.scala
├── conf
├── admin.routes
├── application.conf
├── client
│ └── messages
├── export-additions.sql
├── icon.png
├── initial-config.conf
├── logback-test.xml
├── logback.xml
├── messages
└── routes
├── dblibs
├── lib
│ ├── db2-db2jcc4.jar
│ ├── informix-ifxjdbc.jar
│ ├── informix-ifxjdbcx.jar
│ ├── informix-ifxlang.jar
│ ├── informix-ifxlsupp.jar
│ ├── informix-ifxsqlj.jar
│ ├── informix-ifxtools.jar
│ └── oracle-ojdbc7.jar
└── src
│ └── main
│ └── resources
│ └── sampledb
│ └── sampledb.sqlite
├── doc
└── src
│ ├── assets
│ └── images
│ │ ├── logo-small.png
│ │ └── logo.png
│ ├── contribute
│ ├── index.md
│ ├── packaging.md
│ ├── sbtTasks.md
│ └── technology.md
│ ├── database
│ ├── db2.md
│ ├── h2.md
│ ├── index.md
│ ├── informix.md
│ ├── mysql.md
│ ├── oracle.md
│ ├── postgresql.md
│ ├── sqlite.md
│ └── sqlserver.md
│ ├── databaseflow.css
│ ├── feature
│ ├── charting.md
│ ├── graphql.md
│ ├── history.md
│ ├── index.md
│ ├── queryplan.md
│ ├── sqleditor.md
│ └── visualization.md
│ ├── gettingStarted.md
│ ├── index.md
│ ├── logo.png
│ └── manage
│ ├── configuration.md
│ ├── installLocal.md
│ └── installShared.md
├── license
├── project
├── Client.scala
├── Database.scala
├── Dependencies.scala
├── Documentation.scala
├── Packaging.scala
├── PlayUtils.scala
├── Server.scala
├── Shared.scala
├── build.properties
└── plugins.sbt
├── public
├── browserconfig.xml
├── fonts
│ ├── roboto-mono_bold-italic.eot
│ ├── roboto-mono_bold-italic.svg
│ ├── roboto-mono_bold-italic.ttf
│ ├── roboto-mono_bold-italic.woff
│ ├── roboto-mono_bold.eot
│ ├── roboto-mono_bold.svg
│ ├── roboto-mono_bold.ttf
│ ├── roboto-mono_bold.woff
│ ├── roboto-mono_italic.eot
│ ├── roboto-mono_italic.svg
│ ├── roboto-mono_italic.ttf
│ ├── roboto-mono_italic.woff
│ ├── roboto-mono_light-italic.eot
│ ├── roboto-mono_light-italic.svg
│ ├── roboto-mono_light-italic.ttf
│ ├── roboto-mono_light-italic.woff
│ ├── roboto-mono_light.eot
│ ├── roboto-mono_light.svg
│ ├── roboto-mono_light.ttf
│ ├── roboto-mono_light.woff
│ ├── roboto-mono_medium-italic.eot
│ ├── roboto-mono_medium-italic.svg
│ ├── roboto-mono_medium-italic.ttf
│ ├── roboto-mono_medium-italic.woff
│ ├── roboto-mono_medium.eot
│ ├── roboto-mono_medium.svg
│ ├── roboto-mono_medium.ttf
│ ├── roboto-mono_medium.woff
│ ├── roboto-mono_regular.eot
│ ├── roboto-mono_regular.svg
│ ├── roboto-mono_regular.ttf
│ ├── roboto-mono_regular.woff
│ ├── roboto-mono_thin-italic.eot
│ ├── roboto-mono_thin-italic.svg
│ ├── roboto-mono_thin-italic.ttf
│ ├── roboto-mono_thin-italic.woff
│ ├── roboto-mono_thin.eot
│ ├── roboto-mono_thin.svg
│ ├── roboto-mono_thin.ttf
│ └── roboto-mono_thin.woff
├── images
│ └── ui
│ │ └── favicon
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── apple-touch-icon-precomposed.png
│ │ ├── apple-touch-icon.png
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── favicon-512.png
│ │ ├── favicon-alt.psd
│ │ ├── favicon.bmp
│ │ ├── favicon.ico
│ │ ├── favicon.png
│ │ ├── favicon.psd
│ │ ├── icon-amber.png
│ │ ├── icon-amber.svg
│ │ ├── icon-amber@2x.png
│ │ ├── icon-black.png
│ │ ├── icon-black.svg
│ │ ├── icon-black@2x.png
│ │ ├── icon-blue-grey.png
│ │ ├── icon-blue-grey.svg
│ │ ├── icon-blue-grey@2x.png
│ │ ├── icon-blue.png
│ │ ├── icon-blue.svg
│ │ ├── icon-blue@2x.png
│ │ ├── icon-brown.png
│ │ ├── icon-brown.svg
│ │ ├── icon-brown@2x.png
│ │ ├── icon-cyan.png
│ │ ├── icon-cyan.svg
│ │ ├── icon-cyan@2x.png
│ │ ├── icon-deep-orange.png
│ │ ├── icon-deep-orange.svg
│ │ ├── icon-deep-orange@2x.png
│ │ ├── icon-deep-purple.png
│ │ ├── icon-deep-purple.svg
│ │ ├── icon-deep-purple@2x.png
│ │ ├── icon-green.png
│ │ ├── icon-green.svg
│ │ ├── icon-green@2x.png
│ │ ├── icon-grey.png
│ │ ├── icon-grey.svg
│ │ ├── icon-grey@2x.png
│ │ ├── icon-indigo.png
│ │ ├── icon-indigo.svg
│ │ ├── icon-indigo@2x.png
│ │ ├── icon-light-blue.png
│ │ ├── icon-light-blue.svg
│ │ ├── icon-light-blue@2x.png
│ │ ├── icon-light-green.png
│ │ ├── icon-light-green.svg
│ │ ├── icon-light-green@2x.png
│ │ ├── icon-orange.png
│ │ ├── icon-orange.svg
│ │ ├── icon-orange@2x.png
│ │ ├── icon-pink.png
│ │ ├── icon-pink.svg
│ │ ├── icon-pink@2x.png
│ │ ├── icon-purple.png
│ │ ├── icon-purple.svg
│ │ ├── icon-purple@2x.png
│ │ ├── icon-red.png
│ │ ├── icon-red.svg
│ │ ├── icon-red@2x.png
│ │ ├── icon-teal.png
│ │ ├── icon-teal.svg
│ │ ├── icon-teal@2x.png
│ │ ├── mstile-144x144.png
│ │ ├── mstile-150x150.png
│ │ ├── mstile-310x150.png
│ │ ├── mstile-310x310.png
│ │ ├── mstile-70x70.png
│ │ └── safari-pinned-tab.svg
├── manifest.json
└── vendor
│ ├── ace
│ └── ace-concat.min.js
│ ├── fontawesome
│ └── font-awesome.min.css
│ ├── fonts
│ ├── FontAwesome.otf
│ ├── fontawesome-webfont.eot
│ ├── fontawesome-webfont.svg
│ ├── fontawesome-webfont.ttf
│ ├── fontawesome-webfont.woff
│ ├── fontawesome-webfont.woff2
│ └── roboto
│ │ ├── Roboto-Bold.eot
│ │ ├── Roboto-Bold.ttf
│ │ ├── Roboto-Bold.woff
│ │ ├── Roboto-Bold.woff2
│ │ ├── Roboto-Light.eot
│ │ ├── Roboto-Light.ttf
│ │ ├── Roboto-Light.woff
│ │ ├── Roboto-Light.woff2
│ │ ├── Roboto-Medium.eot
│ │ ├── Roboto-Medium.ttf
│ │ ├── Roboto-Medium.woff
│ │ ├── Roboto-Medium.woff2
│ │ ├── Roboto-Regular.eot
│ │ ├── Roboto-Regular.ttf
│ │ ├── Roboto-Regular.woff
│ │ ├── Roboto-Regular.woff2
│ │ ├── Roboto-Thin.eot
│ │ ├── Roboto-Thin.ttf
│ │ ├── Roboto-Thin.woff
│ │ └── Roboto-Thin.woff2
│ ├── graphql
│ ├── fetch.min.js
│ ├── graphiql.css
│ ├── graphiql.js
│ ├── graphiql.min.js
│ ├── react-dom.js
│ ├── react-dom.min.js
│ ├── react.js
│ ├── react.min.js
│ ├── voyager.css
│ ├── voyager.css.map
│ ├── voyager.js
│ ├── voyager.js.map
│ ├── voyager.lib.js
│ ├── voyager.lib.js.map
│ ├── voyager.min.js
│ ├── voyager.min.js.map
│ └── voyager.worker.js
│ ├── jquery
│ └── jquery.min.js
│ ├── materialize
│ ├── materialize.min.css
│ └── materialize.min.js
│ ├── mermaid
│ ├── mermaid.min.js
│ └── svg-pan-zoom.min.js
│ ├── momentjs
│ └── moment.min.js
│ ├── mousetrap
│ └── mousetrap.min.js
│ └── plotly
│ └── plotly.min.js
├── readme.md
├── scalastyle-config.xml
├── shared
└── src
│ └── main
│ └── scala
│ ├── models
│ ├── RequestMessages.scala
│ ├── ResponseMessages.scala
│ ├── audit
│ │ ├── AuditRecord.scala
│ │ ├── AuditStatus.scala
│ │ └── AuditType.scala
│ ├── engine
│ │ ├── DatabaseEngine.scala
│ │ ├── EngineColumnParser.scala
│ │ ├── EngineQueries.scala
│ │ ├── capabilities
│ │ │ └── EngineCapabilities.scala
│ │ ├── functions
│ │ │ ├── DB2Functions.scala
│ │ │ ├── FunctionProvider.scala
│ │ │ ├── H2Functions.scala
│ │ │ ├── InformixFunctions.scala
│ │ │ ├── MySQLFunctions.scala
│ │ │ ├── OracleFunctions.scala
│ │ │ ├── PostgreSQLFunctions.scala
│ │ │ ├── SQLServerFunctions.scala
│ │ │ └── SQLiteFunctions.scala
│ │ └── types
│ │ │ ├── DB2Types.scala
│ │ │ ├── H2Types.scala
│ │ │ ├── InformixTypes.scala
│ │ │ ├── MySQLTypes.scala
│ │ │ ├── OracleTypes.scala
│ │ │ ├── PostgreSQLTypes.scala
│ │ │ ├── SQLServerTypes.scala
│ │ │ ├── SQLiteTypes.scala
│ │ │ └── TypeProvider.scala
│ ├── plan
│ │ ├── PlanError.scala
│ │ ├── PlanNode.scala
│ │ └── PlanResult.scala
│ ├── query
│ │ ├── QueryCheckResult.scala
│ │ ├── QueryError.scala
│ │ ├── QueryFilter.scala
│ │ ├── QueryResult.scala
│ │ ├── RowDataOptions.scala
│ │ ├── SavedQuery.scala
│ │ ├── SharedResult.scala
│ │ ├── SqlParser.scala
│ │ └── TransactionState.scala
│ ├── schema
│ │ ├── Column.scala
│ │ ├── ColumnDetails.scala
│ │ ├── ColumnType.scala
│ │ ├── EnumType.scala
│ │ ├── FilterOp.scala
│ │ ├── ForeignKey.scala
│ │ ├── Index.scala
│ │ ├── IndexColumn.scala
│ │ ├── PrimaryKey.scala
│ │ ├── Procedure.scala
│ │ ├── ProcedureParam.scala
│ │ ├── Reference.scala
│ │ ├── Schema.scala
│ │ ├── Table.scala
│ │ └── View.scala
│ ├── template
│ │ ├── Icons.scala
│ │ └── Theme.scala
│ └── user
│ │ ├── Permission.scala
│ │ └── UserPreferences.scala
│ └── util
│ ├── Config.scala
│ ├── JsonSerializers.scala
│ ├── NullUtils.scala
│ ├── NumberUtils.scala
│ ├── StringKeyUtils.scala
│ └── TipsAndTricks.scala
└── test
├── resources
└── plan
│ ├── mysql-complicated-query.json
│ ├── mysql-complicated-query.sql
│ ├── mysql-nested-loop.json
│ ├── mysql-nested-loop.sql
│ ├── postgres-complicated-query.json
│ ├── postgres-complicated-query.sql
│ ├── postgres-nested-join.json
│ └── postgres-nested-join.sql
└── test
├── plan
├── MySqlPlanParseTest.scala
├── PlanParseTestHelper.scala
└── PostgresPlanParseTest.scala
└── query
├── EngineQueriesTest.scala
└── SqlParserTest.scala
/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | project/project/target
3 | project/target
4 | target
5 | tmp
6 | .history
7 | dist
8 | /.idea
9 | /*.iml
10 | /db
11 | /out
12 |
13 | /cache
14 |
15 | /build/*
16 |
17 | /.idea_modules
18 | /.classpath
19 | /.project
20 | /RUNNING_PID
21 | /.settings
22 | **/licenses
23 |
24 | buildinfo.properties
25 |
--------------------------------------------------------------------------------
/.java-version:
--------------------------------------------------------------------------------
1 | 1.8
2 |
--------------------------------------------------------------------------------
/.scalafix.conf:
--------------------------------------------------------------------------------
1 | rewrites = ["https://gist.githubusercontent.com/sjrd/ef8bb7c52be1451b3a3b9bab6a187549/raw/0b1d451d266bce20921bbff3a74722610d604509/ScalaJSRewrites.scala"]
2 | imports.organize = false
3 | imports.removeUnused = true
4 |
--------------------------------------------------------------------------------
/.scalariform.conf:
--------------------------------------------------------------------------------
1 | #alignArguments=false
2 | #alignParameters=true
3 | #alignSingleLineCaseStatements=true
4 | #alignSingleLineCaseStatements.maxArrowIndent=40
5 | #compactControlReadability=false
6 | #compactStringConcatenation=false
7 | danglingCloseParenthesis=Preserve
8 | #doubleIndentClassDeclaration=false
9 | doubleIndentConstructorArguments=true
10 | #doubleIndentMethodDeclaration=false
11 | #firstArgumentOnNewline=Force
12 | #firstParameterOnNewline=Force
13 | #formatXml=true
14 | #indentLocalDefs=false
15 | #indentPackageBlocks=true
16 | #indentSpaces=2
17 | #indentWithTabs=false
18 | #multilineScaladocCommentsStartOnFirstLine=false
19 | #newlineAtEndOfFile=false
20 | #placeScaladocAsterisksBeneathSecondAsterisk=false
21 | #preserveSpaceBeforeArguments=false
22 | #rewriteArrowSymbols=false
23 | #spaceBeforeColon=false
24 | #spaceBeforeContextColon=false
25 | #spaceInsideBrackets=false
26 | #spaceInsideParentheses=false
27 | spacesAroundMultiImports=false
28 | #spacesWithinPatternBinders=true
29 |
--------------------------------------------------------------------------------
/Jenkinsfile:
--------------------------------------------------------------------------------
1 | pipeline {
2 | agent any
3 |
4 | stages {
5 | stage('Build') {
6 | steps {
7 | sh "sbt -no-colors -batch dist site/dist"
8 | archiveArtifacts artifacts: '**/target/universal/*.zip', fingerprint: true
9 | }
10 | }
11 |
12 | stage('Publish') {
13 | steps {
14 | echo 'TODO: publish'
15 | }
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/DatabaseFlow.scala:
--------------------------------------------------------------------------------
1 | import play.api._
2 | import play.core.server.{ProdServerStart, RealServerProcess, ServerConfig, ServerProvider}
3 | import util.Logging
4 |
5 | object DatabaseFlow extends Logging {
6 | def main(args: Array[String]): Unit = startServer(new RealServerProcess(args))
7 |
8 | def startServer(process: RealServerProcess) = {
9 | val config: ServerConfig = ProdServerStart.readServerConfigSettings(process)
10 | val application: Application = {
11 | val environment = Environment(config.rootDir, process.classLoader, Mode.Prod)
12 | val context = ApplicationLoader.Context.create(environment)
13 | val loader = ApplicationLoader(context)
14 | loader.load(context)
15 | }
16 | Play.start(application)
17 |
18 | val serverProvider: ServerProvider = ServerProvider.fromConfiguration(process.classLoader, config.configuration)
19 | val server = serverProvider.createServer(config, application)
20 | process.addShutdownHook(server.stop())
21 | server
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/_components.less:
--------------------------------------------------------------------------------
1 | .table-options {
2 | .section-content {
3 | padding: 5px 20px 10px 20px;
4 | }
5 | }
6 |
7 | .help-panel {
8 | padding: 10px 20px;
9 | margin-top: 20px;
10 | }
11 |
12 | #tip-detail {
13 | padding: 15px;
14 | margin-bottom: 10px;
15 | text-align: center;
16 | }
17 |
18 | .padded {
19 | padding: 12px 0;
20 | }
21 | .padded.horizontal {
22 | padding: 12px;
23 | }
24 | .with-padding {
25 | padding: 12px;
26 | }
27 |
28 | .content-panel {
29 | .right.fa-close {
30 | margin-top: 6px;
31 | margin-right: 4px;
32 | }
33 | .panel-title {
34 | line-height: 28px;
35 | font-size: 24px;
36 | font-weight: 300;
37 | margin-bottom: 10px;
38 | }
39 | .panel-action {
40 | margin: 10px 0;
41 | a {
42 | margin-right: 10px;
43 | }
44 | a.right {
45 | margin-left: 10px;
46 | margin-right: 0;
47 | }
48 | }
49 | }
50 |
51 | .history-table {
52 | td {
53 | vertical-align: top;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/_editor.less:
--------------------------------------------------------------------------------
1 | .sql-textarea {
2 | border: 1px solid rgba(120, 120, 120, 0.2);
3 | width: 100%;
4 | }
5 |
6 | .sql-parameters {
7 | border: 1px solid #ddd;
8 | border-top: none;
9 | padding: 10px 0 18px 0;
10 | background-color: white;
11 | input {
12 | margin-bottom: 0;
13 | }
14 | }
15 |
16 | .ace_editor.ace-tm {
17 | border: 1px rgba(120, 120, 120, 0.2) solid;
18 |
19 | .ace_marker-layer .ace_selection {
20 | background-color: #eee;
21 | }
22 |
23 | .ace_gutter {
24 | font-family: 'roboto-mono', 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace !important;
25 | }
26 |
27 | .ace_content {
28 | font-family: 'roboto-mono', 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
29 |
30 | .ace_text-layer {
31 | box-shadow: 0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);
32 |
33 | .ace_line {
34 | font-family: 'roboto-mono', 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/_queryplannode.less:
--------------------------------------------------------------------------------
1 | .tree {
2 | .node {
3 | border: 1px solid #ccc;
4 | padding: 5px 10px;
5 | color: #666;
6 | text-align: left;
7 | font-size: 14px;
8 | cursor: pointer;
9 | //white-space: nowrap;
10 | //overflow: hidden;
11 | //text-overflow: ellipsis;
12 | display: inline-block;
13 | width: 270px;
14 |
15 | transition: all 0.5s;
16 | -webkit-transition: all 0.5s;
17 | -moz-transition: all 0.5s;
18 |
19 |
20 | .node-percentage, .node-duration {
21 | float: right;
22 | }
23 |
24 | .node-stat-divider {
25 | float: right;
26 | margin: 0 6px;
27 | }
28 |
29 | .node-properties {
30 | overflow-x: scroll;
31 | }
32 | }
33 |
34 | .node:hover, .node:hover + ul li .node {
35 | background: #fff;
36 | color: #000;
37 | border: 1px solid #aaa;
38 | }
39 |
40 | /*
41 | .node:hover+ul li::after,
42 | .node:hover+ul li::before,
43 | .node:hover+ul::before,
44 | .node:hover+ul ul::before{
45 | border-color: #aaa;
46 | }
47 | */
48 | }
49 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/_savequery.less:
--------------------------------------------------------------------------------
1 | #save-query-modal {
2 | i {
3 | margin-right: 5px;
4 | }
5 |
6 | .row {
7 | margin-bottom: 0;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/_tabs.less:
--------------------------------------------------------------------------------
1 | .tab-container {
2 | margin: 0;
3 | .tabs {
4 | overflow: hidden;
5 | .tab {
6 | text-transform: none;
7 | }
8 | }
9 | }
10 |
11 | .tabs .tab a {
12 | transition: opacity .28s ease;
13 | }
14 |
15 | .tabs .tab a:hover {
16 | opacity: 0.5;
17 | }
18 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/graphiql.less:
--------------------------------------------------------------------------------
1 | @import "_font";
2 | @import "_fontmono";
3 |
4 | body {
5 | height: 100%;
6 | margin: 0;
7 | width: 100%;
8 | overflow: hidden;
9 | }
10 |
11 | #graphiql {
12 | height: 100vh;
13 | }
14 |
15 | .graphiql-container .docExplorerShow:before {
16 | border-left: 2px solid #fff;
17 | border-top: 2px solid #fff;
18 | }
19 |
20 | .graphiql-container .CodeMirror-lines {
21 | padding: 0;
22 | }
23 |
24 | .graphiql-container .execute-button-wrap {
25 | margin: 0 12px;
26 | }
27 |
28 | .graphiql-container .execute-button {
29 | box-shadow: none;
30 | }
31 |
32 | .graphiql-container .toggle-button {
33 | float: left;
34 | font-size: 1.3rem;
35 | margin: -4px 14px 0 -4px;
36 | cursor: pointer;
37 | }
38 |
39 | .graphiql-container .title-link {
40 | color: #fff;
41 | text-decoration: none;
42 | font-weight: 300;
43 | }
44 |
--------------------------------------------------------------------------------
/app/controllers/HomeController.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import services.connection.ConnectionSettingsService
4 | import util.ApplicationContext
5 |
6 | import scala.concurrent.Future
7 |
8 | @javax.inject.Singleton
9 | class HomeController @javax.inject.Inject() (override val ctx: ApplicationContext) extends BaseController {
10 | def home() = withSession("home") { implicit request =>
11 | val connections = ConnectionSettingsService.getVisible(request.identity)
12 | Future.successful(Ok(views.html.index(request.identity, connections)))
13 | }
14 |
15 | def untrail(path: String) = Action.async {
16 | Future.successful(MovedPermanently(s"/$path"))
17 | }
18 |
19 | def externalLink(url: String) = withSession("external.link") { implicit request =>
20 | Future.successful(Redirect(if (url.startsWith("http")) { url } else { "http://" + url }))
21 | }
22 |
23 | def ping(timestamp: Long) = withSession("ping") { implicit request =>
24 | Future.successful(Ok(timestamp.toString))
25 | }
26 |
27 | def robots() = withSession("robots") { implicit request =>
28 | Future.successful(Ok("User-agent: *\nDisallow: /"))
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/controllers/MessagesController.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import java.net.URL
4 |
5 | import play.api.i18n.Messages
6 | import util.ApplicationContext
7 |
8 | import scala.concurrent.Future
9 |
10 | @javax.inject.Singleton
11 | class MessagesController @javax.inject.Inject() (override val ctx: ApplicationContext) extends BaseController {
12 | private[this] def parseMsgs(url: URL) = Messages.parse(Messages.UrlMessageSource(url), url.toString).fold(e => throw e, identity)
13 |
14 | private[this] lazy val msgs = parseMsgs(getClass.getClassLoader.getResource("client/messages"))
15 |
16 | private[this] val responses = {
17 | val vals = msgs.map { m =>
18 | s""""${m._1}": "${m._2}""""
19 | }.mkString(",\n ")
20 | s"""window.messages = {\n $vals\n}"""
21 | }
22 |
23 | def strings() = withoutSession("strings") { implicit request =>
24 | Future.successful(Ok(responses).as("application/javascript"))
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/controllers/admin/AdminController.scala:
--------------------------------------------------------------------------------
1 | package controllers.admin
2 |
3 | import controllers.BaseController
4 | import services.connection.ConnectionSettingsService
5 | import util.ApplicationContext
6 |
7 | import scala.concurrent.Future
8 |
9 | @javax.inject.Singleton
10 | class AdminController @javax.inject.Inject() (override val ctx: ApplicationContext) extends BaseController {
11 | def index = withAdminSession("admin-index") { implicit request =>
12 | Future.successful(Ok(views.html.admin.index(request.identity)))
13 | }
14 |
15 | def status = withAdminSession("admin-status") { implicit request =>
16 | Future.successful(Ok(views.html.admin.status(request.identity)))
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/controllers/admin/SandboxController.scala:
--------------------------------------------------------------------------------
1 | package controllers.admin
2 |
3 | import akka.util.Timeout
4 | import controllers.BaseController
5 | import models.sandbox.SandboxTask
6 | import util.FutureUtils.defaultContext
7 | import util.ApplicationContext
8 |
9 | import scala.concurrent.Future
10 | import scala.concurrent.duration._
11 |
12 | @javax.inject.Singleton
13 | class SandboxController @javax.inject.Inject() (override val ctx: ApplicationContext) extends BaseController {
14 | implicit val timeout = Timeout(10.seconds)
15 |
16 | def list = withAdminSession("sandbox.list") { implicit request =>
17 | Future.successful(Ok(views.html.admin.sandbox.list(request.identity)))
18 | }
19 |
20 | def sandbox(key: String) = withAdminSession("sandbox." + key) { implicit request =>
21 | val sandbox = SandboxTask.withName(key)
22 | sandbox.run(ctx).map { result =>
23 | Ok(views.html.admin.sandbox.run(request.identity, sandbox, result))
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/controllers/admin/SettingsController.scala:
--------------------------------------------------------------------------------
1 | package controllers.admin
2 |
3 | import controllers.BaseController
4 | import models.settings.SettingKey
5 | import services.settings.SettingsService
6 | import util.ApplicationContext
7 | import util.web.FormUtils
8 |
9 | import scala.concurrent.Future
10 |
11 | @javax.inject.Singleton
12 | class SettingsController @javax.inject.Inject() (override val ctx: ApplicationContext) extends BaseController {
13 | def settings = withAdminSession("admin-settings") { implicit request =>
14 | Future.successful(Ok(views.html.admin.settings(request.identity)))
15 | }
16 |
17 | def saveSettings = withAdminSession("admin-settings-save") { implicit request =>
18 | val form = FormUtils.getForm(request)
19 | form.foreach { x =>
20 | SettingKey.withNameOption(x._1) match {
21 | case Some(settingKey) => SettingsService.set(settingKey, x._2)
22 | case None => log.warn(messagesApi("admin.settings.invalid", x._1)(request.lang))
23 | }
24 | }
25 | Future.successful(Redirect(controllers.routes.HomeController.home()))
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/controllers/schema/SchemaController.scala:
--------------------------------------------------------------------------------
1 | package controllers.schema
2 |
3 | import controllers.BaseController
4 | import services.connection.ConnectionSettingsService
5 | import services.schema.{MermaidChartService, SchemaService}
6 | import util.ApplicationContext
7 |
8 | import util.FutureUtils.defaultContext
9 |
10 | @javax.inject.Singleton
11 | class SchemaController @javax.inject.Inject() (override val ctx: ApplicationContext) extends BaseController {
12 | def chart(id: String) = withSession("detail." + id) { implicit request =>
13 | val conn = ConnectionSettingsService.connFor(id).getOrElse(throw new IllegalStateException(s"Invalid connection [$id]"))
14 | SchemaService.getSchemaWithDetails(Some(request.identity), conn).map { schema =>
15 | val chartData = MermaidChartService.chartFor(schema)
16 | Ok(views.html.schema.mermaid(request.identity, id, conn.name, chartData))
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/models/InternalMessages.scala:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import java.util.UUID
4 |
5 | import akka.actor.ActorRef
6 | import models.user.User
7 |
8 | sealed trait InternalMessage
9 |
10 | case class SocketStarted(user: User, socketId: UUID, conn: ActorRef) extends InternalMessage
11 | case class SocketStopped(socketId: UUID) extends InternalMessage
12 |
13 | case object GetSystemStatus extends InternalMessage
14 | case class SystemStatus(sockets: Seq[(UUID, String)]) extends InternalMessage
15 |
16 | case class SendSocketTrace(id: UUID) extends InternalMessage
17 | case class SocketTraceResponse(id: UUID, userId: UUID, username: String) extends InternalMessage
18 |
19 | case class SendClientTrace(id: UUID) extends InternalMessage
20 | case class ClientTraceResponse(id: UUID, data: io.circe.Json) extends InternalMessage
21 |
--------------------------------------------------------------------------------
/app/models/auth/AuthEnv.scala:
--------------------------------------------------------------------------------
1 | package models.auth
2 |
3 | import com.mohiva.play.silhouette.api.Env
4 | import com.mohiva.play.silhouette.impl.authenticators.CookieAuthenticator
5 | import models.user.User
6 |
7 | trait AuthEnv extends Env {
8 | type I = User
9 | type A = CookieAuthenticator
10 | }
11 |
--------------------------------------------------------------------------------
/app/models/connection/ConnectionSettings.scala:
--------------------------------------------------------------------------------
1 | package models.connection
2 |
3 | import java.util.UUID
4 |
5 | import models.engine.DatabaseEngine
6 | import models.engine.DatabaseEngine.PostgreSQL
7 | import models.user.Permission
8 |
9 | object ConnectionSettings {
10 | val defaultEngine = PostgreSQL
11 | }
12 |
13 | case class ConnectionSettings(
14 | id: UUID,
15 | name: String,
16 | slug: String,
17 | owner: UUID,
18 | read: Permission = Permission.User,
19 | edit: Permission = Permission.Private,
20 | description: String = "",
21 | engine: DatabaseEngine = ConnectionSettings.defaultEngine,
22 | host: Option[String] = None,
23 | port: Option[Int] = None,
24 | dbName: Option[String] = None,
25 | extra: Option[String] = None,
26 | urlOverride: Option[String] = None,
27 | username: String = "",
28 | password: String = ""
29 | ) {
30 | val url = urlOverride.getOrElse(engine.url(host, port, dbName, extra))
31 | }
32 |
--------------------------------------------------------------------------------
/app/models/database/Conversions.scala:
--------------------------------------------------------------------------------
1 | package models.database
2 |
3 | import java.sql.Timestamp
4 |
5 | import org.joda.time.{LocalDateTime, LocalDate, DateTime, ReadableInstant}
6 | import util.DateUtils
7 |
8 | object Conversions {
9 | @SuppressWarnings(Array("MethodReturningAny"))
10 | def convert(x: AnyRef): AnyRef = x match {
11 | case num: BigDecimal => num.underlying()
12 | case num: BigInt => BigDecimal(num).underlying()
13 |
14 | // Convert Joda times to UTC.
15 | case ts: ReadableInstant => new Timestamp(new DateTime(ts.getMillis, ts.getZone).toDateTimeISO.getMillis)
16 | case d: LocalDate => new java.sql.Date(d.toDate.getTime)
17 | case d: LocalDateTime => new java.sql.Date(DateUtils.toMillis(d))
18 |
19 | // Pass everything else through.
20 | case _ => x
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/models/database/PoolSettings.scala:
--------------------------------------------------------------------------------
1 | package models.database
2 |
3 | import java.util.UUID
4 |
5 | import models.engine.DatabaseEngine
6 | import services.database.ssl.SslSettings
7 |
8 | case class PoolSettings(
9 | id: UUID = UUID.randomUUID,
10 | connectionId: UUID,
11 | name: Option[String] = None,
12 | engine: DatabaseEngine,
13 | url: String,
14 | username: String,
15 | password: String,
16 | maxWait: Long = 30000,
17 | maxSize: Int = 16,
18 | jdbcProperties: Map[String, String] = Map.empty,
19 | sslSettings: Option[SslSettings] = None,
20 | connectionInitSql: Option[String] = None
21 | )
22 |
--------------------------------------------------------------------------------
/app/models/database/Query.scala:
--------------------------------------------------------------------------------
1 | package models.database
2 |
3 | import java.sql.ResultSet
4 |
5 | trait RawQuery[A] {
6 | def sql: String
7 | def values: Seq[Any] = Seq.empty
8 | def handle(results: ResultSet): A
9 | }
10 |
11 | trait Query[A] extends RawQuery[A] {
12 | override def handle(results: ResultSet) = reduce(new Row.Iter(results))
13 | def reduce(rows: Iterator[Row]): A
14 | }
15 |
16 | trait SingleRowQuery[A] extends Query[A] {
17 | def map(row: Row): A
18 | override final def reduce(rows: Iterator[Row]) = if (rows.hasNext) {
19 | rows.map(map).next()
20 | } else {
21 | throw new IllegalStateException(s"No row returned for [$sql].")
22 | }
23 | }
24 |
25 | trait FlatSingleRowQuery[A] extends Query[Option[A]] {
26 | def flatMap(row: Row): Option[A]
27 | override final def reduce(rows: Iterator[Row]) = if (rows.hasNext) { flatMap(rows.next()) } else { None }
28 | }
29 |
--------------------------------------------------------------------------------
/app/models/database/Statement.scala:
--------------------------------------------------------------------------------
1 | package models.database
2 |
3 | trait Statement {
4 | def sql: String
5 | def values: Seq[Any] = Seq.empty
6 | }
7 |
--------------------------------------------------------------------------------
/app/models/ddl/CreateAuditRecordTable.scala:
--------------------------------------------------------------------------------
1 | package models.ddl
2 |
3 | case object CreateAuditRecordTable extends CreateTableStatement("audit_records") {
4 | override val sql = s"""
5 | create table "$tableName" (
6 | "id" uuid not null primary key,
7 |
8 | "audit_type" varchar(32) not null,
9 |
10 | "owner" uuid not null,
11 | "connection" uuid,
12 |
13 | "status" varchar(32) not null,
14 | "sql" text,
15 | "error" text,
16 | "rows_affected" int,
17 | "elapsed" int not null,
18 | "occurred" timestamp not null
19 | );
20 |
21 | create index "idx_${tableName}_user" on "$tableName" ("owner");
22 | """
23 | }
24 |
--------------------------------------------------------------------------------
/app/models/ddl/CreateConnectionsTable.scala:
--------------------------------------------------------------------------------
1 | package models.ddl
2 |
3 | object CreateConnectionsTable extends CreateTableStatement("connections") {
4 | override val sql = s"""create table "$tableName" (
5 | "id" uuid not null primary key,
6 | "name" varchar(512) not null,
7 | "slug" varchar(512) not null,
8 | "owner" uuid,
9 | "read" varchar(128) not null,
10 | "edit" varchar(128) not null,
11 | "description" varchar(4096) not null,
12 | "engine" varchar(128) not null,
13 | "host" varchar(2048),
14 | "db_name" varchar(2048),
15 | "extra" varchar(2048),
16 | "url_override" text,
17 | "username" varchar(512) not null,
18 | "password" varchar(2048) not null
19 | );
20 |
21 | create unique index "${tableName}_name_idx" on "$tableName" ("name");
22 | create unique index "${tableName}_slug_idx" on "$tableName" ("slug");
23 | """
24 | }
25 |
--------------------------------------------------------------------------------
/app/models/ddl/CreateGraphQLTable.scala:
--------------------------------------------------------------------------------
1 | package models.ddl
2 |
3 | case object CreateGraphQLTable extends CreateTableStatement("graphql") {
4 | override val sql = s"""
5 | create table "$tableName" (
6 | "id" uuid primary key,
7 | "connection_id" uuid,
8 | "category" varchar(512),
9 | "name" varchar(512) not null,
10 | "query" varchar(65536),
11 | "owner" uuid not null,
12 | "read" varchar(128) not null,
13 | "edit" varchar(128) not null,
14 | "created" timestamp not null
15 | );
16 |
17 | create index "${tableName}_owner_idx" on "$tableName" ("owner");
18 | """
19 | }
20 |
--------------------------------------------------------------------------------
/app/models/ddl/CreatePasswordInfoTable.scala:
--------------------------------------------------------------------------------
1 | package models.ddl
2 |
3 | case object CreatePasswordInfoTable extends CreateTableStatement("password_info") {
4 | override val sql = s"""
5 | create table "$tableName" (
6 | "provider" varchar(64) not null,
7 | "key" varchar(2048) not null,
8 | "hasher" varchar(64) not null,
9 | "password" varchar(256) not null,
10 | "salt" varchar(256),
11 | "created" timestamp not null,
12 | constraint "${tableName}_pkey" primary key ("provider", "key")
13 | );
14 | """
15 | }
16 |
--------------------------------------------------------------------------------
/app/models/ddl/CreateQueryResultsTable.scala:
--------------------------------------------------------------------------------
1 | package models.ddl
2 |
3 | case object CreateQueryResultsTable extends CreateTableStatement("query_results") {
4 | override val sql = s"""
5 | create table "$tableName" (
6 | "id" uuid primary key,
7 | "query_id" uuid not null,
8 | "connection_id" uuid not null,
9 | "owner" uuid,
10 | "status" varchar(32),
11 | "source" varchar(128),
12 | "sql" text,
13 | "columns" int not null default 0,
14 | "rows" int not null default 0,
15 | "first_message" int not null default 0,
16 | "duration" int not null default 0,
17 | "last_accessed" timestamp,
18 | "created" timestamp not null
19 | );
20 |
21 | create index "${tableName}_owner_idx" on "$tableName" ("owner");
22 | """
23 | }
24 |
--------------------------------------------------------------------------------
/app/models/ddl/CreateSavedQueriesTable.scala:
--------------------------------------------------------------------------------
1 | package models.ddl
2 |
3 | case object CreateSavedQueriesTable extends CreateTableStatement("saved_queries") {
4 | override val sql = s"""
5 | create table "$tableName" (
6 | "id" uuid not null primary key,
7 |
8 | "name" varchar(1024) not null,
9 | "description" varchar(4096),
10 | "sql" varchar(65536) not null,
11 | "params" varchar(4096),
12 |
13 | "owner" uuid,
14 | "connection" uuid,
15 | "read" varchar(64) not null,
16 | "edit" varchar(64) not null,
17 | "last_ran" timestamp,
18 | "created" timestamp not null,
19 | "updated" timestamp not null
20 | );
21 |
22 | create index "idx_${tableName}_owner" on "$tableName" ("owner");
23 | """
24 | }
25 |
--------------------------------------------------------------------------------
/app/models/ddl/CreateSettingsTable.scala:
--------------------------------------------------------------------------------
1 | package models.ddl
2 |
3 | case object CreateSettingsTable extends CreateTableStatement("setting_values") {
4 | override val sql = s"""
5 | create table "$tableName" (
6 | "k" varchar(256) primary key,
7 | "v" varchar(4096) not null
8 | );
9 | """
10 | }
11 |
--------------------------------------------------------------------------------
/app/models/ddl/CreateSharedResultTable.scala:
--------------------------------------------------------------------------------
1 | package models.ddl
2 |
3 | case object CreateSharedResultTable extends CreateTableStatement("shared_results") {
4 | override val sql = s"""
5 | create table "$tableName" (
6 | "id" uuid primary key,
7 | "title" varchar(512) not null,
8 | "description" varchar(4096),
9 | "owner" uuid not null,
10 | "viewable_by" varchar(128) not null,
11 | "connection_id" uuid not null,
12 | "sql" varchar(65536) not null,
13 | "source_type" varchar(256),
14 | "source_name" varchar(256),
15 | "source_sort_column" varchar(128),
16 | "source_sort_asc" boolean,
17 | "filter_column" varchar(128),
18 | "filter_op" varchar(4),
19 | "filter_type" varchar(32),
20 | "filter_value" varchar(256),
21 | "chart" text,
22 | "last_accessed" timestamp not null,
23 | "created" timestamp not null
24 | );
25 |
26 | create index "${tableName}_owner_idx" on "$tableName" ("owner");
27 | """
28 | }
29 |
--------------------------------------------------------------------------------
/app/models/ddl/CreateTableStatement.scala:
--------------------------------------------------------------------------------
1 | package models.ddl
2 |
3 | import models.database.Statement
4 | import models.engine.DatabaseEngine
5 | import services.database.core.MasterDatabase
6 |
7 | abstract class CreateTableStatement(val tableName: String, val eng: DatabaseEngine = {
8 | MasterDatabase.settings.map(_.engine).getOrElse(throw new IllegalStateException("Missing master database engine."))
9 | }) extends Statement
10 |
--------------------------------------------------------------------------------
/app/models/ddl/CreateUsersTable.scala:
--------------------------------------------------------------------------------
1 | package models.ddl
2 |
3 | case object CreateUsersTable extends CreateTableStatement("dbf_users") {
4 | override val sql = s"""
5 | create table "$tableName" (
6 | "id" uuid primary key,
7 | "username" varchar(256),
8 | "prefs" varchar(4096) not null,
9 | "email" varchar(1024) not null,
10 | "role" varchar(64) not null,
11 | "created" timestamp not null
12 | );
13 |
14 | create unique index "${tableName}_email_idx" on "$tableName" ("email");
15 | create unique index "${tableName}_username_idx" on "$tableName" ("username");
16 | """
17 | }
18 |
--------------------------------------------------------------------------------
/app/models/ddl/DdlQueries.scala:
--------------------------------------------------------------------------------
1 | package models.ddl
2 |
3 | import models.database.{Row, SingleRowQuery, Statement}
4 | import models.engine.DatabaseEngine
5 |
6 | object DdlQueries {
7 | case class DoesTableExist(tableName: String) extends SingleRowQuery[Boolean] {
8 | override val sql = "select count(*) as c from information_schema.tables WHERE (table_name = ? or table_name = ?);"
9 | override val values = tableName :: tableName.toUpperCase :: Nil
10 | override def map(row: Row) = row.as[Long]("c") > 0
11 | }
12 |
13 | case class TruncateTable(tableName: String) extends Statement {
14 | override val sql = s"""truncate table \"$tableName\""""
15 | }
16 |
17 | case class DropTable(tableName: String)(implicit val engine: DatabaseEngine) extends Statement {
18 | override val sql = s"drop table ${engine.cap.leftQuote}$tableName${engine.cap.rightQuote}"
19 | }
20 |
21 | def trim(s: String) = s.replaceAll("""[\s]+""", " ").trim
22 | }
23 |
--------------------------------------------------------------------------------
/app/models/forms/GraphQLForm.scala:
--------------------------------------------------------------------------------
1 | package models.forms
2 |
3 | import java.util.UUID
4 |
5 | import models.user.Permission
6 | import play.api.data.Form
7 | import play.api.data.Forms._
8 |
9 | object GraphQLForm {
10 | val form = Form(
11 | mapping(
12 | "id" -> optional(uuid),
13 | "connection" -> optional(uuid),
14 | "category" -> optional(text),
15 | "name" -> nonEmptyText,
16 | "query" -> nonEmptyText,
17 | "read" -> nonEmptyText.transform(s => Permission.withName(s), (p: Permission) => p.toString),
18 | "edit" -> nonEmptyText.transform(s => Permission.withName(s), (p: Permission) => p.toString)
19 | )(GraphQLForm.apply)(GraphQLForm.unapply)
20 | )
21 | }
22 |
23 | case class GraphQLForm(
24 | id: Option[UUID],
25 | connection: Option[UUID],
26 | category: Option[String],
27 | name: String,
28 | query: String,
29 | read: Permission,
30 | edit: Permission
31 | )
32 |
--------------------------------------------------------------------------------
/app/models/graphql/ColumnGraphQL.scala:
--------------------------------------------------------------------------------
1 | package models.graphql
2 |
3 | import models.schema.ColumnType
4 | import util.StringKeyUtils
5 |
6 | object ColumnGraphQL {
7 | def getColumnField(
8 | colName: String, description: Option[String], columnType: ColumnType, notNull: Boolean, sqlTypeName: String
9 | ) = if (notNull) {
10 | ColumnNotNullGraphQL.getColumnField(colName, description, columnType, StringKeyUtils.cleanName(colName), sqlTypeName)
11 | } else {
12 | ColumnNullableGraphQL.getColumnField(colName, description, columnType, StringKeyUtils.cleanName(colName), sqlTypeName)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/models/graphql/ConnectionGraphQLSchema.scala:
--------------------------------------------------------------------------------
1 | package models.graphql
2 |
3 | import models.connection.{ConnectionGraphQL, ConnectionSettings}
4 | import sangria.renderer.SchemaRenderer
5 | import sangria.schema._
6 |
7 | case class ConnectionGraphQLSchema(cs: ConnectionSettings) {
8 | val queryType = ObjectType(
9 | name = "Query",
10 | description = s"The main query interface for [${cs.name}].",
11 | fields = ConnectionGraphQL.queryFieldsForConnection(cs)
12 | )
13 |
14 | val mutationType = ObjectType(
15 | name = "Mutation",
16 | description = s"The main mutation interface for [${cs.name}].",
17 | fields = ConnectionGraphQL.mutationFieldsForConnection(cs)
18 | )
19 |
20 | val schema = sangria.schema.Schema(query = queryType, mutation = None, subscription = None, additionalTypes = Nil)
21 |
22 | lazy val renderedSchema = SchemaRenderer.renderSchema(schema)
23 | }
24 |
--------------------------------------------------------------------------------
/app/models/graphql/GraphQLContext.scala:
--------------------------------------------------------------------------------
1 | package models.graphql
2 |
3 | import models.user.User
4 | import util.ApplicationContext
5 |
6 | case class GraphQLContext(app: ApplicationContext, user: User)
7 |
--------------------------------------------------------------------------------
/app/models/queries/dynamic/DeleteRowStatement.scala:
--------------------------------------------------------------------------------
1 | package models.queries.dynamic
2 |
3 | import models.database.Statement
4 | import models.engine.DatabaseEngine
5 | import models.schema.Column
6 |
7 | case class DeleteRowStatement(
8 | name: String, pk: Seq[(String, String)], columns: Seq[Column], engine: DatabaseEngine
9 | ) extends Statement {
10 | private[this] def quote(s: String) = engine.cap.leftQuote + s + engine.cap.rightQuote
11 |
12 | private[this] val pkColumns = columns.flatMap(col => pk.find(_._1 == col.name).map(col -> _._2))
13 | private[this] val pkValues = pkColumns.map(x => ColumnValueParser.parse(x._1.columnType, x._2))
14 | private[this] val pkClause = pkColumns.zip(pkValues).map { x =>
15 | val placeholder = x._2 match {
16 | case Right(_) => "?"
17 | case Left(ex) => ex
18 | }
19 | s"${quote(x._1._1.name)} = $placeholder"
20 | }.mkString(" and ")
21 |
22 | override val sql = s"delete from ${quote(name)} where $pkClause"
23 |
24 | override val values: Seq[Any] = pkValues.flatMap {
25 | case Right(x) => Some(x)
26 | case Left(_) => None
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/models/queries/dynamic/InsertRowStatement.scala:
--------------------------------------------------------------------------------
1 | package models.queries.dynamic
2 |
3 | import models.database.Statement
4 | import models.engine.DatabaseEngine
5 | import models.schema.Column
6 |
7 | case class InsertRowStatement(name: String, params: Map[String, String], columns: Seq[Column], engine: DatabaseEngine) extends Statement {
8 | private[this] def quote(s: String) = engine.cap.leftQuote + s + engine.cap.rightQuote
9 | private[this] val activeColumns = columns.flatMap(col => params.get(col.name).map(col -> _))
10 | private[this] val parsedValues = activeColumns.map(x => ColumnValueParser.parse(x._1.columnType, x._2))
11 |
12 | override val sql = {
13 | val columns = activeColumns.map(c => quote(c._1.name)).mkString(", ")
14 | val placeholders = parsedValues.map {
15 | case Right(_) => "?"
16 | case Left(x) => x
17 | }.mkString(", ")
18 | s"insert into ${quote(name)} ($columns) values ($placeholders)"
19 | }
20 |
21 | override val values: Seq[Any] = parsedValues.flatMap {
22 | case Right(x) => Some(x)
23 | case Left(_) => None
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/models/queries/export/CsvExportQuery.scala:
--------------------------------------------------------------------------------
1 | package models.queries.export
2 |
3 | import java.io.OutputStream
4 |
5 | import com.github.tototoshi.csv.CSVWriter
6 | import models.database.{Query, Row}
7 |
8 | case class CsvExportQuery(override val sql: String, override val values: Seq[Any], out: OutputStream) extends Query[Unit] {
9 | override def reduce(rows: Iterator[Row]) = {
10 | val writer = CSVWriter.open(out)
11 | if (rows.hasNext) {
12 | val firstRow = rows.next()
13 | val md = firstRow.rs.getMetaData
14 | val cc = md.getColumnCount
15 | val columns = (1 to cc).map(md.getColumnLabel)
16 | writer.writeRow(columns)
17 |
18 | val firstRowData = (1 to cc).map(i => firstRow.asOpt[Any](i).fold("")(_.toString))
19 | writer.writeRow(firstRowData)
20 |
21 | rows.foreach { row =>
22 | val data = (1 to cc).map(i => row.asOpt[Any](i).fold("")(_.toString))
23 | writer.writeRow(data)
24 | }
25 | }
26 | writer.close()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/models/queries/result/InsertResultRow.scala:
--------------------------------------------------------------------------------
1 | package models.queries.result
2 |
3 | import models.database.Statement
4 | import models.engine.DatabaseEngine
5 |
6 | case class InsertResultRow(tableName: String, columns: Seq[String], override val values: Seq[Any])(implicit engine: DatabaseEngine) extends Statement {
7 | override val sql = {
8 | val quotedName = engine.cap.leftQuote + tableName + engine.cap.rightQuote
9 | s"""insert into $quotedName (
10 | ${columns.map(engine.cap.leftQuote + _ + engine.cap.rightQuote).mkString(", ")}
11 | ) values (
12 | ${values.map(_ => "?").mkString(", ")}
13 | )
14 | """
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/models/queries/settings/SettingQueries.scala:
--------------------------------------------------------------------------------
1 | package models.queries.settings
2 |
3 | import models.database.{Row, Statement}
4 | import models.queries.BaseQueries
5 | import models.settings.{Setting, SettingKey}
6 |
7 | object SettingQueries extends BaseQueries[Setting] {
8 | override protected val tableName = "setting_values"
9 | override protected def idColumns = Seq("k")
10 | override protected val columns = Seq("k", "v")
11 | override protected val searchColumns = columns
12 |
13 | val insert = Insert
14 | def removeById(k: SettingKey) = RemoveById(Seq(k.toString))
15 | val getAll = GetAll
16 | def getById(k: SettingKey) = GetById(Seq(k.toString))
17 | val search = Search
18 |
19 | case class Update(s: Setting) extends Statement {
20 | override val sql = {
21 | updateSql(Seq("v"))
22 | }
23 | override val values = Seq[Any](s.value, s.key.toString)
24 | }
25 |
26 | override protected def fromRow(row: Row) = Setting(SettingKey.withNameOption(row.as[String]("k")).getOrElse(SettingKey.Invalid), row.as[String]("v"))
27 | override protected def toDataSeq(s: Setting) = Seq[Any](s.key.toString, s.value)
28 | }
29 |
--------------------------------------------------------------------------------
/app/models/result/CachedResult.scala:
--------------------------------------------------------------------------------
1 | package models.result
2 |
3 | import java.util.UUID
4 |
5 | import org.joda.time.LocalDateTime
6 | import util.DateUtils
7 |
8 | case class CachedResult(
9 | resultId: UUID,
10 | queryId: UUID,
11 | connectionId: UUID,
12 | owner: UUID,
13 | status: String = "starting",
14 | source: Option[String],
15 | sql: String,
16 | columns: Int = 0,
17 | rows: Int = 0,
18 | firstMessage: Int = 0,
19 | duration: Int = 0,
20 | lastAccessed: LocalDateTime = DateUtils.now,
21 | created: LocalDateTime = DateUtils.now
22 | ) {
23 | lazy val tableName = "result_" + resultId.toString.replaceAllLiterally("-", "")
24 | }
25 |
--------------------------------------------------------------------------------
/app/models/result/CachedResultTransform.scala:
--------------------------------------------------------------------------------
1 | package models.result
2 |
3 | import models.query.QueryResult
4 | import models.schema.ColumnType
5 |
6 | object CachedResultTransform {
7 | def transform(columns: Seq[QueryResult.Col], data: Seq[Option[Any]]) = columns.zip(data).map {
8 | case x if x._1.t == ColumnType.DateType && x._2.exists(_.isInstanceOf[String]) => x._2.map(_.toString.stripSuffix(" 00:00:00"))
9 | case x if x._1.t == ColumnType.StringType && x._2.exists(_.isInstanceOf[String]) => x._2.map { s =>
10 | val str = s.toString
11 | if (str.length > x._1.precision.getOrElse(Int.MaxValue)) {
12 | str.substring(0, x._1.precision.getOrElse(Int.MaxValue))
13 | } else {
14 | str
15 | }
16 | }
17 | case x => x._2
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/models/result/QueryResultRow.scala:
--------------------------------------------------------------------------------
1 | package models.result
2 |
3 | case class QueryResultRow(columns: Seq[String], data: Seq[Option[String]]) {
4 | private[this] val columnIndexes = columns.zipWithIndex.toMap
5 | private[this] def colIndex(col: String) = columnIndexes.getOrElse(col, throw new IllegalStateException(s"Invalid column [$col]."))
6 |
7 | def getCell(col: String) = data(colIndex(col))
8 | def getRequiredCell(col: String) = getCell(col).getOrElse(throw new IllegalStateException(s"Null value for column [$col]."))
9 | }
10 |
--------------------------------------------------------------------------------
/app/models/settings/ExportModel.scala:
--------------------------------------------------------------------------------
1 | package models.settings
2 |
3 | import models.connection.ConnectionSettings
4 | import models.query.SavedQuery
5 |
6 | case class ExportModel(settings: Seq[Setting], connections: Seq[ConnectionSettings], savedQueries: Seq[SavedQuery])
7 |
--------------------------------------------------------------------------------
/app/models/settings/Setting.scala:
--------------------------------------------------------------------------------
1 | package models.settings
2 |
3 | case class Setting(key: SettingKey, value: String) {
4 | lazy val isDefault = value == key.default
5 | override def toString = s"$key=$value"
6 | lazy val asBool = value == "true"
7 | }
8 |
--------------------------------------------------------------------------------
/app/models/user/ProfileData.scala:
--------------------------------------------------------------------------------
1 | package models.user
2 |
3 | import models.template.Theme
4 |
5 | case class ProfileData(
6 | username: String,
7 | theme: Theme
8 | )
9 |
--------------------------------------------------------------------------------
/app/models/user/RegistrationData.scala:
--------------------------------------------------------------------------------
1 | package models.user
2 |
3 | case class RegistrationData(
4 | username: String = "",
5 | email: String = "",
6 | password: String = "",
7 | passwordConfirm: String = ""
8 | )
9 |
--------------------------------------------------------------------------------
/app/models/user/User.scala:
--------------------------------------------------------------------------------
1 | package models.user
2 |
3 | import java.util.UUID
4 |
5 | import com.mohiva.play.silhouette.api.{Identity, LoginInfo}
6 | import org.joda.time.LocalDateTime
7 | import util.DateUtils
8 |
9 | object User {
10 | val mock = User(UUID.fromString("11111111-1111-1111-1111-111111111111"), "Test User", UserPreferences.empty, LoginInfo("anonymous", "guest"))
11 | }
12 |
13 | case class User(
14 | id: UUID,
15 | username: String,
16 | preferences: UserPreferences,
17 | profile: LoginInfo,
18 | role: Role = Role.User,
19 | created: LocalDateTime = DateUtils.now
20 | ) extends Identity {
21 | def isAdmin = role == Role.Admin
22 | }
23 |
--------------------------------------------------------------------------------
/app/models/user/UserForms.scala:
--------------------------------------------------------------------------------
1 | package models.user
2 |
3 | import com.mohiva.play.silhouette.api.util.Credentials
4 | import models.template.Theme
5 | import play.api.data.Forms._
6 | import play.api.data._
7 |
8 | object UserForms {
9 | val signInForm = Form(mapping(
10 | "email" -> email,
11 | "password" -> nonEmptyText
12 | )(Credentials.apply)(Credentials.unapply))
13 |
14 | val registrationForm = Form(mapping(
15 | "username" -> nonEmptyText,
16 | "email" -> nonEmptyText,
17 | "password" -> nonEmptyText,
18 | "passwordConfirm" -> nonEmptyText
19 | )(RegistrationData.apply)(RegistrationData.unapply))
20 |
21 | val profileForm = Form(mapping(
22 | "username" -> nonEmptyText,
23 | "theme" -> nonEmptyText.transform(
24 | (s) => Theme.withName(s),
25 | (t: Theme) => t.toString
26 | )
27 | )(ProfileData.apply)(ProfileData.unapply))
28 |
29 | case class PasswordChange(oldPassword: String, newPassword: String, confirm: String)
30 |
31 | val changePasswordForm = Form(mapping(
32 | "old" -> nonEmptyText,
33 | "new" -> nonEmptyText,
34 | "confirm" -> nonEmptyText
35 | )(PasswordChange.apply)(PasswordChange.unapply))
36 | }
37 |
--------------------------------------------------------------------------------
/app/models/user/UserProfile.scala:
--------------------------------------------------------------------------------
1 | package models.user
2 |
3 | import java.util.UUID
4 |
5 | import org.joda.time.LocalDateTime
6 |
7 | case class UserProfile(
8 | id: UUID,
9 | username: String,
10 | email: String,
11 | role: String,
12 | created: LocalDateTime
13 | )
14 |
--------------------------------------------------------------------------------
/app/services/config/DatabaseConfig.scala:
--------------------------------------------------------------------------------
1 | package services.config
2 |
3 | import com.typesafe.config.Config
4 | import models.engine.DatabaseEngine
5 | import models.engine.DatabaseEngine.H2
6 |
7 | import scala.util.control.NonFatal
8 |
9 | object DatabaseConfig {
10 | def fromConfig(cfg: Config) = {
11 | val engine = try {
12 | DatabaseEngine.withName(cfg.getString("db"))
13 | } catch {
14 | case NonFatal(_) => H2
15 | }
16 | val url = try {
17 | cfg.getString("url")
18 | } catch {
19 | case NonFatal(_) => "default"
20 | }
21 | val username = try {
22 | cfg.getString("username")
23 | } catch {
24 | case NonFatal(_) => "databaseflow"
25 | }
26 | val password = try {
27 | cfg.getString("password")
28 | } catch {
29 | case NonFatal(_) => "flow"
30 | }
31 | DatabaseConfig(engine, url, username, password)
32 | }
33 | }
34 |
35 | case class DatabaseConfig(engine: DatabaseEngine, url: String, username: String, password: String)
36 |
--------------------------------------------------------------------------------
/app/services/database/MasterDdl.scala:
--------------------------------------------------------------------------------
1 | package services.database
2 |
3 | import models.database.Queryable
4 | import models.ddl._
5 | import util.Logging
6 |
7 | object MasterDdl extends Logging {
8 | val tables = Seq(
9 | CreateUsersTable,
10 | CreatePasswordInfoTable,
11 |
12 | CreateConnectionsTable,
13 | CreateSavedQueriesTable,
14 |
15 | CreateQueryResultsTable,
16 | CreateSharedResultTable,
17 |
18 | CreateGraphQLTable,
19 |
20 | CreateSettingsTable,
21 | CreateAuditRecordTable
22 | )
23 |
24 | def update(q: Queryable) = {
25 | tables.foreach { t =>
26 | val exists = q.query(DdlQueries.DoesTableExist(t.tableName))
27 | if (!exists) {
28 | log.info(s"Creating missing table [${t.tableName}].")
29 | q.executeUpdate(t)
30 | }
31 | }
32 | }
33 |
34 | def wipe(q: Queryable) = {
35 | log.warn("Wiping database schema.")
36 | val tableNames = tables.reverseMap(_.tableName)
37 | tableNames.map { tableName =>
38 | q.executeUpdate(DdlQueries.TruncateTable(tableName))
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/services/database/core/MasterDatabase.scala:
--------------------------------------------------------------------------------
1 | package services.database.core
2 |
3 | import java.util.UUID
4 |
5 | import models.database._
6 | import util.Logging
7 |
8 | object MasterDatabase extends CoreDatabase with Logging {
9 | override val connectionId = UUID.fromString("00000000-0000-0000-0000-000000000000")
10 | override val name = s"${util.Config.projectName} Storage"
11 | override val slug = s"${util.Config.projectSlug}-storage"
12 | override val title = s"${util.Config.projectName} Storage"
13 | override val description = s"Internal storage used by ${util.Config.projectName}."
14 | override val configKey = "master"
15 | override val dbName = "databaseflow"
16 |
17 | def query[A](q: RawQuery[A]) = conn.query(q)
18 | def executeUnknown[A](q: Query[A], resultId: Option[UUID] = None) = conn.executeUnknown(q, resultId)
19 | def executeUpdate(s: Statement) = conn.executeUpdate(s)
20 | def transaction[A](f: Transaction => A) = conn.transaction(f)
21 | }
22 |
--------------------------------------------------------------------------------
/app/services/database/core/ResultCacheDatabase.scala:
--------------------------------------------------------------------------------
1 | package services.database.core
2 |
3 | import java.util.UUID
4 |
5 | object ResultCacheDatabase extends CoreDatabase {
6 | override val connectionId = UUID.fromString("11111111-1111-1111-1111-111111111111")
7 | override val name = "Result Cache"
8 | override val slug = "result-cache"
9 | override val title = s"${util.Config.projectName} Result Cache"
10 | override val description = s"Storage used by ${util.Config.projectName} to cache query results."
11 | override val configKey = "resultCache"
12 | override val dbName = "result-cache"
13 | }
14 |
--------------------------------------------------------------------------------
/app/services/database/ssl/SslParams.scala:
--------------------------------------------------------------------------------
1 | package services.database.ssl
2 |
3 | import java.security.KeyStore
4 |
5 | case class SslParams(identityStore: KeyStore, identityStorePassword: String, trustStore: KeyStore)
6 |
--------------------------------------------------------------------------------
/app/services/database/ssl/SslSettings.scala:
--------------------------------------------------------------------------------
1 | package services.database.ssl
2 |
3 | case class SslSettings(
4 | clientCertKeyStorePath: String,
5 | trustKeyStoreProviderPath: String,
6 | clientCertKeyStorePassword: Option[String],
7 | clientCertKeyStoreProvider: Option[String] = None,
8 | trustKeyStoreProvider: Option[String] = None
9 | )
10 |
--------------------------------------------------------------------------------
/app/services/database/transaction/TransactionProvider.scala:
--------------------------------------------------------------------------------
1 | package services.database.transaction
2 |
3 | import models.database.Transaction
4 |
5 | trait TransactionProvider {
6 | def transactionExists: Boolean
7 | def currentTransaction: Transaction
8 | def begin(transaction: Transaction): Unit
9 | def end(): Unit
10 | def rollback(): Unit
11 | }
12 |
--------------------------------------------------------------------------------
/app/services/explore/ExploreFetcherHelper.scala:
--------------------------------------------------------------------------------
1 | package services.explore
2 |
3 | import models.graphql.GraphQLContext
4 | import models.result.QueryResultRow
5 | import sangria.execution.deferred.{Fetcher, HasId}
6 |
7 | import scala.concurrent.Future
8 |
9 | object ExploreFetcherHelper {
10 | def getFetchers(schema: models.schema.Schema, hasIds: ExploreHasIdHelper.HasIds) = {
11 | hasIds.map { hasId =>
12 | hasId._2.toList match {
13 | case Nil => throw new IllegalStateException(s"Empty columns for [$hasId].")
14 | case single :: Nil => Fetcher[GraphQLContext, QueryResultRow, Int]((_, ids) => {
15 | Future.successful(Seq.empty[QueryResultRow])
16 | })(hasId._3.asInstanceOf[HasId[QueryResultRow, Int]])
17 | case _ => throw new IllegalStateException(s"Multiple columns for [$hasId].")
18 | }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/services/plan/h2/H2ParseService.scala:
--------------------------------------------------------------------------------
1 | package services.plan.h2
2 |
3 | import java.util.UUID
4 |
5 | import models.plan.PlanError
6 | import services.plan.PlanParseService
7 |
8 | object H2ParseService extends PlanParseService("h2") {
9 | override def parse(sql: String, queryId: UUID, plan: String, startMs: Long) = {
10 | Left(PlanError(
11 | queryId = queryId,
12 | sql = sql,
13 | code = "NotSupported",
14 | message = "No plan support for H2 currently.",
15 | occurred = startMs
16 | ))
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/services/plan/mysql/MySqlParseKeys.scala:
--------------------------------------------------------------------------------
1 | package services.plan.mysql
2 |
3 | object MySqlParseKeys {
4 | val keyQueryBlock = "query_block"
5 | val keyTable = "table"
6 | val keyTableName = "table_name"
7 | val keyAccessType = "access_type"
8 | val keyQuerySpecifications = "query_specifications"
9 | val keyMaterializedFromSubquery = "materialized_from_subquery"
10 | val keyAttachedCondition = "attached_condition"
11 | val keyMessage = "message"
12 | }
13 |
--------------------------------------------------------------------------------
/app/services/plan/postgres/PostgresParseHelper.scala:
--------------------------------------------------------------------------------
1 | package services.plan.postgres
2 |
3 | import io.circe.Json
4 | import models.plan.PlanNode
5 | import services.plan.postgres.PostgresParseKeys._
6 |
7 | object PostgresParseHelper {
8 | def getOutput(o: Option[Json]) = o.map { j =>
9 | j.asArray.get.map(_.asString.get)
10 | }
11 |
12 | def getCosts(params: Map[String, Json]) = PlanNode.Costs(
13 | estimatedRows = params.get(keyPlanRows) match {
14 | case Some(n) if n.isNumber => n.asNumber.get.toDouble.toInt
15 | case _ => 0
16 | },
17 | actualRows = params.get(keyActualRows).map {
18 | case n if n.isNumber => n.asNumber.get.toDouble.toInt
19 | case _ => 0
20 | },
21 | duration = params.get(keyActualTotalTime).map {
22 | case n if n.isNumber => n.asNumber.get.toDouble
23 | case _ => 0
24 | },
25 | cost = params.get(keyTotalCost).map {
26 | case n if n.isNumber => n.asNumber.get.toDouble.toInt
27 | case _ => 0
28 | }
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/app/services/plan/postgres/PostgresParseKeys.scala:
--------------------------------------------------------------------------------
1 | package services.plan.postgres
2 |
3 | object PostgresParseKeys {
4 | val keyNodeType = "Node Type"
5 | val keyActualRows = "Actual Rows"
6 | val keyPlanRows = "Plan Rows"
7 | val keyActualTotalTime = "Actual Total Time"
8 | val keyActualLoops = "Actual Loops"
9 | val keyTotalCost = "Total Cost"
10 | val keyPlans = "Plans"
11 | val keyRelationName = "Relation Name"
12 | val keySchema = "Schema"
13 | val keyAlias = "Alias"
14 | val keyGroupKey = "Group Key"
15 | val keySortKey = "Sort Key"
16 | val keyJoinType = "Join Type"
17 | val keyIndexName = "Index Name"
18 | val keyHashCondition = "Hash Cond"
19 | val keyOutput = "Output"
20 |
21 | val tagPlan = "plan_"
22 | }
23 |
--------------------------------------------------------------------------------
/app/services/query/ParameterService.scala:
--------------------------------------------------------------------------------
1 | package services.query
2 |
3 | import models.query.SavedQuery
4 | import util.Logging
5 |
6 | object ParameterService extends Logging {
7 | def merge(sql: String, params: Seq[SavedQuery.Param]) = {
8 | var merged = sql
9 | params.foreach { param =>
10 | if (param.v.trim.nonEmpty) {
11 | var idx = Math.max(merged.indexOf("{" + param.k + ":"), merged.indexOf("{" + param.k + "}"))
12 | while (idx > -1) {
13 | val end = merged.indexOf('}', idx) + 1
14 | merged = merged.replaceAllLiterally(merged.substring(idx, end), param.v)
15 | idx = Math.max(merged.indexOf("{" + param.k + ":"), merged.indexOf("{" + param.k + "}"))
16 | }
17 | }
18 | }
19 | merged
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/services/query/ProcedureService.scala:
--------------------------------------------------------------------------------
1 | package services.query
2 |
3 | import java.util.UUID
4 |
5 | import services.schema.SchemaService
6 | import util.Logging
7 |
8 | object ProcedureService extends Logging {
9 | def callProcedure(connectionId: UUID, userId: UUID, queryId: UUID, name: String, params: Map[String, String], resultId: UUID) = {
10 | SchemaService.getProcedure(connectionId, name) match {
11 | case Some(proc) =>
12 | log.info(s"Calling [${proc.name}(${proc.getValues(params).map(x => x._1 + " = " + x._2).mkString(", ")})].")
13 | log.info(s"Connection: $connectionId, User: $userId, Query: $queryId, Result: $resultId.")
14 | case None => throw new IllegalStateException(s"Unknown procedure [$name].")
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/services/query/QueryResultRowService.scala:
--------------------------------------------------------------------------------
1 | package services.query
2 |
3 | import java.util.UUID
4 |
5 | import models.query.{QueryResult, RowDataOptions}
6 | import models.result.QueryResultRow
7 | import models.user.User
8 | import util.FutureUtils.defaultContext
9 |
10 | object QueryResultRowService {
11 | def getTableData(user: User, connectionId: UUID, name: String, columns: Seq[String], rdo: RowDataOptions) = {
12 | RowDataService.getRowData(user, connectionId, QueryResult.SourceType.Table, name, columns, rdo).map(transform)
13 | }
14 |
15 | def getViewData(user: User, connectionId: UUID, name: String, columns: Seq[String], rdo: RowDataOptions) = {
16 | RowDataService.getRowData(user, connectionId, QueryResult.SourceType.View, name, columns, rdo).map(transform)
17 | }
18 |
19 | private[this] def transform(result: QueryResult) = {
20 | val columns = result.columns.map(_.name)
21 | result.data.map { row =>
22 | QueryResultRow(columns, row)
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/services/result/CachedResultActor.scala:
--------------------------------------------------------------------------------
1 | package services.result
2 |
3 | import akka.actor.{Actor, Props}
4 | import org.joda.time.LocalDateTime
5 | import util.FutureUtils.defaultContext
6 | import util.Logging
7 |
8 | import scala.concurrent.duration._
9 |
10 | object CachedResultActor {
11 | case class Cleanup(since: Option[LocalDateTime])
12 | def props() = Props(new CachedResultActor())
13 | }
14 |
15 | class CachedResultActor() extends Actor with Logging {
16 | override def preStart() = {
17 | log.debug("Query result cache cleanup is scheduled to run every ten minutes.")
18 | context.system.scheduler.schedule(10.minutes, 10.minutes, self, CachedResultActor.Cleanup(None))
19 | }
20 |
21 | override def receive = {
22 | case c: CachedResultActor.Cleanup =>
23 | val ret = CachedResultService.cleanup(c.since.getOrElse(new LocalDateTime().minusDays(2)))
24 | if (ret.removed.nonEmpty || ret.orphans.nonEmpty) {
25 | log.info(ret.toString)
26 | }
27 | sender() ! ret
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/services/schema/MetadataEnums.scala:
--------------------------------------------------------------------------------
1 | package services.schema
2 |
3 | import models.database.{Query, Row}
4 | import models.engine.DatabaseEngine
5 | import models.schema.EnumType
6 | import services.database.DatabaseConnection
7 | import util.Logging
8 |
9 | object MetadataEnums extends Logging {
10 | case object EnumQuery extends Query[Seq[EnumType]] {
11 | override def sql = """
12 | select t.typname, e.enumlabel
13 | from pg_enum e
14 | join pg_type t on e.enumtypid = t.oid
15 | where t.typname != 'myenum'
16 | order by t.typname, e.enumsortorder
17 | """
18 | override def reduce(rows: Iterator[Row]) = rows.map { row =>
19 | (row.as[String]("typname"), row.as[String]("enumlabel"))
20 | }.toSeq.groupBy(_._1).map(e => EnumType(e._1, e._2.map(_._2))).toSeq
21 | }
22 |
23 | def getEnums(db: DatabaseConnection) = db.engine match {
24 | case DatabaseEngine.PostgreSQL => db.query(EnumQuery)
25 | case _ => Seq.empty[EnumType]
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/services/schema/MetadataIdentifiers.scala:
--------------------------------------------------------------------------------
1 | package services.schema
2 |
3 | import java.sql.DatabaseMetaData
4 |
5 | import models.database.Row
6 |
7 | object MetadataIdentifiers {
8 | def getRowIdentifier(metadata: DatabaseMetaData, catalog: Option[String], schema: Option[String], name: String) = {
9 | val rs = metadata.getBestRowIdentifier(catalog.orNull, schema.orNull, name, DatabaseMetaData.bestRowSession, true)
10 | new Row.Iter(rs).map(_.as[String]("COLUMN_NAME")).toList
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/util/ApplicationHelper.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import services.config.ConfigFileService
4 | import util.FutureUtils.defaultContext
5 |
6 | import scala.concurrent.Future
7 |
8 | trait ApplicationHelper { this: ApplicationContext =>
9 | def startupComplete(debug: Boolean) = {
10 | if ((!debug) && java.awt.Desktop.isDesktopSupported) {
11 | Future {
12 | Thread.sleep(2000)
13 | java.awt.Desktop.getDesktop.browse(new java.net.URI("http://localhost:4260"))
14 | }
15 | }
16 |
17 | log.warn(s"${util.Config.projectName} started.")
18 | if (ConfigFileService.isDocker) {
19 | log.warn(" - Head to http://[docker address]:4260 to get started!")
20 | log.warn(" - Since this is a docker container, you'll need to expose port 4260, by using the command flag [-p 4260:4260].")
21 | } else {
22 | log.warn(" - Head to http://localhost:4260 to get started!")
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/util/ExceptionUtils.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import akka.actor.ActorRef
4 | import models.ServerError
5 |
6 | object ExceptionUtils extends Logging {
7 | def actorErrorFunction(out: ActorRef, key: String, t: Throwable) = {
8 | log.warn(s"Error [$key] encountered of type [${t.getClass.getSimpleName}].", t)
9 | out ! ServerError(key, t.getMessage)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/app/util/FutureUtils.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import java.util.concurrent.Executors
4 |
5 | import scala.concurrent.ExecutionContext
6 |
7 | object FutureUtils {
8 | implicit val defaultContext = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(16))
9 | }
10 |
--------------------------------------------------------------------------------
/app/util/JsonUtils.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import io.circe.Json
4 |
5 | object JsonUtils {
6 | def toString(v: Json): String = v match {
7 | case s if s.isString => s.asString.get
8 | case n if n.isNumber => n.asNumber.get.toDouble.toString
9 | case a if a.isArray => "[" + a.asArray.get.map(v => toString(v)).mkString(", ") + "]"
10 | case b if b.isBoolean => b.asBoolean.get.toString
11 | case o if o.isObject => "{" + toStringMap(o.asObject.get.toMap).map(x => s"${x._1}: ${x._2}").mkString(", ") + "}"
12 | case x => throw new IllegalStateException(s"Invalid param type [${x.getClass.getName}].")
13 | }
14 |
15 | def toStringMap(params: Map[String, Json]): Map[String, String] = params.map(p => p._1 -> toString(p._2))
16 | }
17 |
--------------------------------------------------------------------------------
/app/util/Logging.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import org.slf4j.LoggerFactory
4 | import play.api.Logger
5 |
6 | trait Logging {
7 | protected[this] val log = new Logger(LoggerFactory.getLogger(getClass))
8 | }
9 |
--------------------------------------------------------------------------------
/app/util/SlugUtils.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import java.net.URLEncoder
4 |
5 | object SlugUtils {
6 | def slugFor(title: String) = URLEncoder.encode(title.toLowerCase.replaceAllLiterally(" ", "-"), "utf-8")
7 | }
8 |
--------------------------------------------------------------------------------
/app/util/web/ErrorHandler.scala:
--------------------------------------------------------------------------------
1 | package util.web
2 |
3 | import javax.inject._
4 |
5 | import play.api.http.DefaultHttpErrorHandler
6 | import play.api._
7 | import play.api.mvc._
8 | import play.api.routing.Router
9 | import util.Logging
10 | import scala.concurrent._
11 |
12 | class ErrorHandler @Inject() (
13 | env: Environment, config: Configuration, sourceMapper: OptionalSourceMapper, router: Provider[Router]
14 | ) extends DefaultHttpErrorHandler(env, config, sourceMapper, router) with Logging {
15 |
16 | override def onProdServerError(request: RequestHeader, ex: UsefulException) = Future.successful(
17 | Results.InternalServerError(views.html.error.serverError(request.path, Some(ex))(request.session, request.flash))
18 | )
19 |
20 | override def onClientError(request: RequestHeader, statusCode: Int, message: String) = Future.successful(
21 | Results.NotFound(views.html.error.notFound(request.path)(request.session, request.flash))
22 | )
23 |
24 | override protected def onBadRequest(request: RequestHeader, error: String) = Future.successful(
25 | Results.BadRequest(views.html.error.badRequest(request.path, error)(request.session, request.flash))
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/app/util/web/FormUtils.scala:
--------------------------------------------------------------------------------
1 | package util.web
2 |
3 | import play.api.data.FormError
4 | import play.api.mvc.{AnyContent, Request}
5 |
6 | object FormUtils {
7 | def getForm(request: Request[AnyContent]) = request.body.asFormUrlEncoded match {
8 | case Some(f) => f.mapValues(x => x.mkString(","))
9 | case None => throw new IllegalStateException("Missing form post.")
10 | }
11 |
12 | def errorsToString(errors: Seq[FormError]) = errors.map(e => e.key + ": " + e.message).mkString(", ")
13 | }
14 |
--------------------------------------------------------------------------------
/app/util/web/MessageFrameFormatter.scala:
--------------------------------------------------------------------------------
1 | package util.web
2 |
3 | import models.{RequestMessage, ResponseMessage}
4 | import play.api.mvc.WebSocket.MessageFlowTransformer
5 | import util.Logging
6 |
7 | import util.JsonSerializers._
8 |
9 | class MessageFrameFormatter(debug: Boolean) extends Logging {
10 | val stringTransformer = MessageFlowTransformer.stringMessageFlowTransformer.map(s => decodeJson[RequestMessage](s) match {
11 | case Right(x) => x
12 | case Left(err) => throw err
13 | }).contramap { rm: ResponseMessage => rm.asJson.spaces2 }
14 |
15 | def transformer(binary: Boolean) = stringTransformer
16 | }
17 |
--------------------------------------------------------------------------------
/app/util/web/RequestHandler.scala:
--------------------------------------------------------------------------------
1 | package util.web
2 |
3 | import javax.inject.Inject
4 | import play.api.http._
5 | import play.api.mvc.RequestHeader
6 | import play.api.routing.Router
7 | import play.core.DefaultWebCommands
8 | import util.Logging
9 |
10 | class RequestHandler @Inject() (
11 | errorHandler: HttpErrorHandler,
12 | configuration: HttpConfiguration,
13 | filters: HttpFilters,
14 | router: Router
15 | ) extends DefaultHttpRequestHandler(new DefaultWebCommands, None, router, errorHandler, configuration, filters.filters) with Logging {
16 |
17 | override def routeRequest(request: RequestHeader) = {
18 | if (!Option(request.path).exists(_.startsWith("/assets"))) {
19 | log.info(s"Request from [${request.remoteAddress}]: ${request.toString()}")
20 | }
21 | super.routeRequest(request)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/util/web/WebFilters.scala:
--------------------------------------------------------------------------------
1 | package util.web
2 |
3 | import javax.inject.Inject
4 |
5 | import play.api.http.HttpFilters
6 | import play.filters.gzip.GzipFilter
7 |
8 | class WebFilters @Inject() (customLoggingFilter: LoggingFilter, gzipFilter: GzipFilter) extends HttpFilters {
9 | override def filters = Seq(customLoggingFilter, gzipFilter)
10 | }
11 |
--------------------------------------------------------------------------------
/app/views/admin/sandbox/list.scala.html:
--------------------------------------------------------------------------------
1 | @(user: models.user.User)(implicit request: Request[AnyContent], session: Session, flash: Flash, message: Messages)
2 | @layout.simple(Some(user), "Sandbox List") {
3 |
4 |
5 |
6 |
Sandbox Actions
7 |
8 |
9 | @models.sandbox.SandboxTask.values.map { s =>
10 | -
11 | @s.name
12 |
@s.description
13 |
14 | }
15 |
16 |
17 |
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/app/views/admin/sandbox/metrics.scala.html:
--------------------------------------------------------------------------------
1 | @(json: String)
2 | @json
3 |
--------------------------------------------------------------------------------
/app/views/admin/sandbox/run.scala.html:
--------------------------------------------------------------------------------
1 | @(user: models.user.User, s: models.sandbox.SandboxTask, result: models.sandbox.SandboxTask.Result)(
2 | implicit request: Request[AnyContent], session: Session, flash: Flash, message: Messages
3 | )@layout.simple(user = Some(user), title = s"${s.name} Result") {
4 |
5 |
6 |
7 |
8 |
@result.status [@{result.elapsed}ms]
9 | @s.name
10 |
11 |
@result.task.description
12 |
@result.result
13 |
14 |
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/app/views/components/formErrors.scala.html:
--------------------------------------------------------------------------------
1 | @(errors: Seq[FormError])(implicit messages: Messages)@if(errors.nonEmpty) {
2 |
3 |
4 |
5 | @errors.map { error =>
6 |
7 | @error.key: @messages(error.message, error.args: _*)
8 |
9 | }
10 |
11 |
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/app/views/components/motd.scala.html:
--------------------------------------------------------------------------------
1 | @()@defining(services.settings.SettingsService(models.settings.SettingKey.MessageOfTheDay)) { message =>
2 | @if(message.trim.nonEmpty) {
3 |
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/app/views/components/permissionsPanel.scala.html:
--------------------------------------------------------------------------------
1 | @(title: String, fieldName: String, key: String, showPublic: Boolean = false)(implicit messages: Messages)
2 |
3 |
@title
4 | @if(showPublic) {
5 |
6 |
7 |
}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/app/views/error/badRequest.scala.html:
--------------------------------------------------------------------------------
1 | @(path: String, error: String)(implicit session: Session, flash: Flash)@layout.materialize(None, "Page Not Found") {
2 |
3 |
@path
4 |
Bad Request: @error
5 |
6 | }
7 |
--------------------------------------------------------------------------------
/app/views/error/notFound.scala.html:
--------------------------------------------------------------------------------
1 | @(path: String)(implicit session: Session, flash: Flash)@layout.materialize(None, "Page Not Found") {
2 |
3 |
@path
4 |
Page Not Found
5 |
6 | }
7 |
--------------------------------------------------------------------------------
/app/views/error/serverError.scala.html:
--------------------------------------------------------------------------------
1 | @(path: String, ex: Option[Throwable])(implicit session: Session, flash: Flash)@layout.materialize(None, "Server Error") {
2 |
3 |
@path
4 |
Exception Encountered
5 | @ex.map { x => }
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/app/views/layout/materialize.scala.html:
--------------------------------------------------------------------------------
1 | @(user: Option[models.user.User], title: String, scripts: Seq[String] = Nil, stylesheets: Seq[String] = Nil)(content: Html)(
2 | implicit session: Session, flash: Flash
3 | )@basic(
4 | user,
5 | title,
6 | scripts = Seq(
7 | routes.Assets.versioned("vendor/jquery/jquery.min.js").url,
8 | routes.Assets.versioned("vendor/materialize/materialize.min.js").url
9 | ) ++ scripts,
10 | stylesheets = Seq(
11 | routes.Assets.versioned("vendor/fontawesome/font-awesome.min.css").url,
12 | routes.Assets.versioned("vendor/materialize/materialize.min.css").url,
13 | controllers.routes.Assets.versioned("stylesheets/databaseflow.min.css").url
14 | ) ++ stylesheets
15 | )(content)
16 |
--------------------------------------------------------------------------------
/app/views/maintenance.scala.html:
--------------------------------------------------------------------------------
1 | @()(
2 | implicit request: Request[AnyContent], session: Session, flash: Flash, messages: Messages
3 | )@layout.simple(None, util.Config.projectName) {
4 |
5 |
6 |
7 |
Demo Unavailable
8 |
We're sorry, but the demo just wasn't on a server big enough for all this traffic.
9 |
Please visit databaseflow.com to download a trial and discover Database Flow.
10 |
11 |
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/app/views/modal/columnModal.scala.html:
--------------------------------------------------------------------------------
1 | @()(implicit messages: Messages)
10 |
--------------------------------------------------------------------------------
/app/views/modal/confirmModal.scala.html:
--------------------------------------------------------------------------------
1 | @()(implicit messages: Messages)
11 |
--------------------------------------------------------------------------------
/app/views/modal/planNodeModal.scala.html:
--------------------------------------------------------------------------------
1 | @()(implicit messages: Messages)
10 |
--------------------------------------------------------------------------------
/app/views/modal/reconnectModal.scala.html:
--------------------------------------------------------------------------------
1 | @()(implicit messages: Messages)
2 |
3 |
@messages("modal.reconnect.prompt")
4 |
5 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/app/views/modal/rowDetailModal.scala.html:
--------------------------------------------------------------------------------
1 | @()(implicit messages: Messages)
12 |
--------------------------------------------------------------------------------
/app/views/modal/rowUpdateModal.scala.html:
--------------------------------------------------------------------------------
1 | @()(implicit messages: Messages)
11 |
--------------------------------------------------------------------------------
/app/views/query/navbar.scala.html:
--------------------------------------------------------------------------------
1 | @(user: Option[models.user.User], dbName: String, txSupported: Boolean)(implicit request: Request[AnyContent], session: Session, flash: Flash, messages: Messages)
2 |
3 | @views.html.components.userDropdown(user)
4 |
5 |
6 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/views/result/export.scala.html:
--------------------------------------------------------------------------------
1 | @(result: models.query.SharedResult)
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | @defining(controllers.query.routes.QueryController.main(result.connectionId.toString).url + "#sql-" + java.net.URLEncoder.encode(result.sql, "UTF-8")) { href =>
11 |
12 |
13 |
14 | }
15 |
16 |
24 |
--------------------------------------------------------------------------------
/app/views/result/title.scala.html:
--------------------------------------------------------------------------------
1 | @(result: models.query.SharedResult, username: String)
2 | @result.title
3 |
4 | Created @util.DateUtils.niceDate(util.DateUtils.fromMillis(result.created).toLocalDate) by @username
5 |
6 |
9 |
--------------------------------------------------------------------------------
/app/views/schema/mermaid.scala.html:
--------------------------------------------------------------------------------
1 | @(user: models.user.User, connectionId: String, connectionName: String, chartData: String)(
2 | implicit request: Request[AnyContent], session: Session, flash: Flash, messages: Messages
3 | )@layout.simple(
4 | user = Some(user),
5 | title = connectionName + " Schema",
6 | mainDivClass = "mermaid-container",
7 | scripts = Seq(
8 | routes.Assets.versioned("vendor/mermaid/mermaid.min.js").url,
9 | routes.Assets.versioned("vendor/mermaid/svg-pan-zoom.min.js").url
10 | ),
11 | stylesheets = Seq(routes.Assets.versioned("stylesheets/mermaid.min.css").url)
12 | ) {
13 | @Html(chartData)
14 |
32 | }
33 |
--------------------------------------------------------------------------------
/bin/docker.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
4 | project_dir=${dir}
5 | cd $project_dir/..
6 |
7 | sbt docker:publishLocal
8 | docker save --output=./target/databaseflow.docker databaseflow
9 | gzip ./target/databaseflow.docker
10 |
--------------------------------------------------------------------------------
/bin/publish.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
4 | project_dir=${dir}
5 | cd $project_dir/..
6 |
7 | sbt dist
8 |
9 | rm -rf ./tmp/databaseflow
10 |
11 | unzip ./target/universal/databaseflow.zip -d ./target/universal/databaseflow
12 | mv ./target/universal/databaseflow ./tmp/
13 |
14 | rsync -zrv --delete ./tmp/databaseflow/* kyle@demo.databaseflow.com:~/apps/demo.databaseflow.com
15 |
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | // Database Flow build file. See `./project` for definitions.
2 |
3 | useGpg := true
4 |
5 | pgpSecretRing := file("/Users/kyle/.gnupg/pubring.kbx")
6 |
7 | lazy val doc = Documentation.doc
8 |
9 | lazy val sharedJs = Shared.sharedJs
10 |
11 | lazy val client = Client.client
12 |
13 | lazy val charting = Client.charting
14 |
15 | lazy val sharedJvm = Shared.sharedJvm
16 |
17 | lazy val dblibs = Database.dblibs
18 |
19 | lazy val server = Server.server
20 |
--------------------------------------------------------------------------------
/charting/src/main/scala/models/charting/ChartColumn.scala:
--------------------------------------------------------------------------------
1 | package models.charting
2 |
3 | import scala.scalajs.js
4 |
5 | @js.native
6 | trait ChartColumn extends js.Object {
7 | //def name: String = js.native
8 | //def `type`: String = js.native
9 | def parse(text: String): js.Any = js.native
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/charting/src/main/scala/models/charting/ChartSettings.scala:
--------------------------------------------------------------------------------
1 | package models.charting
2 |
3 | import scala.scalajs.js
4 |
5 | case class ChartSettings(
6 | t: ChartType = ChartType.Line,
7 | selects: Map[String, String] = Map.empty,
8 | flags: Map[String, Boolean] = Map.empty
9 | ) {
10 | def merge(s: ChartSettings) = this.copy(selects = selects ++ s.selects, flags = flags ++ s.flags)
11 |
12 | lazy val asJson = {
13 | val selectsJson = selects.map(s => s""""${s._1}": "${s._2}"""").mkString(", ")
14 | val flagsJson = flags.map(s => s""""${s._1}": ${s._2}""").mkString(", ")
15 | s"""{"t": "${t.id}", "selects": { $selectsJson }, "flags": { $flagsJson } }"""
16 | }
17 | }
18 |
19 | object ChartSettings {
20 | def fromJs(settings: js.Dynamic) = {
21 | val t = ChartType.withNameOption(settings.t.toString) match {
22 | case Some(x) => x
23 | case None => ChartType.Line
24 | }
25 | val selects = settings.selects.asInstanceOf[js.Dictionary[String]].toMap
26 | val flags = settings.flags.asInstanceOf[js.Dictionary[Boolean]].toMap
27 | ChartSettings(t = t, selects = selects, flags = flags)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/charting/src/main/scala/models/charting/options/BoxPlotOptions.scala:
--------------------------------------------------------------------------------
1 | package models.charting.options
2 |
3 | import models.charting.ChartSettings
4 |
5 | import scala.scalajs.js
6 |
7 | case object BoxPlotOptions extends ChartOptions {
8 | override val selects = Seq("x" -> "X", "y" -> "Y")
9 | override val flags = Seq(
10 | ("statistics", "Statistics", false),
11 | ("horizontal", "Horizontal", false)
12 | )
13 |
14 | override def getJsData(settings: ChartSettings, columns: Seq[(String, String)], data: js.Array[js.Array[String]]) = Seq(
15 | js.Dynamic.literal(
16 | "x" -> getDataColumn("x", settings, columns, data),
17 | "y" -> getDataColumn("y", settings, columns, data),
18 | "type" -> "box"
19 | )
20 | )
21 |
22 | override def getJsOptions(settings: ChartSettings) = js.Dynamic.literal()
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/charting/src/main/scala/models/charting/options/ChartOptions.scala:
--------------------------------------------------------------------------------
1 | package models.charting.options
2 |
3 | import models.charting.ChartSettings
4 |
5 | import scala.scalajs.js
6 |
7 | trait ChartOptions {
8 | def selects: Seq[(String, String)]
9 | def flags: Seq[(String, String, Boolean)] = Nil
10 |
11 | def getJsData(settings: ChartSettings, columns: Seq[(String, String)], data: js.Array[js.Array[String]]): Seq[js.Dynamic]
12 | def getJsOptions(settings: ChartSettings): js.Dynamic
13 |
14 | protected[this] def getDataColumn(key: String, settings: ChartSettings, columns: Seq[(String, String)], data: js.Array[js.Array[String]]) = {
15 | settings.selects.get(key) match {
16 | case Some(_) =>
17 | val col = settings.selects.getOrElse(key, "")
18 | val idx = columns.indexWhere(_._1 == col)
19 | val ret = if (idx == -1) { js.Array() } else { data.map(_(idx)) }
20 | ret
21 | case None => js.Array()
22 | }
23 | }
24 |
25 | protected[this] def selectValue(settings: ChartSettings, key: String) = settings.selects.getOrElse(key, "")
26 | }
27 |
--------------------------------------------------------------------------------
/charting/src/main/scala/models/charting/options/HistogramOptions.scala:
--------------------------------------------------------------------------------
1 | package models.charting.options
2 |
3 | import models.charting.ChartSettings
4 |
5 | import scala.scalajs.js
6 |
7 | case object HistogramOptions extends ChartOptions {
8 | override val selects = Seq("x" -> "X", "text" -> "Text")
9 | override val flags = Seq(("horizontal", "Horizontal", false))
10 |
11 | override def getJsData(settings: ChartSettings, columns: Seq[(String, String)], data: js.Array[js.Array[String]]) = Seq(
12 | js.Dynamic.literal(
13 | "x" -> getDataColumn("x", settings, columns, data),
14 | "text" -> getDataColumn("text", settings, columns, data),
15 | "type" -> "histogram"
16 | )
17 | )
18 |
19 | override def getJsOptions(settings: ChartSettings) = js.Dynamic.literal()
20 | }
21 |
--------------------------------------------------------------------------------
/charting/src/main/scala/models/charting/options/PieChartOptions.scala:
--------------------------------------------------------------------------------
1 | package models.charting.options
2 |
3 | import models.charting.ChartSettings
4 |
5 | import scala.scalajs.js
6 |
7 | case object PieChartOptions extends ChartOptions {
8 | override val selects = Seq("values" -> "Values", "labels" -> "Text")
9 | override val flags = Seq(
10 | ("showLabel", "Label", false),
11 | ("showValue", "Value", false),
12 | ("showPercentage", "Percentage", true),
13 | ("sorted", "Sorted", true)
14 | )
15 |
16 | override def getJsData(settings: ChartSettings, columns: Seq[(String, String)], data: js.Array[js.Array[String]]) = Seq(
17 | js.Dynamic.literal(
18 | "values" -> getDataColumn("values", settings, columns, data),
19 | "labels" -> getDataColumn("labels", settings, columns, data),
20 | "type" -> "pie"
21 | )
22 | )
23 |
24 | override def getJsOptions(settings: ChartSettings) = js.Dynamic.literal()
25 | }
26 |
--------------------------------------------------------------------------------
/charting/src/main/scala/models/charting/options/ScatterPlotOptions.scala:
--------------------------------------------------------------------------------
1 | package models.charting.options
2 |
3 | import models.charting.ChartSettings
4 |
5 | import scala.scalajs.js
6 |
7 | case object ScatterPlotOptions extends ChartOptions {
8 | override val selects = Seq("x" -> "X", "y" -> "Y", "color" -> "Color", "text" -> "Text")
9 | override val flags = Seq()
10 |
11 | override def getJsData(settings: ChartSettings, columns: Seq[(String, String)], data: js.Array[js.Array[String]]) = Seq(
12 | js.Dynamic.literal(
13 | "x" -> getDataColumn("x", settings, columns, data),
14 | "y" -> getDataColumn("y", settings, columns, data),
15 | "color" -> getDataColumn("color", settings, columns, data),
16 | "text" -> getDataColumn("text", settings, columns, data),
17 | "mode" -> "markers",
18 | "type" -> "scatter"
19 | )
20 | )
21 |
22 | override def getJsOptions(settings: ChartSettings) = js.Dynamic.literal(
23 | "margin" -> js.Dynamic.literal("t" -> 40),
24 | "xaxis" -> js.Dynamic.literal("title" -> selectValue(settings, "x")),
25 | "yaxis" -> js.Dynamic.literal("title" -> selectValue(settings, "y"))
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/charting/src/main/scala/util/LogUtils.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import scala.scalajs.js.Dynamic.global
4 |
5 | object LogUtils {
6 | def logJs(o: scalajs.js.Any) = {
7 | global.window.debug = o
8 | global.console.log(o)
9 | }
10 |
11 | def info(msg: String): Unit = {
12 | global.console.info(msg)
13 | }
14 |
15 | def error(msg: String): Unit = {
16 | global.console.error(msg)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/client/src/main/scala/DatabaseFlowApp.scala:
--------------------------------------------------------------------------------
1 | import org.scalajs.jquery.{jQuery => $}
2 | import scribe.Logging
3 | import services.{InitService, NotificationService}
4 |
5 | import scala.scalajs.js.annotation.JSExportTopLevel
6 |
7 | @JSExportTopLevel(name = "DatabaseFlow")
8 | class DatabaseFlowApp extends Logging with NetworkHelper with ResponseMessageHelper {
9 | val debug = true
10 |
11 | InitService.init(sendMessage, connect _)
12 |
13 | logger.info("Database Flow has started.")
14 |
15 | protected[this] def handleServerError(reason: String, content: String) = {
16 | val lp = $("#loading-panel")
17 | val isLoading = lp.css("display") == "block"
18 | if (isLoading) {
19 | $("#tab-loading").text("Connection Error")
20 | val c = $("#loading-content", lp)
21 | c.text(s"Error loading database ($reason): $content")
22 | } else {
23 | NotificationService.error(reason, content)
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/client/src/main/scala/models/template/FeedbackTemplate.scala:
--------------------------------------------------------------------------------
1 | package models.template
2 |
3 | import util.Messages
4 |
5 | import scalatags.Text.all._
6 |
7 | object FeedbackTemplate {
8 | def content(email: String) = {
9 | val content = div(id := "feedback-panel")(
10 | p(Messages("feedback.notice", util.Config.projectName)),
11 | div(cls := "input-field")(
12 | input(id := "feedback-email-input", cls := "validate", `type` := "email", value := email, placeholder := Messages("feedback.email"))()
13 | ),
14 | div(cls := "input-field")(
15 | textarea(id := "feedback-content-input", cls := "materialize-textarea", placeholder := Messages("feedback.prompt"))()
16 | )
17 | )
18 |
19 | StaticPanelTemplate.row(StaticPanelTemplate.panel(
20 | content = content,
21 | iconAndTitle = Some(Icons.feedback -> span(Messages("feedback.title"))),
22 | actions = Seq(
23 | a(href := "", cls := "theme-text right submit-feedback")(Messages("feedback.submit")),
24 | div(style := "clear: both;")
25 | )
26 | ))
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/client/src/main/scala/models/template/ProgressTemplate.scala:
--------------------------------------------------------------------------------
1 | package models.template
2 |
3 | import java.util.UUID
4 |
5 | import util.Messages
6 |
7 | import scalatags.Text.all._
8 |
9 | object ProgressTemplate {
10 | def loadingPanel(queryId: UUID, title: String, resultId: UUID) = {
11 | div(id := resultId.toString, cls := s"panel progress-panel progress-$queryId")(
12 | StaticPanelTemplate.card(
13 | content = div(
14 | div("Loading for ", span(cls := "timer")("0"), " seconds..."),
15 | div(cls := "cancel-query-link", a(href := "#")(Messages("general.cancel")))
16 | ),
17 | iconAndTitle = Some(Icons.loading + " " + Icons.spin -> span(title)),
18 | showClose = false
19 | )
20 | )
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/client/src/main/scala/models/template/column/ViewColumnDetailTemplate.scala:
--------------------------------------------------------------------------------
1 | package models.template.column
2 |
3 | import models.schema.Column
4 | import util.Messages
5 |
6 | import scalatags.Text.all._
7 |
8 | object ViewColumnDetailTemplate {
9 | def columnPanel(columns: Seq[Column]) = {
10 | tableFor(columns)
11 | }
12 |
13 | private[this] def tableFor(columns: Seq[Column]) = table(cls := "bordered highlight responsive-table")(
14 | thead(tr(
15 | th(Messages("th.name")),
16 | th(Messages("th.type"))
17 | )),
18 | tbody(columns.map { col =>
19 | tr(
20 | td(ColumnTemplate.linkFor(col)),
21 | td(col.columnType.toString)
22 | )
23 | })
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/client/src/main/scala/models/template/proc/ProcedureCallTemplate.scala:
--------------------------------------------------------------------------------
1 | package models.template.proc
2 |
3 | import models.schema.ProcedureParam
4 |
5 | import scalatags.Text.all._
6 |
7 | object ProcedureCallTemplate {
8 | private[this] def forParam(param: ProcedureParam) = div(cls := "col s12 m4")(param.name + ": " + param.columnType)
9 | def forParams(params: Seq[ProcedureParam]) = div(cls := "param-form-container")(params.map(p => forParam(p)): _*)
10 | }
11 |
--------------------------------------------------------------------------------
/client/src/main/scala/models/template/query/QueryFilterTemplate.scala:
--------------------------------------------------------------------------------
1 | package models.template.query
2 |
3 | import models.query.QueryResult
4 | import models.template.Icons
5 | import util.Messages
6 |
7 | import scalatags.Text.all._
8 |
9 | object QueryFilterTemplate {
10 | def activeFilterPanel(qr: QueryResult) = div(qr.source.flatMap(_.filterOpt) match {
11 | case Some(filter) =>
12 | div(cls := "active-filter z-depth-1")(
13 | div(cls := "filter-cancel-link")(i(cls := "theme-text fa " + Icons.close)),
14 | i(cls := "fa " + Icons.filter),
15 | Messages("query.active.filter"),
16 | ": ",
17 | strong(filter.col),
18 | s" ${filter.op.symbol} ",
19 | strong(filter.v)
20 | )
21 | case None => ""
22 | })
23 | }
24 |
--------------------------------------------------------------------------------
/client/src/main/scala/models/template/query/QueryParametersTemplate.scala:
--------------------------------------------------------------------------------
1 | package models.template.query
2 |
3 | import java.util.UUID
4 |
5 | import scalatags.Text.all._
6 |
7 | object QueryParametersTemplate {
8 | def forValues(queryId: UUID, values: Seq[(String, String, String)]) = {
9 | div(cls := "row")(values.map(renderInput(queryId, _)): _*)
10 | }
11 |
12 | private[this] def renderInput(queryId: UUID, x: (String, String, String)) = {
13 | div(cls := "col s12 m4 l3 input-field")(
14 | input(id := queryId + "-parameter-" + x._1, data("key") := x._1, data("t") := x._2, `type` := "text", `value` := x._3),
15 | label(`for` := queryId + "-parameter-" + x._1, cls := "active")(x._1)
16 | )
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/client/src/main/scala/models/template/query/StatementResultsTemplate.scala:
--------------------------------------------------------------------------------
1 | package models.template.query
2 |
3 | import java.util.UUID
4 |
5 | import models.query.QueryResult
6 | import models.template.StaticPanelTemplate
7 | import util.{Messages, TemplateHelper}
8 |
9 | import scalatags.Text.all._
10 |
11 | object StatementResultsTemplate {
12 | def forStatementResults(qr: QueryResult, dateIsoString: String, durationMs: Int, resultId: UUID) = {
13 | val rowLabel = if (qr.rowsAffected == 1) { "row" } else { "rows" }
14 | val content = div(id := s"$resultId")(
15 | a(href := "#", cls := "results-sql-link right theme-text")(Messages("th.sql")),
16 | p(s"${qr.rowsAffected} $rowLabel affected ", TemplateHelper.toTimeago(dateIsoString), s" in [${durationMs}ms]."),
17 | div(cls := "z-depth-1 statement-result-sql")(
18 | pre(cls := "pre-wrap")(qr.sql)
19 | )
20 | )
21 |
22 | StaticPanelTemplate.card(
23 | content = content,
24 | showClose = false
25 | )
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/client/src/main/scala/models/template/results/ChartResultTemplate.scala:
--------------------------------------------------------------------------------
1 | package models.template.results
2 |
3 | import java.util.UUID
4 |
5 | import util.Messages
6 |
7 | import scalatags.Text.all._
8 |
9 | object ChartResultTemplate {
10 | def forChartResults(chartId: UUID) = div(id := chartId.toString, cls := "results-chart-panel initially-hidden")(
11 | div(cls := "loading")(Messages("query.chart.loading")),
12 | div(cls := "chart-options-panel z-depth-1 initially-hidden chart-options-padding"),
13 | div(cls := "chart-container initially-hidden")(
14 | div(cls := "chart-panel")
15 | )
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/client/src/main/scala/models/template/tbl/RowDetailTemplate.scala:
--------------------------------------------------------------------------------
1 | package models.template.tbl
2 |
3 | import models.query.QueryResult
4 |
5 | import scalatags.Text.all._
6 |
7 | object RowDetailTemplate {
8 | def forData(data: Seq[(QueryResult.Col, String)]) = div(cls := "row-detail-container")(
9 | table(cls := "data-table bordered highlight")(
10 | tbody(data.map { d =>
11 | tr(
12 | td(d._1.name),
13 | td(d._2)
14 | )
15 | }: _*)
16 | )
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/client/src/main/scala/models/template/tbl/TableForeignKeyDetailTemplate.scala:
--------------------------------------------------------------------------------
1 | package models.template.tbl
2 |
3 | import models.schema.ForeignKey
4 | import util.Messages
5 |
6 | import scalatags.Text.all._
7 |
8 | object TableForeignKeyDetailTemplate {
9 | def foreignKeyPanel(foreignKeys: Seq[ForeignKey]) = {
10 | tableFor(foreignKeys)
11 | }
12 |
13 | private[this] def tableFor(foreignKeys: Seq[ForeignKey]) = table(
14 | thead(tr(
15 | td(Messages("th.name")),
16 | td(Messages("th.source.columns")),
17 | td(Messages("th.target.table")),
18 | td(Messages("th.target.columns"))
19 | )),
20 | tbody(
21 | foreignKeys.map { key =>
22 | tr(
23 | td(key.name),
24 | td(key.references.map(_.source).mkString(", ")),
25 | td(key.targetTable),
26 | td(key.references.map(_.target).mkString(", "))
27 | )
28 | }
29 | )
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/client/src/main/scala/models/template/tbl/TableIndexDetailTemplate.scala:
--------------------------------------------------------------------------------
1 | package models.template.tbl
2 |
3 | import models.schema.Index
4 | import util.Messages
5 |
6 | import scalatags.Text.all._
7 |
8 | object TableIndexDetailTemplate {
9 | def indexPanel(indexes: Seq[Index]) = {
10 | tableFor(indexes)
11 | }
12 |
13 | private[this] def tableFor(indexes: Seq[Index]) = table(cls := "bordered highlight responsive-table")(
14 | thead(
15 | tr(
16 | th(Messages("th.name")),
17 | th(Messages("th.unique")),
18 | th(Messages("th.type")),
19 | th(Messages("th.columns"))
20 | )
21 | ),
22 | tbody(
23 | indexes.map { idx =>
24 | val uniq = idx.unique.toString
25 | tr(
26 | td(idx.name),
27 | td(uniq),
28 | td(idx.indexType),
29 | td(idx.columns.mkString(", "))
30 | )
31 | }
32 | )
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/client/src/main/scala/models/template/typ/EnumDetailTemplate.scala:
--------------------------------------------------------------------------------
1 | package models.template.typ
2 |
3 | import java.util.UUID
4 |
5 | import models.schema.EnumType
6 | import models.template.{Icons, StaticPanelTemplate}
7 |
8 | import scalatags.Text.all._
9 |
10 | object EnumDetailTemplate {
11 | def forEnum(queryId: UUID, enum: EnumType) = {
12 | val content = div(ul(cls := "collection")(enum.values.map(v => li(cls := "collection-item")(v)): _*))
13 |
14 | div(id := s"panel-$queryId", cls := "workspace-panel")(
15 | StaticPanelTemplate.row(StaticPanelTemplate.panel(content, iconAndTitle = Some(Icons.enum -> span(enum.key)))),
16 | div(id := s"workspace-$queryId")
17 | )
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/client/src/main/scala/services/NotificationService.scala:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import scala.scalajs.js
4 |
5 | object NotificationService {
6 | private[this] val materialize = js.Dynamic.global.Materialize
7 | private[this] var lastError: Option[(String, String)] = None
8 |
9 | def info(reason: String, content: String, duration: Int = 2500) = {
10 | materialize.toast(reason + ": " + content, duration)
11 | }
12 |
13 | def warn(reason: String, content: String, duration: Int = 2500) = {
14 | materialize.toast(reason + ": " + content, duration)
15 | }
16 |
17 | def error(reason: String, content: String, duration: Int = 2500) = {
18 | lastError = Some(reason -> content)
19 | materialize.toast(reason + ": " + content, duration)
20 | }
21 |
22 | def getLastError = lastError
23 | }
24 |
--------------------------------------------------------------------------------
/client/src/main/scala/services/ShortcutService.scala:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import java.util.UUID
4 |
5 | import org.scalajs.dom
6 | import util.KeyboardShortcut
7 |
8 | import scala.scalajs.js
9 |
10 | object ShortcutService {
11 | private[this] val mt = js.Dynamic.global.Mousetrap
12 |
13 | def init() = {
14 | KeyboardShortcut.values.filter(_.isGlobal).foreach { shortcut =>
15 | mt.bind(shortcut.pattern, (e: js.Dynamic) => {
16 | e.preventDefault()
17 | shortcut.call(None)
18 | })
19 | }
20 | }
21 |
22 | def configureEditor(id: UUID) = {
23 | val sel = dom.document.querySelector(s"#sql-textarea-$id")
24 | val trap = mt(sel)
25 |
26 | KeyboardShortcut.values.filterNot(_.isGlobal).foreach { shortcut =>
27 | trap.bind(shortcut.pattern, (e: js.Dynamic) => {
28 | e.preventDefault()
29 | shortcut.call(Some(id))
30 | })
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/client/src/main/scala/services/TextChangeService.scala:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import java.util.UUID
4 |
5 | import org.scalajs.dom
6 | import org.scalajs.dom.BeforeUnloadEvent
7 | import services.query.TransactionService
8 | import util.NullUtils
9 |
10 | object TextChangeService {
11 | private[this] val dirtyEditors = collection.mutable.HashSet.empty[UUID]
12 |
13 | def init() = dom.window.onbeforeunload = (_: BeforeUnloadEvent) => {
14 | if (TransactionService.isInTransaction) {
15 | "Your active transaction will be rolled back."
16 | } else if (dirtyEditors.nonEmpty) {
17 | "Changes you made may not be saved."
18 | } else {
19 | NullUtils.inst
20 | }
21 | }
22 |
23 | def markDirty(id: UUID) = if (!dirtyEditors(id)) {
24 | dirtyEditors += id
25 | }
26 |
27 | def markClean(id: UUID) = if (dirtyEditors(id)) {
28 | dirtyEditors -= id
29 | }
30 |
31 | def shouldClose(id: UUID) = if (dirtyEditors(id)) {
32 | if (dom.window.confirm("You have unsaved changes. Close this query?")) {
33 | dirtyEditors -= id
34 | true
35 | } else {
36 | false
37 | }
38 | } else {
39 | true
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/client/src/main/scala/services/query/QueryErrorService.scala:
--------------------------------------------------------------------------------
1 | package services.query
2 |
3 | import models.template.query.QueryErrorTemplate
4 | import models.{QueryCheckResponse, QueryErrorResponse}
5 | import org.scalajs.jquery.{jQuery => $}
6 | import ui.ProgressManager
7 | import ui.editor.EditorManager
8 | import util.TemplateHelper
9 |
10 | object QueryErrorService {
11 | def handleQueryErrorResponse(qer: QueryErrorResponse) = {
12 | val occurred = new scalajs.js.Date(qer.error.occurred.toDouble)
13 | val content = QueryErrorTemplate.forQueryError(qer, occurred.toISOString)
14 | ProgressManager.completeProgress(qer.error.queryId, qer.id, qer.index, content)
15 |
16 | val panel = $("#" + qer.id)
17 | val sqlEl = $(".query-result-sql", panel)
18 | var sqlShown = false
19 | TemplateHelper.clickHandler($(".results-sql-link", panel), _ => {
20 | if (sqlShown) { sqlEl.hide() } else { sqlEl.show() }
21 | sqlShown = !sqlShown
22 | })
23 | }
24 |
25 | def handleQueryCheckResponse(qcr: QueryCheckResponse) = {
26 | EditorManager.highlightErrors(qcr.queryId, qcr.results)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/client/src/main/scala/services/query/RowCountService.scala:
--------------------------------------------------------------------------------
1 | package services.query
2 |
3 | import models.QueryResultRowCount
4 | import org.scalajs.jquery.{jQuery => $}
5 | import util.NumberUtils
6 |
7 | object RowCountService {
8 | def handleResultRowCount(qrrc: QueryResultRowCount) = {
9 | val panel = $(s"#${qrrc.resultId}", $(s"#workspace-${qrrc.queryId}"))
10 | val rowCountEl = $(".total-row-count", panel)
11 | if (qrrc.overflow) {
12 | rowCountEl.text(s" of at least ${NumberUtils.withCommas(qrrc.count)} ")
13 | } else if (qrrc.count > 100) {
14 | rowCountEl.text(s" of ${NumberUtils.withCommas(qrrc.count)} total ")
15 | }
16 | $(".total-duration", panel).text(NumberUtils.withCommas(qrrc.durationMs))
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/client/src/main/scala/services/query/StatementResultService.scala:
--------------------------------------------------------------------------------
1 | package services.query
2 |
3 | import java.util.UUID
4 |
5 | import models.query.QueryResult
6 | import models.template.query.StatementResultsTemplate
7 | import org.scalajs.jquery.{jQuery => $}
8 | import ui.ProgressManager
9 | import util.TemplateHelper
10 |
11 | object StatementResultService {
12 | def handleNewStatementResults(resultId: UUID, index: Int, result: QueryResult, durationMs: Int): Unit = {
13 | val occurred = new scalajs.js.Date(result.occurred.toDouble)
14 | TransactionService.incrementCount()
15 |
16 | val content = StatementResultsTemplate.forStatementResults(result, occurred.toISOString, durationMs, resultId)
17 | ProgressManager.completeProgress(result.queryId, resultId, index, content)
18 |
19 | val panel = $(s"#$resultId", $(s"#workspace-${result.queryId}"))
20 | val sqlEl = $(".statement-result-sql", panel)
21 | var sqlShown = false
22 | TemplateHelper.clickHandler($(".results-sql-link", panel), _ => {
23 | if (sqlShown) { sqlEl.hide() } else { sqlEl.show() }
24 | sqlShown = !sqlShown
25 | })
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/client/src/main/scala/ui/UserManager.scala:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import java.util.UUID
4 |
5 | import models.UserSettings
6 | import models.user.UserPreferences
7 |
8 | object UserManager {
9 | var userId: Option[UUID] = None
10 | var username: Option[String] = None
11 | var email: Option[String] = None
12 | var preferences: Option[UserPreferences] = None
13 | val rowsReturned = 100
14 |
15 | def onUserSettings(us: UserSettings) = {
16 | userId = Some(us.userId)
17 | username = Some(us.username)
18 | email = Some(us.email)
19 | preferences = Some(us.preferences)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/client/src/main/scala/ui/WorkspaceManager.scala:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import org.scalajs.jquery.{jQuery => $}
4 |
5 | object WorkspaceManager {
6 | lazy val workspace = $("#workspace")
7 |
8 | def append(html: String) = {
9 | workspace.append(html)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/main/scala/ui/metadata/ModelFilterManager.scala:
--------------------------------------------------------------------------------
1 | package ui.metadata
2 |
3 | import org.scalajs.dom
4 | import org.scalajs.jquery.{JQuery, jQuery => $}
5 |
6 | case class ModelFilterManager(queryPanel: JQuery) {
7 | var activeFilter: Option[String] = None
8 | val trs = $("tbody tr", queryPanel)
9 |
10 | def filter(s: Option[String]) = if (activeFilter != s) {
11 | s match {
12 | case Some(v) => trs.each { (e: dom.Element) =>
13 | val el = $(e)
14 | val source = $("td:first-child", el).text().toLowerCase
15 | if (source.contains(v)) {
16 | el.show()
17 | } else {
18 | el.hide()
19 | }
20 | }
21 | case None => trs.each((e: dom.Element) => $(e).show())
22 | }
23 | activeFilter = s
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/client/src/main/scala/ui/modal/PlanNodeDetailManager.scala:
--------------------------------------------------------------------------------
1 | package ui.modal
2 |
3 | import models.plan.PlanNode
4 | import models.template.query.QueryPlanNodeDetailTemplate
5 | import org.scalajs.jquery.{jQuery => $}
6 | import util.TemplateHelper
7 |
8 | import scala.scalajs.js
9 |
10 | object PlanNodeDetailManager {
11 | private[this] val modal = js.Dynamic.global.$("#plan-node-modal")
12 |
13 | private[this] val modalContent = $("#plan-node-modal-content", modal)
14 | private[this] val modalLink = $("#plan-node-ok-link", modal)
15 |
16 | def init() = TemplateHelper.clickHandler(modalLink, _ => {
17 | close()
18 | })
19 |
20 | def show(node: PlanNode, total: Either[Int, Double]) = {
21 | val content = QueryPlanNodeDetailTemplate.forNode(node, total)
22 | modalContent.html(content.toString)
23 | modal.openModal()
24 | }
25 |
26 | def close(): Unit = modal.closeModal()
27 | }
28 |
--------------------------------------------------------------------------------
/client/src/main/scala/ui/modal/QueryExportFormManager.scala:
--------------------------------------------------------------------------------
1 | package ui.modal
2 |
3 | import java.util.UUID
4 |
5 | import models.query.QueryResult
6 | import org.scalajs.jquery.{jQuery => $}
7 | import util.TemplateHelper
8 |
9 | import scala.scalajs.js
10 | import util.JsonSerializers._
11 |
12 | object QueryExportFormManager {
13 | private[this] val modal = js.Dynamic.global.$("#export-modal")
14 |
15 | private[this] val inputQueryId = $("#input-export-query-id", modal)
16 | private[this] val inputSource = $("#input-export-source", modal)
17 |
18 | def init() = TemplateHelper.clickHandler($("#export-cancel-link", modal), _ => modal.closeModal())
19 |
20 | def show(queryId: UUID, source: QueryResult.Source) = {
21 | inputQueryId.value(queryId.toString)
22 | inputSource.value(source.asJson.spaces2)
23 | modal.openModal()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/client/src/main/scala/ui/modal/ReconnectManager.scala:
--------------------------------------------------------------------------------
1 | package ui.modal
2 |
3 | import org.scalajs.jquery.{jQuery => $}
4 | import util.TemplateHelper
5 |
6 | import scala.scalajs.js
7 |
8 | object ReconnectManager {
9 | private[this] var activeCallback: Option[() => Unit] = None
10 | private[this] val modal = js.Dynamic.global.$("#reconnect-modal")
11 |
12 | private[this] val errorContent = $("#reconnect-error-content", modal)
13 | private[this] val link = $("#reconnect-action-link", modal)
14 |
15 | if (link.length != 1) {
16 | throw new IllegalStateException("Missing reconnect link.")
17 | }
18 |
19 | def init() = TemplateHelper.clickHandler(link, _ => {
20 | activeCallback match {
21 | case Some(cb) => cb()
22 | case None => throw new IllegalStateException("No active callback.")
23 | }
24 | close()
25 | })
26 |
27 | def show(callback: () => Unit, error: String) = {
28 | activeCallback = Some(callback)
29 | errorContent.text(error)
30 | modal.openModal()
31 | }
32 |
33 | def close(): Unit = modal.closeModal()
34 | }
35 |
--------------------------------------------------------------------------------
/client/src/main/scala/ui/query/QueryCheckManager.scala:
--------------------------------------------------------------------------------
1 | package ui.query
2 |
3 | import java.util.UUID
4 |
5 | import models.CheckQuery
6 | import org.scalajs.dom
7 | import util.NetworkMessage
8 |
9 | object QueryCheckManager {
10 | private[this] var sqlChecks = Map.empty[UUID, String]
11 |
12 | def isChanged(queryId: UUID, s: String) = !sqlChecks.get(queryId).contains(s)
13 |
14 | def check(queryId: UUID, sql: String) = {
15 | //util.Logging.info(s"Checking SQL [$sql].")
16 | dom.window.setTimeout(() => {
17 | val currentSql = SqlManager.getSql(queryId)
18 | val params = ParameterManager.getParams(currentSql, queryId)._2
19 | val merged = ParameterManager.merge(currentSql, params)
20 | if (merged == sql) {
21 | sqlChecks += (queryId -> merged)
22 | NetworkMessage.sendMessage(CheckQuery(queryId, merged))
23 | }
24 | }, 1000)
25 | }
26 |
27 | def remove(queryId: UUID) = sqlChecks -= queryId
28 | }
29 |
--------------------------------------------------------------------------------
/client/src/main/scala/ui/query/RowDataManager.scala:
--------------------------------------------------------------------------------
1 | package ui.query
2 |
3 | import java.util.UUID
4 |
5 | import models.GetRowData
6 | import models.query.{QueryResult, RowDataOptions}
7 | import scribe.Logging
8 | import ui.ProgressManager
9 | import util.NetworkMessage
10 |
11 | object RowDataManager extends Logging {
12 | def showRowData(key: QueryResult.SourceType, queryId: UUID, name: String, options: RowDataOptions, resultId: UUID): Unit = {
13 | logger.debug(s"Showing [$key] row data for [$name] with options [$options].")
14 | if (options.offset.forall(_ == 0)) {
15 | ProgressManager.startProgress(queryId, resultId, name)
16 | }
17 | NetworkMessage.sendMessage(GetRowData(key, queryId, name, options, resultId))
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/client/src/main/scala/ui/query/SharedResultManager.scala:
--------------------------------------------------------------------------------
1 | package ui.query
2 |
3 | import java.util.UUID
4 |
5 | import models.query.SharedResult
6 | import ui.metadata.MetadataManager
7 |
8 | object SharedResultManager {
9 | var sharedResults = Map.empty[UUID, SharedResult]
10 | var usernameMap = Map.empty[UUID, String]
11 |
12 | def updateSharedResults(srs: Seq[SharedResult], usernames: Map[UUID, String]) = {
13 | usernameMap = usernameMap ++ usernames
14 | srs.foreach { sr =>
15 | sharedResults = sharedResults + (sr.id -> sr)
16 | }
17 | MetadataManager.updateSharedResults(sharedResults.values.toSeq.sortBy(_.title))
18 | }
19 |
20 | def sharedResultDetail(id: UUID) = {
21 | val url = s"/shared/$id"
22 | org.scalajs.dom.window.open(url, "_blank")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/client/src/main/scala/util/Messages.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import scala.scalajs.js
4 | import scala.scalajs.js.annotation.JSBracketAccess
5 |
6 | object Messages {
7 | @js.native
8 | trait MessageObject extends js.Object {
9 | @JSBracketAccess
10 | def apply(key: String): Any = js.native
11 | }
12 |
13 | lazy val jsMessages = {
14 | val ret = js.Dynamic.global.messages.asInstanceOf[MessageObject]
15 | if (ret == None.orNull) {
16 | throw new IllegalStateException("Missing localization object [messages].")
17 | }
18 | ret
19 | }
20 |
21 | def apply(s: String, args: Any*) = {
22 | val msg = Option(jsMessages(s)) match {
23 | case Some(x) => x.toString match {
24 | case "undefined" => s
25 | case y => y
26 | }
27 | case None => s
28 | }
29 | args.zipWithIndex.foldLeft(msg) { (x, y) =>
30 | x.replaceAllLiterally(s"{${y._2}}", y._1.toString)
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/client/src/main/scala/util/NetworkMessage.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import models.RequestMessage
4 |
5 | object NetworkMessage {
6 | var latencyMs: Option[Int] = None
7 | var sentMessageCount = 0
8 | var receivedMessageCount = 0
9 |
10 | private[this] var sendF: Option[(RequestMessage) => Unit] = None
11 |
12 | def register(f: (RequestMessage) => Unit) = sendF match {
13 | case Some(_) => throw new IllegalStateException("Double registration.")
14 | case None => sendF = Some(f)
15 | }
16 |
17 | def sendMessage(requestMessage: RequestMessage) = sendF match {
18 | case Some(f) => f(requestMessage)
19 | case None => throw new IllegalStateException("Message send before start.")
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/client/src/main/scala/util/ScriptLoader.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import org.scalajs.jquery.{jQuery => $}
4 |
5 | import scala.scalajs.js
6 |
7 | object ScriptLoader {
8 | private[this] val scriptRoutes = js.Dynamic.global.scriptRoutes
9 |
10 | private[this] val scripts = Seq(
11 | "charting" -> scriptRoutes.charting.toString,
12 | "plotly" -> scriptRoutes.plotly.toString
13 | )
14 | private[this] var loadedScripts = Seq.empty[String]
15 |
16 | $.ajaxSetup(js.Dynamic.literal(
17 | "cache" -> true
18 | ))
19 |
20 | def loadScript(key: String, callback: () => Unit): Unit = {
21 | if (loadedScripts.contains(key)) {
22 | callback()
23 | } else {
24 | val url = scripts.find(_._1 == key).map(_._2).getOrElse(throw new IllegalStateException(s"Invalid script [$key]."))
25 | $.getScript(url, () => {
26 | loadedScripts = loadedScripts :+ key
27 | callback()
28 | })
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/conf/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/conf/icon.png
--------------------------------------------------------------------------------
/conf/initial-config.conf:
--------------------------------------------------------------------------------
1 | databaseflow {
2 | // Used for storing users, saved queries, and other data.
3 | master {
4 | // Default is an H2 database file named "databaseflow", located in ~/.databaseflow for Linux/OSX, or %APPDATA%\DatabaseFlow for Windows.
5 | db = "h2"
6 | url = "default"
7 |
8 | // Alternately, you can use a custom H2 file.
9 | // db = "h2"
10 | // url = "jdbc:h2:~/database.h2db"
11 | // username = "databaseflow"
12 | // password = "flow"
13 |
14 | // ...or a PostgreSQL server.
15 | // db = "postgres"
16 | // url = "jdbc:postgresql://localhost:5432/databaseflow?stringtype=unspecified"
17 | // username = "databaseflow"
18 | // password = "flow"
19 | }
20 | // Used as a cache for query results, for later sorting and filtering.
21 | resultCache {
22 | // Default is an H2 database file named "result-cache", located in ~/.databaseflow for Linux/OSX, or %APPDATA%\DatabaseFlow for Windows.
23 | db = "h2"
24 | url = "default"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/conf/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | UTF-8
7 | %-5coloredLevel %d{HH:mm:ss.SSS} %cyan(%logger{15}): %msg%n%xException
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/conf/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | UTF-8
7 | %-5coloredLevel %d{HH:mm:ss.SSS} %cyan(%logger{15}): %msg%n%xException
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/dblibs/lib/db2-db2jcc4.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/dblibs/lib/db2-db2jcc4.jar
--------------------------------------------------------------------------------
/dblibs/lib/informix-ifxjdbc.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/dblibs/lib/informix-ifxjdbc.jar
--------------------------------------------------------------------------------
/dblibs/lib/informix-ifxjdbcx.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/dblibs/lib/informix-ifxjdbcx.jar
--------------------------------------------------------------------------------
/dblibs/lib/informix-ifxlang.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/dblibs/lib/informix-ifxlang.jar
--------------------------------------------------------------------------------
/dblibs/lib/informix-ifxlsupp.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/dblibs/lib/informix-ifxlsupp.jar
--------------------------------------------------------------------------------
/dblibs/lib/informix-ifxsqlj.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/dblibs/lib/informix-ifxsqlj.jar
--------------------------------------------------------------------------------
/dblibs/lib/informix-ifxtools.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/dblibs/lib/informix-ifxtools.jar
--------------------------------------------------------------------------------
/dblibs/lib/oracle-ojdbc7.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/dblibs/lib/oracle-ojdbc7.jar
--------------------------------------------------------------------------------
/dblibs/src/main/resources/sampledb/sampledb.sqlite:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/dblibs/src/main/resources/sampledb/sampledb.sqlite
--------------------------------------------------------------------------------
/doc/src/assets/images/logo-small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/doc/src/assets/images/logo-small.png
--------------------------------------------------------------------------------
/doc/src/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/doc/src/assets/images/logo.png
--------------------------------------------------------------------------------
/doc/src/contribute/index.md:
--------------------------------------------------------------------------------
1 | @@@ index
2 |
3 | * [SBT Tasks](sbtTasks.md)
4 | * [Packaging](packaging.md)
5 | * [Technology](technology.md)
6 |
7 | @@@
8 |
9 | # Contribute
10 |
11 | Database Flow is a standard open source Scala SBT project, and can be run like any other SBT application.
12 | Many SBT plugins are provided to make development easier and safer.
13 | To get started, clone the git repo, and run `sbt` in the project's directory.
14 | Once the prompt appears, execute "run", and the service will become available on port 4260.
15 |
16 | ## Project Documentation
17 |
18 | Project | Description
19 | ------------------------------------------------|----------------------------------------------------------------------------------------------------
20 | [Shared](../api/shared/index.html) | Common code shared between JVM and JavaScript, mostly base schema definitions and support classes
21 | [Client](../api/client/index.html) | ScalaJS classes that create the web user interface
22 | [Server](../api/server/index.html) | Main Database Flow server, handling all http requests and CLI arguments
23 |
24 |
--------------------------------------------------------------------------------
/doc/src/contribute/packaging.md:
--------------------------------------------------------------------------------
1 | # Packaging
2 |
3 | Database Flow can produce a standard Play distribution by running `dist` from SBT.
4 |
5 | Other distribution formats are available. More than you'd ever need, in fact:
6 |
7 | * `assembly` - Produces a self-contained "uberjar" capable of being run with `java -jar boilerplay.jar`.
8 | * `jdkPackager:packageBin` - Creates an OS-specific installer, such as a `DMG` for macOS, an `exe` for windows, or a Debian package.
9 | * `docker:publishLocal` - Build a Docker image and publishes it to your local Docker repository.
10 |
--------------------------------------------------------------------------------
/doc/src/contribute/sbtTasks.md:
--------------------------------------------------------------------------------
1 | # SBT Tasks
2 |
3 | * Run `scalastyle` to find style and linting violations. The plugin is configured through `./scalastyle-config.xml`.
4 | * When you compile, your code is automatically formatted. You can manually run the formatter with `scalariformFormat`.
5 | * To see a visualization of the project's dependencies, run `dependencyGraph`.
6 | * Keep up-to-date by running `dependencyUpdates` and upgrading the versions in `./project/Dependencies.scala`.
7 | * Run `stats` to get a listing of various metrics, including lines of code and project size.
8 | * You can find the exact memory layout of a given class by running `jol:internals my.Class`.
9 | * To get meta and run a plugin that introspects plugins, run `about-plugins`.
10 | * Generate a visualization of your project dependencies with `projectsGraphDot`.
11 | * Using `classDiagram my.class` will generate an SVG diagram of your class and its ancestors.
12 |
--------------------------------------------------------------------------------
/doc/src/contribute/technology.md:
--------------------------------------------------------------------------------
1 | # Technology
2 |
3 | Database flow relies on a bunch of tremendous open source projects. Here's a few of them.
4 |
5 | * [Scala](http://www.scala-lang.org)
6 | * [ScalaJS](https://www.scala-js.org)
7 | * [Play Framework](https://www.playframework.com)
8 | * [Akka](http://akka.io)
9 | * [Sangria](http://sangria-graphql.org)
10 | * [Enumeratum$](https://github.com/lloydmeta/enumeratum)
11 | * [Circe](https://circe.github.io/circe)
12 | * [Scalatags]("https://github.com/lihaoyi/scalatags)
13 | * [HikariCP](https://github.com/brettwooldridge/HikariCP)
14 | * [Silhouette](http://silhouette.mohiva.com)
15 | * [ScalaCrypt](https://github.com/Richard-W/scalacrypt")
16 | * [GraphiQL](https://github.com/graphql/graphiql)
17 | * [GraphQL Voyager](https://apis.guru/graphql-voyager)
18 | * [Materialize](http://materializecss.com)
19 | * [PlotlyJS](https://plot.ly/javascript)
20 | * [FontAwesome](http://fontawesome.io)
21 | * [MomentJS](http://momentjs.com)
22 |
--------------------------------------------------------------------------------
/doc/src/database/db2.md:
--------------------------------------------------------------------------------
1 | # DB/2
2 |
3 | IBM DB/2 might be supported for queries and charting.
4 |
5 | We've never been able to stand up a DB/2 server to test. Please let us know on [Github](https://github.com/KyleU/databaseflow) if you find an issue.
6 |
--------------------------------------------------------------------------------
/doc/src/database/h2.md:
--------------------------------------------------------------------------------
1 | # H2
2 |
--------------------------------------------------------------------------------
/doc/src/database/index.md:
--------------------------------------------------------------------------------
1 | @@@ index
2 |
3 | * [PostgreSQL](postgresql.md)
4 | * [MySQL](mysql.md)
5 | * [H2](h2.md)
6 | * [SQLite](sqlite.md)
7 | * [Oracle](oracle.md)
8 | * [SQL Server](sqlserver.md)
9 | * [Informix](informix.md)
10 | * [DB/2](db2.md)
11 |
12 | @@@
13 |
14 | # Supported Databases
15 |
16 | Database | Support
17 | ----------------------------------|-------------------------------------------------
18 | [PostgreSQL](postgresql.md) | All features supported
19 | [MySQL](mysql.md) | All features supported
20 | [H2](h2.md) | Query plan visualization, charting
21 | [SQLite](sqlite.md) | Query plan visualization, charting
22 | [Oracle](oracle.md) | Couldn't find a server to connect to, but should work
23 | [SQL Server](sqlserver.md) | Query plan visualization, charting
24 | [Informix](informix.md) | Basic tests, not all datatypes supported
25 | [DB/2](db2.md) | I've got the driver, but was never able to get a server running for testing
26 |
--------------------------------------------------------------------------------
/doc/src/database/informix.md:
--------------------------------------------------------------------------------
1 | # Informix
2 |
3 | Informix might be supported for queries and charting.
4 |
5 | We've never been able to stand up an Informix server to test. Please let us know on [Github](https://github.com/KyleU/databaseflow) if you find an issue.
6 |
--------------------------------------------------------------------------------
/doc/src/database/mysql.md:
--------------------------------------------------------------------------------
1 | # MySQL
2 |
3 | MySQL is supported for queries, charting, and query plan visualization.
4 |
5 | Please let us know on [Github](https://github.com/KyleU/databaseflow) if you find an issue.
6 |
--------------------------------------------------------------------------------
/doc/src/database/oracle.md:
--------------------------------------------------------------------------------
1 | # Oracle
2 |
3 | Oracle is supported for queries and charting.
4 |
5 | We've actually never been able to test Oracle, please let us know on [Github](https://github.com/KyleU/databaseflow) if you find an issue.
6 |
--------------------------------------------------------------------------------
/doc/src/database/postgresql.md:
--------------------------------------------------------------------------------
1 | # PostgreSQL
2 |
3 | PostgreSQL is the primary supported database for Database Flow, all features are tested on Postgres first.
4 | Queries, charting, and query plan visualization are all available.
5 |
6 | Please let us know on [Github](https://github.com/KyleU/databaseflow) if you find an issue.
7 |
8 |
--------------------------------------------------------------------------------
/doc/src/database/sqlite.md:
--------------------------------------------------------------------------------
1 | # SQLite
2 |
3 | SQLite is supported for queries and charting.
4 |
5 | We've tested basic functionality, please let us know on [Github](https://github.com/KyleU/databaseflow) if you find an issue.
6 |
--------------------------------------------------------------------------------
/doc/src/database/sqlserver.md:
--------------------------------------------------------------------------------
1 | # SQL Server
2 |
3 | Microsoft SQL Server is supported for queries and charting.
4 |
5 | We've tested basic functionality, please let us know on [Github](https://github.com/KyleU/databaseflow) if you find an issue.
6 |
--------------------------------------------------------------------------------
/doc/src/databaseflow.css:
--------------------------------------------------------------------------------
1 | .md-footer-copyright {
2 | display: none;
3 | }
4 |
--------------------------------------------------------------------------------
/doc/src/feature/charting.md:
--------------------------------------------------------------------------------
1 | # Charting
2 |
3 | We use plotly.js to bring you the best charts available. Share charts and results with ease, and save queries for later use.
4 |
--------------------------------------------------------------------------------
/doc/src/feature/graphql.md:
--------------------------------------------------------------------------------
1 | # GraphQL
2 |
3 | Use the power of GraphQL to explore and discover your database. Query your database schema and browse table relationships using GraphiQL.
4 |
--------------------------------------------------------------------------------
/doc/src/feature/history.md:
--------------------------------------------------------------------------------
1 | # Query Activity
2 |
3 | Query history and audit logs ensure you never forget the queries you write. Granular permissions help you share databases and results with your team.
4 |
--------------------------------------------------------------------------------
/doc/src/feature/index.md:
--------------------------------------------------------------------------------
1 | @@@ index
2 |
3 | * [SQL Editor](sqleditor.md)
4 | * [Query Plan](queryplan.md)
5 | * [Visualization](visualization.md)
6 | * [History](history.md)
7 | * [GraphQL](graphql.md)
8 | * [Charting](charting.md)
9 |
10 | @@@
11 |
12 | # Features
13 |
14 | ...
15 |
--------------------------------------------------------------------------------
/doc/src/feature/queryplan.md:
--------------------------------------------------------------------------------
1 | # Query Plan
2 |
3 | Visualize the performance of your MySQL or PostgreSQL queries with a sophisticated plan viewer.
4 | Supporting explain and analyze plans, improving the performance of your queries just got easier.
5 |
--------------------------------------------------------------------------------
/doc/src/feature/sqleditor.md:
--------------------------------------------------------------------------------
1 | # SQL Editor
2 |
3 | Syntax highlighting and auto-completion make writing complicated queries easy. Use query parameters to help others use your shared SQL.
4 |
5 | Sort and filter your results without re-running your query. By linking to related tables, Database Flow helps you drill down to the data you need.
6 |
7 | High definition retina graphics and a responsive interface help you navigate your data on any device. Hotkeys and shortcuts increase your productivity.
8 |
--------------------------------------------------------------------------------
/doc/src/feature/visualization.md:
--------------------------------------------------------------------------------
1 | # Visualization
2 |
3 | Visualize the relationships and columns in your database schema. Using GraphQL Voyager and MermaidJS, you can pan, zoom and navigate through all your data.
4 |
5 |
--------------------------------------------------------------------------------
/doc/src/gettingStarted.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | ## Installation
4 |
5 | * Download the latest `databaseflow.jar` file from [Github](https://github.com/KyleU/databaseflow/releases).
6 | * Run `java -jar databaseflow.jar` - a new browser tab pointed to `http://localhost:4260` will open automatically.
7 |
8 | ## Configuration
9 |
10 | * If you're on Windows, config files are stored in `%APPDATA%\Database Flow`. For macOS and Linux, the configuration folder may be found in `~/.databaseflow`/
11 | * The main configuation file is named `databaseflow.conf`.
12 | * You may change the configuration for file path, mail setup, and storage locations.
13 |
--------------------------------------------------------------------------------
/doc/src/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/doc/src/logo.png
--------------------------------------------------------------------------------
/doc/src/manage/configuration.md:
--------------------------------------------------------------------------------
1 | # Configuration
2 |
--------------------------------------------------------------------------------
/doc/src/manage/installLocal.md:
--------------------------------------------------------------------------------
1 | # Local Installation
2 |
--------------------------------------------------------------------------------
/doc/src/manage/installShared.md:
--------------------------------------------------------------------------------
1 | # Shared Installation
2 |
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Kyle Unverferth
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/project/Database.scala:
--------------------------------------------------------------------------------
1 | import sbt.Keys._
2 | import sbt._
3 |
4 | object Database {
5 | private[this] val dependencies = {
6 | import Dependencies._
7 | Seq(Jdbc.hikariCp, Jdbc.h2, Jdbc.mysql, Jdbc.postgres, Jdbc.sqlite, Jdbc.sqlServer)
8 | }
9 |
10 | private[this] lazy val dblibsSettings = Shared.commonSettings ++ Seq(name := "dblibs", libraryDependencies ++= dependencies)
11 |
12 | lazy val dblibs = Project(id = "dblibs", base = file("dblibs")).settings(dblibsSettings: _*)
13 | }
14 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.3.0
2 |
--------------------------------------------------------------------------------
/public/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | #2b5797
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_bold-italic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_bold-italic.eot
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_bold-italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_bold-italic.ttf
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_bold-italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_bold-italic.woff
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_bold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_bold.eot
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_bold.ttf
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_bold.woff
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_italic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_italic.eot
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_italic.ttf
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_italic.woff
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_light-italic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_light-italic.eot
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_light-italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_light-italic.ttf
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_light-italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_light-italic.woff
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_light.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_light.eot
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_light.ttf
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_light.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_light.woff
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_medium-italic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_medium-italic.eot
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_medium-italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_medium-italic.ttf
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_medium-italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_medium-italic.woff
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_medium.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_medium.eot
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_medium.ttf
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_medium.woff
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_regular.eot
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_regular.ttf
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_regular.woff
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_thin-italic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_thin-italic.eot
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_thin-italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_thin-italic.ttf
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_thin-italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_thin-italic.woff
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_thin.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_thin.eot
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_thin.ttf
--------------------------------------------------------------------------------
/public/fonts/roboto-mono_thin.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/fonts/roboto-mono_thin.woff
--------------------------------------------------------------------------------
/public/images/ui/favicon/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/favicon-16x16.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/favicon-32x32.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/favicon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/favicon-512.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/favicon-alt.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/favicon-alt.psd
--------------------------------------------------------------------------------
/public/images/ui/favicon/favicon.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/favicon.bmp
--------------------------------------------------------------------------------
/public/images/ui/favicon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/favicon.ico
--------------------------------------------------------------------------------
/public/images/ui/favicon/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/favicon.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/favicon.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/favicon.psd
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-amber.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-amber.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-amber.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-amber@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-amber@2x.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-black.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-black.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-black@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-black@2x.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-blue-grey.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-blue-grey.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-blue-grey.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-blue-grey@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-blue-grey@2x.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-blue.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-blue.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-blue@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-blue@2x.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-brown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-brown.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-brown.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-brown@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-brown@2x.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-cyan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-cyan.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-cyan.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-cyan@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-cyan@2x.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-deep-orange.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-deep-orange.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-deep-orange.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-deep-orange@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-deep-orange@2x.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-deep-purple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-deep-purple.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-deep-purple.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-deep-purple@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-deep-purple@2x.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-green.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-green.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-green@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-green@2x.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-grey.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-grey.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-grey.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-grey@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-grey@2x.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-indigo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-indigo.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-indigo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-indigo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-indigo@2x.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-light-blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-light-blue.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-light-blue.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-light-blue@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-light-blue@2x.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-light-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-light-green.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-light-green.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-light-green@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-light-green@2x.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-orange.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-orange.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-orange.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-orange@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-orange@2x.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-pink.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-pink.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-pink.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-pink@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-pink@2x.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-purple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-purple.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-purple.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-purple@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-purple@2x.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-red.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-red.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-red@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-red@2x.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-teal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-teal.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-teal.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/ui/favicon/icon-teal@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/icon-teal@2x.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/mstile-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/mstile-144x144.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/mstile-150x150.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/mstile-310x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/mstile-310x150.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/mstile-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/mstile-310x310.png
--------------------------------------------------------------------------------
/public/images/ui/favicon/mstile-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/images/ui/favicon/mstile-70x70.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "Database Flow",
4 | "icons": [
5 | {
6 | "src": "\/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image\/png"
9 | },
10 | {
11 | "src": "\/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image\/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "display": "standalone",
18 | "version": "1.0",
19 | "default_locale": "en"
20 | }
21 |
--------------------------------------------------------------------------------
/public/vendor/fonts/FontAwesome.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/FontAwesome.otf
--------------------------------------------------------------------------------
/public/vendor/fonts/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/public/vendor/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/public/vendor/fonts/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/public/vendor/fonts/fontawesome-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/fontawesome-webfont.woff2
--------------------------------------------------------------------------------
/public/vendor/fonts/roboto/Roboto-Bold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/roboto/Roboto-Bold.eot
--------------------------------------------------------------------------------
/public/vendor/fonts/roboto/Roboto-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/roboto/Roboto-Bold.ttf
--------------------------------------------------------------------------------
/public/vendor/fonts/roboto/Roboto-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/roboto/Roboto-Bold.woff
--------------------------------------------------------------------------------
/public/vendor/fonts/roboto/Roboto-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/roboto/Roboto-Bold.woff2
--------------------------------------------------------------------------------
/public/vendor/fonts/roboto/Roboto-Light.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/roboto/Roboto-Light.eot
--------------------------------------------------------------------------------
/public/vendor/fonts/roboto/Roboto-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/roboto/Roboto-Light.ttf
--------------------------------------------------------------------------------
/public/vendor/fonts/roboto/Roboto-Light.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/roboto/Roboto-Light.woff
--------------------------------------------------------------------------------
/public/vendor/fonts/roboto/Roboto-Light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/roboto/Roboto-Light.woff2
--------------------------------------------------------------------------------
/public/vendor/fonts/roboto/Roboto-Medium.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/roboto/Roboto-Medium.eot
--------------------------------------------------------------------------------
/public/vendor/fonts/roboto/Roboto-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/roboto/Roboto-Medium.ttf
--------------------------------------------------------------------------------
/public/vendor/fonts/roboto/Roboto-Medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/roboto/Roboto-Medium.woff
--------------------------------------------------------------------------------
/public/vendor/fonts/roboto/Roboto-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/roboto/Roboto-Medium.woff2
--------------------------------------------------------------------------------
/public/vendor/fonts/roboto/Roboto-Regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/roboto/Roboto-Regular.eot
--------------------------------------------------------------------------------
/public/vendor/fonts/roboto/Roboto-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/roboto/Roboto-Regular.ttf
--------------------------------------------------------------------------------
/public/vendor/fonts/roboto/Roboto-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/roboto/Roboto-Regular.woff
--------------------------------------------------------------------------------
/public/vendor/fonts/roboto/Roboto-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/roboto/Roboto-Regular.woff2
--------------------------------------------------------------------------------
/public/vendor/fonts/roboto/Roboto-Thin.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/roboto/Roboto-Thin.eot
--------------------------------------------------------------------------------
/public/vendor/fonts/roboto/Roboto-Thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/roboto/Roboto-Thin.ttf
--------------------------------------------------------------------------------
/public/vendor/fonts/roboto/Roboto-Thin.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/roboto/Roboto-Thin.woff
--------------------------------------------------------------------------------
/public/vendor/fonts/roboto/Roboto-Thin.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kyleu/databaseflow/0d87e62d549c1ac8c50f3f95873ca587b8808df4/public/vendor/fonts/roboto/Roboto-Thin.woff2
--------------------------------------------------------------------------------
/public/vendor/graphql/voyager.css.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"voyager.css","sources":[],"mappings":";;","sourceRoot":""}
--------------------------------------------------------------------------------
/shared/src/main/scala/models/audit/AuditRecord.scala:
--------------------------------------------------------------------------------
1 | package models.audit
2 |
3 | import java.util.UUID
4 |
5 | import util.JsonSerializers._
6 |
7 | object AuditRecord {
8 | implicit val jsonEncoder: Encoder[AuditRecord] = deriveEncoder
9 | implicit val jsonDecoder: Decoder[AuditRecord] = deriveDecoder
10 | }
11 |
12 | case class AuditRecord(
13 | id: UUID = UUID.randomUUID,
14 | auditType: AuditType,
15 | owner: UUID,
16 | connection: Option[UUID],
17 | status: AuditStatus = AuditStatus.OK,
18 | sql: Option[String],
19 | error: Option[String] = None,
20 | rowsAffected: Option[Int] = None,
21 | elapsed: Int,
22 | occurred: Long
23 | )
24 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/audit/AuditStatus.scala:
--------------------------------------------------------------------------------
1 | package models.audit
2 |
3 | import enumeratum._
4 |
5 | object AuditStatus extends Enum[AuditStatus] with CirceEnum[AuditStatus] {
6 | case object Started extends AuditStatus
7 | case object OK extends AuditStatus
8 | case object Error extends AuditStatus
9 |
10 | override val values = findValues
11 | }
12 |
13 | sealed trait AuditStatus extends EnumEntry
14 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/engine/EngineColumnParser.scala:
--------------------------------------------------------------------------------
1 | package models.engine
2 |
3 | import java.util.UUID
4 |
5 | import models.schema.ColumnType
6 | import models.schema.ColumnType._
7 |
8 | import scala.util.control.NonFatal
9 |
10 | object EngineColumnParser {
11 | def parse(t: ColumnType, v: String) = try {
12 | Right(fromString(t, v))
13 | } catch {
14 | case NonFatal(_) => Left(v)
15 | }
16 |
17 | def fromString(t: ColumnType, s: String): Any = t match {
18 | case BigDecimalType => BigDecimal(s)
19 | case BooleanType => s == "true" || s == "1" || s == "yes"
20 | case ByteType => s.toByte
21 | case ShortType => s.toShort
22 | case IntegerType => s.toInt
23 | case LongType => s.toLong
24 | case FloatType => s.toFloat
25 | case DoubleType => s.toDouble
26 | case ByteArrayType => s.getBytes
27 | case DateType => s
28 | case TimeType => s
29 | case TimestampType => s
30 | case TimestampZonedType => s
31 | case UuidType => UUID.fromString(s)
32 | case _ => s
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/engine/functions/FunctionProvider.scala:
--------------------------------------------------------------------------------
1 | package models.engine.functions
2 |
3 | trait FunctionProvider {
4 | def functions: Seq[String]
5 | }
6 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/engine/functions/InformixFunctions.scala:
--------------------------------------------------------------------------------
1 | /* Generated Code */
2 | // scalastyle:off
3 | package models.engine.functions
4 |
5 | object InformixFunctions extends FunctionProvider {
6 | override val functions = Seq(
7 | "abs",
8 | "avg",
9 | "bit_length",
10 | "cast",
11 | "coalesce",
12 | "concat",
13 | "count",
14 | "day",
15 | "extract",
16 | "hour",
17 | "length",
18 | "locate",
19 | "lower",
20 | "max",
21 | "min",
22 | "minute",
23 | "mod",
24 | "month",
25 | "nullif",
26 | "second",
27 | "sqrt",
28 | "str",
29 | "substring",
30 | "sum",
31 | "trim",
32 | "upper",
33 | "year"
34 | )
35 | }
36 | // scalastyle:on
37 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/engine/functions/SQLiteFunctions.scala:
--------------------------------------------------------------------------------
1 | /* Generated Code */
2 | // scalastyle:off
3 | package models.engine.functions
4 |
5 | object SQLiteFunctions extends FunctionProvider {
6 | override val functions = Seq(
7 | "concat",
8 | "mod",
9 | "substr",
10 | "substring"
11 | )
12 | }
13 | // scalastyle:on
14 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/engine/types/DB2Types.scala:
--------------------------------------------------------------------------------
1 | /* Generated Code */
2 | package models.engine.types
3 |
4 | object DB2Types extends TypeProvider {
5 | override val columnTypes = Seq(
6 | "bigint",
7 | "varchar",
8 | "smallint",
9 | "blob",
10 | "smallint",
11 | "char",
12 | "clob",
13 | "date",
14 | "double",
15 | "float",
16 | "integer",
17 | "nvarchar",
18 | "long varchar for bit data",
19 | "long varchar",
20 | "nchar",
21 | "nclob",
22 | "numeric",
23 | "nvarchar",
24 | "real",
25 | "smallint",
26 | "time",
27 | "timestamp",
28 | "smallint",
29 | "varchar",
30 | "varchar"
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/engine/types/H2Types.scala:
--------------------------------------------------------------------------------
1 | /* Generated Code */
2 | package models.engine.types
3 |
4 | object H2Types extends TypeProvider {
5 | override val columnTypes = Seq(
6 | "bigint",
7 | "binary",
8 | "boolean",
9 | "blob",
10 | "boolean",
11 | "char",
12 | "clob",
13 | "date",
14 | "decimal",
15 | "double",
16 | "float",
17 | "integer",
18 | "nvarchar",
19 | "longvarbinary",
20 | "longvarchar",
21 | "nchar",
22 | "nclob",
23 | "decimal",
24 | "nvarchar",
25 | "real",
26 | "smallint",
27 | "time",
28 | "timestamp",
29 | "tinyint",
30 | "binary",
31 | "varchar"
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/engine/types/InformixTypes.scala:
--------------------------------------------------------------------------------
1 | /* Generated Code */
2 | package models.engine.types
3 |
4 | object InformixTypes extends TypeProvider {
5 | override val columnTypes = Seq(
6 | "int8",
7 | "byte",
8 | "smallint",
9 | "blob",
10 | "boolean",
11 | "char",
12 | "clob",
13 | "date",
14 | "decimal",
15 | "float",
16 | "smallfloat",
17 | "integer",
18 | "nvarchar",
19 | "blob",
20 | "clob",
21 | "nchar",
22 | "nclob",
23 | "decimal",
24 | "nvarchar",
25 | "smallfloat",
26 | "smallint",
27 | "datetime hour to second",
28 | "datetime year to fraction",
29 | "smallint",
30 | "byte",
31 | "varchar"
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/engine/types/MySQLTypes.scala:
--------------------------------------------------------------------------------
1 | /* Generated Code */
2 | package models.engine.types
3 |
4 | object MySQLTypes extends TypeProvider {
5 | override val columnTypes = Seq(
6 | "bigint",
7 | "binary",
8 | "bit",
9 | "longblob",
10 | "bit",
11 | "char",
12 | "longtext",
13 | "date",
14 | "double precision",
15 | "float",
16 | "integer",
17 | "nvarchar",
18 | "longblob",
19 | "longtext",
20 | "nchar",
21 | "nclob",
22 | "decimal",
23 | "nvarchar",
24 | "real",
25 | "smallint",
26 | "time",
27 | "datetime",
28 | "tinyint",
29 | "longblob",
30 | "longtext"
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/engine/types/OracleTypes.scala:
--------------------------------------------------------------------------------
1 | /* Generated Code */
2 | package models.engine.types
3 |
4 | object OracleTypes extends TypeProvider {
5 | override val columnTypes = Seq(
6 | "number",
7 | "long raw",
8 | "number",
9 | "blob",
10 | "number",
11 | "char",
12 | "clob",
13 | "date",
14 | "number",
15 | "double precision",
16 | "float",
17 | "number",
18 | "nvarchar2",
19 | "long raw",
20 | "long",
21 | "nchar",
22 | "nclob",
23 | "number",
24 | "nvarchar2",
25 | "real",
26 | "number",
27 | "date",
28 | "timestamp",
29 | "number",
30 | "long raw",
31 | "long"
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/engine/types/PostgreSQLTypes.scala:
--------------------------------------------------------------------------------
1 | /* Generated Code */
2 | package models.engine.types
3 |
4 | object PostgreSQLTypes extends TypeProvider {
5 | override val columnTypes = Seq(
6 | "int8",
7 | "bytea",
8 | "bool",
9 | "oid",
10 | "boolean",
11 | "char",
12 | "text",
13 | "date",
14 | "float8",
15 | "float4",
16 | "int4",
17 | "json",
18 | "nvarchar",
19 | "bytea",
20 | "text",
21 | "nchar",
22 | "nclob",
23 | "numeric",
24 | "nvarchar",
25 | "uuid",
26 | "real",
27 | "int2",
28 | "time",
29 | "timestamp",
30 | "int2",
31 | "bytea",
32 | "varchar"
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/engine/types/SQLServerTypes.scala:
--------------------------------------------------------------------------------
1 | /* Generated Code */
2 | package models.engine.types
3 |
4 | object SQLServerTypes extends TypeProvider {
5 | override val columnTypes = Seq(
6 | "bigint",
7 | "binary",
8 | "bit",
9 | "varbinary",
10 | "bit",
11 | "char",
12 | "varchar",
13 | "date",
14 | "double precision",
15 | "float",
16 | "int",
17 | "nvarchar",
18 | "varbinary",
19 | "varchar",
20 | "nchar",
21 | "nvarchar",
22 | "numeric",
23 | "nvarchar",
24 | "real",
25 | "smallint",
26 | "time",
27 | "datetime2",
28 | "smallint",
29 | "varbinary",
30 | "varchar"
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/engine/types/SQLiteTypes.scala:
--------------------------------------------------------------------------------
1 | /* Generated Code */
2 | package models.engine.types
3 |
4 | object SQLiteTypes extends TypeProvider {
5 | override val columnTypes = Seq(
6 | "integer",
7 | "tinyint",
8 | "smallint",
9 | "integer",
10 | "bigint",
11 | "float",
12 | "real",
13 | "double",
14 | "numeric",
15 | "decimal",
16 | "char",
17 | "varchar",
18 | "longvarchar",
19 | "date",
20 | "time",
21 | "timestamp",
22 | "blob",
23 | "blob",
24 | "blob",
25 | "null",
26 | "blob",
27 | "clob",
28 | "integer"
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/engine/types/TypeProvider.scala:
--------------------------------------------------------------------------------
1 | package models.engine.types
2 |
3 | trait TypeProvider {
4 | def columnTypes: Seq[String]
5 | }
6 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/plan/PlanError.scala:
--------------------------------------------------------------------------------
1 | package models.plan
2 |
3 | import java.util.UUID
4 |
5 | import util.JsonSerializers._
6 |
7 | object PlanError {
8 | implicit val jsonEncoder: Encoder[PlanError] = deriveEncoder
9 | implicit val jsonDecoder: Decoder[PlanError] = deriveDecoder
10 | }
11 |
12 | case class PlanError(
13 | queryId: UUID,
14 | sql: String,
15 | code: String,
16 | message: String,
17 | raw: Option[String] = None,
18 | occurred: Long = System.currentTimeMillis
19 | )
20 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/plan/PlanResult.scala:
--------------------------------------------------------------------------------
1 | package models.plan
2 |
3 | import java.util.UUID
4 |
5 | import util.JsonSerializers._
6 |
7 | object PlanResult {
8 | implicit val jsonEncoder: Encoder[PlanResult] = deriveEncoder
9 | implicit val jsonDecoder: Decoder[PlanResult] = deriveDecoder
10 | }
11 |
12 | case class PlanResult(
13 | queryId: UUID,
14 | action: String,
15 | sql: String,
16 | raw: String,
17 | node: PlanNode,
18 | occurred: Long = System.currentTimeMillis
19 | )
20 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/query/QueryCheckResult.scala:
--------------------------------------------------------------------------------
1 | package models.query
2 |
3 | import util.JsonSerializers._
4 |
5 | object QueryCheckResult {
6 | implicit val jsonEncoder: Encoder[QueryCheckResult] = deriveEncoder
7 | implicit val jsonDecoder: Decoder[QueryCheckResult] = deriveDecoder
8 | }
9 |
10 | case class QueryCheckResult(
11 | sql: String,
12 | error: Option[String] = None,
13 | index: Option[Int] = None
14 | )
15 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/query/QueryError.scala:
--------------------------------------------------------------------------------
1 | package models.query
2 |
3 | import java.util.UUID
4 |
5 | import util.JsonSerializers._
6 |
7 | object QueryError {
8 | implicit val jsonEncoder: Encoder[QueryError] = deriveEncoder
9 | implicit val jsonDecoder: Decoder[QueryError] = deriveDecoder
10 | }
11 |
12 | case class QueryError(
13 | queryId: UUID,
14 | sql: String,
15 | code: String,
16 | message: String,
17 | index: Option[Int] = None,
18 | elapsedMs: Int,
19 | occurred: Long = System.currentTimeMillis
20 | )
21 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/query/QueryFilter.scala:
--------------------------------------------------------------------------------
1 | package models.query
2 |
3 | import models.schema.{ColumnType, FilterOp}
4 | import util.JsonSerializers._
5 |
6 | object QueryFilter {
7 | implicit val jsonEncoder: Encoder[QueryFilter] = deriveEncoder
8 | implicit val jsonDecoder: Decoder[QueryFilter] = deriveDecoder
9 | }
10 |
11 | case class QueryFilter(col: String, op: FilterOp, t: ColumnType, v: String)
12 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/query/SavedQuery.scala:
--------------------------------------------------------------------------------
1 | package models.query
2 |
3 | import java.util.UUID
4 |
5 | import models.user.Permission
6 | import util.JsonSerializers._
7 |
8 | object SavedQuery {
9 | object Param {
10 | implicit val jsonEncoder: Encoder[Param] = deriveEncoder
11 | implicit val jsonDecoder: Decoder[Param] = deriveDecoder
12 | }
13 |
14 | case class Param(k: String, v: String)
15 |
16 | implicit val jsonEncoder: Encoder[SavedQuery] = deriveEncoder
17 | implicit val jsonDecoder: Decoder[SavedQuery] = deriveDecoder
18 | }
19 |
20 | case class SavedQuery(
21 | id: UUID = UUID.randomUUID,
22 | name: String = "Untitled Query",
23 | description: Option[String] = None,
24 | sql: String = "",
25 | params: Seq[SavedQuery.Param] = Seq.empty,
26 |
27 | owner: UUID,
28 | connection: Option[UUID] = None,
29 | read: Permission = Permission.User,
30 | edit: Permission = Permission.Private,
31 |
32 | lastRan: Option[Long] = None,
33 |
34 | created: Long = System.currentTimeMillis,
35 | updated: Long = System.currentTimeMillis,
36 | loadedAt: Long = System.currentTimeMillis
37 | )
38 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/query/SharedResult.scala:
--------------------------------------------------------------------------------
1 | package models.query
2 |
3 | import java.util.UUID
4 |
5 | import models.user.Permission
6 | import util.JsonSerializers._
7 |
8 | object SharedResult {
9 | implicit val jsonEncoder: Encoder[SharedResult] = deriveEncoder
10 | implicit val jsonDecoder: Decoder[SharedResult] = deriveDecoder
11 | }
12 |
13 | case class SharedResult(
14 | id: UUID = UUID.randomUUID,
15 | title: String = "",
16 | description: Option[String] = None,
17 | owner: UUID,
18 | viewableBy: Permission = Permission.User,
19 | connectionId: UUID,
20 | sql: String,
21 | source: QueryResult.Source,
22 | chart: Option[String] = None,
23 | lastAccessed: Long = System.currentTimeMillis,
24 | created: Long = System.currentTimeMillis
25 | )
26 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/query/TransactionState.scala:
--------------------------------------------------------------------------------
1 | package models.query
2 |
3 | import enumeratum._
4 |
5 | sealed trait TransactionState extends EnumEntry
6 |
7 | object TransactionState extends Enum[TransactionState] with CirceEnum[TransactionState] {
8 | case object NotStarted extends TransactionState
9 | case object Started extends TransactionState
10 | case object RolledBack extends TransactionState
11 | case object Committed extends TransactionState
12 |
13 | override val values = findValues
14 | }
15 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/schema/Column.scala:
--------------------------------------------------------------------------------
1 | package models.schema
2 |
3 | import util.JsonSerializers._
4 |
5 | object Column {
6 | implicit val jsonEncoder: Encoder[Column] = deriveEncoder
7 | implicit val jsonDecoder: Decoder[Column] = deriveDecoder
8 | }
9 |
10 | case class Column(
11 | name: String,
12 | description: Option[String],
13 | definition: Option[String],
14 | primaryKey: Boolean,
15 | notNull: Boolean,
16 | autoIncrement: Boolean,
17 | columnType: ColumnType,
18 | sqlTypeCode: Int,
19 | sqlTypeName: String,
20 | size: String,
21 | sizeAsInt: Int,
22 | scale: Int,
23 | defaultValue: Option[String]
24 | )
25 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/schema/ColumnDetails.scala:
--------------------------------------------------------------------------------
1 | package models.schema
2 |
3 | import util.JsonSerializers._
4 |
5 | object ColumnDetails {
6 | implicit val jsonEncoder: Encoder[ColumnDetails] = deriveEncoder
7 | implicit val jsonDecoder: Decoder[ColumnDetails] = deriveDecoder
8 | }
9 |
10 | case class ColumnDetails(
11 | count: Long,
12 | distinctCount: Long,
13 | min: Option[Double] = None,
14 | max: Option[Double] = None,
15 | sum: Option[Double] = None,
16 | avg: Option[Double] = None,
17 | variance: Option[Double] = None,
18 | stdDev: Option[Double] = None,
19 | error: Option[String] = None
20 | )
21 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/schema/EnumType.scala:
--------------------------------------------------------------------------------
1 | package models.schema
2 |
3 | import util.JsonSerializers._
4 |
5 | object EnumType {
6 | implicit val jsonEncoder: Encoder[EnumType] = deriveEncoder
7 | implicit val jsonDecoder: Decoder[EnumType] = deriveDecoder
8 | }
9 |
10 | case class EnumType(
11 | key: String,
12 | values: Seq[String]
13 | )
14 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/schema/ForeignKey.scala:
--------------------------------------------------------------------------------
1 | package models.schema
2 |
3 | import util.JsonSerializers._
4 |
5 | object ForeignKey {
6 | implicit val jsonEncoder: Encoder[ForeignKey] = deriveEncoder
7 | implicit val jsonDecoder: Decoder[ForeignKey] = deriveDecoder
8 | }
9 |
10 | case class ForeignKey(
11 | name: String,
12 | targetTable: String,
13 | references: List[Reference]
14 | )
15 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/schema/Index.scala:
--------------------------------------------------------------------------------
1 | package models.schema
2 |
3 | import util.JsonSerializers._
4 |
5 | object Index {
6 | implicit val jsonEncoder: Encoder[Index] = deriveEncoder
7 | implicit val jsonDecoder: Decoder[Index] = deriveDecoder
8 | }
9 |
10 | case class Index(
11 | name: String,
12 | unique: Boolean,
13 | indexType: String,
14 | cardinality: Long,
15 | columns: Seq[IndexColumn]
16 | )
17 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/schema/IndexColumn.scala:
--------------------------------------------------------------------------------
1 | package models.schema
2 |
3 | import util.JsonSerializers._
4 |
5 | object IndexColumn {
6 | implicit val jsonEncoder: Encoder[IndexColumn] = deriveEncoder
7 | implicit val jsonDecoder: Decoder[IndexColumn] = deriveDecoder
8 | }
9 |
10 | case class IndexColumn(name: String, ascending: Boolean) {
11 | override def toString = name + (if (ascending) { "" } else { " (desc)" })
12 | }
13 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/schema/PrimaryKey.scala:
--------------------------------------------------------------------------------
1 | package models.schema
2 |
3 | import util.JsonSerializers._
4 |
5 | object PrimaryKey {
6 | implicit val jsonEncoder: Encoder[PrimaryKey] = deriveEncoder
7 | implicit val jsonDecoder: Decoder[PrimaryKey] = deriveDecoder
8 | }
9 |
10 | case class PrimaryKey(
11 | name: String,
12 | columns: List[String]
13 | )
14 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/schema/Procedure.scala:
--------------------------------------------------------------------------------
1 | package models.schema
2 |
3 | import util.JsonSerializers._
4 |
5 | object Procedure {
6 | implicit val jsonEncoder: Encoder[Procedure] = deriveEncoder
7 | implicit val jsonDecoder: Decoder[Procedure] = deriveDecoder
8 | }
9 |
10 | case class Procedure(
11 | name: String,
12 | description: Option[String],
13 | params: Seq[ProcedureParam],
14 | returnsResult: Option[Boolean],
15 | loadedAt: Long = System.currentTimeMillis
16 | ) {
17 | def getValues(paramMap: Map[String, String]) = params.flatMap { p =>
18 | p.paramType match {
19 | case "in" => Some(p.name -> paramMap.get(p.name).orNull)
20 | case _ => throw new IllegalStateException(p.paramType)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/schema/ProcedureParam.scala:
--------------------------------------------------------------------------------
1 | package models.schema
2 |
3 | import util.JsonSerializers._
4 |
5 | object ProcedureParam {
6 | implicit val jsonEncoder: Encoder[ProcedureParam] = deriveEncoder
7 | implicit val jsonDecoder: Decoder[ProcedureParam] = deriveDecoder
8 | }
9 |
10 | case class ProcedureParam(
11 | name: String,
12 | description: Option[String],
13 | paramType: String,
14 | columnType: ColumnType,
15 | sqlTypeCode: Int,
16 | sqlTypeName: String,
17 | nullable: Boolean
18 | )
19 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/schema/Reference.scala:
--------------------------------------------------------------------------------
1 | package models.schema
2 |
3 | import util.JsonSerializers._
4 |
5 | object Reference {
6 | implicit val jsonEncoder: Encoder[Reference] = deriveEncoder
7 | implicit val jsonDecoder: Decoder[Reference] = deriveDecoder
8 | }
9 |
10 | case class Reference(source: String, target: String)
11 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/schema/Table.scala:
--------------------------------------------------------------------------------
1 | package models.schema
2 |
3 | import java.util.UUID
4 |
5 | import util.JsonSerializers._
6 |
7 | object Table {
8 | PrimaryKey("p", Nil).asJson
9 | ForeignKey("f", "k", Nil).asJson
10 | implicit val jsonEncoder: Encoder[Table] = deriveEncoder
11 | implicit val jsonDecoder: Decoder[Table] = deriveDecoder
12 | }
13 |
14 | case class Table(
15 | name: String,
16 | connection: UUID,
17 | catalog: Option[String],
18 | schema: Option[String],
19 | description: Option[String],
20 | definition: Option[String],
21 |
22 | storageEngine: Option[String] = None,
23 |
24 | rowCountEstimate: Option[Long] = None,
25 | averageRowLength: Option[Int] = None,
26 | dataLength: Option[Long] = None,
27 |
28 | columns: Seq[Column] = Nil,
29 | rowIdentifier: Seq[String] = Nil,
30 | primaryKey: Option[PrimaryKey] = None,
31 | foreignKeys: Seq[ForeignKey] = Nil,
32 | indexes: Seq[Index] = Nil,
33 |
34 | createTime: Option[Long] = None,
35 | loadedAt: Long = System.currentTimeMillis
36 | )
37 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/schema/View.scala:
--------------------------------------------------------------------------------
1 | package models.schema
2 |
3 | import java.util.UUID
4 |
5 | import util.JsonSerializers._
6 |
7 | object View {
8 | implicit val jsonEncoder: Encoder[View] = deriveEncoder
9 | implicit val jsonDecoder: Decoder[View] = deriveDecoder
10 | }
11 |
12 | case class View(
13 | name: String,
14 | connection: UUID,
15 | catalog: Option[String],
16 | schema: Option[String],
17 | description: Option[String],
18 | definition: Option[String],
19 |
20 | columns: Seq[Column] = Nil,
21 |
22 | loadedAt: Long = System.currentTimeMillis
23 | )
24 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/user/Permission.scala:
--------------------------------------------------------------------------------
1 | package models.user
2 |
3 | import enumeratum._
4 |
5 | sealed abstract class Permission(val id: String) extends EnumEntry {
6 | override val toString = id
7 | }
8 |
9 | object Permission extends Enum[Permission] with CirceEnum[Permission] {
10 | case object Visitor extends Permission("visitor")
11 | case object User extends Permission("user")
12 | case object Administrator extends Permission("admin")
13 | case object Private extends Permission("private")
14 |
15 | override def values = findValues
16 | }
17 |
--------------------------------------------------------------------------------
/shared/src/main/scala/models/user/UserPreferences.scala:
--------------------------------------------------------------------------------
1 | package models.user
2 |
3 | import models.template.Theme
4 | import util.JsonSerializers._
5 |
6 | object UserPreferences {
7 | implicit val jsonEncoder: Encoder[UserPreferences] = deriveEncoder
8 | implicit val jsonDecoder: Decoder[UserPreferences] = deriveDecoder
9 |
10 | val empty = UserPreferences()
11 |
12 | def readFrom(s: String) = decodeJson[UserPreferences](s) match {
13 | case Right(x) => x
14 | case Left(_) => UserPreferences.empty
15 | }
16 | }
17 |
18 | case class UserPreferences(
19 | theme: Theme = Theme.BlueGrey
20 | )
21 |
--------------------------------------------------------------------------------
/shared/src/main/scala/util/Config.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | object Config {
4 |
5 | val projectId = "databaseflow"
6 | val projectName = "Database Flow"
7 | val projectSlug = "database-flow"
8 | val projectUrl = "https://databaseflow.com"
9 | val projectVersion = "1.0.0"
10 | val pageSize = 100
11 | }
12 |
--------------------------------------------------------------------------------
/shared/src/main/scala/util/NullUtils.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | object NullUtils {
4 | val char = '∅'
5 | val str = char.toString
6 |
7 | val inst = None.orNull
8 |
9 | def isNull(v: Any) = v == inst
10 | def notNull(v: Any) = v != inst
11 | }
12 |
--------------------------------------------------------------------------------
/shared/src/main/scala/util/StringKeyUtils.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | object StringKeyUtils {
4 | private[this] def badChars = Seq(" " -> "_", "." -> "_", "(" -> "", ")" -> "", "#" -> "", "!" -> "")
5 |
6 | def cleanName(s: String) = {
7 | val swapped = badChars.foldLeft(s)((l, r) => l.replaceAllLiterally(r._1, r._2))
8 | if (swapped.head.isLetter) { swapped } else { "_" + swapped }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/shared/src/main/scala/util/TipsAndTricks.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import enumeratum._
4 |
5 | sealed abstract class TipsAndTricks(val key: String) extends EnumEntry
6 |
7 | object TipsAndTricks extends Enum[TipsAndTricks] with CirceEnum[TipsAndTricks] {
8 | case object ProfileTheme extends TipsAndTricks("theme")
9 | case object SearchHotkey extends TipsAndTricks("hotkey.search")
10 | case object NewQuery extends TipsAndTricks("hotkey.new")
11 | case object SwitchTabs extends TipsAndTricks("hotkey.switch")
12 | case object OpenInNewTab extends TipsAndTricks("new.tab")
13 |
14 | override val values = findValues
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/test/resources/plan/mysql-nested-loop.json:
--------------------------------------------------------------------------------
1 | {
2 | "query_block": {
3 | "select_id": 1,
4 | "nested_loop": [
5 | {
6 | "table": {
7 | "table_name": "actor",
8 | "access_type": "ALL",
9 | "rows": 200,
10 | "filtered": 100
11 | }
12 | },
13 | {
14 | "table": {
15 | "table_name": "city",
16 | "access_type": "ALL",
17 | "rows": 775,
18 | "filtered": 100,
19 | "using_join_buffer": "Block Nested Loop"
20 | }
21 | }
22 | ]
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/test/resources/plan/mysql-nested-loop.sql:
--------------------------------------------------------------------------------
1 | select * from actor, city limit 5;
2 |
--------------------------------------------------------------------------------
/test/resources/plan/postgres-complicated-query.sql:
--------------------------------------------------------------------------------
1 | SELECT film.film_id AS fid,
2 | film.title,
3 | film.description,
4 | category.name AS category,
5 | film.rental_rate AS price,
6 | film.length,
7 | film.rating,
8 | group_concat(((upper("substring"(actor.first_name::text, 1, 1)) || lower("substring"(actor.first_name::text, 2))) || upper("substring"(actor.last_name::text, 1, 1))) || lower("substring"(actor.last_name::text, 2))) AS actors
9 | FROM category
10 | LEFT JOIN film_category ON category.category_id = film_category.category_id
11 | LEFT JOIN film ON film_category.film_id = film.film_id
12 | JOIN film_actor ON film.film_id = film_actor.film_id
13 | JOIN actor ON film_actor.actor_id = actor.actor_id
14 | GROUP BY film.film_id, film.title, film.description, category.name, film.rental_rate, film.length, film.rating;
15 |
--------------------------------------------------------------------------------
/test/resources/plan/postgres-nested-join.sql:
--------------------------------------------------------------------------------
1 | select * from actor, address, city limit 5;
2 |
--------------------------------------------------------------------------------
/test/test/plan/MySqlPlanParseTest.scala:
--------------------------------------------------------------------------------
1 | package test.plan
2 |
3 | import models.engine.MySQL
4 | import org.scalatest.{FlatSpec, Matchers}
5 |
6 | class MySqlPlanParseTest extends FlatSpec with Matchers {
7 | "MySQL Plan Parser" should "load basic MySQL plan" in {
8 | val result = PlanParseTestHelper.test("mysql-nested-loop", MySQL)
9 | PlanParseTestHelper.debugPlanResult(result)
10 | 1 should be(1)
11 | }
12 |
13 | it should "load complex MySQL plan" in {
14 | val result = PlanParseTestHelper.test("mysql-complicated-query", MySQL)
15 | PlanParseTestHelper.debugPlanResult(result)
16 | 1 should be(1)
17 | }
18 |
19 | it should "throw IllegalArgumentException if invalid JSON is passed" in {
20 | a[IllegalArgumentException] should be thrownBy {
21 | throw new IllegalArgumentException("!")
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/test/test/plan/PostgresPlanParseTest.scala:
--------------------------------------------------------------------------------
1 | package test.plan
2 |
3 | import models.engine.DatabaseEngine.PostgreSQL
4 | import org.scalatest.{FlatSpec, Matchers}
5 |
6 | class PostgresPlanParseTest extends FlatSpec with Matchers {
7 | "PostgreSQL Plan Parser" should "load basic PostgreSQL plan" in {
8 | val result = PlanParseTestHelper.test("postgres-nested-join", PostgreSQL)
9 | 1 should be(1)
10 | }
11 |
12 | it should "load complex PostgreSQL plan" in {
13 | val result = PlanParseTestHelper.test("postgres-complicated-query", PostgreSQL)
14 | PlanParseTestHelper.debugPlanResult(result)
15 | 1 should be(1)
16 | }
17 |
18 | it should "throw IllegalArgumentException if invalid JSON is passed" in {
19 | a[IllegalArgumentException] should be thrownBy {
20 | throw new IllegalArgumentException("!")
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------