· 8 years ago · Mar 24, 2018, 11:46 AM
1import SQLite from 'react-native-sqlite-2'
2import {Platform} from "react-native";
3
4const openedDb = {};
5
6/**
7 * Wrapper for react-native-sqlite-2.
8 * Methods:
9 * <ul>
10 * <li>Promise<Array<Object>> {@link execute}(statement, ...variables) : Execute a statement. Variables will be binded into the statement. It returns the list of items matching the statement.</li>
11 * <li>Promise<Array<Object>> {@link executeAdvanced}(statement, ...variables) : Same as {@link execute}, except that the returned value is {@link Database.Result}.</li>
12 * </ul>
13 */
14class Database {
15 constructor(name){
16 // check opened db
17 if (!openedDb[name])
18 openedDb[name] = SQLite.openDatabase(name + '.db', '1.0', '', 1);
19
20 this._database = openedDb[name];
21 }
22
23 /**
24 * @param {String} statement Statement to execute.
25 * @param {Array.<String|Number|Boolean>} variables Variables to bind into the statement.
26 * @returns {Promise.<Array.<Object>>} The array of all items matching the statement.
27 */
28 execute(statement, ...variables){
29 return this.executeAdvanced(statement, ...variables)
30 // return loaded items directly
31 .then(response => response.rows);
32 }
33
34 /**
35 * Same as {@link execute}, but returns a result with more details.
36 * @param {String} statement Statement to execute.
37 * @param {Array.<String|Number|Boolean>} variables Variables to bind into the statement.
38 * @returns {Promise.<Database.Result>} The statement's result.
39 */
40 executeAdvanced(statement, ...variables){
41 return new Promise((resolve, reject) =>
42 this._database.transaction(transaction =>
43 transaction.executeSql(statement, variables,
44 (_, result) => {
45 // convert
46 result.rows = result.rows._array;
47 // return the entire result
48 resolve(result);
49 },
50 (_, error) => reject(error))
51 )
52 )
53 }
54
55 /**
56 * @returns {Promise.<Array.<String>>} All tables' names.
57 */
58 getTables(){
59 return execute('SELECT name FROM sqlite_master WHERE type = "table"')
60 .then(results => results.map(result => result.name))
61 .then(names => Platform.select({
62 // remove android meta data table
63 android : names.filter(name => name != 'android_metadata'),
64 default : names
65 }));
66 }
67}
68
69/**
70 * Module to store key-value data. You must pass a string namespace to its constructor.
71 */
72class KeyValueStorage {
73 constructor(namespace){
74 this.namespace = namespace;
75 this.init = database.execute(Queries.init(namespace))
76 // log if error
77 .catch(error => {
78 console.warn("Impossible to init the database for KeyValueStorage with namespace:", namespace, error);
79 throw error;
80 });
81 }
82
83 /**
84 * Set a map og key values.
85 * @param {Object} map Map of key-value to store.
86 * @returns {Promise<Object.<Error>>} Map of possible errors for each key.
87 */
88 async set(map) {
89 await this.init;
90 let promises = {};
91 Object.keys(map)
92 .forEach(key => {
93 let value = map[key];
94 // serialize value
95 value = JSON.stringify(value);
96 // build statement
97 let {statement, variables} = Queries.set(this.namespace, key, value);
98 // persist
99 promises[key] = database.execute(statement, ...variables);
100 });
101
102 // await for each to resolve, and catch their errors for each of them
103 let errors = {};
104 for (let key of Object.keys(promises))
105 try {
106 await promises[key];
107 } catch (error) {
108 errors[key] = error;
109 }
110
111 return errors;
112 }
113
114 /**
115 * Load values for given keys.
116 * @param {Array } keys Keys of value to load.
117 * @returns {Promise.<Object.<{value, error}>>} A map of object {value, error}. If no error is in the key, then you can get the value.
118 */
119 async get(...keys) {
120 await this.init;
121 // load
122 let {statement, variables} = Queries.get(this.namespace, keys);
123 let items = await database.execute(statement, ...variables);
124
125 // convert into one item
126 let results = {};
127 items.forEach(item => {
128 let value = JSON.parse(item.value);
129 // wrap each value inside an object, without error field
130 results[item.key] = {value};
131 });
132
133 // fill all unset keys with empty object (no error, and no value)
134 keys.forEach(key => {
135 results[key] = results[key] || {};
136 });
137
138 return results;
139 }
140
141 /**
142 * Remove values for given keys.
143 * @param keys Keys to remove.
144 * @returns {Promise<Object.<Error>>} Map of possible errors for each key.
145 */
146 async remove(...keys) {
147 await this.init;
148 let {statement, variables} = Queries.remove(this.namespace, keys);
149 await database.execute(statement, ...variables);
150 // return no errors
151 return {};
152 }
153
154 /**
155 * @returns {Promise<Array.<String>>} All keys stored.
156 */
157 async getKeys(){
158 await this.init;
159 let items = await database.execute(`SELECT ${Columns.KEY} from ${this.namespace}`);
160 return items.map(item => item.key);
161 }
162
163 /**
164 * @returns {Promise<Array<String>>} All namespaces stored.
165 */
166 static getNamespaces(){
167 return database.getTables();
168 }
169}
170
171
172
173export {Database, KeyValueStorage}
174
175
176
177// --------- private ---------
178
179const database = new Database("KeyValues");
180
181const Columns = {
182 KEY : "key",
183 VALUE : "value",
184};
185
186const Queries = {
187 // init statement
188 init : table => `CREATE TABLE IF NOT EXISTS ${table}(${Columns.KEY} VARCHAR(1000000000) PRIMARY KEY NOT NULL UNIQUE, ${Columns.VALUE} VARCHAR(1000000000))`,
189
190 set : (table, key, value) => ({
191 statement : `INSERT OR REPLACE INTO ${table} (${Columns.KEY}, ${Columns.VALUE}) values (?, ?)`,
192 variables : [key, value]
193 }),
194
195 get : (table, keys) => ({
196 // place placeholders
197 statement : `SELECT * FROM ${table} WHERE ${Columns.KEY} IN (${keys.map(key => '?').join(',')})`,
198 // convert keys into strings
199 variables : keys.map(key => key+""),
200 }),
201
202 remove : (table, keys) => ({
203 // place placeholders
204 statement : `DELETE FROM ${table} WHERE ${Columns.KEY} IN (${keys.map(key => '?').join(',')})`,
205 // convert keys into strings
206 variables : keys.map(key => key+""),
207 }),
208};