buildOrmContext function

Future<OrmBuildContext?> buildOrmContext(
  1. Map<String, OrmBuildContext> cache,
  2. InterfaceElement clazz,
  3. ConstantReader annotation,
  4. BuildStep buildStep,
  5. Resolver resolver,
  6. bool? autoSnakeCaseNames, {
  7. bool heedExclude = true,
})

Create ORM Context

Implementation

Future<OrmBuildContext?> buildOrmContext(
    Map<String, OrmBuildContext> cache,
    InterfaceElement clazz,
    ConstantReader annotation,
    BuildStep buildStep,
    Resolver resolver,
    bool? autoSnakeCaseNames,
    {bool heedExclude = true}) async {
  // Check for @generatedSerializable
  // ignore: unused_local_variable
  DartObject? generatedSerializable;

  while ((generatedSerializable =
          const TypeChecker.fromRuntime(GeneratedSerializable)
              .firstAnnotationOf(clazz)) !=
      null) {
    if (clazz.supertype != null) {
      clazz = clazz.supertype!.element;
    }
  }

  var id = clazz.location?.components.join('-') ?? '';
  if (cache.containsKey(id)) {
    return cache[id];
  }
  var buildCtx = await buildContext(
      clazz, annotation, buildStep, resolver, autoSnakeCaseNames!,
      heedExclude: heedExclude);
  var ormAnnotation = reviveORMAnnotation(annotation);
  // print(
  //     'tableName (${annotation.objectValue.type.name}) => ${ormAnnotation.tableName} from ${clazz.name} (${annotation.revive().namedArguments})');
  if (buildCtx == null) {
    log.severe('BuildContext is null');

    return null;
  }
  var ctx = OrmBuildContext(
      buildCtx,
      ormAnnotation,
      (ormAnnotation.tableName?.isNotEmpty == true)
          ? ormAnnotation.tableName
          : pluralize(ReCase(clazz.name).snakeCase));
  cache[id] = ctx;

  // Read all fields
  for (var field in buildCtx.fields) {
    // Check for column annotation...
    var element = _findElement(field);
    var columnAnnotation = columnTypeChecker.firstAnnotationOf(element);

    Column? column;
    if (columnAnnotation != null) {
      // print('[ORM_BUILD_CONTEXT] ${element.name} => $columnAnnotation');
      column = reviveColumn(ConstantReader(columnAnnotation));
    }

    if (column == null && isSpecialId(ctx, field)) {
      // This is only for PostgreSQL, so implementations without a `serial` type
      // must handle it accordingly, of course.
      column = const Column(
          type: ColumnType.serial, indexType: IndexType.primaryKey);
    }

    column ??= Column(
      type: inferColumnType(
        buildCtx.resolveSerializedFieldType(field.name),
      ),
    );
    var isEnumField =
        (field.type is InterfaceType && field.type.element is EnumElement);
    var hasColumnAnnotation =
        (columnAnnotation?.getField('type')?.hasKnownValue ?? false);
    column = Column(
      isNullable: column.isNullable,
      length: column.length,
      indexType: column.indexType,
      // Only infer type when not set by the @Column annotation, for enums
      type: (isEnumField && hasColumnAnnotation)
          ? column.type
          : inferColumnType(field.type),
      defaultValue: column.defaultValue,
    );

    // Try to find a relationship
    var el = _findElement(field);
    //el ??= field;
    var ann = relationshipTypeChecker.firstAnnotationOf(el);

    if (ann != null) {
      var cr = ConstantReader(ann);
      var rc = ctx.buildContext.modelClassNameRecase;
      var type = cr.read('type').intValue;
      var localKey = cr.peek('localKey')?.stringValue;
      var foreignKey = cr.peek('foreignKey')?.stringValue;
      var foreignTable = cr.peek('foreignTable')?.stringValue;
      var cascadeOnDelete = cr.peek('cascadeOnDelete')?.boolValue == true;
      var through = cr.peek('through')?.typeValue;
      OrmBuildContext? foreign, throughContext;

      if (foreignTable == null) {
        // if (!isModelClass(field.type) &&
        //     !(field.type is InterfaceType &&
        //         isListOfModelType(field.type as InterfaceType))) {
        var canUse = (field.type is InterfaceType &&
                isListOfModelType(field.type as InterfaceType)) ||
            isModelClass(field.type);
        if (!canUse) {
          throw UnsupportedError(
              'Cannot apply relationship to field "${field.name}" - ${field.type} is not assignable to Model.');
        } else {
          try {
            var refType = field.type;

            if (refType is InterfaceType &&
                const TypeChecker.fromRuntime(List)
                    .isAssignableFromType(refType) &&
                refType.typeArguments.length == 1) {
              refType = refType.typeArguments[0];
            }

            var modelType = firstModelAncestor(refType) ?? refType;
            var modelTypeElement = modelType.element;

            if (modelTypeElement != null) {
              foreign = await buildOrmContext(
                  cache,
                  modelTypeElement as ClassElement,
                  ConstantReader(const TypeChecker.fromRuntime(Orm)
                      .firstAnnotationOf(modelTypeElement)),
                  buildStep,
                  resolver,
                  autoSnakeCaseNames);

              // Resolve throughType as well
              if (through != null && through is InterfaceType) {
                throughContext = await buildOrmContext(
                    cache,
                    through.element,
                    ConstantReader(const TypeChecker.fromRuntime(Serializable)
                        .firstAnnotationOf(modelTypeElement)),
                    buildStep,
                    resolver,
                    autoSnakeCaseNames);
              }

              var ormAnn = const TypeChecker.fromRuntime(Orm)
                  .firstAnnotationOf(modelTypeElement);

              if (ormAnn != null) {
                foreignTable =
                    ConstantReader(ormAnn).peek('tableName')?.stringValue;
              }

              if (foreign != null) {
                foreignTable ??= pluralize(
                    foreign.buildContext.modelClassNameRecase.snakeCase);
              }
            } else {
              log.warning('Ancestor model type for [${field.name}] is null');
            }
          } on StackOverflowError {
            throw UnsupportedError(
                'There is an infinite cycle between ${clazz.name} and ${field.type.getDisplayString(withNullability: true)}. This triggered a stack overflow.');
          }
        }
      }

      // Fill in missing keys
      var rcc = ReCase(field.name);

      String keyName(OrmBuildContext ctx, String missing) {
        var localKeyName =
            findPrimaryFieldInList(ctx, ctx.buildContext.fields)?.name;
        // print(
        //     'Keyname for ${buildCtx.originalClassName}.${field.name} maybe = $_keyName??');
        if (localKeyName == null) {
          throw '${ctx.buildContext.originalClassName} has no defined primary key, '
              'so the relation on field ${buildCtx.originalClassName}.${field.name} must define a $missing.';
        } else {
          return localKeyName;
        }
      }

      if (type == RelationshipType.hasOne || type == RelationshipType.hasMany) {
        localKey ??=
            ctx.buildContext.resolveFieldName(keyName(ctx, 'local key'));
        // print(
        //     'Local key on ${buildCtx.originalClassName}.${field.name} defaulted to $localKey');
        foreignKey ??= '${rc.snakeCase}_$localKey';
      } else if (type == RelationshipType.belongsTo) {
        foreignKey ??=
            ctx.buildContext.resolveFieldName(keyName(foreign!, 'foreign key'));
        localKey ??= '${rcc.snakeCase}_$foreignKey';
      }

      // Figure out the join type.
      var joinType = JoinType.left;
      var joinTypeRdr = cr.peek('joinType')?.objectValue;
      if (joinTypeRdr != null) {
        // Unfortunately, the analyzer library provides little to nothing
        // in the way of reading enums from source, so here's a hack.
        var joinTypeType = (joinTypeRdr.type as InterfaceType);
        var enumFields =
            joinTypeType.element.fields.where((f) => f.isEnumConstant).toList();

        for (var i = 0; i < enumFields.length; i++) {
          if (enumFields[i].computeConstantValue() == joinTypeRdr) {
            joinType = JoinType.values[i];
            break;
          }
        }
      }

      var relation = RelationshipReader(
        type,
        localKey: localKey,
        foreignKey: foreignKey,
        foreignTable: foreignTable,
        cascadeOnDelete: cascadeOnDelete,
        through: through,
        foreign: foreign,
        throughContext: throughContext,
        joinType: joinType,
      );

      log.fine('Relation on ${buildCtx.originalClassName}.${field.name} => '
          'foreignKey=$foreignKey, localKey=$localKey');

      if (relation.type == RelationshipType.belongsTo) {
        var localKey = relation.localKey;

        if (localKey != null) {
          var name = ReCase(localKey).camelCase;
          ctx.buildContext.aliases[name] = localKey;

          if (!ctx.effectiveFields.any((f) => f.name == field.name)) {
            var foreignField = relation.findForeignField(ctx);
            var foreign = relation.throughContext ?? relation.foreign;
            var type = foreignField.type;

            if (foreign != null) {
              if (isSpecialId(foreign, foreignField)) {
                // Use integer
                type = field.type.element?.library?.typeProvider.intType
                    as DartType;

                //type = field.type.element?.context.typeProvider.intType;
              }
            }

            var rf = RelationFieldImpl(name, relation, type, field);
            ctx.effectiveFields.add(rf);
          }
        }
      }

      ctx.relations[field.name] = relation;
    } else {
      /*
      if (column.type == null) {
        throw 'Cannot infer SQL column type for field "${ctx.buildContext.originalClassName}.${field.name}" with type "${field.type.getDisplayString(withNullability: true)}".';
      }
      */

      // Expressions...
      column = Column(
        isNullable: column.isNullable,
        length: column.length,
        type: column.type,
        indexType: column.indexType,
        defaultValue: column.defaultValue,
        expression:
            ConstantReader(columnAnnotation).peek('expression')?.stringValue,
      );

      ctx.columns[field.name] = column;

      if (!ctx.effectiveFields.any((f) => f.name == field.name)) {
        ctx.effectiveFields.add(field);
      }
    }
  }

  return ctx;
}