Skip to content

Commit

Permalink
fix: remove jsonb_array and fix jsonb data types such that it can be …
Browse files Browse the repository at this point in the history
…used in schema design and graphql api (#4409)
  • Loading branch information
mswertz authored Nov 27, 2024
1 parent 0099429 commit 1b4b502
Showing 29 changed files with 421 additions and 92 deletions.
4 changes: 3 additions & 1 deletion apps/metadata-utils/src/fieldHelpers.ts
Original file line number Diff line number Diff line change
@@ -32,6 +32,7 @@ export const fieldTypes = () => {
"TEXT_ARRAY",
"UUID",
"UUID_ARRAY",
"JSON",
];
};

@@ -56,7 +57,8 @@ export const isValueType = (column: IColumn) => {
column.columnType === "DATETIME" ||
column.columnType === "INT" ||
column.columnType === "LONG" ||
column.columnType === "DECIMAL"
column.columnType === "DECIMAL" ||
column.columnType === "JSON"
);
};

3 changes: 1 addition & 2 deletions apps/metadata-utils/src/types.ts
Original file line number Diff line number Diff line change
@@ -30,8 +30,7 @@ export type CellValueType =
| "DATETIME"
| "DATETIME_ARRAY"
| "PERIOD"
| "JSONB"
| "JSONB_ARRAY"
| "JSON"
| "REF"
| "REF_ARRAY"
| "REFBACK"
2 changes: 2 additions & 0 deletions apps/molgenis-components/lib/main.js
Original file line number Diff line number Diff line change
@@ -53,6 +53,7 @@ import InputDate from "../src/components/forms/InputDate.vue";
import InputDateTime from "../src/components/forms/InputDateTime.vue";
import InputDecimal from "../src/components/forms/InputDecimal.vue";
import InputEmail from "../src/components/forms/InputEmail.vue";
import InputJson from "../src/components/forms/InputJson.vue";
import InputFile from "../src/components/forms/InputFile.vue";
import InputGroup from "../src/components/forms/InputGroup.vue";
import InputHeading from "../src/components/forms/InputHeading.vue";
@@ -189,6 +190,7 @@ export {
InputDateTime,
InputDecimal,
InputEmail,
InputJson,
InputFile,
InputGroup,
InputHeading,
Original file line number Diff line number Diff line change
@@ -48,6 +48,7 @@ const filterTypeMap = {
HYPERLINK_ARRAY: StringFilter,
TEXT: StringFilter,
TEXT_ARRAY: StringFilter,
JSON: StringFilter,
UUID: StringFilter,
UUID_ARRAY: StringFilter,
INT: IntegerFilter,
14 changes: 14 additions & 0 deletions apps/molgenis-components/src/components/forms/FormInput.vue
Original file line number Diff line number Diff line change
@@ -34,6 +34,7 @@ import InputText from "../forms/InputText.vue";
import BaseInput from "../forms/baseInputs/BaseInput.vue";
import InputEmail from "./InputEmail.vue";
import InputHyperlink from "./InputHyperlink.vue";
import InputJson from "./InputJson.vue";
import InputRefList from "./InputRefList.vue";
const typeToInputMap = {
@@ -43,6 +44,7 @@ const typeToInputMap = {
HYPERLINK: InputHyperlink,
STRING: InputString,
TEXT: InputText,
JSON: InputJson,
INT: InputInt,
LONG: InputLong,
DECIMAL: InputDecimal,
@@ -368,6 +370,17 @@ export default {
</div>
<div>You typed: {{ JSON.stringify(textValueArray, null, 2) }}</div>
</DemoItem>
<DemoItem>
<div>
<FormInput
id="json-example"
columnType="JSON"
label="Example json input"
v-model="jsonValue"
/>
</div>
<div>You typed: {{ jsonValue }}</div>
</DemoItem>
<DemoItem>
<div>
<FormInput
@@ -540,6 +553,7 @@ export default {
intValueArray: [5, 37],
textValue: "example text",
textValueArray: ["text", "more text"],
jsonValue: '{"name":"bofke"}',
longValue: "1337",
longValueArray: ["0", "101"],
decimalValue: 3.7,
103 changes: 103 additions & 0 deletions apps/molgenis-components/src/components/forms/InputJson.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<template>
<FormGroup
:id="id"
:label="label"
:required="required"
:description="description"
:errorMessage="stringError"
>
<InputGroup>
<textarea
:id="id"
:ref="id"
:name="name"
:value="modelValue"
@input="
$emit('update:modelValue', ($event.target as HTMLInputElement).value)
"
type="text"
class="form-control"
:class="{ 'is-invalid': stringError }"
:aria-describedby="id"
:placeholder="placeholder"
:readonly="readonly"
/>
</InputGroup>
</FormGroup>
</template>

<script lang="ts">
import FormGroup from "./FormGroup.vue";
import InputGroup from "./InputGroup.vue";
import BaseInput from "./baseInputs/BaseInput.vue";
import { isJsonObjectOrArray } from "./formUtils/formUtils";
export default {
name: "InputJson",
components: { FormGroup, InputGroup },
extends: BaseInput,
props: {
modelValue: {
type: [String, null],
default: null,
},
},
computed: {
stringError() {
if (typeof this.modelValue === "string") {
try {
if (!isJsonObjectOrArray(JSON.parse(this.modelValue))) {
return `Root element must be an object or array`;
}
} catch {
return `Please enter valid JSON`;
}
return this.errorMessage;
} else {
return this.errorMessage;
}
},
},
};
</script>

<style scoped>
.is-invalid {
background-image: none;
}
span:hover .hoverIcon {
visibility: visible;
}
</style>

<docs>
<template>
<div>
<InputJson
id="input-json"
v-model="value"
label="My JSON input label"
description="Some help needed?"
/>
You typed: {{ value }}<br />
<b>Readonly</b>
<InputJson
id="input-json2"
:readonly="true"
v-model="readOnlyValue"
description="Should not be able to edit this"
/>
</div>
</template>
<script>
export default {
data: function () {
return {
value: '{"name":"bofke"}',
readOnlyValue: '{"name":"bofke"}',
};
},
};
</script>
</docs>
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ import {
splitColumnIdsByHeadings,
isMissingValue,
isRequired,
isJsonObjectOrArray,
} from "./formUtils";
import type { ITableMetaData, IColumn } from "metadata-utils";
const { AUTO_ID, HEADING } = constants;
@@ -375,4 +376,23 @@ describe("isValidHyperLink", () => {
constants.HYPERLINK_REGEX.test("https://example.com/(test)".toLowerCase())
).toBe(true);
});

describe("isJsonObjectOrArray", () => {
test("only JSON object/array should return true (after parsing from JSON string)", () => {
expect(isJsonObjectOrArray(JSON.parse('{"key":"value"}'))).toBe(true);
expect(isJsonObjectOrArray(JSON.parse('["string1", "string2"]'))).toBe(
true
);
expect(
isJsonObjectOrArray(
JSON.parse('{"key1":{"key2":["value1", "value2"]}}')
)
).toBe(true);
expect(isJsonObjectOrArray(JSON.parse('"string"'))).toBe(false);
expect(isJsonObjectOrArray(JSON.parse("1"))).toBe(false);
expect(isJsonObjectOrArray(JSON.parse("true"))).toBe(false);
expect(isJsonObjectOrArray(JSON.parse("false"))).toBe(false);
expect(isJsonObjectOrArray(JSON.parse("null"))).toBe(false);
});
});
});
Original file line number Diff line number Diff line change
@@ -84,6 +84,15 @@ function getColumnError(
if (type === "PERIOD_ARRAY" && containsInvalidPeriod(value)) {
return "Invalid Period: should start with a P and should contain at least a Y(year), M(month) or D(day): e.g. 'P1Y3M14D'";
}
if (type === "JSON") {
try {
if (!isJsonObjectOrArray(JSON.parse(value))) {
return `Root element must be an object or array`;
}
} catch {
return `Please enter valid JSON`;
}
}
if (column.validation) {
return getColumnValidationError(column.validation, rowData, tableMetaData);
}
@@ -227,6 +236,13 @@ function containsInvalidPeriod(periods: any) {
return periods.find((period: any) => !isValidPeriod(period));
}

export function isJsonObjectOrArray(parsedJson: any) {
if (typeof parsedJson === "object" && parsedJson !== null) {
return true;
}
return false;
}

export function removeKeyColumns(tableMetaData: ITableMetaData, rowData: IRow) {
const keyColumnsIds = tableMetaData?.columns
?.filter((column: IColumn) => column.key === 1)
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ import HyperlinkDisplay from "./cellTypes/HyperlinkDisplay.vue";
const typeMap: { [key: string]: string } = {
FILE: "FileDisplay",
TEXT: "TextDisplay",
JSON: "TextDisplay",
REFBACK: "ListDisplay",
REF: "ObjectDisplay",
ONTOLOGY: "ObjectDisplay",
Original file line number Diff line number Diff line change
@@ -835,7 +835,8 @@ function graphqlFilter(
if (conditions.length) {
if (
col.columnType.startsWith("STRING") ||
col.columnType.startsWith("TEXT")
col.columnType.startsWith("TEXT") ||
col.columnType.startsWith("JSON")
) {
filter[col.id] = { like: conditions };
} else if (col.columnType.startsWith("BOOL")) {
1 change: 1 addition & 0 deletions apps/schema/src/columnTypes.js
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ export default [
"HYPERLINK_ARRAY",
"INT",
"INT_ARRAY",
"JSON",
"LONG",
"LONG_ARRAY",
"ONTOLOGY",
Original file line number Diff line number Diff line change
@@ -1,19 +1,57 @@
package org.molgenis.emx2.graphql;

import com.fasterxml.jackson.core.JsonProcessingException;
import graphql.schema.*;
import jakarta.servlet.http.Part;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import org.molgenis.emx2.BinaryFileWrapper;
import org.molgenis.emx2.utils.MolgenisObjectMapper;

public class GraphqlCustomTypes {
private static final MolgenisObjectMapper objectMapper = MolgenisObjectMapper.INTERNAL;

private GraphqlCustomTypes() {
// hide constructor
}

public static final GraphQLScalarType GraphQLJsonAsString =
GraphQLScalarType.newScalar()
.name("JsonString")
.description("A JSON represented as string")
.coercing(
new Coercing<String, String>() {

@Override
public String serialize(Object dataFetcherResult) {
// Convert Java object to JSON string
try {
return objectMapper.getWriter().writeValueAsString(dataFetcherResult);
} catch (JsonProcessingException e) {
throw new CoercingSerializeException(
"Unable to serialize to JSON string: " + e.getMessage());
}
}

@Override
public String parseValue(Object input) {
// Pass-through parsing (only used for input values)
return input.toString();
}

@Override
public String parseLiteral(Object input) {
// Pass-through literal parsing
if (input instanceof graphql.language.StringValue) {
return ((graphql.language.StringValue) input).getValue();
}
throw new CoercingParseLiteralException("Value is not a valid JSON string");
}
})
.build();

// thanks to https://stackoverflow.com/questions/57372259/how-to-upload-files-with-graphql-java
public static final GraphQLScalarType GraphQLFileUpload =
GraphQLScalarType.newScalar()
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@
import static org.molgenis.emx2.graphql.GraphqlApiMutationResult.Status.SUCCESS;
import static org.molgenis.emx2.graphql.GraphqlApiMutationResult.typeForMutationResult;
import static org.molgenis.emx2.graphql.GraphqlConstants.*;
import static org.molgenis.emx2.graphql.GraphqlCustomTypes.GraphQLJsonAsString;
import static org.molgenis.emx2.sql.SqlQuery.*;

import graphql.Scalars;
@@ -187,6 +188,10 @@ private void createTableField(Column col, GraphQLObjectType.Builder tableBuilder
tableBuilder.field(
GraphQLFieldDefinition.newFieldDefinition().name(id).type(Scalars.GraphQLString));
break;
case JSON:
tableBuilder.field(
GraphQLFieldDefinition.newFieldDefinition().name(id).type(GraphQLJsonAsString));
break;
case STRING_ARRAY:
case EMAIL_ARRAY:
case HYPERLINK_ARRAY:
@@ -196,7 +201,6 @@ private void createTableField(Column col, GraphQLObjectType.Builder tableBuilder
case DATETIME_ARRAY:
case PERIOD_ARRAY:
case UUID_ARRAY:
case JSONB_ARRAY:
tableBuilder.field(
GraphQLFieldDefinition.newFieldDefinition()
.name(id)
@@ -507,6 +511,8 @@ private GraphQLScalarType graphQLTypeOf(Column col) {
return GraphQLLong;
case DECIMAL, DECIMAL_ARRAY:
return Scalars.GraphQLFloat;
case JSON:
return GraphQLJsonAsString;
case DATE,
DATETIME,
PERIOD,
@@ -908,6 +914,7 @@ private GraphQLInputType getGraphQLInputType(ColumnType columnType) {
case INT_ARRAY -> GraphQLList.list(Scalars.GraphQLInt);
case LONG_ARRAY -> GraphQLList.list(GraphQLLong);
case DECIMAL_ARRAY -> GraphQLList.list(Scalars.GraphQLFloat);
case JSON -> GraphqlCustomTypes.GraphQLJsonAsString;
case STRING_ARRAY,
TEXT_ARRAY,
DATE_ARRAY,
Loading
Oops, something went wrong.

0 comments on commit 1b4b502

Please sign in to comment.