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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import find from 'lodash/find';
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) => {

View File

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

View File

@ -9,24 +9,22 @@ const keyValueSchema = Yup.object({
enabled: Yup.boolean().defined()
}).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 requestMethodSchema = Yup.string().oneOf(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']).required('method is required');
const requestBodySchema = Yup.object({
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'),
text: Yup.string().max(10240, 'text must be 10240 characters or less'),
xml: Yup.string().max(10240, 'xml must be 10240 characters or less'),
formUrlEncoded: keyValueSchema,
multipartForm: keyValueSchema,
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').nullable(),
xml: Yup.string().max(10240, 'xml must be 10240 characters or less').nullable(),
formUrlEncoded: Yup.array().of(keyValueSchema).nullable(),
multipartForm: Yup.array().of(keyValueSchema).nullable(),
}).noUnknown(true).strict();
// 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
// schema structure based on other request type
const requestSchema = Yup.object({
type: requestTypeSchema,
url: requestUrlSchema,
method: requestMethodSchema,
headers: Yup.array().of(keyValueSchema).required('headers are required'),
@ -36,13 +34,13 @@ const requestSchema = Yup.object({
const itemSchema = Yup.object({
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()
.min(1, 'name must be atleast 1 characters')
.max(50, 'name must be 100 characters or less')
.required('name is required'),
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')
}),
items: Yup.lazy(() => Yup.array().of(itemSchema))

View File

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

View File

@ -5,7 +5,6 @@ const { requestSchema } = require("./index");
describe('Request Schema Validation', () => {
it('request schema must validate successfully - simple request', async () => {
const request = {
type: 'http',
url: 'https://restcountries.com/v2/alpha/in',
method: 'GET',
headers: [],
@ -19,28 +18,8 @@ describe('Request Schema Validation', () => {
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 () => {
const request = {
type: 'http',
url: 'https://restcountries.com/v2/alpha/in',
method: 'GET-junk',
headers: [],
@ -59,7 +38,6 @@ describe('Request Schema Validation', () => {
it('request schema must throw an error of header name is missing', async () => {
const request = {
type: 'http',
url: 'https://restcountries.com/v2/alpha/in',
method: 'GET',
headers: [{
@ -83,7 +61,6 @@ describe('Request Schema Validation', () => {
it('request schema must throw an error of param value is missing', async () => {
const request = {
type: 'http',
url: 'https://restcountries.com/v2/alpha/in',
method: 'GET',
headers: [],

View File

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