diff --git a/api/.dockerignore b/api/.dockerignore new file mode 100644 index 0000000..faf23bd --- /dev/null +++ b/api/.dockerignore @@ -0,0 +1,10 @@ +.dart_tool +.idea +.pub +.vscode +logs/ +test/ +build/ +.analysis-options +.packages +*.g.dart \ No newline at end of file diff --git a/api/.gitignore b/api/.gitignore index 57ad605..a0541d9 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -1,28 +1,95 @@ # Created by .ignore support plugin (hsz.mobi) ### Dart template -# See https://www.dartlang.org/guides/libraries/private-files +# See https://www.dartlang.org/tools/private-files.html + +# source_gen +.dart_tool # Files and directories created by pub -.dart_tool/ +.buildlog .packages +.project +.pub/ +.scripts-bin/ build/ -# If you're building an application, you may want to check-in your pubspec.lock -pubspec.lock - -# Directory created by dartdoc -# If you don't generate documentation locally you can remove this line. -doc/api/ +**/packages/ -# Avoid committing generated Javascript files: +# Files created by dart2js +# (Most Dart developers will use pub build to compile Dart, use/modify these +# rules if you intend to use dart2js directly +# Convention is to use extension '.dart.js' for Dart compiled to Javascript to +# differentiate from explicit Javascript files) *.dart.js -*.info.json # Produced by the --dump-info flag. -*.js # When generated by dart2js. Don't specify *.js if your - # project includes source files written in JavaScript. -*.js_ +*.part.js *.js.deps *.js.map +*.info.json + +# Directory created by dartdoc +doc/api/ + +# Don't commit pubspec lock file +# (Library packages only! Remove pattern if developing an application package) +pubspec.lock +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/workspace.xml +.idea/tasks.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml + +# Sensitive or high-churn files: +.idea/dataSources.ids +.idea/dataSources.xml +.idea/dataSources.local.xml +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml + +# Gradle: +.idea/gradle.xml +.idea/libraries + +# Mongo Explorer plugin: +.idea/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### VSCode template +.vscode/ +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +logs/ +*.pem +.DS_Store +server_log.txt -.flutter-plugins -.flutter-plugins-dependencies +.metals/ -config.yaml \ No newline at end of file +/config/production.yaml +/config/development.yaml diff --git a/api/AUTHORS.md b/api/AUTHORS.md new file mode 100644 index 0000000..ac95ab5 --- /dev/null +++ b/api/AUTHORS.md @@ -0,0 +1,12 @@ +Primary Authors +=============== + +* __[Thomas Hii](dukefirehawk.apps@gmail.com)__ + + Thomas is the current maintainer of the code base. He has refactored and migrated the + code base to support NNBD. + +* __[Tobe O](thosakwe@gmail.com)__ + + Tobe has written much of the original code prior to NNBD migration. He has moved on and + is no longer involved with the project. diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md new file mode 100644 index 0000000..6c9985b --- /dev/null +++ b/api/CHANGELOG.md @@ -0,0 +1,9 @@ +# Change Log + +## 1.0.0 + +* Changed to use `angel3` packages +* Updated to support NNBD +* Updated README +* Updated default `postgresql` setup +* Updated linter to `package:lints` diff --git a/api/CONTRIBUTING.md b/api/CONTRIBUTING.md new file mode 100644 index 0000000..b34fbca --- /dev/null +++ b/api/CONTRIBUTING.md @@ -0,0 +1,24 @@ +# Contribution + +Any help from the open-source community is always welcome and needed: + +1. Found an issue? + - Please [fill a bug report][tracker] with error message and steps to reproduce it. +2. Wish a feature? + - Open a feature request with use cases. +3. Are you using and liking the project? + - Create an article about your use case + - Do a post on your likes and dislikes + - Make a donation. +4. Are you a developer? + - Fix a bug and send a [pull request][pull_request] + - Implement a new feature + - Improve the Unit Tests + - Improve the [User Guide][doc] and send a [document pull request][doc_repo] +5. Have you already helped in any way? + - **Many thanks to the contributors and everybody that uses this project!** + +[tracker]: https://github.com/dukefirehawk/angel/issues +[pull_request]: https://github.com/dukefirehawk/angel/pulls +[doc]: https://angel3-docs.dukefirehawk.com +[doc_repo]: https://github.com/dukefirehawk/angel3-guide/pulls \ No newline at end of file diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..c84bc87 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,14 @@ +FROM google/dart:latest + +COPY ./ ./ + +# Install dependencies, pre-build +RUN pub get + +# Optionally build generaed sources. +# RUN pub run build_runner build + +# Set environment, start server +ENV ANGEL_ENV=production +EXPOSE 3000 +CMD dart bin/prod.dart diff --git a/api/LICENSE b/api/LICENSE new file mode 100644 index 0000000..df5e063 --- /dev/null +++ b/api/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2021, dukefirehawk.com +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/api/README.md b/api/README.md index 7849780..2088a61 100644 --- a/api/README.md +++ b/api/README.md @@ -1,37 +1,62 @@ -# dde_gesture_manager +# ORM Starter Application for Angel3 framework -## Running the Application Locally +This is an ORM starter application for [Angel3 framework](https://angel3-framework.web.app) which is a full-stack Web framework in Dart. The default database is `postgresql`. `mysql` support is still in active development. -Run `conduit serve` from this directory to run the application. For running within an IDE, run `bin/main.dart`. By default, a configuration file named `config.yaml` will be used. +## Installation & Setup -You must have a `config.yaml` file that has correct database connection info, which should point to a local database. To configure a database to match your application's schema, run the following commands: +1. Download and install [Dart](https://dart.dev/get-dart). +2. Install `postgresql` version 9, 10, 11 or 12. **postgresql 13 is not working as the driver do not support SCRAM** +3. Create a new user and database in postgres using `psql` cli. For example: -``` -# if this is a project, run db generate first -conduit db generate -conduit db upgrade --connect postgres://user:password@localhost:5432/app_name -``` + ```sql + postgres=# create database appdb; + postgres=# create user appuser with encrypted password 'App1970#'; + postgres=# grant all privileges on database appdb to appuser; + ``` -To generate a SwaggerUI client, run `conduit document client`. +4. Update the `postgres` section in the `config/default.yaml` file with the newly created user and database name. -## Running Application Tests + ```yaml + postgres: + host: localhost + port: 5432 + database_name: appdb + username: appuser + password: App1970# + useSSL: false + time_zone: UTC + ``` -Tests are run with a local PostgreSQL database named `conduit_test_db`. If this database does not exist, create it from your SQL prompt: +5. Run the migration to generate `migrations` and `greetings` tables in the database. -CREATE DATABASE conduit_test_db; -CREATE USER conduit_test_user WITH createdb; -ALTER USER conduit_test_user WITH password 'conduit!'; -GRANT all ON DATABASE conduit_test_db TO conduit_test_user; + ```bash + dart bin/migration.dart + ``` +### Development -To run all tests for this application, run the following in this directory: +1. Run the following command to start Angel3 server in dev mode to *hot-reloaded* on file changes: -``` -pub run test -``` + ```bash + dart --observe bin/dev.dart + ``` -The default configuration file used when testing is `config.src.yaml`. This file should be checked into version control. It also the template for configuration files used in deployment. +2. Modify the code and watch the changes applied to the application -## Deploying an Application +### Production -See the documentation for [Deployment](https://conduit.io/docs/deploy/). \ No newline at end of file +1. Run the following command: + + ```bash + dart bin/prod.dart + ``` + +2. Run as docker. Edit and run the provided `Dockerfile` to build the image. + +## Resources + +Visit the [Developer Guide](https://angel3-docs.dukefirehawk.com/guides) for dozens of guides and resources, including video tutorials, to get up and running as quickly as possible with Angel3. + +Examples and complete projects can be found [here](https://angel3-framework.web.app/#/examples). + +You can also view the [API Documentation](https://pub.dev/documentation/angel3_framework/latest/). diff --git a/api/analysis_options.yaml b/api/analysis_options.yaml index d474280..d66fc45 100644 --- a/api/analysis_options.yaml +++ b/api/analysis_options.yaml @@ -1,98 +1,6 @@ -analyzer: - strong-mode: - implicit-casts: false +include: package:lints/recommended.yaml linter: rules: - - always_declare_return_types - - always_put_control_body_on_new_line - - always_put_required_named_parameters_first - - always_require_non_null_named_parameters - - annotate_overrides - - avoid_bool_literals_in_conditional_expressions - - avoid_double_and_int_checks - - avoid_empty_else - - avoid_field_initializers_in_const_classes - - avoid_init_to_null - - avoid_null_checks_in_equality_operators - - avoid_positional_boolean_parameters - - avoid_relative_lib_imports - - avoid_renaming_method_parameters - - avoid_return_types_on_setters - - avoid_returning_null - - avoid_single_cascade_in_expression_statements - - avoid_slow_async_io - - avoid_types_as_parameter_names - - avoid_unused_constructor_parameters - - await_only_futures - - camel_case_types - - cancel_subscriptions - - close_sinks - - comment_references - - constant_identifier_names - - control_flow_in_finally - - directives_ordering - - empty_catches - - empty_constructor_bodies - - empty_statements - - hash_and_equals - - implementation_imports - - invariant_booleans - - iterable_contains_unrelated_type - - join_return_with_assignment - - library_names - - library_prefixes - - list_remove_unrelated_type - - literal_only_boolean_expressions - - no_duplicate_case_values - - non_constant_identifier_names - - null_closures - - package_api_docs - - package_names - - package_prefixed_library_names - - parameter_assignments - - prefer_adjacent_string_concatenation - - prefer_asserts_in_initializer_lists - - prefer_collection_literals - - prefer_conditional_assignment - - prefer_const_constructors - - prefer_const_constructors_in_immutables - - prefer_const_declarations - - prefer_const_literals_to_create_immutables - - prefer_constructors_over_static_methods - - prefer_contains - - prefer_equal_for_default_values - - prefer_final_fields - - prefer_final_locals - - prefer_foreach - - prefer_generic_function_type_aliases - - prefer_interpolation_to_compose_strings - - prefer_is_empty - - prefer_is_not_empty - - prefer_iterable_whereType - - prefer_typing_uninitialized_variables - - recursive_getters - - slash_for_doc_comments - - sort_constructors_first - - sort_unnamed_constructors_first - - test_types_in_equals - - throw_in_finally - - type_annotate_public_apis - - type_init_formals - - unawaited_futures - - unnecessary_const - - unnecessary_getters_setters - - unnecessary_lambdas - - unnecessary_new - - unnecessary_null_aware_assignments - - unnecessary_null_in_if_null_operators - - unnecessary_overrides - - unnecessary_parenthesis - - unnecessary_statements - - unnecessary_this - - unrelated_type_equality_checks - - use_rethrow_when_possible - - use_string_buffers - - use_to_and_as_if_applicable - - valid_regexps - - void_checks \ No newline at end of file + avoid_renaming_method_parameters: false + overridden_fields: false diff --git a/api/bin/dev.dart b/api/bin/dev.dart new file mode 100644 index 0000000..05028ae --- /dev/null +++ b/api/bin/dev.dart @@ -0,0 +1,28 @@ +import 'dart:io'; +import 'package:dde_gesture_manager_api/dde_gesture_manager_api.dart'; +import 'package:belatuk_pretty_logging/belatuk_pretty_logging.dart'; +import 'package:angel3_container/mirrors.dart'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_hot/angel3_hot.dart'; +import 'package:logging/logging.dart'; + +void main() async { + // Watch the config/ and web/ directories for changes, and hot-reload the server. + hierarchicalLoggingEnabled = true; + + var hot = HotReloader(() async { + var logger = Logger.detached('dde_gesture_manager_api') + ..level = Level.ALL + ..onRecord.listen(prettyLog); + var app = Angel(logger: logger, reflector: MirrorsReflector()); + await app.configure(configureServer); + return app; + }, [ + Directory('config'), + Directory('lib'), + ]); + + var server = await hot.startServer('127.0.0.1', 3000); + print( + 'dde_gesture_manager_api server listening at http://${server.address.address}:${server.port}'); +} diff --git a/api/bin/main.dart b/api/bin/main.dart deleted file mode 100644 index 37d4c6c..0000000 --- a/api/bin/main.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:dde_gesture_manager/dde_gesture_manager.dart'; - -Future main() async { - final app = Application() - ..options.configurationFilePath = "config.yaml" - ..options.port = 8888; - - await app.startOnCurrentIsolate(); - - print("Application started on port: ${app.options.port}."); - print("Click to open in browser: http://localhost:${app.options.port}"); - print("Use Ctrl-C (SIGINT) to stop running the application."); -} diff --git a/api/bin/migrate.dart b/api/bin/migrate.dart new file mode 100644 index 0000000..5c5e413 --- /dev/null +++ b/api/bin/migrate.dart @@ -0,0 +1,28 @@ +import 'package:dde_gesture_manager_api/src/config/plugins/orm.dart'; +import 'package:dde_gesture_manager_api/models.dart'; +import 'package:angel3_configuration/angel3_configuration.dart'; +import 'package:angel3_migration_runner/angel3_migration_runner.dart'; +import 'package:angel3_migration_runner/postgres.dart'; +import 'package:file/local.dart'; +import 'package:logging/logging.dart'; + +void main(List args) async { + // Enable the logging + Logger.root.level = Level.INFO; + Logger.root.onRecord.listen((rec) { + print('${rec.time}: ${rec.level.name}: ${rec.loggerName}: ${rec.message}'); + + if (rec.error != null) { + print(rec.error); + print(rec.stackTrace); + } + }); + + var fs = LocalFileSystem(); + var configuration = await loadStandaloneConfiguration(fs); + var connection = await connectToPostgres(configuration); + var migrationRunner = PostgresMigrationRunner(connection, migrations: [ + UserMigration(), + ]); + await runMigrations(migrationRunner, args); +} diff --git a/api/bin/prod.dart b/api/bin/prod.dart new file mode 100644 index 0000000..3a4437b --- /dev/null +++ b/api/bin/prod.dart @@ -0,0 +1,27 @@ +import 'package:dde_gesture_manager_api/dde_gesture_manager_api.dart'; +import 'package:angel3_production/angel3_production.dart'; + +// NOTE: By default, the Runner class does not use the `MirrorsReflector`, or any +// reflector, by default. +// +// If your application is using any sort of functionality reliant on annotations or reflection, +// either include the MirrorsReflector, or use a static reflector variant. +// +// The following use cases require reflection: +// * Use of Controllers, via @Expose() or @ExposeWS() +// * Use of dependency injection into constructors, whether in controllers or plain `container.make` calls +// * Use of the `ioc` function in any route +// +// The `MirrorsReflector` from `package:angel_container/mirrors.dart` is by far the most convenient pattern, +// so use it if possible. +// +// However, the following alternatives exist: +// * Generation via `package:angel_container_generator` +// * Creating an instance of `StaticReflector` +// * Manually implementing the `Reflector` interface (cumbersome; not recommended) +// +// As of January 4th, 2018, the documentation has not yet been updated to state this, +// so in the meantime, visit the Angel chat for further questions: +// +// https://gitter.im/angel_dart/discussion +void main(List args) => Runner('dde_gesture_manager_api', configureServer).run(args); diff --git a/api/config.src.yaml b/api/config.src.yaml deleted file mode 100644 index d3edf9a..0000000 --- a/api/config.src.yaml +++ /dev/null @@ -1,6 +0,0 @@ -database: - username: conduit_test_user - password: conduit! - host: localhost - port: 15432 # change this value - databaseName: conduit_test_db diff --git a/api/config/default.yaml b/api/config/default.yaml new file mode 100644 index 0000000..16dd769 --- /dev/null +++ b/api/config/default.yaml @@ -0,0 +1,12 @@ +# Default server configuration. +host: 127.0.0.1 +port: 3000 +postgres: + host: localhost + port: 5432 + database_name: appdb + username: appuser + password: App1970# + useSSL: false + time_zone: UTC +jwt_secret: "OvA9SBLnncot8gFHvt8Gh1qkQ1ptGIQW" \ No newline at end of file diff --git a/api/lib/channel.dart b/api/lib/channel.dart deleted file mode 100644 index 2eeca45..0000000 --- a/api/lib/channel.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:dde_gesture_manager/dde_gesture_manager.dart'; -import 'package:dde_gesture_manager/model/model.dart'; - -/// This type initializes an application. -/// -/// Override methods in this class to set up routes and initialize services like -/// database connections. See http://conduit.io/docs/http/channel/. -class DdeGestureManagerChannel extends ApplicationChannel { - late ManagedContext context; - - /// Initialize services in this method. - /// - /// Implement this method to initialize services, read values from [options] - /// and any other initialization required before constructing [entryPoint]. - /// - /// This method is invoked prior to [entryPoint] being accessed. - @override - Future prepare() async { - logger.onRecord.listen( - (rec) => print("$rec ${rec.error ?? ""} ${rec.stackTrace ?? ""}")); - - final config = - DdeGestureManagerConfiguration(options!.configurationFilePath!); - context = contextWithConnectionInfo(config.database!); - } - - /// Construct the request channel. - /// - /// Return an instance of some [Controller] that will be the initial receiver - /// of all [Request]s. - /// - /// This method is invoked after [prepare]. - @override - Controller get entryPoint { - final router = Router(); - - router - .route("/model/[:id]") - .link(() => ManagedObjectController(context)); - - router - .route("/") - .linkFunction((Request request) async => Response.ok('ok')); - - return router; - } - - /* - * Helper methods - */ - - ManagedContext contextWithConnectionInfo( - DatabaseConfiguration connectionInfo) { - final dataModel = ManagedDataModel.fromCurrentMirrorSystem(); - final psc = PostgreSQLPersistentStore( - connectionInfo.username, - connectionInfo.password, - connectionInfo.host, - connectionInfo.port, - connectionInfo.databaseName); - - return ManagedContext(dataModel, psc); - } -} - -/// An instance of this class reads values from a configuration -/// file specific to this application. -/// -/// Configuration files must have key-value for the properties in this class. -/// For more documentation on configuration files, see https://conduit.io/docs/configure/ and -/// https://pub.dartlang.org/packages/safe_config. -class DdeGestureManagerConfiguration extends Configuration { - DdeGestureManagerConfiguration(String fileName) - : super.fromFile(File(fileName)); - - DatabaseConfiguration? database; -} diff --git a/api/lib/dde_gesture_manager.dart b/api/lib/dde_gesture_manager.dart deleted file mode 100644 index 713096c..0000000 --- a/api/lib/dde_gesture_manager.dart +++ /dev/null @@ -1,11 +0,0 @@ -/// dde_gesture_manager -/// -/// A conduit web server. -library dde_gesture_manager; - -export 'dart:async'; -export 'dart:io'; - -export 'package:conduit/conduit.dart'; - -export 'channel.dart'; diff --git a/api/lib/dde_gesture_manager_api.dart b/api/lib/dde_gesture_manager_api.dart new file mode 100644 index 0000000..aa2f208 --- /dev/null +++ b/api/lib/dde_gesture_manager_api.dart @@ -0,0 +1,17 @@ +/// Your very own web application! +import 'dart:async'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:file/local.dart'; +import 'src/config/config.dart' as configuration; +import 'src/routes/routes.dart' as routes; + +/// Configures the server instance. +Future configureServer(Angel app) async { + // Grab a handle to the file system, so that we can do things like + // serve static files. + var fs = const LocalFileSystem(); + + // Set up our application, using the plug-ins defined with this project. + await app.configure(configuration.configureServer(fs)); + await app.configure(routes.configureServer(fs)); +} diff --git a/api/lib/model/model.dart b/api/lib/model/model.dart deleted file mode 100644 index 6eb8a1b..0000000 --- a/api/lib/model/model.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:dde_gesture_manager/dde_gesture_manager.dart'; - -class Model extends ManagedObject<_Model> implements _Model { - @override - void willInsert() { - createdAt = DateTime.now().toUtc(); - } -} - -class _Model { - @primaryKey - int? id; - - @Column(indexed: true) - String? name; - - DateTime? createdAt; -} diff --git a/api/lib/models.dart b/api/lib/models.dart new file mode 100644 index 0000000..b72d675 --- /dev/null +++ b/api/lib/models.dart @@ -0,0 +1 @@ +export 'src/models/user.dart'; diff --git a/api/lib/src/config/config.dart b/api/lib/src/config/config.dart new file mode 100644 index 0000000..358f42e --- /dev/null +++ b/api/lib/src/config/config.dart @@ -0,0 +1,30 @@ +import 'package:angel3_configuration/angel3_configuration.dart'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_jinja/angel3_jinja.dart'; +import 'package:file/file.dart'; +import 'plugins/plugins.dart' as plugins; + +/// This is a perfect place to include configuration and load plug-ins. +AngelConfigurer configureServer(FileSystem fileSystem) { + return (Angel app) async { + // Load configuration from the `config/` directory. + // + // See: https://github.com/angel-dart/configuration + await app.configure(configuration(fileSystem)); + + // Configure our application to render jinja templates from the `views/` directory. + // + // See: https://github.com/angel-dart/jinja + await app.configure(jinja(path: fileSystem.directory('views').path)); + + // Apply another plug-ins, i.e. ones that *you* have written. + // + // Typically, the plugins in `lib/src/config/plugins/plugins.dart` are plug-ins + // that add functionality specific to your application. + // + // If you write a plug-in that you plan to use again, or are + // using one created by the community, include it in + // `lib/src/config/config.dart`. + await plugins.configureServer(app); + }; +} diff --git a/api/lib/src/config/plugins/orm.dart b/api/lib/src/config/plugins/orm.dart new file mode 100644 index 0000000..be286f8 --- /dev/null +++ b/api/lib/src/config/plugins/orm.dart @@ -0,0 +1,35 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_orm/angel3_orm.dart'; +import 'package:angel3_orm_postgres/angel3_orm_postgres.dart'; +import 'package:postgres/postgres.dart'; + +Future configureServer(Angel app) async { + var connection = await connectToPostgres(app.configuration); + await connection.open(); + + var logger = app.environment.isProduction ? null : app.logger; + var executor = PostgreSqlExecutor(connection, logger: logger); + + app + ..container!.registerSingleton(executor) + ..shutdownHooks.add((_) => connection.close()); +} + +Future connectToPostgres(Map configuration) async { + var postgresConfig = configuration['postgres'] as Map? ?? {}; + var connection = PostgreSQLConnection( + postgresConfig['host'] as String? ?? 'localhost', + postgresConfig['port'] as int? ?? 5432, + postgresConfig['database_name'] as String? ?? + Platform.environment['USER'] ?? + Platform.environment['USERNAME'] ?? + '', + username: postgresConfig['username'] as String?, + password: postgresConfig['password'] as String?, + timeZone: postgresConfig['time_zone'] as String? ?? 'UTC', + timeoutInSeconds: postgresConfig['timeout_in_seconds'] as int? ?? 30, + useSSL: postgresConfig['use_ssl'] as bool? ?? false); + return connection; +} diff --git a/api/lib/src/config/plugins/plugins.dart b/api/lib/src/config/plugins/plugins.dart new file mode 100644 index 0000000..acda711 --- /dev/null +++ b/api/lib/src/config/plugins/plugins.dart @@ -0,0 +1,8 @@ +import 'dart:async'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'orm.dart' as orm; + +Future configureServer(Angel app) async { + // Include any plugins you have made here. + await app.configure(orm.configureServer); +} diff --git a/api/lib/src/models/base_model.dart b/api/lib/src/models/base_model.dart new file mode 100644 index 0000000..4704517 --- /dev/null +++ b/api/lib/src/models/base_model.dart @@ -0,0 +1,8 @@ +import 'package:angel3_orm/angel3_orm.dart'; +import 'package:angel3_serialize/angel3_serialize.dart'; + +abstract class BaseModel extends Model { + @SerializableField(isNullable: true) + @Column(type: ColumnType.json) + Map? get metadata; +} diff --git a/api/lib/src/models/user.dart b/api/lib/src/models/user.dart new file mode 100644 index 0000000..1288a20 --- /dev/null +++ b/api/lib/src/models/user.dart @@ -0,0 +1,19 @@ +import 'package:angel3_migration/angel3_migration.dart'; +import 'package:angel3_serialize/angel3_serialize.dart'; +import 'package:angel3_orm/angel3_orm.dart'; +import 'package:dde_gesture_manager_api/src/models/base_model.dart'; +import 'package:optional/optional.dart'; +part 'user.g.dart'; + +@serializable +@orm +abstract class _User extends BaseModel { + @SerializableField(isNullable: false) + String? get email; + + @SerializableField(isNullable: false) + String? get password; + + @SerializableField(isNullable: false) + String? get token; +} \ No newline at end of file diff --git a/api/lib/src/models/user.g.dart b/api/lib/src/models/user.g.dart new file mode 100644 index 0000000..669e5f8 --- /dev/null +++ b/api/lib/src/models/user.g.dart @@ -0,0 +1,369 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user.dart'; + +// ************************************************************************** +// MigrationGenerator +// ************************************************************************** + +class UserMigration extends Migration { + @override + void up(Schema schema) { + schema.create('users', (table) { + table.serial('id').primaryKey(); + table.timeStamp('created_at'); + table.timeStamp('updated_at'); + table.declareColumn( + 'metadata', Column(type: ColumnType('jsonb'), length: 256)); + table.varChar('email', length: 256); + table.varChar('password', length: 256); + table.varChar('token', length: 256); + }); + } + + @override + void down(Schema schema) { + schema.drop('users'); + } +} + +// ************************************************************************** +// OrmGenerator +// ************************************************************************** + +class UserQuery extends Query { + UserQuery({Query? parent, Set? trampoline}) : super(parent: parent) { + trampoline ??= {}; + trampoline.add(tableName); + _where = UserQueryWhere(this); + } + + @override + final UserQueryValues values = UserQueryValues(); + + UserQueryWhere? _where; + + @override + Map get casts { + return {}; + } + + @override + String get tableName { + return 'users'; + } + + @override + List get fields { + return const [ + 'id', + 'created_at', + 'updated_at', + 'metadata', + 'email', + 'password', + 'token' + ]; + } + + @override + UserQueryWhere? get where { + return _where; + } + + @override + UserQueryWhere newWhereClause() { + return UserQueryWhere(this); + } + + static User? parseRow(List row) { + if (row.every((x) => x == null)) { + return null; + } + var model = User( + id: row[0].toString(), + createdAt: (row[1] as DateTime?), + updatedAt: (row[2] as DateTime?), + metadata: (row[3] as Map?), + email: (row[4] as String?), + password: (row[5] as String?), + token: (row[6] as String?)); + return model; + } + + @override + Optional deserialize(List row) { + return Optional.ofNullable(parseRow(row)); + } +} + +class UserQueryWhere extends QueryWhere { + UserQueryWhere(UserQuery query) + : id = NumericSqlExpressionBuilder(query, 'id'), + createdAt = DateTimeSqlExpressionBuilder(query, 'created_at'), + updatedAt = DateTimeSqlExpressionBuilder(query, 'updated_at'), + metadata = MapSqlExpressionBuilder(query, 'metadata'), + email = StringSqlExpressionBuilder(query, 'email'), + password = StringSqlExpressionBuilder(query, 'password'), + token = StringSqlExpressionBuilder(query, 'token'); + + final NumericSqlExpressionBuilder id; + + final DateTimeSqlExpressionBuilder createdAt; + + final DateTimeSqlExpressionBuilder updatedAt; + + final MapSqlExpressionBuilder metadata; + + final StringSqlExpressionBuilder email; + + final StringSqlExpressionBuilder password; + + final StringSqlExpressionBuilder token; + + @override + List get expressionBuilders { + return [id, createdAt, updatedAt, metadata, email, password, token]; + } +} + +class UserQueryValues extends MapQueryValues { + @override + Map get casts { + return {}; + } + + String? get id { + return (values['id'] as String?); + } + + set id(String? value) => values['id'] = value; + DateTime? get createdAt { + return (values['created_at'] as DateTime?); + } + + set createdAt(DateTime? value) => values['created_at'] = value; + DateTime? get updatedAt { + return (values['updated_at'] as DateTime?); + } + + set updatedAt(DateTime? value) => values['updated_at'] = value; + Map? get metadata { + return (values['metadata'] as Map?); + } + + set metadata(Map? value) => values['metadata'] = value; + String? get email { + return (values['email'] as String?); + } + + set email(String? value) => values['email'] = value; + String? get password { + return (values['password'] as String?); + } + + set password(String? value) => values['password'] = value; + String? get token { + return (values['token'] as String?); + } + + set token(String? value) => values['token'] = value; + void copyFrom(User model) { + createdAt = model.createdAt; + updatedAt = model.updatedAt; + metadata = model.metadata; + email = model.email; + password = model.password; + token = model.token; + } +} + +// ************************************************************************** +// JsonModelGenerator +// ************************************************************************** + +@generatedSerializable +class User extends _User { + User( + {this.id, + this.createdAt, + this.updatedAt, + Map? metadata, + required this.email, + required this.password, + required this.token}) + : metadata = Map.unmodifiable(metadata ?? {}); + + /// A unique identifier corresponding to this item. + @override + String? id; + + /// The time at which this item was created. + @override + DateTime? createdAt; + + /// The last time at which this item was updated. + @override + DateTime? updatedAt; + + @override + Map? metadata; + + @override + String? email; + + @override + String? password; + + @override + String? token; + + User copyWith( + {String? id, + DateTime? createdAt, + DateTime? updatedAt, + Map? metadata, + String? email, + String? password, + String? token}) { + return User( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + metadata: metadata ?? this.metadata, + email: email ?? this.email, + password: password ?? this.password, + token: token ?? this.token); + } + + @override + bool operator ==(other) { + return other is _User && + other.id == id && + other.createdAt == createdAt && + other.updatedAt == updatedAt && + MapEquality( + keys: DefaultEquality(), values: DefaultEquality()) + .equals(other.metadata, metadata) && + other.email == email && + other.password == password && + other.token == token; + } + + @override + int get hashCode { + return hashObjects( + [id, createdAt, updatedAt, metadata, email, password, token]); + } + + @override + String toString() { + return 'User(id=$id, createdAt=$createdAt, updatedAt=$updatedAt, metadata=$metadata, email=$email, password=$password, token=$token)'; + } + + Map toJson() { + return UserSerializer.toMap(this); + } +} + +// ************************************************************************** +// SerializerGenerator +// ************************************************************************** + +const UserSerializer userSerializer = UserSerializer(); + +class UserEncoder extends Converter { + const UserEncoder(); + + @override + Map convert(User model) => UserSerializer.toMap(model); +} + +class UserDecoder extends Converter { + const UserDecoder(); + + @override + User convert(Map map) => UserSerializer.fromMap(map); +} + +class UserSerializer extends Codec { + const UserSerializer(); + + @override + UserEncoder get encoder => const UserEncoder(); + @override + UserDecoder get decoder => const UserDecoder(); + static User fromMap(Map map) { + if (map['email'] == null) { + throw FormatException("Missing required field 'email' on User."); + } + + if (map['password'] == null) { + throw FormatException("Missing required field 'password' on User."); + } + + if (map['token'] == null) { + throw FormatException("Missing required field 'token' on User."); + } + + return User( + id: map['id'] as String?, + createdAt: map['created_at'] != null + ? (map['created_at'] is DateTime + ? (map['created_at'] as DateTime) + : DateTime.parse(map['created_at'].toString())) + : null, + updatedAt: map['updated_at'] != null + ? (map['updated_at'] is DateTime + ? (map['updated_at'] as DateTime) + : DateTime.parse(map['updated_at'].toString())) + : null, + metadata: map['metadata'] is Map + ? (map['metadata'] as Map).cast() + : {}, + email: map['email'] as String?, + password: map['password'] as String?, + token: map['token'] as String?); + } + + static Map toMap(_User? model) { + if (model == null) { + return {}; + } + return { + 'id': model.id, + 'created_at': model.createdAt?.toIso8601String(), + 'updated_at': model.updatedAt?.toIso8601String(), + 'metadata': model.metadata, + 'email': model.email, + 'password': model.password, + 'token': model.token + }; + } +} + +abstract class UserFields { + static const List allFields = [ + id, + createdAt, + updatedAt, + metadata, + email, + password, + token + ]; + + static const String id = 'id'; + + static const String createdAt = 'created_at'; + + static const String updatedAt = 'updated_at'; + + static const String metadata = 'metadata'; + + static const String email = 'email'; + + static const String password = 'password'; + + static const String token = 'token'; +} diff --git a/api/lib/src/routes/controllers/controller_extensions.dart b/api/lib/src/routes/controllers/controller_extensions.dart new file mode 100644 index 0000000..f0f6007 --- /dev/null +++ b/api/lib/src/routes/controllers/controller_extensions.dart @@ -0,0 +1,21 @@ +import 'dart:io'; + +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_orm/angel3_orm.dart' as orm; + +extension ResponseNoContent on ResponseContext { + noContent() { + statusCode = HttpStatus.noContent; + return close(); + } +} + +extension QueryWhereId on orm.Query { + set whereId(int id) { + (where as dynamic).id.equals(id); + } +} + +extension QueryExecutor on RequestContext { + orm.QueryExecutor get queryExecutor => container!.make(); +} diff --git a/api/lib/src/routes/controllers/user_controllers.dart b/api/lib/src/routes/controllers/user_controllers.dart new file mode 100644 index 0000000..d98b1e3 --- /dev/null +++ b/api/lib/src/routes/controllers/user_controllers.dart @@ -0,0 +1,16 @@ +import 'dart:async'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:dde_gesture_manager_api/models.dart'; +import 'controller_extensions.dart'; + +Future configureServer(Angel app) async { + app.get( + '/user/int:id', + (req, res) async { + var user = await (UserQuery()..where?.metadata.contains({"uid": req.params[UserFields.id]})) + .getOne(req.queryExecutor); + + return res.json(user.value); + }, + ); +} diff --git a/api/lib/src/routes/routes.dart b/api/lib/src/routes/routes.dart new file mode 100644 index 0000000..23a8dec --- /dev/null +++ b/api/lib/src/routes/routes.dart @@ -0,0 +1,39 @@ +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:file/file.dart'; +import 'controllers/user_controllers.dart' as UserControllers; + +/// Put your app routes here! +/// +/// See the wiki for information about routing, requests, and responses: +/// * https://angel3-docs.dukefirehawk.com/guides/basic-routing +/// * https://angel3-docs.dukefirehawk.com/guides/requests-and-responses + +AngelConfigurer configureServer(FileSystem fileSystem) { + return (Angel app) async { + // Typically, you want to mount controllers first, after any global middleware. + await app.configure(UserControllers.configureServer); + + // Throw a 404 if no route matched the request. + app.fallback((req, res) => throw AngelHttpException.notFound()); + + // Set our application up to handle different errors. + // + // Read the following for documentation: + // * https://angel3-docs.dukefirehawk.com/guides/error-handling + + var oldErrorHandler = app.errorHandler; + app.errorHandler = (e, req, res) async { + if (req.accepts('text/html', strict: true)) { + if (e.statusCode == 404 && req.accepts('text/html', strict: true)) { + await res.render('error.html', {'message': 'No router exists for ${req.uri}'}); + } else { + return await res.render('error.html', { + 'message': [e.message, '', e.stackTrace.toString().replaceAll('\n', '
')].join('
') + }); + } + } else { + return await oldErrorHandler(e, req, res); + } + }; + }; +} diff --git a/api/pubspec.yaml b/api/pubspec.yaml index c56bbff..877dd7b 100644 --- a/api/pubspec.yaml +++ b/api/pubspec.yaml @@ -1,13 +1,34 @@ -name: dde_gesture_manager -description: An conduit application with a database connection and data model. -version: 0.0.1 - +name: dde_gesture_manager_api +version: 1.0.0 +description: An ORM starter application for Angel3 framework +publish_to: none environment: - sdk: ">=2.12.0 <3.0.0" - + sdk: '>=2.12.0 <3.0.0' dependencies: - conduit: ^3.0.0 - + angel3_auth: ^4.0.0 + angel3_configuration: ^4.1.0 + angel3_framework: ^4.2.0 + angel3_migration: ^4.0.0 + angel3_orm: ^4.0.0 + angel3_orm_postgres: ^3.0.0 + angel3_serialize: ^4.1.0 + angel3_production: ^3.1.0 + angel3_static: ^4.1.0 + angel3_validate: ^4.0.0 + belatuk_pretty_logging: ^4.0.0 + optional: ^6.0.0 + logging: ^1.0.0 dev_dependencies: - test: ^1.16.5 - conduit_test: ^3.0.0 \ No newline at end of file + angel3_hot: ^4.2.0 + angel3_jinja: ^2.0.1 + angel3_migration_runner: ^4.0.0 + angel3_orm_generator: ^4.1.0 + angel3_serialize_generator: ^4.2.0 + angel3_test: ^4.0.0 + build_runner: ^2.0.3 + io: ^1.0.0 + test: ^1.17.5 + lints: ^1.0.0 + + + diff --git a/api/test/all_test.dart b/api/test/all_test.dart new file mode 100644 index 0000000..cfb69fc --- /dev/null +++ b/api/test/all_test.dart @@ -0,0 +1,43 @@ +import 'package:dde_gesture_manager_api/dde_gesture_manager_api.dart'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_test/angel3_test.dart'; +import 'package:test/test.dart'; + +// Angel also includes facilities to make testing easier. +// +// `package:angel_test` ships a client that can test +// both plain HTTP and WebSockets. +// +// Tests do not require your server to actually be mounted on a port, +// so they will run faster than they would in other frameworks, where you +// would have to first bind a socket, and then account for network latency. +// +// See the documentation here: +// https://github.com/angel-dart/test +// +// If you are unfamiliar with Dart's advanced testing library, you can read up +// here: +// https://github.com/dart-lang/test + +void main() async { + late TestClient client; + + setUp(() async { + var app = Angel(); + await app.configure(configureServer); + + client = await connectTo(app); + }); + + tearDown(() async { + await client.close(); + }); + + test('index returns 200', () async { + // Request a resource at the given path. + var response = await client.get(Uri.parse('/')); + + // Expect a 200 response. + expect(response, hasStatus(200)); + }); +} diff --git a/api/test/harness/app.dart b/api/test/harness/app.dart deleted file mode 100644 index a78b76f..0000000 --- a/api/test/harness/app.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:conduit_test/conduit_test.dart'; -import 'package:dde_gesture_manager/dde_gesture_manager.dart'; - -export 'package:conduit/conduit.dart'; -export 'package:conduit_test/conduit_test.dart'; -export 'package:dde_gesture_manager/dde_gesture_manager.dart'; -export 'package:test/test.dart'; - -/// A testing harness for dde_gesture_manager. -/// -/// A harness for testing an conduit application. Example test file: -/// -/// void main() { -/// Harness harness = Harness()..install(); -/// -/// test("GET /path returns 200", () async { -/// final response = await harness.agent.get("/path"); -/// expectResponse(response, 200); -/// }); -/// } -/// -class Harness extends TestHarness with TestHarnessORMMixin { - @override - ManagedContext? get context => channel?.context; - - @override - Future onSetUp() async { - await resetData(); - } - - @override - Future onTearDown() async {} - - @override - Future seed() async { - // restore any static data. called by resetData. - } -} diff --git a/api/test/simple_controller_test.dart b/api/test/simple_controller_test.dart deleted file mode 100644 index c2c0c9c..0000000 --- a/api/test/simple_controller_test.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'harness/app.dart'; - -Future main() async { - final harness = Harness()..install(); - - tearDown(() async { - await harness.resetData(); - }); - - test("POST /model", () async { - final response = await harness.agent!.post("/model", body: {"name": "Bob"}); - expect( - response, - hasResponse(200, - body: {"id": isNotNull, "name": "Bob", "createdAt": isTimestamp})); - }); - - test("GET /model/:id returns previously created object", () async { - var response = await harness.agent!.post("/model", body: {"name": "Bob"}); - - final createdObject = response?.body.as(); - response = - await harness.agent!.request("/model/${createdObject["id"]}").get(); - expect( - response, - hasResponse(200, body: { - "id": createdObject["id"], - "name": createdObject["name"], - "createdAt": createdObject["createdAt"] - })); - }); -} diff --git a/api/views/error.html b/api/views/error.html new file mode 100644 index 0000000..5b94687 --- /dev/null +++ b/api/views/error.html @@ -0,0 +1,5 @@ +{% extends "layout.html" %} +{% block title %}Error{% endblock %} +{% block body %} +

