├── .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 |
4 |
@message
5 |
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) 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 | 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 |
7 |
@result.sql
8 |
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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------