From f7bf44cc56e0e5fda4851ae42e8ba0f42089005f Mon Sep 17 00:00:00 2001 From: Vincent Laugier Date: Sun, 12 Dec 2021 15:46:50 +0100 Subject: [PATCH 1/4] Add generation of typescript code --- src/Tool/Tool-typescript.jl | 289 ++++++++++++++++++++++++++++++++++++ src/Tool/Tool.jl | 191 ++++-------------------- test/Tool/runtests-Tool.jl | 26 ++++ 3 files changed, 345 insertions(+), 161 deletions(-) create mode 100644 src/Tool/Tool-typescript.jl create mode 100644 test/Tool/runtests-Tool.jl diff --git a/src/Tool/Tool-typescript.jl b/src/Tool/Tool-typescript.jl new file mode 100644 index 0000000..df50239 --- /dev/null +++ b/src/Tool/Tool-typescript.jl @@ -0,0 +1,289 @@ +function generate_typescript_code(dbconn::LibPQ.Connection, + outdir::String, + relative_path_to_enum_dir::String + ;lang_code = "eng", + module_name_for_all_schemas::Union{String,Missing} = "Model") + + object_model = generate_object_model( + dbconn, + lang_code, + module_name_for_all_schemas = module_name_for_all_schemas + ) + + generate_typescript_enums_from_object_model(object_model, outdir) + + generate_typescript_classes_from_object_model(object_model, + outdir, + relative_path_to_enum_dir) + +end + + +function generate_typescript_enums_from_object_model(object_model::Dict, outdir::String) + + outdir = joinpath(outdir,"enum") + + if !isdir(outdir) + mkpath(outdir) + end + + enums = object_model[:enums] + + for e in enums + + type_name = e[:type_name] + file_path = joinpath(outdir,"$type_name.ts") + + content = "export enum $type_name {\n\n" + + for (idx,v) in enumerate(e[:values]) + content *= " $v = $idx, \n" + end + + content *= "\n" + content *= "}" # close enum + + write(file_path,content) + end + + + +end + +""" + get_typescript_type_of_elt_type(julia_elt_type::String) + +Eg. Model.Patient returns Patient +""" + function get_typescript_type_of_elt_type(julia_elt_type::String) + + julia_elt_type = (last ∘ split)(julia_elt_type,".") + + typescript_elt_type = try + basic_elt_type = eval(Symbol(julia_elt_type)) + if basic_elt_type == Bool + typescript_elt_type = "boolean" + elseif basic_elt_type == String + typescript_elt_type = "string" + elseif (basic_elt_type <: Date + || basic_elt_type <: DateTime + || basic_elt_type <: ZonedDateTime) + typescript_elt_type = "Date" + elseif basic_elt_type <: Number + typescript_elt_type = "number" + end + catch e + typescript_elt_type = julia_elt_type + end + return typescript_elt_type +end + +""" + get_typescript_elt_type(_field::Dict) + +Eg. Vector{Model.Patient} returns Patient +""" +function get_typescript_elt_type(_field::Dict) + chop(get_typescript_type(_field), tail = 2) # Remove the trailing '[]' +end + +""" + get_typescript_type(_field::Dict) + +Eg. Vector{Model.Patient} returns Patient[] +""" +function get_typescript_type(_field::Dict) + + _regexVector = r"Vector{([a-zA-Z0-9._]+)}" + + if (_m = match(_regexVector, _field[:field_type])) |> !isnothing + elt_type_name = _m |> + n -> string(n.captures[1]) + typescript_elt_type = get_typescript_type_of_elt_type(elt_type_name) + return "$typescript_elt_type[]" + else + return get_typescript_type_of_elt_type(_field[:field_type]) + end + +end + + + function generate_typescript_classes_from_object_model(object_model::Dict, + outdir::String, + relative_path_to_enum_dir::String) + + outdir = joinpath(outdir,"classes") + + modules = object_model[:modules] + structs = object_model[:structs] + fields = object_model[:fields] + + # Reset content + for _struct in structs + _struct[:struct_content] = "" + end + + # ################ # + # Open the struct # + # ################ # + for _struct in structs + str = "export class $(_struct[:name]) {\n\n" + _struct[:struct_content] *= str + end + + # ################## # + # Declare the fields # + # ################## # + for f in fields + + if !haskey(f,:struct) + @error f + return + end + + _struct = f[:struct] + + field_name = f[:name] + typescript_type = get_typescript_type(f) + str = " $field_name:$typescript_type;\n" + _struct[:struct_content] *= str + end + + # #################### # + # Open the constructor # + # #################### # + for _struct in structs + str = "\n" + str *= " constructor(_json:Object) {\n" + _struct[:struct_content] *= str + end + + # ####################################################### # + # Add the arguments declaration of the second constructor # + # ####################################################### # + for f in fields + + _struct = f[:struct] + + field_name = f[:name] + indent = repeat(" ", 8) + str = "" + if (f[:is_onetoone] || f[:is_manytoone]) + str *= indent * "if (_json['$field_name'] != null) {\n" + str *= indent * " " * "this.$field_name = new $(get_typescript_type_of_elt_type(f[:field_type]))(_json['$field_name']);\n" + str *= indent * "}\n" + elseif f[:is_onetomany] + elt_type = get_typescript_elt_type(f) + str *= indent * "if (_json['$field_name'] != null) {\n" + str *= indent * " " * "for (let e of _json['$field_name']) {\n" + str *= indent * " " * "this.$field_name.push(new $elt_type(e));\n" + str *= indent * " " * "}\n" + str *= indent * "}\n" + elseif f[:is_enum] + str *= indent * "if (_json['$field_name'] != null) {\n" + str *= indent * " " * "this.$field_name = Number($(get_typescript_type_of_elt_type(f[:field_type]))[_json['$field_name']]);\n" + str *= indent * "}\n" + elseif f[:is_vectorofenum] + elt_type = get_typescript_elt_type(f) + str *= indent * "if (_json['$field_name'] != null) {\n" + str *= indent * " " * "for (let e of _json['$field_name']) {\n" + str *= indent * " " * "this.$field_name.push(Number($elt_type[e]));\n" + str *= indent * " " * "}\n" + str *= indent * "}\n" + else + str *= indent * "this.$field_name = _json['$field_name'];\n" + end + _struct[:struct_content] *= str + end + + # ############################ # + # Close the constructor # + # ############################ # + for _struct in structs + str = " }\n" + _struct[:struct_content] *= str + end + + # ################################################# # + # Initialize the vector of imports for every struct # + # ################################################# # + for _struct in structs + _struct[:imports] = [] + end + + # ######################################################## # + # Add the imports for enums, vetors of enum, complex types # + # ######################################################## # + for f in fields + + _struct = f[:struct] + if (f[:is_onetoone] || f[:is_manytoone]) + elt_type = get_typescript_type_of_elt_type(f[:field_type]) + pathToImport = joinpath("./",elt_type) + push!( + _struct[:imports], + "import { $elt_type } from \"$pathToImport\"" + ) + elseif f[:is_onetomany] + elt_type = get_typescript_elt_type(f) + pathToImport = joinpath("./",elt_type) + push!( + _struct[:imports], + "import { $elt_type } from \"$pathToImport\"" + ) + elseif f[:is_enum] + elt_type = get_typescript_type_of_elt_type(f[:field_type]) + pathToImport = joinpath(relative_path_to_enum_dir,elt_type) + push!( + _struct[:imports], + "import { $elt_type } from \"$pathToImport\"" + ) + elseif f[:is_vectorofenum] + elt_type = get_typescript_elt_type(f) + pathToImport = joinpath(relative_path_to_enum_dir,elt_type) + push!( + _struct[:imports], + "import { $elt_type } from \"$pathToImport\"" + ) + end + + end + # Add the string of imports + for _struct in structs + if length(_struct[:imports]) > 0 + stringOfImports = join(unique(_struct[:imports]),";\n") * ";\n\n" + _struct[:struct_content] = stringOfImports * _struct[:struct_content] + end + end + + # ################ # + # Close the struct # + # ################ # + for _struct in structs + str = "\n} " + _struct[:struct_content] *= str + end + + # ############## # + # Write to files # + # ############## # + + # Empty the modules dirs + for _module in modules + module_dir = joinpath(outdir,_module[:name]) + rm(module_dir, recursive=true, force = true) + mkpath(module_dir) + end + + + # Write the content of structs to files + for _struct in structs + module_dir = joinpath(outdir,_struct[:module][:name]) + if !isdir(module_dir) + mkpath(module_dir) + end + file_path = joinpath(module_dir,"$(_struct[:name]).ts") + write(file_path,_struct[:struct_content]) + end + + end diff --git a/src/Tool/Tool.jl b/src/Tool/Tool.jl index 0076f30..c99048c 100644 --- a/src/Tool/Tool.jl +++ b/src/Tool/Tool.jl @@ -76,6 +76,26 @@ function build_field_name(str_arr::Vector{String}, return field_name end +function is_vector_of_enum(coltype::String, + elttype::String, + customtypes_names::Vector{String}) + if (coltype == "ARRAY") + if elttype[2:end] in customtypes_names # remove the leading underscore + return true + end + end + return false +end + +function is_vector_of_enum(coltype::String, + elttype::String, + customtypes::Dict) + customtypes_names = keys(customtypes) |> collect |> n -> string.(n) + return is_vector_of_enum(coltype, + elttype, + customtypes_names) +end + function get_fieldtype_from_coltype(coltype::String, elttype::String, customtypes::Dict, @@ -113,7 +133,7 @@ function get_fieldtype_from_coltype(coltype::String, elseif (coltype == "ARRAY") if (elttype == "_text" || elttype == "_varchar") attrtype = "Vector{String}" - elseif elttype[2:end] in customtypes_names + elseif is_vector_of_enum(coltype,elttype,customtypes_names) elttype = elttype[2:end] # remove the leading underscore attrtype = "Vector{$(build_enum_name_w_module(elttype))}" else @@ -230,6 +250,7 @@ function generate_object_model( end manytoone_field[:is_onetomany] = false manytoone_field[:is_enum] = false + manytoone_field[:is_vectorofenum] = false # Build a field name by one of the following options: # Case 1: The FK is composed of one column only. In this case we use @@ -313,6 +334,7 @@ function generate_object_model( id_field[:is_onetoone] = false id_field[:is_onetomany] = false id_field[:is_enum] = false + id_field[:is_vectorofenum] = false field_name = build_field_name(pkcol,lang_code) id_field[:name] = field_name @@ -345,6 +367,7 @@ function generate_object_model( basic_field[:is_onetoone] = false basic_field[:is_onetomany] = false basic_field[:is_enum] = false + basic_field[:is_vectorofenum] = false field_name = build_field_name(colname,lang_code) basic_field[:name] = field_name @@ -354,9 +377,13 @@ function generate_object_model( custom_types ;tablename = table, colname = colname) - # Check if it is an enum + # Check if it is an enum or a vector of enum if coldef[:type] == "USER-DEFINED" basic_field[:is_enum] = true + elseif is_vector_of_enum(coldef[:type], + coldef[:elttype_if_array], + custom_types) + basic_field[:is_vectorofenum] = true end basic_field[:field_type] = field_type @@ -385,6 +412,7 @@ function generate_object_model( onetomany_field[:is_onetoone] = false onetomany_field[:is_onetomany] = true onetomany_field[:is_enum] = false + onetomany_field[:is_vectorofenum] = false onetomany_type_name_w_module = manytoone_field[:field_type] # Public.Staff # Build a field_name using one of the following options: @@ -520,10 +548,6 @@ function generate_structs_from_object_model(object_model::Dict, outdir::String) end str = " $field_name::Union{Missing,$field_type}\n" - if f[:name] == "policeOfficerRequests" - @info "$(f[:name]) -> $str" - end - _struct[:struct_content] *= str end @@ -865,158 +889,3 @@ get_table_name() = \"$table\" return object_model end - -function generate_julia_struct_from_table( - dbconn::LibPQ.Connection, - schema_name::String, - table_name::String, - struct_name::String, - filename_for_struct::String, - filename_for_orm_module::String, - ;ignored_columns::Vector{String} = Vector{String}(), - camelcase_is_default::Bool = true, - exceptions_to_default::Vector{String} = Vector{String}()) - - @info "generate_julia_struct_from_table" - - # Retrieve the columns names and types - - query_string = "SELECT column_name, - data_type AS column_type, - udt_name AS element_type - from information_schema.columns - WHERE table_schema = \$1 AND table_name = \$2" - - cols = execute_plain_query(query_string, - [schema_name,table_name], - dbconn) - - # For convenience, we make sure that 'colnames_to_camelcase' and - # 'colnames_left_as_it_is' are vectors - if camelcase_is_default - colnames_to_camelcase = cols[:,:column_name] - filter!(x -> !(x in exceptions_to_default),colnames_to_camelcase) - else - colnames_to_camelcase = exceptions_to_default - end - - mapping_arr = [] - attrs_declarations = [] - attrsnames = [] - colsnames = [] - - # Declare the indentation unit - indent = " " - - for c in eachrow(cols) - - colname = c.column_name - coltype = c.column_type - elttype = c.element_type - attrname = colname - - # Skip ignored columns - if (colname in ignored_columns) - continue - end - - if (colname in colnames_to_camelcase) - attrname = StringCases.camelize(colname) - end - - attrtype = Union{Missing,String} - push!(colsnames,colname) - push!(attrsnames,attrname) - push!(mapping_arr,":$attrname => \"$colname\"") - - # Deduce the corresponding Julia type - if (coltype == "character" - || coltype == "character varying" - || coltype == "text" - || coltype == "uuid" - || coltype == "USER-DEFINED") - attrtype = "String" - elseif (coltype == "boolean") - attrtype = "Bool" - elseif (coltype == "smallint") - attrtype = "Int16" - elseif (coltype == "integer" || coltype == "interval") - attrtype = "Int32" - elseif (coltype == "bigint") - attrtype = "Int64" - elseif (coltype == "numeric") - attrtype = "Float64" - elseif (coltype == "date") - attrtype = "Date" - elseif (coltype == "time without time zone") - attrtype = "Time" - elseif (coltype == "timestamp without time zone") - attrtype = "DateTime" - elseif (coltype == "timestamp with time zone") - attrtype = "ZonedDateTime" - elseif (coltype == "ARRAY") - if (elttype == "_text" || elttype == "_varchar") - attrtype = "Vector{String}" - else - error("Unknown array type[$elttype] for column[$colname]") - end - else - error("Unknown type[$coltype] for column[$colname]") - end - - # Declare the attribute - push!(attrs_declarations, - "$attrname::Union{Missing,$attrtype}") - - end # ENDOF for-loop on columns - - # - # Prepare the constructors - # - constructor1 = "$struct_name(args::NamedTuple) = $struct_name(;args...)" - constructor2 = "$struct_name(;\n" - constructor2 *= join(string.(repeat(indent,2),attrsnames," = missing,\n")) - constructor2 *= " ) = (\n" - - constructor2 *= "$(repeat(indent,3))x = new(" * join(repeat(["missing"],length(attrsnames)),", ") * ");\n" - constructor2 *= - join(string.(repeat(indent,3),"x.",attrsnames," = " , attrsnames),"; \n") * ";" - - constructor2 *= "\n$(repeat(indent,3))return x" - constructor2 *= "\n$(repeat(indent,3)))" - - # - # Prepare the mapping - # - mapping = "" - - # - # Build the strings - # - struct_content = (" -abstract type I$struct_name <: IEntity end \n -mutable struct $struct_name <: I$struct_name - - $(join(attrs_declarations,"\n ")) - - $constructor1 - $constructor2 - -end -") - - orm_content = (" -data_type = $struct_name -PostgresORM.get_orm(x::$struct_name) = return($(struct_name)ORM) -gettablename() = \"$schema_name.$table_name\" -const columns_selection_and_mapping = - Dict( - $(join(mapping_arr,",\n$(repeat(indent,1))")) - ) -") - - write(filename_for_struct,struct_content) - write(filename_for_orm_module,orm_content) - - cols -end diff --git a/test/Tool/runtests-Tool.jl b/test/Tool/runtests-Tool.jl new file mode 100644 index 0000000..970c694 --- /dev/null +++ b/test/Tool/runtests-Tool.jl @@ -0,0 +1,26 @@ +include("../runtests-prerequisite.jl") +using Serialization + +@testset "Test Tool.get_typescript_type(_field::Dict)`" begin + object_model = Serialization.deserialize("test/assets/object_model.jld") + object_model = Serialization.deserialize("/home/vlaugier/CODE/forensic/medilegist/Medilegist.jl/dev/PostgresORM/test/assets/object_model.jld") + + onetomany_field = filter(x-> x[:is_onetomany], object_model[:fields])[1] + PostgresORM.Tool.get_typescript_type(onetomany_field) + + enum_field = filter(x-> x[:is_enum], object_model[:fields])[1] + PostgresORM.Tool.get_typescript_type(enum_field) + + + byte_field = filter(x-> x[:field_type] == "Vector{UInt8}", object_model[:fields])[1] + PostgresORM.Tool.get_typescript_type(byte_field) + +end + +@testset "Test Tool.get_typescript_type_of_elt_type(julia_elt_type::String)`" begin + PostgresORM.Tool.get_typescript_type_of_elt_type("Bool") == "boolean" + PostgresORM.Tool.get_typescript_type_of_elt_type("Model.Mammal") == "Mammal" + PostgresORM.Tool.get_typescript_type_of_elt_type("Model.Exam") == "Exam" +end + +4 From db51acac73891527fc3455856510087b6eb310e6 Mon Sep 17 00:00:00 2001 From: Vincent Laugier Date: Sun, 12 Dec 2021 15:54:11 +0100 Subject: [PATCH 2/4] Make the constructors generated by the reverse engineering more standard --- Project.toml | 2 +- src/Tool/Tool.jl | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Project.toml b/Project.toml index 6ff0a48..46b8f0d 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "PostgresORM" uuid = "748b5efa-ed57-4836-b183-a38105a77fdd" authors = ["Vincent Laugier "] -version = "0.1.7" +version = "0.2.0" [deps] DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" diff --git a/src/Tool/Tool.jl b/src/Tool/Tool.jl index c99048c..3b34a3c 100644 --- a/src/Tool/Tool.jl +++ b/src/Tool/Tool.jl @@ -586,7 +586,7 @@ function generate_structs_from_object_model(object_model::Dict, outdir::String) # ######################################################### # for _struct in structs str = "" - str *= " ) = (\n" + str *= " ) = begin\n" str *= " x = new(" _struct[:struct_content] *= str end @@ -606,7 +606,7 @@ function generate_structs_from_object_model(object_model::Dict, outdir::String) # Close 'new(missing, missing, ...)' of the second constructor # # ############################################################ # for _struct in structs - str = ");\n" + str = ")\n" _struct[:struct_content] *= str end @@ -617,7 +617,7 @@ function generate_structs_from_object_model(object_model::Dict, outdir::String) _struct = f[:struct] - str = " x.$(f[:name]) = $(f[:name]);\n" + str = " x.$(f[:name]) = $(f[:name])\n" _struct[:struct_content] *= str end @@ -626,7 +626,7 @@ function generate_structs_from_object_model(object_model::Dict, outdir::String) # ######################################################## # for _struct in structs str = " return x\n" - str *= " )\n" + str *= " end\n" _struct[:struct_content] *= str end From 4c0e9089c81dc2d156fa91d2656fa617802cee2f Mon Sep 17 00:00:00 2001 From: Vincent Laugier Date: Fri, 25 Feb 2022 11:28:15 +0100 Subject: [PATCH 3/4] Support for array for floats and array of integers --- src/Tool/Tool.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Tool/Tool.jl b/src/Tool/Tool.jl index 3b34a3c..88f74f4 100644 --- a/src/Tool/Tool.jl +++ b/src/Tool/Tool.jl @@ -133,6 +133,10 @@ function get_fieldtype_from_coltype(coltype::String, elseif (coltype == "ARRAY") if (elttype == "_text" || elttype == "_varchar") attrtype = "Vector{String}" + elseif (elttype == "_numeric") + attrtype = "Vector{Float64}" + elseif (elttype == "_int4") + attrtype = "Vector{Int64}" elseif is_vector_of_enum(coltype,elttype,customtypes_names) elttype = elttype[2:end] # remove the leading underscore attrtype = "Vector{$(build_enum_name_w_module(elttype))}" From 6ebac90b6e03325ed6ef34f43094ca1da1f14d09 Mon Sep 17 00:00:00 2001 From: Vincent Laugier Date: Fri, 25 Feb 2022 11:29:17 +0100 Subject: [PATCH 4/4] Bump version --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 46b8f0d..93e53a0 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "PostgresORM" uuid = "748b5efa-ed57-4836-b183-a38105a77fdd" authors = ["Vincent Laugier "] -version = "0.2.0" +version = "0.3.0" [deps] DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"