2
0
mirror of https://github.com/frappe/books.git synced 2024-11-10 07:40:55 +00:00

Merge pull request #110 from frappe/knex

feat: Use Knex for query building
This commit is contained in:
Faris Ansari 2019-12-10 01:49:02 +05:30 committed by GitHub
commit 218b1a7a6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 682 additions and 520 deletions

View File

@ -1,19 +1,10 @@
language: node_js
node_js:
- "8"
services:
- mysql
before_install:
- mysql -u root -e 'create database test_frappejs'
- echo "USE mysql;\nCREATE USER 'test_frappejs'@'localhost' IDENTIFIED BY 'test_frappejs';\nFLUSH PRIVILEGES;\n"
- echo "USE mysql;\nGRANT ALL PRIVILEGES ON \`test_frappejs\`.* TO 'test_frappejs'@'localhost';\n"
- "git clone https://github.com/frappe/frappejs && cd frappejs"
- "npm install -g yarn"
- "yarn"
- 8
script:
- "yarn link && yarn link frappejs"
- "yarn run test"
- yarn
- yarn link
- yarn link frappejs
- yarn test

View File

@ -1,18 +1,23 @@
const frappe = require('frappejs');
const Observable = require('frappejs/utils/observable');
const Knex = require('knex');
module.exports = class Database extends Observable {
constructor() {
super();
this.initTypeMap();
this.connectionParams = {};
}
async connect() {
// this.conn
connect() {
this.knex = Knex(this.connectionParams);
this.knex.on('query-error', error => {
error.type = this.getError(error);
});
}
close() {
this.conn.close();
//
}
async migrate() {
@ -31,30 +36,21 @@ module.exports = class Database extends Observable {
await this.commit();
}
async createTable(doctype, newName = null) {
let meta = frappe.getMeta(doctype);
let columns = [];
let indexes = [];
tableExists(table) {
return this.knex.schema.hasTable(table);
}
for (let field of meta.getValidFields({ withChildren: false })) {
if (this.typeMap[field.fieldtype]) {
this.updateColumnDefinition(field, columns, indexes);
async createTable(doctype, tableName = null) {
let fields = this.getValidFields(doctype);
return await this.runCreateTableQuery(tableName || doctype, fields);
}
runCreateTableQuery(doctype, fields) {
return this.knex.schema.createTable(doctype, table => {
for (let field of fields) {
this.buildColumnForTable(table, field);
}
}
return await this.runCreateTableQuery(newName || doctype, columns, indexes);
}
async tableExists(table) {
// return true if table exists
}
async runCreateTableQuery(doctype, columns, indexes) {
// override
}
updateColumnDefinition(field, columns, indexes) {
// return `${df.fieldname} ${this.typeMap[df.fieldtype]} ${ ? "PRIMARY KEY" : ""} ${df.required && !df.default ? "NOT NULL" : ""} ${df.default ? `DEFAULT ${df.default}` : ""}`
});
}
async alterTable(doctype) {
@ -62,26 +58,66 @@ module.exports = class Database extends Observable {
let diff = await this.getColumnDiff(doctype);
let newForeignKeys = await this.getNewForeignKeys(doctype);
if (diff.added.length) {
await this.addColumns(doctype, diff.added);
return this.knex.schema
.table(doctype, table => {
if (diff.added.length) {
for (let field of diff.added) {
this.buildColumnForTable(table, field);
}
}
if (diff.removed.length) {
this.removeColumns(doctype, diff.removed);
}
})
.then(() => {
if (newForeignKeys.length) {
return this.addForeignKeys(doctype, newForeignKeys);
}
});
}
buildColumnForTable(table, field) {
let columnType = this.getColumnType(field);
let column = table[columnType](field.fieldname);
// primary key
if (field.fieldname === 'name') {
column.primary();
}
if (diff.removed.length) {
await this.removeColumns(doctype, diff.removed);
// default value
if (field.default) {
column.defaultTo(field.default);
}
if (newForeignKeys.length) {
await this.addForeignKeys(doctype, newForeignKeys);
// required
if (field.required) {
column.notNullable();
}
// link
if (field.fieldtype === 'Link' && field.target && column.foreign) {
let meta = frappe.getMeta(field.target);
column
.foreign(field.fieldname)
.references('name')
.inTable(meta.getBaseDocType())
.onUpdate('CASCADE')
.onDelete('RESTRICT');
}
}
async getColumnDiff(doctype) {
const tableColumns = await this.getTableColumns(doctype);
const validFields = frappe.getMeta(doctype).getValidFields({ withChildren: false });
const validFields = this.getValidFields(doctype);
const diff = { added: [], removed: [] };
for (let field of validFields) {
if (!tableColumns.includes(field.fieldname) && this.typeMap[field.fieldtype]) {
if (
!tableColumns.includes(field.fieldname) &&
this.getColumnType(field)
) {
diff.added.push(field);
}
}
@ -96,25 +132,21 @@ module.exports = class Database extends Observable {
return diff;
}
async addColumns(doctype, added) {
for (let field of added) {
await this.runAddColumnQuery(doctype, field);
}
}
async removeColumns(doctype, removed) {
for (let column of removed) {
await this.runRemoveColumnQuery(doctype, column);
}
}
async getNewForeignKeys(doctype) {
let foreignKeys = await this.getForeignKeys(doctype);
let newForeignKeys = [];
let meta = frappe.getMeta(doctype);
for (let field of meta.getValidFields({ withChildren: false })) {
if (field.fieldtype === 'Link' && !foreignKeys.includes(field.fieldname)) {
if (
field.fieldtype === 'Link' &&
!foreignKeys.includes(field.fieldname)
) {
newForeignKeys.push(field);
}
}
@ -127,18 +159,14 @@ module.exports = class Database extends Observable {
}
}
async getForeignKey(doctype, field) {
async getForeignKeys(doctype, field) {
return []
}
async getTableColumns(doctype) {
return [];
}
async runAddColumnQuery(doctype, field) {
// alter table {doctype} add column ({column_def});
}
async get(doctype, name = null, fields = '*') {
let meta = frappe.getMeta(doctype);
let doc;
@ -161,7 +189,7 @@ module.exports = class Database extends Observable {
for (let field of tableFields) {
doc[field.fieldname] = await this.getAll({
doctype: field.childtype,
fields: ["*"],
fields: ['*'],
filters: { parent: doc.name },
orderBy: 'idx',
order: 'asc'
@ -184,15 +212,15 @@ module.exports = class Database extends Observable {
return doc;
}
async getOne(doctype, name, fields = '*') {
// select {fields} form {doctype} where name = ?
}
getOne(doctype, name, fields = '*') {
let meta = frappe.getMeta(doctype);
let baseDoctype = meta.getBaseDocType();
prepareFields(fields) {
if (fields instanceof Array) {
fields = fields.join(", ");
}
return fields;
return this.knex
.select(fields)
.from(baseDoctype)
.where('name', name)
.first();
}
triggerChange(doctype, name) {
@ -225,12 +253,11 @@ module.exports = class Database extends Observable {
return doc;
}
async insertChildren(meta, doc, doctype) {
let tableFields = meta.getTableFields();
for (let field of tableFields) {
let idx = 0;
for (let child of (doc[field.fieldname] || [])) {
for (let child of doc[field.fieldname] || []) {
this.prepareChild(doctype, doc.name, child, field, idx);
await this.insertOne(field.childtype, child);
idx++;
@ -238,8 +265,15 @@ module.exports = class Database extends Observable {
}
}
async insertOne(doctype, doc) {
// insert into {doctype} ({fields}) values ({values})
insertOne(doctype, doc) {
let fields = this.getValidFields(doctype);
if (!doc.name) {
doc.name = frappe.getRandomString();
}
let formattedDoc = this.getFormattedDoc(fields, doc);
return this.knex(doctype).insert(formattedDoc);
}
async update(doctype, doc) {
@ -265,28 +299,37 @@ module.exports = class Database extends Observable {
async updateChildren(meta, doc, doctype) {
let tableFields = meta.getTableFields();
for (let field of tableFields) {
// first key is "parent" - for SQL params
let added = [doc.name];
for (let child of (doc[field.fieldname] || [])) {
this.prepareChild(doctype, doc.name, child, field, added.length - 1);
let added = [];
for (let child of doc[field.fieldname] || []) {
this.prepareChild(doctype, doc.name, child, field, added.length);
if (await this.exists(field.childtype, child.name)) {
await this.updateOne(field.childtype, child);
}
else {
} else {
await this.insertOne(field.childtype, child);
}
added.push(child.name);
}
await this.runDeleteOtherChildren(field, added);
await this.runDeleteOtherChildren(field, doc.name, added);
}
}
async updateOne(doctype, doc) {
// update {doctype} set {field=value} where name=?
updateOne(doctype, doc) {
let validFields = this.getValidFields(doctype);
let fieldsToUpdate = Object.keys(doc).filter(f => f !== 'name');
let fields = validFields.filter(df => fieldsToUpdate.includes(df.fieldname));
let formattedDoc = this.getFormattedDoc(fields, doc);
return this.knex(doctype)
.where('name', doc.name)
.update(formattedDoc);
}
async runDeleteOtherChildren(field, added) {
// delete from doctype where parent = ? and name not in (?, ?, ?)
runDeleteOtherChildren(field, parent, added) {
// delete other children
return this.knex(field.childtype)
.where('parent', parent)
.andWhere('name', 'not in', added)
.delete();
}
async updateSingle(meta, doc, doctype) {
@ -299,18 +342,25 @@ module.exports = class Database extends Observable {
parent: doctype,
fieldname: field.fieldname,
value: value
})
});
await singleValue.insert();
}
}
}
async deleteSingleValues(name) {
// await frappe.db.run('delete from SingleValue where parent=?', name)
deleteSingleValues(name) {
return this.knex('SingleValue')
.where('parent', name)
.delete();
}
async rename(doctype, oldName, newName) {
// await frappe.db.run('update doctype set name = ? where name = ?', name)
let meta = frappe.getMeta(doctype);
let baseDoctype = meta.getBaseDocType();
await this.knex(baseDoctype)
.update({ name: newName })
.where('name', oldName);
await frappe.db.commit();
}
prepareChild(parenttype, parent, child, field, idx) {
@ -323,16 +373,17 @@ module.exports = class Database extends Observable {
child.idx = idx;
}
getKeys(doctype) {
getValidFields(doctype) {
return frappe.getMeta(doctype).getValidFields({ withChildren: false });
}
getFormattedValues(fields, doc) {
let values = fields.map(field => {
getFormattedDoc(fields, doc) {
let formattedDoc = {};
fields.map(field => {
let value = doc[field.fieldname];
return this.getFormattedValue(field, value);
formattedDoc[field.fieldname] = this.getFormattedValue(field, value);
});
return values;
return formattedDoc;
}
getFormattedValue(field, value) {
@ -387,11 +438,15 @@ module.exports = class Database extends Observable {
}
async deleteOne(doctype, name) {
// delete from {doctype} where name = ?
return this.knex(doctype)
.where('name', name)
.delete();
}
async deleteChildren(parenttype, parent) {
// delete from {parenttype} where parent = ?
deleteChildren(parenttype, parent) {
return this.knex(parenttype)
.where('parent', parent)
.delete();
}
async exists(doctype, name) {
@ -427,14 +482,57 @@ module.exports = class Database extends Observable {
}
async setValues(doctype, name, fieldValuePair) {
//
let doc = Object.assign({}, fieldValuePair, { name });
return this.updateOne(doctype, doc);
}
getAll({ doctype, fields, filters, start, limit, orderBy = 'modified', order = 'desc' } = {}) {
// select {fields} from {doctype} where {filters} order by {orderBy} {order} limit {start} {limit}
getAll({
doctype,
fields,
filters,
start,
limit,
groupBy,
orderBy = 'creation',
order = 'desc'
} = {}) {
let meta = frappe.getMeta(doctype);
let baseDoctype = meta.getBaseDocType();
if (!fields) {
fields = meta.getKeywordFields();
fields.push('name');
}
if (typeof fields === 'string') {
fields = [fields];
}
if (meta.filters) {
filters = Object.assign({}, filters, meta.filters);
}
let builder = this.knex.select(fields).from(baseDoctype);
this.applyFiltersToBuilder(builder, filters);
if (orderBy) {
builder.orderBy(orderBy, order);
}
if (groupBy) {
builder.groupBy(groupBy);
}
if (start) {
builder.offset(start);
}
if (limit) {
builder.limit(limit);
}
return builder;
}
getFilterConditions(filters) {
applyFiltersToBuilder(builder, filters) {
// {"status": "Open"} => `status = "Open"`
// {"status": "Open", "name": ["like", "apple%"]}
@ -445,9 +543,8 @@ module.exports = class Database extends Observable {
let filtersArray = [];
for (let key in filters) {
let value = filters[key];
let field = key;
for (let field in filters) {
let value = filters[field];
let operator = '=';
let comparisonValue = value;
@ -460,7 +557,7 @@ module.exports = class Database extends Observable {
operator = 'like';
}
if (['like', 'includes'].includes(operator) && !comparisonValue.includes('%')) {
if (operator === 'like' && !comparisonValue.includes('%')) {
comparisonValue = `%${comparisonValue}%`;
}
}
@ -475,74 +572,41 @@ module.exports = class Database extends Observable {
}
}
let conditions = filtersArray.map(filter => {
filtersArray.map(filter => {
const [field, operator, comparisonValue] = filter;
let placeholder = Array.isArray(comparisonValue) ?
comparisonValue.map(v => '?').join(', ') :
'?';
return `ifnull(${field}, '') ${operator} (${placeholder})`;
builder.where(field, operator, comparisonValue);
});
let values = filtersArray.reduce((acc, filter) => {
const comparisonValue = filter[2];
if (Array.isArray(comparisonValue)) {
acc = acc.concat(comparisonValue);
} else {
acc.push(comparisonValue);
}
return acc;
}, []);
return {
conditions: conditions.length ? conditions.join(" and ") : "",
values
};
}
async run(query, params) {
run(query, params) {
// run query
return this.sql(query, params);
}
async sql(query, params) {
sql(query, params) {
// run sql
return this.knex.raw(query, params);
}
async commit() {
// commit
}
initTypeMap() {
this.typeMap = {
'AutoComplete': 'text'
, 'Currency': 'real'
, 'Int': 'integer'
, 'Float': 'real'
, 'Percent': 'real'
, 'Check': 'integer'
, 'Small Text': 'text'
, 'Long Text': 'text'
, 'Code': 'text'
, 'Text Editor': 'text'
, 'Date': 'text'
, 'Datetime': 'text'
, 'Time': 'text'
, 'Text': 'text'
, 'Data': 'text'
, 'Link': 'text'
, 'DynamicLink': 'text'
, 'Password': 'text'
, 'Select': 'text'
, 'Read Only': 'text'
, 'File': 'text'
, 'Attach': 'text'
, 'AttachImage': 'text'
, 'Signature': 'text'
, 'Color': 'text'
, 'Barcode': 'text'
, 'Geolocation': 'text'
try {
await this.sql('commit');
} catch (e) {
if (e.type !== frappe.errors.CannotCommitError) {
throw e;
}
}
}
}
getColumnType(field) {
return this.typeMap[field.fieldtype];
}
getError(err) {
return frappe.errors.DatabaseError;
}
initTypeMap() {
this.typeMap = {};
}
};

View File

@ -1,294 +1,92 @@
const frappe = require('frappejs');
const sqlite3 = require('sqlite3').verbose();
const Database = require('./database');
const errors = require('frappejs/common/errors');
const debug = false;
class SqliteDatabase extends Database {
constructor({ dbPath }) {
super();
this.dbPath = dbPath;
}
connect(dbPath) {
if (dbPath) {
this.dbPath = dbPath;
}
return new Promise((resolve, reject) => {
this.conn = new sqlite3.Database(this.dbPath, (err) => {
if (err) {
console.log(err);
reject(err);
return;
this.connectionParams = {
client: 'sqlite3',
connection: {
filename: this.dbPath
},
pool: {
afterCreate(conn, done) {
conn.run('PRAGMA foreign_keys=ON');
done();
}
if (debug) {
this.conn.on('trace', (trace) => console.log(trace));
}
this.run('PRAGMA foreign_keys=ON').then(resolve);
});
});
}
async tableExists(table) {
const name = await this.sql(`SELECT name FROM sqlite_master WHERE type='table' AND name='${table}'`);
return (name && name.length) ? true : false;
},
useNullAsDefault: true
};
}
async addForeignKeys(doctype, newForeignKeys) {
await this.run('PRAGMA foreign_keys=OFF');
await this.run('BEGIN TRANSACTION');
await this.sql('PRAGMA foreign_keys=OFF');
await this.sql('BEGIN TRANSACTION');
const tempName = 'TEMP' + doctype
const tempName = 'TEMP' + doctype;
// create temp table
await this.createTable(doctype, tempName);
const columns = (await this.getTableColumns(tempName)).join(', ');
// copy from old to new table
await this.run(`INSERT INTO ${tempName} (${columns}) SELECT ${columns} from ${doctype}`);
await this.knex(tempName).insert(this.knex.select().from(doctype));
// drop old table
await this.run(`DROP TABLE ${doctype}`);
await this.knex.schema.dropTable(doctype);
// rename new table
await this.run(`ALTER TABLE ${tempName} RENAME TO ${doctype}`);
await this.knex.schema.renameTable(tempName, doctype);
await this.run('COMMIT');
await this.run('PRAGMA foreign_keys=ON');
await this.sql('COMMIT');
await this.sql('PRAGMA foreign_keys=ON');
}
removeColumns() {
// pass
}
async runCreateTableQuery(doctype, columns, indexes) {
const query = `CREATE TABLE IF NOT EXISTS ${doctype} (
${columns.join(", ")} ${indexes.length ? (", " + indexes.join(", ")) : ''})`;
return await this.run(query);
}
updateColumnDefinition(field, columns, indexes) {
let def = this.getColumnDefinition(field);
columns.push(def);
if (field.fieldtype === 'Link' && field.target) {
let meta = frappe.getMeta(field.target);
indexes.push(`FOREIGN KEY (${field.fieldname}) REFERENCES ${meta.getBaseDocType()} ON UPDATE CASCADE ON DELETE RESTRICT`);
}
}
getColumnDefinition(field) {
let defaultValue = field.default;
if (typeof defaultValue === 'string') {
defaultValue = `'${defaultValue}'`
}
let def = [
field.fieldname,
this.typeMap[field.fieldtype],
field.fieldname === 'name' ? 'PRIMARY KEY NOT NULL' : '',
field.required ? 'NOT NULL' : '',
field.default ? `DEFAULT ${defaultValue}` : ''
].join(' ');
return def;
}
async getTableColumns(doctype) {
return (await this.sql(`PRAGMA table_info(${doctype})`)).map(d => d.name);
}
async getForeignKeys(doctype) {
return (await this.sql(`PRAGMA foreign_key_list(${doctype})`)).map(d => d.from);
}
async runAddColumnQuery(doctype, field, values) {
await this.run(`ALTER TABLE ${doctype} ADD COLUMN ${this.getColumnDefinition(field)}`, values);
}
getOne(doctype, name, fields = '*') {
let meta = frappe.getMeta(doctype);
let baseDoctype = meta.getBaseDocType();
fields = this.prepareFields(fields);
return new Promise((resolve, reject) => {
this.conn.get(`select ${fields} from ${baseDoctype}
where name = ?`, name,
(err, row) => {
resolve(row || {});
});
});
}
async insertOne(doctype, doc) {
let fields = this.getKeys(doctype);
let placeholders = fields.map(d => '?').join(', ');
if (!doc.name) {
doc.name = frappe.getRandomString();
}
return await this.run(`insert into ${doctype}
(${fields.map(field => field.fieldname).join(", ")})
values (${placeholders})`, this.getFormattedValues(fields, doc));
}
async updateOne(doctype, doc) {
let fields = this.getKeys(doctype);
let assigns = fields.map(field => `${field.fieldname} = ?`);
let values = this.getFormattedValues(fields, doc);
// additional name for where clause
values.push(doc.name);
return await this.run(`update ${doctype}
set ${assigns.join(", ")} where name=?`, values);
}
async runDeleteOtherChildren(field, added) {
// delete other children
// `delete from doctype where parent = ? and name not in (?, ?, ?)}`
await this.run(`delete from ${field.childtype}
where
parent = ? and
name not in (${added.slice(1).map(d => '?').join(', ')})`, added);
}
async deleteOne(doctype, name) {
return await this.run(`delete from ${doctype} where name=?`, name);
}
async deleteChildren(parenttype, parent) {
await this.run(`delete from ${parenttype} where parent=?`, parent);
}
async deleteSingleValues(name) {
await frappe.db.run('delete from SingleValue where parent=?', name)
}
async rename(doctype, oldName, newName) {
let meta = frappe.getMeta(doctype);
let baseDoctype = meta.getBaseDocType();
await frappe.db.run(`update ${baseDoctype} set name = ? where name = ?`, [newName, oldName]);
await frappe.db.commit();
}
async setValues(doctype, name, fieldValuePair) {
const meta = frappe.getMeta(doctype);
const baseDoctype = meta.getBaseDocType();
const validFields = this.getKeys(doctype);
const validFieldnames = validFields.map(df => df.fieldname);
const fieldsToUpdate = Object.keys(fieldValuePair)
.filter(fieldname => validFieldnames.includes(fieldname))
// assignment part of query
const assigns = fieldsToUpdate.map(fieldname => `${fieldname} = ?`);
// values
const values = fieldsToUpdate.map(fieldname => {
const field = meta.getField(fieldname);
const value = fieldValuePair[fieldname];
return this.getFormattedValue(field, value);
});
// additional name for where clause
values.push(name);
return await this.run(`update ${baseDoctype}
set ${assigns.join(', ')} where name=?`, values);
}
getAll({ doctype, fields, filters, start, limit, orderBy = 'modified', groupBy, order = 'desc' } = {}) {
let meta = frappe.getMeta(doctype);
let baseDoctype = meta.getBaseDocType();
if (!fields) {
fields = meta.getKeywordFields();
}
if (typeof fields === 'string') {
fields = [fields];
}
if (meta.filters) {
filters = Object.assign({}, filters, meta.filters);
}
let conditions = this.getFilterConditions(filters);
let query = `select ${fields.join(", ")}
from ${baseDoctype}
${conditions.conditions ? "where" : ""} ${conditions.conditions}
${groupBy ? ("group by " + groupBy.join(', ')) : ""}
${orderBy ? ("order by " + orderBy) : ""} ${orderBy ? (order || "asc") : ""}
${limit ? ("limit " + limit) : ""} ${start ? ("offset " + start) : ""}`;
return this.sql(query, conditions.values);
}
run(query, params) {
return new Promise((resolve, reject) => {
this.conn.run(query, params, (err) => {
if (err) {
console.error('Error in sql:', query);
let Error = this.getError(err);
reject(new Error());
} else {
resolve();
}
});
});
}
sql(query, params) {
return new Promise((resolve, reject) => {
this.conn.all(query, params, (err, rows) => {
if (err) {
console.error('Error in sql:', query);
reject(err)
}
resolve(rows);
});
});
}
async commit() {
try {
await this.run('commit');
} catch (e) {
if (e.name !== 'CannotCommitError') {
throw e;
}
}
return (await this.sql(`PRAGMA foreign_key_list(${doctype})`)).map(
d => d.from
);
}
initTypeMap() {
// prettier-ignore
this.typeMap = {
'AutoComplete': 'text'
, 'Currency': 'real'
, 'Int': 'integer'
, 'Float': 'real'
, 'Percent': 'real'
, 'Check': 'integer'
, 'Small Text': 'text'
, 'Long Text': 'text'
, 'Code': 'text'
, 'Text Editor': 'text'
, 'Date': 'text'
, 'Datetime': 'text'
, 'Time': 'text'
, 'Text': 'text'
, 'Data': 'text'
, 'Link': 'text'
, 'DynamicLink': 'text'
, 'Password': 'text'
, 'Select': 'text'
, 'Read Only': 'text'
, 'File': 'text'
, 'Attach': 'text'
, 'AttachImage': 'text'
, 'Signature': 'text'
, 'Color': 'text'
, 'Barcode': 'text'
, 'Geolocation': 'text'
}
'AutoComplete': 'text',
'Currency': 'float',
'Int': 'integer',
'Float': 'float',
'Percent': 'float',
'Check': 'integer',
'Small Text': 'text',
'Long Text': 'text',
'Code': 'text',
'Text Editor': 'text',
'Date': 'text',
'Datetime': 'text',
'Time': 'text',
'Text': 'text',
'Data': 'text',
'Link': 'text',
'DynamicLink': 'text',
'Password': 'text',
'Select': 'text',
'Read Only': 'text',
'File': 'text',
'Attach': 'text',
'AttachImage': 'text',
'Signature': 'text',
'Color': 'text',
'Barcode': 'text',
'Geolocation': 'text'
};
}
getError(err) {
@ -296,18 +94,13 @@ class SqliteDatabase extends Database {
return frappe.errors.LinkValidationError;
}
if (err.message.includes('SQLITE_ERROR: cannot commit')) {
return SqliteDatabase.CannotCommitError;
return frappe.errors.CannotCommitError;
}
return {
19: frappe.errors.DuplicateEntryError
}[err.errno] || Error;
}
}
SqliteDatabase.CannotCommitError = class CannotCommitError extends errors.DatabaseError {
constructor(message) {
super(message);
this.name = 'CannotCommitError';
return (
{
19: frappe.errors.DuplicateEntryError
}[err.errno] || Error
);
}
}

View File

@ -58,6 +58,13 @@ class DatabaseError extends BaseError {
}
}
class CannotCommitError extends DatabaseError {
constructor(message) {
super(message);
this.name = 'CannotCommitError';
}
}
class ValueError extends ValidationError {}
class Conflict extends ValidationError {}
@ -86,6 +93,7 @@ module.exports = {
DuplicateEntryError,
LinkValidationError,
DatabaseError,
CannotCommitError,
MandatoryError,
throw: throwError
};

View File

@ -231,7 +231,7 @@ module.exports = class BaseDocument extends Observable {
async load() {
let data = await frappe.db.get(this.doctype, this.name);
if (data.name) {
if (data && data.name) {
this.syncValues(data);
if (this.meta.isSingle) {
this.setDefaults();

View File

@ -42,8 +42,13 @@ module.exports = class BaseMeta extends BaseDocument {
].concat(this.fields);
}
// attach default precision to Float and Currency
this.fields = this.fields.map(df => {
// name field is always required
if (df.fieldname === 'name') {
df.required = 1;
}
// attach default precision to Float and Currency
if (['Float', 'Currency'].includes(df.fieldtype)) {
let defaultPrecision = frappe.SystemSettings ? frappe.SystemSettings.floatPrecision : 2;
df.precision = df.precision || defaultPrecision;

View File

@ -7,7 +7,7 @@
"frappe": "cli.js"
},
"scripts": {
"test": "NODE_ENV=test mocha --timeout 3000 tests",
"test": "NODE_ENV=test mocha --timeout 3000 tests --exit",
"test-watch": "NODE_ENV=test mocha --timeout 3000 tests --watch --reporter=min",
"start": "nodemon app.js"
},
@ -33,6 +33,7 @@
"file-loader": "^1.1.11",
"friendly-errors-webpack-plugin": "^1.7.0",
"html-webpack-plugin": "^3.2.0",
"knex": "^0.20.3",
"luxon": "^1.0.0",
"mkdirp": "^0.5.1",
"morgan": "^1.9.0",
@ -44,7 +45,7 @@
"sass-loader": "^7.0.3",
"showdown": "^1.8.6",
"socket.io": "^2.0.4",
"sqlite3": "^4.0.9",
"sqlite3": "^4.1.1",
"tailwindcss": "1.1.1",
"vue": "^2.6.10",
"vue-flatpickr-component": "^8.1.2",

View File

@ -14,7 +14,6 @@ const common = require('frappejs/common');
const bodyParser = require('body-parser');
const fs = require('fs');
const { setupExpressRoute: setRouteForPDF } = require('frappejs/server/pdf');
const auth = require('./../auth/auth')();
const morgan = require('morgan');
const { addWebpackMiddleware } = require('../webpack/serve');
const { getAppConfig, resolveAppDir } = require('../webpack/utils');
@ -26,7 +25,7 @@ require.extensions['.html'] = function (module, filename) {
};
module.exports = {
async start({backend, connectionParams, models, authConfig=null}) {
async start({backend, connectionParams, models}) {
await this.init();
if (models) {
@ -49,10 +48,6 @@ module.exports = {
app.use(cors());
}
if(authConfig) {
this.setupAuthentication(app, authConfig);
}
// socketio
io.on('connection', function (socket) {
frappe.db.bindSocketServer(socket);
@ -80,7 +75,7 @@ module.exports = {
async init() {
frappe.isServer = true;
await frappe.init();
frappe.init();
frappe.registerModels(frappeModels, 'server');
frappe.registerLibs(common);
@ -92,11 +87,4 @@ module.exports = {
await frappe.db.connect();
await frappe.db.migrate();
},
setupAuthentication(app, authConfig) {
app.post("/api/signup", auth.signup);
app.post("/api/login", auth.login);
app.use(auth.initialize(authConfig));
app.all("/api/resource/*", auth.authenticate());
}
}

View File

@ -50,6 +50,5 @@ async function handlePDFRequest(req, res) {
module.exports = {
makePDF,
setupExpressRoute,
getPDFForElectron
setupExpressRoute
};

View File

@ -1,15 +1,16 @@
const server = require('frappejs/server');
const frappe = require('frappejs');
const server = require('frappejs/server');
const SQLite = require('frappejs/backends/sqlite');
module.exports = {
async initSqlite({dbPath = '_test.db', models} = {}) {
server.init();
if (models) {
frappe.registerModels(models, 'server');
}
await server.initDb({
backend: 'sqlite',
connectionParams: {dbPath: dbPath},
});
async initSqlite({ dbPath = '_test.db', models } = {}) {
server.init();
if (models) {
frappe.registerModels(models, 'server');
}
}
frappe.db = new SQLite({ dbPath });
await frappe.db.connect();
await frappe.db.migrate();
}
};

View File

@ -3,47 +3,90 @@ const frappe = require('frappejs');
const helpers = require('./helpers');
describe('Database', () => {
before(async function() {
await helpers.initSqlite();
before(async function() {
await helpers.initSqlite();
});
beforeEach(async () => {
await frappe.db.sql('delete from todo');
await frappe.insert({
doctype: 'ToDo',
subject: 'testing 1',
status: 'Open'
});
await frappe.insert({
doctype: 'ToDo',
subject: 'testing 3',
status: 'Open'
});
await frappe.insert({
doctype: 'ToDo',
subject: 'testing 2',
status: 'Closed'
});
});
it('should insert and get values', async () => {
let subjects = await frappe.db.getAll({
doctype: 'ToDo',
fields: ['name', 'subject']
});
subjects = subjects.map(d => d.subject);
assert.ok(subjects.includes('testing 1'));
assert.ok(subjects.includes('testing 2'));
assert.ok(subjects.includes('testing 3'));
});
it('should filter correct values', async () => {
let todos = await frappe.db.getAll({
doctype: 'ToDo',
fields: ['name', 'subject'],
filters: { status: 'Open' }
});
let subjects = todos.map(d => d.subject);
assert.ok(subjects.includes('testing 1'));
assert.ok(subjects.includes('testing 3'));
assert.equal(subjects.includes('testing 2'), false);
todos = await frappe.db.getAll({
doctype: 'ToDo',
fields: ['name', 'subject'],
filters: { status: 'Closed' }
});
subjects = todos.map(d => d.subject);
assert.equal(subjects.includes('testing 1'), false);
assert.equal(subjects.includes('testing 3'), false);
assert.ok(subjects.includes('testing 2'));
});
it('should delete records', async () => {
let todos = await frappe.db.getAll({ doctype: 'ToDo' });
frappe.db.delete('ToDo', todos[0].name);
todos = await frappe.db.getAll({ doctype: 'ToDo' });
assert.equal(todos.length, 2);
});
it('should update records', async () => {
let todo = (await frappe.db.getAll({ doctype: 'ToDo', limit: 1 }))[0];
frappe.db.update('ToDo', {
name: todo.name,
subject: 'updated subject'
});
it('should insert and get values', async () => {
await frappe.db.sql('delete from todo');
await frappe.insert({doctype:'ToDo', subject: 'testing 1'});
await frappe.insert({doctype:'ToDo', subject: 'testing 3'});
await frappe.insert({doctype:'ToDo', subject: 'testing 2'});
todo = (
await frappe.db.getAll({
doctype: 'ToDo',
fields: ['subject'],
filters: { name: todo.name }
})
)[0];
let subjects = await frappe.db.getAll({doctype:'ToDo', fields:['name', 'subject']})
subjects = subjects.map(d => d.subject);
assert.ok(subjects.includes('testing 1'));
assert.ok(subjects.includes('testing 2'));
assert.ok(subjects.includes('testing 3'));
});
it('should filter correct values', async () => {
let subjects = null;
await frappe.db.sql('delete from todo');
await frappe.insert({doctype:'ToDo', subject: 'testing 1', status: 'Open'});
await frappe.insert({doctype:'ToDo', subject: 'testing 3', status: 'Open'});
await frappe.insert({doctype:'ToDo', subject: 'testing 2', status: 'Closed'});
subjects = await frappe.db.getAll({doctype:'ToDo', fields:['name', 'subject'],
filters:{status: 'Open'}});
subjects = subjects.map(d => d.subject);
assert.ok(subjects.includes('testing 1'));
assert.ok(subjects.includes('testing 3'));
assert.equal(subjects.includes('testing 2'), false);
subjects = await frappe.db.getAll({doctype:'ToDo', fields:['name', 'subject'],
filters:{status: 'Closed'}});
subjects = subjects.map(d => d.subject);
assert.equal(subjects.includes('testing 1'), false);
assert.equal(subjects.includes('testing 3'), false);
assert.ok(subjects.includes('testing 2'));
});
});
assert.equal(todo.subject, 'updated subject');
});
});

View File

@ -167,9 +167,11 @@ function makeConfig() {
}
};
let modifiedConfig = appConfig.configureWebpack(config);
if (modifiedConfig) {
return modifiedConfig;
if (appConfig.configureWebpack) {
let modifiedConfig = appConfig.configureWebpack(config);
if (modifiedConfig) {
return modifiedConfig;
}
}
return config;

303
yarn.lock
View File

@ -442,6 +442,11 @@ arr-union@^3.1.0:
resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
array-each@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f"
integrity sha1-p5SvDAWrF1KEbudTofIRoFugxE8=
array-find-index@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
@ -457,6 +462,11 @@ array-flatten@^2.1.0:
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099"
integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==
array-slice@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-1.1.0.tgz#e368ea15f89bc7069f7ffb89aec3a6c7d4ac22d4"
integrity sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==
array-union@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
@ -814,6 +824,11 @@ bluebird@^3.1.1, bluebird@^3.5.0, bluebird@^3.5.1, bluebird@^3.5.5:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f"
integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==
bluebird@^3.7.1:
version "3.7.2"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
version "4.11.8"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
@ -1419,6 +1434,11 @@ color-name@1.1.3:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
colorette@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.1.0.tgz#1f943e5a357fac10b4e0f5aaef3b14cdc1af6ec7"
integrity sha512-6S062WDQUXi6hOfkO/sBPVwE5ASXY4G2+b4atvhJfSsuUUhIaUKlkjLe9692Ipyt5/a+IPF5aVTu3V5gvXq5cg==
combined-stream@^1.0.6, combined-stream@~1.0.6:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
@ -1441,6 +1461,11 @@ commander@^2.13.0, commander@^2.20.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
commander@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-4.0.1.tgz#b67622721785993182e807f4883633e6401ba53c"
integrity sha512-IPF4ouhCP+qdlcmCedhxX4xiGBPyigb8v5NeUp+0LyhwLgxMqyp3S0vl7TAPfS/hiP7FC3caI/PB9lTmP8r1NA==
commander@~2.19.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
@ -1865,6 +1890,13 @@ debug@3.1.0, debug@~3.1.0:
dependencies:
ms "2.0.0"
debug@4.1.1, debug@^4.1.0, debug@^4.1.1, debug@~4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
dependencies:
ms "^2.1.1"
debug@^3.0.0, debug@^3.1.0, debug@^3.2.5, debug@^3.2.6:
version "3.2.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
@ -1872,13 +1904,6 @@ debug@^3.0.0, debug@^3.1.0, debug@^3.2.5, debug@^3.2.6:
dependencies:
ms "^2.1.1"
debug@^4.1.0, debug@^4.1.1, debug@~4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
dependencies:
ms "^2.1.1"
decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@ -1994,6 +2019,11 @@ destroy@~1.0.4:
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
detect-file@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7"
integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=
detect-indent@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
@ -2548,6 +2578,13 @@ expand-brackets@^2.1.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
expand-tilde@^2.0.0, expand-tilde@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502"
integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=
dependencies:
homedir-polyfill "^1.0.1"
express@^4.16.2, express@^4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
@ -2599,7 +2636,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
assign-symbols "^1.0.0"
is-extendable "^1.0.1"
extend@~3.0.2:
extend@^3.0.0, extend@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
@ -2758,6 +2795,32 @@ find-up@^3.0.0:
dependencies:
locate-path "^3.0.0"
findup-sync@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1"
integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==
dependencies:
detect-file "^1.0.0"
is-glob "^4.0.0"
micromatch "^3.0.4"
resolve-dir "^1.0.1"
fined@^1.0.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/fined/-/fined-1.2.0.tgz#d00beccf1aa2b475d16d423b0238b713a2c4a37b"
integrity sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==
dependencies:
expand-tilde "^2.0.2"
is-plain-object "^2.0.3"
object.defaults "^1.1.0"
object.pick "^1.2.0"
parse-filepath "^1.0.1"
flagged-respawn@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-1.0.1.tgz#e7de6f1279ddd9ca9aac8a5971d618606b3aab41"
integrity sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==
flatpickr@^4.5.1:
version "4.6.2"
resolved "https://registry.yarnpkg.com/flatpickr/-/flatpickr-4.6.2.tgz#50e1b4fc84fbf67c5b0919ba3ddc330221f126da"
@ -2949,6 +3012,11 @@ get-value@^2.0.3, get-value@^2.0.6:
resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
getopts@2.2.5:
version "2.2.5"
resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.2.5.tgz#67a0fe471cacb9c687d817cab6450b96dde8313b"
integrity sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA==
getpass@^0.1.1:
version "0.1.7"
resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
@ -2995,6 +3063,26 @@ global-dirs@^0.1.0:
dependencies:
ini "^1.3.4"
global-modules@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"
integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==
dependencies:
global-prefix "^1.0.1"
is-windows "^1.0.1"
resolve-dir "^1.0.0"
global-prefix@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe"
integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=
dependencies:
expand-tilde "^2.0.2"
homedir-polyfill "^1.0.1"
ini "^1.3.4"
is-windows "^1.0.1"
which "^1.2.14"
globals@^9.18.0:
version "9.18.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
@ -3210,6 +3298,13 @@ home-or-tmp@^2.0.0:
os-homedir "^1.0.0"
os-tmpdir "^1.0.1"
homedir-polyfill@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8"
integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==
dependencies:
parse-passwd "^1.0.0"
hosted-git-info@^2.1.4, hosted-git-info@^2.7.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
@ -3466,7 +3561,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@ -3494,6 +3589,11 @@ internal-ip@^4.3.0:
default-gateway "^4.2.0"
ipaddr.js "^1.9.0"
interpret@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.0.0.tgz#b783ffac0b8371503e9ab39561df223286aa5433"
integrity sha512-e0/LknJ8wpMMhTiWcjivB+ESwIuvHnBSlBbmP/pSb8CQJldoj1p2qv7xGZ/+BtbTziYRFSz8OsvdbiX45LtYQA==
invariant@^2.2.2:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
@ -3531,6 +3631,14 @@ ipaddr.js@^1.9.0:
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
is-absolute@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576"
integrity sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==
dependencies:
is-relative "^1.0.0"
is-windows "^1.0.1"
is-accessor-descriptor@^0.1.6:
version "0.1.6"
resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
@ -3748,6 +3856,13 @@ is-regex@^1.0.4:
dependencies:
has "^1.0.1"
is-relative@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d"
integrity sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==
dependencies:
is-unc-path "^1.0.0"
is-retry-allowed@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34"
@ -3770,12 +3885,19 @@ is-typedarray@~1.0.0:
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
is-unc-path@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-1.0.0.tgz#d731e8898ed090a12c352ad2eaed5095ad322c9d"
integrity sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==
dependencies:
unc-path-regex "^0.1.2"
is-utf8@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=
is-windows@^1.0.0, is-windows@^1.0.2:
is-windows@^1.0.0, is-windows@^1.0.1, is-windows@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
@ -3984,6 +4106,27 @@ kind-of@^6.0.0, kind-of@^6.0.2:
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==
knex@^0.20.3:
version "0.20.3"
resolved "https://registry.yarnpkg.com/knex/-/knex-0.20.3.tgz#85178cd6873f75827be86d054c4e117bb4d9657b"
integrity sha512-zzYO34pSCCYVqRTbCp8xL+Z7fvHQl5anif3Oacu6JaHFDubB7mFGWRRJBNSO3N8Ql4g4CxUgBctaPiliwoOsNA==
dependencies:
bluebird "^3.7.1"
colorette "1.1.0"
commander "^4.0.1"
debug "4.1.1"
getopts "2.2.5"
inherits "~2.0.4"
interpret "^2.0.0"
liftoff "3.1.0"
lodash "^4.17.15"
mkdirp "^0.5.1"
pg-connection-string "2.1.0"
tarn "^2.0.0"
tildify "2.0.0"
uuid "^3.3.3"
v8flags "^3.1.3"
latest-version@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15"
@ -4017,6 +4160,20 @@ lcid@^2.0.0:
dependencies:
invert-kv "^2.0.0"
liftoff@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-3.1.0.tgz#c9ba6081f908670607ee79062d700df062c52ed3"
integrity sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==
dependencies:
extend "^3.0.0"
findup-sync "^3.0.0"
fined "^1.0.1"
flagged-respawn "^1.0.0"
is-plain-object "^2.0.4"
object.map "^1.0.0"
rechoir "^0.6.2"
resolve "^1.1.7"
load-json-file@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
@ -4083,6 +4240,11 @@ lodash@^4.17.11, lodash@^4.17.3, lodash@^4.17.4:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba"
integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==
lodash@^4.17.15:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
loglevel@^1.6.3:
version "1.6.3"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.3.tgz#77f2eb64be55a404c9fd04ad16d57c1d6d6b1280"
@ -4153,6 +4315,13 @@ make-dir@^2.0.0:
pify "^4.0.1"
semver "^5.6.0"
make-iterator@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6"
integrity sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==
dependencies:
kind-of "^6.0.2"
mamacro@^0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/mamacro/-/mamacro-0.0.3.tgz#ad2c9576197c9f1abf308d0787865bd975a3f3e4"
@ -4165,7 +4334,7 @@ map-age-cleaner@^0.1.1:
dependencies:
p-defer "^1.0.0"
map-cache@^0.2.2:
map-cache@^0.2.0, map-cache@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
@ -4253,7 +4422,7 @@ methods@~1.1.2:
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8:
micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8:
version "3.1.10"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
@ -4817,6 +4986,16 @@ object-visit@^1.0.0:
dependencies:
isobject "^3.0.0"
object.defaults@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/object.defaults/-/object.defaults-1.1.0.tgz#3a7f868334b407dea06da16d88d5cd29e435fecf"
integrity sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=
dependencies:
array-each "^1.0.1"
array-slice "^1.0.0"
for-own "^1.0.0"
isobject "^3.0.0"
object.getownpropertydescriptors@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16"
@ -4825,7 +5004,15 @@ object.getownpropertydescriptors@^2.0.3:
define-properties "^1.1.2"
es-abstract "^1.5.1"
object.pick@^1.3.0:
object.map@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/object.map/-/object.map-1.0.1.tgz#cf83e59dc8fcc0ad5f4250e1f78b3b81bd801d37"
integrity sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=
dependencies:
for-own "^1.0.0"
make-iterator "^1.0.0"
object.pick@^1.2.0, object.pick@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=
@ -5048,6 +5235,15 @@ parse-color@^1.0.0:
dependencies:
color-convert "~0.5.0"
parse-filepath@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891"
integrity sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=
dependencies:
is-absolute "^1.0.0"
map-cache "^0.2.0"
path-root "^0.1.1"
parse-json@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
@ -5063,6 +5259,11 @@ parse-json@^4.0.0:
error-ex "^1.3.1"
json-parse-better-errors "^1.0.1"
parse-passwd@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
parseqs@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d"
@ -5129,6 +5330,18 @@ path-parse@^1.0.6:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
path-root-regex@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/path-root-regex/-/path-root-regex-0.1.2.tgz#bfccdc8df5b12dc52c8b43ec38d18d72c04ba96d"
integrity sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=
path-root@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/path-root/-/path-root-0.1.1.tgz#9a4a6814cac1c0cd73360a95f32083c8ea4745b7"
integrity sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=
dependencies:
path-root-regex "^0.1.0"
path-to-regexp@0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
@ -5171,6 +5384,11 @@ performance-now@^2.1.0:
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
pg-connection-string@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.1.0.tgz#e07258f280476540b24818ebb5dca29e101ca502"
integrity sha512-bhlV7Eq09JrRIvo1eKngpwuqKtJnNhZdpdOlvrPrA4dxqXPjxSrbNrfnIDmTpwMyRszrcV4kU5ZA4mMsQUrjdg==
pify@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@ -5651,6 +5869,13 @@ readdirp@^2.2.1:
micromatch "^3.1.10"
readable-stream "^2.0.2"
rechoir@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=
dependencies:
resolve "^1.1.6"
redent@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
@ -5819,6 +6044,14 @@ resolve-cwd@^2.0.0:
dependencies:
resolve-from "^3.0.0"
resolve-dir@^1.0.0, resolve-dir@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43"
integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=
dependencies:
expand-tilde "^2.0.0"
global-modules "^1.0.0"
resolve-from@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
@ -5829,6 +6062,13 @@ resolve-url@^0.2.1:
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
resolve@^1.1.6, resolve@^1.1.7:
version "1.13.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.13.1.tgz#be0aa4c06acd53083505abb35f4d66932ab35d16"
integrity sha512-CxqObCX8K8YtAhOBRg+lrcdn+LK+WYOS8tSjqSFbjtrI5PnS63QPhZl4+yKfrU9tdsbMu9Anr/amegT87M9Z6w==
dependencies:
path-parse "^1.0.6"
resolve@^1.10.0:
version "1.11.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.1.tgz#ea10d8110376982fef578df8fc30b9ac30a07a3e"
@ -6310,10 +6550,10 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
sqlite3@^4.0.9:
version "4.0.9"
resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-4.0.9.tgz#cff74550fa5a1159956815400bdef69245557640"
integrity sha512-IkvzjmsWQl9BuBiM4xKpl5X8WCR4w0AeJHRdobCdXZ8dT/lNc1XS6WqvY35N6+YzIIgzSBeY5prdFObID9F9tA==
sqlite3@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-4.1.1.tgz#539a42e476640796578e22d589b3283c28055242"
integrity sha512-CvT5XY+MWnn0HkbwVKJAyWEMfzpAPwnTiB3TobA5Mri44SrTovmmh499NPQP+gatkeOipqPlBLel7rn4E/PCQg==
dependencies:
nan "^2.12.1"
node-pre-gyp "^0.11.0"
@ -6569,6 +6809,11 @@ tar@^4:
safe-buffer "^5.1.2"
yallist "^3.0.3"
tarn@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/tarn/-/tarn-2.0.0.tgz#c68499f69881f99ae955b4317ca7d212d942fdee"
integrity sha512-7rNMCZd3s9bhQh47ksAQd92ADFcJUjjbyOvyFjNLwTPpGieFHMC84S+LOzw0fx1uh6hnDz/19r8CPMnIjJlMMA==
temp-file@^3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/temp-file/-/temp-file-3.3.4.tgz#73af868cd7cb7400a44e4bb03e653b2280ce2878"
@ -6635,6 +6880,11 @@ thunky@^1.0.2:
resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.0.3.tgz#f5df732453407b09191dae73e2a8cc73f381a826"
integrity sha512-YwT8pjmNcAXBZqrubu22P4FYsh2D4dxRmnWBOL8Jk8bUcRUtc5326kx32tuTmFDAZtLOGEVNl8POAR8j896Iow==
tildify@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/tildify/-/tildify-2.0.0.tgz#f205f3674d677ce698b7067a99e949ce03b4754a"
integrity sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==
timed-out@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
@ -6782,6 +7032,11 @@ uglify-js@3.4.x:
commander "~2.19.0"
source-map "~0.6.1"
unc-path-regex@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo=
undefsafe@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.2.tgz#225f6b9e0337663e0d8e7cfd686fc2836ccace76"
@ -6986,6 +7241,18 @@ uuid@^3.0.1, uuid@^3.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
uuid@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866"
integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==
v8flags@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.1.3.tgz#fc9dc23521ca20c5433f81cc4eb9b3033bb105d8"
integrity sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==
dependencies:
homedir-polyfill "^1.0.1"
validate-npm-package-license@^3.0.1:
version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
@ -7204,7 +7471,7 @@ which-module@^2.0.0:
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
which@^1.2.9:
which@^1.2.14, which@^1.2.9:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==