forked from extern/bruno
feat: added schema validation before saving collections to idb
This commit is contained in:
parent
a78bdf87fe
commit
4ff268712f
@ -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}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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' });
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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) => {
|
||||
|
@ -4,7 +4,6 @@ import filter from 'lodash/filter';
|
||||
|
||||
const initialState = {
|
||||
workspaces: [],
|
||||
collectionUids: [],
|
||||
activeWorkspaceUid: null
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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: [],
|
||||
|
@ -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([
|
||||
|
@ -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: [],
|
||||
|
@ -1,5 +1,7 @@
|
||||
const { workspaceSchema} = require("./workspaces");
|
||||
const { workspaceSchema } = require("./workspaces");
|
||||
const { collectionSchema } = require("./collections");
|
||||
|
||||
module.exports = {
|
||||
collectionSchema,
|
||||
workspaceSchema
|
||||
};
|
Loading…
Reference in New Issue
Block a user