Ben Ashton
4 years ago
commit
5bf0b6752b
15 changed files with 734 additions and 0 deletions
@ -0,0 +1,31 @@
|
||||
{ |
||||
"name": "pawsqlite-cordova-adapter", |
||||
"version": "1.0.0", |
||||
"description": "Cordova adapter for PawSQLite", |
||||
"cordova": { |
||||
"id": "org.n0m.pawsqlite", |
||||
"platforms": [ |
||||
"android" |
||||
] |
||||
}, |
||||
"scripts": { |
||||
"test": "echo \"Error: no test specified\" && exit 1", |
||||
"build": "webpack" |
||||
}, |
||||
"repository": { |
||||
"type": "git", |
||||
"url": "https://git.n0m.org/n0m/PawSQLite-Cordova-Adapter.git" |
||||
}, |
||||
"keywords": [ |
||||
"cordova", |
||||
"sqlite", |
||||
"ecosystem:cordova", |
||||
"cordova-android" |
||||
], |
||||
"author": "Ben Ashton", |
||||
"license": "MIT", |
||||
"devDependencies": { |
||||
"webpack": "5.x", |
||||
"webpack-cli": "4.x" |
||||
} |
||||
} |
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0" |
||||
id="org.n0m.pawsqlite" version="0.2.3"> |
||||
<name>PawSQLite</name> |
||||
<description>Cordova SQLite Plugin</description> |
||||
<license></license> |
||||
<keywords>cordova,sqlite</keywords> |
||||
<js-module src="www/pawsqlite-cordova-adapter.js" name="PawSQLite"> |
||||
<clobbers target="PawSQLite" /> |
||||
</js-module> |
||||
<platform name="android"> |
||||
<config-file target="res/xml/config.xml" parent="/*"> |
||||
<feature name="PawSQLite"> |
||||
<param name="android-package" value="org.n0m.pawsqlite.PawSQLite" /> |
||||
</feature> |
||||
</config-file> |
||||
|
||||
<config-file target="AndroidManifest.xml" parent="/*"> |
||||
</config-file> |
||||
|
||||
<source-file src="src/android/CallbackWrapper.java" target-dir="src/ca/patterpaws/sqlite" /> |
||||
<source-file src="src/android/DB.java" target-dir="src/ca/patterpaws/sqlite" /> |
||||
<source-file src="src/android/DBAction.java" target-dir="src/ca/patterpaws/sqlite" /> |
||||
<source-file src="src/android/DBManager.java" target-dir="src/ca/patterpaws/sqlite" /> |
||||
<source-file src="src/android/DBRequest.java" target-dir="src/ca/patterpaws/sqlite" /> |
||||
<source-file src="src/android/DBRunner.java" target-dir="src/ca/patterpaws/sqlite" /> |
||||
<source-file src="src/android/PawSQLite.java" target-dir="src/ca/patterpaws/sqlite" /> |
||||
<source-file src="src/android/QueryWrapper.java" target-dir="src/ca/patterpaws/sqlite" /> |
||||
</platform> |
||||
</plugin> |
@ -0,0 +1,72 @@
|
||||
package org.n0m.pawsqlite; |
||||
|
||||
import org.json.JSONArray; |
||||
import org.json.JSONException; |
||||
import org.json.JSONObject; |
||||
|
||||
import java.io.StringWriter; |
||||
import java.io.PrintWriter; |
||||
|
||||
import org.apache.cordova.CallbackContext; |
||||
|
||||
|
||||
public class CallbackWrapper { |
||||
private CallbackContext callbackContext; |
||||
|
||||
CallbackWrapper(CallbackContext callbackContext) { |
||||
this.callbackContext = callbackContext; |
||||
} |
||||
|
||||
public void error(String name) { |
||||
error(name, null, null); |
||||
} |
||||
public void error(String name, String message) { |
||||
error(name, message, null); |
||||
} |
||||
|
||||
public void error(String name, Exception e) { |
||||
String message = null; |
||||
if (e != null) { |
||||
message = e.getMessage(); |
||||
} |
||||
error(name, message, e); |
||||
} |
||||
|
||||
public void error(String name, String message, Exception e) { |
||||
try { |
||||
JSONObject response = new JSONObject(); |
||||
response.put("name", name); |
||||
|
||||
if (message != null) { |
||||
response.put("message", message); |
||||
} |
||||
|
||||
if (e != null) { |
||||
StringWriter stringWriter = new StringWriter(); |
||||
e.printStackTrace(new PrintWriter(stringWriter)); |
||||
response.put("trace", stringWriter.toString()); |
||||
} |
||||
|
||||
callbackContext.error(response); |
||||
} catch (JSONException jsonException) { |
||||
callbackContext.error("Encountered error and unable to generate " + |
||||
"response"); |
||||
} |
||||
} |
||||
|
||||
public void success() { |
||||
JSONObject response = new JSONObject(); |
||||
success(response); |
||||
} |
||||
|
||||
public void success(JSONObject response) { |
||||
if (!response.has("success")) { |
||||
try { |
||||
response.put("success", true); |
||||
} catch (JSONException e) { |
||||
// Will never happen
|
||||
} |
||||
} |
||||
callbackContext.success(response); |
||||
} |
||||
} |
@ -0,0 +1,132 @@
|
||||
package org.n0m.pawsqlite; |
||||
|
||||
import java.io.File; |
||||
|
||||
import org.json.JSONArray; |
||||
import org.json.JSONException; |
||||
import org.json.JSONObject; |
||||
|
||||
import android.util.Log; |
||||
|
||||
import android.database.sqlite.SQLiteDatabase; |
||||
import android.database.SQLException; |
||||
import android.database.Cursor; |
||||
|
||||
|
||||
class DB { |
||||
private static final String TAG = "PawSQLite"; |
||||
|
||||
public final String dbName; |
||||
private File dbFile; |
||||
|
||||
public SQLiteDatabase db; |
||||
|
||||
|
||||
public DB (File dbFile, String dbName) { |
||||
this.dbFile = dbFile; |
||||
this.dbName = dbName; |
||||
} |
||||
|
||||
public void handleRequest(DBRequest request) { |
||||
try { |
||||
switch(request.action) { |
||||
case VERSION: |
||||
version(request.callback); |
||||
break; |
||||
case OPEN: |
||||
open(request.callback); |
||||
break; |
||||
case SQL: |
||||
sql(request.args, request.callback); |
||||
break; |
||||
case CLOSE: |
||||
close(request.callback); |
||||
break; |
||||
case DELETE: |
||||
delete(request.callback); |
||||
break; |
||||
default: |
||||
request.callback.error("Unrecognised Action"); |
||||
} |
||||
} catch (JSONException e) { |
||||
request.callback.error("JSONException", e); |
||||
} |
||||
} |
||||
|
||||
public void open(CallbackWrapper callback) throws JSONException { |
||||
if (db == null || !db.isOpen()) { |
||||
db = SQLiteDatabase.openOrCreateDatabase(dbFile, null); |
||||
} |
||||
|
||||
JSONObject response = new JSONObject(); |
||||
response.put("path", db.getPath()); |
||||
response.put("version", db.getVersion()); |
||||
callback.success(response); |
||||
} |
||||
|
||||
|
||||
public void delete(CallbackWrapper callback) { |
||||
if (db == null) { |
||||
if(SQLiteDatabase.deleteDatabase(dbFile)) { |
||||
callback.success(); |
||||
} else { |
||||
callback.error("Unable to Delete Database", |
||||
"SQLiteDatabase.deleteDatabase returned false"); |
||||
} |
||||
} else { |
||||
callback.error("Unable to Delete Database", |
||||
"Database is currently open"); |
||||
} |
||||
} |
||||
|
||||
public void sql(JSONArray args, CallbackWrapper callback) throws JSONException { |
||||
if (db == null) { |
||||
callback.error( |
||||
"DB Not open", |
||||
"Database: " + dbName + " is not open" |
||||
); |
||||
} |
||||
|
||||
String query = args.optString(0); |
||||
// Remove query from args
|
||||
args.remove(0); |
||||
|
||||
QueryWrapper queryWrapper = new QueryWrapper(db, query, args); |
||||
|
||||
JSONObject result; |
||||
try { |
||||
result = queryWrapper.execute(); |
||||
} catch (SQLException e) { |
||||
callback.error("SQLException", e); |
||||
return; |
||||
} catch (Exception e) { |
||||
callback.error("Exception", e); |
||||
return; |
||||
} |
||||
|
||||
callback.success(result); |
||||
} |
||||
|
||||
public void close(CallbackWrapper callback) { |
||||
if (db != null) { |
||||
db.close(); |
||||
} |
||||
callback.success(); |
||||
} |
||||
|
||||
public void version(CallbackWrapper callback) throws JSONException { |
||||
String query = "select sqlite_version() AS sqlite_version"; |
||||
SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(":memory:", null); |
||||
Cursor cursor = db.rawQuery(query, null); |
||||
String sqliteVersion = ""; |
||||
if (cursor.moveToNext()) { |
||||
sqliteVersion = cursor.getString(0); |
||||
} |
||||
cursor.close(); |
||||
db.close(); |
||||
|
||||
JSONObject result = new JSONObject(); |
||||
result.put("version", sqliteVersion); |
||||
callback.success(result); |
||||
} |
||||
} |
@ -0,0 +1,9 @@
|
||||
package org.n0m.pawsqlite; |
||||
|
||||
public enum DBAction { |
||||
VERSION, |
||||
OPEN, |
||||
CLOSE, |
||||
DELETE, |
||||
SQL |
||||
} |
@ -0,0 +1,44 @@
|
||||
package org.n0m.pawsqlite; |
||||
|
||||
import java.util.concurrent.ConcurrentHashMap; |
||||
|
||||
import org.apache.cordova.CordovaInterface; |
||||
|
||||
import java.io.File; |
||||
|
||||
public class DBManager { |
||||
private static final String TAG = "PawSQLite"; |
||||
|
||||
private CordovaInterface cordova; |
||||
private ConcurrentHashMap<String, DBRunner> dbRunnerMap = |
||||
new ConcurrentHashMap<String, DBRunner>(); |
||||
|
||||
|
||||
DBManager(CordovaInterface cordova) { |
||||
this.cordova = cordova; |
||||
} |
||||
|
||||
public void queueRequest(String dbName, DBRequest request) { |
||||
DBRunner dbRunner = dbRunnerMap.get(dbName); |
||||
|
||||
if (dbRunner == null) { |
||||
File dbFile = cordova.getActivity().getDatabasePath(dbName); |
||||
dbRunner = new DBRunner(dbFile, dbName); |
||||
this.cordova.getThreadPool().execute(dbRunner); |
||||
dbRunnerMap.put(dbName, dbRunner); |
||||
} |
||||
|
||||
dbRunner.queueRequest(request); |
||||
|
||||
// Remove dbRunner if it is being closed
|
||||
if (request.action == DBAction.CLOSE || |
||||
request.action == DBAction.DELETE) { |
||||
|
||||
this.remove(dbName); |
||||
} |
||||
} |
||||
|
||||
public void remove(String dbName) { |
||||
dbRunnerMap.remove(dbName); |
||||
} |
||||
} |
@ -0,0 +1,18 @@
|
||||
package org.n0m.pawsqlite; |
||||
|
||||
import org.json.JSONArray; |
||||
|
||||
|
||||
public class DBRequest { |
||||
private static final String TAG = "PawSQLite"; |
||||
|
||||
public DBAction action; |
||||
public JSONArray args; |
||||
public CallbackWrapper callback; |
||||
|
||||
DBRequest(DBAction action, JSONArray args, CallbackWrapper callback) { |
||||
this.action = action; |
||||
this.args = args; |
||||
this.callback = callback; |
||||
} |
||||
} |
@ -0,0 +1,50 @@
|
||||
package org.n0m.pawsqlite; |
||||
|
||||
import java.io.File; |
||||
import java.util.concurrent.LinkedBlockingQueue; |
||||
|
||||
import android.util.Log; |
||||
|
||||
|
||||
public class DBRunner implements Runnable { |
||||
private static final String TAG = "PawSQLite"; |
||||
|
||||
private LinkedBlockingQueue<DBRequest> queue; |
||||
|
||||
public String dbName; |
||||
private File dbFile; |
||||
|
||||
DBRunner(File dbFile, final String dbName) { |
||||
queue = new LinkedBlockingQueue<DBRequest>(); |
||||
this.dbFile = dbFile; |
||||
this.dbName = dbName; |
||||
} |
||||
|
||||
public void queueRequest(DBRequest request) { |
||||
try { |
||||
queue.put(request); |
||||
} catch (InterruptedException e) { |
||||
Log.e(TAG, "Unexpected Error", e); |
||||
request.callback.error( |
||||
"Unexpected Error", |
||||
"Thread Interrupted", |
||||
e |
||||
); |
||||
} |
||||
} |
||||
|
||||
public void run() { |
||||
DBRequest request; |
||||
DB db = new DB(dbFile, dbName); |
||||
|
||||
while(true) { |
||||
try { |
||||
request = queue.take(); |
||||
} catch (InterruptedException e) { |
||||
Log.e(TAG, "Unexpected Error", e); |
||||
continue; |
||||
} |
||||
db.handleRequest(request); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,67 @@
|
||||
package org.n0m.pawsqlite; |
||||
|
||||
import android.util.Log; |
||||
|
||||
import org.apache.cordova.CordovaPlugin; |
||||
import org.apache.cordova.CallbackContext; |
||||
|
||||
import org.json.JSONArray; |
||||
import org.json.JSONException; |
||||
import org.json.JSONObject; |
||||
|
||||
|
||||
public class PawSQLite extends CordovaPlugin { |
||||
private static final String TAG = "PawSQLite"; |
||||
|
||||
DBManager dbManager; |
||||
|
||||
@Override |
||||
protected void pluginInitialize() { |
||||
Log.d(TAG, "Initialized Plugin"); |
||||
this.dbManager = new DBManager(this.cordova); |
||||
} |
||||
|
||||
@Override |
||||
public boolean execute(String actionStr, JSONArray args, CallbackContext callbackContext) throws JSONException { |
||||
CallbackWrapper callback = new CallbackWrapper(callbackContext); |
||||
|
||||
// Get Action
|
||||
DBAction action; |
||||
try { |
||||
action = DBAction.valueOf(actionStr.toUpperCase()); |
||||
} catch (IllegalArgumentException e) { |
||||
return false; |
||||
} |
||||
|
||||
// Get DB Name
|
||||
String dbName = args.optString(0).trim(); |
||||
|
||||
if (dbName.isEmpty()) { |
||||
callback.error( |
||||
"Unknown Database", |
||||
"Database name not included in request" |
||||
); |
||||
return true; |
||||
} |
||||
|
||||
// Remove dbName so that remaining args can be handled appropriately
|
||||
args.remove(0); |
||||
|
||||
// Queue request
|
||||
DBRequest request = new DBRequest(action, args, callback); |
||||
dbManager.queueRequest(dbName, request); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
private JSONObject jsonError(String name, String message) throws JSONException { |
||||
JSONObject response = new JSONObject(); |
||||
|
||||
JSONObject error = new JSONObject(); |
||||
error.put("name", name); |
||||
error.put("message", message); |
||||
response.put("error", error); |
||||
|
||||
return response; |
||||
} |
||||
} |
@ -0,0 +1,188 @@
|
||||
package org.n0m.pawsqlite; |
||||
|
||||
import org.json.JSONArray; |
||||
import org.json.JSONException; |
||||
import org.json.JSONObject; |
||||
|
||||
import android.database.sqlite.SQLiteDatabase; |
||||
import android.database.sqlite.SQLiteStatement; |
||||
import android.database.SQLException; |
||||
import android.database.Cursor; |
||||
|
||||
|
||||
class QueryWrapper { |
||||
static enum QueryType { |
||||
SELECT, |
||||
INSERT, |
||||
UPDATE, |
||||
DELETE, |
||||
BEGIN, |
||||
COMMIT, |
||||
ROLLBACK, |
||||
OTHER |
||||
} |
||||
|
||||
SQLiteDatabase db; |
||||
String query; |
||||
QueryType queryType; |
||||
JSONArray args; |
||||
|
||||
public QueryWrapper(SQLiteDatabase db, String query, JSONArray args) { |
||||
this.db = db; |
||||
this.args = args; |
||||
|
||||
query = query.trim(); |
||||
|
||||
// Strip trailing semi-colon
|
||||
if (query.endsWith(";")) { |
||||
query = query.substring(0, query.length() - 1); |
||||
} |
||||
|
||||
String operation = query |
||||
.replaceAll("[^a-zA-Z].*", "") |
||||
.toUpperCase(); |
||||
|
||||
try { |
||||
queryType = QueryType.valueOf(operation); |
||||
} catch (IllegalArgumentException e) { |
||||
queryType = QueryType.OTHER; |
||||
} |
||||
|
||||
this.query = query; |
||||
} |
||||
|
||||
public JSONObject execute() throws JSONException, SQLException { |
||||
JSONObject result = new JSONObject(); |
||||
|
||||
result.put("query", query); |
||||
result.put("args", args); |
||||
|
||||
switch (queryType) { |
||||
case SELECT: |
||||
executeRaw(result); |
||||
break; |
||||
case INSERT: |
||||
executeInsert(result); |
||||
break; |
||||
case UPDATE: |
||||
case DELETE: |
||||
executeUpdateDelete(result); |
||||
break; |
||||
default: |
||||
executeOther(result); |
||||
break; |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
private JSONObject executeInsert(JSONObject result) throws JSONException, SQLException { |
||||
SQLiteStatement statement = compileStatement(); |
||||
|
||||
long rowId = statement.executeInsert(); |
||||
|
||||
result.put("insertId", rowId); |
||||
|
||||
// Allow chaining
|
||||
return result; |
||||
} |
||||
|
||||
|
||||
private JSONObject executeUpdateDelete(JSONObject result) throws JSONException, SQLException { |
||||
SQLiteStatement statement = compileStatement(); |
||||
|
||||
int rowsAffected = statement.executeUpdateDelete(); |
||||
|
||||
result.put("rowsAffected", rowsAffected); |
||||
|
||||
// Allow chaining
|
||||
return result; |
||||
} |
||||
|
||||
private JSONObject executeOther(JSONObject result) throws JSONException, SQLException { |
||||
SQLiteStatement statement = compileStatement(); |
||||
statement.execute(); |
||||
return result; |
||||
} |
||||
|
||||
|
||||
private SQLiteStatement compileStatement() throws JSONException, SQLException { |
||||
SQLiteStatement statement = db.compileStatement(query); |
||||
|
||||
for (int i = 0; i < args.length(); i++) { |
||||
if (args.get(i) instanceof Float || args.get(i) instanceof Double) { |
||||
statement.bindDouble(i + 1, args.getDouble(i)); |
||||
} else if (args.get(i) instanceof Number) { |
||||
statement.bindLong(i + 1, args.getLong(i)); |
||||
} else if (args.isNull(i)) { |
||||
statement.bindNull(i + 1); |
||||
} else { |
||||
statement.bindString(i + 1, args.getString(i)); |
||||
} |
||||
} |
||||
|
||||
return statement; |
||||
} |
||||
|
||||
|
||||
private JSONObject executeRaw(JSONObject result) throws JSONException { |
||||
String[] stringArgs = new String[args.length()]; |
||||
|
||||
for (int i = 0; i < args.length(); i++) { |
||||
if (args.isNull(i)) { |
||||
stringArgs[i] = ""; |
||||
} else { |
||||
stringArgs[i] = args.getString(i); |
||||
} |
||||
} |
||||
|
||||
Cursor cur = db.rawQuery(query, stringArgs); |
||||
|
||||
if (cur != null) { |
||||
JSONArray rows = cursorToJSONArray(cur); |
||||
result.put("rows", rows); |
||||
cur.close(); |
||||
} |
||||
|
||||
// Allow chaining
|
||||
return result; |
||||
} |
||||
|
||||
private JSONArray cursorToJSONArray(Cursor cur) throws JSONException { |
||||
JSONArray rows = new JSONArray(); |
||||
|
||||
if (cur != null && cur.moveToFirst()) { |
||||
int colCount = cur.getColumnCount(); |
||||
|
||||
do { |
||||
JSONObject row = new JSONObject(); |
||||
|
||||
for (int i = 0; i < colCount; i++) { |
||||
String key = cur.getColumnName(i); |
||||
|
||||
switch(cur.getType(i)) { |
||||
case Cursor.FIELD_TYPE_NULL: |
||||
row.put(key, JSONObject.NULL); |
||||
break; |
||||
case Cursor.FIELD_TYPE_INTEGER: |
||||
row.put(key, cur.getLong(i)); |
||||
break; |
||||
case Cursor.FIELD_TYPE_FLOAT: |
||||
row.put(key, cur.getDouble(i)); |
||||
break; |
||||
case Cursor.FIELD_TYPE_STRING: |
||||
case Cursor.FIELD_TYPE_BLOB: |
||||
default: |
||||
row.put(key, cur.getString(i)); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
rows.put(row); |
||||
} |
||||
while (cur.moveToNext()); |
||||
} |
||||
|
||||
return rows; |
||||
} |
||||
} |
@ -0,0 +1,14 @@
|
||||
let DEBUG = false; |
||||
|
||||
export function log(...args) { |
||||
if (DEBUG) { |
||||
console.log(...args); |
||||
}
|
||||
} |
||||
|
||||
export function enableDebug(active) { |
||||
DEBUG = !!active; |
||||
log("PawSQLite-Cordova-Adapter: debugging " + ( |
||||
DEBUG ? "enabled" : "disabled") |
||||
); |
||||
} |
@ -0,0 +1,31 @@
|
||||
import { PSQLAdapterError } from "./psql_adapter_error.mjs"; |
||||
import { log, enableDebug } from "./log.mjs"; |
||||
|
||||
|
||||
export const PSQLAdapter = { |
||||
name: "PawSQLiteCordovaAdapter", |
||||
|
||||
open: (dbName) => new Promise((resolve, reject) => { |
||||
cordova.exec(resolve, (e) => { |
||||
reject(new PSQLAdapterError(e)); |
||||
}, "PawSQLite", "open", [dbName]); |
||||
}), |
||||
close: (dbName) => new Promise((resolve, reject) => { |
||||
cordova.exec(resolve, (e) => { |
||||
reject(new PSQLAdapterError(e)); |
||||
}, "PawSQLite", "close", [dbName]); |
||||
}), |
||||
sql: (dbName, sql, ...args) => new Promise((resolve, reject) => { |
||||
log(sql); |
||||
cordova.exec(resolve, (e) => { |
||||
reject(new PSQLAdapterError(e)); |
||||
}, "PawSQLite", "sql", [dbName, sql, ...args]); |
||||
}), |
||||
delete: (dbName) => new Promise((resolve, reject) => { |
||||
cordova.exec(resolve, (e) => { |
||||
reject(new PSQLAdapterError(e)); |
||||
}, "PawSQLite", "delete", [dbName]); |
||||
}), |
||||
|
||||
debug: enableDebug |
||||
}; |
@ -0,0 +1,28 @@
|
||||
export class PSQLAdapterError extends Error { |
||||
constructor(response) { |
||||
if (response.hasOwnProperty("message")) { |
||||
super(response.message); |
||||
} else { |
||||
super(); |
||||
} |
||||
if (response.hasOwnProperty("name")) { |
||||
this.name = response.name; |
||||
} else { |
||||
this.name = "PSQLAdapterError"; |
||||
} |
||||
if (response.hasOwnProperty("trace")) { |
||||
this.trace = response.trace; |
||||
} |
||||
} |
||||
|
||||
toString() { |
||||
let str = this.name; |
||||
if (this.hasOwnProperty("message")) { |
||||
str += ": " + this.message; |
||||
} |
||||
if (this.hasOwnProperty("trace")) { |
||||
str += "\n" + this.trace; |
||||
} |
||||
return str; |
||||
} |
||||
} |
@ -0,0 +1,17 @@
|
||||
var webpack = require('webpack'); |
||||
var libraryName = 'pawsqlite-cordova-adapter'; |
||||
var outputFile = libraryName + '.js'; |
||||
|
||||
var config = { |
||||
mode: 'development', |
||||
entry: __dirname + '/src/psql_adapter.mjs', |
||||
devtool: 'source-map', |
||||
output: { |
||||
path: __dirname + '/www', |
||||
filename: outputFile, |
||||
libraryExport: 'default', |
||||
libraryTarget: 'commonjs2', |
||||
} |
||||
}; |
||||
|
||||
module.exports = config; |
Loading…
Reference in new issue