I'm trying to use sambas database as I need it for the web version of my app but I'm struggling with it.
I have an OpeningTimes form in my app that I save to db as a Map.
The logic I use in the saving method is: Load the form from db using the userName parameter of the OpeningTimes object I pass in , if none is found then save it, if instead a record is found then update it with the passed in object. I'm using flutter_bloc to manage it all so in OpeningTimesBloc when the form is saved to db, it loads it and yield it so to update UI to last saved sate. The problem is that when loading from db after updating, the loaded value is not the one passed in update method( eg monMorAct: to be updated from true to false):
flutter: updateOpeningTimes() : update opening time received {userName: zazza zenigata, monMorOp: 10:00, monMorCl: 19:00, monMorAct: false, monAftOp: , monAftCl: , monAftAct: false, tueMorOp: , tueMorCl: , tueMorAct: false, tueAftOp: , tueAftCl: , tueAftAct: false, wedMorOp: , wedMorCl: , wedMorAct: false, wedAftOp: , wedAftCl: , wedAftAct: false, thuMorOp: , thuMorCl: , thuMorAct: false, thuAftOp: , thuAftCl: , thuAftAct: false, friMorOp: , friMorCl: , friMorAct: false, friAftOp: , friAftCl: , friAftAct: false, satMorOp: , satMorCl: , satMorAct: false, satAftOp: , satAftCl: , satAftAct: false, sunMorOp: , sunMorCl: , sunMorAct: false, sunAftOp: , sunAftCl: , sunAftAct: false}
but when loading it:
flutter: loadOpeningTimes() snapshot is: Record(openingTimeStorage, 1) {userName: zazza zenigata, monMorOp: 10:00, monMorCl: 19:00, monMorAct: true, monAftOp: , monAftCl: , monAftAct: false, tueMorOp: , tueMorCl: , tueMorAct: false, tueAftOp: , tueAftCl: , tueAftAct: false, wedMorOp: , wedMorCl: , wedMorAct: false, wedAftOp: , wedAftCl: , wedAftAct: false, thuMorOp: , thuMorCl: , thuMorAct: false, thuAftOp: , thuAftCl: , thuAftAct: false, friMorOp: , friMorCl: , friMorAct: false, friAftOp: , friAftCl: , friAftAct: false, satMorOp: , satMorCl: , satMorAct: false, satAftOp: , satAftCl: , satAftAct: false, sunMorOp: , sunMorCl: , sunMorAct: false, sunAftOp: , sunAftCl: , sunAftAct: false}
Can you spot what I am doing wrong?
As always thank you very much for your time and help.
This is the singleton:
import 'dart:async';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sembast/sembast.dart';
import 'package:sembast/sembast_io.dart';
class AppDatabase {
// Singleton instance
static final AppDatabase _singleton = AppDatabase._();
// Singleton accessor
static AppDatabase get instance => _singleton;
// Completer is used for transforming synchronous code into asynchronous code.
Completer<Database> _dbOpenCompleter;
// A private constructor. Allows us to create instances of AppDatabase
// only from within the AppDatabase class itself.
AppDatabase._();
// Sembast database object
Database _database;
// Database object accessor
Future<Database> get database async {
// If completer is null, AppDatabaseClass is newly instantiated, so database is not yet opened
if (_dbOpenCompleter == null) {
_dbOpenCompleter = Completer();
// Calling _openDatabase will also complete the completer with database instance
_openDatabase();
}
// If the database is already opened, awaiting the future will happen instantly.
// Otherwise, awaiting the returned future will take some time - until complete() is called
// on the Completer in _openDatabase() below.
return _dbOpenCompleter.future;
}
Future _openDatabase() async {
// Get a platform-specific directory where persistent app data can be stored
final appDocumentDir = await getApplicationDocumentsDirectory();
// Path with the form: /platform-specific-directory/demo.db
final dbPath = join(appDocumentDir.path, 'demo.db');
final database = await databaseFactoryIo.openDatabase(dbPath);
// Any code awaiting the Completer's future will now start executing
_dbOpenCompleter.complete(database);
}
}
This is the bloc:
class OpeningTimesBloc extends Bloc<OpeningTimesEvent, OpeningTimesState> {
OpeningTimesRepository _openingTimesRepository = OpeningTimesRepository();
#override
OpeningTimesState get initialState => InitialState();
#override
Stream<OpeningTimesState> mapEventToState(OpeningTimesEvent event) async* {
if (event is LoadOpeningTimes) {
print('GetOpeningTimes event received');
yield* _mapLoadOpeningTimesToState(event);
}
if (event is SaveOpeningTimes) {
yield* _mapSaveOpeningTimesToState(event);
}
}
Stream<OpeningTimesState> _mapLoadOpeningTimesToState(
LoadOpeningTimes event) async* {
OpeningTimes openingTimes = await _openingTimesRepository.loadOpeningTimes(
userName: event.user.name);
print(
'_mapLoadOpeningTimesToState() loaded openingTimes is: ${openingTimes.toMap().toString()}');
yield ShopOpeningTimes(openingTimes);
}
Stream<OpeningTimesState> _mapSaveOpeningTimesToState(
SaveOpeningTimes event) async* {
await _openingTimesRepository.saveOpeningTimes(event.openingTimes);
add(LoadOpeningTimes(event.user));
}
}
this is the repository:
import 'package:sembast/sembast.dart';
class OpeningTimesRepository {
// name for the storage
static const String openingTimeStorage = 'openingTimeStorage';
// storage reference
final _openingTimesFolder = intMapStoreFactory.store(openingTimeStorage);
// database instance
Future<Database> get _db async => await AppDatabase.instance.database;
Future saveOpeningTimes(OpeningTimes openingTimes) async {
print(
'saveOpeningTimes(): save opening times received ${openingTimes.toMap().toString()}'); // prints correct
final snapshot = await loadOpeningTimes(userName: openingTimes.userName);
print('saveOpeningTimes() snapshot is $snapshot');
if (snapshot == null) {
print('opening times are to save');
await _openingTimesFolder.add(await _db, openingTimes.toMap());
} else {
print('opening times are to update');
await updateOpeningTimes(openingTimes);
}
}
Future updateOpeningTimes(OpeningTimes openingTimes) async {
print(
'updateOpeningTimes() : update opening time received ${openingTimes.toMap().toString()}'); // correct
final Finder finder = Finder(filter: Filter.byKey(openingTimes.userName));
await _openingTimesFolder.update(await _db, openingTimes.toMap(),
finder: finder);
}
Future<OpeningTimes> loadOpeningTimes({String userName}) async {
print(
'loadOpeningTimes() called userName is $userName'); // prints correct userName
final finder = Finder(sortOrders: [SortOrder('userName')]);
final snapshot =
await _openingTimesFolder.findFirst(await _db, finder: finder);
print('loadOpeningTimes() snapshot is: ${snapshot}'); // correct
return OpeningTimes.fromMap(snapshot.value);
}
}
and this is the model:
import 'package:flutter/material.dart';
class OpeningTimes {
int id;
final String userName;
final String monMorOp;
final String monMorCl;
final bool monMorAct;
final String monAftOp;
final String monAftCl;
final bool monAftAct;
final String tueMorOp;
final String tueMorCl;
final bool tueMorAct;
final String tueAftOp;
final String tueAftCl;
final bool tueAftAct;
final String wedMorOp;
final String wedMorCl;
final bool wedMorAct;
final String wedAftOp;
final String wedAftCl;
final bool wedAftAct;
final String thuMorOp;
final String thuMorCl;
final bool thuMorAct;
final String thuAftOp;
final String thuAftCl;
final bool thuAftAct;
final String friMorOp;
final String friMorCl;
final bool friMorAct;
final String friAftOp;
final String friAftCl;
final bool friAftAct;
final String satMorOp;
final String satMorCl;
final bool satMorAct;
final String satAftOp;
final String satAftCl;
final bool satAftAct;
final String sunMorOp;
final String sunMorCl;
final bool sunMorAct;
final String sunAftOp;
final String sunAftCl;
final bool sunAftAct;
OpeningTimes(
{#required this.userName,
#required this.monMorOp,
#required this.monMorCl,
#required this.monMorAct,
#required this.monAftOp,
#required this.monAftCl,
#required this.monAftAct,
#required this.tueMorOp,
#required this.tueMorCl,
#required this.tueMorAct,
#required this.tueAftOp,
#required this.tueAftCl,
#required this.tueAftAct,
#required this.wedMorOp,
#required this.wedMorCl,
#required this.wedMorAct,
#required this.wedAftOp,
#required this.wedAftCl,
#required this.wedAftAct,
#required this.thuMorOp,
#required this.thuMorCl,
#required this.thuMorAct,
#required this.thuAftOp,
#required this.thuAftCl,
#required this.thuAftAct,
#required this.friMorOp,
#required this.friMorCl,
#required this.friMorAct,
#required this.friAftOp,
#required this.friAftCl,
#required this.friAftAct,
#required this.satMorOp,
#required this.satMorCl,
#required this.satMorAct,
#required this.satAftOp,
#required this.satAftCl,
#required this.satAftAct,
#required this.sunMorOp,
#required this.sunMorCl,
#required this.sunMorAct,
#required this.sunAftOp,
#required this.sunAftCl,
#required this.sunAftAct});
#override
List<Object> get props => [
userName,
monMorOp,
monMorCl,
monMorAct,
monAftOp,
monAftCl,
monAftAct,
tueMorOp,
tueMorCl,
tueMorAct,
tueAftOp,
tueAftCl,
tueAftAct,
wedMorOp,
wedMorCl,
wedMorAct,
wedAftOp,
wedAftCl,
wedAftAct,
thuMorOp,
thuMorCl,
thuMorAct,
thuAftOp,
thuAftCl,
thuAftAct,
friMorOp,
friMorCl,
friMorAct,
friAftOp,
friAftCl,
friAftAct,
satMorOp,
satMorCl,
satMorAct,
satAftOp,
satAftCl,
satAftAct,
sunMorOp,
sunMorCl,
sunMorAct,
sunAftOp,
sunAftCl,
sunAftAct
];
factory OpeningTimes.fromMap(Map<String, dynamic> map) => new OpeningTimes(
userName: map['userName'],
monMorOp: map['monMorOp'],
monMorCl: map['monMorCl'],
monMorAct: map['monMorAct'],
monAftOp: map['monAftOp'],
monAftCl: map['monAftCl'],
monAftAct: map['monAftAct'],
tueMorOp: map['tueMorOp'],
tueMorCl: map['tueMorCl'],
tueMorAct: map['tueMorAct'],
tueAftOp: map['tueAftOp'],
tueAftCl: map['tueAftCl'],
tueAftAct: map['tueAftAct'],
wedMorOp: map['wedMorOp'],
wedMorCl: map['wedMorCl'],
wedMorAct: map['wedMorAct'],
wedAftOp: map['wedAftOp'],
wedAftCl: map['wedAftCl'],
wedAftAct: map['wedAftAct'],
thuMorOp: map['thuMorOp'],
thuMorCl: map['thuMorCl'],
thuMorAct: map['thuMorAct'],
thuAftOp: map['thuAftOp'],
thuAftCl: map['thuAftCl'],
thuAftAct: map['thuAftAct'],
friMorOp: map['friMorOp'],
friMorCl: map['friMorCl'],
friMorAct: map['friMorAct'],
friAftOp: map['friAftOp'],
friAftCl: map['friAftCl'],
friAftAct: map['friAftAct'],
satMorOp: map['satMorOp'],
satMorCl: map['satMorCl'],
satMorAct: map['satMorAct'],
satAftOp: map['satAftOp'],
satAftCl: map['satAftCl'],
satAftAct: map['satAftAct'],
sunMorOp: map['sunMorOp'],
sunMorCl: map['sunMorCl'],
sunMorAct: map['sunMorAct'],
sunAftOp: map['sunAftOp'],
sunAftCl: map['sunAftCl'],
sunAftAct: map['sunAftAct'],
);
Map<String, dynamic> toMap() => {
'userName': userName,
'monMorOp': monMorOp,
'monMorCl': monMorCl,
'monMorAct': monMorAct,
'monAftOp': monAftOp,
'monAftCl': monAftCl,
'monAftAct': monAftAct,
'tueMorOp': tueMorOp,
'tueMorCl': tueMorCl,
'tueMorAct': tueMorAct,
'tueAftOp': tueAftOp,
'tueAftCl': tueAftCl,
'tueAftAct': tueAftAct,
'wedMorOp': wedMorOp,
'wedMorCl': wedMorCl,
'wedMorAct': tueMorAct,
'wedAftOp': wedAftOp,
'wedAftCl': wedAftCl,
'wedAftAct': wedAftAct,
'thuMorOp': thuMorOp,
'thuMorCl': thuMorCl,
'thuMorAct': thuMorAct,
'thuAftOp': thuAftOp,
'thuAftCl': thuAftCl,
'thuAftAct': thuAftAct,
'friMorOp': friMorOp,
'friMorCl': friMorCl,
'friMorAct': friMorAct,
'friAftOp': friAftOp,
'friAftCl': friAftCl,
'friAftAct': friAftAct,
'satMorOp': satMorOp,
'satMorCl': satMorCl,
'satMorAct': satMorAct,
'satAftOp': satAftOp,
'satAftCl': satAftCl,
'satAftAct': satAftAct,
'sunMorOp': sunMorOp,
'sunMorCl': sunMorCl,
'sunMorAct': sunMorAct,
'sunAftOp': sunAftOp,
'sunAftCl': sunAftCl,
'sunAftAct': sunAftAct
};
}
Updated repository methods to use autogenerated key:
static const String openingTimeStorage = 'openingTimeStorage';
// storage reference
final _openingTimesFolder = intMapStoreFactory.store(openingTimeStorage);
// database instance
Future<Database> get _db async => await AppDatabase.instance.database;
Future saveOpeningTimes(OpeningTimes openingTimes) async {
print(
'saveOpeningTimes(): save opening times received ${openingTimes.toMap().toString()}'); // prints correct
final snapshot = await loadOpeningTimes();
print('saveOpeningTimes() snapshot is $snapshot');
if (snapshot == null) {
print('opening times are to save');
await _openingTimesFolder.add(await _db, openingTimes.toMap());
return;
} else {
print('opening times are to update');
await updateOpeningTimes(openingTimes);
}
}
Future updateOpeningTimes(OpeningTimes openingTimes) async {
print(
'updateOpeningTimes() : update opening time received ${openingTimes.toMap().toString()}'); // correct
final Finder finder = Finder(
filter: Filter.byKey(
_openingTimesFolder.record(openingTimes.id).get(await _db)));
await _openingTimesFolder.update(await _db, openingTimes.toMap(),
finder: finder);
}
Future<OpeningTimes> loadOpeningTimes() async {
final finder = Finder(sortOrders: [SortOrder('userName')]);
final snapshot =
await _openingTimesFolder.findFirst(await _db, finder: finder);
if (snapshot != null) {
print('loadOpeningTimes() snapshot is: $snapshot'); // correct
OpeningTimes openingTimes = OpeningTimes.fromMap(snapshot.value);
openingTimes.id = snapshot.key;
return openingTimes;
} else {
return null;
}
}
on updating I get flutter: Erros is Invalid argument(s): Record key cannot be null.
In updateOpeningTimes your filter looks incorrect.
final Finder finder = Finder(filter: Filter.byKey(openingTimes.userName));
Here you are looking for objects with the userName as the key but the key is an int (autogenerated) here so it cannot match. You should have something like:
final Finder finder = Finder(filter: Filter.equals('userName', userName));
In loadOpeningTimes, you just take the first record sorted by userName. If you look for an explicit userName, you should use the same finder than for update.
As a side note, it is a lot faster to load records by key: store.record(key).get(db)
I am trying to implement a simple change for an apex class in production. I have the proper class and proper test class. The test class runs successfully in sandbox without errors, but apparently the error is coming from the TestHelper default test class in Salesforce. When trying to deploy in production it throws the error "Method does not exist or incorrect signature: void createUser(Id, String, String, Date, Integer) from the type TestHelper"
I've tried the usual of changing the method it references to public static void, but to no avail, it throws errors in code
This is my test class:
#isTest
private class OppLineItemInvntryBO_AType_OppStge_Test {
#testSetup public static void setup() {
Profile p = [SELECT Id FROM Profile
WHERE Name = 'profile1' LIMIT 1];
Date myDate = Date.newinstance(2019,07,01);
User testUser = TestHelper.createUser(p.Id,
'company1','legalentity1',myDate,327001);
And this is my TestHelper class:
public with sharing class TestHelper {
public static User createUser(Id profileId, String company) {
Integer rnd = getRandomNumber(10000);
User user = new User(
Username = 'john.doe#acme.com' + String.valueOf(rnd),
Email = 'john.doe' + String.valueOf(rnd) + '#acme.com',
LastName = 'Doe',
FirstName = 'John',
Alias = 'JD' + String.valueOf(rnd),
ProfileId = profileId,
LocaleSidKey = 'en_US',
LanguageLocaleKey = 'en_US',
TimeZoneSidKey = 'America/Los_Angeles',
EmailEncodingKey='UTF-8',
CompanyName = company);
insert user;
return user;
}
public static Integer getRandomNumber(Integer size){
Double d = math.random() * size;
return d.intValue();
}
}
The full error is this:
API Name - OppLineItemInvntryBO_AType_OppStge_Test
Type - Apex Class
Line - 14
Column - 36
Error Message - Method does not exist or incorrect signature: void createUser(Id, String, String, Date, Integer) from the type TestHelper
You are invoking the createUser method with 5 parameters in OppLineItemInvntryBO_AType_OppStge_Test class where as in the TestHelper class , the createUser method accepts only 2 parameters. Thats why you are getting this error. Try to invoke the method with correct parameters.