Error: {{ message }}

+{% endblock %} \ No newline at end of file diff --git a/api/views/layout.html b/api/views/layout.html new file mode 100644 index 0000000..2217499 --- /dev/null +++ b/api/views/layout.html @@ -0,0 +1,10 @@ + + + + + {% block title %}{% endblock %} + + + {% block body %}{% endblock %} + + \ No newline at end of file diff --git a/api/web/css/site.css b/api/web/css/site.css new file mode 100644 index 0000000..9e40b8d --- /dev/null +++ b/api/web/css/site.css @@ -0,0 +1,27 @@ +html, body { + height: 100%; +} + +body { + margin: 0; + padding: 0; + width: 100%; + display: table; + font-weight: 100; + font-family: 'Lato', sans-serif; +} + +.container { + text-align: center; + display: table-cell; + vertical-align: middle; +} + +.content { + text-align: center; + display: inline-block; +} + +.title { + font-size: 96px; +} \ No newline at end of file diff --git a/api/web/images/favicon.png b/api/web/images/favicon.png new file mode 100644 index 0000000..b8f5e2c Binary files /dev/null and b/api/web/images/favicon.png differ diff --git a/api/web/login.html b/api/web/login.html deleted file mode 100644 index df15053..0000000 --- a/api/web/login.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - Login - - - -
-

Login

-
- - - -
- - -
-
- - -
- -
-
- - - \ No newline at end of file diff --git a/api/web/robots.txt b/api/web/robots.txt new file mode 100644 index 0000000..f328961 --- /dev/null +++ b/api/web/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: /admin \ No newline at end of file