Skip to content

Commit e3000b6

Browse files
authored
fix: ERPNext DocTypes support for generating sdls (#58)
* fix: use `format_docty[e` for all doctype instances * fix: warn and skip doctypes with invalid names * fix: rework Enum name generation to reduce name clashes and prevent invalid charaters
1 parent 272a413 commit e3000b6

File tree

2 files changed

+121
-26
lines changed

2 files changed

+121
-26
lines changed

frappe_graphql/utils/generate_sdl/__init__.py

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
IGNORED_DOCTYPES = [
77
"Installed Application",
88
"Installed Applications",
9+
"Content Activity", # broken EPRNext DocType
910
]
1011

1112
SDL_PREDEFINED_DOCTYPES = [
@@ -19,7 +20,24 @@
1920
"Gender", "Has Role", "Role Profile", "Role", "Language",
2021

2122
# File.attached_to_doctype
22-
"DocType", "Module Def", "DocField", "DocPerm"
23+
"DocType", "Module Def", "DocField", "DocPerm",
24+
25+
# Other
26+
"DocType Action",
27+
"DocType Link",
28+
"Domain",
29+
"Dynamic Link"
30+
]
31+
32+
GQL_RESERVED_TERMS = [
33+
"Query",
34+
"Mutation",
35+
"Subscription",
36+
"Int",
37+
"Float",
38+
"Boolean",
39+
"ID",
40+
"String",
2341
]
2442

2543

@@ -47,8 +65,34 @@ def write_file(filename, contents):
4765
f.write(contents)
4866

4967
for doctype in doctypes:
50-
if doctype not in specific_doctypes and \
51-
(doctype in IGNORED_DOCTYPES or doctype in SDL_PREDEFINED_DOCTYPES):
68+
69+
# Warn if there is an "s" form plural of a doctype
70+
if doctype[:-2:-1] == "s":
71+
if doctype[:-1:1] in doctypes and doctype not in IGNORED_DOCTYPES:
72+
73+
IGNORED_DOCTYPES.append(doctype)
74+
75+
print("WARN: sdl generation of DocTypes that are named with the 's' form " +
76+
"plural of another DocType is not supported. " +
77+
f"Skipping sdl generation for \"{doctype}\"")
78+
79+
# Warn if a DocType has a reserved name
80+
if doctype in GQL_RESERVED_TERMS:
81+
print("WARN: sdl generation of DocTypes that share names with the following " +
82+
f"GQL Reserved terms is not supported: {GQL_RESERVED_TERMS}. " +
83+
f"Skipping sdl generation for \"{doctype}\"")
84+
85+
# Warn if a Doctype has an 'invalid' name
86+
if "-" in doctype:
87+
print("WARN: The following DocType has an invalid character '-' in its name " +
88+
f"and will not be resolved automatically: {doctype}. " +
89+
"A custom resolver will have to be implemented.")
90+
91+
if doctype not in specific_doctypes and (
92+
doctype in IGNORED_DOCTYPES or
93+
doctype in SDL_PREDEFINED_DOCTYPES or
94+
doctype in GQL_RESERVED_TERMS
95+
):
5296
continue
5397
sdl = get_doctype_sdl(doctype=doctype, options=options)
5498
write_file(doctype, sdl)

frappe_graphql/utils/generate_sdl/doctype.py

Lines changed: 74 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
import inflect
23

34
import frappe
@@ -13,15 +14,17 @@ def get_doctype_sdl(doctype, options):
1314
ignore_custom_fields=False
1415
)
1516
"""
17+
generated_enums = frappe._dict()
18+
1619
meta = frappe.get_meta(doctype)
17-
sdl, defined_fieldnames = get_basic_doctype_sdl(meta, options=options)
20+
sdl, defined_fieldnames = get_basic_doctype_sdl(meta, options=options, generated_enums=generated_enums)
1821

1922
# Extend Doctype with Custom Fields
2023
if not options.ignore_custom_fields and len(meta.get_custom_fields()):
2124
sdl += get_custom_field_sdl(meta, defined_fieldnames, options=options)
2225

2326
if not options.disable_enum_select_fields:
24-
sdl += get_select_docfield_enums(meta=meta, options=options)
27+
sdl += get_select_docfield_enums(meta=meta, options=options, generated_enums=generated_enums)
2528

2629
# DocTypeSortingInput
2730
if not meta.issingle:
@@ -34,7 +37,7 @@ def get_doctype_sdl(doctype, options):
3437
return sdl
3538

3639

37-
def get_basic_doctype_sdl(meta: Meta, options: dict):
40+
def get_basic_doctype_sdl(meta: Meta, options: dict, generated_enums=None):
3841
dt = format_doctype(meta.name)
3942
sdl = f"type {dt} implements BaseDocType {{"
4043

@@ -62,7 +65,7 @@ def get_basic_doctype_sdl(meta: Meta, options: dict):
6265
if cint(field.get("is_custom_field")):
6366
continue
6467
defined_fieldnames.append(field.fieldname)
65-
sdl += f"\n {get_field_sdl(meta, field, options=options)}"
68+
sdl += f"\n {get_field_sdl(meta, field, options=options, generated_enums=generated_enums)}"
6669
if field.fieldtype in ("Link", "Dynamic Link"):
6770
sdl += f"\n {get_link_field_name_sdl(field)}"
6871

@@ -87,14 +90,26 @@ def get_custom_field_sdl(meta, defined_fieldnames, options):
8790
return sdl
8891

8992

90-
def get_select_docfield_enums(meta, options):
93+
def get_select_docfield_enums(meta, options, generated_enums=None):
9194
sdl = ""
9295
for field in meta.get("fields", {"fieldtype": "Select"}):
93-
if options.ignore_custom_fields and cint(field.get("is_custom_field")):
96+
97+
has_no_options = all([len(x or "") == 0 for x in (field.options or "").split("\n")])
98+
99+
has_invalid_options = False
100+
if any([
101+
contains_reserved_characters(option)
102+
for option in (field.options or "").split("\n")
103+
]):
104+
has_invalid_options = True
105+
106+
if (options.ignore_custom_fields and cint(field.get("is_custom_field"))) \
107+
or has_no_options \
108+
or has_invalid_options:
94109
continue
95110

96111
sdl += "\n\n"
97-
sdl += f"enum {get_select_docfield_enum_name(meta.name, field)} {{"
112+
sdl += f"enum {get_select_docfield_enum_name(meta.name, field, generated_enums)} {{"
98113
for option in (field.get("options") or "").split("\n"):
99114
if not option or not len(option):
100115
continue
@@ -166,15 +181,15 @@ def get_query_type_extension(meta: Meta):
166181
return sdl
167182

168183

169-
def get_field_sdl(meta, docfield, options: dict):
170-
return f"{docfield.fieldname}: {get_graphql_type(meta, docfield, options=options)}"
184+
def get_field_sdl(meta, docfield, options: dict, generated_enums: list = None):
185+
return f"{docfield.fieldname}: {get_graphql_type(meta, docfield, options=options, generated_enums=generated_enums)}"
171186

172187

173188
def get_link_field_name_sdl(docfield):
174189
return f"{docfield.fieldname}__name: String"
175190

176191

177-
def get_graphql_type(meta, docfield, options: dict):
192+
def get_graphql_type(meta, docfield, options: dict, generated_enums=None):
178193
string_fieldtypes = [
179194
"Small Text", "Long Text", "Code", "Text Editor", "Markdown Editor", "HTML Editor",
180195
"Date", "Datetime", "Time", "Text", "Data", "Rating", "Read Only",
@@ -194,23 +209,29 @@ def get_graphql_type(meta, docfield, options: dict):
194209
elif docfield.fieldtype in float_fieldtypes:
195210
graphql_type = "Float"
196211
elif docfield.fieldtype == "Link":
197-
graphql_type = f"{docfield.options.replace(' ', '')}"
212+
graphql_type = f"{format_doctype(docfield.options)}"
198213
elif docfield.fieldtype == "Dynamic Link":
199214
graphql_type = "BaseDocType"
200215
elif docfield.fieldtype in table_fields:
201-
graphql_type = f"[{docfield.options.replace(' ', '')}!]!"
216+
graphql_type = f"[{format_doctype(docfield.options)}!]!"
202217
elif docfield.fieldtype == "Password":
203218
graphql_type = "Password"
204219
elif docfield.fieldtype == "Select":
205-
graphql_type = get_select_docfield_enum_name(meta.name, docfield)
220+
graphql_type = get_select_docfield_enum_name(meta.name, docfield, generated_enums)
206221

207222
# Mark NonNull if there is no empty option and is required
208-
has_empty_option = any(
209-
[len(x or "") == 0 for x in (docfield.options or "").split("\n")])
210-
if docfield.reqd and has_empty_option:
211-
frappe.throw(
212-
frappe._("Please check your SELECT doc field on doctype {0}: {1}. The select field cannot be empty and required.").format(docfield.parent, docfield.fieldname))
213-
if docfield.reqd and not has_empty_option:
223+
has_empty_option = all([len(x or "") == 0 for x in (docfield.options or "").split("\n")])
224+
225+
has_invalid_options = False
226+
if any([
227+
contains_reserved_characters(option)
228+
for option in (docfield.options or "").split("\n")
229+
]):
230+
has_invalid_options = True
231+
232+
if has_empty_option or has_invalid_options:
233+
graphql_type = "String"
234+
if docfield.reqd:
214235
graphql_type += "!"
215236
else:
216237
frappe.throw(f"Invalid fieldtype: {docfield.fieldtype}")
@@ -227,9 +248,39 @@ def get_plural(doctype):
227248

228249

229250
def format_doctype(doctype):
230-
return doctype.replace(" ", "")
251+
return remove_reserved_characters(doctype.replace(" ", "").replace("-", "_"))
252+
253+
254+
def get_select_docfield_enum_name(doctype, docfield, generated_enums=None):
255+
256+
name = remove_reserved_characters(
257+
f"{doctype}{(docfield.label or docfield.fieldname).title()}SelectOptions"
258+
.replace(" ", ""))
231259

260+
if name in generated_enums.values():
261+
name = remove_reserved_characters(
262+
f"{doctype}{(docfield.fieldname).title()}SelectOptions"
263+
.replace(" ", ""))
232264

233-
def get_select_docfield_enum_name(doctype, docfield):
234-
return f"{doctype}{(docfield.label or docfield.fieldname).title()}SelectOptions".replace(
235-
" ", "")
265+
if generated_enums is not None:
266+
if docfield in generated_enums:
267+
name = generated_enums[docfield]
268+
else:
269+
generated_enums[docfield] = name
270+
271+
return name
272+
273+
274+
def remove_reserved_characters(string):
275+
return re.sub(r"[^A-Za-z0-9_ ]", "", string)
276+
277+
278+
def contains_reserved_characters(string):
279+
if not string:
280+
return False
281+
282+
matches = re.match(r"^[A-Za-z_ ][A-Za-z0-9_ ]*$", string)
283+
if matches:
284+
return False
285+
else:
286+
return True

0 commit comments

Comments
 (0)