Hey @charliecruzan,
Thanks for your message. I included all of the potentially related code here. Let me know your thoughts. I fiddled with it some more this morning in case there was a promise issue in the async redux dispatch. Doesn’t seem to be.
// db creation
const db = SQLite.openDatabase("mobile_flashcards.db"); // create a DB if none exists and otherwise open it
// sql queries
export const createDecks = "create table if not exists decks " + "(" + ([
"title TEXT PRIMARY KEY NOT NULL",
"category TEXT DEFAULT 'Uncategorized'", // category name or null
"create_date TEXT NOT NULL", // date iso string
"last_tested INTEGER", // date iso string - will contain same value as deckScores related date
"last_score REAL", // real number or null
].join(", ")) + ")";
export const createDeck = "INSERT INTO decks (title, create_date, category) VALUES (?, ?, ?)";
export const createCards = "CREATE TABLE IF NOT EXISTS cards " + "(" + ([
"card_id TEXT PRIMARY KEY", // date num in unix epoch
"deck_id TEXT NOT NULL",
"question TEXT NOT NULL",
"answer TEXT NOT NULL",
"FOREIGN KEY (deck_id) REFERENCES decks(title) ON UPDATE CASCADE ON DELETE CASCADE"
]).join(", ") + ")";
export const createCard = "INSERT INTO cards (card_id, deck_id, question, answer) VALUES (?, ?, ?, ?)";
export const getDecksAndCards = "SELECT * FROM decks INNER JOIN cards ON cards.deck_id = decks.title ORDER BY decks.title, card_id";
export const checkForExistingTable = "SELECT name FROM sqlite_master WHERE type='table' AND name=?";
// functions using sql queries
const boundNoLogTx = function(Query, params = []) {
// just shortens function call in initial data population
return this.executeSql(Query, params,
() => {},
(transaction, error) => errorHandler(this, error));
}
export function checkForExistingTable() {
// checks if decks exists - existing db
// returns bool representing that decks exists
return new Promise((res, rej) => db.transaction(tx => {
tx.executeSql(Queries.checkForExistingTable, ["decks"],
(_, { rows }) => res(rows._array.length > 0),
(_, error) => rej(error)
);
}));
}
export function populateInitialData(queryList = [
Queries.dropDeckScores,
Queries.dropCards,
Queries.dropDecks,
Queries.createDecks,
Queries.createCards,
Queries.createDeckScores,
]) {
console.log("populate data called");
return new Promise((res, rej) => db.transaction(tx => {
const func = boundLoggingTx.bind(tx); // to shorten function call by binding tx to this keyword
const noLogFunc = boundNoLogTx.bind(tx);
queryList.forEach((query) => noLogFunc(query));
Object.keys(decks).forEach((name) => {
noLogFunc(Queries.createDeck, [name, getSafeTimeISO(), "Uncategorized"]);
decks[name].questions.forEach(card => {
let time = getSafeTimeISO()
noLogFunc(Queries.createCard, [time, name, card.question, card.answer]);
});
});
tx.executeSql(Queries.getDecksAndCards, [],
(_, { rows }) => res(rows._array),
(_, error) => rej(error));
}));
}
export function getDecksAndCards() {
console.log("get decks and cards called");
return new Promise((res, rej) => db.transaction(tx => {
tx.executeSql(Queries.getDecksAndCards, [],
(_, { rows }) => res(rows._array),
(_, error) => rej(error));
})
);
}
export function createDeck(title, category = "Uncategorized", created = getCurrentTimeISOString()) {
return new Promise((res, rej) => db.transaction(tx => {
tx.executeSql(Queries.createDeck, [title, created, category],
(_, { rows }) => {},
(_, error) => rej(error)
);
tx.executeSql(Queries.getDeck, [title],
(_, { rows }) => res(rows._array),
(_, error) => rej(error));
})
);
}
export function createCard(deck_id, question, answer, card_id = getCurrentTimeISOString()) {
return new Promise((res, rej) => db.transaction(tx => {
tx.executeSql(Queries.createCard, [card_id, deck_id, question, answer],
(_, { rows }) => {},
(_, error) => rej(error));
tx.executeSql(Queries.getCard, [card_id],
(_, { rows }) => res(rows._array),
(_, error) => rej(error));
})
);
}
// redux actions related to receiving initial data
export const handleReceiveDecks = () => ( // uses redux-thunk
async (dispatch) => {
// dispatch(startLoading()); // not necessary as loading is currently extremely quick
try {
const existingData = await checkForExistingTable();
let data;
if (existingData) {
console.log("Is existing data:", existingData); // does get here and log true
data = await getDecksAndCards();
} else {
console.log("Is not existing data:", existingData);
data = await populateInitialData();
}
const [ decks, categories ] = formatDecksAndCards(data);
dispatch(receiveDecks(decks));
dispatch(receiveCategories(categories));
// dispatch(endLoading()); // not necessary as laoding is currently extremely quick
} catch (error) {
console.log(error);
}
}
)
const receiveDecks = (decks) => {
return {
type: RECEIVE_DECKS,
decks,
};
}
export function receiveCategories(categories) {
return {
type: RECEIVE_CATEGORIES,
categories,
};
}
function formatDecksAndCards(data) {
let decks = {};
let categories = {};
data.forEach((card) => {
// builds decks and categories from sql data for redux store
if (decks[card.title] !== undefined) {
// primary deck data already in decks - just add the card to the questions arr
decks[card.title].questions = decks[card.title].questions.concat([{
question: card.question,
answer: card.answer,
card_id: card.card_id,
deck_id: card.deck_id
}]);
} else {
// deck not yet created in decks
let { title, category, create_date, last_score, last_tested, card_id, question, answer, deck_id } = card;
decks[card.title] = {
title,
category,
create_date,
last_score,
last_tested,
questions: [
{
card_id,
question,
answer,
deck_id
}
]
};
if (category === null) {
// uncategorized deck
if (categories["Uncategorized"] !== undefined) {
// uncategorized category already exists in categories
categories["Uncategorized"].add(title);
} else {
// uncategorized category not yet created in categories
categories["Uncategorized"] = new Set([title]);
}
}
else if (categories[category] !== undefined) {
// category already exists in categories and category is not null
categories[category].add(title);
} else {
// category does not exist in categories - make new set
categories[category] = new Set([title]);
}
}
});
return [decks, categories];
}
// component that calls the function
class Home extends React.Component {
state = {
selectedCategory: "Show All",
isModalVisible: false,
}
componentDidMount() {
this.props.dispatch(handleReceiveDecks());
// this.askPermission(); // commented to prevent generating new notifications
clearLocalNotifications(); // for development
if (this.props.activeDeck !== null) {
this.props.dispatch(clearActiveDeck());
}
}
// etc...
// relevant section of decks reducer
switch(action.type) {
case RECEIVE_DECKS:
return {
...store,
...action.decks
};
// relevant section of categories reducer
switch (action.type) {
case RECEIVE_CATEGORIES:
return {
...store,
...action.categories,
};
// related helpers
const setIncrement = () => {
// deterministic time increment to prevent primary key collisions in database
let cache = 0;
return () => {
cache+=1;
return cache;
}
}
const getIncrement = setIncrement();
const getSafeTime = () => {
return new Date(Date.now()+getIncrement());
}
export const getSafeTimeISO = () => {
// prevents collisions in initial data population
// for in-app use - use getCurrentTimeISOString
return getSafeTime().toISOString();
}