wip: me panel.

pull/6/head
DebuggerX 3 years ago
parent 048c54e080
commit 85a7d36fda

@ -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.

@ -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`.

@ -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.

@ -0,0 +1,15 @@
# Angel3 ORM
![Pub Version (including pre-releases)](https://img.shields.io/pub/v/angel3_orm?include_prereleases)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)
[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/master/packages/orm/angel_orm/LICENSE)
Runtime support for Angel3 ORM. Includes a clean, database-agnostic query builder and relationship/join support.
## Supported database
* PostgreSQL version 10, 11, 12, 13 and 14
* MySQL 8.0 or later
For documentation about the ORM, see [Developer Guide](https://angel3-docs.dukefirehawk.com/guides/orm)

@ -0,0 +1 @@
include: package:lints/recommended.yaml

@ -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 }

@ -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;
}

@ -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';
}

@ -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();
}
}

@ -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);
}

@ -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)})';
}
}

@ -0,0 +1,3 @@
import 'package:charcode/ascii.dart';
bool isAscii(int ch) => ch >= $nul && ch <= $del;

@ -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:dde_gesture_manager_api/models.dart';
import 'package:dde_gesture_manager_api/src/config/plugins/orm.dart';
import 'package:dde_gesture_manager_api/src/models/download_history.dart';
import 'package:dde_gesture_manager_api/src/models/like_record.dart';
import 'package:file/local.dart';
import 'package:logging/logging.dart';
@ -29,6 +31,8 @@ void main(List<String> args) async {
UserMigration(),
UserSeed(),
SchemeMigration(),
DownloadHistoryMigration(),
LikeRecordMigration(),
]);
await runMigrations(migrationRunner, args);
}

@ -31,10 +31,17 @@ class SchemeApis {
String get upload => [path, 'upload'].joinPath();
String get userUploads => [path, 'user', 'uploads'].joinPath();
String markAsShared({required StringParam schemeId}) => [path, 'mark_as_shared', schemeId].joinPath();
String user({required StringParam type}) => [path, 'user', type].joinPath();
String download({required StringParam schemeId}) => [path, 'download', schemeId].joinPath();
String like({required StringParam schemeId, required StringParam isLike}) => [path, 'like', schemeId, isLike].joinPath();
}
final _paramsMap = {
'BoolParam': BoolParam.nameOnRoute,
'IntParam': IntParam.nameOnRoute,
'DoubleParam': DoubleParam.nameOnRoute,
'StringParam': StringParam.nameOnRoute,
@ -58,6 +65,18 @@ extension RouteUrl on Function {
}
}
class BoolParam {
final bool val;
String? name;
BoolParam(this.val);
BoolParam.nameOnRoute(this.name) : val = true;
@override
String toString() => name == null ? val.toString() : 'bool:$name';
}
class IntParam {
final int val;
String? name;
@ -94,6 +113,10 @@ class StringParam {
String toString() => name == null ? val.toString() : ':$name';
}
extension BoolParamExt on bool {
BoolParam get param => BoolParam(this);
}
extension IntParamExt on int {
IntParam get param => IntParam(this);
}

@ -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([])
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(
[
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 {
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 '../controllers/controller_extensions.dart';
RequestHandler jwtMiddleware() {
RequestHandler jwtMiddleware({ignoreError = false}) {
return (RequestContext req, ResponseContext res, {bool throwError = true}) async {
bool _reject(ResponseContext res) {
bool _reject(ResponseContext res, [ignoreError = false]) {
if (ignoreError) return true;
if (throwError) {
res.forbidden();
}
@ -18,17 +19,22 @@ RequestHandler jwtMiddleware() {
if (reqContainer.has<User>() || req.method == 'OPTIONS') {
return true;
} else if (reqContainer.has<Future<User>>()) {
try {
User user = await reqContainer.makeAsync<User>();
var authToken = req.container!.make<AuthToken>();
if (user.secret(req.app!.configuration['password_salt']) != authToken.payload[UserFields.password]) {
return _reject(res);
return _reject(res, ignoreError);
}
} catch (e) {
if (ignoreError) return true;
rethrow;
}
return true;
} else {
return _reject(res);
return _reject(res, ignoreError);
}
} else {
return _reject(res);
return _reject(res, ignoreError);
}
};
}

@ -2,6 +2,8 @@ import 'dart:async';
import 'package:angel3_framework/angel3_framework.dart';
import 'package:dde_gesture_manager_api/apis.dart';
import 'package:dde_gesture_manager_api/src/models/download_history.dart';
import 'package:dde_gesture_manager_api/src/models/like_record.dart';
import 'package:dde_gesture_manager_api/src/models/scheme.dart';
import 'package:dde_gesture_manager_api/src/routes/controllers/middlewares.dart';
import 'package:logging/logging.dart';
@ -20,16 +22,18 @@ Future configureServer(Angel app) async {
var scheme = SchemeSerializer.fromMap(req.bodyAsMap);
var schemeQuery = SchemeQuery();
schemeQuery.where!.uuid.equals(scheme.uuid!);
var one = await schemeQuery.getOne(req.queryExecutor);
req.queryExecutor.transaction((tx) async {
var one = await schemeQuery.getOne(tx);
schemeQuery = SchemeQuery();
schemeQuery.values.copyFrom(scheme);
schemeQuery.values.uid = int.parse(req.user.id!);
schemeQuery.values.uid = req.user!.idAsInt;
if (one.isEmpty) {
await schemeQuery.insert(req.queryExecutor);
return await schemeQuery.insert(tx);
} else {
schemeQuery.whereId = int.parse(one.value.id!);
await schemeQuery.updateOne(req.queryExecutor);
schemeQuery.whereId = one.value.idAsInt;
return await schemeQuery.updateOne(tx);
}
});
} catch (e) {
_log.severe(e);
return res.unProcessableEntity();
@ -40,19 +44,143 @@ Future configureServer(Angel app) async {
),
);
app.post(
Apis.scheme.markAsShared.route,
chain(
[
jwtMiddleware(),
(req, res) async {
var schemeId = req.params['schemeId'];
var schemeQuery = SchemeQuery();
schemeQuery.where!.uuid.equals(schemeId);
schemeQuery.values.shared = true;
await schemeQuery.updateOne(req.queryExecutor);
return res.noContent();
},
],
),
);
app.get(
Apis.scheme.user.route,
chain(
[
jwtMiddleware(),
(req, res) async {
var schemeQuery = SimpleSchemeQuery();
var type = req.params['type'];
var likeRecordTableName = LikeRecordQuery().tableName;
schemeQuery.leftJoin(likeRecordTableName, SchemeFields.id, LikeRecordFields.schemeId, alias: 'lr');
switch (type) {
case 'uploaded':
schemeQuery.where!.uid.equals(req.user!.idAsInt);
break;
case 'downloaded':
var downloadHistoryTableName = DownloadHistoryQuery().tableName;
schemeQuery.leftJoin(downloadHistoryTableName, SchemeFields.id, DownloadHistoryFields.schemeId,
alias: 'dh');
schemeQuery.where!.raw('dh.${DownloadHistoryFields.uid} = ${req.user!.idAsInt}');
break;
case 'liked':
schemeQuery.where!.raw('lr.${LikeRecordFields.uid} = ${req.user!.idAsInt}');
schemeQuery.where!.raw('lr.${LikeRecordFields.liked} = true');
break;
default:
return res.unProcessableEntity();
}
schemeQuery.orderBy('${schemeQuery.tableName}.${SchemeFields.updatedAt}', descending: true);
return schemeQuery.get(req.queryExecutor).then((value) => value.map(transSimpleSchemeMetaData).toList());
},
],
),
);
app.get(
Apis.scheme.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.userUploads,
Apis.scheme.like.route,
chain(
[
jwtMiddleware(),
(req, res) async {
bool isLike = req.params['isLike'] == 'like';
bool needUpdate = true;
var schemeQuery = SchemeQuery();
schemeQuery.where!.uid.equals(int.parse(req.user.id!));
schemeQuery.orderBy(SchemeFields.updatedAt, descending: true);
return schemeQuery.get(req.queryExecutor).then((value) => value.map((e) => {
'name': e.name,
'description': e.description,
}).toList());
schemeQuery.where?.uuid.equals(req.params['schemeId']);
var optionalScheme = await schemeQuery.getOne(req.queryExecutor);
if (optionalScheme.isNotEmpty) {
var scheme = optionalScheme.value;
if (req.user != null) {
///
var likeRecordQuery = LikeRecordQuery();
likeRecordQuery.where?.uid.equals(req.user!.idAsInt);
likeRecordQuery.where?.schemeId.equals(scheme.idAsInt);
var likeRecordCheck = await likeRecordQuery.getOne(req.queryExecutor);
likeRecordQuery = LikeRecordQuery();
likeRecordQuery.values
.copyFrom(LikeRecord(uid: req.user!.idAsInt, schemeId: scheme.idAsInt, liked: isLike));
if (likeRecordCheck.isEmpty) {
likeRecordQuery.insert(req.queryExecutor);
} else if (likeRecordCheck.value.liked != isLike) {
likeRecordQuery.whereId = likeRecordCheck.value.idAsInt;
likeRecordQuery.updateOne(req.queryExecutor);
} else {
needUpdate = false;
}
}
if (needUpdate) {
/// /
schemeQuery = SchemeQuery();
schemeQuery.whereId = scheme.idAsInt;
Map<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();
},
],
),

@ -8,8 +8,7 @@ dependencies:
angel3_auth: ^4.0.0
angel3_configuration: ^4.1.0
angel3_framework: ^4.2.0
angel3_migration: ^4.0.0
angel3_orm: ^4.0.0
angel3_migration: ^4.0.3
angel3_orm_postgres: ^3.0.0
angel3_serialize: ^4.1.0
angel3_production: ^3.1.0
@ -21,13 +20,15 @@ dependencies:
yaml: ^3.1.0
mailer: ^5.0.2
uuid: ^3.0.5
angel3_orm:
path: 3rd_party/angel3_orm
neat_cache:
path: 3rd_party/neat_cache
dev_dependencies:
angel3_hot: ^4.2.0
angel3_jinja: ^2.0.1
angel3_migration_runner: ^4.0.0
angel3_orm_generator: ^4.1.0
angel3_orm_generator: ^4.1.3
angel3_serialize_generator: ^4.2.0
angel3_test: ^4.0.0
build_runner: ^2.0.3
@ -35,5 +36,6 @@ dev_dependencies:
test: ^1.17.5
lints: ^1.0.0
dependency_overrides:
angel3_orm:
path: 3rd_party/angel3_orm

@ -6,6 +6,7 @@ import 'package:dde_gesture_manager/extensions.dart';
import 'package:dde_gesture_manager/models/scheme.dart' as AppScheme;
import 'package:dde_gesture_manager/utils/helper.dart';
import 'package:dde_gesture_manager/utils/notificator.dart';
import 'package:dde_gesture_manager/widgets/me.dart';
import 'package:dde_gesture_manager_api/apis.dart';
import 'package:dde_gesture_manager_api/models.dart';
import 'package:http/http.dart' as http;
@ -85,7 +86,7 @@ class Api {
if (ignoreErrorHandle)
throw e;
else
return _handleHttpError(e);
_handleHttpError(e);
},
);
@ -116,7 +117,7 @@ class Api {
if (ignoreErrorHandle)
throw e;
else
return _handleHttpError(e);
_handleHttpError(e);
},
);
@ -158,6 +159,19 @@ class Api {
),
).then((value) => value == HttpStatus.noContent);
static Future<List<Scheme>> userUploads() =>
_get(Apis.scheme.userUploads, listRespBuilderWrap(SchemeSerializer.fromMap));
static Future<List<SimpleSchemeTransMetaData>> userSchemes({required SchemeListType type}) =>
_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,
);
}

@ -11,6 +11,7 @@ import 'package:dde_gesture_manager/themes/dark.dart';
import 'package:dde_gesture_manager/themes/light.dart';
import 'package:dde_gesture_manager/utils/helper.dart';
import 'package:dde_gesture_manager/utils/init.dart';
import 'package:dde_gesture_manager/utils/simple_throttle.dart';
import 'package:flutter/material.dart';
import 'pages/home.dart';
@ -71,15 +72,7 @@ class MyApp extends StatelessWidget {
firstChild: Builder(builder: (context) {
Future.microtask(() {
initEvents(context);
if (H().lastCheckAuthStatusTime != null &&
H().lastCheckAuthStatusTime!.difference(DateTime.now()) < Duration(minutes: 10)) return;
if (context.read<ConfigsProvider>().accessToken.notNull) {
Api.checkAuthStatus().then((value) {
if (!value) context.read<ConfigsProvider>().setProps(email: '', accessToken: '');
});
} else {
H().lastCheckAuthStatusTime = DateTime.now();
}
SimpleThrottle.throttledFunc(_checkAuthStatus, timeout: const Duration(minutes: 5))?.call(context);
});
return Container();
}),
@ -91,3 +84,15 @@ class MyApp extends StatelessWidget {
);
}
}
void _checkAuthStatus(BuildContext context) {
if (H().lastCheckAuthStatusTime != null &&
H().lastCheckAuthStatusTime!.difference(DateTime.now()) < Duration(minutes: 10)) return;
if (context.read<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/constants/sp_keys.dart';
import 'package:dde_gesture_manager/utils/helper.dart';
import 'package:dde_gesture_manager/extensions.dart';
@ProviderModel()
class ContentLayout {
@ -9,7 +12,7 @@ class ContentLayout {
bool? marketOrMeOpened;
@ProviderModelProp()
bool? currentIsMarket = true;
bool? currentIsMarket = H().sp.getString(SPKeys.accessToken).isNull;
bool get isMarket => currentIsMarket ?? true;
}

@ -184,8 +184,8 @@ class _LocalManagerState extends State<LocalManager> {
),
),
),
Container(height: 5),
Container(
Padding(
padding: const EdgeInsets.only(top: 5),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [

@ -82,7 +82,7 @@ class MarketOrMe extends StatelessWidget {
Widget buildMeContent(BuildContext context) {
var accessToken = context.watch<ConfigsProvider>().accessToken;
if (accessToken.isNull) return LoginWidget();
return MeWidget();
return Expanded(child: MeWidget());
}
Widget buildMarketContent(BuildContext context) {

@ -49,9 +49,9 @@ Future<void> initEvents(BuildContext context) async {
}
}
if (!_updateChecked)
Api.checkAppVersion(ignoreErrorHandle: true).then((value) async {
if (!_updateChecked) {
_updateChecked = true;
Api.checkAppVersion(ignoreErrorHandle: true).then((value) async {
var info = await PackageInfo.fromPlatform();
var _buildNumber = int.parse(info.buildNumber);
var _newVersionCode = value?.versionCode ?? 0;
@ -74,6 +74,7 @@ Future<void> initEvents(BuildContext context) 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(),
));
factory DButton.download({
Key? key,
required enabled,
GestureTapCallback? onTap,
height = defaultButtonHeight * .7,
width = defaultButtonHeight * .7,
}) =>
DButton(
key: key,
width: width,
height: height,
onTap: enabled ? onTap : null,
child: Tooltip(
child: Opacity(opacity: enabled ? 1 : 0.4, child: const Icon(Icons.file_download, size: 18)),
message: LocaleKeys.operation_download.tr(),
));
factory DButton.share({
Key? key,
required enabled,
GestureTapCallback? onTap,
height = defaultButtonHeight * .7,
width = defaultButtonHeight * .7,
}) =>
DButton(
key: key,
width: width,
height: height,
onTap: enabled ? onTap : null,
child: Tooltip(
child: Opacity(opacity: enabled ? 1 : 0.4, child: const Icon(Icons.share, size: 18)),
message: LocaleKeys.operation_share.tr(),
));
factory DButton.like({
Key? key,
required enabled,
GestureTapCallback? onTap,
height = defaultButtonHeight * .7,
width = defaultButtonHeight * .7,
}) =>
DButton(
key: key,
width: width,
height: height,
onTap: enabled ? onTap : null,
child: Tooltip(
child: Opacity(opacity: enabled ? 1 : 0.4, child: const Icon(Icons.thumb_up, size: 16)),
message: LocaleKeys.operation_like.tr(),
));
factory DButton.dropdown({
Key? key,
width = 60.0,

@ -1,13 +1,23 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:collection/collection.dart';
import 'package:dde_gesture_manager/constants/constants.dart';
import 'package:dde_gesture_manager/extensions.dart';
import 'package:dde_gesture_manager/http/api.dart';
import 'package:dde_gesture_manager/models/configs.provider.dart';
import 'package:dde_gesture_manager/models/settings.provider.dart';
import 'package:dde_gesture_manager/utils/notificator.dart';
import 'package:dde_gesture_manager_api/models.dart';
import 'package:flutter/material.dart';
import 'package:dde_gesture_manager/extensions.dart';
import 'package:flutter_platform_alert/flutter_platform_alert.dart';
import 'dde_button.dart';
enum SchemeListType {
uploaded,
downloaded,
liked,
}
class MeWidget extends StatefulWidget {
const MeWidget({Key? key}) : super(key: key);
@ -16,23 +26,46 @@ class MeWidget extends StatefulWidget {
}
class _MeWidgetState extends State<MeWidget> {
List<Scheme> uploads = [];
List<SimpleSchemeTransMetaData> _schemes = [];
SchemeListType _type = SchemeListType.uploaded;
String? _selected;
String? _hovering;
@override
void initState() {
super.initState();
Api.userUploads().then((value) {
Api.userSchemes(type: _type).then((value) {
if (mounted)
setState(() {
_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(() {
uploads = value;
_schemes = value;
});
});
});
}
@override
Widget build(BuildContext context) {
var currentSelectedScheme = _schemes.firstWhereOrNull((e) => e.uuid == _selected);
return Padding(
padding: EdgeInsets.symmetric(vertical: 10),
padding: EdgeInsets.only(top: 10),
child: Column(
children: [
Row(
@ -54,12 +87,157 @@ class _MeWidgetState extends State<MeWidget> {
),
],
),
Text('我的上传'),
Container(
height: 400,
Padding(
padding: const EdgeInsets.only(top: 3, bottom: 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SchemeListType.uploaded,
SchemeListType.downloaded,
SchemeListType.liked,
]
.map(
(e) => Flexible(
flex: 1,
fit: FlexFit.tight,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
setState(() {
_type = e;
});
Api.userSchemes(type: e).then((value) {
if (mounted)
setState(() {
_schemes = value;
});
});
},
child: Center(
child: Text(
'${LocaleKeys.me_scheme_types}.${e.name}'.tr(),
style: _type == e ? TextStyle(fontWeight: FontWeight.bold, fontSize: 15) : null,
),
),
),
),
),
)
.toList(),
),
),
Expanded(
child: Container(
decoration: BoxDecoration(
border: Border.all(
width: .3,
color: context.t.dividerColor,
),
borderRadius: BorderRadius.circular(defaultBorderRadius),
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 1, vertical: 2),
child: ListView.builder(
itemBuilder: (context, index) => Text(uploads[index].name ?? ''),
itemCount: uploads.length,
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
source_gen: 1.1.0
dependency_overrides:
angel3_orm:
path: ../api/3rd_party/angel3_orm
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

@ -72,7 +72,10 @@
"apply": "apply",
"paste": "paste",
"logout": "sign out",
"upload": "upload"
"upload": "upload",
"download": "download",
"share": "share to market",
"like": "like"
},
"str": {
"null": "Null",
@ -146,6 +149,11 @@
"upload": {
"success": "Upload success ~",
"failed": "Upload failed.."
},
"share": {
"title": "Are you sure to sharing?",
"description": "Other users can see this scheme and download it after share",
"success": "Share success"
}
},
"me": {
@ -155,6 +163,11 @@
"email_hint": "Please enter email",
"password_hint": "Please enter 8-16-bit password",
"email_error_hint": "Please enter your vaild email"
},
"scheme_types": {
"uploaded": "Uploaded",
"downloaded": "Downloaded",
"liked": "Liked"
}
}
}

@ -72,7 +72,10 @@
"apply": "应用",
"paste": "粘贴",
"logout": "退出登录",
"upload": "上传"
"upload": "上传",
"download": "下载",
"share": "分享到市场",
"like": "点赞"
},
"str": {
"null": "无",
@ -146,6 +149,11 @@
"upload": {
"success": "上传成功~",
"failed": "上传失败。。"
},
"share": {
"title": "确定分享?",
"description": "分享后其他用户可以看到本方案并下载使用",
"success": "分享成功"
}
},
"me": {
@ -155,6 +163,11 @@
"email_hint": "请输入邮箱",
"password_hint": "请输入8-16位密码",
"email_error_hint": "请输入正确的邮箱"
},
"scheme_types": {
"uploaded": "我的上传",
"downloaded": "我的下载",
"liked": "我的点赞"
}
}
}
Loading…
Cancel
Save