barkest_core 1.5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (308) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/Gemfile +22 -0
  4. data/Gemfile.lock +254 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.md +364 -0
  7. data/Rakefile +37 -0
  8. data/app/assets/fonts/barkest_core/ArchivoNarrow-Bold.ttf +0 -0
  9. data/app/assets/fonts/barkest_core/ArchivoNarrow-BoldItalic.ttf +0 -0
  10. data/app/assets/fonts/barkest_core/ArchivoNarrow-Italic.ttf +0 -0
  11. data/app/assets/fonts/barkest_core/ArchivoNarrow-Regular.ttf +0 -0
  12. data/app/assets/images/barkest_core/.keep +0 -0
  13. data/app/assets/images/barkest_core/barcode-B.svg +181 -0
  14. data/app/assets/javascripts/barkest_core/.keep +0 -0
  15. data/app/assets/javascripts/barkest_core/application.js +22 -0
  16. data/app/assets/javascripts/barkest_core/bootstrap-datepicker.js +1800 -0
  17. data/app/assets/javascripts/barkest_core/field_init.js +7 -0
  18. data/app/assets/javascripts/barkest_core/jquery.doubleScroll.js +112 -0
  19. data/app/assets/javascripts/barkest_core/masked_edit.js +25 -0
  20. data/app/assets/javascripts/barkest_core/system_status.js.erb +201 -0
  21. data/app/assets/stylesheets/barkest_core/.keep +0 -0
  22. data/app/assets/stylesheets/barkest_core/application.css +17 -0
  23. data/app/assets/stylesheets/barkest_core/custom.css.scss +264 -0
  24. data/app/assets/stylesheets/barkest_core/datepicker3.css +790 -0
  25. data/app/controllers/.keep +0 -0
  26. data/app/controllers/access_groups_controller.rb +74 -0
  27. data/app/controllers/account_activations_controller.rb +29 -0
  28. data/app/controllers/application_controller.rb +5 -0
  29. data/app/controllers/barkest_core/application_controller_base.rb +113 -0
  30. data/app/controllers/barkest_core/engine_controller_base.rb +15 -0
  31. data/app/controllers/barkest_core/testsub_controller.rb +21 -0
  32. data/app/controllers/contact_controller.rb +32 -0
  33. data/app/controllers/log_view_controller.rb +31 -0
  34. data/app/controllers/password_resets_controller.rb +126 -0
  35. data/app/controllers/sessions_controller.rb +64 -0
  36. data/app/controllers/status_controller.rb +150 -0
  37. data/app/controllers/system_config_controller.rb +238 -0
  38. data/app/controllers/system_update_controller.rb +164 -0
  39. data/app/controllers/test_access_controller.rb +44 -0
  40. data/app/controllers/test_report_controller.rb +75 -0
  41. data/app/controllers/users_controller.rb +218 -0
  42. data/app/helpers/.keep +0 -0
  43. data/app/helpers/barkest_core/application_helper.rb +134 -0
  44. data/app/helpers/barkest_core/form_helper.rb +469 -0
  45. data/app/helpers/barkest_core/html_helper.rb +70 -0
  46. data/app/helpers/barkest_core/misc_helper.rb +68 -0
  47. data/app/helpers/barkest_core/pdf_helper.rb +180 -0
  48. data/app/helpers/barkest_core/recaptcha_helper.rb +115 -0
  49. data/app/helpers/barkest_core/sessions_helper.rb +94 -0
  50. data/app/helpers/barkest_core/status_helper.rb +118 -0
  51. data/app/helpers/barkest_core/users_helper.rb +32 -0
  52. data/app/mailers/.keep +0 -0
  53. data/app/mailers/application_mailer.rb +5 -0
  54. data/app/mailers/barkest_core/application_mailer_base.rb +30 -0
  55. data/app/mailers/barkest_core/contact_form.rb +20 -0
  56. data/app/mailers/barkest_core/user_mailer.rb +44 -0
  57. data/app/models/.keep +0 -0
  58. data/app/models/access_group.rb +121 -0
  59. data/app/models/access_group_group_member.rb +13 -0
  60. data/app/models/access_group_user_member.rb +11 -0
  61. data/app/models/barkest_core/auth_config.rb +95 -0
  62. data/app/models/barkest_core/authorize_failure.rb +7 -0
  63. data/app/models/barkest_core/contact_message.rb +37 -0
  64. data/app/models/barkest_core/database_config.rb +223 -0
  65. data/app/models/barkest_core/db_table.rb +21 -0
  66. data/app/models/barkest_core/email_config.rb +132 -0
  67. data/app/models/barkest_core/global_status.rb +267 -0
  68. data/app/models/barkest_core/log_entry.rb +101 -0
  69. data/app/models/barkest_core/log_view_options.rb +51 -0
  70. data/app/models/barkest_core/ms_sql_db_definition.rb +441 -0
  71. data/app/models/barkest_core/ms_sql_definition.rb +221 -0
  72. data/app/models/barkest_core/ms_sql_function.rb +423 -0
  73. data/app/models/barkest_core/not_logged_in.rb +7 -0
  74. data/app/models/barkest_core/pdf_table_builder.rb +407 -0
  75. data/app/models/barkest_core/self_update_config.rb +37 -0
  76. data/app/models/barkest_core/user_alert.rb +29 -0
  77. data/app/models/barkest_core/user_alert_generators.rb +58 -0
  78. data/app/models/barkest_core/user_manager.rb +404 -0
  79. data/app/models/barkest_core/work_path.rb +74 -0
  80. data/app/models/disable_user.rb +18 -0
  81. data/app/models/ldap_access_group.rb +15 -0
  82. data/app/models/system_config.rb +99 -0
  83. data/app/models/user.rb +405 -0
  84. data/app/models/user_login_history.rb +11 -0
  85. data/app/views/.keep +0 -0
  86. data/app/views/access_groups/_form.html.erb +19 -0
  87. data/app/views/access_groups/edit.html.erb +2 -0
  88. data/app/views/access_groups/index.html.erb +32 -0
  89. data/app/views/access_groups/new.html.erb +2 -0
  90. data/app/views/access_groups/show.html.erb +4 -0
  91. data/app/views/barkest_core/contact_form/contact.html.erb +16 -0
  92. data/app/views/barkest_core/contact_form/contact.text.erb +13 -0
  93. data/app/views/barkest_core/testsub/_links.html.erb +5 -0
  94. data/app/views/barkest_core/testsub/page1.html.erb +3 -0
  95. data/app/views/barkest_core/testsub/page2.html.erb +2 -0
  96. data/app/views/barkest_core/testsub/page3.html.erb +2 -0
  97. data/app/views/barkest_core/user_mailer/account_activation.html.erb +7 -0
  98. data/app/views/barkest_core/user_mailer/account_activation.text.erb +6 -0
  99. data/app/views/barkest_core/user_mailer/invalid_password_reset.html.erb +3 -0
  100. data/app/views/barkest_core/user_mailer/invalid_password_reset.text.erb +5 -0
  101. data/app/views/barkest_core/user_mailer/password_reset.html.erb +8 -0
  102. data/app/views/barkest_core/user_mailer/password_reset.text.erb +7 -0
  103. data/app/views/contact/index.html.erb +24 -0
  104. data/app/views/layouts/_footer_copyright.html.erb +1 -0
  105. data/app/views/layouts/_menu_admin.html.erb +5 -0
  106. data/app/views/layouts/_menu_anon.html.erb +0 -0
  107. data/app/views/layouts/_menu_auth.html.erb +3 -0
  108. data/app/views/layouts/_menu_footer.html.erb +1 -0
  109. data/app/views/layouts/_nav_logo.html.erb +1 -0
  110. data/app/views/layouts/application.html.erb +2 -0
  111. data/app/views/layouts/barkest_core/_application.html.erb +24 -0
  112. data/app/views/layouts/barkest_core/_footer.html.erb +18 -0
  113. data/app/views/layouts/barkest_core/_header.html.erb +38 -0
  114. data/app/views/layouts/barkest_core/_html_mailer.html.erb +11 -0
  115. data/app/views/layouts/barkest_core/_menu_account.html.erb +14 -0
  116. data/app/views/layouts/barkest_core/_menu_sample.html.erb +1 -0
  117. data/app/views/layouts/barkest_core/_messages.html.erb +4 -0
  118. data/app/views/layouts/barkest_core/_shim.html.erb +4 -0
  119. data/app/views/layouts/barkest_core/_subheader.html.erb +1 -0
  120. data/app/views/layouts/barkest_core/_text_mailer.text.erb +4 -0
  121. data/app/views/layouts/mailer.html.erb +1 -0
  122. data/app/views/layouts/mailer.text.erb +1 -0
  123. data/app/views/log_view/index.html.erb +100 -0
  124. data/app/views/password_resets/edit.html.erb +20 -0
  125. data/app/views/password_resets/new.html.erb +14 -0
  126. data/app/views/sessions/new.html.erb +27 -0
  127. data/app/views/shared/_error_messages.html.erb +29 -0
  128. data/app/views/shared/_generic_user_alert.html.erb +4 -0
  129. data/app/views/status/current.html.erb +34 -0
  130. data/app/views/status/test.html.erb +50 -0
  131. data/app/views/system_config/index.html.erb +25 -0
  132. data/app/views/system_config/show_auth.html.erb +28 -0
  133. data/app/views/system_config/show_database.html.erb +36 -0
  134. data/app/views/system_config/show_email.html.erb +21 -0
  135. data/app/views/system_config/show_self_update.html.erb +13 -0
  136. data/app/views/system_update/index.html.erb +31 -0
  137. data/app/views/system_update/new.html.erb +2 -0
  138. data/app/views/test_access/allow_anon.html.erb +2 -0
  139. data/app/views/test_access/require_admin.html.erb +2 -0
  140. data/app/views/test_access/require_group_x.html.erb +2 -0
  141. data/app/views/test_access/require_user.html.erb +2 -0
  142. data/app/views/test_report/index.csv.csvrb +23 -0
  143. data/app/views/test_report/index.html.erb +6 -0
  144. data/app/views/test_report/index.pdf.prawn +50 -0
  145. data/app/views/test_report/index.xlsx.axlsx +28 -0
  146. data/app/views/users/_user.html.erb +57 -0
  147. data/app/views/users/_user_details.html.erb +15 -0
  148. data/app/views/users/_user_details_for_list.html.erb +1 -0
  149. data/app/views/users/_user_form.html.erb +13 -0
  150. data/app/views/users/disable_confirm.html.erb +19 -0
  151. data/app/views/users/edit.html.erb +15 -0
  152. data/app/views/users/index.html.erb +9 -0
  153. data/app/views/users/new.html.erb +10 -0
  154. data/app/views/users/show.html.erb +46 -0
  155. data/bin/rails +12 -0
  156. data/config/routes.rb +3 -0
  157. data/db/migrate/20160617172539_create_access_groups.rb +10 -0
  158. data/db/migrate/20160617172725_create_users.rb +26 -0
  159. data/db/migrate/20160617172833_create_user_login_histories.rb +12 -0
  160. data/db/migrate/20160622151720_create_access_group_user_members.rb +9 -0
  161. data/db/migrate/20160622151925_create_access_group_group_members.rb +9 -0
  162. data/db/migrate/20160701005706_create_ldap_access_groups.rb +11 -0
  163. data/db/migrate/20161108155029_create_system_configs.rb +11 -0
  164. data/db/seeds/barkest_core_01_create_users.rb +42 -0
  165. data/db/seeds.rb +53 -0
  166. data/lib/barkest_core/concerns/association_with_defaults.rb +55 -0
  167. data/lib/barkest_core/concerns/boolean_parser.rb +88 -0
  168. data/lib/barkest_core/concerns/date_parser.rb +181 -0
  169. data/lib/barkest_core/concerns/email_tester.rb +55 -0
  170. data/lib/barkest_core/concerns/encrypted_fields.rb +156 -0
  171. data/lib/barkest_core/concerns/named_model.rb +73 -0
  172. data/lib/barkest_core/concerns/number_parser.rb +145 -0
  173. data/lib/barkest_core/concerns/utc_conversion.rb +60 -0
  174. data/lib/barkest_core/engine.rb +105 -0
  175. data/lib/barkest_core/extensions/active_record_extensions.rb +120 -0
  176. data/lib/barkest_core/extensions/application_configuration_extensions.rb +38 -0
  177. data/lib/barkest_core/extensions/application_extensions.rb +50 -0
  178. data/lib/barkest_core/extensions/axlsx_extenstions.rb +157 -0
  179. data/lib/barkest_core/extensions/fixture_set_extensions.rb +107 -0
  180. data/lib/barkest_core/extensions/generator_extensions.rb +271 -0
  181. data/lib/barkest_core/extensions/main_app_extensions.rb +35 -0
  182. data/lib/barkest_core/extensions/prawn_document_extensions.rb +367 -0
  183. data/lib/barkest_core/extensions/prawn_table_extensions.rb +131 -0
  184. data/lib/barkest_core/extensions/router_extensions.rb +106 -0
  185. data/lib/barkest_core/extensions/simple_formatter_extensions.rb +66 -0
  186. data/lib/barkest_core/extensions/test_case_extensions.rb +348 -0
  187. data/lib/barkest_core/extensions/time_extensions.rb +164 -0
  188. data/lib/barkest_core/handlers/csv_handler.rb +30 -0
  189. data/lib/barkest_core/version.rb +3 -0
  190. data/lib/barkest_core.rb +324 -0
  191. data/lib/generators/barkest/install_generator.rb +102 -0
  192. data/lib/generators/barkest_core/actions/01_patch_application_controller.rb +55 -0
  193. data/lib/generators/barkest_core/actions/02_patch_application_mailer.rb +56 -0
  194. data/lib/generators/barkest_core/actions/03_patch_assets.rb +62 -0
  195. data/lib/generators/barkest_core/actions/04_patch_layouts.rb +36 -0
  196. data/lib/generators/barkest_core/actions/05_patch_routes.rb +93 -0
  197. data/lib/generators/barkest_core/actions/06_patch_seeds.rb +60 -0
  198. data/lib/generators/barkest_core/actions/07_copy_migrations.rb +51 -0
  199. data/lib/generators/barkest_core/actions/08_configure_database.rb +52 -0
  200. data/lib/generators/barkest_core/actions/09_configure_secrets.rb +29 -0
  201. data/lib/generators/barkest_core/actions/99_patch_gitignore.rb +57 -0
  202. data/lib/generators/barkest_core/install_generator.rb +17 -0
  203. data/test/barkest_core_test.rb +83 -0
  204. data/test/controllers/access_groups_controller_test.rb +53 -0
  205. data/test/controllers/contact_controller_test.rb +10 -0
  206. data/test/controllers/sessions_controller_test.rb +10 -0
  207. data/test/controllers/users_controller_test.rb +10 -0
  208. data/test/dummy/.gitignore +10 -0
  209. data/test/dummy/README.rdoc +28 -0
  210. data/test/dummy/Rakefile +6 -0
  211. data/test/dummy/app/assets/images/.keep +0 -0
  212. data/test/dummy/app/assets/javascripts/application.js +14 -0
  213. data/test/dummy/app/assets/stylesheets/application.css +16 -0
  214. data/test/dummy/app/controllers/application_controller.rb +5 -0
  215. data/test/dummy/app/controllers/concerns/.keep +0 -0
  216. data/test/dummy/app/helpers/application_helper.rb +2 -0
  217. data/test/dummy/app/mailers/.keep +0 -0
  218. data/test/dummy/app/mailers/application_mailer.rb +3 -0
  219. data/test/dummy/app/models/.keep +0 -0
  220. data/test/dummy/app/models/concerns/.keep +0 -0
  221. data/test/dummy/app/views/layouts/application.html.erb +1 -0
  222. data/test/dummy/app/views/layouts/mailer.html.erb +1 -0
  223. data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  224. data/test/dummy/app/views/system_config/show_fake.html.erb +3 -0
  225. data/test/dummy/bin/bundle +3 -0
  226. data/test/dummy/bin/rails +4 -0
  227. data/test/dummy/bin/rake +4 -0
  228. data/test/dummy/bin/setup +29 -0
  229. data/test/dummy/config/application.rb +27 -0
  230. data/test/dummy/config/boot.rb +5 -0
  231. data/test/dummy/config/environment.rb +5 -0
  232. data/test/dummy/config/environments/development.rb +47 -0
  233. data/test/dummy/config/environments/production.rb +79 -0
  234. data/test/dummy/config/environments/test.rb +44 -0
  235. data/test/dummy/config/initializers/assets.rb +11 -0
  236. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  237. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  238. data/test/dummy/config/initializers/db_updater_ext.rb +33 -0
  239. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  240. data/test/dummy/config/initializers/inflections.rb +16 -0
  241. data/test/dummy/config/initializers/mime_types.rb +4 -0
  242. data/test/dummy/config/initializers/session_store.rb +3 -0
  243. data/test/dummy/config/initializers/sys_config_ext.rb +12 -0
  244. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  245. data/test/dummy/config/locales/en.yml +23 -0
  246. data/test/dummy/config/routes.rb +60 -0
  247. data/test/dummy/config.ru +4 -0
  248. data/test/dummy/db/schema.rb +95 -0
  249. data/test/dummy/db/seeds/barkest_core_01_create_users.rb +42 -0
  250. data/test/dummy/db/seeds.rb +51 -0
  251. data/test/dummy/lib/assets/.keep +0 -0
  252. data/test/dummy/log/.keep +0 -0
  253. data/test/dummy/public/404.html +67 -0
  254. data/test/dummy/public/422.html +67 -0
  255. data/test/dummy/public/500.html +66 -0
  256. data/test/dummy/public/favicon.ico +0 -0
  257. data/test/dummy/sql/my_test_view.sql +3 -0
  258. data/test/fixtures/access_groups.yml +21 -0
  259. data/test/fixtures/users.yml +71 -0
  260. data/test/helpers/barkest_core/sessions_helper_test.rb +22 -0
  261. data/test/integration/access_group_mgmt_test.rb +33 -0
  262. data/test/integration/access_test.rb +24 -0
  263. data/test/integration/account_activations_access_test.rb +12 -0
  264. data/test/integration/contact_test.rb +98 -0
  265. data/test/integration/extra_partial_test.rb +41 -0
  266. data/test/integration/log_view_access_test.rb +12 -0
  267. data/test/integration/password_resets_test.rb +101 -0
  268. data/test/integration/reports_test.rb +53 -0
  269. data/test/integration/status_access_test.rb +27 -0
  270. data/test/integration/system_config_access_test.rb +24 -0
  271. data/test/integration/system_update_access_test.rb +19 -0
  272. data/test/integration/users_access_test.rb +34 -0
  273. data/test/integration/users_edit_test.rb +178 -0
  274. data/test/integration/users_index_test.rb +62 -0
  275. data/test/integration/users_login_test.rb +67 -0
  276. data/test/integration/users_signup_test.rb +54 -0
  277. data/test/mailers/.keep +0 -0
  278. data/test/mailers/barkest_core/contact_form_test.rb +28 -0
  279. data/test/mailers/barkest_core/user_mailer_test.rb +43 -0
  280. data/test/mailers/previews/barkest_core/contact_form_preview.rb +17 -0
  281. data/test/mailers/previews/barkest_core/user_mailer_preview.rb +26 -0
  282. data/test/models/access_group_group_member_test.rb +28 -0
  283. data/test/models/access_group_test.rb +114 -0
  284. data/test/models/access_group_user_member_test.rb +28 -0
  285. data/test/models/barkest_core/auth_config_test.rb +57 -0
  286. data/test/models/barkest_core/bool_parser_test.rb +28 -0
  287. data/test/models/barkest_core/contact_message_test.rb +61 -0
  288. data/test/models/barkest_core/database_config_test.rb +33 -0
  289. data/test/models/barkest_core/date_parser_test.rb +110 -0
  290. data/test/models/barkest_core/email_config_test.rb +57 -0
  291. data/test/models/barkest_core/global_status_test.rb +50 -0
  292. data/test/models/barkest_core/ms_sql_db_updater_test.rb +115 -0
  293. data/test/models/barkest_core/ms_sql_definition_test.rb +102 -0
  294. data/test/models/barkest_core/ms_sql_function_test.rb +131 -0
  295. data/test/models/barkest_core/number_parser_test.rb +29 -0
  296. data/test/models/barkest_core/self_update_config_test.rb +29 -0
  297. data/test/models/barkest_core/user_alert_test.rb +19 -0
  298. data/test/models/barkest_core/user_manager_test.rb +34 -0
  299. data/test/models/barkest_core/work_path_test.rb +26 -0
  300. data/test/models/disable_user_test.rb +27 -0
  301. data/test/models/generic_time_test.rb +66 -0
  302. data/test/models/ldap_access_group_test.rb +31 -0
  303. data/test/models/pdf_table_builder_test.rb +6 -0
  304. data/test/models/system_config_test.rb +78 -0
  305. data/test/models/user_login_history_test.rb +37 -0
  306. data/test/models/user_test.rb +130 -0
  307. data/test/test_helper.rb +63 -0
  308. metadata +798 -0
