Skip to content

Commit a963af0

Browse files
authored
strict-typed Scalars: JSON and JSONString. (#6490)
1 parent 15eced3 commit a963af0

9 files changed

Lines changed: 138 additions & 112 deletions

File tree

.changeset/some-wolves-wish.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"saleor-dashboard": patch
3+
---
4+
5+
Set strict-typed Scalars: JSON and JSONString. Previously Codegen generated `any` types, making them insecure in the codebase. Now they are `unknown` and `string`. Now it's explicit that JSON must be narrowed (e.g. with Zod schema) and JSONString must be first parsed.

codegen-main.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ const config: CodegenConfig = {
3939
// Decimal: "number",
4040
// Minute: "number",
4141
// GenericScalar: "JSONValue",
42-
// JSON: "JSONValue",
43-
// JSONString: "string",
42+
JSON: "unknown",
43+
JSONString: "string",
4444
// Metadata: "Record<string, string>",
4545
// PositiveDecimal: "number",
4646
// Upload: "unknown",

src/discounts/models/transformRule.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { type CataloguePredicateAPI, type OrderPredicateAPI } from "@dashboard/discounts/types";
12
import {
23
type PromotionRuleDetailsFragment,
34
type PromotionRuleInput,
@@ -35,27 +36,39 @@ export const mapAPIRuleToForm = (
3536
}
3637

3738
if (type === PromotionTypeEnum.CATALOGUE) {
39+
/**
40+
* TODO: Saleor stores predicate as JSON, which for Dashboard is "unknown".
41+
* We need to create a Zod schema that will parse it to known shape. If parsing fails, Sentry should be triggered.
42+
* Currently code implicitly casts, which should be ok, but is not entirely safe
43+
*/
44+
const predicate = rule.cataloguePredicate as CataloguePredicateAPI;
45+
3846
const catalogueConditions = prepareCatalogueRuleConditions(
39-
rule.cataloguePredicate,
47+
predicate,
4048
labelMaps.conditionsValues,
4149
);
4250

4351
return {
4452
...baseRuleData,
4553
conditions: catalogueConditions,
46-
hasPredicateNestedConditions: hasPredicateNestedConditions(rule.cataloguePredicate),
54+
hasPredicateNestedConditions: hasPredicateNestedConditions(predicate),
4755
};
4856
}
4957

5058
if (type === PromotionTypeEnum.ORDER) {
51-
const orderconditions = prepareOrderConditions(
52-
rule.orderPredicate?.discountedObjectPredicate ?? {},
53-
);
59+
/**
60+
* TODO: Saleor stores predicate as JSON, which for Dashboard is "unknown".
61+
* We need to create a Zod schema that will parse it to known shape. If parsing fails, Sentry should be triggered.
62+
* Currently code implicitly casts, which should be ok, but is not entirely safe
63+
*/
64+
const predicate = rule.orderPredicate as OrderPredicateAPI;
65+
66+
const orderconditions = prepareOrderConditions(predicate?.discountedObjectPredicate ?? {});
5467

5568
return {
5669
...baseRuleData,
5770
conditions: orderconditions,
58-
hasPredicateNestedConditions: hasPredicateNestedConditions(rule.orderPredicate),
71+
hasPredicateNestedConditions: hasPredicateNestedConditions(predicate),
5972
};
6073
}
6174

src/discounts/views/DiscountDetails/hooks/useFetchConditionsOptionsDetails.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,14 @@ export function getAllConditionsOptionsIdsToFetch(
5252
}
5353

5454
const allConditionsIds = data.promotion.rules.reduce((acc, rule) => {
55-
reduceConditionsLabels(rule.cataloguePredicate, acc);
55+
/**
56+
* TODO: Saleor stores predicate as JSON, which for Dashboard is "unknown".
57+
* We need to create a Zod schema that will parse it to known shape. If parsing fails, Sentry should be triggered.
58+
* Currently code implicitly casts, which should be ok, but is not entirely safe
59+
*/
60+
const predicate = rule.cataloguePredicate as CataloguePredicateAPI;
61+
62+
reduceConditionsLabels(predicate, acc);
5663

5764
return acc;
5865
}, initAllConditionsIds);

src/graphql/types.generated.ts

Lines changed: 92 additions & 92 deletions
Large diffs are not rendered by default.

src/products/components/ProductUpdatePage/ProductUpdatePage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,8 @@ const ProductUpdatePage = ({
320320
const newProductDescription = productDescriptionField.value;
321321

322322
// cache may be empty if editor was not used before sending event to app
323-
const productDescriptionWithFallback = descriptionCache.current ?? product.description;
323+
const productDescriptionWithFallback =
324+
descriptionCache.current ?? (JSON.parse(product.description) as OutputData);
324325

325326
try {
326327
const parsedEditorJs = JSON.parse(newProductDescription) as OutputData;

src/products/utils/handlers.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ describe("Product handlers", () => {
1616
id: "1",
1717
url: "",
1818
type: ProductMediaType.IMAGE,
19-
oembedData: null,
19+
oembedData: "",
2020
__typename: "ProductMedia",
2121
},
2222
{
2323
id: "2",
2424
url: "",
2525
type: ProductMediaType.IMAGE,
26-
oembedData: null,
26+
oembedData: "",
2727
__typename: "ProductMedia",
2828
},
2929
],
@@ -47,7 +47,7 @@ describe("Product handlers", () => {
4747
id: "3",
4848
url: "",
4949
type: ProductMediaType.IMAGE,
50-
oembedData: null,
50+
oembedData: "",
5151
__typename: "ProductMedia",
5252
},
5353
],
@@ -79,21 +79,21 @@ describe("Product handlers", () => {
7979
id: "1",
8080
url: "",
8181
type: ProductMediaType.IMAGE,
82-
oembedData: null,
82+
oembedData: "",
8383
__typename: "ProductMedia",
8484
},
8585
{
8686
id: "2",
8787
url: "",
8888
type: ProductMediaType.IMAGE,
89-
oembedData: null,
89+
oembedData: "",
9090
__typename: "ProductMedia",
9191
},
9292
{
9393
id: "3",
9494
url: "",
9595
type: ProductMediaType.IMAGE,
96-
oembedData: null,
96+
oembedData: "",
9797
__typename: "ProductMedia",
9898
},
9999
],
@@ -125,14 +125,14 @@ describe("Product handlers", () => {
125125
id: "1",
126126
url: "",
127127
type: ProductMediaType.IMAGE,
128-
oembedData: null,
128+
oembedData: "",
129129
__typename: "ProductMedia",
130130
},
131131
{
132132
id: "2",
133133
url: "",
134134
type: ProductMediaType.IMAGE,
135-
oembedData: null,
135+
oembedData: "",
136136
__typename: "ProductMedia",
137137
},
138138
],

src/translations/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export interface TranslationField<T extends string = string> {
3131
id?: string;
3232
displayName: string;
3333
name: T;
34-
translation: string;
34+
translation: string | null;
3535
type: TranslationFieldType;
3636
value: string;
3737
}

src/translations/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export const mapAttributeValuesToTranslationFields = (
7575
name: attrVal.name,
7676
translation: attrVal.translation?.richText || attrVal.translation?.plainText || null,
7777
type: attrVal.richText ? "rich" : "short",
78-
value: attrVal.richText || attrVal.plainText,
78+
value: attrVal.richText || attrVal.plainText || "",
7979
})) || [];
8080

8181
export const getAttributeValueTranslationsInputData = (

0 commit comments

Comments
 (0)