Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Immutable entities via copyWithId setter #393

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions generator/lib/src/code_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ class CodeBuilder extends Builder {
propInModel.name = prop.name;
propInModel.type = prop.type;
propInModel.flags = prop.flags;
propInModel.generatorFlags = prop.generatorFlags;
propInModel.dartFieldType = prop.dartFieldType;
propInModel.relationTarget = prop.relationTarget;

Expand Down
16 changes: 13 additions & 3 deletions generator/lib/src/code_chunks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ class CodeChunks {
id: ${createIdUid(property.id)},
name: '${property.name}',
type: ${property.type},
flags: ${property.flags}
flags: ${property.flags},
generatorFlags: ${property.generatorFlags}
$additionalArgs
)
''';
Expand Down Expand Up @@ -185,8 +186,12 @@ class CodeChunks {
}

static String setId(ModelEntity entity) {
if (entity.idProperty
.hasGeneratorFlag(OBXPropertyGeneratorFlags.USE_COPY_WITH_ID)) {
return '{return object.copyWithId(id);}';
}
if (!entity.idProperty.fieldIsReadOnly) {
return '{object.${propertyFieldName(entity.idProperty)} = id;}';
return '{object.${propertyFieldName(entity.idProperty)} = id; return object;}';
}
// Note: this is a special case handling read-only IDs with assignable=true.
// Such ID must already be set, i.e. it could not have been assigned.
Expand All @@ -198,6 +203,7 @@ class CodeChunks {
"doesn't match the inserted ID (ID \$id). "
'You must assign an ID before calling [box.put()].');
}
return object;
}''';
}

