feat: added schema validation before saving collections to idb

This commit is contained in:
Anoop M D 2022-10-15 02:48:06 +05:30
parent a78bdf87fe
commit 4ff268712f
16 changed files with 88 additions and 85 deletions

View File

@ -122,7 +122,7 @@ const RequestTabPanel = () => {
className="px-4" className="px-4"
style={{width: `${leftPaneWidth}px`, height: 'calc(100% - 5px)'}} style={{width: `${leftPaneWidth}px`, height: 'calc(100% - 5px)'}}
> >
{item.type === 'graphql' ? ( {item.type === 'graphql-request' ? (
<GraphQLRequestPane <GraphQLRequestPane
onRunQuery={runQuery} onRunQuery={runQuery}
schema={schema} schema={schema}
@ -132,7 +132,7 @@ const RequestTabPanel = () => {
/> />
) : null} ) : null}
{item.type === 'http' ? ( {item.type === 'http-request' ? (
<HttpRequestPane <HttpRequestPane
item={item} item={item}
collection={collection} collection={collection}

View File

@ -3,7 +3,7 @@ import classnames from 'classnames';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
const RequestMethod = ({item}) => { const RequestMethod = ({item}) => {
if(!['http', 'graphql'].includes(item.type)) { if(!['http-request', 'graphql-request'].includes(item.type)) {
return null; return null;
} }

View File

@ -17,9 +17,9 @@ const NewRequest = ({collection, item, isEphermal, onClose}) => {
enableReinitialize: true, enableReinitialize: true,
initialValues: { initialValues: {
requestName: '', requestName: '',
requestType: 'http', requestType: 'http-request',
requestUrl: '', requestUrl: '',
requestMethod: 'get' requestMethod: 'GET'
}, },
validationSchema: Yup.object({ validationSchema: Yup.object({
requestName: Yup.string() requestName: Yup.string()
@ -44,19 +44,13 @@ const NewRequest = ({collection, item, isEphermal, onClose}) => {
})); }));
} else { } else {
dispatch(newHttpRequest({ dispatch(newHttpRequest({
requestName: values.requestName, requestName: values.requestName,
requestType: values.requestType, requestType: values.requestType,
requestUrl: values.requestUrl, requestUrl: values.requestUrl,
requestMethod: values.requestMethod, requestMethod: values.requestMethod,
collectionUid: collection.uid, collectionUid: collection.uid,
itemUid: item ? item.uid : null itemUid: item ? item.uid : null
})) }));
.then((action) => {
dispatch(addTab({
uid: action.payload.item.uid,
collectionUid: collection.uid
}));
});
} }
onClose(); onClose();
} }
@ -90,7 +84,7 @@ const NewRequest = ({collection, item, isEphermal, onClose}) => {
type="radio" name="requestType" type="radio" name="requestType"
onChange={formik.handleChange} onChange={formik.handleChange}
value="http" value="http"
checked={formik.values.requestType === 'http'} checked={formik.values.requestType === 'http-request'}
/> />
<label htmlFor="http" className="ml-1 cursor-pointer select-none">Http</label> <label htmlFor="http" className="ml-1 cursor-pointer select-none">Http</label>
@ -99,11 +93,11 @@ const NewRequest = ({collection, item, isEphermal, onClose}) => {
className="ml-4 cursor-pointer" className="ml-4 cursor-pointer"
type="radio" name="requestType" type="radio" name="requestType"
onChange={(event) => { onChange={(event) => {
formik.setFieldValue('requestMethod', 'post') formik.setFieldValue('requestMethod', 'POST')
formik.handleChange(event); formik.handleChange(event);
}} }}
value="graphql" value="graphql"
checked={formik.values.requestType === 'graphql'} checked={formik.values.requestType === 'graphql-request'}
/> />
<label htmlFor="graphql" className="ml-1 cursor-pointer select-none">Graphql</label> <label htmlFor="graphql" className="ml-1 cursor-pointer select-none">Graphql</label>
</div> </div>

View File

@ -10,13 +10,11 @@ const useIdb = () => {
useEffect(() => { useEffect(() => {
let dbName = `bruno`; let dbName = `bruno`;
let connection = openDB(dbName, 2, { let connection = openDB(dbName, 1, {
upgrade(db, oldVersion, newVersion, transaction) { upgrade(db, oldVersion, newVersion, transaction) {
switch(oldVersion) { switch(oldVersion) {
case 0: case 0:
const collectionStore = db.createObjectStore('collection', { keyPath: 'uid' }); const collectionStore = db.createObjectStore('collection', { keyPath: 'uid' });
collectionStore.createIndex('transactionIdIndex', 'transaction_id');
case 1:
const workspaceStore = db.createObjectStore('workspace', { keyPath: 'uid' }); const workspaceStore = db.createObjectStore('workspace', { keyPath: 'uid' });
} }
} }

View File

@ -10,6 +10,7 @@ import {
findParentItemInCollection, findParentItemInCollection,
isItemAFolder isItemAFolder
} from 'utils/collections'; } from 'utils/collections';
import { collectionSchema } from '@usebruno/schema';
import { waitForNextTick } from 'utils/common'; import { waitForNextTick } from 'utils/common';
import cancelTokens, { saveCancelToken, deleteCancelToken } from 'utils/network/cancelTokens'; import cancelTokens, { saveCancelToken, deleteCancelToken } from 'utils/network/cancelTokens';
import { saveCollectionToIdb, deleteCollectionInIdb } from 'utils/idb'; import { saveCollectionToIdb, deleteCollectionInIdb } from 'utils/idb';
@ -36,18 +37,18 @@ export const createCollection = (collectionName) => (dispatch, getState) => {
const newCollection = { const newCollection = {
uid: uuid(), uid: uuid(),
name: collectionName, name: collectionName,
items: [], items: []
environments: [],
}; };
const requestItem = { const requestItem = {
uid: uuid(), uid: uuid(),
type: 'http', type: 'http-request',
name: 'Untitled', name: 'Untitled',
request: { request: {
method: 'GET', method: 'GET',
url: '', url: '',
headers: [], headers: [],
params: [],
body: { body: {
mode: 'none', mode: 'none',
json: null, json: null,
@ -65,7 +66,9 @@ export const createCollection = (collectionName) => (dispatch, getState) => {
const { activeWorkspaceUid } = state.workspaces; const { activeWorkspaceUid } = state.workspaces;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
saveCollectionToIdb(window.__idb, newCollection) collectionSchema
.validate(newCollection)
.then(() => saveCollectionToIdb(window.__idb, newCollection))
.then(() => dispatch(_createCollection(newCollection))) .then(() => dispatch(_createCollection(newCollection)))
.then(waitForNextTick) .then(waitForNextTick)
.then(() => dispatch(addCollectionToWorkspace(activeWorkspaceUid, newCollection.uid))) .then(() => dispatch(addCollectionToWorkspace(activeWorkspaceUid, newCollection.uid)))
@ -90,7 +93,9 @@ export const renameCollection = (newName, collectionUid) => (dispatch, getState)
ignoreDraft: true ignoreDraft: true
}); });
saveCollectionToIdb(window.__idb, collectionToSave) collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => { .then(() => {
dispatch(_renameCollection({ dispatch(_renameCollection({
newName: newName, newName: newName,
@ -135,7 +140,9 @@ export const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => {
const collectionCopy = cloneDeep(collection); const collectionCopy = cloneDeep(collection);
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy); const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
saveCollectionToIdb(window.__idb, collectionToSave) collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => { .then(() => {
dispatch(_saveRequest({ dispatch(_saveRequest({
itemUid: itemUid, itemUid: itemUid,
@ -208,7 +215,9 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS
} }
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy); const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
saveCollectionToIdb(window.__idb, collectionToSave) collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => { .then(() => {
dispatch(_newItem({ dispatch(_newItem({
item: item, item: item,
@ -239,7 +248,9 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
ignoreDraft: true ignoreDraft: true
}); });
saveCollectionToIdb(window.__idb, collectionToSave) collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => { .then(() => {
dispatch(_renameItem({ dispatch(_renameItem({
newName: newName, newName: newName,
@ -286,7 +297,9 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy); const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
saveCollectionToIdb(window.__idb, collectionToSave) collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => { .then(() => {
dispatch(_cloneItem({ dispatch(_cloneItem({
parentItemUid: parentItem ? parentItem.uid : null, parentItemUid: parentItem ? parentItem.uid : null,
@ -309,7 +322,9 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
deleteItemInCollection(itemUid, collectionCopy); deleteItemInCollection(itemUid, collectionCopy);
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy); const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
saveCollectionToIdb(window.__idb, collectionToSave) collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => { .then(() => {
dispatch(_deleteItem({ dispatch(_deleteItem({
itemUid: itemUid, itemUid: itemUid,
@ -369,7 +384,9 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
} }
const collectionToSave = transformCollectionToSaveToIdb(collectionCopy); const collectionToSave = transformCollectionToSaveToIdb(collectionCopy);
saveCollectionToIdb(window.__idb, collectionToSave) collectionSchema
.validate(collectionToSave)
.then(() => saveCollectionToIdb(window.__idb, collectionToSave))
.then(() => { .then(() => {
dispatch(_newItem({ dispatch(_newItem({
item: item, item: item,
@ -377,6 +394,13 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
collectionUid: collectionUid collectionUid: collectionUid
})); }));
}) })
.then(waitForNextTick)
.then(() => {
dispatch(addTab({
uid: item.uid,
collectionUid: collection.uid
}));
})
.then(() => resolve()) .then(() => resolve())
.catch(reject); .catch(reject);
}); });

View File

@ -17,7 +17,8 @@ const seedWorkpace = () => {
const uid = uuid(); const uid = uuid();
const workspace = { const workspace = {
uid: uid, uid: uid,
name: 'My workspace' name: 'My workspace',
collectionUids: []
}; };
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -50,7 +51,8 @@ export const loadWorkspacesFromIdb = () => (dispatch) => {
export const addWorkspace = (workspaceName) => (dispatch) => { export const addWorkspace = (workspaceName) => (dispatch) => {
const newWorkspace = { const newWorkspace = {
uid: uuid(), uid: uuid(),
name: workspaceName name: workspaceName,
collectionUids: []
}; };
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@ -4,7 +4,6 @@ import filter from 'lodash/filter';
const initialState = { const initialState = {
workspaces: [], workspaces: [],
collectionUids: [],
activeWorkspaceUid: null activeWorkspaceUid: null
}; };

View File

@ -218,9 +218,6 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
const collectionToSave = {}; const collectionToSave = {};
collectionToSave.name = collection.name; collectionToSave.name = collection.name;
collectionToSave.uid = collection.uid; collectionToSave.uid = collection.uid;
collectionToSave.userId = collection.userId;
collectionToSave.orgId = collection.orgId;
collectionToSave.environments = cloneDeep(collection.environments);
collectionToSave.items = []; collectionToSave.items = [];
copyItems(collection.items, collectionToSave.items); copyItems(collection.items, collectionToSave.items);
@ -243,7 +240,7 @@ export const deleteItemInCollection = (itemUid, collection) => {
export const isItemARequest = (item) => { export const isItemARequest = (item) => {
return item.hasOwnProperty('request') return item.hasOwnProperty('request')
&& ['http', 'graphql'].includes(item.type) && ['http-request', 'graphql-request'].includes(item.type)
&& !item.items; && !item.items;
}; };

View File

@ -6,7 +6,7 @@ import { sendHttpRequestInBrowser } from './browser';
const sendNetworkRequest = async (item, options) => { const sendNetworkRequest = async (item, options) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if(item.type === 'http') { if(item.type === 'http-request') {
const timeStart = Date.now(); const timeStart = Date.now();
sendHttpRequest(item.draft ? item.draft.request : item.request, options) sendHttpRequest(item.draft ? item.draft.request : item.request, options)
.then((response) => { .then((response) => {

View File

@ -1,7 +1,7 @@
import find from 'lodash/find'; import find from 'lodash/find';
export const isItemARequest = (item) => { export const isItemARequest = (item) => {
return item.hasOwnProperty('request') && ['http', 'graphql'].includes(item.type); return item.hasOwnProperty('request') && ['http-request', 'graphql-request'].includes(item.type);
}; };
export const isItemAFolder = (item) => { export const isItemAFolder = (item) => {

View File

@ -12,9 +12,9 @@ uid Unique id
name collection name name collection name
items Items (folders and requests) items Items (folders and requests)
|-uid A unique id |-uid A unique id
|-name Request name |-name Item name
|-type Item type (folder, http-request, graphql-request)
|-request Request object |-request Request object
|-type Request type (http, graphql)
|-url Request url |-url Request url
|-method Request method |-method Request method
|-headers Request headers (array of key-val) |-headers Request headers (array of key-val)

View File

@ -9,24 +9,22 @@ const keyValueSchema = Yup.object({
enabled: Yup.boolean().defined() enabled: Yup.boolean().defined()
}).noUnknown(true).strict(); }).noUnknown(true).strict();
const requestTypeSchema = Yup.string().oneOf(['http', 'graphql']).required('type is required');
const requestUrlSchema = Yup.string().min(0).max(2048, 'name must be 2048 characters or less').defined(); const requestUrlSchema = Yup.string().min(0).max(2048, 'name must be 2048 characters or less').defined();
const requestMethodSchema = Yup.string().oneOf(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']).required('method is required'); const requestMethodSchema = Yup.string().oneOf(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']).required('method is required');
const requestBodySchema = Yup.object({ const requestBodySchema = Yup.object({
mode: Yup.string().oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm']).required('mode is required'), mode: Yup.string().oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm']).required('mode is required'),
json: Yup.string().max(10240, 'json must be 10240 characters or less'), json: Yup.string().max(10240, 'json must be 10240 characters or less').nullable(),
text: Yup.string().max(10240, 'text must be 10240 characters or less'), text: Yup.string().max(10240, 'text must be 10240 characters or less').nullable(),
xml: Yup.string().max(10240, 'xml must be 10240 characters or less'), xml: Yup.string().max(10240, 'xml must be 10240 characters or less').nullable(),
formUrlEncoded: keyValueSchema, formUrlEncoded: Yup.array().of(keyValueSchema).nullable(),
multipartForm: keyValueSchema, multipartForm: Yup.array().of(keyValueSchema).nullable(),
}).noUnknown(true).strict(); }).noUnknown(true).strict();
// Right now, the request schema is very tightly coupled with http request // Right now, the request schema is very tightly coupled with http request
// As we introduce more request types in the future, we will improve the definition to support // As we introduce more request types in the future, we will improve the definition to support
// schema structure based on other request type // schema structure based on other request type
const requestSchema = Yup.object({ const requestSchema = Yup.object({
type: requestTypeSchema,
url: requestUrlSchema, url: requestUrlSchema,
method: requestMethodSchema, method: requestMethodSchema,
headers: Yup.array().of(keyValueSchema).required('headers are required'), headers: Yup.array().of(keyValueSchema).required('headers are required'),
@ -36,13 +34,13 @@ const requestSchema = Yup.object({
const itemSchema = Yup.object({ const itemSchema = Yup.object({
uid: uidSchema, uid: uidSchema,
type: Yup.string().oneOf(['request', 'folder']).required('type is required'), type: Yup.string().oneOf(['http-request', 'graphql-request', 'folder']).required('type is required'),
name: Yup.string() name: Yup.string()
.min(1, 'name must be atleast 1 characters') .min(1, 'name must be atleast 1 characters')
.max(50, 'name must be 100 characters or less') .max(50, 'name must be 100 characters or less')
.required('name is required'), .required('name is required'),
request: requestSchema.when('type', { request: requestSchema.when('type', {
is: 'request', is: (type) => ['http-request', 'graphql-request'].includes(type),
then: (schema) => schema.required('request is required when item-type is request') then: (schema) => schema.required('request is required when item-type is request')
}), }),
items: Yup.lazy(() => Yup.array().of(itemSchema)) items: Yup.lazy(() => Yup.array().of(itemSchema))

View File

@ -46,9 +46,8 @@ describe('Collection Schema Validation', () => {
items: [{ items: [{
uid: uuid(), uid: uuid(),
name: 'Get Countries', name: 'Get Countries',
type: 'request', type: 'http-request',
request: { request: {
type: 'http',
url: 'https://restcountries.com/v2/alpha/in', url: 'https://restcountries.com/v2/alpha/in',
method: 'GET', method: 'GET',
headers: [], headers: [],
@ -95,9 +94,8 @@ describe('Collection Schema Validation', () => {
items: [{ items: [{
uid: uuid(), uid: uuid(),
name: 'Get Countries', name: 'Get Countries',
type: 'request', type: 'http-request',
request: { request: {
type: 'http',
url: 'https://restcountries.com/v2/alpha/in', url: 'https://restcountries.com/v2/alpha/in',
method: 'GET', method: 'GET',
headers: [], headers: [],

View File

@ -41,11 +41,25 @@ describe('Item Schema Validation', () => {
]); ]);
}); });
it('item schema must throw an error if request is not present when item-type is request', async () => { it('item schema must throw an error if request is not present when item-type is http-request', async () => {
const item = { const item = {
uid: uuid(), uid: uuid(),
name: 'Get Users', name: 'Get Users',
type: 'request' type: 'http-request'
};
return Promise.all([
expect(itemSchema.validate(item)).rejects.toEqual(
validationErrorWithMessages('request is required when item-type is request')
)
]);
});
it('item schema must throw an error if request is not present when item-type is graphql-request', async () => {
const item = {
uid: uuid(),
name: 'Get Users',
type: 'graphql-request'
}; };
return Promise.all([ return Promise.all([

View File

@ -5,7 +5,6 @@ const { requestSchema } = require("./index");
describe('Request Schema Validation', () => { describe('Request Schema Validation', () => {
it('request schema must validate successfully - simple request', async () => { it('request schema must validate successfully - simple request', async () => {
const request = { const request = {
type: 'http',
url: 'https://restcountries.com/v2/alpha/in', url: 'https://restcountries.com/v2/alpha/in',
method: 'GET', method: 'GET',
headers: [], headers: [],
@ -19,28 +18,8 @@ describe('Request Schema Validation', () => {
expect(isValid).toBeTruthy(); expect(isValid).toBeTruthy();
}); });
it('request schema must throw an error of type is invalid', async () => {
const request = {
type: 'http-junk',
url: 'https://restcountries.com/v2/alpha/in',
method: 'GET',
headers: [],
params: [],
body: {
mode: 'none'
}
};
return Promise.all([
expect(requestSchema.validate(request)).rejects.toEqual(
validationErrorWithMessages('type must be one of the following values: http, graphql')
)
]);
});
it('request schema must throw an error of method is invalid', async () => { it('request schema must throw an error of method is invalid', async () => {
const request = { const request = {
type: 'http',
url: 'https://restcountries.com/v2/alpha/in', url: 'https://restcountries.com/v2/alpha/in',
method: 'GET-junk', method: 'GET-junk',
headers: [], headers: [],
@ -59,7 +38,6 @@ describe('Request Schema Validation', () => {
it('request schema must throw an error of header name is missing', async () => { it('request schema must throw an error of header name is missing', async () => {
const request = { const request = {
type: 'http',
url: 'https://restcountries.com/v2/alpha/in', url: 'https://restcountries.com/v2/alpha/in',
method: 'GET', method: 'GET',
headers: [{ headers: [{
@ -83,7 +61,6 @@ describe('Request Schema Validation', () => {
it('request schema must throw an error of param value is missing', async () => { it('request schema must throw an error of param value is missing', async () => {
const request = { const request = {
type: 'http',
url: 'https://restcountries.com/v2/alpha/in', url: 'https://restcountries.com/v2/alpha/in',
method: 'GET', method: 'GET',
headers: [], headers: [],

View File

@ -1,5 +1,7 @@
const { workspaceSchema} = require("./workspaces"); const { workspaceSchema } = require("./workspaces");
const { collectionSchema } = require("./collections");
module.exports = { module.exports = {
collectionSchema,
workspaceSchema workspaceSchema
}; };