@@ -0,0 +1,221 @@
1
+ require 'zlib'
2
+
3
+ module BarkestCore
4
+
5
+ ##
6
+ # Contains a SQL definition to create a single table, view, function, or procedure.
7
+ #
8
+ # SQL source is not validated, however simple checks are made to ensure that only
9
+ # one DDL statement is present unless you are creating a procedure in which case this
10
+ # check is skipped.
11
+ #
12
+ # To reference another object in your definition, prefix @Z~ to the beginning of the
13
+ # object name. For instance 'SELECT * FROM @Z~my_table' could be expanded to
14
+ # 'SELECT * FROM zz_barkest_core_my_table'.
15
+ #
16
+ # Function return types are grabbed as well so you know if your function is returning
17
+ # a table or an integral type.
18
+ class MsSqlDefinition
19
+ InvalidDefinition = Class.new(StandardError)
20
+
21
+ EmptyDefinition = Class.new(InvalidDefinition)
22
+ MissingCreateStatement = Class.new(InvalidDefinition)
23
+ ExtraDDL = Class.new(InvalidDefinition)
24
+ UnmatchedBracket = Class.new(InvalidDefinition)
25
+ UnclosedQuote = Class.new(InvalidDefinition)
26
+ UnmatchedComment = Class.new(InvalidDefinition)
27
+ MissingReturnType = Class.new(InvalidDefinition)
28
+
29
+ attr_accessor :name_prefix
30
+
31
+ attr_reader :command, :name, :type, :definition, :version, :return_type, :source_location
32
+
33
+ def initialize(raw_sql, source = '', default_timestamp = 0)
34
+
35
+ @source_location = source.to_s
36
+ @return_type = :table # the default. functions can be different. procedures can be iffy since they may or may not return a result set.
37
+ @command = 'CREATE'
38
+
39
+ if default_timestamp.is_a?(String)
40
+ default_timestamp = Time.utc_parse(default_timestamp)
41
+ end
42
+
43
+ if default_timestamp.is_a?(Date)
44
+ default_timestamp = (default_timestamp.strftime('%Y%m%d') + '0000').to_i
45
+ elsif default_timestamp.is_a?(Time)
46
+ default_timestamp = "#{default_timestamp.year.to_s.rjust(4,'0')}#{default_timestamp.month.to_s.rjust(2,'0')}#{default_timestamp.day.to_s.rjust(2,'0')}#{default_timestamp.hour.to_s.rjust(2,'0')}#{default_timestamp.min.to_s.rjust(2,'0')}".to_i
47
+ end
48
+
49
+ default_timestamp = 0 unless default_timestamp.is_a?(Fixnum)
50
+
51
+ ts_regex = /^--\s*(?<YR>\d{4})-?(?<MON>\d{2})-?(?<DAY>\d{2})\s*(?:(?<HR>\d{2}):?(?<MIN>\d{2})?)?\s*$/
52
+
53
+ timestamp = nil
54
+
55
+ raw_sql = raw_sql.to_s.lstrip
56
+ # strip leading comment lines.
57
+ while raw_sql[0...2] == '--' || raw_sql[0...2] == '/*'
58
+ if raw_sql[0...2] == '--'
59
+ # trim off the first line.
60
+ first_line,_,raw_sql = raw_sql.partition("\n").map {|v| v.strip}
61
+ raw_sql ||= ''
62
+ unless timestamp
63
+ if (ts = ts_regex.match(first_line))
64
+ timestamp = "#{ts['YR']}#{ts['MON']}#{ts['DAY']}#{ts['HR'] || '00'}#{ts['MIN'] || '00'}".to_i
65
+ end
66
+ end
67
+ else
68
+ # find the first */ sequence in the string.
69
+ comment_end = raw_sql.index('*/')
70
+ raise UnmatchedComment, 'raw_sql starts with "/*" sequence with no matching "*/" sequence' unless comment_end
71
+
72
+ # find the last /* sequence before that.
73
+ comment_start = raw_sql.rindex('/*', comment_end)
74
+
75
+ # remove this comment
76
+ raw_sql = (raw_sql[0...comment_start].to_s + raw_sql[(comment_end + 2)..-1].to_s).lstrip
77
+ end
78
+ end
79
+
80
+ timestamp ||= default_timestamp
81
+
82
+ raise EmptyDefinition, 'raw_sql contains no data' if raw_sql.blank?
83
+
84
+ # first line should be CREATE VIEW|FUNCTION|PROCEDURE "XYZ"
85
+ # or ALTER TABLE "XYZ"
86
+ regex = /^(?:(?<CMD>ALTER)\s+(?<TYPE>TABLE)|(?<CMD>CREATE)\s+(?<TYPE>TABLE|VIEW|FUNCTION|PROCEDURE))\s+["\[]?(?<NAME>[A-Z][A-Z0-9_]*)["\]]?\s+(?<DEFINITION>.*)$/im
87
+ match = regex.match(raw_sql)
88
+
89
+ raise MissingCreateStatement, 'raw_sql must contain a "CREATE|ALTER VIEW|FUNCTION|PROCEDURE" statement' unless match
90
+
91
+ @command = match['CMD'].upcase
92
+ @type = match['TYPE'].upcase
93
+ @name = match['NAME']
94
+ @definition = match['DEFINITION'].strip
95
+
96
+ # we're going to test the definition loosely.
97
+ # so first we'll get rid of all valid literals and comments.
98
+ # this will leave behind mangled invalid SQL, but we can use it to determine if there are any simple issues.
99
+ # all removed components are replaced by single spaces to ensure that the remaining components are separate from
100
+ # each other.
101
+ test_sql = match['DEFINITION']
102
+ .gsub(/\s+/,' ') # condense whitespace
103
+ .split(/(?:(?:'[^']*')|(?:"[^"]*")|(?:\[[^\[\]]*\]))/m).join(' ') # remove all quoted literals '', "", and []
104
+ .split(/--[^\r\n]*/).join(' ') # remove all single-line comments
105
+
106
+ # remove all multi-line comments
107
+ # the regex will find matches for all of the innermost multi-line comments.
108
+ regex = /\/\*(?:(?:[^\/\*])|(?:\/[^\*]))*\*\//m
109
+
110
+ # so we go through removing them until there are no longer any matches.
111
+ while regex.match(test_sql)
112
+ test_sql = test_sql.split(regex).join(' ')
113
+ end
114
+
115
+ # now we can test for a number of missing items nice and easily.
116
+ raise UnmatchedBracket, 'raw_sql contains an opening bracket with no closing bracket' if test_sql.include?('[')
117
+ raise UnmatchedBracket, 'raw_sql contains a closing bracket with no opening bracket' if test_sql.include?(']')
118
+ raise UnclosedQuote, 'raw_sql contains an unclosed string literal' if test_sql.include?("'")
119
+ raise UnclosedQuote, 'raw_sql contains an unclosed ANSI quoted literal' if test_sql.include?('"')
120
+ raise UnmatchedComment, 'raw_sql contains a "/*" sequence with no matching "*/" sequence' if test_sql.include?('/*')
121
+ raise UnmatchedComment, 'raw_sql contains a "*/" sequence with no matching "/*" sequence' if test_sql.include?('*/')
122
+
123
+ unless type == 'PROCEDURE'
124
+ # and finally we can test for extra DDL.
125
+ # only the initial CREATE statement is allowed.
126
+ regex = /\s(create|alter|drop|grant)\s/im
127
+ if (match = regex.match(test_sql))
128
+ raise ExtraDDL, "raw_sql contains a #{match[1]} statement after the initial CREATE statement"
129
+ end
130
+ end
131
+
132
+ # and for functions, get the return type.
133
+ if type == 'FUNCTION'
134
+ regex = /\sRETURNS\s+(?:@[A-Z][A-Z0-9_]*\s+)?(?<TYPE>[A-Z][A-Z0-9_()]*)\s/im
135
+
136
+ match = regex.match(@definition)
137
+ raise MissingReturnType, 'raw_sql seems to be missing the RETURNS statement for the function.' unless match
138
+
139
+ rtype = match['TYPE'].downcase
140
+ rsize = 0
141
+ if rtype.include?('(')
142
+ rtype,_,rsize = rtype.partition('(')
143
+ rsize = rsize.partition(')')[0].to_i
144
+ end
145
+
146
+ @return_type =
147
+ case rtype
148
+ when 'bit'
149
+ :boolean
150
+
151
+ when 'int', 'integer', 'bigint', 'smallint', 'tinyint'
152
+ :integer
153
+
154
+ when 'decimal', 'numeric', 'money', 'smallmoney'
155
+ :decimal
156
+
157
+ when 'float', 'real'
158
+ :float
159
+
160
+ when 'date', 'datetime', 'datetime2', 'time', 'smalldatetime', 'datetimeoffset'
161
+ :time
162
+
163
+ when 'char', 'varchar', 'text', 'nchar', 'nvarchar', 'ntext', 'binary', 'varbinary', 'image'
164
+ :string
165
+
166
+ else
167
+ rtype.to_sym
168
+ end
169
+ end
170
+
171
+ # set the version.
172
+ @version = timestamp.to_s.ljust(12, '0') + Zlib.crc32(@definition).to_s(16).ljust(8,'0').upcase
173
+ end
174
+
175
+ def prefixed_name
176
+ prefix = name_prefix.to_s
177
+ return name if prefix == ''
178
+ return name if name.index(prefix) == 0
179
+ prefix + name
180
+ end
181
+
182
+ def is_create?
183
+ command == 'CREATE'
184
+ end
185
+
186
+ def update_sql
187
+ "#{command} #{type} \"#{prefixed_name}\"\n#{definition.gsub('@Z~',name_prefix)}"
188
+ end
189
+
190
+ def drop_sql
191
+ "DROP #{type} \"#{prefixed_name}\""
192
+ end
193
+
194
+ def grant_sql(user_name)
195
+ sel_exec = if type == 'PROCEDURE'
196
+ 'EXECUTE'
197
+ elsif type == 'FUNCTION' && return_type != :table
198
+ 'EXECUTE'
199
+ elsif type == 'TABLE'
200
+ 'SELECT, INSERT, UPDATE, DELETE'
201
+ else
202
+ 'SELECT'
203
+ end
204
+
205
+ "GRANT VIEW DEFINITION,#{sel_exec} ON \"#{prefixed_name}\" TO \"#{user_name}\""
206
+ end
207
+
208
+ def to_s
209
+ "#{type} #{name}"
210
+ end
211
+
212
+ def ==(other)
213
+ return false unless other.is_a?(MsSqlDefinition)
214
+ return false unless other.name == name
215
+ return false unless other.type == type
216
+ return false unless other.definition == definition
217
+ true
218
+ end
219
+
220
+ end
221
+ end
@@ -0,0 +1,423 @@
1
+ module BarkestCore
2
+
3
+ ##
4
+ # This class provides a model-like interface to SQL Server User Defined Functions.
5
+ #
6
+ # It's understandable that in terms of separation of concerns, logic has no place in the database.
7
+ # In the SQL Server world, UDFs cannot make changes to data, they can only present it.
8
+ # With that in mind, I consider UDFs to be parameterized queries, that are often times orders of magnitude
9
+ # faster than trying to construct a query via ActiveRecord.
10
+ #
11
+ # Although "models" inheriting from this class are not ActiveRecord models, this class does include
12
+ # ActiveModel::Model and ActiveModel::Validations to allow you to construct your UDF models with similar attributes
13
+ # to ActiveRecord models. For instance, you can ensure that returned values meet certain requirements using the
14
+ # validations. This allows you to further remove logic from the database and still gain the benefit of running
15
+ # a parameterized query.
16
+ class MsSqlFunction
17
+
18
+ InvalidConnection = Class.new(StandardError)
19
+
20
+ include ActiveModel::Model
21
+ include ActiveModel::Validations
22
+
23
+ include DateParser
24
+ include NumberParser
25
+ include BooleanParser
26
+
27
+ ##
28
+ # Sets the connection handler to use for this function.
29
+ #
30
+ # The default behavior is to piggyback on the ActiveRecord::Base connections.
31
+ # To override this behavior, provide a class that responds to the :connection method.
32
+ #
33
+ # class MyFunction < MsSqlFunction
34
+ # use_connection SomeMsSqlTable
35
+ # end
36
+ #
37
+ def self.use_connection(connected_object)
38
+ @conn_handler =
39
+ if connected_object.is_a?(Class)
40
+ connected_object
41
+ else
42
+ const_get(connected_object || 'ActiveRecord::Base')
43
+ end
44
+ raise ArgumentError.new('Connected object must be a class or class name.') unless @conn_handler
45
+ raise ArgumentError.new('Connected object must respond to :connection') unless @conn_handler.respond_to?(:connection)
46
+ end
47
+
48
+ ##
49
+ # Gets a connection from the connection handler.
50
+ #
51
+ # The connection must be to a SQL Server since this class has no idea how to work with UDFs in any other language at this time.
52
+ #
53
+ def self.connection
54
+ conn = connection_handler.connection
55
+ raise InvalidConnection unless conn.is_a?(ActiveRecord::ConnectionAdapters::SQLServerAdapter)
56
+ conn
57
+ end
58
+
59
+ ##
60
+ # Gets the UDF name for this class.
61
+ #
62
+ # It is important that you do not set this on the MsSqlFunction class itself.
63
+ #
64
+ def self.function_name
65
+ return '(none)' if self == MsSqlFunction
66
+ @udf ||= ''
67
+ end
68
+
69
+ ##
70
+ # Sets the UDF name for this class.
71
+ #
72
+ # It is important that you do not set this on the MsSqlFunction class itself.
73
+ #
74
+ def self.function_name=(value)
75
+ raise StandardError.new("Function name for #{self} cannot be set.") if self == MsSqlFunction
76
+ raise StandardError.new("Function name for #{self} cannot be set more than once.") unless function_name.blank?
77
+ @udf = process_udf(value)
78
+ end
79
+
80
+ ##
81
+ # Returns parameter information for the UDF.
82
+ #
83
+ # The returned hash contains the most important attributes for most applications including :type, :data_type, and :default.
84
+ #
85
+ def self.parameters
86
+ @param_info.inject({}) { |memo,(k,v)| memo[k] = { type: v[:type], data_type: v[:data_type], default: v[:default] }; memo }
87
+ end
88
+
89
+ ##
90
+ # Sets the default values for parameters.
91
+ #
92
+ # The +values+ should be a hash using the parameter name as the key and the default as the value.
93
+ # The easiest way to ensure it works is to set the defaults in the hash returned from #parameters.
94
+ #
95
+ def self.parameters=(values)
96
+ if values && values.is_a?(Hash)
97
+ values.each do |k,v|
98
+ if @param_info[k]
99
+ if v.is_a?(Hash)
100
+ @param_info[k][:default] = v[:default]
101
+ else
102
+ @param_info[k][:default] = v
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ ##
110
+ # Gets the column information for the UDF.
111
+ def self.columns
112
+ @column_info
113
+ end
114
+
115
+ ##
116
+ # Selects the data from the UDF using the provided parameters.
117
+ #
118
+ # MyFunction.select(user: 'john', day_of_week: 3)
119
+ #
120
+ # Returns an array containing the rows returned.
121
+ #
122
+ def self.select(params = {})
123
+
124
+ args = []
125
+
126
+ params = {} unless params.is_a?(Hash)
127
+
128
+ @param_info.each do |k,v|
129
+ args[v[:ordinal]] = [ v[:data_type], self.send(v[:format], params[k] || v[:default]) ]
130
+ end
131
+
132
+ where = ''
133
+ idx = args.count
134
+ params.each do |k,v|
135
+ unless @param_info.include? k
136
+ where += ' AND ' unless where.blank?
137
+ where += "([#{k}]"
138
+ if v.is_a? Array
139
+ # IN clause
140
+ where += ' IN (' + v.map{ |value| quote_param(value)[0] }.join(', ') + ')'
141
+ elsif v.is_a? Hash
142
+ if v.include? :between
143
+ v = v[:between]
144
+ raise ArgumentError.new("between clause for #{k} requires an array argument") unless v.is_a? Array
145
+ where += " BETWEEN @#{idx} AND @#{idx + 1}"
146
+ value,type = quote_param(v[0])
147
+ args[idx] = [ type, value ]
148
+ value,type = quote_param(v[1])
149
+ args[idx + 1] = [ type, value ]
150
+ idx += 2
151
+ elsif v.include? :like
152
+ where += " LIKE @#{idx}"
153
+ value,type = quote_param(v[:like].to_s)
154
+ args[idx] = [ type, value ]
155
+ idx += 1
156
+ else
157
+ operator = nil
158
+ value = nil
159
+ { not: '<>', lt: '<', lte: '<=', gt: '>', gte: '>=', eq: '=' }.each do |key,op|
160
+ if v.include? key
161
+ operator = op
162
+ value = v[key]
163
+ break
164
+ end
165
+ end
166
+ raise ArgumentError.new("unknown clause for #{k}") unless operator
167
+ where += " #{operator} @#{idx}"
168
+ value,type = quote_param(value)
169
+ args[idx] = [ type, value ]
170
+ idx += 1
171
+ end
172
+ else
173
+ where += " = @#{idx}"
174
+ value,type = quote_param(v)
175
+ args[idx] = [ type, value ]
176
+ idx += 1
177
+ end
178
+ where += ')'
179
+ end
180
+ end
181
+
182
+ sql = "SELECT * FROM #{@udf}(#{@udf_args})"
183
+ sql += " WHERE #{where}" unless where.blank?
184
+
185
+ ret = []
186
+
187
+ execute(sql, args) do |row|
188
+ ret << self.new(row)
189
+ end
190
+
191
+ ret
192
+ end
193
+
194
+
195
+ private
196
+
197
+ def self.quote_param(value)
198
+ if value.nil?
199
+ [ 'NULL', 'varchar(1)' ]
200
+ elsif value.is_a? Integer
201
+ [ value.to_s, 'integer' ]
202
+ elsif value.is_a? Float
203
+ [ value.to_s, 'float' ]
204
+ elsif value.is_a?(Date) || value.is_a?(Time)
205
+ [ value.strftime('%Y-%m-%d %H:%M:%S'), 'datetime' ]
206
+ elsif value.is_a? TrueClass
207
+ [ 1, 'bit' ]
208
+ elsif value.is_a? FalseClass
209
+ [ 0, 'bit' ]
210
+ else
211
+ [ "'#{value.to_s.gsub('\'','\'\'')}'", 'varchar(max)' ]
212
+ end
213
+ end
214
+
215
+ def self.instrumenter
216
+ @instrumenter ||= ActiveSupport::Notifications.instrumenter
217
+ end
218
+
219
+
220
+ def self.execute(sql, binds)
221
+ sql = "EXEC sp_executesql N'#{sql.gsub('\'','\'\'')}'"
222
+
223
+ unless binds.blank?
224
+ binds.each_with_index do |v,i|
225
+ sql += i == 0 ? ', N\'' : ', '
226
+ sql += "@#{i} #{v[0]}"
227
+ end
228
+ sql += '\''
229
+ binds.each_with_index do |v,i|
230
+ sql += ", @#{i}=#{v[1]}"
231
+ end
232
+ end
233
+
234
+ ret = []
235
+
236
+ conn = connection
237
+ instrumenter.instrument(
238
+ "sql.active_record",
239
+ :sql => sql,
240
+ :name => 'SQL',
241
+ :connection_id => conn.object_id,
242
+ :statement_name => nil,
243
+ :binds => nil) do
244
+ conn.instance_variable_get("@connection").execute(sql).each(as: :hash) do |row|
245
+ ret << row
246
+ yield row if block_given?
247
+ end
248
+ end
249
+
250
+ ret
251
+ end
252
+
253
+
254
+ def self.parse_for_string_filter(value)
255
+ value.nil? ? 'NULL' : "'#{value.to_s.gsub('\'','\'\'')}'"
256
+ end
257
+
258
+ def parse_for_string_filter(value)
259
+ self.class.parse_for_string_filter value
260
+ end
261
+
262
+ def self.connection_handler
263
+ @conn_handler ||= const_get('ActiveRecord::Base')
264
+ end
265
+
266
+ def self.get_udf_definition(name)
267
+ execute('SELECT R.ROUTINE_CATALOG AS [catalog], R.ROUTINE_SCHEMA AS [schema], R.ROUTINE_NAME AS [name], ' +
268
+ 'R.ROUTINE_DEFINITION AS [definition] FROM INFORMATION_SCHEMA.ROUTINES R WHERE ' +
269
+ 'R.ROUTINE_TYPE=\'FUNCTION\' AND R.DATA_TYPE=\'TABLE\' AND R.ROUTINE_NAME=@0',
270
+ [
271
+ ['varchar(100)', parse_for_string_filter(name)]
272
+ ]
273
+ ) do |row|
274
+ return [ row['catalog'], row['schema'], row['name'], row['definition'] ]
275
+ end
276
+ [ nil, nil, nil, nil ]
277
+ end
278
+
279
+
280
+ def self.get_udf_params(sql_def)
281
+ # get everything before the return definition
282
+ # should be something like "CREATE FUNCTION xyz (a type, b type)"
283
+ sql_def = sql_def.split(/\sreturn/i)[0]
284
+
285
+ ret = {}
286
+
287
+ if sql_def['(']
288
+ param_defs = sql_def.split('(', 2)[1].rpartition(')')[0].split(',').map{|d| d.strip}
289
+
290
+ param_defs.each_with_index do |raw,idx|
291
+ pname,pdatatype = raw.split(' ')
292
+
293
+ psym = pname
294
+ if psym[0] == '@'
295
+ psym = psym[1..-1]
296
+ end
297
+ psym = psym.underscore.to_sym
298
+
299
+ pdatatype.downcase!
300
+
301
+ if pdatatype.include? 'date'
302
+ pfmt = :parse_for_date_filter
303
+ ptype = :datetime
304
+ elsif pdatatype.include? 'float'
305
+ pfmt = :parse_for_float_filter
306
+ ptype = :float
307
+ elsif pdatatype.include? 'int'
308
+ pfmt = :parse_for_int_filter
309
+ ptype = :integer
310
+ elsif pdatatype.include? 'bit'
311
+ pfmt = :parse_for_boolean_filter
312
+ ptype = :boolean
313
+ else
314
+ pfmt = :parse_for_string_filter
315
+ ptype = :string
316
+ end
317
+
318
+ ret[psym] = { name: pname, data_type: pdatatype, type: ptype, format: pfmt, ordinal: idx }
319
+ end
320
+ end
321
+
322
+ ret
323
+ end
324
+
325
+
326
+ def self.get_udf_columns(catalog, schema, name)
327
+ execute('SELECT C.COLUMN_NAME AS [name], C.IS_NULLABLE AS [nullable], C.DATA_TYPE AS [type], ' +
328
+ 'C.CHARACTER_MAXIMUM_LENGTH AS [length], C.ORDINAL_POSITION AS [ordinal] ' +
329
+ 'FROM INFORMATION_SCHEMA.ROUTINE_COLUMNS C ' +
330
+ 'WHERE C.TABLE_CATALOG=@0 AND C.TABLE_SCHEMA=@1 AND C.TABLE_NAME=@2 ' +
331
+ 'ORDER BY C.ORDINAL_POSITION',
332
+ [
333
+ ['varchar(100)', parse_for_string_filter(catalog)],
334
+ ['varchar(100)', parse_for_string_filter(schema)],
335
+ ['varchar(100)', parse_for_string_filter(name)]
336
+ ]
337
+ ) do |row|
338
+ yield row if block_given?
339
+ end
340
+ end
341
+
342
+
343
+ def self.process_udf(name)
344
+ catalog, schema, name, sql_def = get_udf_definition(name)
345
+
346
+ raise StandardError.new("The specified function '#{name.to_s.gsub('\'','\'\'')}' could not be defined.") if sql_def.blank?
347
+
348
+ @param_info = get_udf_params(sql_def)
349
+
350
+ if @param_info.blank?
351
+ @udf_args = ''
352
+ else
353
+ @udf_args = @param_info.map.with_index { |v,i| "@#{i}" }.join(', ')
354
+ end
355
+
356
+ @column_info = [ ]
357
+
358
+ get_udf_columns(catalog, schema, name) do |column|
359
+ col_key = column['name'].underscore.to_sym
360
+ getter = col_key
361
+ setter = "#{col_key}="
362
+
363
+ col_info = { name: column['name'], key: col_key, ordinal: column['ordinal'], nullable: true, length: -1 }
364
+
365
+ type = column['type'].downcase
366
+
367
+ unless column['length'].blank?
368
+ type += "(#{column['length']})"
369
+ col_info[:length] = column['length'].to_i
370
+ end
371
+
372
+ col_info[:data_type] = type
373
+
374
+ attr_reader col_key
375
+
376
+ if type == 'int' || type == 'integer'
377
+ define_method setter do |value|
378
+ instance_variable_set "@#{col_key}", parse_for_int_column(value)
379
+ end
380
+ col_info[:type] = :integer
381
+ elsif type == 'float'
382
+ define_method setter do |value|
383
+ instance_variable_set "@#{col_key}", parse_for_float_column(value)
384
+ end
385
+ col_info[:type] = :float
386
+ elsif type == 'date' || (type == 'datetime' && col_key.to_s.include?('date'))
387
+ define_method setter do |value|
388
+ instance_variable_set "@#{col_key}", parse_for_date_column(value)
389
+ end
390
+ col_info[:type] = :datetime
391
+ elsif type == 'datetime'
392
+ define_method setter do |value|
393
+ instance_variable_set "@#{col_key}", parse_for_time_column(value)
394
+ end
395
+ col_info[:type] = :datetime
396
+ elsif type == 'bit'
397
+ define_method setter do |value|
398
+ instance_variable_set "@#{col_key}", parse_for_boolean_column(value)
399
+ end
400
+ define_method "#{getter}?" do
401
+ instance_variable_get "@#{col_key}"
402
+ end
403
+ col_info[:type] = :boolean
404
+ else
405
+ define_method setter do |value|
406
+ instance_variable_set "@#{col_key}", value.nil? ? nil : value.to_s
407
+ end
408
+ col_info[:type] = :string
409
+ end
410
+
411
+ if column['nullable'].upcase == 'NO'
412
+ validates col_key, presence: true
413
+ col_info[:nullable] = false
414
+ end
415
+
416
+ @column_info << col_info
417
+ end
418
+
419
+ "[#{schema}].[#{name}]"
420
+ end
421
+
422
+ end
423
+ end