From 317aa006e309fbb1c0d4ab84152b6bfe64d6cdfa Mon Sep 17 00:00:00 2001 From: debuggerx Date: Thu, 30 Dec 2021 21:13:48 +0800 Subject: [PATCH 1/6] feat: add upload button. --- app/lib/pages/gesture_editor.dart | 29 +++++++++++++++++++++++++++++ app/lib/utils/notificator.dart | 2 +- app/lib/widgets/dde_button.dart | 17 +++++++++++++++++ app/resources/langs/en.json | 18 +++++++++++++++--- app/resources/langs/zh-CN.json | 16 ++++++++++++++-- 5 files changed, 76 insertions(+), 6 deletions(-) diff --git a/app/lib/pages/gesture_editor.dart b/app/lib/pages/gesture_editor.dart index 537b42d..38c2f2d 100644 --- a/app/lib/pages/gesture_editor.dart +++ b/app/lib/pages/gesture_editor.dart @@ -1,6 +1,7 @@ import 'package:adaptive_scrollbar/adaptive_scrollbar.dart'; import 'package:dde_gesture_manager/constants/constants.dart'; import 'package:dde_gesture_manager/extensions.dart'; +import 'package:dde_gesture_manager/models/configs.provider.dart'; import 'package:dde_gesture_manager/models/content_layout.provider.dart'; import 'package:dde_gesture_manager/models/local_schemes_provider.dart'; import 'package:dde_gesture_manager/models/scheme.dart'; @@ -18,6 +19,7 @@ import 'package:dde_gesture_manager/widgets/table_cell_shortcut_listener.dart'; import 'package:dde_gesture_manager/widgets/table_cell_text_field.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_platform_alert/flutter_platform_alert.dart'; import 'package:uuid/uuid.dart'; const double _headingRowHeight = 56; @@ -292,6 +294,33 @@ class GestureEditor extends StatelessWidget { }, ), ), + Padding( + padding: const EdgeInsets.only(left: 10), + child: DButton.upload( + enabled: schemeProvider.uploaded == false, + onTap: () async { + if (context.read().accessToken.isNull) { + return Notificator.showAlert( + title: LocaleKeys.info_login_for_upload_title.tr(), + description: LocaleKeys.info_login_for_upload_description.tr(), + ).then((value) { + value.sout(); + if (value == CustomButton.positiveButton) { + context + .read() + .setProps(marketOrMeOpened: true, currentIsMarket: false); + } + }); + } + Notificator.showConfirm( + title: LocaleKeys.info_upload_and_share_title.tr(), + description: LocaleKeys.info_upload_and_share_description.tr(), + positiveButtonTitle: LocaleKeys.str_share.tr(), + negativeButtonTitle: LocaleKeys.str_cancel.tr(), + ); + }, + ), + ), ], ), Divider(), diff --git a/app/lib/utils/notificator.dart b/app/lib/utils/notificator.dart index 4872c46..f686dcc 100644 --- a/app/lib/utils/notificator.dart +++ b/app/lib/utils/notificator.dart @@ -15,7 +15,7 @@ class Notificator { return AlertImpl().showAlert( windowTitle: title, text: description, - positiveButtonTitle: positiveButtonTitle, + positiveButtonTitle: positiveButtonTitle ?? LocaleKeys.str_ok.tr(), ); } diff --git a/app/lib/widgets/dde_button.dart b/app/lib/widgets/dde_button.dart index 1b94fb9..7c726e7 100644 --- a/app/lib/widgets/dde_button.dart +++ b/app/lib/widgets/dde_button.dart @@ -124,6 +124,23 @@ class DButton extends StatefulWidget { message: LocaleKeys.operation_logout.tr(), )); + factory DButton.upload({ + Key? key, + required enabled, + GestureTapCallback? onTap, + height = defaultButtonHeight, + width = defaultButtonHeight, + }) => + DButton( + key: key, + width: width, + height: height, + onTap: enabled ? onTap : null, + child: Tooltip( + child: Opacity(opacity: enabled ? 1 : 0.4, child: const Icon(Icons.cloud_upload, size: 20)), + message: LocaleKeys.operation_upload.tr(), + )); + factory DButton.dropdown({ Key? key, width = 60.0, diff --git a/app/resources/langs/en.json b/app/resources/langs/en.json index bcd5583..f0eb2bf 100644 --- a/app/resources/langs/en.json +++ b/app/resources/langs/en.json @@ -66,19 +66,23 @@ } }, "operation": { - "add": "Add", + "add": "add", "delete": "delete", "duplicate": "duplicate", "apply": "apply", "paste": "paste", - "logout": "sign out" + "logout": "sign out", + "upload": "upload" }, "str": { "null": "Null", "new_scheme": "New gesture scheme", "copy": "copy", "yes": "Yes", - "no": "No" + "no": "No", + "ok": "OK", + "share": "Share", + "cancel": "Cancel" }, "built_in_commands": { "ShowWorkspace": "ShowWorkspace", @@ -126,6 +130,14 @@ "description_for_startup": "Click [{YES}] to view, click [{NO}] ignore this update", "title_already_latest": "Already the latest version ~", "description_for_manual": "Visit the official website to see more?" + }, + "login_for_upload": { + "title": "please login", + "description": "You need to login first to perform upload operations" + }, + "upload_and_share": { + "title": "Share the scheme at the same time?", + "description": "If you select [Share], other users can see this scheme and download it;\nIf you select [Cancel], you can still find this scheme in the [My Upload] list and share." } }, "me": { diff --git a/app/resources/langs/zh-CN.json b/app/resources/langs/zh-CN.json index 3832629..737f8c1 100644 --- a/app/resources/langs/zh-CN.json +++ b/app/resources/langs/zh-CN.json @@ -71,14 +71,18 @@ "duplicate": "复制", "apply": "应用", "paste": "粘贴", - "logout": "退出登录" + "logout": "退出登录", + "upload": "上传" }, "str": { "null": "无", "new_scheme": "新建手势方案", "copy": "副本", "yes": "是", - "no": "否" + "no": "否", + "ok": "好的", + "share": "分享", + "cancel": "放弃" }, "built_in_commands": { "ShowWorkspace": "显示工作区", @@ -126,6 +130,14 @@ "description_for_startup": "点击[{yes}]查看,点击[{no}]忽略本次更新", "title_already_latest": "已经是最新版本~", "description_for_manual": "是否前去官网查看?" + }, + "login_for_upload": { + "title": "请登录", + "description": "您需要先登录才能进行上传操作" + }, + "upload_and_share": { + "title": "是否同时分享到方案市场?", + "description": "如果选择[分享],其他用户可以看到本方案并下载使用;\n如果选择[放弃],您仍可以稍后在[我的上传]列表中找到本方案进行操作。" } }, "me": { From 048c54e080573488153a32dd8b381981f21bb246 Mon Sep 17 00:00:00 2001 From: debuggerx Date: Fri, 31 Dec 2021 18:05:13 +0800 Subject: [PATCH 2/6] feat: add upload logic. --- api/bin/migrate.dart | 9 +-- api/lib/apis.dart | 11 ++++ api/lib/models.dart | 3 +- api/lib/src/config/plugins/jwt.dart | 2 +- api/lib/src/models/scheme.dart | 35 +++++++++++ api/lib/src/models/user.dart | 4 ++ .../src/routes/controllers/auth_controllers.dart | 13 ++++ .../routes/controllers/controller_extensions.dart | 17 +++++- api/lib/src/routes/controllers/middlewares.dart | 7 +-- .../src/routes/controllers/scheme_controllers.dart | 60 +++++++++++++++++++ api/lib/src/routes/routes.dart | 2 + app/lib/http/api.dart | 38 ++++++++++-- app/lib/main.dart | 14 ++++- app/lib/models/scheme.dart | 2 +- app/lib/pages/gesture_editor.dart | 26 +++++++- app/lib/pages/market_or_me.dart | 30 +--------- app/lib/utils/helper.dart | 2 + app/lib/widgets/login.dart | 6 ++ app/lib/widgets/me.dart | 69 ++++++++++++++++++++++ app/resources/langs/en.json | 8 +++ app/resources/langs/zh-CN.json | 8 +++ 21 files changed, 317 insertions(+), 49 deletions(-) create mode 100644 api/lib/src/models/scheme.dart create mode 100644 api/lib/src/routes/controllers/scheme_controllers.dart create mode 100644 app/lib/widgets/me.dart diff --git a/api/bin/migrate.dart b/api/bin/migrate.dart index 8963f56..eec2b33 100644 --- a/api/bin/migrate.dart +++ b/api/bin/migrate.dart @@ -1,10 +1,10 @@ -import 'package:angel3_migration/angel3_migration.dart'; -import 'package:angel3_orm_postgres/angel3_orm_postgres.dart'; -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/angel3_migration.dart'; import 'package:angel3_migration_runner/angel3_migration_runner.dart'; import 'package:angel3_migration_runner/postgres.dart'; +import 'package:angel3_orm_postgres/angel3_orm_postgres.dart'; +import 'package:dde_gesture_manager_api/models.dart'; +import 'package:dde_gesture_manager_api/src/config/plugins/orm.dart'; import 'package:file/local.dart'; import 'package:logging/logging.dart'; @@ -28,6 +28,7 @@ void main(List args) async { var migrationRunner = PostgresMigrationRunner(connection, migrations: [ UserMigration(), UserSeed(), + SchemeMigration(), ]); await runMigrations(migrationRunner, args); } diff --git a/api/lib/apis.dart b/api/lib/apis.dart index 3f1ba2f..3692087 100644 --- a/api/lib/apis.dart +++ b/api/lib/apis.dart @@ -7,6 +7,7 @@ class Apis { static final system = SystemApis(); static final auth = AuthApis(); + static final scheme = SchemeApis(); } class AuthApis { @@ -15,6 +16,8 @@ class AuthApis { String get loginOrSignup => [path, 'login_or_signup'].joinPath(); String confirmSignup({required StringParam accessKey}) => [path, 'confirm_sign_up', accessKey].joinPath(); + + String get status => [path, 'status'].joinPath(); } class SystemApis { @@ -23,6 +26,14 @@ class SystemApis { String get appVersion => [path, 'app-version'].joinPath(); } +class SchemeApis { + static final String path = '/scheme'; + + String get upload => [path, 'upload'].joinPath(); + + String get userUploads => [path, 'user', 'uploads'].joinPath(); +} + final _paramsMap = { 'IntParam': IntParam.nameOnRoute, 'DoubleParam': DoubleParam.nameOnRoute, diff --git a/api/lib/models.dart b/api/lib/models.dart index c608c7a..04f6864 100644 --- a/api/lib/models.dart +++ b/api/lib/models.dart @@ -1,3 +1,4 @@ export 'src/models/user.dart'; export 'src/models/app_version.dart'; -export 'src/models/login_success.dart'; \ No newline at end of file +export 'src/models/login_success.dart'; +export 'src/models/scheme.dart'; \ No newline at end of file diff --git a/api/lib/src/config/plugins/jwt.dart b/api/lib/src/config/plugins/jwt.dart index 8cf8700..e209066 100644 --- a/api/lib/src/config/plugins/jwt.dart +++ b/api/lib/src/config/plugins/jwt.dart @@ -9,7 +9,7 @@ Future configureServer(Angel app) async { allowCookie: false, deserializer: (p) async => (UserQuery()..where!.id.equals(int.parse(p))) .getOne(app.container!.make()) - .then((value) => value.value), + .then((value) => value.isNotEmpty ? value.value : User(email: '')), serializer: (p) => p.id ?? '', ); await auth.configureServer(app); diff --git a/api/lib/src/models/scheme.dart b/api/lib/src/models/scheme.dart new file mode 100644 index 0000000..49e3516 --- /dev/null +++ b/api/lib/src/models/scheme.dart @@ -0,0 +1,35 @@ +import 'package:angel3_orm/angel3_orm.dart'; +import 'package:angel3_serialize/angel3_serialize.dart'; +import 'package:dde_gesture_manager_api/src/models/base_model.dart'; +import 'package:angel3_migration/angel3_migration.dart'; +import 'package:optional/optional.dart'; + +part 'scheme.g.dart'; + +@serializable +@orm +abstract class _Scheme extends BaseModel { + @Column(isNullable: false, indexType: IndexType.unique) + @SerializableField(isNullable: false) + String? get uuid; + + @Column(isNullable: false) + @SerializableField(isNullable: false) + String? get name; + + @Column(isNullable: false, indexType: IndexType.standardIndex) + @SerializableField(isNullable: true, exclude: true) + int? uid; + + @Column(type: ColumnType.text) + String? description; + + @Column(isNullable: false, indexType: IndexType.standardIndex) + @SerializableField(defaultValue: false, isNullable: false) + bool? get shared; + + @Column(type: ColumnType.jsonb) + @SerializableField() + @DefaultsTo([]) + List? get gestures; +} diff --git a/api/lib/src/models/user.dart b/api/lib/src/models/user.dart index c34c7ef..8aab835 100644 --- a/api/lib/src/models/user.dart +++ b/api/lib/src/models/user.dart @@ -20,5 +20,9 @@ abstract class _User extends BaseModel { @SerializableField(isNullable: true, exclude: true) String? get password; + @Column(isNullable: false) + @SerializableField(defaultValue: false) + bool? get blocked; + String secret(String salt) => base64.encode(Hmac(sha256, salt.codeUnits).convert((password ?? '').codeUnits).bytes); } diff --git a/api/lib/src/routes/controllers/auth_controllers.dart b/api/lib/src/routes/controllers/auth_controllers.dart index aae33e1..35d8823 100644 --- a/api/lib/src/routes/controllers/auth_controllers.dart +++ b/api/lib/src/routes/controllers/auth_controllers.dart @@ -5,6 +5,7 @@ import 'package:angel3_auth/angel3_auth.dart'; import 'package:angel3_framework/angel3_framework.dart'; import 'package:dde_gesture_manager_api/apis.dart'; import 'package:dde_gesture_manager_api/models.dart'; +import 'package:dde_gesture_manager_api/src/routes/controllers/middlewares.dart'; import 'package:mailer/mailer.dart'; import 'package:mailer/smtp_server.dart'; import 'package:uuid/uuid.dart'; @@ -48,6 +49,8 @@ Future configureServer(Angel app) async { return res.notFound(); } else if (user.value.password != userParams.password) { return res.unauthorized(); + } else if (user.value.blocked == true) { + return res.forbidden(); } else { var angelAuth = req.container!.make(); await angelAuth.loginById(user.value.id!, req, res); @@ -75,4 +78,14 @@ Future configureServer(Angel app) async { } return res.render('sign_up_result.html', {'success': false}); }); + + app.get( + Apis.auth.status, + chain( + [ + jwtMiddleware(), + (req, res) => req.user.blocked == false ? res.noContent() : res.forbidden(), + ], + ), + ); } diff --git a/api/lib/src/routes/controllers/controller_extensions.dart b/api/lib/src/routes/controllers/controller_extensions.dart index f13140e..b33549b 100644 --- a/api/lib/src/routes/controllers/controller_extensions.dart +++ b/api/lib/src/routes/controllers/controller_extensions.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:angel3_framework/angel3_framework.dart'; import 'package:angel3_orm/angel3_orm.dart' as orm; +import 'package:dde_gesture_manager_api/models.dart'; import 'package:dde_gesture_manager_api/src/config/plugins/redis_cache.dart'; import 'package:neat_cache/neat_cache.dart'; @@ -20,6 +21,16 @@ extension ResponseNoContent on ResponseContext { statusCode = HttpStatus.unauthorized; return close(); } + + forbidden() { + statusCode = HttpStatus.forbidden; + return close(); + } + + unProcessableEntity() { + statusCode = HttpStatus.unprocessableEntity; + return close(); + } } extension QueryWhereId on orm.Query { @@ -32,6 +43,10 @@ extension QueryExecutor on RequestContext { orm.QueryExecutor get queryExecutor => container!.make(); } -extension RedisExecutor on RequestContext { +extension RedisClient on RequestContext { Cache get cache => container!.make().cache; } + +extension JWTUserInstance on RequestContext { + User get user => container!.make(); +} diff --git a/api/lib/src/routes/controllers/middlewares.dart b/api/lib/src/routes/controllers/middlewares.dart index 84c9b63..2ea42ff 100644 --- a/api/lib/src/routes/controllers/middlewares.dart +++ b/api/lib/src/routes/controllers/middlewares.dart @@ -2,16 +2,15 @@ import 'package:angel3_auth/angel3_auth.dart'; import 'package:angel3_framework/angel3_framework.dart'; import 'package:dde_gesture_manager_api/models.dart'; +import '../controllers/controller_extensions.dart'; RequestHandler jwtMiddleware() { return (RequestContext req, ResponseContext res, {bool throwError = true}) async { bool _reject(ResponseContext res) { if (throwError) { - res.statusCode = 403; - throw AngelHttpException.forbidden(); - } else { - return false; + res.forbidden(); } + return false; } if (req.container != null) { diff --git a/api/lib/src/routes/controllers/scheme_controllers.dart b/api/lib/src/routes/controllers/scheme_controllers.dart new file mode 100644 index 0000000..4d4a1ca --- /dev/null +++ b/api/lib/src/routes/controllers/scheme_controllers.dart @@ -0,0 +1,60 @@ +import 'dart:async'; + +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:dde_gesture_manager_api/apis.dart'; +import 'package:dde_gesture_manager_api/src/models/scheme.dart'; +import 'package:dde_gesture_manager_api/src/routes/controllers/middlewares.dart'; +import 'package:logging/logging.dart'; +import 'controller_extensions.dart'; + +Future configureServer(Angel app) async { + final _log = Logger('scheme_controller'); + + app.post( + Apis.scheme.upload, + chain( + [ + jwtMiddleware(), + (req, res) async { + try { + var scheme = SchemeSerializer.fromMap(req.bodyAsMap); + var schemeQuery = SchemeQuery(); + schemeQuery.where!.uuid.equals(scheme.uuid!); + var one = await schemeQuery.getOne(req.queryExecutor); + schemeQuery = SchemeQuery(); + schemeQuery.values.copyFrom(scheme); + schemeQuery.values.uid = int.parse(req.user.id!); + if (one.isEmpty) { + await schemeQuery.insert(req.queryExecutor); + } else { + schemeQuery.whereId = int.parse(one.value.id!); + await schemeQuery.updateOne(req.queryExecutor); + } + } catch (e) { + _log.severe(e); + return res.unProcessableEntity(); + } + return res.noContent(); + }, + ], + ), + ); + + app.get( + Apis.scheme.userUploads, + chain( + [ + jwtMiddleware(), + (req, res) async { + var schemeQuery = SchemeQuery(); + schemeQuery.where!.uid.equals(int.parse(req.user.id!)); + schemeQuery.orderBy(SchemeFields.updatedAt, descending: true); + return schemeQuery.get(req.queryExecutor).then((value) => value.map((e) => { + 'name': e.name, + 'description': e.description, + }).toList()); + }, + ], + ), + ); +} \ No newline at end of file diff --git a/api/lib/src/routes/routes.dart b/api/lib/src/routes/routes.dart index 4b86464..83f4a8e 100644 --- a/api/lib/src/routes/routes.dart +++ b/api/lib/src/routes/routes.dart @@ -2,6 +2,7 @@ import 'package:angel3_framework/angel3_framework.dart'; import 'package:file/file.dart'; import 'controllers/auth_controllers.dart' as auth_controllers; import 'controllers/system_controllers.dart' as system_controllers; +import 'controllers/scheme_controllers.dart' as scheme_controllers; /// Put your app routes here! /// @@ -22,6 +23,7 @@ AngelConfigurer configureServer(FileSystem fileSystem) { // Typically, you want to mount controllers first, after any global middleware. await app.configure(system_controllers.configureServerWithFileSystem(fileSystem)); await app.configure(auth_controllers.configureServer); + await app.configure(scheme_controllers.configureServer); // Throw a 404 if no route matched the request. app.fallback((req, res) => throw AngelHttpException.notFound()); diff --git a/app/lib/http/api.dart b/app/lib/http/api.dart index 44fcafa..b3264fe 100644 --- a/app/lib/http/api.dart +++ b/app/lib/http/api.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:dde_gesture_manager/constants/sp_keys.dart'; import 'package:dde_gesture_manager/extensions.dart'; +import 'package:dde_gesture_manager/models/scheme.dart' as AppScheme; import 'package:dde_gesture_manager/utils/helper.dart'; import 'package:dde_gesture_manager/utils/notificator.dart'; import 'package:dde_gesture_manager_api/apis.dart'; @@ -13,7 +14,12 @@ typedef T BeanBuilder(Map res); typedef T HandleRespBuild(http.Response resp); -getStatusCodeFunc(Map resp) => resp["statusCode"]; +typedef int GetStatusCodeFunc(Map resp); + +int getStatusCodeFunc(Map resp) => resp["statusCode"] as int; + +BeanBuilder> listRespBuilderWrap(BeanBuilder builder) => + (Map resp) => (resp['list'] as List).map((e) => builder(e)).toList(); class HttpErrorCode extends Error { int statusCode; @@ -40,11 +46,13 @@ class Api { } static HandleRespBuild _handleRespBuild(BeanBuilder builder) => (http.Response resp) { - if (builder == getStatusCodeFunc) return builder({"statusCode": resp.statusCode}); + if (builder is GetStatusCodeFunc) return builder({"statusCode": resp.statusCode}); T res; try { - res = builder(json.decode(resp.body)); + var decodeBody = json.decode(utf8.decode(resp.bodyBytes)); + res = decodeBody is Map ? builder(decodeBody) : builder({'list': decodeBody}); } catch (e) { + e.sout(); throw HttpErrorCode(resp.statusCode, message: resp.body); } return res; @@ -67,7 +75,7 @@ class Api { path: path, ), headers: { - HttpHeaders.contentTypeHeader: ContentType.json.value, + HttpHeaders.contentTypeHeader: ContentType.json.toString(), }..addAll( ignoreToken ? {} : {HttpHeaders.authorizationHeader: 'Bearer ${H().sp.getString(SPKeys.accessToken)}'}), ) @@ -98,7 +106,7 @@ class Api { ), body: jsonEncode(body), headers: { - HttpHeaders.contentTypeHeader: ContentType.json.value, + HttpHeaders.contentTypeHeader: ContentType.json.toString(), }..addAll( ignoreToken ? {} : {HttpHeaders.authorizationHeader: 'Bearer ${H().sp.getString(SPKeys.accessToken)}'}), ) @@ -132,4 +140,24 @@ class Api { ignoreToken: true, ignoreErrorHandle: ignoreErrorHandle, ); + + static Future checkAuthStatus() => _get(Apis.auth.status, getStatusCodeFunc, ignoreErrorHandle: true) + .then((value) => value == HttpStatus.noContent); + + static Future uploadScheme({required AppScheme.Scheme scheme, required bool share}) => _post( + Apis.scheme.upload, + getStatusCodeFunc, + body: SchemeSerializer.toMap( + Scheme( + name: scheme.name, + uuid: scheme.id, + description: scheme.description, + gestures: scheme.gestures, + shared: share, + ), + ), + ).then((value) => value == HttpStatus.noContent); + + static Future> userUploads() => + _get(Apis.scheme.userUploads, listRespBuilderWrap(SchemeSerializer.fromMap)); } diff --git a/app/lib/main.dart b/app/lib/main.dart index 74cf9c3..a507d48 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -3,6 +3,7 @@ import 'package:dde_gesture_manager/constants/sp_keys.dart'; import 'package:dde_gesture_manager/constants/supported_locales.dart'; import 'package:dde_gesture_manager/extensions.dart'; import 'package:dde_gesture_manager/generated/codegen_loader.g.dart'; +import 'package:dde_gesture_manager/http/api.dart'; import 'package:dde_gesture_manager/models/configs.dart'; import 'package:dde_gesture_manager/models/configs.provider.dart'; import 'package:dde_gesture_manager/models/settings.provider.dart'; @@ -68,7 +69,18 @@ class MyApp extends StatelessWidget { ], ), firstChild: Builder(builder: (context) { - Future.microtask(() => initEvents(context)); + Future.microtask(() { + initEvents(context); + if (H().lastCheckAuthStatusTime != null && + H().lastCheckAuthStatusTime!.difference(DateTime.now()) < Duration(minutes: 10)) return; + if (context.read().accessToken.notNull) { + Api.checkAuthStatus().then((value) { + if (!value) context.read().setProps(email: '', accessToken: ''); + }); + } else { + H().lastCheckAuthStatusTime = DateTime.now(); + } + }); return Container(); }), secondChild: HomePage(), diff --git a/app/lib/models/scheme.dart b/app/lib/models/scheme.dart index 5ffea37..6d2a385 100644 --- a/app/lib/models/scheme.dart +++ b/app/lib/models/scheme.dart @@ -131,7 +131,7 @@ class Scheme { @ProviderModelProp() List? gestures; - bool get readOnly => uploaded == true || fromMarket == true || id == Uuid.NAMESPACE_NIL; + bool get readOnly => fromMarket == true || id == Uuid.NAMESPACE_NIL; Scheme.parse(scheme) { if (scheme is String) scheme = json.decode(scheme); diff --git a/app/lib/pages/gesture_editor.dart b/app/lib/pages/gesture_editor.dart index 38c2f2d..78a89a1 100644 --- a/app/lib/pages/gesture_editor.dart +++ b/app/lib/pages/gesture_editor.dart @@ -1,6 +1,7 @@ import 'package:adaptive_scrollbar/adaptive_scrollbar.dart'; import 'package:dde_gesture_manager/constants/constants.dart'; import 'package:dde_gesture_manager/extensions.dart'; +import 'package:dde_gesture_manager/http/api.dart'; import 'package:dde_gesture_manager/models/configs.provider.dart'; import 'package:dde_gesture_manager/models/content_layout.provider.dart'; import 'package:dde_gesture_manager/models/local_schemes_provider.dart'; @@ -297,14 +298,13 @@ class GestureEditor extends StatelessWidget { Padding( padding: const EdgeInsets.only(left: 10), child: DButton.upload( - enabled: schemeProvider.uploaded == false, + enabled: schemeProvider.readOnly == false, onTap: () async { if (context.read().accessToken.isNull) { return Notificator.showAlert( title: LocaleKeys.info_login_for_upload_title.tr(), description: LocaleKeys.info_login_for_upload_description.tr(), ).then((value) { - value.sout(); if (value == CustomButton.positiveButton) { context .read() @@ -317,7 +317,27 @@ class GestureEditor extends StatelessWidget { description: LocaleKeys.info_upload_and_share_description.tr(), positiveButtonTitle: LocaleKeys.str_share.tr(), negativeButtonTitle: LocaleKeys.str_cancel.tr(), - ); + ).then((value) { + bool? _share; + if (value == CustomButton.positiveButton) + _share = true; + else if (value == CustomButton.negativeButton) _share = false; + + if (_share != null) { + Api.uploadScheme(scheme: schemeProvider, share: _share).then((value) { + if (value) { + Notificator.success(context, title: LocaleKeys.info_upload_success.tr()); + var localSchemesProvider = context.read(); + var localSchemeEntry = localSchemesProvider.schemes! + .firstWhere((ele) => ele.scheme.id == schemeProvider.id); + localSchemeEntry.scheme.uploaded = true; + localSchemeEntry.save(localSchemesProvider); + } else { + Notificator.error(context, title: LocaleKeys.info_upload_failed.tr()); + } + }); + } + }); }, ), ), diff --git a/app/lib/pages/market_or_me.dart b/app/lib/pages/market_or_me.dart index e769260..00e1a00 100644 --- a/app/lib/pages/market_or_me.dart +++ b/app/lib/pages/market_or_me.dart @@ -1,10 +1,10 @@ -import 'package:auto_size_text/auto_size_text.dart'; import 'package:dde_gesture_manager/constants/constants.dart'; import 'package:dde_gesture_manager/extensions.dart'; import 'package:dde_gesture_manager/models/configs.provider.dart'; import 'package:dde_gesture_manager/models/content_layout.provider.dart'; import 'package:dde_gesture_manager/widgets/dde_button.dart'; import 'package:dde_gesture_manager/widgets/login.dart'; +import 'package:dde_gesture_manager/widgets/me.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -82,33 +82,7 @@ class MarketOrMe extends StatelessWidget { Widget buildMeContent(BuildContext context) { var accessToken = context.watch().accessToken; if (accessToken.isNull) return LoginWidget(); - - return Padding( - padding: EdgeInsets.symmetric(vertical: 10), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Icon(Icons.person, size: defaultButtonHeight), - Flexible( - child: AutoSizeText( - context.watch().email ?? '', - style: TextStyle( - fontSize: 18, - ), - maxLines: 1, - ), - ), - DButton.logout( - enabled: true, - onTap: () => context.read().setProps(accessToken: '', email: ''), - ), - ], - ), - ], - ), - ); + return MeWidget(); } Widget buildMarketContent(BuildContext context) { diff --git a/app/lib/utils/helper.dart b/app/lib/utils/helper.dart index 88c9b19..b4b1ad3 100644 --- a/app/lib/utils/helper.dart +++ b/app/lib/utils/helper.dart @@ -34,6 +34,8 @@ class H { BuildContext get topContext => _topContext; + DateTime? lastCheckAuthStatusTime; + initTopContext(BuildContext context) { _topContext = context; } diff --git a/app/lib/widgets/login.dart b/app/lib/widgets/login.dart index 20e1386..ef648ce 100644 --- a/app/lib/widgets/login.dart +++ b/app/lib/widgets/login.dart @@ -54,6 +54,12 @@ class _LoginWidgetState extends State { title: LocaleKeys.info_sign_up_hint_title.tr(), description: LocaleKeys.info_sign_up_hint_description.tr(), ); + else if (code == HttpStatus.forbidden) + Notificator.info( + context, + title: LocaleKeys.info_user_blocked_hint_title.tr(), + description: LocaleKeys.info_user_blocked_hint_description.tr(), + ); else throw e; } diff --git a/app/lib/widgets/me.dart b/app/lib/widgets/me.dart new file mode 100644 index 0000000..3cd6663 --- /dev/null +++ b/app/lib/widgets/me.dart @@ -0,0 +1,69 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:dde_gesture_manager/constants/constants.dart'; +import 'package:dde_gesture_manager/http/api.dart'; +import 'package:dde_gesture_manager/models/configs.provider.dart'; +import 'package:dde_gesture_manager_api/models.dart'; +import 'package:flutter/material.dart'; +import 'package:dde_gesture_manager/extensions.dart'; + +import 'dde_button.dart'; + +class MeWidget extends StatefulWidget { + const MeWidget({Key? key}) : super(key: key); + + @override + _MeWidgetState createState() => _MeWidgetState(); +} + +class _MeWidgetState extends State { + List uploads = []; + + @override + void initState() { + super.initState(); + Api.userUploads().then((value) { + if (mounted) + setState(() { + uploads = value; + }); + }); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 10), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Icon(Icons.person, size: defaultButtonHeight), + Flexible( + child: AutoSizeText( + context.watch().email ?? '', + style: TextStyle( + fontSize: 18, + ), + maxLines: 1, + ), + ), + DButton.logout( + enabled: true, + onTap: () => context.read().setProps(accessToken: '', email: ''), + ), + ], + ), + Text('我的上传'), + Container( + height: 400, + child: ListView.builder( + itemBuilder: (context, index) => Text(uploads[index].name ?? ''), + itemCount: uploads.length, + ), + ), + ], + ), + ); + } +} diff --git a/app/resources/langs/en.json b/app/resources/langs/en.json index f0eb2bf..5fbc48c 100644 --- a/app/resources/langs/en.json +++ b/app/resources/langs/en.json @@ -138,6 +138,14 @@ "upload_and_share": { "title": "Share the scheme at the same time?", "description": "If you select [Share], other users can see this scheme and download it;\nIf you select [Cancel], you can still find this scheme in the [My Upload] list and share." + }, + "user_blocked_hint": { + "title": "The account has been blocked!", + "description": "If you have any questions, please contact me using email." + }, + "upload": { + "success": "Upload success ~", + "failed": "Upload failed.." } }, "me": { diff --git a/app/resources/langs/zh-CN.json b/app/resources/langs/zh-CN.json index 737f8c1..d613146 100644 --- a/app/resources/langs/zh-CN.json +++ b/app/resources/langs/zh-CN.json @@ -138,6 +138,14 @@ "upload_and_share": { "title": "是否同时分享到方案市场?", "description": "如果选择[分享],其他用户可以看到本方案并下载使用;\n如果选择[放弃],您仍可以稍后在[我的上传]列表中找到本方案进行操作。" + }, + "user_blocked_hint": { + "title": "该账号已被封禁!", + "description": "如有疑问请通过发送邮件联系" + }, + "upload": { + "success": "上传成功~", + "failed": "上传失败。。" } }, "me": { From 85a7d36fdae65fbf22a931940bbd5021df9e3818 Mon Sep 17 00:00:00 2001 From: debuggerx Date: Fri, 7 Jan 2022 18:04:59 +0800 Subject: [PATCH 3/6] wip: me panel. --- api/3rd_party/angel3_orm/AUTHORS.md | 12 + api/3rd_party/angel3_orm/CHANGELOG.md | 230 +++++++ api/3rd_party/angel3_orm/LICENSE | 30 + api/3rd_party/angel3_orm/README.md | 15 + api/3rd_party/angel3_orm/analysis_options.yaml | 1 + api/3rd_party/angel3_orm/lib/angel3_orm.dart | 15 + api/3rd_party/angel3_orm/lib/src/annotations.dart | 31 + api/3rd_party/angel3_orm/lib/src/builder.dart | 675 +++++++++++++++++++++ api/3rd_party/angel3_orm/lib/src/join_builder.dart | 74 +++ api/3rd_party/angel3_orm/lib/src/join_on.dart | 8 + .../angel3_orm/lib/src/map_query_values.dart | 9 + api/3rd_party/angel3_orm/lib/src/migration.dart | 135 +++++ api/3rd_party/angel3_orm/lib/src/order_by.dart | 8 + api/3rd_party/angel3_orm/lib/src/query.dart | 425 +++++++++++++ api/3rd_party/angel3_orm/lib/src/query_base.dart | 81 +++ .../angel3_orm/lib/src/query_executor.dart | 23 + api/3rd_party/angel3_orm/lib/src/query_values.dart | 89 +++ api/3rd_party/angel3_orm/lib/src/query_where.dart | 69 +++ api/3rd_party/angel3_orm/lib/src/relations.dart | 91 +++ api/3rd_party/angel3_orm/lib/src/union.dart | 38 ++ api/3rd_party/angel3_orm/lib/src/util.dart | 3 + api/3rd_party/angel3_orm/mono_pkg.yaml | 0 api/3rd_party/angel3_orm/pubspec.yaml | 21 + api/bin/migrate.dart | 4 + api/lib/apis.dart | 25 +- api/lib/src/models/download_history.dart | 19 + api/lib/src/models/like_record.dart | 23 + api/lib/src/models/scheme.dart | 88 +++ .../src/routes/controllers/auth_controllers.dart | 2 +- .../routes/controllers/controller_extensions.dart | 8 +- api/lib/src/routes/controllers/middlewares.dart | 22 +- .../src/routes/controllers/scheme_controllers.dart | 164 ++++- api/pubspec.yaml | 12 +- app/lib/http/api.dart | 22 +- app/lib/main.dart | 23 +- app/lib/models/content_layout.dart | 5 +- app/lib/pages/local_manager.dart | 4 +- app/lib/pages/market_or_me.dart | 2 +- app/lib/utils/init_linux.dart | 5 +- app/lib/utils/simple_throttle.dart | 45 ++ app/lib/widgets/dde_button.dart | 51 ++ app/lib/widgets/me.dart | 200 +++++- app/pubspec.yaml | 4 + app/resources/langs/en.json | 15 +- app/resources/langs/zh-CN.json | 15 +- 45 files changed, 2775 insertions(+), 66 deletions(-) create mode 100644 api/3rd_party/angel3_orm/AUTHORS.md create mode 100644 api/3rd_party/angel3_orm/CHANGELOG.md create mode 100644 api/3rd_party/angel3_orm/LICENSE create mode 100644 api/3rd_party/angel3_orm/README.md create mode 100644 api/3rd_party/angel3_orm/analysis_options.yaml create mode 100644 api/3rd_party/angel3_orm/lib/angel3_orm.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/annotations.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/builder.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/join_builder.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/join_on.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/map_query_values.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/migration.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/order_by.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/query.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/query_base.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/query_executor.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/query_values.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/query_where.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/relations.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/union.dart create mode 100644 api/3rd_party/angel3_orm/lib/src/util.dart create mode 100644 api/3rd_party/angel3_orm/mono_pkg.yaml create mode 100644 api/3rd_party/angel3_orm/pubspec.yaml create mode 100644 api/lib/src/models/download_history.dart create mode 100644 api/lib/src/models/like_record.dart create mode 100644 app/lib/utils/simple_throttle.dart diff --git a/api/3rd_party/angel3_orm/AUTHORS.md b/api/3rd_party/angel3_orm/AUTHORS.md new file mode 100644 index 0000000..ac95ab5 --- /dev/null +++ b/api/3rd_party/angel3_orm/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/3rd_party/angel3_orm/CHANGELOG.md b/api/3rd_party/angel3_orm/CHANGELOG.md new file mode 100644 index 0000000..4dbc079 --- /dev/null +++ b/api/3rd_party/angel3_orm/CHANGELOG.md @@ -0,0 +1,230 @@ +# Change Log + +## 4.0.4 + +* Changed default varchar size to 255 +* Changed default primary key to serial + +## 4.0.3 + +* Removed debugging messages + +## 4.0.2 + +* Updated linter to `package:lints` +* Set `createdAt` and `updatedAt` to current datetime as default + +## 4.0.1 + +* Fixed expressions parsing error +* Fixed json data type error +* Added debug logging to sql query execution + +## 4.0.0 + +* Updated `Optional` package + +## 4.0.0-beta.4 + +* Added `hasSize` to `ColumnType` + +## 4.0.0-beta.3 + +* Updated README +* Fixed NNBD issues + +## 4.0.0-beta.2 + +* Fixed static analysis warning + +## 4.0.0-beta.1 + +* Migrated to support Dart SDK 2.12.x NNBD + +## 3.0.0 + +* Migrated to work with Dart SDK 2.12.x Non NNBD + +## 2.1.0-beta.3 + +* Remove parentheses from `AS` when renaming raw `expressions`. + +## 2.1.0-beta.2 + +* Add `expressions` to `Query`, to support custom SQL expressions that are +read as normal fields. + +## 2.1.0-beta.1 + +* Calls to `leftJoin`, etc. alias all fields in a child query, to prevent +`ambiguous column a0.id` errors. + +## 2.1.0-beta + +* Split the formerly 600+ line `src/query.dart` up into +separate files. +* **BREAKING**: Add a required `QueryExecutor` argument to `transaction` +callbacks. +* Make `JoinBuilder` take `to` as a `String Function()`. This will allow +ORM queries to reference their joined subqueries. +* Removed deprecated `Join`, `toSql`, `sanitizeExpression`, `isAscii`. +* Always put `ORDER BY` before `LIMIT`. +* `and`, `or`, `not` in `QueryWhere` include parentheses. +* Add `joinType` to `Relationship` class. + +## 2.0.2 + +* Place `LIMIT` and `OFFSET` after `ORDER BY`. + +## 2.0.1 + +* Apply `package:pedantic` fixes. +* `@PrimaryKey()` no longer defaults to `serial`, allowing its type to be +inferenced. + +## 2.0.0 + +* Add `isNull`, `isNotNull` getters to builders. + +## 2.0.0-dev.24 + +* Fix a bug that caused syntax errors on `ORDER BY`. +* Add `pattern` to `like` on string builder. `sanitize` is optional. +* Add `RawSql`. + +## 2.0.0-dev.23 + +* Add `@ManyToMany` annotation, which builds many-to-many relations. + +## 2.0.0-dev.22 + +* `compileInsert` will explicitly never emit a key not belonging to the +associated query. + +## 2.0.0-dev.21 + +* Add tableName to query + +## 2.0.0-dev.20 + +* Join updates. + +## 2.0.0-dev.19 + +* Implement cast-based `double` support. +* Finish `ListSqlExpressionBuilder`. + +## 2.0.0-dev.18 + +* Add `ListSqlExpressionBuilder` (still in development). + +## 2.0.0-dev.17 + +* Add `EnumSqlExpressionBuilder`. + +## 2.0.0-dev.16 + +* Add `MapSqlExpressionBuilder` for JSON/JSONB support. + +## 2.0.0-dev.15 + +* Remove `Column.defaultValue`. +* Deprecate `toSql` and `sanitizeExpression`. +* Refactor builders so that strings are passed through + +## 2.0.0-dev.14 + +* Remove obsolete `@belongsToMany`. + +## 2.0.0-dev.13 + +* Push for consistency with orm_gen @ `2.0.0-dev`. + +## 2.0.0-dev.12 + +* Always apply `toSql` escapes. + +## 2.0.0-dev.11 + +* Remove `limit(1)` except on `getOne` + +## 2.0.0-dev.10 + +* Add `withFields` to `compile()` + +## 2.0.0-dev.9 + +* Permanent preamble fix + +## 2.0.0-dev.8 + +* Escapes + +## 2.0.0-dev.7 + +* Update `toSql` +* Add `isTrue` and `isFalse` + +## 2.0.0-dev.6 + +* Add `delete`, `insert` and `update` methods to `Query`. + +## 2.0.0-dev.4 + +* Add more querying methods. +* Add preamble to `Query.compile`. + +## 2.0.0-dev.3 + +* Brought back old-style query builder. +* Strong-mode updates, revised `Join`. + +## 2.0.0-dev.2 + +* Renamed `ORM` to `Orm`. +* `Orm` now requires a database type. + +## 2.0.0-dev.1 + +* Restored all old PostgreSQL-specific annotations. Rather than a smart runtime, +having a codegen capable of building ORM's for multiple databases can potentially +provide a very fast ORM for everyone. + +## 2.0.0-dev + +* Removed PostgreSQL-specific functionality, so that the ORM can ultimately +target all services. +* Created a better `Join` model. +* Created a far better `Query` model. +* Removed `lib/server.dart` + +## 1.0.0-alpha+10 + +* Split into `angel_orm.dart` and `server.dart`. Prevents DDC failures. + +## 1.0.0-alpha+7 + +* Added a `@belongsToMany` annotation class. +* Resolved [##20](https://github.com/angel-dart/orm/issues/20). The +`PostgreSQLConnectionPool` keeps track of which connections have been opened now. + +## 1.0.0-alpha+6 + +* `DateTimeSqlExpressionBuilder` will no longer automatically +insert quotation marks around names. + +## 1.0.0-alpha+5 + +* Corrected a typo that was causing the aforementioned test failures. +`==` becomes `=`. + +## 1.0.0-alpha+4 + +* Added a null-check in `lib/src/query.dart##L24` to (hopefully) prevent it from +crashing on Travis. + +## 1.0.0-alpha+3 + +* Added `isIn`, `isNotIn`, `isBetween`, `isNotBetween` to `SqlExpressionBuilder` and its +subclasses. +* Added a dependency on `package:meta`. diff --git a/api/3rd_party/angel3_orm/LICENSE b/api/3rd_party/angel3_orm/LICENSE new file mode 100644 index 0000000..a81d1a8 --- /dev/null +++ b/api/3rd_party/angel3_orm/LICENSE @@ -0,0 +1,30 @@ + +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/3rd_party/angel3_orm/README.md b/api/3rd_party/angel3_orm/README.md new file mode 100644 index 0000000..43f12ff --- /dev/null +++ b/api/3rd_party/angel3_orm/README.md @@ -0,0 +1,15 @@ +# Angel3 ORM + +![Pub Version (including pre-releases)](https://img.shields.io/pub/v/angel3_orm?include_prereleases) +[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety) +[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion) +[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/master/packages/orm/angel_orm/LICENSE) + +Runtime support for Angel3 ORM. Includes a clean, database-agnostic query builder and relationship/join support. + +## Supported database + +* PostgreSQL version 10, 11, 12, 13 and 14 +* MySQL 8.0 or later + +For documentation about the ORM, see [Developer Guide](https://angel3-docs.dukefirehawk.com/guides/orm) diff --git a/api/3rd_party/angel3_orm/analysis_options.yaml b/api/3rd_party/angel3_orm/analysis_options.yaml new file mode 100644 index 0000000..ea2c9e9 --- /dev/null +++ b/api/3rd_party/angel3_orm/analysis_options.yaml @@ -0,0 +1 @@ +include: package:lints/recommended.yaml \ No newline at end of file diff --git a/api/3rd_party/angel3_orm/lib/angel3_orm.dart b/api/3rd_party/angel3_orm/lib/angel3_orm.dart new file mode 100644 index 0000000..9195227 --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/angel3_orm.dart @@ -0,0 +1,15 @@ +export 'src/annotations.dart'; +export 'src/builder.dart'; +export 'src/join_builder.dart'; +export 'src/join_on.dart'; +export 'src/map_query_values.dart'; +export 'src/migration.dart'; +export 'src/order_by.dart'; +export 'src/query_base.dart'; +export 'src/query_executor.dart'; +export 'src/query_values.dart'; +export 'src/query_where.dart'; +export 'src/query.dart'; +export 'src/relations.dart'; +export 'src/union.dart'; +export 'src/util.dart'; diff --git a/api/3rd_party/angel3_orm/lib/src/annotations.dart b/api/3rd_party/angel3_orm/lib/src/annotations.dart new file mode 100644 index 0000000..891cddc --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/annotations.dart @@ -0,0 +1,31 @@ +/// A raw SQL statement that specifies a date/time default to the +/// current time. +const RawSql currentTimestamp = RawSql('CURRENT_TIMESTAMP'); + +/// Can passed to a [MigrationColumn] to default to a raw SQL expression. +class RawSql { + /// The raw SQL text. + final String value; + + const RawSql(this.value); +} + +/// Canonical instance of [ORM]. Implies all defaults. +const Orm orm = Orm(); + +class Orm { + /// The name of the table to query. + /// + /// Inferred if not present. + final String? tableName; + + /// Whether to generate migrations for this model. + /// + /// Defaults to [:true:]. + final bool generateMigrations; + + const Orm({this.tableName, this.generateMigrations = true}); +} + +/// The various types of join. +enum JoinType { inner, left, right, full, self } diff --git a/api/3rd_party/angel3_orm/lib/src/builder.dart b/api/3rd_party/angel3_orm/lib/src/builder.dart new file mode 100644 index 0000000..313d125 --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/builder.dart @@ -0,0 +1,675 @@ +import 'package:intl/intl.dart' show DateFormat; +import 'query.dart'; + +final DateFormat dateYmd = DateFormat('yyyy-MM-dd'); +final DateFormat dateYmdHms = DateFormat('yyyy-MM-dd HH:mm:ss'); + +abstract class SqlExpressionBuilder { + final Query query; + final String columnName; + String? _cast; + bool _isProperty = false; + String? _substitution; + + SqlExpressionBuilder(this.query, this.columnName); + + String get substitution { + var c = _isProperty ? 'prop' : columnName; + return _substitution ??= query.reserveName(c); + } + + bool get hasValue; + + String? compile(); +} + +class NumericSqlExpressionBuilder + extends SqlExpressionBuilder { + bool _hasValue = false; + String _op = '='; + String? _raw; + T? _value; + + NumericSqlExpressionBuilder(Query query, String columnName) + : super(query, columnName); + + @override + bool get hasValue => _hasValue; + + bool _change(String op, T value) { + _raw = null; + _op = op; + _value = value; + return _hasValue = true; + } + + @override + String? compile() { + if (_raw != null) return _raw; + if (_value == null) return null; + var v = _value.toString(); + if (T == double) v = 'CAST ("$v" as decimal)'; + if (_cast != null) v = 'CAST ($v AS $_cast)'; + return '$_op $v'; + } + + bool operator <(T value) => _change('<', value); + + bool operator >(T value) => _change('>', value); + + bool operator <=(T value) => _change('<=', value); + + bool operator >=(T value) => _change('>=', value); + + void get isNull { + _raw = 'IS NULL'; + _hasValue = true; + } + + void get isNotNull { + _raw = 'IS NOT NULL'; + _hasValue = true; + } + + void lessThan(T value) { + _change('<', value); + } + + void lessThanOrEqualTo(T value) { + _change('<=', value); + } + + void greaterThan(T value) { + _change('>', value); + } + + void greaterThanOrEqualTo(T value) { + _change('>=', value); + } + + void equals(T value) { + _change('=', value); + } + + void notEquals(T value) { + _change('!=', value); + } + + void isBetween(T lower, T upper) { + _raw = 'BETWEEN $lower AND $upper'; + _hasValue = true; + } + + void isNotBetween(T lower, T upper) { + _raw = 'NOT BETWEEN $lower AND $upper'; + _hasValue = true; + } + + void isIn(Iterable values) { + _raw = 'IN (' + values.join(', ') + ')'; + _hasValue = true; + } + + void isNotIn(Iterable values) { + _raw = 'NOT IN (' + values.join(', ') + ')'; + _hasValue = true; + } +} + +class EnumSqlExpressionBuilder extends SqlExpressionBuilder { + final int Function(T) _getValue; + bool _hasValue = false; + String _op = '='; + String? _raw; + int? _value; + + EnumSqlExpressionBuilder(Query query, String columnName, this._getValue) + : super(query, columnName); + + @override + bool get hasValue => _hasValue; + + bool _change(String op, T value) { + _raw = null; + _op = op; + _value = _getValue(value); + return _hasValue = true; + } + + UnsupportedError _unsupported() => + UnsupportedError('Enums do not support this operation.'); + + @override + String compile() { + if (_raw != null) { + return _raw!; + } + if (_value == null) { + return ''; + } + return '$_op $_value'; + } + + void get isNull { + _raw = 'IS NULL'; + _hasValue = true; + } + + void get isNotNull { + _raw = 'IS NOT NULL'; + _hasValue = true; + } + + void equals(T value) { + _change('=', value); + } + + void notEquals(T value) { + _change('!=', value); + } + + void isBetween(T lower, T upper) => throw _unsupported(); + + void isNotBetween(T lower, T upper) => throw _unsupported(); + + void isIn(Iterable values) { + _raw = 'IN (' + values.map(_getValue).join(', ') + ')'; + _hasValue = true; + } + + void isNotIn(Iterable values) { + _raw = 'NOT IN (' + values.map(_getValue).join(', ') + ')'; + _hasValue = true; + } +} + +class StringSqlExpressionBuilder extends SqlExpressionBuilder { + bool _hasValue = false; + String? _op = '=', _raw, _value; + + StringSqlExpressionBuilder(Query query, String columnName) + : super(query, columnName); + + @override + bool get hasValue => _hasValue; + + String get lowerName => '${substitution}_lower'; + + String get upperName => '${substitution}_upper'; + + bool _change(String op, String value) { + _raw = null; + _op = op; + _value = value; + query.substitutionValues[substitution] = _value; + return _hasValue = true; + } + + @override + String? compile() { + if (_raw != null) return _raw; + if (_value == null) return null; + return '$_op @$substitution'; + } + + void isEmpty() => equals(''); + + void equals(String value) { + _change('=', value); + } + + void notEquals(String value) { + _change('!=', value); + } + + /// Builds a `LIKE` predicate. + /// + /// To prevent injections, an optional [sanitizer] is called with a name that + /// will be escaped by the underlying [QueryExecutor]. Use this if the [pattern] + /// is not constant, and/or involves user input. + /// + /// Otherwise, you can omit [sanitizer]. + /// + /// Example: + /// ```dart + /// carNameBuilder.like('%Mazda%'); + /// carNameBuilder.like((name) => 'Mazda %$name%'); + /// ``` + void like(String pattern, {String Function(String)? sanitize}) { + sanitize ??= (s) => pattern; + _raw = 'LIKE \'' + sanitize('@$substitution') + '\''; + query.substitutionValues[substitution] = pattern; + _hasValue = true; + _value = null; + } + + void isBetween(String lower, String upper) { + query.substitutionValues[lowerName] = lower; + query.substitutionValues[upperName] = upper; + _raw = 'BETWEEN @$lowerName AND @$upperName'; + _hasValue = true; + } + + void isNotBetween(String lower, String upper) { + query.substitutionValues[lowerName] = lower; + query.substitutionValues[upperName] = upper; + _raw = 'NOT BETWEEN @$lowerName AND @$upperName'; + _hasValue = true; + } + + void get isNull { + _raw = 'IS NULL'; + _hasValue = true; + } + + void get isNotNull { + _raw = 'IS NOT NULL'; + _hasValue = true; + } + + String _in(Iterable values) { + return 'IN (' + + values.map((v) { + var name = query.reserveName('${columnName}_in_value'); + query.substitutionValues[name] = v; + return '@$name'; + }).join(', ') + + ')'; + } + + void isIn(Iterable values) { + _raw = _in(values); + _hasValue = true; + } + + void isNotIn(Iterable values) { + _raw = 'NOT ' + _in(values); + _hasValue = true; + } +} + +class BooleanSqlExpressionBuilder extends SqlExpressionBuilder { + bool _hasValue = false; + String? _op = '=', _raw; + bool? _value; + + BooleanSqlExpressionBuilder(Query query, String columnName) + : super(query, columnName); + + @override + bool get hasValue => _hasValue; + + bool _change(String op, bool value) { + _raw = null; + _op = op; + _value = value; + return _hasValue = true; + } + + @override + String? compile() { + if (_raw != null) return _raw; + if (_value == null) return null; + var v = _value! ? 'TRUE' : 'FALSE'; + if (_cast != null) v = 'CAST ($v AS $_cast)'; + return '$_op $v'; + } + + void get isTrue => equals(true); + + void get isFalse => equals(false); + + void get isNull { + _raw = 'IS NULL'; + _hasValue = true; + } + + void get isNotNull { + _raw = 'IS NOT NULL'; + _hasValue = true; + } + + void equals(bool value) { + _change('=', value); + } + + void notEquals(bool value) { + _change('!=', value); + } +} + +class DateTimeSqlExpressionBuilder extends SqlExpressionBuilder { + NumericSqlExpressionBuilder? _year, + _month, + _day, + _hour, + _minute, + _second; + + String? _raw; + + DateTimeSqlExpressionBuilder(Query query, String columnName) + : super(query, columnName); + + NumericSqlExpressionBuilder get year => + _year ??= NumericSqlExpressionBuilder(query, 'year'); + NumericSqlExpressionBuilder get month => + _month ??= NumericSqlExpressionBuilder(query, 'month'); + NumericSqlExpressionBuilder get day => + _day ??= NumericSqlExpressionBuilder(query, 'day'); + NumericSqlExpressionBuilder get hour => + _hour ??= NumericSqlExpressionBuilder(query, 'hour'); + NumericSqlExpressionBuilder get minute => + _minute ??= NumericSqlExpressionBuilder(query, 'minute'); + NumericSqlExpressionBuilder get second => + _second ??= NumericSqlExpressionBuilder(query, 'second'); + + @override + bool get hasValue => + _raw?.isNotEmpty == true || + _year?.hasValue == true || + _month?.hasValue == true || + _day?.hasValue == true || + _hour?.hasValue == true || + _minute?.hasValue == true || + _second?.hasValue == true; + + bool _change(String _op, DateTime dt, bool time) { + var dateString = time ? dateYmdHms.format(dt) : dateYmd.format(dt); + _raw = '$columnName $_op \'$dateString\''; + return true; + } + + bool operator <(DateTime value) => _change('<', value, true); + + bool operator <=(DateTime value) => _change('<=', value, true); + + bool operator >(DateTime value) => _change('>', value, true); + + bool operator >=(DateTime value) => _change('>=', value, true); + + void equals(DateTime value, {bool includeTime = true}) { + _change('=', value, includeTime != false); + } + + void lessThan(DateTime value, {bool includeTime = true}) { + _change('<', value, includeTime != false); + } + + void lessThanOrEqualTo(DateTime value, {bool includeTime = true}) { + _change('<=', value, includeTime != false); + } + + void greaterThan(DateTime value, {bool includeTime = true}) { + _change('>', value, includeTime != false); + } + + void greaterThanOrEqualTo(DateTime value, {bool includeTime = true}) { + _change('>=', value, includeTime != false); + } + + void isIn(Iterable values) { + _raw = '$columnName IN (' + + values.map(dateYmdHms.format).map((s) => '$s').join(', ') + + ')'; + } + + void isNotIn(Iterable values) { + _raw = '$columnName NOT IN (' + + values.map(dateYmdHms.format).map((s) => '$s').join(', ') + + ')'; + } + + void isBetween(DateTime lower, DateTime upper) { + var l = dateYmdHms.format(lower), u = dateYmdHms.format(upper); + _raw = "$columnName BETWEEN '$l' and '$u'"; + } + + void isNotBetween(DateTime lower, DateTime upper) { + var l = dateYmdHms.format(lower), u = dateYmdHms.format(upper); + _raw = "$columnName NOT BETWEEN '$l' and '$u'"; + } + + void get isNull { + _raw = '$columnName IS NULL'; + } + + void get isNotNull { + _raw = '$columnName IS NOT NULL'; + } + + @override + String? compile() { + if (_raw?.isNotEmpty == true) return _raw; + var parts = []; + if (year.hasValue == true) { + parts.add('YEAR($columnName) ${year.compile()}'); + } + if (month.hasValue == true) { + parts.add('MONTH($columnName) ${month.compile()}'); + } + if (day.hasValue == true) { + parts.add('DAY($columnName) ${day.compile()}'); + } + if (hour.hasValue == true) { + parts.add('HOUR($columnName) ${hour.compile()}'); + } + if (minute.hasValue == true) { + parts.add('MINUTE($columnName) ${minute.compile()}'); + } + if (second.hasValue == true) { + parts.add('SECOND($columnName) ${second.compile()}'); + } + + return parts.isEmpty ? null : parts.join(' AND '); + } +} + +abstract class JsonSqlExpressionBuilder extends SqlExpressionBuilder { + final List _properties = []; + bool _hasValue = false; + T? _value; + String? _op; + String? _raw; + + JsonSqlExpressionBuilder(Query query, String columnName) + : super(query, columnName); + + JsonSqlExpressionBuilderProperty operator [](K name) { + var p = _property(name); + _properties.add(p); + return p; + } + + JsonSqlExpressionBuilderProperty _property(K name); + + bool get hasRaw => _raw != null || _properties.any((p) => p.hasValue); + + @override + bool get hasValue => _hasValue || _properties.any((p) => p.hasValue); + + T? _encodeValue(T? v) => v; + + bool _change(String op, T value) { + _raw = null; + _op = op; + _value = value; + query.substitutionValues[substitution] = _encodeValue(_value); + return _hasValue = true; + } + + void get isNull { + _raw = 'IS NULL'; + _hasValue = true; + } + + void get isNotNull { + _raw = 'IS NOT NULL'; + _hasValue = true; + } + + @override + String compile() { + var s = _compile(); + if (!_properties.any((p) => p.hasValue)) return s; + //s ??= ''; + + for (var p in _properties) { + if (p.hasValue) { + var c = p.compile(); + + if (c != null) { + _hasValue = true; + //s ??= ''; + + if (p.typed is! DateTimeSqlExpressionBuilder) { + s += '${p.typed!.columnName} '; + } + + s += c; + } + } + } + + return s; + } + + String _compile() { + if (_raw != null) { + return _raw!; + } + if (_value == null) { + return ''; + } + return '::jsonb $_op @$substitution::jsonb'; + } + + void contains(T value) { + _change('@>', value); + } + + void equals(T value) { + _change('=', value); + } +} + +class MapSqlExpressionBuilder extends JsonSqlExpressionBuilder { + MapSqlExpressionBuilder(Query query, String columnName) + : super(query, columnName); + + @override + JsonSqlExpressionBuilderProperty _property(String name) { + return JsonSqlExpressionBuilderProperty(this, name, false); + } + + void containsKey(String key) { + this[key].isNotNull; + } + + void containsPair(key, value) { + contains({key: value}); + } +} + +class ListSqlExpressionBuilder extends JsonSqlExpressionBuilder { + ListSqlExpressionBuilder(Query query, String columnName) + : super(query, columnName); + + @override + List? _encodeValue(List? v) => v; //[json.encode(v)]; + + @override + JsonSqlExpressionBuilderProperty _property(int name) { + return JsonSqlExpressionBuilderProperty(this, name.toString(), true); + } +} + +class JsonSqlExpressionBuilderProperty { + final JsonSqlExpressionBuilder builder; + final String name; + final bool isInt; + SqlExpressionBuilder? _typed; + + JsonSqlExpressionBuilderProperty(this.builder, this.name, this.isInt); + + SqlExpressionBuilder? get typed => _typed; + + bool get hasValue => _typed?.hasValue == true; + + String? compile() => _typed?.compile(); + + T? _set(T Function() value) { + if (_typed is T) { + return _typed as T?; + } else if (_typed != null) { + throw StateError( + '$nameString is already typed as $_typed, and cannot be changed.'); + } else { + _typed = value() + ?.._cast = 'text' + .._isProperty = true; + return _typed as T?; + } + } + + String get nameString { + var n = isInt ? name : "'$name'"; + return '${builder.columnName}::jsonb->>$n'; + } + + void get isNotNull { + builder + .._hasValue = true + .._raw ??= ''; + + var r = builder._raw; + if (r != null) { + builder._raw = r + '$nameString IS NOT NULL'; + } else { + builder._raw = '$nameString IS NOT NULL'; + } + } + + void get isNull { + builder + .._hasValue = true + .._raw ??= ''; + + var r = builder._raw; + if (r != null) { + builder._raw = r + '$nameString IS NULL'; + } else { + builder._raw = '$nameString IS NULL'; + } + } + + StringSqlExpressionBuilder? get asString { + return _set(() => StringSqlExpressionBuilder(builder.query, nameString)); + } + + BooleanSqlExpressionBuilder? get asBool { + return _set(() => BooleanSqlExpressionBuilder(builder.query, nameString)); + } + + DateTimeSqlExpressionBuilder? get asDateTime { + return _set(() => DateTimeSqlExpressionBuilder(builder.query, nameString)); + } + + NumericSqlExpressionBuilder? get asDouble { + return _set( + () => NumericSqlExpressionBuilder(builder.query, nameString)); + } + + NumericSqlExpressionBuilder? get asInt { + return _set( + () => NumericSqlExpressionBuilder(builder.query, nameString)); + } + + MapSqlExpressionBuilder? get asMap { + return _set(() => MapSqlExpressionBuilder(builder.query, nameString)); + } + + ListSqlExpressionBuilder? get asList { + return _set(() => ListSqlExpressionBuilder(builder.query, nameString)); + } +} diff --git a/api/3rd_party/angel3_orm/lib/src/join_builder.dart b/api/3rd_party/angel3_orm/lib/src/join_builder.dart new file mode 100644 index 0000000..c342c77 --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/join_builder.dart @@ -0,0 +1,74 @@ +import 'annotations.dart'; +import 'query.dart'; + +/// Builds a SQL `JOIN` query. +class JoinBuilder { + final JoinType type; + final Query from; + final String? key, value, op, alias; + final bool aliasAllFields; + + /// A callback to produces the expression to join against, i.e. + /// a table name, or the result of compiling a query. + final String Function() to; + final List additionalFields; + + JoinBuilder(this.type, this.from, this.to, this.key, this.value, + {this.op = '=', + this.alias, + this.additionalFields = const [], + this.aliasAllFields = false}) { + //assert(to != null, + // 'computation of this join threw an error, and returned null.'); + } + + String get fieldName { + var v = value; + if (aliasAllFields) { + v = '${alias}_$v'; + } + var right = '${from.tableName}.$v'; + if (alias != null) right = '$alias.$v'; + return right; + } + + String nameFor(String name) { + if (aliasAllFields) name = '${alias}_$name'; + var right = '${from.tableName}.$name'; + if (alias != null) right = '$alias.$name'; + return right; + } + + String compile(Set? trampoline) { + var compiledTo = to(); + //if (compiledTo == null) return null; + if (compiledTo == '') { + return ''; + } + var b = StringBuffer(); + var left = '${from.tableName}.$key'; + var right = fieldName; + switch (type) { + case JoinType.inner: + b.write(' INNER JOIN'); + break; + case JoinType.left: + b.write(' LEFT JOIN'); + break; + case JoinType.right: + b.write(' RIGHT JOIN'); + break; + case JoinType.full: + b.write(' FULL OUTER JOIN'); + break; + case JoinType.self: + b.write(' SELF JOIN'); + break; + } + + b.write(' $compiledTo'); + if (alias != null) b.write(' $alias'); + b.write(' ON $left$op$right'); + return b.toString(); + } +} diff --git a/api/3rd_party/angel3_orm/lib/src/join_on.dart b/api/3rd_party/angel3_orm/lib/src/join_on.dart new file mode 100644 index 0000000..7bfed40 --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/join_on.dart @@ -0,0 +1,8 @@ +import 'builder.dart'; + +class JoinOn { + final SqlExpressionBuilder key; + final SqlExpressionBuilder value; + + JoinOn(this.key, this.value); +} diff --git a/api/3rd_party/angel3_orm/lib/src/map_query_values.dart b/api/3rd_party/angel3_orm/lib/src/map_query_values.dart new file mode 100644 index 0000000..398ba9b --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/map_query_values.dart @@ -0,0 +1,9 @@ +import 'query_values.dart'; + +/// A [QueryValues] implementation that simply writes to a [Map]. +class MapQueryValues extends QueryValues { + final Map values = {}; + + @override + Map toMap() => values; +} diff --git a/api/3rd_party/angel3_orm/lib/src/migration.dart b/api/3rd_party/angel3_orm/lib/src/migration.dart new file mode 100644 index 0000000..9af765e --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/migration.dart @@ -0,0 +1,135 @@ +const List SQL_RESERVED_WORDS = [ + 'SELECT', + 'UPDATE', + 'INSERT', + 'DELETE', + 'FROM', + 'ASC', + 'DESC', + 'VALUES', + 'RETURNING', + 'ORDER', + 'BY', +]; + +/// Applies additional attributes to a database column. +class Column { + /// If `true`, a SQL field will be nullable. + final bool isNullable; + + /// Specifies this column name. + final String name; + + /// Specifies the length of a `VARCHAR`. + final int length; + + /// Explicitly defines a SQL type for this column. + final ColumnType type; + + /// Specifies what kind of index this column is, if any. + final IndexType indexType; + + /// A custom SQL expression to execute, instead of a named column. + final String? expression; + + const Column( + {this.isNullable = true, + this.length = 255, + this.name = "", + this.type = ColumnType.varChar, + this.indexType = IndexType.none, + this.expression}); + + /// Returns `true` if [expression] is not `null`. + bool get hasExpression => expression != null; +} + +class PrimaryKey extends Column { + const PrimaryKey({ColumnType columnType = ColumnType.serial}) + : super(type: columnType, indexType: IndexType.primaryKey); +} + +const Column primaryKey = PrimaryKey(); + +/// Maps to SQL index types. +enum IndexType { + none, + + /// Standard index. + standardIndex, + + /// A primary key. + primaryKey, + + /// A *unique* index. + unique +} + +/// Maps to SQL data types. +/// +/// Features all types from this list: http://www.tutorialspoint.com/sql/sql-data-types.htm +class ColumnType { + /// The name of this data type. + final String name; + final bool hasSize; + + const ColumnType(this.name, [this.hasSize = false]); + + static const ColumnType boolean = ColumnType('boolean'); + + static const ColumnType smallSerial = ColumnType('smallserial'); + static const ColumnType serial = ColumnType('serial'); + static const ColumnType bigSerial = ColumnType('bigserial'); + + // Numbers + static const ColumnType bigInt = ColumnType('bigint'); + static const ColumnType int = ColumnType('int'); + static const ColumnType smallInt = ColumnType('smallint'); + static const ColumnType tinyInt = ColumnType('tinyint'); + static const ColumnType bit = ColumnType('bit'); + static const ColumnType decimal = ColumnType('decimal', true); + static const ColumnType numeric = ColumnType('numeric', true); + static const ColumnType money = ColumnType('money'); + static const ColumnType smallMoney = ColumnType('smallmoney'); + static const ColumnType float = ColumnType('float'); + static const ColumnType real = ColumnType('real'); + + // Dates and times + static const ColumnType dateTime = ColumnType('datetime'); + static const ColumnType smallDateTime = ColumnType('smalldatetime'); + static const ColumnType date = ColumnType('date'); + static const ColumnType time = ColumnType('time'); + static const ColumnType timeStamp = ColumnType('timestamp'); + static const ColumnType timeStampWithTimeZone = + ColumnType('timestamp with time zone'); + + // Strings + static const ColumnType char = ColumnType('char', true); + static const ColumnType varChar = ColumnType('varchar', true); + static const ColumnType varCharMax = ColumnType('varchar(max)'); + static const ColumnType text = ColumnType('text', true); + + // Unicode strings + static const ColumnType nChar = ColumnType('nchar', true); + static const ColumnType nVarChar = ColumnType('nvarchar', true); + static const ColumnType nVarCharMax = ColumnType('nvarchar(max)', true); + static const ColumnType nText = ColumnType('ntext', true); + + // Binary + static const ColumnType binary = ColumnType('binary', true); + static const ColumnType varBinary = ColumnType('varbinary', true); + static const ColumnType varBinaryMax = ColumnType('varbinary(max)', true); + static const ColumnType image = ColumnType('image', true); + + // JSON. + static const ColumnType json = ColumnType('json', true); + static const ColumnType jsonb = ColumnType('jsonb', true); + + // Misc. + static const ColumnType sqlVariant = ColumnType('sql_variant', true); + static const ColumnType uniqueIdentifier = + ColumnType('uniqueidentifier', true); + static const ColumnType xml = ColumnType('xml', true); + static const ColumnType cursor = ColumnType('cursor', true); + static const ColumnType table = ColumnType('table', true); +} diff --git a/api/3rd_party/angel3_orm/lib/src/order_by.dart b/api/3rd_party/angel3_orm/lib/src/order_by.dart new file mode 100644 index 0000000..4501cf4 --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/order_by.dart @@ -0,0 +1,8 @@ +class OrderBy { + final String key; + final bool descending; + + const OrderBy(this.key, {this.descending = false}); + + String compile() => descending ? '$key DESC' : '$key ASC'; +} diff --git a/api/3rd_party/angel3_orm/lib/src/query.dart b/api/3rd_party/angel3_orm/lib/src/query.dart new file mode 100644 index 0000000..f718236 --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/query.dart @@ -0,0 +1,425 @@ +import 'dart:async'; +import 'package:logging/logging.dart'; + +import 'annotations.dart'; +import 'join_builder.dart'; +import 'order_by.dart'; +import 'query_base.dart'; +import 'query_executor.dart'; +import 'query_values.dart'; +import 'query_where.dart'; +import 'package:optional/optional.dart'; + +/// A SQL `SELECT` query builder. +abstract class Query extends QueryBase { + final _log = Logger('Query'); + + final List _joins = []; + final Map _names = {}; + final List _orderBy = []; + + // An optional "parent query". If provided, [reserveName] will operate in + // the parent's context. + final Query? parent; + + /// A map of field names to explicit SQL expressions. The expressions will be aliased + /// to the given names. + final Map expressions = {}; + + String? _crossJoin, _groupBy; + int? _limit, _offset; + + Query({this.parent}); + + @override + Map get substitutionValues => + parent?.substitutionValues ?? super.substitutionValues; + + /// A reference to an abstract query builder. + /// + /// This is usually a generated class. + Where? get where; + + /// A set of values, for an insertion or update. + /// + /// This is usually a generated class. + QueryValues? get values; + + /// Preprends the [tableName] to the [String], [s]. + String adornWithTableName(String s) { + if (expressions.containsKey(s)) { + //return '${expressions[s]} AS $s'; + return '(${expressions[s]} AS $s)'; + } else { + return '$tableName.$s'; + } + } + + /// Returns a unique version of [name], which will not produce a collision within + /// the context of this [query]. + String reserveName(String name) { + if (parent != null) { + return parent!.reserveName(name); + } + // var n = _names[name] ??= 0; + // _names[name]++; + var n = 0; + var nn = _names[name]; + if (nn != null) { + n = nn; + nn++; + _names[name] = nn; + } else { + _names[name] = 1; + } + return n == 0 ? name : '$name$n'; + } + + /// Makes a [Where] clause. + Where newWhereClause() { + throw UnsupportedError( + 'This instance does not support creating WHERE clauses.'); + } + + /// Determines whether this query can be compiled. + /// + /// Used to prevent ambiguities in joins. + bool canCompile(Set trampoline) => true; + + /// Shorthand for calling [where].or with a [Where] clause. + void andWhere(void Function(Where) f) { + var w = newWhereClause(); + f(w); + where?.and(w); + } + + /// Shorthand for calling [where].or with a [Where] clause. + void notWhere(void Function(Where) f) { + var w = newWhereClause(); + f(w); + where?.not(w); + } + + /// Shorthand for calling [where].or with a [Where] clause. + void orWhere(void Function(Where) f) { + var w = newWhereClause(); + f(w); + where?.or(w); + } + + /// Limit the number of rows to return. + void limit(int n) { + _limit = n; + } + + /// Skip a number of rows in the query. + void offset(int n) { + _offset = n; + } + + /// Groups the results by a given key. + void groupBy(String key) { + _groupBy = key; + } + + /// Sorts the results by a key. + void orderBy(String key, {bool descending = false}) { + _orderBy.add(OrderBy(key, descending: descending)); + } + + /// Execute a `CROSS JOIN` (Cartesian product) against another table. + void crossJoin(String tableName) { + _crossJoin = tableName; + } + + String _joinAlias(Set trampoline) { + var i = _joins.length; + + while (true) { + var a = 'a$i'; + if (trampoline.add(a)) { + return a; + } else { + i++; + } + } + } + + String Function() _compileJoin(tableName, Set trampoline) { + if (tableName is String) { + return () => tableName; + } else if (tableName is Query) { + return () { + var c = tableName.compile(trampoline); + //if (c == null) return c; + if (c == '') { + return c; + } + return '($c)'; + }; + } else { + _log.severe('$tableName must be a String or Query'); + throw ArgumentError.value( + tableName, 'tableName', 'must be a String or Query'); + } + } + + void _makeJoin( + tableName, + Set? trampoline, + String? alias, + JoinType type, + String localKey, + String foreignKey, + String op, + List additionalFields) { + trampoline ??= {}; + + // Pivot tables guard against ambiguous fields by excluding tables + // that have already been queried in this scope. + if (trampoline.contains(tableName) && trampoline.contains(this.tableName)) { + // ex. if we have {roles, role_users}, then don't join "roles" again. + return; + } + + var to = _compileJoin(tableName, trampoline); + alias ??= _joinAlias(trampoline); + if (tableName is Query) { + for (var field in tableName.fields) { + tableName.aliases[field] = '${alias}_$field'; + } + } + _joins.add(JoinBuilder(type, this, to, localKey, foreignKey, + op: op, + alias: alias, + additionalFields: additionalFields, + aliasAllFields: tableName is Query)); + } + + /// Execute an `INNER JOIN` against another table. + void join(tableName, String localKey, String foreignKey, + {String op = '=', + List additionalFields = const [], + Set? trampoline, + String? alias}) { + _makeJoin(tableName, trampoline, alias, JoinType.inner, localKey, foreignKey, op, + additionalFields); + } + + /// Execute a `LEFT JOIN` against another table. + void leftJoin(tableName, String localKey, String foreignKey, + {String op = '=', + List additionalFields = const [], + Set? trampoline, + String? alias}) { + _makeJoin(tableName, trampoline, alias, JoinType.left, localKey, foreignKey, op, + additionalFields); + } + + /// Execute a `RIGHT JOIN` against another table. + void rightJoin(tableName, String localKey, String foreignKey, + {String op = '=', + List additionalFields = const [], + Set? trampoline, + String? alias}) { + _makeJoin(tableName, trampoline, alias, JoinType.right, localKey, foreignKey, op, + additionalFields); + } + + /// Execute a `FULL OUTER JOIN` against another table. + void fullOuterJoin(tableName, String localKey, String foreignKey, + {String op = '=', + List additionalFields = const [], + Set? trampoline, + String? alias}) { + _makeJoin(tableName, trampoline, alias, JoinType.full, localKey, foreignKey, op, + additionalFields); + } + + /// Execute a `SELF JOIN`. + void selfJoin(tableName, String localKey, String foreignKey, + {String op = '=', + List additionalFields = const [], + Set? trampoline, + String? alias}) { + _makeJoin(tableName, trampoline, alias, JoinType.self, localKey, foreignKey, op, + additionalFields); + } + + @override + String compile(Set trampoline, + {bool includeTableName = false, + String? preamble, + bool withFields = true, + String? fromQuery}) { + // One table MAY appear multiple times in a query. + if (!canCompile(trampoline)) { + //return null; + //throw Exception('One table appear multiple times in a query'); + return ''; + } + + includeTableName = includeTableName || _joins.isNotEmpty; + var b = StringBuffer(preamble ?? 'SELECT'); + b.write(' '); + List f; + + var compiledJoins = {}; + + //if (fields == null) { + if (fields.isEmpty) { + f = ['*']; + } else { + f = List.from(fields.map((s) { + String? ss = includeTableName ? '$tableName.$s' : s; + if (expressions.containsKey(s)) { + ss = '( ${expressions[s]} )'; + //ss = expressions[s]; + } + var cast = casts[s]; + if (cast != null) ss = 'CAST ($ss AS $cast)'; + if (aliases.containsKey(s)) { + if (cast != null) { + ss = '($ss) AS ${aliases[s]}'; + } else { + ss = '$ss AS ${aliases[s]}'; + } + if (expressions.containsKey(s)) { + // ss = '($ss)'; + } + } else if (expressions.containsKey(s)) { + if (cast != null) { + ss = '($ss) AS $s'; + // ss = '(($ss) AS $s)'; + } else { + ss = '$ss AS $s'; + // ss = '($ss AS $s)'; + } + } + return ss; + })); + _joins.forEach((j) { + var c = compiledJoins[j] = j.compile(trampoline); + //if (c != null) { + if (c != '') { + var additional = j.additionalFields.map(j.nameFor).toList(); + f.addAll(additional); + } else { + // If compilation failed, fill in NULL placeholders. + for (var i = 0; i < j.additionalFields.length; i++) { + f.add('NULL'); + } + } + }); + } + if (withFields) b.write(f.join(', ')); + fromQuery ??= tableName; + b.write(' FROM $fromQuery'); + + // No joins if it's not a select. + if (preamble == null) { + if (_crossJoin != null) b.write(' CROSS JOIN $_crossJoin'); + for (var join in _joins) { + var c = compiledJoins[join]; + if (c != null) b.write(' $c'); + } + } + + var whereClause = + where?.compile(tableName: includeTableName ? tableName : null); + if (whereClause?.isNotEmpty == true) { + b.write(' WHERE $whereClause'); + } + if (_groupBy != null) b.write(' GROUP BY $_groupBy'); + for (var item in _orderBy) { + b.write(' ORDER BY ${item.compile()}'); + } + if (_limit != null) b.write(' LIMIT $_limit'); + if (_offset != null) b.write(' OFFSET $_offset'); + return b.toString(); + } + + @override + Future> getOne(QueryExecutor executor) { + //limit(1); + return super.getOne(executor); + } + + Future> delete(QueryExecutor executor) { + var sql = compile({}, preamble: 'DELETE', withFields: false); + + //_log.fine("Delete Query = $sql"); + + if (_joins.isEmpty) { + return executor + .query(tableName, sql, substitutionValues, + fields.map(adornWithTableName).toList()) + .then((it) => deserializeList(it)); + } else { + return executor.transaction((tx) async { + // TODO: Can this be done with just *one* query? + var existing = await get(tx); + //var sql = compile(preamble: 'SELECT $tableName.id', withFields: false); + return tx + .query(tableName, sql, substitutionValues) + .then((_) => existing); + }); + } + } + + Future> deleteOne(QueryExecutor executor) { + return delete(executor).then((it) => + it.isEmpty == true ? Optional.empty() : Optional.ofNullable(it.first)); + } + + Future> insert(QueryExecutor executor) { + var insertion = values?.compileInsert(this, tableName); + + if (insertion == '') { + throw StateError('No values have been specified for update.'); + } else { + // TODO: How to do this in a non-Postgres DB? + var returning = fields.map(adornWithTableName).join(', '); + var sql = compile({}); + sql = 'WITH $tableName as ($insertion RETURNING $returning) ' + sql; + + //_log.fine("Insert Query = $sql"); + + return executor.query(tableName, sql, substitutionValues).then((it) { + // Return SQL execution results + return it.isEmpty ? Optional.empty() : deserialize(it.first); + }); + } + } + + Future> update(QueryExecutor executor) async { + var updateSql = StringBuffer('UPDATE $tableName '); + var valuesClause = values?.compileForUpdate(this); + + if (valuesClause == '') { + throw StateError('No values have been specified for update.'); + } else { + updateSql.write(' $valuesClause'); + var whereClause = where?.compile(); + if (whereClause?.isNotEmpty == true) { + updateSql.write(' WHERE $whereClause'); + } + if (_limit != null) updateSql.write(' LIMIT $_limit'); + + var returning = fields.map(adornWithTableName).join(', '); + var sql = compile({}); + sql = 'WITH $tableName as ($updateSql RETURNING $returning) ' + sql; + + //_log.fine("Update Query = $sql"); + + return executor + .query(tableName, sql, substitutionValues) + .then((it) => deserializeList(it)); + } + } + + Future> updateOne(QueryExecutor executor) { + return update(executor).then( + (it) => it.isEmpty ? Optional.empty() : Optional.ofNullable(it.first)); + } +} diff --git a/api/3rd_party/angel3_orm/lib/src/query_base.dart b/api/3rd_party/angel3_orm/lib/src/query_base.dart new file mode 100644 index 0000000..17a6215 --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/query_base.dart @@ -0,0 +1,81 @@ +import 'dart:async'; + +import 'query_executor.dart'; +import 'union.dart'; +import 'package:optional/optional.dart'; + +/// A base class for objects that compile to SQL queries, typically within an ORM. +abstract class QueryBase { + /// Casts to perform when querying the database. + Map get casts => {}; + + /// `AS` aliases to inject into the query, if any. + Map aliases = {}; + + /// Values to insert into a prepared statement. + final Map substitutionValues = {}; + + /// The table against which to execute this query. + String get tableName; + + /// The list of fields returned by this query. + /// + /// @deprecated If it's `null`, then this query will perform a `SELECT *`. + /// If it's empty, then this query will perform a `SELECT *`. + List get fields; + + /// A String of all [fields], joined by a comma (`,`). + String get fieldSet => fields.map((k) { + var cast = casts[k]; + if (!aliases.containsKey(k)) { + return cast == null ? k : 'CAST ($k AS $cast)'; + } else { + var inner = cast == null ? k : '(CAST ($k AS $cast))'; + return '$inner AS ${aliases[k]}'; + } + }).join(', '); + + String compile(Set trampoline, + {bool includeTableName = false, + String preamble = '', + bool withFields = true}); + + Optional deserialize(List row); + + List deserializeList(List> it) { + var optResult = it.map(deserialize).toList(); + var result = []; + optResult.forEach((element) { + element.ifPresent((item) { + result.add(item); + }); + }); + + return result; + } + + Future> get(QueryExecutor executor) async { + var sql = compile({}); + + //_log.fine('sql = $sql'); + //_log.fine('substitutionValues = $substitutionValues'); + + return executor.query(tableName, sql, substitutionValues).then((it) { + return deserializeList(it); + }); + } + + Future> getOne(QueryExecutor executor) { + //return get(executor).then((it) => it.isEmpty ? : it.first); + return get(executor).then( + (it) => it.isEmpty ? Optional.empty() : Optional.ofNullable(it.first)); + } + + Union union(QueryBase other) { + return Union(this, other); + } + + Union unionAll(QueryBase other) { + return Union(this, other, all: true); + } +} diff --git a/api/3rd_party/angel3_orm/lib/src/query_executor.dart b/api/3rd_party/angel3_orm/lib/src/query_executor.dart new file mode 100644 index 0000000..fac6195 --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/query_executor.dart @@ -0,0 +1,23 @@ +import 'dart:async'; + +/// An abstract interface that performs queries. +/// +/// This class should be implemented. +abstract class QueryExecutor { + const QueryExecutor(); + + /// Executes a single query. + Future> query( + String tableName, String query, Map substitutionValues, + [List returningFields = const []]); + + /// Enters a database transaction, performing the actions within, + /// and returning the results of [f]. + /// + /// If [f] fails, the transaction will be rolled back, and the + /// responsible exception will be re-thrown. + /// + /// Whether nested transactions are supported depends on the + /// underlying driver. + Future transaction(FutureOr Function(QueryExecutor) f); +} diff --git a/api/3rd_party/angel3_orm/lib/src/query_values.dart b/api/3rd_party/angel3_orm/lib/src/query_values.dart new file mode 100644 index 0000000..2b265cf --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/query_values.dart @@ -0,0 +1,89 @@ +import 'query.dart'; + +abstract class QueryValues { + Map get casts => {}; + + Map toMap(); + + String applyCast(String name, String sub) { + if (casts.containsKey(name)) { + var type = casts[name]; + return 'CAST ($sub as $type)'; + } else { + return sub; + } + } + + String compileInsert(Query query, String tableName) { + var data = Map.from(toMap()); + var now = DateTime.now(); + if (data.containsKey('created_at') && data['created_at'] == null) { + data['created_at'] = now; + } + if (data.containsKey('createdAt') && data['createdAt'] == null) { + data['createdAt'] = now; + } + if (data.containsKey('updated_at') && data['updated_at'] == null) { + data['updated_at'] = now; + } + if (data.containsKey('updatedAt') && data['updatedAt'] == null) { + data['updatedAt'] = now; + } + var keys = data.keys.toList(); + keys.where((k) => !query.fields.contains(k)).forEach(data.remove); + if (data.isEmpty) { + return ''; + } + var fieldSet = data.keys.join(', '); + var b = StringBuffer('INSERT INTO $tableName ($fieldSet) VALUES ('); + var i = 0; + + for (var entry in data.entries) { + if (i++ > 0) b.write(', '); + + var name = query.reserveName(entry.key); + + var s = applyCast(entry.key, '@$name'); + query.substitutionValues[name] = entry.value; + b.write(s); + } + + b.write(')'); + return b.toString(); + } + + String compileForUpdate(Query query) { + var data = toMap(); + if (data.isEmpty) { + return ''; + } + var now = DateTime.now(); + if (data.containsKey('created_at') && data['created_at'] == null) { + data.remove('created_at'); + } + if (data.containsKey('createdAt') && data['createdAt'] == null) { + data.remove('createdAt'); + } + if (data.containsKey('updated_at') && data['updated_at'] == null) { + data['updated_at'] = now; + } + if (data.containsKey('updatedAt') && data['updatedAt'] == null) { + data['updatedAt'] = now; + } + var b = StringBuffer('SET'); + var i = 0; + + for (var entry in data.entries) { + if (i++ > 0) b.write(','); + b.write(' '); + b.write(entry.key); + b.write('='); + + var name = query.reserveName(entry.key); + var s = applyCast(entry.key, '@$name'); + query.substitutionValues[name] = entry.value; + b.write(s); + } + return b.toString(); + } +} diff --git a/api/3rd_party/angel3_orm/lib/src/query_where.dart b/api/3rd_party/angel3_orm/lib/src/query_where.dart new file mode 100644 index 0000000..5a700c2 --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/query_where.dart @@ -0,0 +1,69 @@ +import 'builder.dart'; + +/// Builds a SQL `WHERE` clause. +abstract class QueryWhere { + final Set _and = {}; + final Set _not = {}; + final Set _or = {}; + final Set _raw = {}; + + Iterable get expressionBuilders; + + void and(QueryWhere other) { + _and.add(other); + } + + void not(QueryWhere other) { + _not.add(other); + } + + void or(QueryWhere other) { + _or.add(other); + } + + void raw(String whereRaw) { + _raw.add(whereRaw); + } + + String compile({String? tableName}) { + var b = StringBuffer(); + var i = 0; + + for (var builder in expressionBuilders) { + var key = builder.columnName; + if (tableName != null) key = '$tableName.$key'; + if (builder.hasValue) { + if (i++ > 0) b.write(' AND '); + if (builder is DateTimeSqlExpressionBuilder || + (builder is JsonSqlExpressionBuilder && builder.hasRaw)) { + if (tableName != null) b.write('$tableName.'); + b.write(builder.compile()); + } else { + b.write('$key ${builder.compile()}'); + } + } + } + + for (var raw in _raw) { + if (i++ > 0) b.write(' AND '); + b.write(' ($raw)'); + } + + for (var other in _and) { + var sql = other.compile(); + if (sql.isNotEmpty) b.write(' AND ($sql)'); + } + + for (var other in _not) { + var sql = other.compile(); + if (sql.isNotEmpty) b.write(' NOT ($sql)'); + } + + for (var other in _or) { + var sql = other.compile(); + if (sql.isNotEmpty) b.write(' OR ($sql)'); + } + + return b.toString(); + } +} diff --git a/api/3rd_party/angel3_orm/lib/src/relations.dart b/api/3rd_party/angel3_orm/lib/src/relations.dart new file mode 100644 index 0000000..ab7097e --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/relations.dart @@ -0,0 +1,91 @@ +import 'annotations.dart'; + +abstract class RelationshipType { + static const int hasMany = 0; + static const int hasOne = 1; + static const int belongsTo = 2; + static const int manyToMany = 3; +} + +class Relationship { + final int type; + final String? localKey; + final String? foreignKey; + final String? foreignTable; + final bool cascadeOnDelete; + final JoinType? joinType; + + const Relationship(this.type, + {this.localKey, + this.foreignKey, + this.foreignTable, + this.cascadeOnDelete = false, + this.joinType}); +} + +class HasMany extends Relationship { + const HasMany( + {String? localKey, + String? foreignKey, + String? foreignTable, + bool cascadeOnDelete = false, + JoinType? joinType}) + : super(RelationshipType.hasMany, + localKey: localKey, + foreignKey: foreignKey, + foreignTable: foreignTable, + cascadeOnDelete: cascadeOnDelete == true, + joinType: joinType); +} + +const HasMany hasMany = HasMany(); + +class HasOne extends Relationship { + const HasOne( + {String? localKey, + String? foreignKey, + String? foreignTable, + bool cascadeOnDelete = false, + JoinType? joinType}) + : super(RelationshipType.hasOne, + localKey: localKey, + foreignKey: foreignKey, + foreignTable: foreignTable, + cascadeOnDelete: cascadeOnDelete == true, + joinType: joinType); +} + +const HasOne hasOne = HasOne(); + +class BelongsTo extends Relationship { + const BelongsTo( + {String? localKey, + String? foreignKey, + String? foreignTable, + JoinType? joinType}) + : super(RelationshipType.belongsTo, + localKey: localKey, + foreignKey: foreignKey, + foreignTable: foreignTable, + joinType: joinType); +} + +const BelongsTo belongsTo = BelongsTo(); + +class ManyToMany extends Relationship { + final Type through; + + const ManyToMany(this.through, + {String? localKey, + String? foreignKey, + String? foreignTable, + bool cascadeOnDelete = false, + JoinType? joinType}) + : super( + RelationshipType.hasMany, // Many-to-Many is actually just a hasMany + localKey: localKey, + foreignKey: foreignKey, + foreignTable: foreignTable, + cascadeOnDelete: cascadeOnDelete == true, + joinType: joinType); +} diff --git a/api/3rd_party/angel3_orm/lib/src/union.dart b/api/3rd_party/angel3_orm/lib/src/union.dart new file mode 100644 index 0000000..5ab2f86 --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/union.dart @@ -0,0 +1,38 @@ +import 'query_base.dart'; +import 'package:optional/optional.dart'; + +/// Represents the `UNION` of two subqueries. +class Union extends QueryBase { + /// The subject(s) of this binary operation. + final QueryBase left, right; + + /// Whether this is a `UNION ALL` operation. + final bool all; + + @override + final String tableName; + + Union(this.left, this.right, {this.all = false, String? tableName}) + : tableName = tableName ?? left.tableName { + substitutionValues + ..addAll(left.substitutionValues) + ..addAll(right.substitutionValues); + } + + @override + List get fields => left.fields; + + @override + Optional deserialize(List row) => left.deserialize(row); + + @override + String compile(Set trampoline, + {bool includeTableName = false, + String? preamble, + bool withFields = true}) { + var selector = all == true ? 'UNION ALL' : 'UNION'; + var t1 = Set.from(trampoline); + var t2 = Set.from(trampoline); + return '(${left.compile(t1, includeTableName: includeTableName)}) $selector (${right.compile(t2, includeTableName: includeTableName)})'; + } +} diff --git a/api/3rd_party/angel3_orm/lib/src/util.dart b/api/3rd_party/angel3_orm/lib/src/util.dart new file mode 100644 index 0000000..939b36c --- /dev/null +++ b/api/3rd_party/angel3_orm/lib/src/util.dart @@ -0,0 +1,3 @@ +import 'package:charcode/ascii.dart'; + +bool isAscii(int ch) => ch >= $nul && ch <= $del; diff --git a/api/3rd_party/angel3_orm/mono_pkg.yaml b/api/3rd_party/angel3_orm/mono_pkg.yaml new file mode 100644 index 0000000..e69de29 diff --git a/api/3rd_party/angel3_orm/pubspec.yaml b/api/3rd_party/angel3_orm/pubspec.yaml new file mode 100644 index 0000000..c688aba --- /dev/null +++ b/api/3rd_party/angel3_orm/pubspec.yaml @@ -0,0 +1,21 @@ +name: angel3_orm +version: 4.0.4 +description: Runtime support for Angel3 ORM. Includes base classes for queries. +homepage: https://angel3-framework.web.app/ +repository: https://github.com/dukefirehawk/angel/tree/master/packages/orm/angel_orm +environment: + sdk: '>=2.12.0 <3.0.0' +dependencies: + charcode: ^1.2.0 + intl: ^0.17.0 + meta: ^1.3.0 + string_scanner: ^1.1.0 + optional: ^6.0.0 + logging: ^1.0.0 +dev_dependencies: + angel3_model: ^3.0.0 + angel3_serialize: ^4.1.0 + angel3_serialize_generator: ^4.1.0 + build_runner: ^2.1.1 + test: ^1.17.4 + lints: ^1.0.0 \ No newline at end of file diff --git a/api/bin/migrate.dart b/api/bin/migrate.dart index eec2b33..44584a5 100644 --- a/api/bin/migrate.dart +++ b/api/bin/migrate.dart @@ -5,6 +5,8 @@ import 'package:angel3_migration_runner/postgres.dart'; import 'package:angel3_orm_postgres/angel3_orm_postgres.dart'; import 'package:dde_gesture_manager_api/models.dart'; import 'package:dde_gesture_manager_api/src/config/plugins/orm.dart'; +import 'package:dde_gesture_manager_api/src/models/download_history.dart'; +import 'package:dde_gesture_manager_api/src/models/like_record.dart'; import 'package:file/local.dart'; import 'package:logging/logging.dart'; @@ -29,6 +31,8 @@ void main(List args) async { UserMigration(), UserSeed(), SchemeMigration(), + DownloadHistoryMigration(), + LikeRecordMigration(), ]); await runMigrations(migrationRunner, args); } diff --git a/api/lib/apis.dart b/api/lib/apis.dart index 3692087..ada4277 100644 --- a/api/lib/apis.dart +++ b/api/lib/apis.dart @@ -31,10 +31,17 @@ class SchemeApis { String get upload => [path, 'upload'].joinPath(); - String get userUploads => [path, 'user', 'uploads'].joinPath(); + String markAsShared({required StringParam schemeId}) => [path, 'mark_as_shared', schemeId].joinPath(); + + String user({required StringParam type}) => [path, 'user', type].joinPath(); + + String download({required StringParam schemeId}) => [path, 'download', schemeId].joinPath(); + + String like({required StringParam schemeId, required StringParam isLike}) => [path, 'like', schemeId, isLike].joinPath(); } final _paramsMap = { + 'BoolParam': BoolParam.nameOnRoute, 'IntParam': IntParam.nameOnRoute, 'DoubleParam': DoubleParam.nameOnRoute, 'StringParam': StringParam.nameOnRoute, @@ -58,6 +65,18 @@ extension RouteUrl on Function { } } +class BoolParam { + final bool val; + String? name; + + BoolParam(this.val); + + BoolParam.nameOnRoute(this.name) : val = true; + + @override + String toString() => name == null ? val.toString() : 'bool:$name'; +} + class IntParam { final int val; String? name; @@ -94,6 +113,10 @@ class StringParam { String toString() => name == null ? val.toString() : ':$name'; } +extension BoolParamExt on bool { + BoolParam get param => BoolParam(this); +} + extension IntParamExt on int { IntParam get param => IntParam(this); } diff --git a/api/lib/src/models/download_history.dart b/api/lib/src/models/download_history.dart new file mode 100644 index 0000000..10218d6 --- /dev/null +++ b/api/lib/src/models/download_history.dart @@ -0,0 +1,19 @@ +import 'package:angel3_orm/angel3_orm.dart'; +import 'package:angel3_serialize/angel3_serialize.dart'; +import 'package:dde_gesture_manager_api/src/models/base_model.dart'; +import 'package:angel3_migration/angel3_migration.dart'; +import 'package:optional/optional.dart'; + +part 'download_history.g.dart'; + +@serializable +@orm +abstract class _DownloadHistory extends BaseModel { + @Column(isNullable: false, indexType: IndexType.standardIndex) + @SerializableField(isNullable: false) + int? get uid; + + @Column(isNullable: false, indexType: IndexType.standardIndex) + @SerializableField(isNullable: false) + int? get schemeId; +} \ No newline at end of file diff --git a/api/lib/src/models/like_record.dart b/api/lib/src/models/like_record.dart new file mode 100644 index 0000000..c794cfe --- /dev/null +++ b/api/lib/src/models/like_record.dart @@ -0,0 +1,23 @@ +import 'package:angel3_orm/angel3_orm.dart'; +import 'package:angel3_serialize/angel3_serialize.dart'; +import 'package:dde_gesture_manager_api/src/models/base_model.dart'; +import 'package:angel3_migration/angel3_migration.dart'; +import 'package:optional/optional.dart'; + +part 'like_record.g.dart'; + +@serializable +@orm +abstract class _LikeRecord extends BaseModel { + @Column(isNullable: false, indexType: IndexType.standardIndex) + @SerializableField(isNullable: false) + int? get uid; + + @Column(isNullable: false, indexType: IndexType.standardIndex) + @SerializableField(isNullable: false) + int? get schemeId; + + @Column(isNullable: false, indexType: IndexType.standardIndex) + @SerializableField(isNullable: false) + bool? get liked; +} \ No newline at end of file diff --git a/api/lib/src/models/scheme.dart b/api/lib/src/models/scheme.dart index 49e3516..ff665c3 100644 --- a/api/lib/src/models/scheme.dart +++ b/api/lib/src/models/scheme.dart @@ -33,3 +33,91 @@ abstract class _Scheme extends BaseModel { @DefaultsTo([]) List? get gestures; } + +@serializable +@Orm(tableName: 'schemes', generateMigrations: false) +abstract class _SimpleScheme { + @Column() + int? id; + + @Column(isNullable: false, indexType: IndexType.unique) + @SerializableField(isNullable: false) + String? get uuid; + + @Column(isNullable: false) + @SerializableField(isNullable: false) + String? get name; + + @Column(isNullable: false, indexType: IndexType.standardIndex) + @SerializableField(isNullable: true, exclude: true) + int? uid; + + @Column(type: ColumnType.text) + String? description; + + @Column(isNullable: false, indexType: IndexType.standardIndex) + @SerializableField(defaultValue: false, isNullable: false) + bool? get shared; + + @SerializableField(isNullable: true) + @Column(type: ColumnType.json) + Map? get metadata; + + @SerializableField(isNullable: true) + @Column(expression: 'lr.liked') + bool? get liked; +} + +@serializable +abstract class _SimpleSchemeTransMetaData { + @SerializableField(isNullable: false) + String? get uuid; + + @SerializableField(isNullable: false) + String? get name; + + @SerializableField(isNullable: false) + String? description; + + @SerializableField(defaultValue: false, isNullable: false) + bool? get shared; + + int? get downloads; + + int? get likes; + + bool? get liked; +} + +SimpleSchemeTransMetaData transSimpleSchemeMetaData(SimpleScheme scheme) => SimpleSchemeTransMetaData( + description: scheme.description, + uuid: scheme.uuid, + name: scheme.name, + shared: scheme.shared, + liked: scheme.liked, + likes: scheme.metadata?['likes'] ?? 0, + downloads: scheme.metadata?['downloads'] ?? 0, + ); + +@serializable +abstract class _SchemeForDownload { + @SerializableField(isNullable: false) + String? get uuid; + + @SerializableField(isNullable: false) + String? get name; + + @Column(type: ColumnType.text) + String? description; + + @SerializableField() + @DefaultsTo([]) + List? get gestures; +} + +SchemeForDownload transSchemeForDownload(Scheme scheme) => SchemeForDownload( + uuid: scheme.uuid, + name: scheme.name, + description: scheme.description, + gestures: scheme.gestures, + ); diff --git a/api/lib/src/routes/controllers/auth_controllers.dart b/api/lib/src/routes/controllers/auth_controllers.dart index 35d8823..41d4539 100644 --- a/api/lib/src/routes/controllers/auth_controllers.dart +++ b/api/lib/src/routes/controllers/auth_controllers.dart @@ -84,7 +84,7 @@ Future configureServer(Angel app) async { chain( [ jwtMiddleware(), - (req, res) => req.user.blocked == false ? res.noContent() : res.forbidden(), + (req, res) => req.user!.blocked == false ? res.noContent() : res.forbidden(), ], ), ); diff --git a/api/lib/src/routes/controllers/controller_extensions.dart b/api/lib/src/routes/controllers/controller_extensions.dart index b33549b..117703d 100644 --- a/api/lib/src/routes/controllers/controller_extensions.dart +++ b/api/lib/src/routes/controllers/controller_extensions.dart @@ -48,5 +48,11 @@ extension RedisClient on RequestContext { } extension JWTUserInstance on RequestContext { - User get user => container!.make(); + User? get user { + try { + return container!.make(); + } catch (_) { + return null; + } + } } diff --git a/api/lib/src/routes/controllers/middlewares.dart b/api/lib/src/routes/controllers/middlewares.dart index 2ea42ff..f565a6e 100644 --- a/api/lib/src/routes/controllers/middlewares.dart +++ b/api/lib/src/routes/controllers/middlewares.dart @@ -4,9 +4,10 @@ import 'package:angel3_framework/angel3_framework.dart'; import 'package:dde_gesture_manager_api/models.dart'; import '../controllers/controller_extensions.dart'; -RequestHandler jwtMiddleware() { +RequestHandler jwtMiddleware({ignoreError = false}) { return (RequestContext req, ResponseContext res, {bool throwError = true}) async { - bool _reject(ResponseContext res) { + bool _reject(ResponseContext res, [ignoreError = false]) { + if (ignoreError) return true; if (throwError) { res.forbidden(); } @@ -18,17 +19,22 @@ RequestHandler jwtMiddleware() { if (reqContainer.has() || req.method == 'OPTIONS') { return true; } else if (reqContainer.has>()) { - User user = await reqContainer.makeAsync(); - var authToken = req.container!.make(); - if (user.secret(req.app!.configuration['password_salt']) != authToken.payload[UserFields.password]) { - return _reject(res); + try { + User user = await reqContainer.makeAsync(); + var authToken = req.container!.make(); + if (user.secret(req.app!.configuration['password_salt']) != authToken.payload[UserFields.password]) { + return _reject(res, ignoreError); + } + } catch (e) { + if (ignoreError) return true; + rethrow; } return true; } else { - return _reject(res); + return _reject(res, ignoreError); } } else { - return _reject(res); + return _reject(res, ignoreError); } }; } diff --git a/api/lib/src/routes/controllers/scheme_controllers.dart b/api/lib/src/routes/controllers/scheme_controllers.dart index 4d4a1ca..03f9306 100644 --- a/api/lib/src/routes/controllers/scheme_controllers.dart +++ b/api/lib/src/routes/controllers/scheme_controllers.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'package:angel3_framework/angel3_framework.dart'; import 'package:dde_gesture_manager_api/apis.dart'; +import 'package:dde_gesture_manager_api/src/models/download_history.dart'; +import 'package:dde_gesture_manager_api/src/models/like_record.dart'; import 'package:dde_gesture_manager_api/src/models/scheme.dart'; import 'package:dde_gesture_manager_api/src/routes/controllers/middlewares.dart'; import 'package:logging/logging.dart'; @@ -20,16 +22,18 @@ Future configureServer(Angel app) async { var scheme = SchemeSerializer.fromMap(req.bodyAsMap); var schemeQuery = SchemeQuery(); schemeQuery.where!.uuid.equals(scheme.uuid!); - var one = await schemeQuery.getOne(req.queryExecutor); - schemeQuery = SchemeQuery(); - schemeQuery.values.copyFrom(scheme); - schemeQuery.values.uid = int.parse(req.user.id!); - if (one.isEmpty) { - await schemeQuery.insert(req.queryExecutor); - } else { - schemeQuery.whereId = int.parse(one.value.id!); - await schemeQuery.updateOne(req.queryExecutor); - } + req.queryExecutor.transaction((tx) async { + var one = await schemeQuery.getOne(tx); + schemeQuery = SchemeQuery(); + schemeQuery.values.copyFrom(scheme); + schemeQuery.values.uid = req.user!.idAsInt; + if (one.isEmpty) { + return await schemeQuery.insert(tx); + } else { + schemeQuery.whereId = one.value.idAsInt; + return await schemeQuery.updateOne(tx); + } + }); } catch (e) { _log.severe(e); return res.unProcessableEntity(); @@ -40,21 +44,145 @@ Future configureServer(Angel app) async { ), ); + app.post( + Apis.scheme.markAsShared.route, + chain( + [ + jwtMiddleware(), + (req, res) async { + var schemeId = req.params['schemeId']; + var schemeQuery = SchemeQuery(); + schemeQuery.where!.uuid.equals(schemeId); + schemeQuery.values.shared = true; + await schemeQuery.updateOne(req.queryExecutor); + return res.noContent(); + }, + ], + ), + ); + + app.get( + Apis.scheme.user.route, + chain( + [ + jwtMiddleware(), + (req, res) async { + var schemeQuery = SimpleSchemeQuery(); + var type = req.params['type']; + var likeRecordTableName = LikeRecordQuery().tableName; + schemeQuery.leftJoin(likeRecordTableName, SchemeFields.id, LikeRecordFields.schemeId, alias: 'lr'); + + switch (type) { + case 'uploaded': + schemeQuery.where!.uid.equals(req.user!.idAsInt); + break; + case 'downloaded': + var downloadHistoryTableName = DownloadHistoryQuery().tableName; + schemeQuery.leftJoin(downloadHistoryTableName, SchemeFields.id, DownloadHistoryFields.schemeId, + alias: 'dh'); + schemeQuery.where!.raw('dh.${DownloadHistoryFields.uid} = ${req.user!.idAsInt}'); + break; + case 'liked': + schemeQuery.where!.raw('lr.${LikeRecordFields.uid} = ${req.user!.idAsInt}'); + schemeQuery.where!.raw('lr.${LikeRecordFields.liked} = true'); + break; + default: + return res.unProcessableEntity(); + } + schemeQuery.orderBy('${schemeQuery.tableName}.${SchemeFields.updatedAt}', descending: true); + return schemeQuery.get(req.queryExecutor).then((value) => value.map(transSimpleSchemeMetaData).toList()); + }, + ], + ), + ); + app.get( - Apis.scheme.userUploads, + Apis.scheme.download.route, + chain( + [ + jwtMiddleware(ignoreError: true), + (req, res) async { + var schemeQuery = SchemeQuery(); + schemeQuery.where?.uuid.equals(req.params['schemeId']); + var optionalScheme = await schemeQuery.getOne(req.queryExecutor); + if (optionalScheme.isNotEmpty) { + var scheme = optionalScheme.value; + if (req.user != null) { + /// 增加用户下载记录 + var downloadHistoryQuery = DownloadHistoryQuery(); + downloadHistoryQuery.where?.uid.equals(req.user!.idAsInt); + downloadHistoryQuery.where?.schemeId.equals(scheme.idAsInt); + var notExist = (await downloadHistoryQuery.getOne(req.queryExecutor)).isEmpty; + if (notExist) { + downloadHistoryQuery = DownloadHistoryQuery(); + downloadHistoryQuery.values.copyFrom(DownloadHistory(uid: req.user!.idAsInt, schemeId: scheme.idAsInt)); + await downloadHistoryQuery.insert(req.queryExecutor); + } + } + + /// 增加方案的下载数量 + schemeQuery = SchemeQuery(); + schemeQuery.whereId = scheme.idAsInt; + Map metadata = Map.from(scheme.metadata!); + metadata.update('downloads', (value) => ++value, ifAbsent: () => 1); + schemeQuery.values.metadata = metadata; + schemeQuery.updateOne(req.queryExecutor); + + return res.json(transSchemeForDownload(scheme)); + } + return res.notFound(); + }, + ], + ), + ); + + app.get( + Apis.scheme.like.route, chain( [ jwtMiddleware(), (req, res) async { + bool isLike = req.params['isLike'] == 'like'; + bool needUpdate = true; var schemeQuery = SchemeQuery(); - schemeQuery.where!.uid.equals(int.parse(req.user.id!)); - schemeQuery.orderBy(SchemeFields.updatedAt, descending: true); - return schemeQuery.get(req.queryExecutor).then((value) => value.map((e) => { - 'name': e.name, - 'description': e.description, - }).toList()); + schemeQuery.where?.uuid.equals(req.params['schemeId']); + var optionalScheme = await schemeQuery.getOne(req.queryExecutor); + if (optionalScheme.isNotEmpty) { + var scheme = optionalScheme.value; + if (req.user != null) { + /// 增加用户点赞记录 + var likeRecordQuery = LikeRecordQuery(); + likeRecordQuery.where?.uid.equals(req.user!.idAsInt); + likeRecordQuery.where?.schemeId.equals(scheme.idAsInt); + var likeRecordCheck = await likeRecordQuery.getOne(req.queryExecutor); + likeRecordQuery = LikeRecordQuery(); + likeRecordQuery.values + .copyFrom(LikeRecord(uid: req.user!.idAsInt, schemeId: scheme.idAsInt, liked: isLike)); + if (likeRecordCheck.isEmpty) { + likeRecordQuery.insert(req.queryExecutor); + } else if (likeRecordCheck.value.liked != isLike) { + likeRecordQuery.whereId = likeRecordCheck.value.idAsInt; + likeRecordQuery.updateOne(req.queryExecutor); + } else { + needUpdate = false; + } + } + + if (needUpdate) { + /// 增加/减少方案的点赞数量 + schemeQuery = SchemeQuery(); + schemeQuery.whereId = scheme.idAsInt; + Map metadata = Map.from(scheme.metadata!); + metadata.update('likes', (value) => isLike ? ++value : --value, ifAbsent: () => 1); + schemeQuery.values.metadata = metadata; + schemeQuery.updateOne(req.queryExecutor); + } + + return res.noContent(); + } + return res.notFound(); }, ], ), ); -} \ No newline at end of file +} diff --git a/api/pubspec.yaml b/api/pubspec.yaml index 9831fbc..244c628 100644 --- a/api/pubspec.yaml +++ b/api/pubspec.yaml @@ -8,8 +8,7 @@ dependencies: 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_migration: ^4.0.3 angel3_orm_postgres: ^3.0.0 angel3_serialize: ^4.1.0 angel3_production: ^3.1.0 @@ -21,13 +20,15 @@ dependencies: yaml: ^3.1.0 mailer: ^5.0.2 uuid: ^3.0.5 + angel3_orm: + path: 3rd_party/angel3_orm neat_cache: path: 3rd_party/neat_cache dev_dependencies: angel3_hot: ^4.2.0 angel3_jinja: ^2.0.1 angel3_migration_runner: ^4.0.0 - angel3_orm_generator: ^4.1.0 + angel3_orm_generator: ^4.1.3 angel3_serialize_generator: ^4.2.0 angel3_test: ^4.0.0 build_runner: ^2.0.3 @@ -35,5 +36,6 @@ dev_dependencies: test: ^1.17.5 lints: ^1.0.0 - - +dependency_overrides: + angel3_orm: + path: 3rd_party/angel3_orm diff --git a/app/lib/http/api.dart b/app/lib/http/api.dart index b3264fe..4841c11 100644 --- a/app/lib/http/api.dart +++ b/app/lib/http/api.dart @@ -6,6 +6,7 @@ import 'package:dde_gesture_manager/extensions.dart'; import 'package:dde_gesture_manager/models/scheme.dart' as AppScheme; import 'package:dde_gesture_manager/utils/helper.dart'; import 'package:dde_gesture_manager/utils/notificator.dart'; +import 'package:dde_gesture_manager/widgets/me.dart'; import 'package:dde_gesture_manager_api/apis.dart'; import 'package:dde_gesture_manager_api/models.dart'; import 'package:http/http.dart' as http; @@ -85,7 +86,7 @@ class Api { if (ignoreErrorHandle) throw e; else - return _handleHttpError(e); + _handleHttpError(e); }, ); @@ -116,7 +117,7 @@ class Api { if (ignoreErrorHandle) throw e; else - return _handleHttpError(e); + _handleHttpError(e); }, ); @@ -158,6 +159,19 @@ class Api { ), ).then((value) => value == HttpStatus.noContent); - static Future> userUploads() => - _get(Apis.scheme.userUploads, listRespBuilderWrap(SchemeSerializer.fromMap)); + static Future> userSchemes({required SchemeListType type}) => + _get(Apis.scheme.user(type: type.name.param), listRespBuilderWrap(SimpleSchemeTransMetaDataSerializer.fromMap)); + + static Future likeScheme({required String schemeId, required bool isLike}) => _get( + Apis.scheme.like(schemeId: schemeId.param, isLike: StringParam(isLike ? 'like' : 'unlike')), + getStatusCodeFunc) + .then((value) { + 123.sout(); + return value == HttpStatus.noContent; + }); + + static Future downloadScheme({required String schemeId}) => _get( + Apis.scheme.download(schemeId: schemeId.param), + SchemeForDownloadSerializer.fromMap, + ); } diff --git a/app/lib/main.dart b/app/lib/main.dart index a507d48..93e1f5d 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -11,6 +11,7 @@ import 'package:dde_gesture_manager/themes/dark.dart'; import 'package:dde_gesture_manager/themes/light.dart'; import 'package:dde_gesture_manager/utils/helper.dart'; import 'package:dde_gesture_manager/utils/init.dart'; +import 'package:dde_gesture_manager/utils/simple_throttle.dart'; import 'package:flutter/material.dart'; import 'pages/home.dart'; @@ -71,15 +72,7 @@ class MyApp extends StatelessWidget { firstChild: Builder(builder: (context) { Future.microtask(() { initEvents(context); - if (H().lastCheckAuthStatusTime != null && - H().lastCheckAuthStatusTime!.difference(DateTime.now()) < Duration(minutes: 10)) return; - if (context.read().accessToken.notNull) { - Api.checkAuthStatus().then((value) { - if (!value) context.read().setProps(email: '', accessToken: ''); - }); - } else { - H().lastCheckAuthStatusTime = DateTime.now(); - } + SimpleThrottle.throttledFunc(_checkAuthStatus, timeout: const Duration(minutes: 5))?.call(context); }); return Container(); }), @@ -91,3 +84,15 @@ class MyApp extends StatelessWidget { ); } } + +void _checkAuthStatus(BuildContext context) { + if (H().lastCheckAuthStatusTime != null && + H().lastCheckAuthStatusTime!.difference(DateTime.now()) < Duration(minutes: 10)) return; + if (context.read().accessToken.notNull) { + Api.checkAuthStatus().then((value) { + if (!value) context.read().setProps(email: '', accessToken: ''); + }); + } else { + H().lastCheckAuthStatusTime = DateTime.now(); + } +} diff --git a/app/lib/models/content_layout.dart b/app/lib/models/content_layout.dart index fed062f..57325ef 100644 --- a/app/lib/models/content_layout.dart +++ b/app/lib/models/content_layout.dart @@ -1,4 +1,7 @@ import 'package:dde_gesture_manager/builder/provider_annotation.dart'; +import 'package:dde_gesture_manager/constants/sp_keys.dart'; +import 'package:dde_gesture_manager/utils/helper.dart'; +import 'package:dde_gesture_manager/extensions.dart'; @ProviderModel() class ContentLayout { @@ -9,7 +12,7 @@ class ContentLayout { bool? marketOrMeOpened; @ProviderModelProp() - bool? currentIsMarket = true; + bool? currentIsMarket = H().sp.getString(SPKeys.accessToken).isNull; bool get isMarket => currentIsMarket ?? true; } diff --git a/app/lib/pages/local_manager.dart b/app/lib/pages/local_manager.dart index 14276bd..514a168 100644 --- a/app/lib/pages/local_manager.dart +++ b/app/lib/pages/local_manager.dart @@ -184,8 +184,8 @@ class _LocalManagerState extends State { ), ), ), - Container(height: 5), - Container( + Padding( + padding: const EdgeInsets.only(top: 5), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ diff --git a/app/lib/pages/market_or_me.dart b/app/lib/pages/market_or_me.dart index 00e1a00..af2bf2a 100644 --- a/app/lib/pages/market_or_me.dart +++ b/app/lib/pages/market_or_me.dart @@ -82,7 +82,7 @@ class MarketOrMe extends StatelessWidget { Widget buildMeContent(BuildContext context) { var accessToken = context.watch().accessToken; if (accessToken.isNull) return LoginWidget(); - return MeWidget(); + return Expanded(child: MeWidget()); } Widget buildMarketContent(BuildContext context) { diff --git a/app/lib/utils/init_linux.dart b/app/lib/utils/init_linux.dart index d650cce..b69cbf0 100644 --- a/app/lib/utils/init_linux.dart +++ b/app/lib/utils/init_linux.dart @@ -49,9 +49,9 @@ Future initEvents(BuildContext context) async { } } - if (!_updateChecked) + if (!_updateChecked) { + _updateChecked = true; Api.checkAppVersion(ignoreErrorHandle: true).then((value) async { - _updateChecked = true; var info = await PackageInfo.fromPlatform(); var _buildNumber = int.parse(info.buildNumber); var _newVersionCode = value?.versionCode ?? 0; @@ -74,6 +74,7 @@ Future initEvents(BuildContext context) async { }); } }); + } } Future initConfigs() async { diff --git a/app/lib/utils/simple_throttle.dart b/app/lib/utils/simple_throttle.dart new file mode 100644 index 0000000..7e22507 --- /dev/null +++ b/app/lib/utils/simple_throttle.dart @@ -0,0 +1,45 @@ +import 'dart:collection'; + +import 'package:collection/collection.dart'; + +class _SimpleThrottleNode { + int funcHashCode; + int timestamp; + + _SimpleThrottleNode(this.funcHashCode, this.timestamp); +} + +typedef void _VoidFunc(); + +final _simpleThrottleQueue = Queue(); + +/// Usage: If you have a function : test(int n) => n; +/// you can use SimpleThrottle.throttledFunc(test)?.call(1) to make it throttled +/// this will return function's return value if last call time over the timeout +/// otherwise this will return null. +/// If your function is a 'void function()', you can use SimpleThrottle.invoke(func) to call it throttled, +/// and you can get a throttled function by SimpleThrottle.bind(func) if you do not call it immediately. +class SimpleThrottle { + static T? throttledFunc(T func, + {String? funcKey, Duration timeout = const Duration(seconds: 1)}) { + var node = _simpleThrottleQueue.firstWhereOrNull((element) => element.funcHashCode == (funcKey ?? func).hashCode); + if (node != null) { + if (DateTime.now().millisecondsSinceEpoch - node.timestamp < timeout.inMilliseconds) + return null; + else + node.timestamp = DateTime.now().millisecondsSinceEpoch; + } else { + _simpleThrottleQueue.add(_SimpleThrottleNode((funcKey ?? func).hashCode, DateTime.now().millisecondsSinceEpoch)); + while (_simpleThrottleQueue.length > 16) { + _simpleThrottleQueue.removeFirst(); + } + } + return func; + } + + static void invoke(_VoidFunc func, {String? funcKey, Duration timeout = const Duration(seconds: 1)}) => + throttledFunc(func, timeout: timeout, funcKey: funcKey)?.call(); + + static _VoidFunc bind(_VoidFunc func, {String? funcKey, Duration timeout = const Duration(seconds: 1)}) => + () => invoke(func, timeout: timeout, funcKey: funcKey); +} diff --git a/app/lib/widgets/dde_button.dart b/app/lib/widgets/dde_button.dart index 7c726e7..f0bac0a 100644 --- a/app/lib/widgets/dde_button.dart +++ b/app/lib/widgets/dde_button.dart @@ -141,6 +141,57 @@ class DButton extends StatefulWidget { message: LocaleKeys.operation_upload.tr(), )); + factory DButton.download({ + Key? key, + required enabled, + GestureTapCallback? onTap, + height = defaultButtonHeight * .7, + width = defaultButtonHeight * .7, + }) => + DButton( + key: key, + width: width, + height: height, + onTap: enabled ? onTap : null, + child: Tooltip( + child: Opacity(opacity: enabled ? 1 : 0.4, child: const Icon(Icons.file_download, size: 18)), + message: LocaleKeys.operation_download.tr(), + )); + + factory DButton.share({ + Key? key, + required enabled, + GestureTapCallback? onTap, + height = defaultButtonHeight * .7, + width = defaultButtonHeight * .7, + }) => + DButton( + key: key, + width: width, + height: height, + onTap: enabled ? onTap : null, + child: Tooltip( + child: Opacity(opacity: enabled ? 1 : 0.4, child: const Icon(Icons.share, size: 18)), + message: LocaleKeys.operation_share.tr(), + )); + + factory DButton.like({ + Key? key, + required enabled, + GestureTapCallback? onTap, + height = defaultButtonHeight * .7, + width = defaultButtonHeight * .7, + }) => + DButton( + key: key, + width: width, + height: height, + onTap: enabled ? onTap : null, + child: Tooltip( + child: Opacity(opacity: enabled ? 1 : 0.4, child: const Icon(Icons.thumb_up, size: 16)), + message: LocaleKeys.operation_like.tr(), + )); + factory DButton.dropdown({ Key? key, width = 60.0, diff --git a/app/lib/widgets/me.dart b/app/lib/widgets/me.dart index 3cd6663..7e3d71b 100644 --- a/app/lib/widgets/me.dart +++ b/app/lib/widgets/me.dart @@ -1,13 +1,23 @@ import 'package:auto_size_text/auto_size_text.dart'; +import 'package:collection/collection.dart'; import 'package:dde_gesture_manager/constants/constants.dart'; +import 'package:dde_gesture_manager/extensions.dart'; import 'package:dde_gesture_manager/http/api.dart'; import 'package:dde_gesture_manager/models/configs.provider.dart'; +import 'package:dde_gesture_manager/models/settings.provider.dart'; +import 'package:dde_gesture_manager/utils/notificator.dart'; import 'package:dde_gesture_manager_api/models.dart'; import 'package:flutter/material.dart'; -import 'package:dde_gesture_manager/extensions.dart'; +import 'package:flutter_platform_alert/flutter_platform_alert.dart'; import 'dde_button.dart'; +enum SchemeListType { + uploaded, + downloaded, + liked, +} + class MeWidget extends StatefulWidget { const MeWidget({Key? key}) : super(key: key); @@ -16,23 +26,46 @@ class MeWidget extends StatefulWidget { } class _MeWidgetState extends State { - List uploads = []; + List _schemes = []; + SchemeListType _type = SchemeListType.uploaded; + String? _selected; + String? _hovering; @override void initState() { super.initState(); - Api.userUploads().then((value) { + Api.userSchemes(type: _type).then((value) { if (mounted) setState(() { - uploads = value; + _schemes = value; + _selected = value.isNotEmpty ? value.first.uuid : null; }); }); } + Color _getItemBackgroundColor(int index, String? schemeId) { + Color _color = index % 2 == 0 ? context.t.scaffoldBackgroundColor : context.t.backgroundColor; + if (schemeId == _hovering) _color = context.t.dialogBackgroundColor; + if (schemeId == _selected) _color = context.read().currentActiveColor; + return _color; + } + + _refreshList() { + Future.delayed(const Duration(milliseconds: 100), () { + Api.userSchemes(type: _type).then((value) { + if (mounted) + setState(() { + _schemes = value; + }); + }); + }); + } + @override Widget build(BuildContext context) { + var currentSelectedScheme = _schemes.firstWhereOrNull((e) => e.uuid == _selected); return Padding( - padding: EdgeInsets.symmetric(vertical: 10), + padding: EdgeInsets.only(top: 10), child: Column( children: [ Row( @@ -54,12 +87,157 @@ class _MeWidgetState extends State { ), ], ), - Text('我的上传'), - Container( - height: 400, - child: ListView.builder( - itemBuilder: (context, index) => Text(uploads[index].name ?? ''), - itemCount: uploads.length, + Padding( + padding: const EdgeInsets.only(top: 3, bottom: 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SchemeListType.uploaded, + SchemeListType.downloaded, + SchemeListType.liked, + ] + .map( + (e) => Flexible( + flex: 1, + fit: FlexFit.tight, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + setState(() { + _type = e; + }); + Api.userSchemes(type: e).then((value) { + if (mounted) + setState(() { + _schemes = value; + }); + }); + }, + child: Center( + child: Text( + '${LocaleKeys.me_scheme_types}.${e.name}'.tr(), + style: _type == e ? TextStyle(fontWeight: FontWeight.bold, fontSize: 15) : null, + ), + ), + ), + ), + ), + ) + .toList(), + ), + ), + Expanded( + child: Container( + decoration: BoxDecoration( + border: Border.all( + width: .3, + color: context.t.dividerColor, + ), + borderRadius: BorderRadius.circular(defaultBorderRadius), + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 1, vertical: 2), + child: ListView.builder( + itemBuilder: (context, index) => GestureDetector( + onTap: () { + setState(() { + _selected = _schemes[index].uuid; + }); + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) { + setState(() { + _hovering = _schemes[index].uuid; + }); + }, + child: Container( + color: _getItemBackgroundColor(index, _schemes[index].uuid), + child: Padding( + padding: const EdgeInsets.only(left: 6, right: 12.0), + child: DefaultTextStyle( + style: context.t.textTheme.bodyText2!.copyWith( + color: _selected == _schemes[index].uuid ? Colors.white : null, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(_schemes[index].name ?? ''), + Row( + children: [ + Text('${_schemes[index].downloads ?? 0}'.padLeft(4)), + Icon( + Icons.file_download, + size: 18, + ), + Text('${_schemes[index].likes ?? 0}'.padLeft(4)), + Icon(_schemes[index].liked == true ? Icons.thumb_up : Icons.thumb_up_off_alt, + size: 17), + ] + .map((e) => Padding( + padding: const EdgeInsets.only(right: 3), + child: e, + )) + .toList(), + ), + ], + ), + ), + ), + ), + ), + ), + itemCount: _schemes.length, + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (_type == SchemeListType.uploaded) + DButton.share( + enabled: currentSelectedScheme?.shared == false, + onTap: () { + Notificator.showConfirm( + title: LocaleKeys.info_share_title.tr(), + description: LocaleKeys.info_share_description.tr()) + .then((value) { + if (value == CustomButton.positiveButton) { + Notificator.success(context, title: LocaleKeys.info_share_success.tr()); + } + }); + }, + ), + DButton.like( + enabled: true, + onTap: () { + Api.likeScheme(schemeId: currentSelectedScheme!.uuid!, isLike: !currentSelectedScheme.liked!) + .then((value) { + if (value) { + _refreshList(); + } + }); + }, + ), + DButton.download( + enabled: true, + onTap: () { + Api.downloadScheme(schemeId: currentSelectedScheme!.uuid!).then((value) { + value.sout(); + _refreshList(); + }); + }, + ), + ] + .map((e) => Padding( + padding: const EdgeInsets.only(right: 4), + child: e, + )) + .toList(), ), ), ], diff --git a/app/pubspec.yaml b/app/pubspec.yaml index b774f56..4176979 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -57,6 +57,10 @@ dev_dependencies: build_runner: 2.1.2 source_gen: 1.1.0 +dependency_overrides: + angel3_orm: + path: ../api/3rd_party/angel3_orm + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/app/resources/langs/en.json b/app/resources/langs/en.json index 5fbc48c..e6ae3be 100644 --- a/app/resources/langs/en.json +++ b/app/resources/langs/en.json @@ -72,7 +72,10 @@ "apply": "apply", "paste": "paste", "logout": "sign out", - "upload": "upload" + "upload": "upload", + "download": "download", + "share": "share to market", + "like": "like" }, "str": { "null": "Null", @@ -146,6 +149,11 @@ "upload": { "success": "Upload success ~", "failed": "Upload failed.." + }, + "share": { + "title": "Are you sure to sharing?", + "description": "Other users can see this scheme and download it after share", + "success": "Share success" } }, "me": { @@ -155,6 +163,11 @@ "email_hint": "Please enter email", "password_hint": "Please enter 8-16-bit password", "email_error_hint": "Please enter your vaild email" + }, + "scheme_types": { + "uploaded": "Uploaded", + "downloaded": "Downloaded", + "liked": "Liked" } } } \ No newline at end of file diff --git a/app/resources/langs/zh-CN.json b/app/resources/langs/zh-CN.json index d613146..467bd79 100644 --- a/app/resources/langs/zh-CN.json +++ b/app/resources/langs/zh-CN.json @@ -72,7 +72,10 @@ "apply": "应用", "paste": "粘贴", "logout": "退出登录", - "upload": "上传" + "upload": "上传", + "download": "下载", + "share": "分享到市场", + "like": "点赞" }, "str": { "null": "无", @@ -146,6 +149,11 @@ "upload": { "success": "上传成功~", "failed": "上传失败。。" + }, + "share": { + "title": "确定分享?", + "description": "分享后其他用户可以看到本方案并下载使用", + "success": "分享成功" } }, "me": { @@ -155,6 +163,11 @@ "email_hint": "请输入邮箱", "password_hint": "请输入8-16位密码", "email_error_hint": "请输入正确的邮箱" + }, + "scheme_types": { + "uploaded": "我的上传", + "downloaded": "我的下载", + "liked": "我的点赞" } } } \ No newline at end of file From b7d0ec75eb1261acf95ca4954d03786a98bdcd47 Mon Sep 17 00:00:00 2001 From: debuggerx Date: Mon, 10 Jan 2022 18:01:50 +0800 Subject: [PATCH 4/6] wip: upgrade dependencies; fix type error of http error handler --- api/.gitignore | 2 +- api/3rd_party/angel3_orm/AUTHORS.md | 12 - api/3rd_party/angel3_orm/CHANGELOG.md | 230 ------- api/3rd_party/angel3_orm/LICENSE | 30 - api/3rd_party/angel3_orm/README.md | 15 - api/3rd_party/angel3_orm/analysis_options.yaml | 1 - api/3rd_party/angel3_orm/lib/angel3_orm.dart | 15 - api/3rd_party/angel3_orm/lib/src/annotations.dart | 31 - api/3rd_party/angel3_orm/lib/src/builder.dart | 675 --------------------- api/3rd_party/angel3_orm/lib/src/join_builder.dart | 74 --- api/3rd_party/angel3_orm/lib/src/join_on.dart | 8 - .../angel3_orm/lib/src/map_query_values.dart | 9 - api/3rd_party/angel3_orm/lib/src/migration.dart | 135 ----- api/3rd_party/angel3_orm/lib/src/order_by.dart | 8 - api/3rd_party/angel3_orm/lib/src/query.dart | 425 ------------- api/3rd_party/angel3_orm/lib/src/query_base.dart | 81 --- .../angel3_orm/lib/src/query_executor.dart | 23 - api/3rd_party/angel3_orm/lib/src/query_values.dart | 89 --- api/3rd_party/angel3_orm/lib/src/query_where.dart | 69 --- api/3rd_party/angel3_orm/lib/src/relations.dart | 91 --- api/3rd_party/angel3_orm/lib/src/union.dart | 38 -- api/3rd_party/angel3_orm/lib/src/util.dart | 3 - api/3rd_party/angel3_orm/mono_pkg.yaml | 0 api/3rd_party/angel3_orm/pubspec.yaml | 21 - api/pubspec.yaml | 20 +- app/lib/http/api.dart | 17 +- app/lib/widgets/me.dart | 7 +- app/pubspec.yaml | 3 - 28 files changed, 20 insertions(+), 2112 deletions(-) delete mode 100644 api/3rd_party/angel3_orm/AUTHORS.md delete mode 100644 api/3rd_party/angel3_orm/CHANGELOG.md delete mode 100644 api/3rd_party/angel3_orm/LICENSE delete mode 100644 api/3rd_party/angel3_orm/README.md delete mode 100644 api/3rd_party/angel3_orm/analysis_options.yaml delete mode 100644 api/3rd_party/angel3_orm/lib/angel3_orm.dart delete mode 100644 api/3rd_party/angel3_orm/lib/src/annotations.dart delete mode 100644 api/3rd_party/angel3_orm/lib/src/builder.dart delete mode 100644 api/3rd_party/angel3_orm/lib/src/join_builder.dart delete mode 100644 api/3rd_party/angel3_orm/lib/src/join_on.dart delete mode 100644 api/3rd_party/angel3_orm/lib/src/map_query_values.dart delete mode 100644 api/3rd_party/angel3_orm/lib/src/migration.dart delete mode 100644 api/3rd_party/angel3_orm/lib/src/order_by.dart delete mode 100644 api/3rd_party/angel3_orm/lib/src/query.dart delete mode 100644 api/3rd_party/angel3_orm/lib/src/query_base.dart delete mode 100644 api/3rd_party/angel3_orm/lib/src/query_executor.dart delete mode 100644 api/3rd_party/angel3_orm/lib/src/query_values.dart delete mode 100644 api/3rd_party/angel3_orm/lib/src/query_where.dart delete mode 100644 api/3rd_party/angel3_orm/lib/src/relations.dart delete mode 100644 api/3rd_party/angel3_orm/lib/src/union.dart delete mode 100644 api/3rd_party/angel3_orm/lib/src/util.dart delete mode 100644 api/3rd_party/angel3_orm/mono_pkg.yaml delete mode 100644 api/3rd_party/angel3_orm/pubspec.yaml diff --git a/api/.gitignore b/api/.gitignore index 342a80c..2bb6e81 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -91,7 +91,7 @@ server_log.txt .metals/ -/config/production.yaml +/config/production.yaml.bak /config/development.yaml *.g.dart diff --git a/api/3rd_party/angel3_orm/AUTHORS.md b/api/3rd_party/angel3_orm/AUTHORS.md deleted file mode 100644 index ac95ab5..0000000 --- a/api/3rd_party/angel3_orm/AUTHORS.md +++ /dev/null @@ -1,12 +0,0 @@ -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/3rd_party/angel3_orm/CHANGELOG.md b/api/3rd_party/angel3_orm/CHANGELOG.md deleted file mode 100644 index 4dbc079..0000000 --- a/api/3rd_party/angel3_orm/CHANGELOG.md +++ /dev/null @@ -1,230 +0,0 @@ -# Change Log - -## 4.0.4 - -* Changed default varchar size to 255 -* Changed default primary key to serial - -## 4.0.3 - -* Removed debugging messages - -## 4.0.2 - -* Updated linter to `package:lints` -* Set `createdAt` and `updatedAt` to current datetime as default - -## 4.0.1 - -* Fixed expressions parsing error -* Fixed json data type error -* Added debug logging to sql query execution - -## 4.0.0 - -* Updated `Optional` package - -## 4.0.0-beta.4 - -* Added `hasSize` to `ColumnType` - -## 4.0.0-beta.3 - -* Updated README -* Fixed NNBD issues - -## 4.0.0-beta.2 - -* Fixed static analysis warning - -## 4.0.0-beta.1 - -* Migrated to support Dart SDK 2.12.x NNBD - -## 3.0.0 - -* Migrated to work with Dart SDK 2.12.x Non NNBD - -## 2.1.0-beta.3 - -* Remove parentheses from `AS` when renaming raw `expressions`. - -## 2.1.0-beta.2 - -* Add `expressions` to `Query`, to support custom SQL expressions that are -read as normal fields. - -## 2.1.0-beta.1 - -* Calls to `leftJoin`, etc. alias all fields in a child query, to prevent -`ambiguous column a0.id` errors. - -## 2.1.0-beta - -* Split the formerly 600+ line `src/query.dart` up into -separate files. -* **BREAKING**: Add a required `QueryExecutor` argument to `transaction` -callbacks. -* Make `JoinBuilder` take `to` as a `String Function()`. This will allow -ORM queries to reference their joined subqueries. -* Removed deprecated `Join`, `toSql`, `sanitizeExpression`, `isAscii`. -* Always put `ORDER BY` before `LIMIT`. -* `and`, `or`, `not` in `QueryWhere` include parentheses. -* Add `joinType` to `Relationship` class. - -## 2.0.2 - -* Place `LIMIT` and `OFFSET` after `ORDER BY`. - -## 2.0.1 - -* Apply `package:pedantic` fixes. -* `@PrimaryKey()` no longer defaults to `serial`, allowing its type to be -inferenced. - -## 2.0.0 - -* Add `isNull`, `isNotNull` getters to builders. - -## 2.0.0-dev.24 - -* Fix a bug that caused syntax errors on `ORDER BY`. -* Add `pattern` to `like` on string builder. `sanitize` is optional. -* Add `RawSql`. - -## 2.0.0-dev.23 - -* Add `@ManyToMany` annotation, which builds many-to-many relations. - -## 2.0.0-dev.22 - -* `compileInsert` will explicitly never emit a key not belonging to the -associated query. - -## 2.0.0-dev.21 - -* Add tableName to query - -## 2.0.0-dev.20 - -* Join updates. - -## 2.0.0-dev.19 - -* Implement cast-based `double` support. -* Finish `ListSqlExpressionBuilder`. - -## 2.0.0-dev.18 - -* Add `ListSqlExpressionBuilder` (still in development). - -## 2.0.0-dev.17 - -* Add `EnumSqlExpressionBuilder`. - -## 2.0.0-dev.16 - -* Add `MapSqlExpressionBuilder` for JSON/JSONB support. - -## 2.0.0-dev.15 - -* Remove `Column.defaultValue`. -* Deprecate `toSql` and `sanitizeExpression`. -* Refactor builders so that strings are passed through - -## 2.0.0-dev.14 - -* Remove obsolete `@belongsToMany`. - -## 2.0.0-dev.13 - -* Push for consistency with orm_gen @ `2.0.0-dev`. - -## 2.0.0-dev.12 - -* Always apply `toSql` escapes. - -## 2.0.0-dev.11 - -* Remove `limit(1)` except on `getOne` - -## 2.0.0-dev.10 - -* Add `withFields` to `compile()` - -## 2.0.0-dev.9 - -* Permanent preamble fix - -## 2.0.0-dev.8 - -* Escapes - -## 2.0.0-dev.7 - -* Update `toSql` -* Add `isTrue` and `isFalse` - -## 2.0.0-dev.6 - -* Add `delete`, `insert` and `update` methods to `Query`. - -## 2.0.0-dev.4 - -* Add more querying methods. -* Add preamble to `Query.compile`. - -## 2.0.0-dev.3 - -* Brought back old-style query builder. -* Strong-mode updates, revised `Join`. - -## 2.0.0-dev.2 - -* Renamed `ORM` to `Orm`. -* `Orm` now requires a database type. - -## 2.0.0-dev.1 - -* Restored all old PostgreSQL-specific annotations. Rather than a smart runtime, -having a codegen capable of building ORM's for multiple databases can potentially -provide a very fast ORM for everyone. - -## 2.0.0-dev - -* Removed PostgreSQL-specific functionality, so that the ORM can ultimately -target all services. -* Created a better `Join` model. -* Created a far better `Query` model. -* Removed `lib/server.dart` - -## 1.0.0-alpha+10 - -* Split into `angel_orm.dart` and `server.dart`. Prevents DDC failures. - -## 1.0.0-alpha+7 - -* Added a `@belongsToMany` annotation class. -* Resolved [##20](https://github.com/angel-dart/orm/issues/20). The -`PostgreSQLConnectionPool` keeps track of which connections have been opened now. - -## 1.0.0-alpha+6 - -* `DateTimeSqlExpressionBuilder` will no longer automatically -insert quotation marks around names. - -## 1.0.0-alpha+5 - -* Corrected a typo that was causing the aforementioned test failures. -`==` becomes `=`. - -## 1.0.0-alpha+4 - -* Added a null-check in `lib/src/query.dart##L24` to (hopefully) prevent it from -crashing on Travis. - -## 1.0.0-alpha+3 - -* Added `isIn`, `isNotIn`, `isBetween`, `isNotBetween` to `SqlExpressionBuilder` and its -subclasses. -* Added a dependency on `package:meta`. diff --git a/api/3rd_party/angel3_orm/LICENSE b/api/3rd_party/angel3_orm/LICENSE deleted file mode 100644 index a81d1a8..0000000 --- a/api/3rd_party/angel3_orm/LICENSE +++ /dev/null @@ -1,30 +0,0 @@ - -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/3rd_party/angel3_orm/README.md b/api/3rd_party/angel3_orm/README.md deleted file mode 100644 index 43f12ff..0000000 --- a/api/3rd_party/angel3_orm/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Angel3 ORM - -![Pub Version (including pre-releases)](https://img.shields.io/pub/v/angel3_orm?include_prereleases) -[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety) -[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion) -[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/master/packages/orm/angel_orm/LICENSE) - -Runtime support for Angel3 ORM. Includes a clean, database-agnostic query builder and relationship/join support. - -## Supported database - -* PostgreSQL version 10, 11, 12, 13 and 14 -* MySQL 8.0 or later - -For documentation about the ORM, see [Developer Guide](https://angel3-docs.dukefirehawk.com/guides/orm) diff --git a/api/3rd_party/angel3_orm/analysis_options.yaml b/api/3rd_party/angel3_orm/analysis_options.yaml deleted file mode 100644 index ea2c9e9..0000000 --- a/api/3rd_party/angel3_orm/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: package:lints/recommended.yaml \ No newline at end of file diff --git a/api/3rd_party/angel3_orm/lib/angel3_orm.dart b/api/3rd_party/angel3_orm/lib/angel3_orm.dart deleted file mode 100644 index 9195227..0000000 --- a/api/3rd_party/angel3_orm/lib/angel3_orm.dart +++ /dev/null @@ -1,15 +0,0 @@ -export 'src/annotations.dart'; -export 'src/builder.dart'; -export 'src/join_builder.dart'; -export 'src/join_on.dart'; -export 'src/map_query_values.dart'; -export 'src/migration.dart'; -export 'src/order_by.dart'; -export 'src/query_base.dart'; -export 'src/query_executor.dart'; -export 'src/query_values.dart'; -export 'src/query_where.dart'; -export 'src/query.dart'; -export 'src/relations.dart'; -export 'src/union.dart'; -export 'src/util.dart'; diff --git a/api/3rd_party/angel3_orm/lib/src/annotations.dart b/api/3rd_party/angel3_orm/lib/src/annotations.dart deleted file mode 100644 index 891cddc..0000000 --- a/api/3rd_party/angel3_orm/lib/src/annotations.dart +++ /dev/null @@ -1,31 +0,0 @@ -/// A raw SQL statement that specifies a date/time default to the -/// current time. -const RawSql currentTimestamp = RawSql('CURRENT_TIMESTAMP'); - -/// Can passed to a [MigrationColumn] to default to a raw SQL expression. -class RawSql { - /// The raw SQL text. - final String value; - - const RawSql(this.value); -} - -/// Canonical instance of [ORM]. Implies all defaults. -const Orm orm = Orm(); - -class Orm { - /// The name of the table to query. - /// - /// Inferred if not present. - final String? tableName; - - /// Whether to generate migrations for this model. - /// - /// Defaults to [:true:]. - final bool generateMigrations; - - const Orm({this.tableName, this.generateMigrations = true}); -} - -/// The various types of join. -enum JoinType { inner, left, right, full, self } diff --git a/api/3rd_party/angel3_orm/lib/src/builder.dart b/api/3rd_party/angel3_orm/lib/src/builder.dart deleted file mode 100644 index 313d125..0000000 --- a/api/3rd_party/angel3_orm/lib/src/builder.dart +++ /dev/null @@ -1,675 +0,0 @@ -import 'package:intl/intl.dart' show DateFormat; -import 'query.dart'; - -final DateFormat dateYmd = DateFormat('yyyy-MM-dd'); -final DateFormat dateYmdHms = DateFormat('yyyy-MM-dd HH:mm:ss'); - -abstract class SqlExpressionBuilder { - final Query query; - final String columnName; - String? _cast; - bool _isProperty = false; - String? _substitution; - - SqlExpressionBuilder(this.query, this.columnName); - - String get substitution { - var c = _isProperty ? 'prop' : columnName; - return _substitution ??= query.reserveName(c); - } - - bool get hasValue; - - String? compile(); -} - -class NumericSqlExpressionBuilder - extends SqlExpressionBuilder { - bool _hasValue = false; - String _op = '='; - String? _raw; - T? _value; - - NumericSqlExpressionBuilder(Query query, String columnName) - : super(query, columnName); - - @override - bool get hasValue => _hasValue; - - bool _change(String op, T value) { - _raw = null; - _op = op; - _value = value; - return _hasValue = true; - } - - @override - String? compile() { - if (_raw != null) return _raw; - if (_value == null) return null; - var v = _value.toString(); - if (T == double) v = 'CAST ("$v" as decimal)'; - if (_cast != null) v = 'CAST ($v AS $_cast)'; - return '$_op $v'; - } - - bool operator <(T value) => _change('<', value); - - bool operator >(T value) => _change('>', value); - - bool operator <=(T value) => _change('<=', value); - - bool operator >=(T value) => _change('>=', value); - - void get isNull { - _raw = 'IS NULL'; - _hasValue = true; - } - - void get isNotNull { - _raw = 'IS NOT NULL'; - _hasValue = true; - } - - void lessThan(T value) { - _change('<', value); - } - - void lessThanOrEqualTo(T value) { - _change('<=', value); - } - - void greaterThan(T value) { - _change('>', value); - } - - void greaterThanOrEqualTo(T value) { - _change('>=', value); - } - - void equals(T value) { - _change('=', value); - } - - void notEquals(T value) { - _change('!=', value); - } - - void isBetween(T lower, T upper) { - _raw = 'BETWEEN $lower AND $upper'; - _hasValue = true; - } - - void isNotBetween(T lower, T upper) { - _raw = 'NOT BETWEEN $lower AND $upper'; - _hasValue = true; - } - - void isIn(Iterable values) { - _raw = 'IN (' + values.join(', ') + ')'; - _hasValue = true; - } - - void isNotIn(Iterable values) { - _raw = 'NOT IN (' + values.join(', ') + ')'; - _hasValue = true; - } -} - -class EnumSqlExpressionBuilder extends SqlExpressionBuilder { - final int Function(T) _getValue; - bool _hasValue = false; - String _op = '='; - String? _raw; - int? _value; - - EnumSqlExpressionBuilder(Query query, String columnName, this._getValue) - : super(query, columnName); - - @override - bool get hasValue => _hasValue; - - bool _change(String op, T value) { - _raw = null; - _op = op; - _value = _getValue(value); - return _hasValue = true; - } - - UnsupportedError _unsupported() => - UnsupportedError('Enums do not support this operation.'); - - @override - String compile() { - if (_raw != null) { - return _raw!; - } - if (_value == null) { - return ''; - } - return '$_op $_value'; - } - - void get isNull { - _raw = 'IS NULL'; - _hasValue = true; - } - - void get isNotNull { - _raw = 'IS NOT NULL'; - _hasValue = true; - } - - void equals(T value) { - _change('=', value); - } - - void notEquals(T value) { - _change('!=', value); - } - - void isBetween(T lower, T upper) => throw _unsupported(); - - void isNotBetween(T lower, T upper) => throw _unsupported(); - - void isIn(Iterable values) { - _raw = 'IN (' + values.map(_getValue).join(', ') + ')'; - _hasValue = true; - } - - void isNotIn(Iterable values) { - _raw = 'NOT IN (' + values.map(_getValue).join(', ') + ')'; - _hasValue = true; - } -} - -class StringSqlExpressionBuilder extends SqlExpressionBuilder { - bool _hasValue = false; - String? _op = '=', _raw, _value; - - StringSqlExpressionBuilder(Query query, String columnName) - : super(query, columnName); - - @override - bool get hasValue => _hasValue; - - String get lowerName => '${substitution}_lower'; - - String get upperName => '${substitution}_upper'; - - bool _change(String op, String value) { - _raw = null; - _op = op; - _value = value; - query.substitutionValues[substitution] = _value; - return _hasValue = true; - } - - @override - String? compile() { - if (_raw != null) return _raw; - if (_value == null) return null; - return '$_op @$substitution'; - } - - void isEmpty() => equals(''); - - void equals(String value) { - _change('=', value); - } - - void notEquals(String value) { - _change('!=', value); - } - - /// Builds a `LIKE` predicate. - /// - /// To prevent injections, an optional [sanitizer] is called with a name that - /// will be escaped by the underlying [QueryExecutor]. Use this if the [pattern] - /// is not constant, and/or involves user input. - /// - /// Otherwise, you can omit [sanitizer]. - /// - /// Example: - /// ```dart - /// carNameBuilder.like('%Mazda%'); - /// carNameBuilder.like((name) => 'Mazda %$name%'); - /// ``` - void like(String pattern, {String Function(String)? sanitize}) { - sanitize ??= (s) => pattern; - _raw = 'LIKE \'' + sanitize('@$substitution') + '\''; - query.substitutionValues[substitution] = pattern; - _hasValue = true; - _value = null; - } - - void isBetween(String lower, String upper) { - query.substitutionValues[lowerName] = lower; - query.substitutionValues[upperName] = upper; - _raw = 'BETWEEN @$lowerName AND @$upperName'; - _hasValue = true; - } - - void isNotBetween(String lower, String upper) { - query.substitutionValues[lowerName] = lower; - query.substitutionValues[upperName] = upper; - _raw = 'NOT BETWEEN @$lowerName AND @$upperName'; - _hasValue = true; - } - - void get isNull { - _raw = 'IS NULL'; - _hasValue = true; - } - - void get isNotNull { - _raw = 'IS NOT NULL'; - _hasValue = true; - } - - String _in(Iterable values) { - return 'IN (' + - values.map((v) { - var name = query.reserveName('${columnName}_in_value'); - query.substitutionValues[name] = v; - return '@$name'; - }).join(', ') + - ')'; - } - - void isIn(Iterable values) { - _raw = _in(values); - _hasValue = true; - } - - void isNotIn(Iterable values) { - _raw = 'NOT ' + _in(values); - _hasValue = true; - } -} - -class BooleanSqlExpressionBuilder extends SqlExpressionBuilder { - bool _hasValue = false; - String? _op = '=', _raw; - bool? _value; - - BooleanSqlExpressionBuilder(Query query, String columnName) - : super(query, columnName); - - @override - bool get hasValue => _hasValue; - - bool _change(String op, bool value) { - _raw = null; - _op = op; - _value = value; - return _hasValue = true; - } - - @override - String? compile() { - if (_raw != null) return _raw; - if (_value == null) return null; - var v = _value! ? 'TRUE' : 'FALSE'; - if (_cast != null) v = 'CAST ($v AS $_cast)'; - return '$_op $v'; - } - - void get isTrue => equals(true); - - void get isFalse => equals(false); - - void get isNull { - _raw = 'IS NULL'; - _hasValue = true; - } - - void get isNotNull { - _raw = 'IS NOT NULL'; - _hasValue = true; - } - - void equals(bool value) { - _change('=', value); - } - - void notEquals(bool value) { - _change('!=', value); - } -} - -class DateTimeSqlExpressionBuilder extends SqlExpressionBuilder { - NumericSqlExpressionBuilder? _year, - _month, - _day, - _hour, - _minute, - _second; - - String? _raw; - - DateTimeSqlExpressionBuilder(Query query, String columnName) - : super(query, columnName); - - NumericSqlExpressionBuilder get year => - _year ??= NumericSqlExpressionBuilder(query, 'year'); - NumericSqlExpressionBuilder get month => - _month ??= NumericSqlExpressionBuilder(query, 'month'); - NumericSqlExpressionBuilder get day => - _day ??= NumericSqlExpressionBuilder(query, 'day'); - NumericSqlExpressionBuilder get hour => - _hour ??= NumericSqlExpressionBuilder(query, 'hour'); - NumericSqlExpressionBuilder get minute => - _minute ??= NumericSqlExpressionBuilder(query, 'minute'); - NumericSqlExpressionBuilder get second => - _second ??= NumericSqlExpressionBuilder(query, 'second'); - - @override - bool get hasValue => - _raw?.isNotEmpty == true || - _year?.hasValue == true || - _month?.hasValue == true || - _day?.hasValue == true || - _hour?.hasValue == true || - _minute?.hasValue == true || - _second?.hasValue == true; - - bool _change(String _op, DateTime dt, bool time) { - var dateString = time ? dateYmdHms.format(dt) : dateYmd.format(dt); - _raw = '$columnName $_op \'$dateString\''; - return true; - } - - bool operator <(DateTime value) => _change('<', value, true); - - bool operator <=(DateTime value) => _change('<=', value, true); - - bool operator >(DateTime value) => _change('>', value, true); - - bool operator >=(DateTime value) => _change('>=', value, true); - - void equals(DateTime value, {bool includeTime = true}) { - _change('=', value, includeTime != false); - } - - void lessThan(DateTime value, {bool includeTime = true}) { - _change('<', value, includeTime != false); - } - - void lessThanOrEqualTo(DateTime value, {bool includeTime = true}) { - _change('<=', value, includeTime != false); - } - - void greaterThan(DateTime value, {bool includeTime = true}) { - _change('>', value, includeTime != false); - } - - void greaterThanOrEqualTo(DateTime value, {bool includeTime = true}) { - _change('>=', value, includeTime != false); - } - - void isIn(Iterable values) { - _raw = '$columnName IN (' + - values.map(dateYmdHms.format).map((s) => '$s').join(', ') + - ')'; - } - - void isNotIn(Iterable values) { - _raw = '$columnName NOT IN (' + - values.map(dateYmdHms.format).map((s) => '$s').join(', ') + - ')'; - } - - void isBetween(DateTime lower, DateTime upper) { - var l = dateYmdHms.format(lower), u = dateYmdHms.format(upper); - _raw = "$columnName BETWEEN '$l' and '$u'"; - } - - void isNotBetween(DateTime lower, DateTime upper) { - var l = dateYmdHms.format(lower), u = dateYmdHms.format(upper); - _raw = "$columnName NOT BETWEEN '$l' and '$u'"; - } - - void get isNull { - _raw = '$columnName IS NULL'; - } - - void get isNotNull { - _raw = '$columnName IS NOT NULL'; - } - - @override - String? compile() { - if (_raw?.isNotEmpty == true) return _raw; - var parts = []; - if (year.hasValue == true) { - parts.add('YEAR($columnName) ${year.compile()}'); - } - if (month.hasValue == true) { - parts.add('MONTH($columnName) ${month.compile()}'); - } - if (day.hasValue == true) { - parts.add('DAY($columnName) ${day.compile()}'); - } - if (hour.hasValue == true) { - parts.add('HOUR($columnName) ${hour.compile()}'); - } - if (minute.hasValue == true) { - parts.add('MINUTE($columnName) ${minute.compile()}'); - } - if (second.hasValue == true) { - parts.add('SECOND($columnName) ${second.compile()}'); - } - - return parts.isEmpty ? null : parts.join(' AND '); - } -} - -abstract class JsonSqlExpressionBuilder extends SqlExpressionBuilder { - final List _properties = []; - bool _hasValue = false; - T? _value; - String? _op; - String? _raw; - - JsonSqlExpressionBuilder(Query query, String columnName) - : super(query, columnName); - - JsonSqlExpressionBuilderProperty operator [](K name) { - var p = _property(name); - _properties.add(p); - return p; - } - - JsonSqlExpressionBuilderProperty _property(K name); - - bool get hasRaw => _raw != null || _properties.any((p) => p.hasValue); - - @override - bool get hasValue => _hasValue || _properties.any((p) => p.hasValue); - - T? _encodeValue(T? v) => v; - - bool _change(String op, T value) { - _raw = null; - _op = op; - _value = value; - query.substitutionValues[substitution] = _encodeValue(_value); - return _hasValue = true; - } - - void get isNull { - _raw = 'IS NULL'; - _hasValue = true; - } - - void get isNotNull { - _raw = 'IS NOT NULL'; - _hasValue = true; - } - - @override - String compile() { - var s = _compile(); - if (!_properties.any((p) => p.hasValue)) return s; - //s ??= ''; - - for (var p in _properties) { - if (p.hasValue) { - var c = p.compile(); - - if (c != null) { - _hasValue = true; - //s ??= ''; - - if (p.typed is! DateTimeSqlExpressionBuilder) { - s += '${p.typed!.columnName} '; - } - - s += c; - } - } - } - - return s; - } - - String _compile() { - if (_raw != null) { - return _raw!; - } - if (_value == null) { - return ''; - } - return '::jsonb $_op @$substitution::jsonb'; - } - - void contains(T value) { - _change('@>', value); - } - - void equals(T value) { - _change('=', value); - } -} - -class MapSqlExpressionBuilder extends JsonSqlExpressionBuilder { - MapSqlExpressionBuilder(Query query, String columnName) - : super(query, columnName); - - @override - JsonSqlExpressionBuilderProperty _property(String name) { - return JsonSqlExpressionBuilderProperty(this, name, false); - } - - void containsKey(String key) { - this[key].isNotNull; - } - - void containsPair(key, value) { - contains({key: value}); - } -} - -class ListSqlExpressionBuilder extends JsonSqlExpressionBuilder { - ListSqlExpressionBuilder(Query query, String columnName) - : super(query, columnName); - - @override - List? _encodeValue(List? v) => v; //[json.encode(v)]; - - @override - JsonSqlExpressionBuilderProperty _property(int name) { - return JsonSqlExpressionBuilderProperty(this, name.toString(), true); - } -} - -class JsonSqlExpressionBuilderProperty { - final JsonSqlExpressionBuilder builder; - final String name; - final bool isInt; - SqlExpressionBuilder? _typed; - - JsonSqlExpressionBuilderProperty(this.builder, this.name, this.isInt); - - SqlExpressionBuilder? get typed => _typed; - - bool get hasValue => _typed?.hasValue == true; - - String? compile() => _typed?.compile(); - - T? _set(T Function() value) { - if (_typed is T) { - return _typed as T?; - } else if (_typed != null) { - throw StateError( - '$nameString is already typed as $_typed, and cannot be changed.'); - } else { - _typed = value() - ?.._cast = 'text' - .._isProperty = true; - return _typed as T?; - } - } - - String get nameString { - var n = isInt ? name : "'$name'"; - return '${builder.columnName}::jsonb->>$n'; - } - - void get isNotNull { - builder - .._hasValue = true - .._raw ??= ''; - - var r = builder._raw; - if (r != null) { - builder._raw = r + '$nameString IS NOT NULL'; - } else { - builder._raw = '$nameString IS NOT NULL'; - } - } - - void get isNull { - builder - .._hasValue = true - .._raw ??= ''; - - var r = builder._raw; - if (r != null) { - builder._raw = r + '$nameString IS NULL'; - } else { - builder._raw = '$nameString IS NULL'; - } - } - - StringSqlExpressionBuilder? get asString { - return _set(() => StringSqlExpressionBuilder(builder.query, nameString)); - } - - BooleanSqlExpressionBuilder? get asBool { - return _set(() => BooleanSqlExpressionBuilder(builder.query, nameString)); - } - - DateTimeSqlExpressionBuilder? get asDateTime { - return _set(() => DateTimeSqlExpressionBuilder(builder.query, nameString)); - } - - NumericSqlExpressionBuilder? get asDouble { - return _set( - () => NumericSqlExpressionBuilder(builder.query, nameString)); - } - - NumericSqlExpressionBuilder? get asInt { - return _set( - () => NumericSqlExpressionBuilder(builder.query, nameString)); - } - - MapSqlExpressionBuilder? get asMap { - return _set(() => MapSqlExpressionBuilder(builder.query, nameString)); - } - - ListSqlExpressionBuilder? get asList { - return _set(() => ListSqlExpressionBuilder(builder.query, nameString)); - } -} diff --git a/api/3rd_party/angel3_orm/lib/src/join_builder.dart b/api/3rd_party/angel3_orm/lib/src/join_builder.dart deleted file mode 100644 index c342c77..0000000 --- a/api/3rd_party/angel3_orm/lib/src/join_builder.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'annotations.dart'; -import 'query.dart'; - -/// Builds a SQL `JOIN` query. -class JoinBuilder { - final JoinType type; - final Query from; - final String? key, value, op, alias; - final bool aliasAllFields; - - /// A callback to produces the expression to join against, i.e. - /// a table name, or the result of compiling a query. - final String Function() to; - final List additionalFields; - - JoinBuilder(this.type, this.from, this.to, this.key, this.value, - {this.op = '=', - this.alias, - this.additionalFields = const [], - this.aliasAllFields = false}) { - //assert(to != null, - // 'computation of this join threw an error, and returned null.'); - } - - String get fieldName { - var v = value; - if (aliasAllFields) { - v = '${alias}_$v'; - } - var right = '${from.tableName}.$v'; - if (alias != null) right = '$alias.$v'; - return right; - } - - String nameFor(String name) { - if (aliasAllFields) name = '${alias}_$name'; - var right = '${from.tableName}.$name'; - if (alias != null) right = '$alias.$name'; - return right; - } - - String compile(Set? trampoline) { - var compiledTo = to(); - //if (compiledTo == null) return null; - if (compiledTo == '') { - return ''; - } - var b = StringBuffer(); - var left = '${from.tableName}.$key'; - var right = fieldName; - switch (type) { - case JoinType.inner: - b.write(' INNER JOIN'); - break; - case JoinType.left: - b.write(' LEFT JOIN'); - break; - case JoinType.right: - b.write(' RIGHT JOIN'); - break; - case JoinType.full: - b.write(' FULL OUTER JOIN'); - break; - case JoinType.self: - b.write(' SELF JOIN'); - break; - } - - b.write(' $compiledTo'); - if (alias != null) b.write(' $alias'); - b.write(' ON $left$op$right'); - return b.toString(); - } -} diff --git a/api/3rd_party/angel3_orm/lib/src/join_on.dart b/api/3rd_party/angel3_orm/lib/src/join_on.dart deleted file mode 100644 index 7bfed40..0000000 --- a/api/3rd_party/angel3_orm/lib/src/join_on.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'builder.dart'; - -class JoinOn { - final SqlExpressionBuilder key; - final SqlExpressionBuilder value; - - JoinOn(this.key, this.value); -} diff --git a/api/3rd_party/angel3_orm/lib/src/map_query_values.dart b/api/3rd_party/angel3_orm/lib/src/map_query_values.dart deleted file mode 100644 index 398ba9b..0000000 --- a/api/3rd_party/angel3_orm/lib/src/map_query_values.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'query_values.dart'; - -/// A [QueryValues] implementation that simply writes to a [Map]. -class MapQueryValues extends QueryValues { - final Map values = {}; - - @override - Map toMap() => values; -} diff --git a/api/3rd_party/angel3_orm/lib/src/migration.dart b/api/3rd_party/angel3_orm/lib/src/migration.dart deleted file mode 100644 index 9af765e..0000000 --- a/api/3rd_party/angel3_orm/lib/src/migration.dart +++ /dev/null @@ -1,135 +0,0 @@ -const List SQL_RESERVED_WORDS = [ - 'SELECT', - 'UPDATE', - 'INSERT', - 'DELETE', - 'FROM', - 'ASC', - 'DESC', - 'VALUES', - 'RETURNING', - 'ORDER', - 'BY', -]; - -/// Applies additional attributes to a database column. -class Column { - /// If `true`, a SQL field will be nullable. - final bool isNullable; - - /// Specifies this column name. - final String name; - - /// Specifies the length of a `VARCHAR`. - final int length; - - /// Explicitly defines a SQL type for this column. - final ColumnType type; - - /// Specifies what kind of index this column is, if any. - final IndexType indexType; - - /// A custom SQL expression to execute, instead of a named column. - final String? expression; - - const Column( - {this.isNullable = true, - this.length = 255, - this.name = "", - this.type = ColumnType.varChar, - this.indexType = IndexType.none, - this.expression}); - - /// Returns `true` if [expression] is not `null`. - bool get hasExpression => expression != null; -} - -class PrimaryKey extends Column { - const PrimaryKey({ColumnType columnType = ColumnType.serial}) - : super(type: columnType, indexType: IndexType.primaryKey); -} - -const Column primaryKey = PrimaryKey(); - -/// Maps to SQL index types. -enum IndexType { - none, - - /// Standard index. - standardIndex, - - /// A primary key. - primaryKey, - - /// A *unique* index. - unique -} - -/// Maps to SQL data types. -/// -/// Features all types from this list: http://www.tutorialspoint.com/sql/sql-data-types.htm -class ColumnType { - /// The name of this data type. - final String name; - final bool hasSize; - - const ColumnType(this.name, [this.hasSize = false]); - - static const ColumnType boolean = ColumnType('boolean'); - - static const ColumnType smallSerial = ColumnType('smallserial'); - static const ColumnType serial = ColumnType('serial'); - static const ColumnType bigSerial = ColumnType('bigserial'); - - // Numbers - static const ColumnType bigInt = ColumnType('bigint'); - static const ColumnType int = ColumnType('int'); - static const ColumnType smallInt = ColumnType('smallint'); - static const ColumnType tinyInt = ColumnType('tinyint'); - static const ColumnType bit = ColumnType('bit'); - static const ColumnType decimal = ColumnType('decimal', true); - static const ColumnType numeric = ColumnType('numeric', true); - static const ColumnType money = ColumnType('money'); - static const ColumnType smallMoney = ColumnType('smallmoney'); - static const ColumnType float = ColumnType('float'); - static const ColumnType real = ColumnType('real'); - - // Dates and times - static const ColumnType dateTime = ColumnType('datetime'); - static const ColumnType smallDateTime = ColumnType('smalldatetime'); - static const ColumnType date = ColumnType('date'); - static const ColumnType time = ColumnType('time'); - static const ColumnType timeStamp = ColumnType('timestamp'); - static const ColumnType timeStampWithTimeZone = - ColumnType('timestamp with time zone'); - - // Strings - static const ColumnType char = ColumnType('char', true); - static const ColumnType varChar = ColumnType('varchar', true); - static const ColumnType varCharMax = ColumnType('varchar(max)'); - static const ColumnType text = ColumnType('text', true); - - // Unicode strings - static const ColumnType nChar = ColumnType('nchar', true); - static const ColumnType nVarChar = ColumnType('nvarchar', true); - static const ColumnType nVarCharMax = ColumnType('nvarchar(max)', true); - static const ColumnType nText = ColumnType('ntext', true); - - // Binary - static const ColumnType binary = ColumnType('binary', true); - static const ColumnType varBinary = ColumnType('varbinary', true); - static const ColumnType varBinaryMax = ColumnType('varbinary(max)', true); - static const ColumnType image = ColumnType('image', true); - - // JSON. - static const ColumnType json = ColumnType('json', true); - static const ColumnType jsonb = ColumnType('jsonb', true); - - // Misc. - static const ColumnType sqlVariant = ColumnType('sql_variant', true); - static const ColumnType uniqueIdentifier = - ColumnType('uniqueidentifier', true); - static const ColumnType xml = ColumnType('xml', true); - static const ColumnType cursor = ColumnType('cursor', true); - static const ColumnType table = ColumnType('table', true); -} diff --git a/api/3rd_party/angel3_orm/lib/src/order_by.dart b/api/3rd_party/angel3_orm/lib/src/order_by.dart deleted file mode 100644 index 4501cf4..0000000 --- a/api/3rd_party/angel3_orm/lib/src/order_by.dart +++ /dev/null @@ -1,8 +0,0 @@ -class OrderBy { - final String key; - final bool descending; - - const OrderBy(this.key, {this.descending = false}); - - String compile() => descending ? '$key DESC' : '$key ASC'; -} diff --git a/api/3rd_party/angel3_orm/lib/src/query.dart b/api/3rd_party/angel3_orm/lib/src/query.dart deleted file mode 100644 index f718236..0000000 --- a/api/3rd_party/angel3_orm/lib/src/query.dart +++ /dev/null @@ -1,425 +0,0 @@ -import 'dart:async'; -import 'package:logging/logging.dart'; - -import 'annotations.dart'; -import 'join_builder.dart'; -import 'order_by.dart'; -import 'query_base.dart'; -import 'query_executor.dart'; -import 'query_values.dart'; -import 'query_where.dart'; -import 'package:optional/optional.dart'; - -/// A SQL `SELECT` query builder. -abstract class Query extends QueryBase { - final _log = Logger('Query'); - - final List _joins = []; - final Map _names = {}; - final List _orderBy = []; - - // An optional "parent query". If provided, [reserveName] will operate in - // the parent's context. - final Query? parent; - - /// A map of field names to explicit SQL expressions. The expressions will be aliased - /// to the given names. - final Map expressions = {}; - - String? _crossJoin, _groupBy; - int? _limit, _offset; - - Query({this.parent}); - - @override - Map get substitutionValues => - parent?.substitutionValues ?? super.substitutionValues; - - /// A reference to an abstract query builder. - /// - /// This is usually a generated class. - Where? get where; - - /// A set of values, for an insertion or update. - /// - /// This is usually a generated class. - QueryValues? get values; - - /// Preprends the [tableName] to the [String], [s]. - String adornWithTableName(String s) { - if (expressions.containsKey(s)) { - //return '${expressions[s]} AS $s'; - return '(${expressions[s]} AS $s)'; - } else { - return '$tableName.$s'; - } - } - - /// Returns a unique version of [name], which will not produce a collision within - /// the context of this [query]. - String reserveName(String name) { - if (parent != null) { - return parent!.reserveName(name); - } - // var n = _names[name] ??= 0; - // _names[name]++; - var n = 0; - var nn = _names[name]; - if (nn != null) { - n = nn; - nn++; - _names[name] = nn; - } else { - _names[name] = 1; - } - return n == 0 ? name : '$name$n'; - } - - /// Makes a [Where] clause. - Where newWhereClause() { - throw UnsupportedError( - 'This instance does not support creating WHERE clauses.'); - } - - /// Determines whether this query can be compiled. - /// - /// Used to prevent ambiguities in joins. - bool canCompile(Set trampoline) => true; - - /// Shorthand for calling [where].or with a [Where] clause. - void andWhere(void Function(Where) f) { - var w = newWhereClause(); - f(w); - where?.and(w); - } - - /// Shorthand for calling [where].or with a [Where] clause. - void notWhere(void Function(Where) f) { - var w = newWhereClause(); - f(w); - where?.not(w); - } - - /// Shorthand for calling [where].or with a [Where] clause. - void orWhere(void Function(Where) f) { - var w = newWhereClause(); - f(w); - where?.or(w); - } - - /// Limit the number of rows to return. - void limit(int n) { - _limit = n; - } - - /// Skip a number of rows in the query. - void offset(int n) { - _offset = n; - } - - /// Groups the results by a given key. - void groupBy(String key) { - _groupBy = key; - } - - /// Sorts the results by a key. - void orderBy(String key, {bool descending = false}) { - _orderBy.add(OrderBy(key, descending: descending)); - } - - /// Execute a `CROSS JOIN` (Cartesian product) against another table. - void crossJoin(String tableName) { - _crossJoin = tableName; - } - - String _joinAlias(Set trampoline) { - var i = _joins.length; - - while (true) { - var a = 'a$i'; - if (trampoline.add(a)) { - return a; - } else { - i++; - } - } - } - - String Function() _compileJoin(tableName, Set trampoline) { - if (tableName is String) { - return () => tableName; - } else if (tableName is Query) { - return () { - var c = tableName.compile(trampoline); - //if (c == null) return c; - if (c == '') { - return c; - } - return '($c)'; - }; - } else { - _log.severe('$tableName must be a String or Query'); - throw ArgumentError.value( - tableName, 'tableName', 'must be a String or Query'); - } - } - - void _makeJoin( - tableName, - Set? trampoline, - String? alias, - JoinType type, - String localKey, - String foreignKey, - String op, - List additionalFields) { - trampoline ??= {}; - - // Pivot tables guard against ambiguous fields by excluding tables - // that have already been queried in this scope. - if (trampoline.contains(tableName) && trampoline.contains(this.tableName)) { - // ex. if we have {roles, role_users}, then don't join "roles" again. - return; - } - - var to = _compileJoin(tableName, trampoline); - alias ??= _joinAlias(trampoline); - if (tableName is Query) { - for (var field in tableName.fields) { - tableName.aliases[field] = '${alias}_$field'; - } - } - _joins.add(JoinBuilder(type, this, to, localKey, foreignKey, - op: op, - alias: alias, - additionalFields: additionalFields, - aliasAllFields: tableName is Query)); - } - - /// Execute an `INNER JOIN` against another table. - void join(tableName, String localKey, String foreignKey, - {String op = '=', - List additionalFields = const [], - Set? trampoline, - String? alias}) { - _makeJoin(tableName, trampoline, alias, JoinType.inner, localKey, foreignKey, op, - additionalFields); - } - - /// Execute a `LEFT JOIN` against another table. - void leftJoin(tableName, String localKey, String foreignKey, - {String op = '=', - List additionalFields = const [], - Set? trampoline, - String? alias}) { - _makeJoin(tableName, trampoline, alias, JoinType.left, localKey, foreignKey, op, - additionalFields); - } - - /// Execute a `RIGHT JOIN` against another table. - void rightJoin(tableName, String localKey, String foreignKey, - {String op = '=', - List additionalFields = const [], - Set? trampoline, - String? alias}) { - _makeJoin(tableName, trampoline, alias, JoinType.right, localKey, foreignKey, op, - additionalFields); - } - - /// Execute a `FULL OUTER JOIN` against another table. - void fullOuterJoin(tableName, String localKey, String foreignKey, - {String op = '=', - List additionalFields = const [], - Set? trampoline, - String? alias}) { - _makeJoin(tableName, trampoline, alias, JoinType.full, localKey, foreignKey, op, - additionalFields); - } - - /// Execute a `SELF JOIN`. - void selfJoin(tableName, String localKey, String foreignKey, - {String op = '=', - List additionalFields = const [], - Set? trampoline, - String? alias}) { - _makeJoin(tableName, trampoline, alias, JoinType.self, localKey, foreignKey, op, - additionalFields); - } - - @override - String compile(Set trampoline, - {bool includeTableName = false, - String? preamble, - bool withFields = true, - String? fromQuery}) { - // One table MAY appear multiple times in a query. - if (!canCompile(trampoline)) { - //return null; - //throw Exception('One table appear multiple times in a query'); - return ''; - } - - includeTableName = includeTableName || _joins.isNotEmpty; - var b = StringBuffer(preamble ?? 'SELECT'); - b.write(' '); - List f; - - var compiledJoins = {}; - - //if (fields == null) { - if (fields.isEmpty) { - f = ['*']; - } else { - f = List.from(fields.map((s) { - String? ss = includeTableName ? '$tableName.$s' : s; - if (expressions.containsKey(s)) { - ss = '( ${expressions[s]} )'; - //ss = expressions[s]; - } - var cast = casts[s]; - if (cast != null) ss = 'CAST ($ss AS $cast)'; - if (aliases.containsKey(s)) { - if (cast != null) { - ss = '($ss) AS ${aliases[s]}'; - } else { - ss = '$ss AS ${aliases[s]}'; - } - if (expressions.containsKey(s)) { - // ss = '($ss)'; - } - } else if (expressions.containsKey(s)) { - if (cast != null) { - ss = '($ss) AS $s'; - // ss = '(($ss) AS $s)'; - } else { - ss = '$ss AS $s'; - // ss = '($ss AS $s)'; - } - } - return ss; - })); - _joins.forEach((j) { - var c = compiledJoins[j] = j.compile(trampoline); - //if (c != null) { - if (c != '') { - var additional = j.additionalFields.map(j.nameFor).toList(); - f.addAll(additional); - } else { - // If compilation failed, fill in NULL placeholders. - for (var i = 0; i < j.additionalFields.length; i++) { - f.add('NULL'); - } - } - }); - } - if (withFields) b.write(f.join(', ')); - fromQuery ??= tableName; - b.write(' FROM $fromQuery'); - - // No joins if it's not a select. - if (preamble == null) { - if (_crossJoin != null) b.write(' CROSS JOIN $_crossJoin'); - for (var join in _joins) { - var c = compiledJoins[join]; - if (c != null) b.write(' $c'); - } - } - - var whereClause = - where?.compile(tableName: includeTableName ? tableName : null); - if (whereClause?.isNotEmpty == true) { - b.write(' WHERE $whereClause'); - } - if (_groupBy != null) b.write(' GROUP BY $_groupBy'); - for (var item in _orderBy) { - b.write(' ORDER BY ${item.compile()}'); - } - if (_limit != null) b.write(' LIMIT $_limit'); - if (_offset != null) b.write(' OFFSET $_offset'); - return b.toString(); - } - - @override - Future> getOne(QueryExecutor executor) { - //limit(1); - return super.getOne(executor); - } - - Future> delete(QueryExecutor executor) { - var sql = compile({}, preamble: 'DELETE', withFields: false); - - //_log.fine("Delete Query = $sql"); - - if (_joins.isEmpty) { - return executor - .query(tableName, sql, substitutionValues, - fields.map(adornWithTableName).toList()) - .then((it) => deserializeList(it)); - } else { - return executor.transaction((tx) async { - // TODO: Can this be done with just *one* query? - var existing = await get(tx); - //var sql = compile(preamble: 'SELECT $tableName.id', withFields: false); - return tx - .query(tableName, sql, substitutionValues) - .then((_) => existing); - }); - } - } - - Future> deleteOne(QueryExecutor executor) { - return delete(executor).then((it) => - it.isEmpty == true ? Optional.empty() : Optional.ofNullable(it.first)); - } - - Future> insert(QueryExecutor executor) { - var insertion = values?.compileInsert(this, tableName); - - if (insertion == '') { - throw StateError('No values have been specified for update.'); - } else { - // TODO: How to do this in a non-Postgres DB? - var returning = fields.map(adornWithTableName).join(', '); - var sql = compile({}); - sql = 'WITH $tableName as ($insertion RETURNING $returning) ' + sql; - - //_log.fine("Insert Query = $sql"); - - return executor.query(tableName, sql, substitutionValues).then((it) { - // Return SQL execution results - return it.isEmpty ? Optional.empty() : deserialize(it.first); - }); - } - } - - Future> update(QueryExecutor executor) async { - var updateSql = StringBuffer('UPDATE $tableName '); - var valuesClause = values?.compileForUpdate(this); - - if (valuesClause == '') { - throw StateError('No values have been specified for update.'); - } else { - updateSql.write(' $valuesClause'); - var whereClause = where?.compile(); - if (whereClause?.isNotEmpty == true) { - updateSql.write(' WHERE $whereClause'); - } - if (_limit != null) updateSql.write(' LIMIT $_limit'); - - var returning = fields.map(adornWithTableName).join(', '); - var sql = compile({}); - sql = 'WITH $tableName as ($updateSql RETURNING $returning) ' + sql; - - //_log.fine("Update Query = $sql"); - - return executor - .query(tableName, sql, substitutionValues) - .then((it) => deserializeList(it)); - } - } - - Future> updateOne(QueryExecutor executor) { - return update(executor).then( - (it) => it.isEmpty ? Optional.empty() : Optional.ofNullable(it.first)); - } -} diff --git a/api/3rd_party/angel3_orm/lib/src/query_base.dart b/api/3rd_party/angel3_orm/lib/src/query_base.dart deleted file mode 100644 index 17a6215..0000000 --- a/api/3rd_party/angel3_orm/lib/src/query_base.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'dart:async'; - -import 'query_executor.dart'; -import 'union.dart'; -import 'package:optional/optional.dart'; - -/// A base class for objects that compile to SQL queries, typically within an ORM. -abstract class QueryBase { - /// Casts to perform when querying the database. - Map get casts => {}; - - /// `AS` aliases to inject into the query, if any. - Map aliases = {}; - - /// Values to insert into a prepared statement. - final Map substitutionValues = {}; - - /// The table against which to execute this query. - String get tableName; - - /// The list of fields returned by this query. - /// - /// @deprecated If it's `null`, then this query will perform a `SELECT *`. - /// If it's empty, then this query will perform a `SELECT *`. - List get fields; - - /// A String of all [fields], joined by a comma (`,`). - String get fieldSet => fields.map((k) { - var cast = casts[k]; - if (!aliases.containsKey(k)) { - return cast == null ? k : 'CAST ($k AS $cast)'; - } else { - var inner = cast == null ? k : '(CAST ($k AS $cast))'; - return '$inner AS ${aliases[k]}'; - } - }).join(', '); - - String compile(Set trampoline, - {bool includeTableName = false, - String preamble = '', - bool withFields = true}); - - Optional deserialize(List row); - - List deserializeList(List> it) { - var optResult = it.map(deserialize).toList(); - var result = []; - optResult.forEach((element) { - element.ifPresent((item) { - result.add(item); - }); - }); - - return result; - } - - Future> get(QueryExecutor executor) async { - var sql = compile({}); - - //_log.fine('sql = $sql'); - //_log.fine('substitutionValues = $substitutionValues'); - - return executor.query(tableName, sql, substitutionValues).then((it) { - return deserializeList(it); - }); - } - - Future> getOne(QueryExecutor executor) { - //return get(executor).then((it) => it.isEmpty ? : it.first); - return get(executor).then( - (it) => it.isEmpty ? Optional.empty() : Optional.ofNullable(it.first)); - } - - Union union(QueryBase other) { - return Union(this, other); - } - - Union unionAll(QueryBase other) { - return Union(this, other, all: true); - } -} diff --git a/api/3rd_party/angel3_orm/lib/src/query_executor.dart b/api/3rd_party/angel3_orm/lib/src/query_executor.dart deleted file mode 100644 index fac6195..0000000 --- a/api/3rd_party/angel3_orm/lib/src/query_executor.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'dart:async'; - -/// An abstract interface that performs queries. -/// -/// This class should be implemented. -abstract class QueryExecutor { - const QueryExecutor(); - - /// Executes a single query. - Future> query( - String tableName, String query, Map substitutionValues, - [List returningFields = const []]); - - /// Enters a database transaction, performing the actions within, - /// and returning the results of [f]. - /// - /// If [f] fails, the transaction will be rolled back, and the - /// responsible exception will be re-thrown. - /// - /// Whether nested transactions are supported depends on the - /// underlying driver. - Future transaction(FutureOr Function(QueryExecutor) f); -} diff --git a/api/3rd_party/angel3_orm/lib/src/query_values.dart b/api/3rd_party/angel3_orm/lib/src/query_values.dart deleted file mode 100644 index 2b265cf..0000000 --- a/api/3rd_party/angel3_orm/lib/src/query_values.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'query.dart'; - -abstract class QueryValues { - Map get casts => {}; - - Map toMap(); - - String applyCast(String name, String sub) { - if (casts.containsKey(name)) { - var type = casts[name]; - return 'CAST ($sub as $type)'; - } else { - return sub; - } - } - - String compileInsert(Query query, String tableName) { - var data = Map.from(toMap()); - var now = DateTime.now(); - if (data.containsKey('created_at') && data['created_at'] == null) { - data['created_at'] = now; - } - if (data.containsKey('createdAt') && data['createdAt'] == null) { - data['createdAt'] = now; - } - if (data.containsKey('updated_at') && data['updated_at'] == null) { - data['updated_at'] = now; - } - if (data.containsKey('updatedAt') && data['updatedAt'] == null) { - data['updatedAt'] = now; - } - var keys = data.keys.toList(); - keys.where((k) => !query.fields.contains(k)).forEach(data.remove); - if (data.isEmpty) { - return ''; - } - var fieldSet = data.keys.join(', '); - var b = StringBuffer('INSERT INTO $tableName ($fieldSet) VALUES ('); - var i = 0; - - for (var entry in data.entries) { - if (i++ > 0) b.write(', '); - - var name = query.reserveName(entry.key); - - var s = applyCast(entry.key, '@$name'); - query.substitutionValues[name] = entry.value; - b.write(s); - } - - b.write(')'); - return b.toString(); - } - - String compileForUpdate(Query query) { - var data = toMap(); - if (data.isEmpty) { - return ''; - } - var now = DateTime.now(); - if (data.containsKey('created_at') && data['created_at'] == null) { - data.remove('created_at'); - } - if (data.containsKey('createdAt') && data['createdAt'] == null) { - data.remove('createdAt'); - } - if (data.containsKey('updated_at') && data['updated_at'] == null) { - data['updated_at'] = now; - } - if (data.containsKey('updatedAt') && data['updatedAt'] == null) { - data['updatedAt'] = now; - } - var b = StringBuffer('SET'); - var i = 0; - - for (var entry in data.entries) { - if (i++ > 0) b.write(','); - b.write(' '); - b.write(entry.key); - b.write('='); - - var name = query.reserveName(entry.key); - var s = applyCast(entry.key, '@$name'); - query.substitutionValues[name] = entry.value; - b.write(s); - } - return b.toString(); - } -} diff --git a/api/3rd_party/angel3_orm/lib/src/query_where.dart b/api/3rd_party/angel3_orm/lib/src/query_where.dart deleted file mode 100644 index 5a700c2..0000000 --- a/api/3rd_party/angel3_orm/lib/src/query_where.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'builder.dart'; - -/// Builds a SQL `WHERE` clause. -abstract class QueryWhere { - final Set _and = {}; - final Set _not = {}; - final Set _or = {}; - final Set _raw = {}; - - Iterable get expressionBuilders; - - void and(QueryWhere other) { - _and.add(other); - } - - void not(QueryWhere other) { - _not.add(other); - } - - void or(QueryWhere other) { - _or.add(other); - } - - void raw(String whereRaw) { - _raw.add(whereRaw); - } - - String compile({String? tableName}) { - var b = StringBuffer(); - var i = 0; - - for (var builder in expressionBuilders) { - var key = builder.columnName; - if (tableName != null) key = '$tableName.$key'; - if (builder.hasValue) { - if (i++ > 0) b.write(' AND '); - if (builder is DateTimeSqlExpressionBuilder || - (builder is JsonSqlExpressionBuilder && builder.hasRaw)) { - if (tableName != null) b.write('$tableName.'); - b.write(builder.compile()); - } else { - b.write('$key ${builder.compile()}'); - } - } - } - - for (var raw in _raw) { - if (i++ > 0) b.write(' AND '); - b.write(' ($raw)'); - } - - for (var other in _and) { - var sql = other.compile(); - if (sql.isNotEmpty) b.write(' AND ($sql)'); - } - - for (var other in _not) { - var sql = other.compile(); - if (sql.isNotEmpty) b.write(' NOT ($sql)'); - } - - for (var other in _or) { - var sql = other.compile(); - if (sql.isNotEmpty) b.write(' OR ($sql)'); - } - - return b.toString(); - } -} diff --git a/api/3rd_party/angel3_orm/lib/src/relations.dart b/api/3rd_party/angel3_orm/lib/src/relations.dart deleted file mode 100644 index ab7097e..0000000 --- a/api/3rd_party/angel3_orm/lib/src/relations.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'annotations.dart'; - -abstract class RelationshipType { - static const int hasMany = 0; - static const int hasOne = 1; - static const int belongsTo = 2; - static const int manyToMany = 3; -} - -class Relationship { - final int type; - final String? localKey; - final String? foreignKey; - final String? foreignTable; - final bool cascadeOnDelete; - final JoinType? joinType; - - const Relationship(this.type, - {this.localKey, - this.foreignKey, - this.foreignTable, - this.cascadeOnDelete = false, - this.joinType}); -} - -class HasMany extends Relationship { - const HasMany( - {String? localKey, - String? foreignKey, - String? foreignTable, - bool cascadeOnDelete = false, - JoinType? joinType}) - : super(RelationshipType.hasMany, - localKey: localKey, - foreignKey: foreignKey, - foreignTable: foreignTable, - cascadeOnDelete: cascadeOnDelete == true, - joinType: joinType); -} - -const HasMany hasMany = HasMany(); - -class HasOne extends Relationship { - const HasOne( - {String? localKey, - String? foreignKey, - String? foreignTable, - bool cascadeOnDelete = false, - JoinType? joinType}) - : super(RelationshipType.hasOne, - localKey: localKey, - foreignKey: foreignKey, - foreignTable: foreignTable, - cascadeOnDelete: cascadeOnDelete == true, - joinType: joinType); -} - -const HasOne hasOne = HasOne(); - -class BelongsTo extends Relationship { - const BelongsTo( - {String? localKey, - String? foreignKey, - String? foreignTable, - JoinType? joinType}) - : super(RelationshipType.belongsTo, - localKey: localKey, - foreignKey: foreignKey, - foreignTable: foreignTable, - joinType: joinType); -} - -const BelongsTo belongsTo = BelongsTo(); - -class ManyToMany extends Relationship { - final Type through; - - const ManyToMany(this.through, - {String? localKey, - String? foreignKey, - String? foreignTable, - bool cascadeOnDelete = false, - JoinType? joinType}) - : super( - RelationshipType.hasMany, // Many-to-Many is actually just a hasMany - localKey: localKey, - foreignKey: foreignKey, - foreignTable: foreignTable, - cascadeOnDelete: cascadeOnDelete == true, - joinType: joinType); -} diff --git a/api/3rd_party/angel3_orm/lib/src/union.dart b/api/3rd_party/angel3_orm/lib/src/union.dart deleted file mode 100644 index 5ab2f86..0000000 --- a/api/3rd_party/angel3_orm/lib/src/union.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'query_base.dart'; -import 'package:optional/optional.dart'; - -/// Represents the `UNION` of two subqueries. -class Union extends QueryBase { - /// The subject(s) of this binary operation. - final QueryBase left, right; - - /// Whether this is a `UNION ALL` operation. - final bool all; - - @override - final String tableName; - - Union(this.left, this.right, {this.all = false, String? tableName}) - : tableName = tableName ?? left.tableName { - substitutionValues - ..addAll(left.substitutionValues) - ..addAll(right.substitutionValues); - } - - @override - List get fields => left.fields; - - @override - Optional deserialize(List row) => left.deserialize(row); - - @override - String compile(Set trampoline, - {bool includeTableName = false, - String? preamble, - bool withFields = true}) { - var selector = all == true ? 'UNION ALL' : 'UNION'; - var t1 = Set.from(trampoline); - var t2 = Set.from(trampoline); - return '(${left.compile(t1, includeTableName: includeTableName)}) $selector (${right.compile(t2, includeTableName: includeTableName)})'; - } -} diff --git a/api/3rd_party/angel3_orm/lib/src/util.dart b/api/3rd_party/angel3_orm/lib/src/util.dart deleted file mode 100644 index 939b36c..0000000 --- a/api/3rd_party/angel3_orm/lib/src/util.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'package:charcode/ascii.dart'; - -bool isAscii(int ch) => ch >= $nul && ch <= $del; diff --git a/api/3rd_party/angel3_orm/mono_pkg.yaml b/api/3rd_party/angel3_orm/mono_pkg.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/api/3rd_party/angel3_orm/pubspec.yaml b/api/3rd_party/angel3_orm/pubspec.yaml deleted file mode 100644 index c688aba..0000000 --- a/api/3rd_party/angel3_orm/pubspec.yaml +++ /dev/null @@ -1,21 +0,0 @@ -name: angel3_orm -version: 4.0.4 -description: Runtime support for Angel3 ORM. Includes base classes for queries. -homepage: https://angel3-framework.web.app/ -repository: https://github.com/dukefirehawk/angel/tree/master/packages/orm/angel_orm -environment: - sdk: '>=2.12.0 <3.0.0' -dependencies: - charcode: ^1.2.0 - intl: ^0.17.0 - meta: ^1.3.0 - string_scanner: ^1.1.0 - optional: ^6.0.0 - logging: ^1.0.0 -dev_dependencies: - angel3_model: ^3.0.0 - angel3_serialize: ^4.1.0 - angel3_serialize_generator: ^4.1.0 - build_runner: ^2.1.1 - test: ^1.17.4 - lints: ^1.0.0 \ No newline at end of file diff --git a/api/pubspec.yaml b/api/pubspec.yaml index 244c628..579e527 100644 --- a/api/pubspec.yaml +++ b/api/pubspec.yaml @@ -9,33 +9,27 @@ dependencies: angel3_configuration: ^4.1.0 angel3_framework: ^4.2.0 angel3_migration: ^4.0.3 - angel3_orm_postgres: ^3.0.0 + angel3_orm: ^4.0.5 + angel3_orm_postgres: ^3.3.0 angel3_serialize: ^4.1.0 - angel3_production: ^3.1.0 + angel3_production: ^3.1.2 angel3_static: ^4.1.0 - angel3_validate: ^4.0.0 belatuk_pretty_logging: ^4.0.0 optional: ^6.0.0 logging: ^1.0.0 yaml: ^3.1.0 mailer: ^5.0.2 uuid: ^3.0.5 - angel3_orm: - path: 3rd_party/angel3_orm neat_cache: path: 3rd_party/neat_cache dev_dependencies: - angel3_hot: ^4.2.0 + angel3_hot: ^4.3.0 angel3_jinja: ^2.0.1 - angel3_migration_runner: ^4.0.0 - angel3_orm_generator: ^4.1.3 - angel3_serialize_generator: ^4.2.0 + angel3_migration_runner: ^4.1.1 + angel3_orm_generator: ^4.3.0 + angel3_serialize_generator: ^4.3.0 angel3_test: ^4.0.0 build_runner: ^2.0.3 io: ^1.0.0 test: ^1.17.5 lints: ^1.0.0 - -dependency_overrides: - angel3_orm: - path: 3rd_party/angel3_orm diff --git a/app/lib/http/api.dart b/app/lib/http/api.dart index 4841c11..8fe7a4f 100644 --- a/app/lib/http/api.dart +++ b/app/lib/http/api.dart @@ -46,9 +46,9 @@ class Api { } } - static HandleRespBuild _handleRespBuild(BeanBuilder builder) => (http.Response resp) { + static HandleRespBuild _handleRespBuild(BeanBuilder builder) => (http.Response resp) { if (builder is GetStatusCodeFunc) return builder({"statusCode": resp.statusCode}); - T res; + T? res; try { var decodeBody = json.decode(utf8.decode(resp.bodyBytes)); res = decodeBody is Map ? builder(decodeBody) : builder({'list': decodeBody}); @@ -59,7 +59,7 @@ class Api { return res; }; - static Future _get( + static Future _get( String path, BeanBuilder builder, { Map? queryParams, @@ -80,7 +80,7 @@ class Api { }..addAll( ignoreToken ? {} : {HttpHeaders.authorizationHeader: 'Bearer ${H().sp.getString(SPKeys.accessToken)}'}), ) - .then( + .then( _handleRespBuild(builder), onError: (e) { if (ignoreErrorHandle) @@ -90,7 +90,7 @@ class Api { }, ); - static Future _post( + static Future _post( String path, BeanBuilder builder, { Map? body, @@ -111,7 +111,7 @@ class Api { }..addAll( ignoreToken ? {} : {HttpHeaders.authorizationHeader: 'Bearer ${H().sp.getString(SPKeys.accessToken)}'}), ) - .then( + .then( _handleRespBuild(builder), onError: (e) { if (ignoreErrorHandle) @@ -159,18 +159,17 @@ class Api { ), ).then((value) => value == HttpStatus.noContent); - static Future> userSchemes({required SchemeListType type}) => + static Future?> userSchemes({required SchemeListType type}) => _get(Apis.scheme.user(type: type.name.param), listRespBuilderWrap(SimpleSchemeTransMetaDataSerializer.fromMap)); static Future likeScheme({required String schemeId, required bool isLike}) => _get( Apis.scheme.like(schemeId: schemeId.param, isLike: StringParam(isLike ? 'like' : 'unlike')), getStatusCodeFunc) .then((value) { - 123.sout(); return value == HttpStatus.noContent; }); - static Future downloadScheme({required String schemeId}) => _get( + static Future downloadScheme({required String schemeId}) => _get( Apis.scheme.download(schemeId: schemeId.param), SchemeForDownloadSerializer.fromMap, ); diff --git a/app/lib/widgets/me.dart b/app/lib/widgets/me.dart index 7e3d71b..978c239 100644 --- a/app/lib/widgets/me.dart +++ b/app/lib/widgets/me.dart @@ -35,7 +35,7 @@ class _MeWidgetState extends State { void initState() { super.initState(); Api.userSchemes(type: _type).then((value) { - if (mounted) + if (mounted && value != null) setState(() { _schemes = value; _selected = value.isNotEmpty ? value.first.uuid : null; @@ -53,7 +53,7 @@ class _MeWidgetState extends State { _refreshList() { Future.delayed(const Duration(milliseconds: 100), () { Api.userSchemes(type: _type).then((value) { - if (mounted) + if (mounted && value != null) setState(() { _schemes = value; }); @@ -108,9 +108,10 @@ class _MeWidgetState extends State { _type = e; }); Api.userSchemes(type: e).then((value) { - if (mounted) + if (mounted && value != null) setState(() { _schemes = value; + _selected = value.first.uuid; }); }); }, diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 4176979..410e658 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -57,9 +57,6 @@ dev_dependencies: build_runner: 2.1.2 source_gen: 1.1.0 -dependency_overrides: - angel3_orm: - path: ../api/3rd_party/angel3_orm # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From 58263f4548df01f6cbfd8e254f63fa2628ae0c61 Mon Sep 17 00:00:00 2001 From: debuggerx Date: Tue, 11 Jan 2022 18:54:55 +0800 Subject: [PATCH 5/6] wip: upgrade dependencies --- api/.gitignore | 2 +- api/Dockerfile | 21 ++-- api/docker-compose.yml | 34 +++++ api/lib/apis.dart | 4 + api/lib/src/models/like_record.dart | 17 +++ api/lib/src/models/scheme.dart | 58 +++++++++ .../src/routes/controllers/scheme_controllers.dart | 131 +++++++++++++------ api/pubspec.yaml | 1 - api/start.sh | 7 ++ app/lib/constants/constants.dart | 2 +- app/lib/pages/market_or_me.dart | 12 +- app/lib/widgets/market.dart | 102 +++++++++++++++ app/lib/widgets/me.dart | 139 ++++++++++++++------- app/pubspec.yaml | 23 ++-- app/resources/langs/en.json | 8 +- app/resources/langs/zh-CN.json | 8 +- 16 files changed, 455 insertions(+), 114 deletions(-) create mode 100644 api/docker-compose.yml create mode 100644 api/start.sh create mode 100644 app/lib/widgets/market.dart diff --git a/api/.gitignore b/api/.gitignore index 2bb6e81..342a80c 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -91,7 +91,7 @@ server_log.txt .metals/ -/config/production.yaml.bak +/config/production.yaml /config/development.yaml *.g.dart diff --git a/api/Dockerfile b/api/Dockerfile index c84bc87..d7f0050 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,14 +1,17 @@ -FROM google/dart:latest - +FROM dart:stable AS build-env +LABEL stage=dart_builder +ENV PUB_HOSTED_URL="https://pub.flutter-io.cn" +ENV ANGEL_ENV=production COPY ./ ./ - -# Install dependencies, pre-build RUN pub get +RUN dart compile exe bin/prod.dart -o /server -# Optionally build generaed sources. -# RUN pub run build_runner build - -# Set environment, start server +FROM scratch +WORKDIR /app ENV ANGEL_ENV=production +ADD ./views ./views +ADD ./config ./config +COPY --from=build-env /runtime/ / +COPY --from=build-env /server /app EXPOSE 3000 -CMD dart bin/prod.dart +ENTRYPOINT ["./server", "-a", "0.0.0.0", "--port", "3000"] \ No newline at end of file diff --git a/api/docker-compose.yml b/api/docker-compose.yml new file mode 100644 index 0000000..2dbb198 --- /dev/null +++ b/api/docker-compose.yml @@ -0,0 +1,34 @@ +version: "2.1" + +services: + mysql: + image: postgres:latest + restart: always + container_name: dgm_postgres + command: --default-authentication-plugin=mysql_native_password + environment: + MYSQL_ROOT_PASSWORD: Dx@8917312 + MYSQL_DATABASE: piwigo + MYSQL_USER: debuggerx + MYSQL_PASSWORD: dx8917312 + volumes: + - /mnt/hd500/db:/var/lib/mysql + ports: + - "5432:5432" + networks: + - mynet + + api: + build: . + container_name: dgm_api + links: + - dgm_postgres + ports: + - 8888:8888 + networks: + - mynet + restart: unless-stopped + +networks: + mynet: + driver: bridge \ No newline at end of file diff --git a/api/lib/apis.dart b/api/lib/apis.dart index ada4277..292ce4c 100644 --- a/api/lib/apis.dart +++ b/api/lib/apis.dart @@ -35,9 +35,13 @@ class SchemeApis { String user({required StringParam type}) => [path, 'user', type].joinPath(); + String market({required StringParam type, required IntParam page, required IntParam pageSize}) => [path, 'market', type, page, pageSize].joinPath(); + String download({required StringParam schemeId}) => [path, 'download', schemeId].joinPath(); String like({required StringParam schemeId, required StringParam isLike}) => [path, 'like', schemeId, isLike].joinPath(); + + String get userLikes => [path, 'user-likes'].joinPath(); } final _paramsMap = { diff --git a/api/lib/src/models/like_record.dart b/api/lib/src/models/like_record.dart index c794cfe..3847c6d 100644 --- a/api/lib/src/models/like_record.dart +++ b/api/lib/src/models/like_record.dart @@ -20,4 +20,21 @@ abstract class _LikeRecord extends BaseModel { @Column(isNullable: false, indexType: IndexType.standardIndex) @SerializableField(isNullable: false) bool? get liked; +} + + +@serializable +@Orm(tableName: 'like_records', generateMigrations: false) +abstract class _UserLikes { + @Column(isNullable: false) + @SerializableField(isNullable: false) + int? id; + + @Column(isNullable: false) + @SerializableField(exclude: true) + int? get uid; + + @Column(isNullable: false) + @SerializableField(exclude: true) + bool? get liked; } \ No newline at end of file diff --git a/api/lib/src/models/scheme.dart b/api/lib/src/models/scheme.dart index ff665c3..c0c1cba 100644 --- a/api/lib/src/models/scheme.dart +++ b/api/lib/src/models/scheme.dart @@ -71,6 +71,9 @@ abstract class _SimpleScheme { @serializable abstract class _SimpleSchemeTransMetaData { @SerializableField(isNullable: false) + int? id; + + @SerializableField(isNullable: false) String? get uuid; @SerializableField(isNullable: false) @@ -90,6 +93,7 @@ abstract class _SimpleSchemeTransMetaData { } SimpleSchemeTransMetaData transSimpleSchemeMetaData(SimpleScheme scheme) => SimpleSchemeTransMetaData( + id: scheme.id, description: scheme.description, uuid: scheme.uuid, name: scheme.name, @@ -121,3 +125,57 @@ SchemeForDownload transSchemeForDownload(Scheme scheme) => SchemeForDownload( description: scheme.description, gestures: scheme.gestures, ); + +@serializable +@Orm(tableName: 'schemes', generateMigrations: false) +abstract class _MarketScheme { + @Column() + int? id; + + @Column(isNullable: false, indexType: IndexType.unique) + @SerializableField(isNullable: false) + String? get uuid; + + @Column(isNullable: false) + @SerializableField(isNullable: false) + String? get name; + + @Column(type: ColumnType.text) + String? description; + + @Column(isNullable: false) + @SerializableField(exclude: true) + bool? get shared; + + @SerializableField(isNullable: true) + @Column(type: ColumnType.json) + Map? get metadata; +} + +@serializable +abstract class _MarketSchemeTransMetaData { + @SerializableField(isNullable: false) + int? id; + + @SerializableField(isNullable: false) + String? get uuid; + + @SerializableField(isNullable: false) + String? get name; + + @SerializableField(isNullable: false) + String? description; + + int? get downloads; + + int? get likes; +} + +MarketSchemeTransMetaData transMarketSchemeMetaData(MarketScheme scheme) => MarketSchemeTransMetaData( + id: scheme.id, + description: scheme.description, + uuid: scheme.uuid, + name: scheme.name, + likes: scheme.metadata?['likes'] ?? 0, + downloads: scheme.metadata?['downloads'] ?? 0, +); \ No newline at end of file diff --git a/api/lib/src/routes/controllers/scheme_controllers.dart b/api/lib/src/routes/controllers/scheme_controllers.dart index 03f9306..cdf22ff 100644 --- a/api/lib/src/routes/controllers/scheme_controllers.dart +++ b/api/lib/src/routes/controllers/scheme_controllers.dart @@ -62,41 +62,6 @@ Future configureServer(Angel app) async { ); app.get( - Apis.scheme.user.route, - chain( - [ - jwtMiddleware(), - (req, res) async { - var schemeQuery = SimpleSchemeQuery(); - var type = req.params['type']; - var likeRecordTableName = LikeRecordQuery().tableName; - schemeQuery.leftJoin(likeRecordTableName, SchemeFields.id, LikeRecordFields.schemeId, alias: 'lr'); - - switch (type) { - case 'uploaded': - schemeQuery.where!.uid.equals(req.user!.idAsInt); - break; - case 'downloaded': - var downloadHistoryTableName = DownloadHistoryQuery().tableName; - schemeQuery.leftJoin(downloadHistoryTableName, SchemeFields.id, DownloadHistoryFields.schemeId, - alias: 'dh'); - schemeQuery.where!.raw('dh.${DownloadHistoryFields.uid} = ${req.user!.idAsInt}'); - break; - case 'liked': - schemeQuery.where!.raw('lr.${LikeRecordFields.uid} = ${req.user!.idAsInt}'); - schemeQuery.where!.raw('lr.${LikeRecordFields.liked} = true'); - break; - default: - return res.unProcessableEntity(); - } - schemeQuery.orderBy('${schemeQuery.tableName}.${SchemeFields.updatedAt}', descending: true); - return schemeQuery.get(req.queryExecutor).then((value) => value.map(transSimpleSchemeMetaData).toList()); - }, - ], - ), - ); - - app.get( Apis.scheme.download.route, chain( [ @@ -185,4 +150,100 @@ Future configureServer(Angel app) async { ], ), ); + + app.get( + Apis.scheme.user.route, + chain( + [ + jwtMiddleware(), + (req, res) async { + var schemeQuery = SimpleSchemeQuery(); + var type = req.params['type']; + var likeRecordTableName = LikeRecordQuery().tableName; + schemeQuery.leftJoin(likeRecordTableName, SchemeFields.id, LikeRecordFields.schemeId, alias: 'lr'); + + switch (type) { + case 'uploaded': + schemeQuery.where!.uid.equals(req.user!.idAsInt); + break; + case 'downloaded': + var downloadHistoryTableName = DownloadHistoryQuery().tableName; + schemeQuery.leftJoin(downloadHistoryTableName, SchemeFields.id, DownloadHistoryFields.schemeId, + alias: 'dh'); + schemeQuery.where!.raw('dh.${DownloadHistoryFields.uid} = ${req.user!.idAsInt}'); + break; + case 'liked': + schemeQuery.where!.raw('lr.${LikeRecordFields.uid} = ${req.user!.idAsInt}'); + schemeQuery.where!.raw('lr.${LikeRecordFields.liked} = true'); + break; + default: + return res.unProcessableEntity(); + } + schemeQuery.orderBy('${schemeQuery.tableName}.${SchemeFields.updatedAt}', descending: true); + return schemeQuery.get(req.queryExecutor).then((value) => value.map(transSimpleSchemeMetaData).toList()); + }, + ], + ), + ); + + const recommend = "(metadata->'recommends') is null ,(metadata->'recommends')::int"; + const updated = "updated_at"; + const likes = "(metadata->'likes') is null ,(metadata->'likes')::int"; + const downloads = "(metadata->'downloads') is null ,(metadata->'downloads')::int"; + + app.get(Apis.scheme.market.route, (req, res) async { + var schemeQuery = MarketSchemeQuery(); + var type = req.params['type']; + var page = req.params['page'] as int; + var pageSize = req.params['pageSize'] as int; + schemeQuery.where?.shared.equals(true); + late List orders; + + switch (type) { + case 'recommend': + // orders = [recommend, likes, downloads, SchemeFields.id]; + orders = [recommend]; + break; + case 'updated': + orders = [updated]; + break; + case 'likes': + // orders = [likes, recommend, downloads, SchemeFields.id]; + orders = [likes]; + break; + case 'downloads': + // orders = [downloads, recommend, likes, SchemeFields.id]; + orders = [downloads]; + break; + default: + return res.unProcessableEntity(); + } + for (var order in orders) { + schemeQuery.orderBy(order, descending: true); + } + schemeQuery.offset(page * pageSize); + schemeQuery.limit(pageSize + 1); + return schemeQuery.get(req.queryExecutor).then((value) { + var hasMore = value.length > pageSize; + if (hasMore) value.removeLast(); + return { + 'hasMore': hasMore, + 'items': value.map(transMarketSchemeMetaData).toList(), + }; + }); + }); + + app.get( + Apis.scheme.userLikes, + chain( + [ + jwtMiddleware(), + (req, res) async => (UserLikesQuery() + ..where?.uid.equals(req.user!.idAsInt) + ..where?.liked.equals(true)) + .get(req.queryExecutor) + .then((value) => value.map((e) => e.id).toList()), + ], + ), + ); } diff --git a/api/pubspec.yaml b/api/pubspec.yaml index 579e527..2b58d05 100644 --- a/api/pubspec.yaml +++ b/api/pubspec.yaml @@ -13,7 +13,6 @@ dependencies: angel3_orm_postgres: ^3.3.0 angel3_serialize: ^4.1.0 angel3_production: ^3.1.2 - angel3_static: ^4.1.0 belatuk_pretty_logging: ^4.0.0 optional: ^6.0.0 logging: ^1.0.0 diff --git a/api/start.sh b/api/start.sh new file mode 100644 index 0000000..89f09ef --- /dev/null +++ b/api/start.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +docker build -t dgm_api_image . +docker image prune -f --filter label=stage=dart_builder +docker rm -f dgm_api +docker run -d --restart=always --name dgm_api -p 3000:3000 dgm_api_image +docker image prune -f diff --git a/app/lib/constants/constants.dart b/app/lib/constants/constants.dart index 0a4ab78..f4214a8 100644 --- a/app/lib/constants/constants.dart +++ b/app/lib/constants/constants.dart @@ -3,7 +3,7 @@ import 'package:flutter/cupertino.dart'; /// [UOS设计指南](https://docs.uniontech.com/zh/content/t_dbG3kBK9iDf9B963ok) const double localManagerPanelWidth = 260; -const double marketOrMePanelWidth = 300; +const double marketOrMePanelWidth = 450; const shortDuration = const Duration(milliseconds: 100); diff --git a/app/lib/pages/market_or_me.dart b/app/lib/pages/market_or_me.dart index af2bf2a..447c963 100644 --- a/app/lib/pages/market_or_me.dart +++ b/app/lib/pages/market_or_me.dart @@ -4,6 +4,7 @@ import 'package:dde_gesture_manager/models/configs.provider.dart'; import 'package:dde_gesture_manager/models/content_layout.provider.dart'; import 'package:dde_gesture_manager/widgets/dde_button.dart'; import 'package:dde_gesture_manager/widgets/login.dart'; +import 'package:dde_gesture_manager/widgets/market.dart'; import 'package:dde_gesture_manager/widgets/me.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -18,15 +19,14 @@ class MarketOrMe extends StatelessWidget { var layoutProvider = context.watch(); bool isOpen = layoutProvider.marketOrMeOpened == true; bool isMarket = layoutProvider.isMarket; - bool showLogin = context.watch().accessToken.isNull && !isMarket; return AnimatedContainer( duration: mediumDuration, curve: Curves.easeInOut, - width: isOpen ? marketOrMePanelWidth * (showLogin ? 1.5 : 1) : 0, + width: isOpen ? marketOrMePanelWidth * 1 : 0, child: OverflowBox( alignment: Alignment.centerLeft, - maxWidth: marketOrMePanelWidth * (showLogin ? 1.5 : 1), - minWidth: marketOrMePanelWidth * (showLogin ? 1.5 : 1), + maxWidth: marketOrMePanelWidth, + minWidth: marketOrMePanelWidth, child: Material( color: context.t.backgroundColor, elevation: isOpen ? 10 : 0, @@ -85,7 +85,5 @@ class MarketOrMe extends StatelessWidget { return Expanded(child: MeWidget()); } - Widget buildMarketContent(BuildContext context) { - return Container(); - } + Widget buildMarketContent(BuildContext context) => Expanded(child: MarketWidget()); } diff --git a/app/lib/widgets/market.dart b/app/lib/widgets/market.dart new file mode 100644 index 0000000..33886a7 --- /dev/null +++ b/app/lib/widgets/market.dart @@ -0,0 +1,102 @@ +import 'package:dde_gesture_manager/constants/constants.dart'; +import 'package:dde_gesture_manager/widgets/dde_button.dart'; +import 'package:flutter/material.dart'; +import 'package:dde_gesture_manager/extensions.dart'; + +enum MarketSortType { + recommend, + updated, + likes, + downloads, +} + +class MarketWidget extends StatefulWidget { + const MarketWidget({Key? key}) : super(key: key); + + @override + _MarketWidgetState createState() => _MarketWidgetState(); +} + +class _MarketWidgetState extends State { + MarketSortType _type = MarketSortType.recommend; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(top: 15, bottom: 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MarketSortType.recommend, + MarketSortType.updated, + MarketSortType.likes, + MarketSortType.downloads, + ] + .map( + (e) => Flexible( + flex: 1, + fit: FlexFit.tight, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + setState(() { + _type = e; + }); + }, + child: Center( + child: Text( + '${LocaleKeys.market_sort_types}.${e.name}'.tr(), + style: _type == e ? TextStyle(fontWeight: FontWeight.bold, fontSize: 15) : null, + ), + ), + ), + ), + ), + ) + .toList(), + ), + ), + Expanded( + child: Container( + decoration: BoxDecoration( + border: Border.all( + width: .3, + color: context.t.dividerColor, + ), + borderRadius: BorderRadius.circular(defaultBorderRadius), + ), + child: Column( + children: [Text('asd')], + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + DButton.like( + enabled: true, + onTap: () {}, + ), + DButton.download( + enabled: true, + onTap: () {}, + ), + ] + .map((e) => Padding( + padding: const EdgeInsets.only(right: 4), + child: e, + )) + .toList(), + ), + ), + ], + ); + } +} diff --git a/app/lib/widgets/me.dart b/app/lib/widgets/me.dart index 978c239..c8d00d5 100644 --- a/app/lib/widgets/me.dart +++ b/app/lib/widgets/me.dart @@ -1,4 +1,5 @@ import 'package:auto_size_text/auto_size_text.dart'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:collection/collection.dart'; import 'package:dde_gesture_manager/constants/constants.dart'; import 'package:dde_gesture_manager/extensions.dart'; @@ -9,6 +10,8 @@ import 'package:dde_gesture_manager/utils/notificator.dart'; import 'package:dde_gesture_manager_api/models.dart'; import 'package:flutter/material.dart'; import 'package:flutter_platform_alert/flutter_platform_alert.dart'; +import 'package:markdown_editor_ot/markdown_editor.dart'; +import 'package:numeral/fun.dart'; import 'dde_button.dart'; @@ -137,60 +140,102 @@ class _MeWidgetState extends State { ), borderRadius: BorderRadius.circular(defaultBorderRadius), ), - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 1, vertical: 2), - child: ListView.builder( - itemBuilder: (context, index) => GestureDetector( - onTap: () { - setState(() { - _selected = _schemes[index].uuid; - }); - }, - child: MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (_) { - setState(() { - _hovering = _schemes[index].uuid; - }); - }, - child: Container( - color: _getItemBackgroundColor(index, _schemes[index].uuid), - child: Padding( - padding: const EdgeInsets.only(left: 6, right: 12.0), - child: DefaultTextStyle( - style: context.t.textTheme.bodyText2!.copyWith( - color: _selected == _schemes[index].uuid ? Colors.white : null, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(_schemes[index].name ?? ''), - Row( - children: [ - Text('${_schemes[index].downloads ?? 0}'.padLeft(4)), - Icon( - Icons.file_download, - size: 18, - ), - Text('${_schemes[index].likes ?? 0}'.padLeft(4)), - Icon(_schemes[index].liked == true ? Icons.thumb_up : Icons.thumb_up_off_alt, - size: 17), - ] - .map((e) => Padding( - padding: const EdgeInsets.only(right: 3), - child: e, - )) - .toList(), + child: Column( + children: [ + Flexible( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 1, vertical: 2), + child: ListView.builder( + itemBuilder: (context, index) => GestureDetector( + onTap: () { + setState(() { + _selected = _schemes[index].uuid; + }); + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) { + setState(() { + _hovering = _schemes[index].uuid; + }); + }, + child: Container( + color: _getItemBackgroundColor(index, _schemes[index].uuid), + child: Padding( + padding: const EdgeInsets.only(left: 6, right: 12.0), + child: DefaultTextStyle( + style: context.t.textTheme.bodyText2!.copyWith( + color: _selected == _schemes[index].uuid ? Colors.white : null, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(_schemes[index].name ?? ''), + Row( + children: [ + SizedBox( + width: 50, + child: Align( + alignment: Alignment.centerRight, + child: AutoSizeText( + '${numeral(_schemes[index].downloads ?? 0, fractionDigits: 1)}' + .padLeft(5), + ), + ), + ), + Icon( + Icons.file_download, + size: 18, + ), + SizedBox( + width: 50, + child: Align( + alignment: Alignment.centerRight, + child: AutoSizeText( + '${numeral(_schemes[index].likes ?? 0, fractionDigits: 1)}'.padLeft(5), + ), + ), + ), + Icon(_schemes[index].liked == true ? Icons.thumb_up : Icons.thumb_up_off_alt, + size: 17), + ] + .map((e) => Padding( + padding: const EdgeInsets.only(right: 3), + child: e, + )) + .toList(), + ), + ], + ), ), - ], + ), ), ), ), + itemCount: _schemes.length, ), ), ), - itemCount: _schemes.length, - ), + Divider(thickness: .5), + Flexible( + child: Padding( + padding: const EdgeInsets.only(left: 8), + child: MdPreview( + text: _schemes.firstWhereOrNull((e) => e.uuid == _selected)?.description ?? '', + widgetImage: (imageUrl) => CachedNetworkImage( + imageUrl: imageUrl, + placeholder: (context, url) => const SizedBox( + width: double.infinity, + height: 300, + child: Center(child: CircularProgressIndicator()), + ), + errorWidget: (context, url, error) => const Icon(Icons.error), + ), + onCodeCopied: () {}, + ), + ), + ) + ], ), ), ), diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 410e658..09381ad 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -23,25 +23,26 @@ environment: dependencies: flutter: sdk: flutter - cupertino_icons: ^1.0.2 - window_manager: ^0.0.5 + cupertino_icons: ^1.0.4 + window_manager: ^0.1.3 localstorage: ^4.0.0+1 - shared_preferences: ^2.0.7 - xdg_directories: 0.2.0 - gsettings: 0.2.0 - provider: ^6.0.0 - package_info_plus: ^1.0.6 + shared_preferences: ^2.0.12 + xdg_directories: ^0.2.0 + gsettings: 0.2.3 + provider: ^6.0.2 + package_info_plus: ^1.3.0 easy_localization: ^3.0.0 glass_kit: ^2.0.1 rect_getter: ^1.0.0 - path_provider: ^2.0.5 + path_provider: ^2.0.8 uuid: ^3.0.5 adaptive_scrollbar: ^2.1.0 flutter_platform_alert: ^0.2.1 cached_network_image: ^3.2.0 - url_launcher: ^6.0.17 + url_launcher: ^6.0.18 flutter_login: ^3.1.0 auto_size_text: ^3.0.0 + numeral: ^1.2.5 markdown_editor_ot: path: 3rd_party/markdown_editor_ot cherry_toast: @@ -54,8 +55,8 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: 2.1.2 - source_gen: 1.1.0 + build_runner: 2.1.7 + source_gen: 1.2.1 # For information on the generic Dart part of this file, see the diff --git a/app/resources/langs/en.json b/app/resources/langs/en.json index e6ae3be..dabc42f 100644 --- a/app/resources/langs/en.json +++ b/app/resources/langs/en.json @@ -20,7 +20,13 @@ "tip": "Display help documentation" }, "market": { - "title": "Scheme market" + "title": "Scheme market", + "sort_types": { + "recommend": "Recommend", + "updated": "updated", + "likes": "likes", + "downloads": "downloads" + } }, "local_manager": { "title": "Local scheme management", diff --git a/app/resources/langs/zh-CN.json b/app/resources/langs/zh-CN.json index 467bd79..d842369 100644 --- a/app/resources/langs/zh-CN.json +++ b/app/resources/langs/zh-CN.json @@ -20,7 +20,13 @@ "tip": "显示帮助文档" }, "market": { - "title": "方案市场" + "title": "方案市场", + "sort_types": { + "recommend": "推荐", + "updated": "最近更新", + "likes": "点赞数", + "downloads": "下载量" + } }, "local_manager": { "title": "本地方案管理", From 1a4530ba55f5e55a2954c4621d516955a8d4cf55 Mon Sep 17 00:00:00 2001 From: debuggerx Date: Wed, 12 Jan 2022 16:23:41 +0800 Subject: [PATCH 6/6] wip: update docker-compose file and start up script. --- api/Dockerfile | 1 + api/config/default.yaml | 19 +++++---- api/docker-compose.yml | 45 ++++++++++++++-------- .../src/routes/controllers/scheme_controllers.dart | 5 ++- api/start.sh | 11 +++--- 5 files changed, 50 insertions(+), 31 deletions(-) diff --git a/api/Dockerfile b/api/Dockerfile index d7f0050..2d8d4cb 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -5,6 +5,7 @@ ENV ANGEL_ENV=production COPY ./ ./ RUN pub get RUN dart compile exe bin/prod.dart -o /server +ENTRYPOINT ["dart", "bin/migrate.dart", "up"] FROM scratch WORKDIR /app diff --git a/api/config/default.yaml b/api/config/default.yaml index 8ee46d8..f527d63 100644 --- a/api/config/default.yaml +++ b/api/config/default.yaml @@ -2,12 +2,17 @@ host: 127.0.0.1 port: 3000 postgres: - host: localhost + host: db port: 5432 - database_name: appdb - username: appuser - password: App1970# - useSSL: false - time_zone: UTC + database_name: gesture_manager + username: postgres + password: gesture_manager_secret + use_ssl: false + time_zone: Asia/Shanghai + +redis: + host: kv + port: 6379 + jwt_secret: "OvA9SBLnncot8gFHvt8Gh1qkQ1ptGIQW" -password_salt: "test" \ No newline at end of file +password_salt: "Z5b84rrgsKmfNFNRExAC4BCJe5aZPdJq" \ No newline at end of file diff --git a/api/docker-compose.yml b/api/docker-compose.yml index 2dbb198..0938656 100644 --- a/api/docker-compose.yml +++ b/api/docker-compose.yml @@ -1,34 +1,45 @@ -version: "2.1" +version: "2.4" services: - mysql: - image: postgres:latest + kv: + image: redis:alpine + restart: always + container_name: dgm_redis + ports: + - "6379:6379" + networks: + - dgm_api_default + + db: + image: postgres:alpine restart: always container_name: dgm_postgres - command: --default-authentication-plugin=mysql_native_password environment: - MYSQL_ROOT_PASSWORD: Dx@8917312 - MYSQL_DATABASE: piwigo - MYSQL_USER: debuggerx - MYSQL_PASSWORD: dx8917312 + POSTGRES_DB: gesture_manager + POSTGRES_PASSWORD: gesture_manager_secret volumes: - - /mnt/hd500/db:/var/lib/mysql + - ../db_data:/var/lib/postgresql/data ports: - "5432:5432" networks: - - mynet + - dgm_api_default api: build: . + image: dgm_api_image container_name: dgm_api - links: - - dgm_postgres ports: - - 8888:8888 + - 3000:3000 + restart: always + depends_on: + - db + - kv + links: + - db:db + - kv:kv networks: - - mynet - restart: unless-stopped + - dgm_api_default networks: - mynet: - driver: bridge \ No newline at end of file + dgm_api_default: + name: dgm_api_default diff --git a/api/lib/src/routes/controllers/scheme_controllers.dart b/api/lib/src/routes/controllers/scheme_controllers.dart index cdf22ff..5b68c61 100644 --- a/api/lib/src/routes/controllers/scheme_controllers.dart +++ b/api/lib/src/routes/controllers/scheme_controllers.dart @@ -221,8 +221,9 @@ Future configureServer(Angel app) async { for (var order in orders) { schemeQuery.orderBy(order, descending: true); } - schemeQuery.offset(page * pageSize); - schemeQuery.limit(pageSize + 1); + schemeQuery + ..offset(page * pageSize) + ..limit(pageSize + 1); return schemeQuery.get(req.queryExecutor).then((value) { var hasMore = value.length > pageSize; if (hasMore) value.removeLast(); diff --git a/api/start.sh b/api/start.sh index 89f09ef..f084689 100644 --- a/api/start.sh +++ b/api/start.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash -docker build -t dgm_api_image . -docker image prune -f --filter label=stage=dart_builder -docker rm -f dgm_api -docker run -d --restart=always --name dgm_api -p 3000:3000 dgm_api_image -docker image prune -f +docker-compose build +docker-compose up -d +MIGRATION_IMAGE="$(docker image ls --filter label=stage=dart_builder -q)" +docker run --name=dgm_api_migrate --network dgm_api_default "$MIGRATION_IMAGE" +docker rm "$(docker ps -a --filter name=dgm_api_migrate -q)" +docker image prune -f --filter label=stage=dart_builder \ No newline at end of file