Skip to content

Commit

Permalink
Validate JSONPath syntax #40
Browse files Browse the repository at this point in the history
  • Loading branch information
rvansa committed Oct 22, 2020
1 parent 98de397 commit 54eaa76
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 8 deletions.
29 changes: 28 additions & 1 deletion src/main/java/io/hyperfoil/tools/horreum/api/SqlService.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
Expand All @@ -25,7 +27,6 @@
import java.util.List;
import java.util.Map;
import java.lang.Exception;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
Expand Down Expand Up @@ -56,6 +57,32 @@ public class SqlService {
private ExecutorService abortExecutor = Executors.newSingleThreadExecutor();
private Map<String, String> signedRoleCache = new ConcurrentHashMap<>();

@GET
@PermitAll
@Path("testjsonpath")
public Response testJsonPath(@QueryParam("query") String jsonpath) {
if (jsonpath == null) {
return Response.status(Response.Status.BAD_REQUEST).entity("No query").build();
}
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT jsonb_path_query_first('{}', ('$' || ?)::jsonpath)")) {
statement.setString(1, jsonpath);
Json result = new Json(false);
try {
statement.execute();
result.add("valid", true);
} catch (SQLException ee) {
result.add("valid", false);
result.add("reason", ee.getMessage());
result.add("errorCode", ee.getErrorCode());
result.add("sqlState", ee.getSQLState());
}
return Response.ok(result).build();
} catch (SQLException e) {
return Response.serverError().entity("Cannot connect to DB.").build();
}
}

@DenyAll
@GET
public Json get(@QueryParam("q") String sql) {
Expand Down
36 changes: 33 additions & 3 deletions webapp/src/components/Accessors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { addOrUpdateExtractor, listExtractors } from '../domain/schemas/api'
import SchemaSelect from './SchemaSelect'

import { alertAction } from "../alerts"
import { testJsonPath } from "../domain/schemas/api"

import {
ActionGroup,
Expand Down Expand Up @@ -32,6 +33,13 @@ function baseName(name: string) {
return name.endsWith("[]") ? name.substring(0, name.length - 2) : name
}

export type ValidationResult = {
valid: boolean,
reason: string,
errorCode: number,
sqlState: string,
}

export interface Extractor {
accessor: string,
schema?: string,
Expand All @@ -40,6 +48,9 @@ export interface Extractor {
newName?: string,
deleted?: boolean,
changed?: boolean,
// temprary fields
validationTimer?: any,
validationResult?: ValidationResult,
}

type AccessorsProps = {
Expand Down Expand Up @@ -157,13 +168,32 @@ export default ({ value = [], onChange = (_: string[]) => {}, isReadOnly, allowA
disabled={ disabledSchemas }
onChange={ value => { setCreated({ ...created, schema: value })}} />
</FormGroup>
<FormGroup label="JSON path" isRequired={true} fieldId="extractor-jsonpath">
<FormGroup
label="JSON path"
isRequired={true}
fieldId="extractor-jsonpath"
validated={ !(created.jsonpath && created.jsonpath.trim().startsWith("$")) && (!created.validationResult || created.validationResult.valid) ? "default" : "error"}
helperTextInvalid={ created.jsonpath && created.jsonpath.trim().startsWith("$") ? "JSON path must not start with '$'" : (created.validationResult?.reason || "") }
>
<TextInput value={ created?.jsonpath || "" }
isRequired
id="extractor-jsonpath"
name="extractor-jsonpath"
validated={ !!created.jsonpath && created.jsonpath !== "" && !created.jsonpath.startsWith("$") ? "default" : "error" }
onChange={ value => setCreated({ ...created, jsonpath: value})}
onChange={ value => {
if (created.validationTimer) {
clearTimeout(created.validationTimer)
}
created.validationTimer = window.setTimeout(() => {
if (created.jsonpath) {
testJsonPath(value).then(result => {
created.validationResult = result
setCreated({ ...created })
})
}
}, 1000)
created.jsonpath = value
setCreated({ ...created })
}}
/>
</FormGroup>
<ActionGroup>
Expand Down
18 changes: 15 additions & 3 deletions webapp/src/domain/schemas/Schema.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import Editor, { ValueGetter } from '../../components/Editor/monaco/Editor';
import AccessIcon from '../../components/AccessIcon'
import AccessChoice from '../../components/AccessChoice'
import OwnerSelect from '../../components/OwnerSelect'
import { Extractor } from '../../components/Accessors';
import { Extractor, ValidationResult } from '../../components/Accessors';
import { Schema } from './reducers';

type SchemaParams = {
Expand Down Expand Up @@ -282,16 +282,28 @@ export default () => {
}}/>
</FormGroup>
<FormGroup label="JSON path" fieldId="jsonpath"
validated={!e.jsonpath || !e.jsonpath.trim().startsWith("$") ? "default" : "error"}
helperTextInvalid="JSON path must not start with '$'">
validated={ !(e.jsonpath && e.jsonpath.trim().startsWith("$")) && (!e.validationResult || e.validationResult.valid) ? "default" : "error"}
helperTextInvalid={ e.jsonpath && e.jsonpath.trim().startsWith("$") ? "JSON path must not start with '$'" : (e.validationResult?.reason || "") }>
<TextInput id="jsonpath"
value={e.jsonpath}
isReadOnly={!isTester}
validated={!e.jsonpath || !e.jsonpath.trim().startsWith("$") ? "default" : "error"}
onChange={newValue => {
e.jsonpath = newValue;
e.changed = true
e.validationResult = undefined
setExtractors([...extractors])
if (e.validationTimer) {
clearTimeout(e.validationTimer)
}
e.validationTimer = window.setTimeout(() => {
if (e.jsonpath) {
api.testJsonPath(e.jsonpath).then(result => {
e.validationResult = result
setExtractors([...extractors])
})
}
}, 1000)
}}/>
</FormGroup>
{ isTester &&
Expand Down
5 changes: 4 additions & 1 deletion webapp/src/domain/schemas/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const endPoints = {
updateAccess: (id: number, owner: string, access: Access) => `${base}/${id}/updateAccess?owner=${owner}&access=${access}`,
extractor: () => `${base}/extractor`,
extractorForSchema: (schemaId: number) => `${base}/extractor?schemaId=${schemaId}`,
testJsonPath: (jsonpath: string) => `/api/sql/testjsonpath?query=${jsonpath}`
}
export const all = ()=>{
return fetchApi(endPoints.base(),null,'get');
Expand Down Expand Up @@ -42,4 +43,6 @@ export const listExtractors = (schemaId?: number) => {

export const addOrUpdateExtractor = (extractor: Extractor) => fetchApi(endPoints.extractor(), extractor, 'post')

export const deleteSchema = (id: number) => fetchApi(endPoints.crud(id), null, 'delete')
export const deleteSchema = (id: number) => fetchApi(endPoints.crud(id), null, 'delete')

export const testJsonPath = (jsonpath: string) => fetchApi(endPoints.testJsonPath(jsonpath), null, 'get')

0 comments on commit 54eaa76

Please sign in to comment.