Skip to content

Commit 3a3e5ce

Browse files
committed
ToOne - support initializing the target in the constructor
1 parent 2df585a commit 3a3e5ce

File tree

3 files changed

+115
-57
lines changed

3 files changed

+115
-57
lines changed

generator/lib/src/code_chunks.dart

Lines changed: 97 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import 'package:pubspec_parse/pubspec_parse.dart';
66
import 'package:source_gen/source_gen.dart' show InvalidGenerationSourceError;
77

88
class CodeChunks {
9-
static String objectboxDart(
10-
ModelInfo model, List<String> imports, Pubspec? pubspec) =>
9+
static String objectboxDart(ModelInfo model, List<String> imports,
10+
Pubspec? pubspec) =>
1111
"""
1212
// GENERATED CODE - DO NOT MODIFY BY HAND
1313
@@ -35,7 +35,8 @@ class CodeChunks {
3535
${defineModel(model)}
3636
3737
final bindings = <Type, EntityDefinition>{
38-
${model.entities.mapIndexed((i, entity) => "${entity.name}: ${entityBinding(i, entity)}").join(",\n")}
38+
${model.entities.mapIndexed((i, entity) => "${entity
39+
.name}: ${entityBinding(i, entity)}").join(",\n")}
3940
};
4041
4142
return ModelDefinition(model, bindings);
@@ -53,9 +54,13 @@ class CodeChunks {
5354
int$nullableOperator fileMode,
5455
int$nullableOperator maxReaders,
5556
bool queriesCaseSensitiveDefault = true,
56-
String$nullableOperator macosApplicationGroup})${obxFlutter ? ' async' : ''} =>
57+
String$nullableOperator macosApplicationGroup})${obxFlutter
58+
? ' async'
59+
: ''} =>
5760
Store(getObjectBoxModel(),
58-
directory: directory${obxFlutter ? ' ?? (await defaultStoreDirectory()).path' : ''},
61+
directory: directory${obxFlutter
62+
? ' ?? (await defaultStoreDirectory()).path'
63+
: ''},
5964
maxDBSizeInKB: maxDBSizeInKB,
6065
fileMode: fileMode,
6166
maxReaders: maxReaders,
@@ -187,9 +192,11 @@ class CodeChunks {
187192
// Such ID must already be set, i.e. it could not have been assigned.
188193
return '''{
189194
if (object.${propertyFieldName(entity.idProperty)} != id) {
190-
throw ArgumentError('Field ${entity.name}.${propertyFieldName(entity.idProperty)} is read-only '
195+
throw ArgumentError('Field ${entity.name}.${propertyFieldName(
196+
entity.idProperty)} is read-only '
191197
'(final or getter-only) and it was declared to be self-assigned. '
192-
'However, the currently inserted object (.${propertyFieldName(entity.idProperty)}=\${object.${propertyFieldName(entity.idProperty)}}) '
198+
'However, the currently inserted object (.${propertyFieldName(
199+
entity.idProperty)}=\${object.${propertyFieldName(entity.idProperty)}}) '
193200
"doesn't match the inserted ID (ID \$id). "
194201
'You must assign an ID before calling [box.put()].');
195202
}
@@ -209,7 +216,8 @@ class CodeChunks {
209216
return '[]';
210217
default:
211218
throw InvalidGenerationSourceError(
212-
'Cannot figure out default value for field: ${p.fieldType} ${p.name}');
219+
'Cannot figure out default value for field: ${p.fieldType} ${p
220+
.name}');
213221
}
214222
}
215223

@@ -286,13 +294,14 @@ class CodeChunks {
286294
} else if (p.type == OBXPropertyType.DateNano) {
287295
if (p.fieldIsNullable) {
288296
accessorSuffix =
289-
' == null ? null : object.${propertyFieldName(p)}';
297+
' == null ? null : object.${propertyFieldName(p)}';
290298
if (p.entity!.nullSafetyEnabled) accessorSuffix += '!';
291299
}
292300
accessorSuffix += '.microsecondsSinceEpoch * 1000';
293301
}
294302
}
295-
return 'fbb.add${_propertyFlatBuffersType[p.type]}($fbField, object.${propertyFieldName(p)}$accessorSuffix);';
303+
return 'fbb.add${_propertyFlatBuffersType[p
304+
.type]}($fbField, object.${propertyFieldName(p)}$accessorSuffix);';
296305
}
297306
});
298307

@@ -316,14 +325,16 @@ class CodeChunks {
316325
// property to its index in entity.properties.
317326
final fieldIndexes = <String, int>{};
318327
final fieldReaders =
319-
entity.properties.mapIndexed((int index, ModelProperty p) {
328+
entity.properties.mapIndexed((int index, ModelProperty p) {
320329
fieldIndexes[propertyFieldName(p)] = index;
321330

322331
String? fbReader;
323332
var readFieldOrNull = () =>
324-
'const $fbReader.vTableGetNullable(buffer, rootOffset, ${propertyFlatBuffersvTableOffset(p)})';
333+
'const $fbReader.vTableGetNullable(buffer, rootOffset, ${propertyFlatBuffersvTableOffset(
334+
p)})';
325335
var readFieldNonNull = ([String? defaultValue]) =>
326-
'const $fbReader.vTableGet(buffer, rootOffset, ${propertyFlatBuffersvTableOffset(p)}, ${defaultValue ?? fieldDefaultValue(p)})';
336+
'const $fbReader.vTableGet(buffer, rootOffset, ${propertyFlatBuffersvTableOffset(
337+
p)}, ${defaultValue ?? fieldDefaultValue(p)})';
327338
var readField =
328339
() => p.fieldIsNullable ? readFieldOrNull() : readFieldNonNull();
329340
final valueVar = '${propertyFieldName(p)}Value';
@@ -336,7 +347,8 @@ class CodeChunks {
336347
fbReader = 'fb.ListReader<int>(fb.Int8Reader())';
337348
if (p.fieldIsNullable) {
338349
preLines.add('final $valueVar = ${readFieldOrNull()};');
339-
return '$valueVar == null ? null : ${p.fieldType}.fromList($valueVar)';
350+
return '$valueVar == null ? null : ${p
351+
.fieldType}.fromList($valueVar)';
340352
} else {
341353
return '${p.fieldType}.fromList(${readFieldNonNull('[]')})';
342354
}
@@ -363,43 +375,32 @@ class CodeChunks {
363375
}
364376
} else {
365377
if (p.type == OBXPropertyType.Date) {
366-
return "DateTime.fromMillisecondsSinceEpoch(${readFieldNonNull('0')})";
378+
return "DateTime.fromMillisecondsSinceEpoch(${readFieldNonNull(
379+
'0')})";
367380
} else if (p.type == OBXPropertyType.DateNano) {
368-
return "DateTime.fromMicrosecondsSinceEpoch((${readFieldNonNull('0')} / 1000).round())";
381+
return "DateTime.fromMicrosecondsSinceEpoch((${readFieldNonNull(
382+
'0')} / 1000).round())";
369383
}
370384
}
371385
throw InvalidGenerationSourceError(
372-
'Invalid property data type ${p.type} for a DateTime field ${entity.name}.${p.name}');
386+
'Invalid property data type ${p.type} for a DateTime field ${entity
387+
.name}.${p.name}');
373388
}
374389
return readField();
375390
}).toList(growable: false);
376391

377-
// add initializers for relations
378-
entity.properties.forEachIndexed((int index, ModelProperty p) {
379-
if (p.isRelation) {
380-
postLines.add(
381-
'object.${propertyFieldName(p)}.targetId = ${fieldReaders[index]};'
382-
'\n object.${propertyFieldName(p)}.attach(store);');
383-
}
384-
});
385-
386-
postLines.addAll(entity.relations.map((ModelRelation rel) =>
387-
'InternalToManyAccess.setRelInfo(object.${rel.name}, store, ${relInfo(entity, rel)}, store.box<${entity.name}>());'));
388-
389-
postLines.addAll(entity.backlinks.map((ModelBacklink bl) {
390-
return 'InternalToManyAccess.setRelInfo(object.${bl.name}, store, ${backlinkRelInfo(entity, bl)}, store.box<${entity.name}>());';
391-
}));
392-
393392
// try to initialize as much as possible using the constructor
394393
entity.constructorParams.forEachWhile((String declaration) {
395394
// See [EntityResolver.constructorParams()] for the format.
396-
final paramName = declaration.split(' ')[0];
397-
final paramType = declaration.split(' ')[1];
395+
final declarationParts = declaration.split(' ');
396+
final paramName = declarationParts[0];
397+
final paramType = declarationParts[1];
398+
final paramDartType = declarationParts[2];
398399

399400
final index = fieldIndexes[paramName];
400401
if (index == null) {
401402
// If we can't find a positional param, we can't use the constructor at all.
402-
if (paramType == 'positional') {
403+
if (paramType == 'positional' || paramType == 'required-named') {
403404
log.warning("Cannot use the default constructor of '${entity.name}': "
404405
"don't know how to initialize param $paramName - no such property.");
405406
constructorLines.clear();
@@ -411,13 +412,26 @@ class CodeChunks {
411412
return true; // continue to the next param
412413
}
413414

415+
var paramValueCode = fieldReaders[index];
416+
if (entity.properties[index].isRelation) {
417+
if (paramDartType.startsWith('ToOne<')) {
418+
paramValueCode = 'ToOne(targetId: ${paramValueCode})';
419+
} else if (paramType == 'optional-named') {
420+
log.info("Skipping constructor parameter $paramName on "
421+
"'${entity.name}': the matching field is a relation but the type "
422+
"isn't - don't know how to initialize this parameter.");
423+
return true;
424+
}
425+
}
426+
414427
switch (paramType) {
415428
case 'positional':
416429
case 'optional':
417-
constructorLines.add(fieldReaders[index]);
430+
constructorLines.add(paramValueCode);
418431
break;
419-
case 'named':
420-
constructorLines.add('$paramName: ${fieldReaders[index]}');
432+
case 'required-named':
433+
case 'optional-named':
434+
constructorLines.add('$paramName: ${paramValueCode}');
421435
break;
422436
default:
423437
throw InvalidGenerationSourceError(
@@ -438,33 +452,56 @@ class CodeChunks {
438452
}
439453
});
440454

455+
// add initializers for relations
456+
entity.properties.forEachIndexed((int index, ModelProperty p) {
457+
if (!p.isRelation) return;
458+
if (fieldReaders[index].isNotEmpty) {
459+
postLines.add(
460+
'object.${propertyFieldName(
461+
p)}.targetId = ${fieldReaders[index]};');
462+
}
463+
postLines.add('object.${propertyFieldName(p)}.attach(store);');
464+
});
465+
466+
postLines.addAll(entity.relations.map((ModelRelation rel) =>
467+
'InternalToManyAccess.setRelInfo(object.${rel.name}, store, ${relInfo(
468+
entity, rel)}, store.box<${entity.name}>());'));
469+
470+
postLines.addAll(entity.backlinks.map((ModelBacklink bl) {
471+
return 'InternalToManyAccess.setRelInfo(object.${bl
472+
.name}, store, ${backlinkRelInfo(entity, bl)}, store.box<${entity
473+
.name}>());';
474+
}));
475+
441476
return '''(Store store, ByteData fbData) {
442477
final buffer = fb.BufferContext(fbData);
443478
final rootOffset = buffer.derefObject(0);
444479
${preLines.join('\n')}
445-
final object = ${entity.name}(${constructorLines.join(', \n')})${cascadeLines.join('\n')};
480+
final object = ${entity.name}(${constructorLines.join(
481+
', \n')})${cascadeLines.join('\n')};
446482
${postLines.join('\n')}
447483
return object;
448484
}''';
449485
}
450486

451487
static String toOneRelations(ModelEntity entity) =>
452488
'[' +
453-
entity.properties
454-
.where((ModelProperty prop) => prop.isRelation)
455-
.map((ModelProperty prop) => 'object.${propertyFieldName(prop)}')
456-
.join(',') +
457-
']';
489+
entity.properties
490+
.where((ModelProperty prop) => prop.isRelation)
491+
.map((ModelProperty prop) => 'object.${propertyFieldName(prop)}')
492+
.join(',') +
493+
']';
458494

459495
static String relInfo(ModelEntity entity, ModelRelation rel) =>
460-
'RelInfo<${entity.name}>.toMany(${rel.id.id}, object.${propertyFieldAccess(entity.idProperty, '!')})';
496+
'RelInfo<${entity.name}>.toMany(${rel.id
497+
.id}, object.${propertyFieldAccess(entity.idProperty, '!')})';
461498

462499
static String backlinkRelInfo(ModelEntity entity, ModelBacklink bl) {
463500
final srcEntity = entity.model.findEntityByName(bl.srcEntity);
464501
if (srcEntity == null) {
465502
throw InvalidGenerationSourceError(
466503
'Invalid relation backlink ${entity.name}.${bl.name} '
467-
'- source entity ${bl.srcEntity} not found.');
504+
'- source entity ${bl.srcEntity} not found.');
468505
}
469506

470507
// either of these will be set, based on the source field that matches
@@ -475,13 +512,13 @@ class CodeChunks {
475512
final matchingProps = srcEntity.properties
476513
.where((p) => p.isRelation && p.relationTarget == entity.name);
477514
final matchingRels =
478-
srcEntity.relations.where((r) => r.targetId == entity.id);
515+
srcEntity.relations.where((r) => r.targetId == entity.id);
479516
final candidatesCount = matchingProps.length + matchingRels.length;
480517
if (candidatesCount > 1) {
481518
throw InvalidGenerationSourceError(
482519
'Ambiguous relation backlink source for ${entity.name}.${bl.name}.'
483-
' Matching property: $matchingProps.'
484-
' Matching standalone relations: $matchingRels.');
520+
' Matching property: $matchingProps.'
521+
' Matching standalone relations: $matchingRels.');
485522
} else if (matchingProps.isNotEmpty) {
486523
srcProp = matchingProps.first;
487524
} else if (matchingRels.isNotEmpty) {
@@ -497,11 +534,14 @@ class CodeChunks {
497534

498535
if (srcRel != null) {
499536
return 'RelInfo<${srcEntity.name}>.toManyBacklink('
500-
'${srcRel.id.id}, object.${propertyFieldAccess(entity.idProperty, '!')})';
537+
'${srcRel.id.id}, object.${propertyFieldAccess(
538+
entity.idProperty, '!')})';
501539
} else if (srcProp != null) {
502540
return 'RelInfo<${srcEntity.name}>.toOneBacklink('
503-
'${srcProp.id.id}, object.${propertyFieldAccess(entity.idProperty, '!')}, '
504-
'(${srcEntity.name} srcObject) => srcObject.${propertyFieldName(srcProp)})';
541+
'${srcProp.id.id}, object.${propertyFieldAccess(
542+
entity.idProperty, '!')}, '
543+
'(${srcEntity.name} srcObject) => srcObject.${propertyFieldName(
544+
srcProp)})';
505545
} else {
506546
throw InvalidGenerationSourceError(
507547
'Unknown relation backlink source for ${entity.name}.${bl.name}');
@@ -511,9 +551,10 @@ class CodeChunks {
511551
static String toManyRelations(ModelEntity entity) {
512552
final definitions = <String>[];
513553
definitions.addAll(entity.relations.map(
514-
(ModelRelation rel) => '${relInfo(entity, rel)}: object.${rel.name}'));
554+
(ModelRelation rel) => '${relInfo(entity, rel)}: object.${rel
555+
.name}'));
515556
definitions.addAll(entity.backlinks.map((ModelBacklink bl) =>
516-
'${backlinkRelInfo(entity, bl)}: object.${bl.name}'));
557+
'${backlinkRelInfo(entity, bl)}: object.${bl.name}'));
517558
return '{${definitions.join(',')}}';
518559
}
519560

@@ -564,7 +605,7 @@ class CodeChunks {
564605
static final ${propertyFieldName(prop)} = ''';
565606
if (prop.isRelation) {
566607
propCode +=
567-
'QueryRelationToOne<${entity.name}, ${prop.relationTarget}>';
608+
'QueryRelationToOne<${entity.name}, ${prop.relationTarget}>';
568609
} else {
569610
propCode += 'Query${fieldType}Property<${entity.name}>';
570611
}

generator/lib/src/entity_resolver.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,9 @@ class EntityResolver extends Builder {
420420
var info = param.name;
421421
if (param.isRequiredPositional) info += ' positional';
422422
if (param.isOptionalPositional) info += ' optional';
423-
if (param.isNamed) info += ' named';
423+
if (param.isRequiredNamed) info += ' required-named';
424+
else if (param.isNamed) info += ' optional-named';
425+
info += ' ${param.type}';
424426
return info;
425427
}).toList(growable: false);
426428
}

objectbox/lib/src/relations/to_one.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,21 @@ class ToOne<EntityT> {
5656

5757
_ToOneValue<EntityT> _value = _ToOneValue<EntityT>.none();
5858

59+
/// Create a ToOne relationship. Normally, you don't assign the target yet.
60+
ToOne({EntityT? target, int? targetId}) {
61+
if (targetId != null) {
62+
if (target != null) {
63+
// May be a user error... and we can't check if (target.id == targetId).
64+
throw ArgumentError(
65+
'Provide at most one specification of a ToOne relation target: '
66+
'either [target] or [targetId] argument');
67+
}
68+
this.targetId = targetId;
69+
} else if (target != null) {
70+
this.target = target;
71+
}
72+
}
73+
5974
/// Get target object. If it's the first access, this reads from DB.
6075
EntityT? get target {
6176
if (_value._state == _ToOneState.lazy) {

0 commit comments

Comments
 (0)