buildQueryClass method

Class buildQueryClass(
  1. OrmBuildContext ctx
)

Generate

Implementation

Class buildQueryClass(OrmBuildContext ctx) {
  return Class((clazz) {
    var rc = ctx.buildContext.modelClassNameRecase;

    var queryWhereType = refer('${rc.pascalCase}QueryWhere');

    log.info('Generating ${rc.pascalCase}QueryWhere');

    var nullableQueryWhereType = TypeReference((b) => b
      ..symbol = '${rc.pascalCase}QueryWhere'
      ..isNullable = true);

    clazz
      ..name = '${rc.pascalCase}Query'
      ..extend = TypeReference((b) {
        b
          ..symbol = 'Query'
          ..types.addAll([
            ctx.buildContext.modelClassType,
            queryWhereType,
          ]);
      });

    // Override casts so that we can cast doubles
    clazz.methods.add(Method((b) {
      b
        ..name = 'casts'
        ..annotations.add(refer('override'))
        ..returns = TypeReference((b) => b
          ..symbol = 'Map'
          ..types.add(refer('String'))
          ..types.add(refer('String')))
        ..type = MethodType.getter
        ..body = Block((b) {
          var args = <String, Expression>{};

          /* Remove casts no numeric values
          for (var field in ctx.effectiveFields) {
            var name = ctx.buildContext.resolveFieldName(field.name);
            var type = ctx.columns[field.name]?.type;
            if (type == null) continue;
            if (floatTypes.contains(type)) {
              //args[name] = literalString('text');
              args[name!] = literalString('char');
            }
          }
          */

          b.addExpression(literalMap(args).returned);
        });
    }));

    // Add values
    clazz.fields.add(Field((b) {
      var type = refer('${rc.pascalCase}QueryValues');
      b
        ..name = 'values'
        ..modifier = FieldModifier.final$
        ..annotations.add(refer('override'))
        ..type = type
        ..assignment = type.newInstance([]).code;
    }));

    // Add tableName
    clazz.methods.add(Method((m) {
      m
        ..name = 'tableName'
        ..returns = refer('String')
        ..annotations.add(refer('override'))
        ..type = MethodType.getter
        ..body = Block((b) {
          b.addExpression(literalString(ctx.tableName!).returned);
        });
    }));

    // Add fields getter
    clazz.methods.add(Method((m) {
      //log.fine('Field: $name');
      m
        ..name = 'fields'
        ..returns = TypeReference((b) => b
          ..symbol = 'List'
          ..types.add(TypeReference((b) => b..symbol = 'String')))
        ..annotations.add(refer('override'))
        ..type = MethodType.getter
        ..body = Block((b) {
          var names = ctx.effectiveFields
              .map((f) =>
                  literalString(ctx.buildContext.resolveFieldName(f.name)!))
              .toList();
          //b.addExpression(literalConstList(names).assignConst('_fields'));
          b.addExpression(
              declareConst('_fields').assign(literalConstList(names)));
          b.addExpression(refer('_selectedFields')
              .property('isEmpty')
              .conditional(
                refer('_fields'),
                refer('_fields')
                    .property('where')
                    .call([
                      CodeExpression(
                          Code('(field) => _selectedFields.contains(field)'))
                    ])
                    .property('toList')
                    .call([]),
              )
              .returned);
        });
    }));

    // Add _selectedFields member
    clazz.fields.add(Field((b) {
      b
        ..name = '_selectedFields'
        ..type = TypeReference((t) => t
          ..symbol = 'List'
          ..types.add(TypeReference((b) => b..symbol = 'String')))
        ..assignment = Code('[]');
    }));

    // Add select(List<String> fields)
    clazz.methods.add(Method((m) {
      m
        ..name = 'select'
        ..returns = refer('${rc.pascalCase}Query')
        ..requiredParameters.add(Parameter((b) => b
          ..name = 'selectedFields'
          ..type = TypeReference((t) => t
            ..symbol = 'List'
            ..types.add(TypeReference((b) => b..symbol = 'String')))))
        ..body = Block((b) {
          b.addExpression(
            refer('_selectedFields').assign(refer('selectedFields')),
          );
          b.addExpression(refer('this').returned);
        });
    }));

    // Add _where member
    clazz.fields.add(Field((b) {
      b
        ..name = '_where'
        ..type = nullableQueryWhereType;
    }));

    // Add where getter
    clazz.methods.add(Method((b) {
      b
        ..name = 'where'
        ..type = MethodType.getter
        ..returns = nullableQueryWhereType
        ..annotations.add(refer('override'))
        ..body = Block((b) => b.addExpression(refer('_where').returned));
    }));

    // newWhereClause()
    clazz.methods.add(Method((b) {
      b
        ..name = 'newWhereClause'
        ..annotations.add(refer('override'))
        ..returns = queryWhereType
        ..body = Block((b) => b.addExpression(
            queryWhereType.newInstance([refer('this')]).returned));
    }));

    // Add parseRow()
    clazz.methods.add(Method((m) {
      m
        ..name = 'parseRow'
        ..returns = refer('Optional<${rc.pascalCase}>')
        ..requiredParameters.add(Parameter((b) => b
          ..name = 'row'
          ..type = refer('List')))
        ..body = Block((b) {
          var i = 0;

          // Build the arguments for model
          var args = <String, Expression>{};
          for (var field in ctx.effectiveFields) {
            var fType = field.type;
            Reference type = convertTypeReference(fType);
            if (isSpecialId(ctx, field)) {
              type = refer('int');
            }

            // Generated Code: row[i]
            var expr = (refer('row').index(literalNum(i++)));
            if (isSpecialId(ctx, field)) {
              // Generated Code: row[i].toString()
              expr = expr.property('toString').call([]);
            } else if (field is RelationFieldImpl) {
              continue;
            } else if (ctx.columns[field.name]?.type == ColumnType.json) {
              expr = refer('json')
                  .property('decode')
                  .call([expr.asA(refer('String'))]).asA(type);
            } else if (floatTypes.contains(ctx.columns[field.name]?.type)) {
              //expr = refer('double')
              //    .property('tryParse')
              //    .call([expr.property('toString').call([])]);
              expr = refer('mapToDouble').call([expr]);
            } else if (fType is InterfaceType &&
                fType.element is EnumElement) {
              /*
               * fields.contains('type') ? row[3] == null ? null :
               *     EnumType.values[(row[3] as int)] : null,
               */
              var isNull = expr.equalTo(literalNull);
              final parseExpression = _deserializeEnumExpression(field, expr);
              expr = isNull.conditional(literalNull, parseExpression);
            } else if (fType.isDartCoreBool) {
              // Generated Code: mapToBool(row[i])
              expr = refer('mapToBool').call([expr]);
            } else if (fType.element?.displayName == 'DateTime') {
              // Generated Code: mapToDateTime(row[i])
              if (fType.nullabilitySuffix == NullabilitySuffix.question) {
                expr = refer('mapToNullableDateTime').call([expr]);
              } else {
                expr = refer('mapToDateTime').call([expr]);
              }
            } else {
              // Generated Code: (row[i] as type?)
              expr = expr.asA(type);
            }

            Expression defaultRef = refer('null');
            if (fType.nullabilitySuffix != NullabilitySuffix.question) {
              if (fType.isDartCoreString) {
                defaultRef = CodeExpression(Code('\'\''));
              } else if (fType.isDartCoreBool) {
                defaultRef = CodeExpression(Code('false'));
              } else if (fType.isDartCoreDouble) {
                defaultRef = CodeExpression(Code('0.0'));
              } else if (fType.isDartCoreInt || fType.isDartCoreNum) {
                defaultRef = CodeExpression(Code('0'));
              } else if (fType.element?.displayName == 'DateTime') {
                defaultRef = CodeExpression(
                    Code('DateTime.parse("1970-01-01 00:00:00")'));
              } else if (fType.isDartCoreList) {
                defaultRef = CodeExpression(Code('[]'));
              }
            }
            expr = refer('fields').property('contains').call([
              literalString(ctx.buildContext.resolveFieldName(field.name)!)
            ]).conditional(expr, defaultRef);
            args[field.name] = expr;
          }

          b.statements.add(Code(
              'if (row.every((x) => x == null)) { return Optional.empty(); }'));
          //b.addExpression(refer('0').assignVar('_index'));

          //b.addExpression(ctx.buildContext.modelClassType
          //    .newInstance([], args).assignVar('model'));
          b.addExpression(declareVar('model')
              .assign(ctx.buildContext.modelClassType.newInstance([], args)));

          ctx.relations.forEach((name, relation) {
            if (!const [
              RelationshipType.hasOne,
              RelationshipType.belongsTo,
              RelationshipType.hasMany
            ].contains(relation.type)) {
              //log.warning('Unsupported relationship for field $name');
              return;
            }

            //log.fine('Process relationship');
            var foreign = relation.foreign;
            if (foreign == null) {
              log.warning('Foreign relationship for field $name is null');
              return;
            }
            //log.fine('Detected relationship ${RelationshipType.belongsTo}');

            var skipToList = refer('row')
                .property('skip')
                .call([literalNum(i)])
                .property('take')
                .call([literalNum(foreign.effectiveFields.length)])
                .property('toList')
                .call([]);

            var parsed = refer(
                    '${foreign.buildContext.modelClassNameRecase.pascalCase}Query')
                .newInstance([])
                .property('parseRow')
                .call([skipToList]);

            //var baseClass = '${foreign.buildContext.originalClassName}';
            //var modelClass =
            //    foreign.buildContext.modelClassNameRecase.pascalCase;
            // Assume: baseclass starts with "_"
            //'_${foreign.buildContext.modelClassNameRecase.pascalCase}';

            //if (relation.type == RelationshipType.hasMany) {
            //  parsed = literalList([parsed.asA(refer(modelClass))]);
            //parsed = literalList([parsed.asA(refer(baseClass))]);
            //  var pp = parsed.accept(DartEmitter(useNullSafetySyntax: true));
            //  parsed = CodeExpression(Code('$pp'));
            //Code('$pp.where((x) => x != null).toList()'));
            //}

            //var expr =
            //    refer('model').property('copyWith').call([], {name: parsed});
            //var block =
            //    Block((b) => b.addExpression(refer('model').assign(expr)));

            var stmt = declareVar('modelOpt').assign(parsed);
            //parsed.assignVar('modelOpt');
            //var e = refer('Optional').property('ifPresent').call([]);

            var val =
                (relation.type == RelationshipType.hasMany) ? '[m]' : 'm';
            var code = Code('''
                   modelOpt.ifPresent((m) {
                    model = model.copyWith($name: $val);
                })
            ''');

            var block = Block((b) {
              b.addExpression(stmt);
              b.addExpression(CodeExpression(code));
            });

            var blockStr =
                block.accept(DartEmitter(useNullSafetySyntax: true));

            var ifStr = 'if (row.length > $i) { $blockStr }';
            b.statements.add(Code(ifStr));

            i += foreign.effectiveFields.length;
          });

          b.addExpression(
              refer('Optional.of').call([refer('model')]).returned);
        });
    }));

    // deserialize
    clazz.methods.add(Method((m) {
      m
        ..name = 'deserialize'
        ..returns = refer('Optional<${rc.pascalCase}>')
        ..annotations.add(refer('override'))
        ..requiredParameters.add(Parameter((b) => b
          ..name = 'row'
          ..type = refer('List')))
        ..body = Block((b) {
          b.addExpression(refer('parseRow').call([refer('row')]).returned);
        });
    }));

    // If there are any relations, we need some overrides.
    clazz.constructors.add(Constructor((b) {
      b
        ..optionalParameters.add(Parameter((b) => b
          ..named = true
          ..name = 'parent'
          ..type = refer('Query?')))
        ..optionalParameters.add(Parameter((b) => b
          ..named = true
          ..name = 'trampoline'
          ..type = refer('Set<String>?')))
        //TypeReference((b) => b
        //..symbol = 'Set'
        //..types.add(refer('String')))))
        ..initializers.add(Code('super(parent: parent)'))
        ..body = Block((b) {
          b.statements.addAll([
            Code('trampoline ??= <String>{};'),
            Code('trampoline.add(tableName);'),
          ]);

          // Add any manual SQL expressions.
          ctx.columns.forEach((name, col) {
            if (col.hasExpression) {
              var lhs = refer('expressions').index(
                  literalString(ctx.buildContext.resolveFieldName(name)!));
              var rhs = literalString(col.expression!);
              b.addExpression(lhs.assign(rhs));
            }
          });

          // Add a constructor that initializes _where
          b.addExpression(
            refer('_where')
                .assign(queryWhereType.newInstance([refer('this')])),
          );

          // Note: this is where subquery fields for relations are added.
          ctx.relations.forEach((fieldName, relation) {
            //var name = ctx.buildContext.resolveFieldName(fieldName);
            if (relation.type == RelationshipType.belongsTo ||
                relation.type == RelationshipType.hasOne ||
                relation.type == RelationshipType.hasMany) {
              //
              //     @ManyToMany(_RoleUser) => relation.throughContext
              //     List<_User> get users; => relation.foreign
              //
              var relationForeign = relation.foreign;
              if (relationForeign == null) {
                log.warning('$fieldName has no relationship in the context');
                return;
              }
              var relationContext =
                  relation.throughContext ?? relation.foreign;

              //log.fine(
              //    '$fieldName relation.throughContext => ${relation.throughContext?.tableName} relation.foreign => ${relation.foreign?.tableName}');
              // If this is a many-to-many, add the fields from the other object.
              var additionalStrs = relationForeign.effectiveFields.map((f) =>
                  relationForeign.buildContext.resolveFieldName(f.name));

              var additionalFields = <Expression>[];
              for (var element in additionalStrs) {
                if (element != null) {
                  additionalFields.add(literalString(element));
                }
              }

              var joinArgs = <Expression>[];
              for (var element in [relation.localKey, relation.foreignKey]) {
                if (element != null) {
                  joinArgs.add(literalString(element));
                }
              }

              // In the case of a many-to-many, we don't generate a subquery field,
              // as it easily leads to stack overflows.
              if (relation.isManyToMany) {
                // We can't simply join against the "through" table; this itself must
                // be a join.
                // (SELECT role_users.role_id, <user_fields>
                // FROM users
                // LEFT JOIN role_users ON role_users.user_id=users.id)
                var foreignFields = additionalStrs
                    .map((f) => '${relationForeign.tableName}.$f');
                var b = StringBuffer('(SELECT ');
                // role_users.role_id
                b.write('${relationContext?.tableName}');
                b.write('.${relation.foreignKey}');
                // , <user_fields>
                b.write(foreignFields.isEmpty
                    ? ''
                    : ', ${foreignFields.join(', ')}');
                // FROM users
                b.write(' FROM ');
                b.write(relationForeign.tableName);
                // LEFT JOIN role_users
                b.write(' LEFT JOIN ${relationContext?.tableName}');
                // Figure out which field on the "through" table points to users (foreign)

                //log.fine('$fieldName query => ${b.toString()}');

                var throughRelation =
                    relationContext?.relations.values.firstWhere((e) {
                  //log.fine(
                  //    'ForeignTable(Rel) => ${e.foreignTable}, ${relationForeign.tableName}');
                  return e.foreignTable == relationForeign.tableName;
                }, orElse: () {
                  // _Role has a many-to-many to _User through _RoleUser, but
                  // _RoleUser has no relation pointing to _User.
                  var b = StringBuffer();
                  b.write(ctx.buildContext.modelClassName);
                  b.write(' has a many-to-many relationship to ');
                  b.write(relationForeign.buildContext.modelClassName);
                  b.write(' through ');
                  b.write(relationContext.buildContext.modelClassName);
                  b.write(', but ');
                  b.write(relationContext.buildContext.modelClassName);
                  b.write(' has no relation pointing to ');
                  b.write(ctx.buildContext.modelClassName);
                  b.write('.');
                  throw b.toString();
                });

                // ON role_users.user_id=users.id)
                b.write(' ON ');
                b.write('${relation.throughContext!.tableName}');
                b.write('.');
                b.write(throughRelation?.localKey);
                b.write('=');
                b.write(relationForeign.tableName);
                b.write('.');
                b.write(throughRelation?.foreignKey);
                b.write(')');

                joinArgs.insert(0, literalString(b.toString()));
              } else {
                // In the past, we would either do a join on the table name
                // itself, or create an instance of a query.
                //
                // From this point on, however, we will create a field for each
                // join, so that users can customize the generated query.
                //
                // There'll be a private `_field`, and then a getter, named `field`,
                // that returns the subquery object.

                var foreignQueryType = refer(
                    '${relationForeign.buildContext.modelClassNameRecase.pascalCase}Query');

                clazz
                  ..fields.add(Field((b) => b
                    ..name = '_$fieldName'
                    ..late = true
                    ..type = foreignQueryType))
                  ..methods.add(Method((b) => b
                    ..name = fieldName
                    ..type = MethodType.getter
                    ..returns = foreignQueryType
                    ..body = refer('_$fieldName').returned.statement));

                // Assign a value to `_field`.
                var queryInstantiation = foreignQueryType.newInstance([], {
                  'trampoline': refer('trampoline'),
                  'parent': refer('this')
                });
                joinArgs.insert(
                    0, refer('_$fieldName').assign(queryInstantiation));
              }

              var joinType = relation.joinTypeString;
              b.addExpression(refer(joinType).call(joinArgs, {
                'additionalFields':
                    literalConstList(additionalFields.toList()),
                'trampoline': refer('trampoline'),
              }));
            }
          });
        });
    }));

    // If we have any many-to-many relations, we need to prevent
    // fetching this table within their joins.
    var manyToMany = ctx.relations.entries.where((e) => e.value.isManyToMany);

    if (manyToMany.isNotEmpty) {
      var outExprs = manyToMany.map<Expression>((e) {
        var foreignTableName = e.value.throughContext!.tableName;
        return CodeExpression(Code('''
        (!(
          trampoline.contains('${ctx.tableName}')
          && trampoline.contains('$foreignTableName')
        ))
        '''));
      });
      var out = outExprs.reduce((a, b) => a.and(b));

      clazz.methods.add(Method((b) {
        b
          ..name = 'canCompile'
          ..annotations.add(refer('override'))
          ..requiredParameters.add(Parameter((b) => b..name = 'trampoline'))
          ..returns = refer('bool')
          ..body = Block((b) {
            b.addExpression(out.returned);
          });
      }));
    }

    // Also, if there is a @HasMany, generate overrides for query methods that
    // execute in a transaction, and invoke fetchLinked.
    if (ctx.relations.values.any((r) => r.type == RelationshipType.hasMany)) {
      for (var methodName in const ['get', 'update', 'delete']) {
        clazz.methods.add(Method((b) {
          var type = ctx.buildContext.modelClassType
              .accept(DartEmitter(useNullSafetySyntax: true));
          b
            ..name = methodName
            ..returns = TypeReference((b) => b
              ..symbol = 'Future'
              ..types.add(TypeReference((b) => b
                ..symbol = 'List'
                ..types.add(TypeReference((b) => b
                  ..symbol = '$type'
                  ..isNullable = false)))))
            ..annotations.add(refer('override'))
            ..requiredParameters.add(Parameter((b) => b
              ..name = 'executor'
              ..type = refer('QueryExecutor')));

          // Collect hasMany options, and ultimately merge them
          var merge = <String>[];

          ctx.relations.forEach((name, relation) {
            if (relation.type == RelationshipType.hasMany) {
              // This is only allowed with lists.
              var field =
                  ctx.buildContext.fields.firstWhere((f) => f.name == name);

              var typeLiteral = convertTypeReference(field.type)
                  .accept(DartEmitter(useNullSafetySyntax: true))
                  .toString()
                  .replaceAll('?', '');
              merge.add('''
                    $name: $typeLiteral.from(l.$name)..addAll(model.$name)
                  ''');
            }
          });

          var merged = merge.join(', ');

          var keyName =
              findPrimaryFieldInList(ctx, ctx.buildContext.fields)?.name;

          if (keyName == null) {
            throw '${ctx.buildContext.originalClassName} has no defined primary key.\n'
                '@HasMany and @ManyToMany relations require a primary key to be defined on the model.';
          }

          b.body = Code('''
                  return super.$methodName(executor).then((result) {
                    return result.fold<List<$type>>([], (out, model) {
                      var idx = out.indexWhere((m) => m.$keyName == model.$keyName);

                      if (idx == -1) {
                        return out..add(model);
                      } else {
                        var l = out[idx];
                        return out..[idx] = l.copyWith($merged);
                      }
                    });
                  });
                  ''');
        }));
      }
    }
  });
}