Expand Down Expand Up @@ -399,6 +405,8 @@ class CodeChunks {
if (entity.properties[index].isRelation) {
if (paramDartType.startsWith('ToOne<')) {
paramValueCode = 'ToOne(targetId: $paramValueCode)';
} else if (paramDartType.startsWith('ToOneProxy<')) {
paramValueCode = 'ToOneProxy(targetId: $paramValueCode)';
} else if (paramType == 'optional-named') {
log.info('Skipping constructor parameter $paramName on '
"'${entity.name}': the matching field is a relation but the type "
Expand All @@ -408,6 +416,8 @@ class CodeChunks {
}
} else if (paramDartType.startsWith('ToMany<')) {
paramValueCode = 'ToMany()';
} else if (paramDartType.startsWith('ToManyProxy<')) {
paramValueCode = 'ToManyProxy()';
} else {
// If we can't find a positional param, we can't use the constructor at all.
if (paramType == 'positional' || paramType == 'required-named') {
Expand Down Expand Up @@ -455,7 +465,7 @@ class CodeChunks {
if (!p.isRelation) return;
if (fieldReaders[index].isNotEmpty) {
postLines.add(
'object.${propertyFieldName(p)}.targetId = ${fieldReaders[index]};');
'object.${propertyFieldName(p)}.relation.targetId = ${fieldReaders[index]};');
}
postLines.add('object.${propertyFieldName(p)}.attach(store);');
});
Expand Down
17 changes: 12 additions & 5 deletions generator/lib/src/entity_resolver.dart
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,17 @@ class EntityResolver extends Builder {

var isToManyRel = false;
int? fieldType;
var flags = 0;
var flags = 0, generatorFlags = 0;
int? propUid;

_idChecker.runIfMatches(f, (annotation) {
flags |= OBXPropertyFlags.ID;
if (annotation.getField('assignable')!.toBoolValue()!) {
flags |= OBXPropertyFlags.ID_SELF_ASSIGNABLE;
}
if (annotation.getField('useCopyWith')!.toBoolValue()!) {
generatorFlags |= OBXPropertyGeneratorFlags.USE_COPY_WITH_ID;
}
});

_propertyChecker.runIfMatches(f, (annotation) {
Expand Down Expand Up @@ -210,6 +213,7 @@ class EntityResolver extends Builder {
final prop = ModelProperty.create(
IdUid(0, propUid ?? 0), f.name, fieldType,
flags: flags,
generatorFlags: generatorFlags,
entity: entity,
uidRequest: propUid != null && propUid == 0);

Expand Down Expand Up @@ -242,7 +246,9 @@ class EntityResolver extends Builder {
final idField = element.fields
.singleWhere((FieldElement f) => f.name == entity.idProperty.name);
if (idField.setter == null) {
if (!entity.idProperty.hasFlag(OBXPropertyFlags.ID_SELF_ASSIGNABLE)) {
if (!entity.idProperty.hasFlag(OBXPropertyFlags.ID_SELF_ASSIGNABLE) &&
!entity.idProperty
.hasGeneratorFlag(OBXPropertyGeneratorFlags.USE_COPY_WITH_ID)) {
throw InvalidGenerationSourceError(
"Entity ${entity.name} has an ID field '${idField.name}' that is "
'not assignable (that usually means it is declared final). '
Expand Down Expand Up @@ -308,7 +314,7 @@ class EntityResolver extends Builder {

final indexAnnotation = _indexChecker.firstAnnotationOfExact(f);
final uniqueAnnotation = _uniqueChecker.firstAnnotationOfExact(f);
if (indexAnnotation == null && uniqueAnnotation == null) return null;
if (indexAnnotation == null && uniqueAnnotation == null) return;

// Throw if property type does not support any index.
if (fieldType == OBXPropertyType.Float ||
Expand Down Expand Up @@ -430,10 +436,11 @@ class EntityResolver extends Builder {
bool isRelationField(FieldElement f) =>
isToOneRelationField(f) || isToManyRelationField(f);

bool isToOneRelationField(FieldElement f) => f.type.element!.name == 'ToOne';
bool isToOneRelationField(FieldElement f) =>
const {'ToOne', 'ToOneProxy'}.contains(f.type.element!.name);

bool isToManyRelationField(FieldElement f) =>
f.type.element!.name == 'ToMany';
const {'ToMany', 'ToManyProxy'}.contains(f.type.element!.name);

bool isNullable(DartType type) =>
type.nullabilitySuffix == NullabilitySuffix.star ||
Expand Down
2 changes: 1 addition & 1 deletion objectbox/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ help: ## Show this help
all: depend test valgrind-test integration-test

depend: ## Build dependencies
dart pub get
dart pub get
../install.sh

test: ## Test all targets
Expand Down
4 changes: 2 additions & 2 deletions objectbox/lib/objectbox.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ export 'src/query.dart'
QueryParamInt,
QueryParamBool,
QueryParamDouble;
export 'src/relations/to_many.dart' show ToMany;
export 'src/relations/to_one.dart' show ToOne;
export 'src/relations/to_many.dart' show ToMany, ToManyProxy;
export 'src/relations/to_one.dart' show ToOne, ToOneProxy;
export 'src/store.dart' show Store, ObservableStore;
export 'src/sync.dart'
show
Expand Down
27 changes: 26 additions & 1 deletion objectbox/lib/src/annotations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,33 @@ class Id {
/// telling which objects are new and which are already saved.
final bool assignable;

/// Use copyWith new id in the entity identifier setter.
/// For example, if your entity is immutable object,
/// you can define useCopyWith as follows
///
/// @Entity()
/// class ImmutableEntity {
/// @Id(useCopyWith: true)
/// final int? id;
///
/// final int payload;
///
/// ImmutableEntity copyWith({int? id, int? unique, int? payload}) =>
/// ImmutableEntity(
/// id: id,
/// unique: unique ?? this.unique,
/// payload: payload ?? this.payload,
/// );
///
/// ImmutableEntity{this.id, required this.unique, required this.payload});
///
/// ImmutableEntity copyWithId(int newId) =>
/// (id != newId) ? entity.copyWith(id: newId) : this;
/// }
final bool useCopyWith;

/// Create an Id annotation.
const Id({this.assignable = false});
const Id({this.assignable = false, this.useCopyWith = false});
}

/// Transient annotation marks fields that should not be stored in the database.
Expand Down
6 changes: 3 additions & 3 deletions objectbox/lib/src/modelinfo/entity_definition.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ class EntityDefinition<T> {
final int Function(T, fb.Builder) objectToFB;
final T Function(Store, ByteData) objectFromFB;
final int? Function(T) getId;
final void Function(T, int) setId;
final List<ToOne> Function(T) toOneRelations;
final Map<RelInfo, ToMany> Function(T) toManyRelations;
final T Function(T, int) setId;
final List<ToOneRelationProvider> Function(T) toOneRelations;
final Map<RelInfo, ToManyRelationProvider> Function(T) toManyRelations;

const EntityDefinition(
{required this.model,
Expand Down
5 changes: 5 additions & 0 deletions objectbox/lib/src/modelinfo/enums.dart
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ abstract class OBXPropertyFlags {
static const int UNIQUE_ON_CONFLICT_REPLACE = 32768;
}

abstract class OBXPropertyGeneratorFlags {
/// Use copyWithId in id setter
static const int USE_COPY_WITH_ID = 65536;
}

abstract class OBXPropertyType {
/// < 1 byte
static const int Bool = 1;
Expand Down
20 changes: 19 additions & 1 deletion objectbox/lib/src/modelinfo/modelproperty.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class ModelProperty {

late String _name;

late int _type, _flags;
late int _type, _flags, _generatorFlags;
IdUid? _indexId;
ModelEntity? entity;
String? relationTarget;
Expand Down Expand Up @@ -51,6 +51,16 @@ class ModelProperty {
_flags = value;
}

int get generatorFlags => _generatorFlags;

set generatorFlags(int? value) {
if (value == null || value < 0) {
throw ArgumentError('generator flags must be defined and may not be < 0');
}

_generatorFlags = value;
}

String get dartFieldType => _dartFieldType!;

set dartFieldType(String value) => _dartFieldType = value;
Expand Down Expand Up @@ -92,6 +102,7 @@ class ModelProperty {
// used in code generator
ModelProperty.create(this.id, String? name, int? type,
{int flags = 0,
int generatorFlags = 0,
String? indexId,
this.entity,
String? dartFieldType,
Expand All @@ -101,6 +112,7 @@ class ModelProperty {
this.name = name;
this.type = type;
this.flags = flags;
this.generatorFlags = generatorFlags;
this.indexId = indexId == null ? null : IdUid.fromString(indexId);
}

Expand All @@ -110,18 +122,21 @@ class ModelProperty {
required String name,
required int type,
required int flags,
int? generatorFlags,
IdUid? indexId,
this.relationTarget})
: _name = name,
_type = type,
_flags = flags,
_generatorFlags = generatorFlags ?? 0,
_indexId = indexId,
uidRequest = false;

ModelProperty.fromMap(Map<String, dynamic> data, ModelEntity? entity)
: this.create(IdUid.fromString(data['id'] as String?),
data['name'] as String?, data['type'] as int?,
flags: data['flags'] as int? ?? 0,
generatorFlags: data['generatorFlags'] as int? ?? 0,
indexId: data['indexId'] as String?,
entity: entity,
dartFieldType: data['dartFieldType'] as String?,
Expand All @@ -134,6 +149,7 @@ class ModelProperty {
ret['name'] = name;
ret['type'] = type;
if (flags != 0) ret['flags'] = flags;
if (generatorFlags != 0) ret['generatorFlags'] = generatorFlags;
if (indexId != null) ret['indexId'] = indexId!.toString();
if (relationTarget != null) ret['relationTarget'] = relationTarget;
if (!forModelJson && _dartFieldType != null) {
Expand All @@ -144,6 +160,7 @@ class ModelProperty {
}

bool hasFlag(int flag) => (flags & flag) == flag;
bool hasGeneratorFlag(int flag) => (generatorFlags & flag) == flag;

bool hasIndexFlag() =>
hasFlag(OBXPropertyFlags.INDEXED) ||
Expand All @@ -167,6 +184,7 @@ class ModelProperty {
result += ' type:${obxPropertyTypeToString(type)}';
if (!isSigned) result += ' unsigned';
result += ' flags:$flags';
result += ' generatorFlags:$generatorFlags';

if (hasIndexFlag()) {
result += ' index:' +
Expand Down
Loading