wip: me panel.
This commit is contained in:
Vendored
+12
@@ -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.
|
||||||
+230
@@ -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`.
|
||||||
Vendored
+30
@@ -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.
|
||||||
Vendored
+15
@@ -0,0 +1,15 @@
|
|||||||
|
# Angel3 ORM
|
||||||
|
|
||||||
|

|
||||||
|
[](https://dart.dev/null-safety)
|
||||||
|
[](https://gitter.im/angel_dart/discussion)
|
||||||
|
[](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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
include: package:lints/recommended.yaml
|
||||||
+15
@@ -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';
|
||||||
@@ -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 }
|
||||||
+675
@@ -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<T> {
|
||||||
|
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<T extends num>
|
||||||
|
extends SqlExpressionBuilder<T> {
|
||||||
|
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<T> values) {
|
||||||
|
_raw = 'IN (' + values.join(', ') + ')';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void isNotIn(Iterable<T> values) {
|
||||||
|
_raw = 'NOT IN (' + values.join(', ') + ')';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EnumSqlExpressionBuilder<T> extends SqlExpressionBuilder<T> {
|
||||||
|
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<T> values) {
|
||||||
|
_raw = 'IN (' + values.map(_getValue).join(', ') + ')';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void isNotIn(Iterable<T> values) {
|
||||||
|
_raw = 'NOT IN (' + values.map(_getValue).join(', ') + ')';
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StringSqlExpressionBuilder extends SqlExpressionBuilder<String> {
|
||||||
|
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<String> values) {
|
||||||
|
return 'IN (' +
|
||||||
|
values.map((v) {
|
||||||
|
var name = query.reserveName('${columnName}_in_value');
|
||||||
|
query.substitutionValues[name] = v;
|
||||||
|
return '@$name';
|
||||||
|
}).join(', ') +
|
||||||
|
')';
|
||||||
|
}
|
||||||
|
|
||||||
|
void isIn(Iterable<String> values) {
|
||||||
|
_raw = _in(values);
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void isNotIn(Iterable<String> values) {
|
||||||
|
_raw = 'NOT ' + _in(values);
|
||||||
|
_hasValue = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BooleanSqlExpressionBuilder extends SqlExpressionBuilder<bool> {
|
||||||
|
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<DateTime> {
|
||||||
|
NumericSqlExpressionBuilder<int>? _year,
|
||||||
|
_month,
|
||||||
|
_day,
|
||||||
|
_hour,
|
||||||
|
_minute,
|
||||||
|
_second;
|
||||||
|
|
||||||
|
String? _raw;
|
||||||
|
|
||||||
|
DateTimeSqlExpressionBuilder(Query query, String columnName)
|
||||||
|
: super(query, columnName);
|
||||||
|
|
||||||
|
NumericSqlExpressionBuilder<int> get year =>
|
||||||
|
_year ??= NumericSqlExpressionBuilder(query, 'year');
|
||||||
|
NumericSqlExpressionBuilder<int> get month =>
|
||||||
|
_month ??= NumericSqlExpressionBuilder(query, 'month');
|
||||||
|
NumericSqlExpressionBuilder<int> get day =>
|
||||||
|
_day ??= NumericSqlExpressionBuilder(query, 'day');
|
||||||
|
NumericSqlExpressionBuilder<int> get hour =>
|
||||||
|
_hour ??= NumericSqlExpressionBuilder(query, 'hour');
|
||||||
|
NumericSqlExpressionBuilder<int> get minute =>
|
||||||
|
_minute ??= NumericSqlExpressionBuilder(query, 'minute');
|
||||||
|
NumericSqlExpressionBuilder<int> 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<DateTime> values) {
|
||||||
|
_raw = '$columnName IN (' +
|
||||||
|
values.map(dateYmdHms.format).map((s) => '$s').join(', ') +
|
||||||
|
')';
|
||||||
|
}
|
||||||
|
|
||||||
|
void isNotIn(Iterable<DateTime> 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 = <String>[];
|
||||||
|
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<T, K> extends SqlExpressionBuilder<T> {
|
||||||
|
final List<JsonSqlExpressionBuilderProperty> _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<Map, String> {
|
||||||
|
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<List, int> {
|
||||||
|
ListSqlExpressionBuilder(Query query, String columnName)
|
||||||
|
: super(query, columnName);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<dynamic>? _encodeValue(List<dynamic>? 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 extends SqlExpressionBuilder?>(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<double>? get asDouble {
|
||||||
|
return _set(
|
||||||
|
() => NumericSqlExpressionBuilder<double>(builder.query, nameString));
|
||||||
|
}
|
||||||
|
|
||||||
|
NumericSqlExpressionBuilder<int>? get asInt {
|
||||||
|
return _set(
|
||||||
|
() => NumericSqlExpressionBuilder<int>(builder.query, nameString));
|
||||||
|
}
|
||||||
|
|
||||||
|
MapSqlExpressionBuilder? get asMap {
|
||||||
|
return _set(() => MapSqlExpressionBuilder(builder.query, nameString));
|
||||||
|
}
|
||||||
|
|
||||||
|
ListSqlExpressionBuilder? get asList {
|
||||||
|
return _set(() => ListSqlExpressionBuilder(builder.query, nameString));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String> 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<String>? 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import 'builder.dart';
|
||||||
|
|
||||||
|
class JoinOn {
|
||||||
|
final SqlExpressionBuilder key;
|
||||||
|
final SqlExpressionBuilder value;
|
||||||
|
|
||||||
|
JoinOn(this.key, this.value);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import 'query_values.dart';
|
||||||
|
|
||||||
|
/// A [QueryValues] implementation that simply writes to a [Map].
|
||||||
|
class MapQueryValues extends QueryValues {
|
||||||
|
final Map<String, dynamic> values = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toMap() => values;
|
||||||
|
}
|
||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
const List<String> 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);
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
}
|
||||||
+425
@@ -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<T, Where extends QueryWhere> extends QueryBase<T> {
|
||||||
|
final _log = Logger('Query');
|
||||||
|
|
||||||
|
final List<JoinBuilder> _joins = [];
|
||||||
|
final Map<String, int> _names = {};
|
||||||
|
final List<OrderBy> _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<String, String> expressions = {};
|
||||||
|
|
||||||
|
String? _crossJoin, _groupBy;
|
||||||
|
int? _limit, _offset;
|
||||||
|
|
||||||
|
Query({this.parent});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> 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<String> 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<String> trampoline) {
|
||||||
|
var i = _joins.length;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
var a = 'a$i';
|
||||||
|
if (trampoline.add(a)) {
|
||||||
|
return a;
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String Function() _compileJoin(tableName, Set<String> 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<String>? trampoline,
|
||||||
|
String? alias,
|
||||||
|
JoinType type,
|
||||||
|
String localKey,
|
||||||
|
String foreignKey,
|
||||||
|
String op,
|
||||||
|
List<String> additionalFields) {
|
||||||
|
trampoline ??= <String>{};
|
||||||
|
|
||||||
|
// 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<String> additionalFields = const [],
|
||||||
|
Set<String>? 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<String> additionalFields = const [],
|
||||||
|
Set<String>? 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<String> additionalFields = const [],
|
||||||
|
Set<String>? 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<String> additionalFields = const [],
|
||||||
|
Set<String>? 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<String> additionalFields = const [],
|
||||||
|
Set<String>? trampoline,
|
||||||
|
String? alias}) {
|
||||||
|
_makeJoin(tableName, trampoline, alias, JoinType.self, localKey, foreignKey, op,
|
||||||
|
additionalFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String compile(Set<String> 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<String> f;
|
||||||
|
|
||||||
|
var compiledJoins = <JoinBuilder, String?>{};
|
||||||
|
|
||||||
|
//if (fields == null) {
|
||||||
|
if (fields.isEmpty) {
|
||||||
|
f = ['*'];
|
||||||
|
} else {
|
||||||
|
f = List<String>.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<Optional<T>> getOne(QueryExecutor executor) {
|
||||||
|
//limit(1);
|
||||||
|
return super.getOne(executor);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<T>> 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<Optional<T>> deleteOne(QueryExecutor executor) {
|
||||||
|
return delete(executor).then((it) =>
|
||||||
|
it.isEmpty == true ? Optional.empty() : Optional.ofNullable(it.first));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Optional<T>> 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<List<T>> 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<Optional<T>> updateOne(QueryExecutor executor) {
|
||||||
|
return update(executor).then(
|
||||||
|
(it) => it.isEmpty ? Optional.empty() : Optional.ofNullable(it.first));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<T> {
|
||||||
|
/// Casts to perform when querying the database.
|
||||||
|
Map<String, String> get casts => {};
|
||||||
|
|
||||||
|
/// `AS` aliases to inject into the query, if any.
|
||||||
|
Map<String, String> aliases = {};
|
||||||
|
|
||||||
|
/// Values to insert into a prepared statement.
|
||||||
|
final Map<String, dynamic> 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<String> 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<String> trampoline,
|
||||||
|
{bool includeTableName = false,
|
||||||
|
String preamble = '',
|
||||||
|
bool withFields = true});
|
||||||
|
|
||||||
|
Optional<T> deserialize(List row);
|
||||||
|
|
||||||
|
List<T> deserializeList(List<List<dynamic>> it) {
|
||||||
|
var optResult = it.map(deserialize).toList();
|
||||||
|
var result = <T>[];
|
||||||
|
optResult.forEach((element) {
|
||||||
|
element.ifPresent((item) {
|
||||||
|
result.add(item);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<T>> 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<Optional<T>> 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<T> union(QueryBase<T> other) {
|
||||||
|
return Union(this, other);
|
||||||
|
}
|
||||||
|
|
||||||
|
Union<T> unionAll(QueryBase<T> other) {
|
||||||
|
return Union(this, other, all: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<List<List>> query(
|
||||||
|
String tableName, String query, Map<String, dynamic> substitutionValues,
|
||||||
|
[List<String> 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<T> transaction<T>(FutureOr<T> Function(QueryExecutor) f);
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import 'query.dart';
|
||||||
|
|
||||||
|
abstract class QueryValues {
|
||||||
|
Map<String, String> get casts => {};
|
||||||
|
|
||||||
|
Map<String, dynamic> 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<String, dynamic>.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import 'builder.dart';
|
||||||
|
|
||||||
|
/// Builds a SQL `WHERE` clause.
|
||||||
|
abstract class QueryWhere {
|
||||||
|
final Set<QueryWhere> _and = {};
|
||||||
|
final Set<QueryWhere> _not = {};
|
||||||
|
final Set<QueryWhere> _or = {};
|
||||||
|
final Set<String> _raw = {};
|
||||||
|
|
||||||
|
Iterable<SqlExpressionBuilder> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+91
@@ -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);
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
import 'query_base.dart';
|
||||||
|
import 'package:optional/optional.dart';
|
||||||
|
|
||||||
|
/// Represents the `UNION` of two subqueries.
|
||||||
|
class Union<T> extends QueryBase<T> {
|
||||||
|
/// The subject(s) of this binary operation.
|
||||||
|
final QueryBase<T> 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<String> get fields => left.fields;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Optional<T> deserialize(List row) => left.deserialize(row);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String compile(Set<String> trampoline,
|
||||||
|
{bool includeTableName = false,
|
||||||
|
String? preamble,
|
||||||
|
bool withFields = true}) {
|
||||||
|
var selector = all == true ? 'UNION ALL' : 'UNION';
|
||||||
|
var t1 = Set<String>.from(trampoline);
|
||||||
|
var t2 = Set<String>.from(trampoline);
|
||||||
|
return '(${left.compile(t1, includeTableName: includeTableName)}) $selector (${right.compile(t2, includeTableName: includeTableName)})';
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
import 'package:charcode/ascii.dart';
|
||||||
|
|
||||||
|
bool isAscii(int ch) => ch >= $nul && ch <= $del;
|
||||||
+21
@@ -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
|
||||||
@@ -5,6 +5,8 @@ import 'package:angel3_migration_runner/postgres.dart';
|
|||||||
import 'package:angel3_orm_postgres/angel3_orm_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/models.dart';
|
||||||
import 'package:dde_gesture_manager_api/src/config/plugins/orm.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:file/local.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
@@ -29,6 +31,8 @@ void main(List<String> args) async {
|
|||||||
UserMigration(),
|
UserMigration(),
|
||||||
UserSeed(),
|
UserSeed(),
|
||||||
SchemeMigration(),
|
SchemeMigration(),
|
||||||
|
DownloadHistoryMigration(),
|
||||||
|
LikeRecordMigration(),
|
||||||
]);
|
]);
|
||||||
await runMigrations(migrationRunner, args);
|
await runMigrations(migrationRunner, args);
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-1
@@ -31,10 +31,17 @@ class SchemeApis {
|
|||||||
|
|
||||||
String get upload => [path, 'upload'].joinPath();
|
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 = {
|
final _paramsMap = {
|
||||||
|
'BoolParam': BoolParam.nameOnRoute,
|
||||||
'IntParam': IntParam.nameOnRoute,
|
'IntParam': IntParam.nameOnRoute,
|
||||||
'DoubleParam': DoubleParam.nameOnRoute,
|
'DoubleParam': DoubleParam.nameOnRoute,
|
||||||
'StringParam': StringParam.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 {
|
class IntParam {
|
||||||
final int val;
|
final int val;
|
||||||
String? name;
|
String? name;
|
||||||
@@ -94,6 +113,10 @@ class StringParam {
|
|||||||
String toString() => name == null ? val.toString() : ':$name';
|
String toString() => name == null ? val.toString() : ':$name';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension BoolParamExt on bool {
|
||||||
|
BoolParam get param => BoolParam(this);
|
||||||
|
}
|
||||||
|
|
||||||
extension IntParamExt on int {
|
extension IntParamExt on int {
|
||||||
IntParam get param => IntParam(this);
|
IntParam get param => IntParam(this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -33,3 +33,91 @@ abstract class _Scheme extends BaseModel {
|
|||||||
@DefaultsTo([])
|
@DefaultsTo([])
|
||||||
List? get gestures;
|
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<String, dynamic>? 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,
|
||||||
|
);
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ Future configureServer(Angel app) async {
|
|||||||
chain(
|
chain(
|
||||||
[
|
[
|
||||||
jwtMiddleware(),
|
jwtMiddleware(),
|
||||||
(req, res) => req.user.blocked == false ? res.noContent() : res.forbidden(),
|
(req, res) => req.user!.blocked == false ? res.noContent() : res.forbidden(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -48,5 +48,11 @@ extension RedisClient on RequestContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension JWTUserInstance on RequestContext {
|
extension JWTUserInstance on RequestContext {
|
||||||
User get user => container!.make<User>();
|
User? get user {
|
||||||
|
try {
|
||||||
|
return container!.make<User>();
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import 'package:angel3_framework/angel3_framework.dart';
|
|||||||
import 'package:dde_gesture_manager_api/models.dart';
|
import 'package:dde_gesture_manager_api/models.dart';
|
||||||
import '../controllers/controller_extensions.dart';
|
import '../controllers/controller_extensions.dart';
|
||||||
|
|
||||||
RequestHandler jwtMiddleware() {
|
RequestHandler jwtMiddleware({ignoreError = false}) {
|
||||||
return (RequestContext req, ResponseContext res, {bool throwError = true}) async {
|
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) {
|
if (throwError) {
|
||||||
res.forbidden();
|
res.forbidden();
|
||||||
}
|
}
|
||||||
@@ -18,17 +19,22 @@ RequestHandler jwtMiddleware() {
|
|||||||
if (reqContainer.has<User>() || req.method == 'OPTIONS') {
|
if (reqContainer.has<User>() || req.method == 'OPTIONS') {
|
||||||
return true;
|
return true;
|
||||||
} else if (reqContainer.has<Future<User>>()) {
|
} else if (reqContainer.has<Future<User>>()) {
|
||||||
User user = await reqContainer.makeAsync<User>();
|
try {
|
||||||
var authToken = req.container!.make<AuthToken>();
|
User user = await reqContainer.makeAsync<User>();
|
||||||
if (user.secret(req.app!.configuration['password_salt']) != authToken.payload[UserFields.password]) {
|
var authToken = req.container!.make<AuthToken>();
|
||||||
return _reject(res);
|
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;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return _reject(res);
|
return _reject(res, ignoreError);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return _reject(res);
|
return _reject(res, ignoreError);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:angel3_framework/angel3_framework.dart';
|
import 'package:angel3_framework/angel3_framework.dart';
|
||||||
import 'package:dde_gesture_manager_api/apis.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/models/scheme.dart';
|
||||||
import 'package:dde_gesture_manager_api/src/routes/controllers/middlewares.dart';
|
import 'package:dde_gesture_manager_api/src/routes/controllers/middlewares.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
@@ -20,16 +22,18 @@ Future configureServer(Angel app) async {
|
|||||||
var scheme = SchemeSerializer.fromMap(req.bodyAsMap);
|
var scheme = SchemeSerializer.fromMap(req.bodyAsMap);
|
||||||
var schemeQuery = SchemeQuery();
|
var schemeQuery = SchemeQuery();
|
||||||
schemeQuery.where!.uuid.equals(scheme.uuid!);
|
schemeQuery.where!.uuid.equals(scheme.uuid!);
|
||||||
var one = await schemeQuery.getOne(req.queryExecutor);
|
req.queryExecutor.transaction((tx) async {
|
||||||
schemeQuery = SchemeQuery();
|
var one = await schemeQuery.getOne(tx);
|
||||||
schemeQuery.values.copyFrom(scheme);
|
schemeQuery = SchemeQuery();
|
||||||
schemeQuery.values.uid = int.parse(req.user.id!);
|
schemeQuery.values.copyFrom(scheme);
|
||||||
if (one.isEmpty) {
|
schemeQuery.values.uid = req.user!.idAsInt;
|
||||||
await schemeQuery.insert(req.queryExecutor);
|
if (one.isEmpty) {
|
||||||
} else {
|
return await schemeQuery.insert(tx);
|
||||||
schemeQuery.whereId = int.parse(one.value.id!);
|
} else {
|
||||||
await schemeQuery.updateOne(req.queryExecutor);
|
schemeQuery.whereId = one.value.idAsInt;
|
||||||
}
|
return await schemeQuery.updateOne(tx);
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_log.severe(e);
|
_log.severe(e);
|
||||||
return res.unProcessableEntity();
|
return res.unProcessableEntity();
|
||||||
@@ -40,21 +44,145 @@ Future configureServer(Angel app) async {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
app.get(
|
app.post(
|
||||||
Apis.scheme.userUploads,
|
Apis.scheme.markAsShared.route,
|
||||||
chain(
|
chain(
|
||||||
[
|
[
|
||||||
jwtMiddleware(),
|
jwtMiddleware(),
|
||||||
(req, res) async {
|
(req, res) async {
|
||||||
|
var schemeId = req.params['schemeId'];
|
||||||
var schemeQuery = SchemeQuery();
|
var schemeQuery = SchemeQuery();
|
||||||
schemeQuery.where!.uid.equals(int.parse(req.user.id!));
|
schemeQuery.where!.uuid.equals(schemeId);
|
||||||
schemeQuery.orderBy(SchemeFields.updatedAt, descending: true);
|
schemeQuery.values.shared = true;
|
||||||
return schemeQuery.get(req.queryExecutor).then((value) => value.map((e) => {
|
await schemeQuery.updateOne(req.queryExecutor);
|
||||||
'name': e.name,
|
return res.noContent();
|
||||||
'description': e.description,
|
|
||||||
}).toList());
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
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(
|
||||||
|
[
|
||||||
|
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<String, dynamic> 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?.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<String, dynamic> 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();
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
+7
-5
@@ -8,8 +8,7 @@ dependencies:
|
|||||||
angel3_auth: ^4.0.0
|
angel3_auth: ^4.0.0
|
||||||
angel3_configuration: ^4.1.0
|
angel3_configuration: ^4.1.0
|
||||||
angel3_framework: ^4.2.0
|
angel3_framework: ^4.2.0
|
||||||
angel3_migration: ^4.0.0
|
angel3_migration: ^4.0.3
|
||||||
angel3_orm: ^4.0.0
|
|
||||||
angel3_orm_postgres: ^3.0.0
|
angel3_orm_postgres: ^3.0.0
|
||||||
angel3_serialize: ^4.1.0
|
angel3_serialize: ^4.1.0
|
||||||
angel3_production: ^3.1.0
|
angel3_production: ^3.1.0
|
||||||
@@ -21,13 +20,15 @@ dependencies:
|
|||||||
yaml: ^3.1.0
|
yaml: ^3.1.0
|
||||||
mailer: ^5.0.2
|
mailer: ^5.0.2
|
||||||
uuid: ^3.0.5
|
uuid: ^3.0.5
|
||||||
|
angel3_orm:
|
||||||
|
path: 3rd_party/angel3_orm
|
||||||
neat_cache:
|
neat_cache:
|
||||||
path: 3rd_party/neat_cache
|
path: 3rd_party/neat_cache
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
angel3_hot: ^4.2.0
|
angel3_hot: ^4.2.0
|
||||||
angel3_jinja: ^2.0.1
|
angel3_jinja: ^2.0.1
|
||||||
angel3_migration_runner: ^4.0.0
|
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_serialize_generator: ^4.2.0
|
||||||
angel3_test: ^4.0.0
|
angel3_test: ^4.0.0
|
||||||
build_runner: ^2.0.3
|
build_runner: ^2.0.3
|
||||||
@@ -35,5 +36,6 @@ dev_dependencies:
|
|||||||
test: ^1.17.5
|
test: ^1.17.5
|
||||||
lints: ^1.0.0
|
lints: ^1.0.0
|
||||||
|
|
||||||
|
dependency_overrides:
|
||||||
|
angel3_orm:
|
||||||
|
path: 3rd_party/angel3_orm
|
||||||
|
|||||||
+18
-4
@@ -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/models/scheme.dart' as AppScheme;
|
||||||
import 'package:dde_gesture_manager/utils/helper.dart';
|
import 'package:dde_gesture_manager/utils/helper.dart';
|
||||||
import 'package:dde_gesture_manager/utils/notificator.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/apis.dart';
|
||||||
import 'package:dde_gesture_manager_api/models.dart';
|
import 'package:dde_gesture_manager_api/models.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
@@ -85,7 +86,7 @@ class Api {
|
|||||||
if (ignoreErrorHandle)
|
if (ignoreErrorHandle)
|
||||||
throw e;
|
throw e;
|
||||||
else
|
else
|
||||||
return _handleHttpError(e);
|
_handleHttpError(e);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ class Api {
|
|||||||
if (ignoreErrorHandle)
|
if (ignoreErrorHandle)
|
||||||
throw e;
|
throw e;
|
||||||
else
|
else
|
||||||
return _handleHttpError(e);
|
_handleHttpError(e);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -158,6 +159,19 @@ class Api {
|
|||||||
),
|
),
|
||||||
).then((value) => value == HttpStatus.noContent);
|
).then((value) => value == HttpStatus.noContent);
|
||||||
|
|
||||||
static Future<List<Scheme>> userUploads() =>
|
static Future<List<SimpleSchemeTransMetaData>> userSchemes({required SchemeListType type}) =>
|
||||||
_get(Apis.scheme.userUploads, listRespBuilderWrap(SchemeSerializer.fromMap));
|
_get(Apis.scheme.user(type: type.name.param), listRespBuilderWrap(SimpleSchemeTransMetaDataSerializer.fromMap));
|
||||||
|
|
||||||
|
static Future<bool> 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<SchemeForDownload> downloadScheme({required String schemeId}) => _get(
|
||||||
|
Apis.scheme.download(schemeId: schemeId.param),
|
||||||
|
SchemeForDownloadSerializer.fromMap,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-9
@@ -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/themes/light.dart';
|
||||||
import 'package:dde_gesture_manager/utils/helper.dart';
|
import 'package:dde_gesture_manager/utils/helper.dart';
|
||||||
import 'package:dde_gesture_manager/utils/init.dart';
|
import 'package:dde_gesture_manager/utils/init.dart';
|
||||||
|
import 'package:dde_gesture_manager/utils/simple_throttle.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'pages/home.dart';
|
import 'pages/home.dart';
|
||||||
@@ -71,15 +72,7 @@ class MyApp extends StatelessWidget {
|
|||||||
firstChild: Builder(builder: (context) {
|
firstChild: Builder(builder: (context) {
|
||||||
Future.microtask(() {
|
Future.microtask(() {
|
||||||
initEvents(context);
|
initEvents(context);
|
||||||
if (H().lastCheckAuthStatusTime != null &&
|
SimpleThrottle.throttledFunc(_checkAuthStatus, timeout: const Duration(minutes: 5))?.call(context);
|
||||||
H().lastCheckAuthStatusTime!.difference(DateTime.now()) < Duration(minutes: 10)) return;
|
|
||||||
if (context.read<ConfigsProvider>().accessToken.notNull) {
|
|
||||||
Api.checkAuthStatus().then((value) {
|
|
||||||
if (!value) context.read<ConfigsProvider>().setProps(email: '', accessToken: '');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
H().lastCheckAuthStatusTime = DateTime.now();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return Container();
|
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<ConfigsProvider>().accessToken.notNull) {
|
||||||
|
Api.checkAuthStatus().then((value) {
|
||||||
|
if (!value) context.read<ConfigsProvider>().setProps(email: '', accessToken: '');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
H().lastCheckAuthStatusTime = DateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import 'package:dde_gesture_manager/builder/provider_annotation.dart';
|
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()
|
@ProviderModel()
|
||||||
class ContentLayout {
|
class ContentLayout {
|
||||||
@@ -9,7 +12,7 @@ class ContentLayout {
|
|||||||
bool? marketOrMeOpened;
|
bool? marketOrMeOpened;
|
||||||
|
|
||||||
@ProviderModelProp()
|
@ProviderModelProp()
|
||||||
bool? currentIsMarket = true;
|
bool? currentIsMarket = H().sp.getString(SPKeys.accessToken).isNull;
|
||||||
|
|
||||||
bool get isMarket => currentIsMarket ?? true;
|
bool get isMarket => currentIsMarket ?? true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,8 +184,8 @@ class _LocalManagerState extends State<LocalManager> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(height: 5),
|
Padding(
|
||||||
Container(
|
padding: const EdgeInsets.only(top: 5),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class MarketOrMe extends StatelessWidget {
|
|||||||
Widget buildMeContent(BuildContext context) {
|
Widget buildMeContent(BuildContext context) {
|
||||||
var accessToken = context.watch<ConfigsProvider>().accessToken;
|
var accessToken = context.watch<ConfigsProvider>().accessToken;
|
||||||
if (accessToken.isNull) return LoginWidget();
|
if (accessToken.isNull) return LoginWidget();
|
||||||
return MeWidget();
|
return Expanded(child: MeWidget());
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildMarketContent(BuildContext context) {
|
Widget buildMarketContent(BuildContext context) {
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ Future<void> initEvents(BuildContext context) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_updateChecked)
|
if (!_updateChecked) {
|
||||||
|
_updateChecked = true;
|
||||||
Api.checkAppVersion(ignoreErrorHandle: true).then((value) async {
|
Api.checkAppVersion(ignoreErrorHandle: true).then((value) async {
|
||||||
_updateChecked = true;
|
|
||||||
var info = await PackageInfo.fromPlatform();
|
var info = await PackageInfo.fromPlatform();
|
||||||
var _buildNumber = int.parse(info.buildNumber);
|
var _buildNumber = int.parse(info.buildNumber);
|
||||||
var _newVersionCode = value?.versionCode ?? 0;
|
var _newVersionCode = value?.versionCode ?? 0;
|
||||||
@@ -74,6 +74,7 @@ Future<void> initEvents(BuildContext context) async {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initConfigs() async {
|
Future<void> initConfigs() async {
|
||||||
|
|||||||
@@ -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 extends Function>(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);
|
||||||
|
}
|
||||||
@@ -141,6 +141,57 @@ class DButton extends StatefulWidget {
|
|||||||
message: LocaleKeys.operation_upload.tr(),
|
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({
|
factory DButton.dropdown({
|
||||||
Key? key,
|
Key? key,
|
||||||
width = 60.0,
|
width = 60.0,
|
||||||
|
|||||||
+189
-11
@@ -1,13 +1,23 @@
|
|||||||
import 'package:auto_size_text/auto_size_text.dart';
|
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/constants/constants.dart';
|
||||||
|
import 'package:dde_gesture_manager/extensions.dart';
|
||||||
import 'package:dde_gesture_manager/http/api.dart';
|
import 'package:dde_gesture_manager/http/api.dart';
|
||||||
import 'package:dde_gesture_manager/models/configs.provider.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:dde_gesture_manager_api/models.dart';
|
||||||
import 'package:flutter/material.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';
|
import 'dde_button.dart';
|
||||||
|
|
||||||
|
enum SchemeListType {
|
||||||
|
uploaded,
|
||||||
|
downloaded,
|
||||||
|
liked,
|
||||||
|
}
|
||||||
|
|
||||||
class MeWidget extends StatefulWidget {
|
class MeWidget extends StatefulWidget {
|
||||||
const MeWidget({Key? key}) : super(key: key);
|
const MeWidget({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@@ -16,23 +26,46 @@ class MeWidget extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MeWidgetState extends State<MeWidget> {
|
class _MeWidgetState extends State<MeWidget> {
|
||||||
List<Scheme> uploads = [];
|
List<SimpleSchemeTransMetaData> _schemes = [];
|
||||||
|
SchemeListType _type = SchemeListType.uploaded;
|
||||||
|
String? _selected;
|
||||||
|
String? _hovering;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
Api.userUploads().then((value) {
|
Api.userSchemes(type: _type).then((value) {
|
||||||
if (mounted)
|
if (mounted)
|
||||||
setState(() {
|
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<SettingsProvider>().currentActiveColor;
|
||||||
|
return _color;
|
||||||
|
}
|
||||||
|
|
||||||
|
_refreshList() {
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () {
|
||||||
|
Api.userSchemes(type: _type).then((value) {
|
||||||
|
if (mounted)
|
||||||
|
setState(() {
|
||||||
|
_schemes = value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
var currentSelectedScheme = _schemes.firstWhereOrNull((e) => e.uuid == _selected);
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 10),
|
padding: EdgeInsets.only(top: 10),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
@@ -54,12 +87,157 @@ class _MeWidgetState extends State<MeWidget> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text('我的上传'),
|
Padding(
|
||||||
Container(
|
padding: const EdgeInsets.only(top: 3, bottom: 2),
|
||||||
height: 400,
|
child: Row(
|
||||||
child: ListView.builder(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
itemBuilder: (context, index) => Text(uploads[index].name ?? ''),
|
children: [
|
||||||
itemCount: uploads.length,
|
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(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ dev_dependencies:
|
|||||||
build_runner: 2.1.2
|
build_runner: 2.1.2
|
||||||
source_gen: 1.1.0
|
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
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,10 @@
|
|||||||
"apply": "apply",
|
"apply": "apply",
|
||||||
"paste": "paste",
|
"paste": "paste",
|
||||||
"logout": "sign out",
|
"logout": "sign out",
|
||||||
"upload": "upload"
|
"upload": "upload",
|
||||||
|
"download": "download",
|
||||||
|
"share": "share to market",
|
||||||
|
"like": "like"
|
||||||
},
|
},
|
||||||
"str": {
|
"str": {
|
||||||
"null": "Null",
|
"null": "Null",
|
||||||
@@ -146,6 +149,11 @@
|
|||||||
"upload": {
|
"upload": {
|
||||||
"success": "Upload success ~",
|
"success": "Upload success ~",
|
||||||
"failed": "Upload failed.."
|
"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": {
|
"me": {
|
||||||
@@ -155,6 +163,11 @@
|
|||||||
"email_hint": "Please enter email",
|
"email_hint": "Please enter email",
|
||||||
"password_hint": "Please enter 8-16-bit password",
|
"password_hint": "Please enter 8-16-bit password",
|
||||||
"email_error_hint": "Please enter your vaild email"
|
"email_error_hint": "Please enter your vaild email"
|
||||||
|
},
|
||||||
|
"scheme_types": {
|
||||||
|
"uploaded": "Uploaded",
|
||||||
|
"downloaded": "Downloaded",
|
||||||
|
"liked": "Liked"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,7 +72,10 @@
|
|||||||
"apply": "应用",
|
"apply": "应用",
|
||||||
"paste": "粘贴",
|
"paste": "粘贴",
|
||||||
"logout": "退出登录",
|
"logout": "退出登录",
|
||||||
"upload": "上传"
|
"upload": "上传",
|
||||||
|
"download": "下载",
|
||||||
|
"share": "分享到市场",
|
||||||
|
"like": "点赞"
|
||||||
},
|
},
|
||||||
"str": {
|
"str": {
|
||||||
"null": "无",
|
"null": "无",
|
||||||
@@ -146,6 +149,11 @@
|
|||||||
"upload": {
|
"upload": {
|
||||||
"success": "上传成功~",
|
"success": "上传成功~",
|
||||||
"failed": "上传失败。。"
|
"failed": "上传失败。。"
|
||||||
|
},
|
||||||
|
"share": {
|
||||||
|
"title": "确定分享?",
|
||||||
|
"description": "分享后其他用户可以看到本方案并下载使用",
|
||||||
|
"success": "分享成功"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"me": {
|
"me": {
|
||||||
@@ -155,6 +163,11 @@
|
|||||||
"email_hint": "请输入邮箱",
|
"email_hint": "请输入邮箱",
|
||||||
"password_hint": "请输入8-16位密码",
|
"password_hint": "请输入8-16位密码",
|
||||||
"email_error_hint": "请输入正确的邮箱"
|
"email_error_hint": "请输入正确的邮箱"
|
||||||
|
},
|
||||||
|
"scheme_types": {
|
||||||
|
"uploaded": "我的上传",
|
||||||
|
"downloaded": "我的下载",
|
||||||
|
"liked": "我的点赞"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user