mikras_utils 0.3.1 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c7d48444e75236d0188b539182a80aefeaea2770cdf7982188d7f055c8b552e
4
- data.tar.gz: 5d790361a1c314d7bda6013c1db9c904b50c81f3be39c8396d6c9e4da43e68bd
3
+ metadata.gz: bf10f02dfcf24bfe2f518edfda534abbb78047b6876610f6a09adfac5217f73c
4
+ data.tar.gz: 7801d2245d2cd810b4defcf94b2bace9f8a6587e3fbebccc49240a689b47763e
5
5
  SHA512:
6
- metadata.gz: 8e117ef1b45f0217ec14065d37405cf4b043259465c82a7df09caebc416a60ec15b80293c45c349ffb9cd0b9bd07fe8b9420b85e19f24a88a5dd62d3be62380f
7
- data.tar.gz: 285faf4c815810af7111d49176218401f86afc1568a915c721ee67787cbb11df2cd6c016bad9d7b33db61b6120d5726d65aa8e2f0d27fc4cfb06a5685f69de9f
6
+ metadata.gz: 565ee40963dc77d9759bd78a55dacddab5296f23c022a07ccce42fd8c890db65eddcb6ce015c909943a34483009233f53d2c75dd0fe05faa1ccca2c1831b5375
7
+ data.tar.gz: 8d8afe7d2bb415edd5c3a275bca441a0d4bf73299b97e5f2d8578c9efa0b37b62840679ba73eeb60b525dbf0254e1a8a3a893ba11c4ba0add75f89706219bf50
data/exe/mkacl CHANGED
@@ -1,13 +1,19 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  SPEC = %(
4
- @ Make RLS policies
4
+ @ Make RLS functions and policies
5
5
 
6
6
  -- [DATABASE [USERNAME]] SPEC-FILE
7
7
 
8
8
  Read SPEC and generate following functions, policies, and triggers
9
9
 
10
- SPEC FILE
10
+ FUNCTIONS
11
+ todo
12
+
13
+ TRIGGERS
14
+ todo
15
+
16
+ SPEC FILE
11
17
  The format is a YAML file where each root key represents a table. A table
12
18
  consists of insert, select, update, and delete actions that are arrays of
13
19
  ACL entries. An ACL entry list of associated roles and possibly a check SQL
@@ -25,9 +31,9 @@ SPEC = %(
25
31
  The following roles are recognized: RLA, LA, TA, KON, AKK, CLA, CTA, ELA,
26
32
  and ETA
27
33
 
28
- Sagsys also defines the ADM role that is excluded because it is now a RBAC
29
- fole, and PUP and NON that are excluded because they are not implemented in
30
- Mikras
34
+ Sagsys also defines the ADM role that is excluded because it is a RBAC role
35
+ in Mikras, and PUP and NON roles that are excluded because they are not
36
+ implemented in Mikras
31
37
 
32
38
  OPTIONS
33
39
  -i,interactive
@@ -101,7 +101,7 @@ module MkAcl
101
101
  case_roles = rule.roles.select { _1 == _1.upcase }
102
102
 
103
103
  if !auth_roles.empty?
104
- role_seq = conn.quote_value_seq(auth_roles)
104
+ role_seq = conn.quote_values(auth_roles)
105
105
  puts %(
106
106
  -- Find "#{action}" system role ACLs
107
107
  _acls := array[]::integer[];
@@ -114,7 +114,7 @@ module MkAcl
114
114
  end
115
115
 
116
116
  if !case_roles.empty?
117
- role_seq = conn.quote_value_seq(case_roles)
117
+ role_seq = conn.quote_values(case_roles)
118
118
  puts %(
119
119
  -- Find "#{action}" case role ACLs
120
120
  select _acls || array_agg(id)
@@ -151,8 +151,8 @@ module MkAcl
151
151
  # Insert a attach record per parent table. This snippet handles
152
152
  # multiple parents of a record but the check algorithm probably does
153
153
  # not TODO
154
- auth_role_list = conn.quote_value_seq table.insert.rules.map(&:auth_roles).flatten
155
- case_role_list = conn.quote_value_seq table.insert.rules.map(&:case_roles).flatten
154
+ auth_role_list = conn.quote_values table.insert.rules.map(&:auth_roles).flatten
155
+ case_role_list = conn.quote_values table.insert.rules.map(&:case_roles).flatten
156
156
  for parent in table.parents
157
157
  id_fields = conn.values %(select column_name from acl.links where table_name = '#{table}')
158
158
  for id_field in id_fields
@@ -17,6 +17,42 @@ module MkAcl
17
17
  @chains = {}
18
18
  end
19
19
 
20
+ # Generate a set of per-table functions that returns the associated
21
+ # case_id/event_id/vist_id for the given record. The Functions are
22
+ # generated for each table in the spec file (todo read from meta)
23
+ #
24
+ # app_portal functions:
25
+ #
26
+ # case_id_of_RECORD(object_id integer)
27
+ # event_id_of_RECORD(object_id integer)
28
+ # visit_id_of_RECORD(object_id integer)
29
+ #
30
+ # 'RECORD' is substituted with the record name of a table. Record names
31
+ # are the singular name of a table. TODO: Make it plural again
32
+ #
33
+ # acl_portal functions with a record argument (used in triggers):
34
+ #
35
+ # case_id_of_RECORD(r record)
36
+ # event_id_of_RECORD(r record)
37
+ # visit_id_of_RECORD(r record)
38
+ #
39
+ # Returns the case, event, or visit id associated with the given record.
40
+ # The record should be an ACL table record type. These functions are used
41
+ # in before insert triggers because it takes a record (eg. NEW) instead
42
+ # of an ID - this saves a lookup
43
+ #
44
+ # acl_portal general id-of functions:
45
+ #
46
+ # case_id_of(table varchar, id integer)
47
+ # event_id_of(table varchar, id integer)
48
+ # visit_id_of(table varchar, id integer)
49
+ #
50
+ # These methods are practically as efficient as using the more
51
+ # specialized versions above
52
+ #
53
+ # Returns null if not found. Note that the record has to exists because
54
+ # we access it to read the foreign keys
55
+ #
20
56
  # TODO: ACL checks so that a user can't get IDs of other users' records
21
57
  def generate
22
58
  # Find chains
@@ -30,7 +66,7 @@ module MkAcl
30
66
  meta.chains
31
67
  where src_schema_name = '#{app_schema}'
32
68
  and dst_schema_name = '#{app_schema}'
33
- and dst_table_name in #{conn.quote_value_list(DOMAIN_TABLES)}
69
+ and dst_table_name in (#{conn.quote_values(DOMAIN_TABLES)})
34
70
  )
35
71
 
36
72
  generate_per_table_id_functions
@@ -44,46 +80,24 @@ module MkAcl
44
80
  # # SQL sequence of spec file tables
45
81
  # def table_seq() @table_seq ||= spec.tables.map(&:uid).join(', ') end
46
82
 
47
- # Generate a set of per-table functions that returns the associated
48
- # case_id/event_id/vist_id for the given record. The Functions are
49
- # generated for each table in the spec file
50
- #
51
- # The geneated functions are
52
- #
53
- # case_id_of_RECORD(id integer)
54
- # event_id_of_RECORD(id integer)
55
- # domain_id_of_RECORD(id integer)
56
- #
57
- # and the record variants
58
- #
59
- # case_id_of_RECORD(r record)
60
- # event_id_of_RECORD(r record)
61
- # domain_id_of_RECORD(r record)
62
- #
63
- # 'RECORD' is substituted with the record name of a table. Record names
64
- # are the singular name of a table. TODO: Make it plural again
65
- #
66
- # The domain functions returns event_id if related to an event and
67
- # case_id if not. It is meant to return the most specialized domin id for
68
- # a record. TODO Is it used?
69
- #
70
- # Returns null if not found. Note that the record has to exists because
71
- # we access it to read the foreign keys
83
+ # Generate
84
+ # case_id_of_RECORD(object_id integer)
85
+ # event_id_of_RECORD(object_id integer)
86
+ # visit_id_of_RECORD(object_id integer)
72
87
  #
73
88
  def generate_per_table_id_functions
74
- # Generate functions by domain
75
- for domain in DOMAINS
89
+ for domain in DOMAINS # Generate functions by domain
76
90
 
77
91
  # Create the identity functions first. The identity functions are
78
92
  # case_id_of_case() and event_id_of_event(). This makes some stuff
79
93
  # easier later on
80
94
  table = "#{domain}s"
81
95
  id_field = "#{domain}_id"
82
- signature = "#{acl_schema}.#{id_field}_of_#{domain}(_id integer)"
96
+ signature = "#{app_schema}.#{id_field}_of_#{domain}(_object_id integer)"
83
97
  puts %(
84
98
  drop function if exists #{signature} cascade;
85
99
  create function #{signature} returns integer as $$
86
- select _id;
100
+ select _object_id;
87
101
  $$ language sql
88
102
  set search_path to ''; -- intentionally empty
89
103
  ).align
@@ -120,47 +134,43 @@ module MkAcl
120
134
  end
121
135
  end
122
136
 
123
- # Tables that references visits, events, and cases
124
- visit_tables = chains['visits'].map(&:first)
125
-
126
- # Tables that references events and cases
127
- event_tables = chains['events'].map(&:first) - visit_tables
128
-
129
- # Tables that references only cases
130
- case_tables = chains['cases'].map(&:first) - event_tables
131
-
132
- # Create domain functions
133
- for domain, tables in { case: case_tables, event: event_tables, visit: visit_tables }
134
- for table_name in tables
135
- record_name = Prick::Inflector.singularize(table_name)
136
- signature = "#{acl_schema}.domain_id_of_#{record_name}(_id integer)"
137
- domain_function = "#{acl_schema}.#{domain}_id_of_#{record_name}"
138
- puts %(
139
- drop function if exists #{signature} cascade;
140
- create function #{signature} returns integer as $$
141
- select #{domain_function}(_id);
142
- $$ language sql
143
- set search_path to ''; -- intentionally empty
144
- ).align
145
- puts
146
- end
147
- end
137
+ # # Tables that references visits, events, and cases
138
+ # visit_tables = chains['visits'].map(&:first)
139
+ #
140
+ # # Tables that references events and cases
141
+ # event_tables = chains['events'].map(&:first) - visit_tables
142
+ #
143
+ # # Tables that references only cases
144
+ # case_tables = chains['cases'].map(&:first) - event_tables
145
+ #
146
+ # # Create domain functions
147
+ # for domain, tables in { case: case_tables, event: event_tables, visit: visit_tables }
148
+ # for table_name in tables
149
+ # record_name = Prick::Inflector.singularize(table_name)
150
+ # signature = "#{acl_schema}.domain_id_of_#{record_name}(_object_id integer)"
151
+ # domain_function = "#{acl_schema}.#{domain}_id_of_#{record_name}"
152
+ # puts %(
153
+ # drop function if exists #{signature} cascade;
154
+ # create function #{signature} returns integer as $$
155
+ # select #{domain_function}(_object_id);
156
+ # $$ language sql
157
+ # set search_path to ''; -- intentionally empty
158
+ # ).align
159
+ # puts
160
+ # end
161
+ # end
148
162
  end
149
163
 
150
- # Generate general case/event id-of functions:
151
- #
164
+ # Generate
152
165
  # case_id_of(table varchar, id integer)
153
166
  # event_id_of(table varchar, id integer)
154
- # domain_id_of(table varchar, id integer)
155
- #
156
- # These methods are practically as efficient as using the more
157
- # specialized versions above
167
+ # visit_id_of(table varchar, id integer)
158
168
  #
159
169
  def generate_general_id_functions
160
170
  for domain in DOMAINS
161
171
  domain_table = Prick::Inflector.pluralize(domain)
162
172
  field = "#{domain}_id"
163
- signature = "#{acl_schema}.#{field}_of(_table varchar, _id integer)"
173
+ signature = "#{acl_schema}.#{field}_of(_table varchar, _object_id integer)"
164
174
 
165
175
  puts %(
166
176
  drop function if exists #{signature} cascade;
@@ -173,11 +183,11 @@ module MkAcl
173
183
  for table_name in chains[domain_table].map(&:first)
174
184
  record_name = Prick::Inflector.singularize(table_name)
175
185
  function_name = "#{field}_of_#{record_name}"
176
- puts "when '#{table_name}' then #{acl_schema}.#{function_name}(_id)"
186
+ puts "when '#{table_name}' then #{acl_schema}.#{function_name}(_object_id)"
177
187
  end
178
- puts "when 'cases' then _id"
179
- puts "when 'events' then _id"
180
- puts "when 'visits' then _id"
188
+ puts "when 'cases' then _object_id"
189
+ puts "when 'events' then _object_id"
190
+ puts "when 'visits' then _object_id"
181
191
  puts "else null"
182
192
  }
183
193
  puts "end case;"
@@ -185,34 +195,13 @@ module MkAcl
185
195
  puts "$$ language sql;"
186
196
  puts
187
197
  end
188
-
189
- signature = "#{acl_schema}.domain_id_of(_table varchar, _id integer)"
190
- puts %(
191
- -- FIXME: Ugly implementation
192
- drop function if exists #{signature} cascade;
193
- create function #{signature} returns integer as $$
194
- select coalesce(
195
- acl_portal.visit_id_of(_table, _id),
196
- acl_portal.event_id_of(_table, _id),
197
- acl_portal.case_id_of(_table, _id)
198
- );
199
- $$ language sql;
200
- ).align
201
- puts
202
198
  end
203
199
 
204
- # General domain-id-of-record functions
205
- #
200
+ # Generate
206
201
  # case_id_of(r record)
207
202
  # event_id_of(r record)
208
203
  # domain_id_of(r record)
209
204
  #
210
- # Returns the case, event, or visit id associated with the given record.
211
- # The record should be an ACL table record type
212
- #
213
- # Very useful in before insert triggers because it takes a record (eg.
214
- # NEW) instead of an ID
215
- #
216
205
  def generate_general_id_of_record_functions
217
206
  links = (conn.structs %(
218
207
  select
@@ -276,22 +265,22 @@ module MkAcl
276
265
  puts
277
266
  end
278
267
 
279
- signature = "#{acl_schema}.domain_id_of(_r record)"
280
- puts %(
281
- -- Note: Ugly implementation but maybe close to optimal
282
- drop function if exists #{signature} cascade;
283
- create function #{signature} returns integer as $$
284
- declare
285
- _id integer;
286
- begin
287
- select coalesce(acl_portal.visit_id_of(_r), acl_portal.event_id_of(_r), acl_portal.case_id_of(_r))
288
- into _id;
289
- return _id;
290
- end;
291
- $$ language plpgsql
292
- set search_path to ''; -- intentionally ''
293
- ).align
294
- puts
268
+ # signature = "#{acl_schema}.domain_id_of(_r record)"
269
+ # puts %(
270
+ # -- Note: Ugly implementation but maybe close to optimal
271
+ # drop function if exists #{signature} cascade;
272
+ # create function #{signature} returns integer as $$
273
+ # declare
274
+ # _object_id integer;
275
+ # begin
276
+ # select coalesce(acl_portal.visit_id_of(_r), acl_portal.event_id_of(_r), acl_portal.case_id_of(_r))
277
+ # into _object_id;
278
+ # return _object_id;
279
+ # end;
280
+ # $$ language plpgsql
281
+ # set search_path to ''; -- intentionally ''
282
+ # ).align
283
+ # puts
295
284
  end
296
285
  end
297
286
  end
@@ -1,4 +1,21 @@
1
1
  module MkAcl
2
+ # Generates the functions
3
+ #
4
+ # acl_portal.user_is_role(_user_id integer, _object_id integer, _roles text[])
5
+ # acl_portal.user_is_role(_user_id integer, _object_id integer, _roles text)
6
+ # Returns true if the user has one of the given roles on the domain
7
+ # object
8
+ #
9
+ # public.current_is_role(_object_id integer, _roles text[])
10
+ # public.current_is_role(_object_id integer, _roles text)
11
+ # Returns true if the current user has one of the given roles on the
12
+ # domain object
13
+ #
14
+ # public.current_is_ROLE(_object_id integer)
15
+ # ROLE is one of the role kinds. Returns true if the current user has the
16
+ # given role
17
+ #
18
+
2
19
  class Generator
3
20
  class RoleFunctions
4
21
  using String::Text
@@ -20,54 +37,34 @@ module MkAcl
20
37
  private
21
38
  def generate_user_role_functions
22
39
  # TODO: Test. Then combine with per-table methods
23
- signature = "#{acl_schema}.user_is_role(_user_id integer, _domain_id integer, _roles text[])"
40
+ signature = "#{acl_schema}.user_is_role(_user_id integer, _object_id integer, _roles text[])"
24
41
  puts %(
25
42
  -- Return true if the user possess one or more of the given roles on the
26
- -- domain record (cases, events, visits) with the given ID
43
+ -- object (cases, events, visits) with the given ID
27
44
  --
28
45
  drop function if exists #{signature} cascade;
29
46
  create function #{signature} returns boolean as $$
30
47
  select exists (
31
48
  select
32
- from #{app_schema}.case_roles cr
33
- join #{app_schema}.case_role_users cru on cru.case_role_id = cr.id
34
- where cr.case_id = _domain_id
35
- and cru.user_id = _user_id
36
- and cr.kind = any(_roles)
37
-
38
- union
39
-
40
- select
41
- from #{app_schema}.event_roles cr
42
- join #{app_schema}.event_role_users cru on cru.event_role_id = cr.id
43
- where cr.event_id = _domain_id
44
- and cru.user_id = _user_id
45
- and cr.kind = any(_roles)
46
-
47
- union
48
-
49
- select
50
- from #{app_schema}.visit_roles cr
51
- join #{app_schema}.visit_role_users cru on cru.visit_role_id = cr.id
52
- where cr.visit_id = _domain_id
53
- and cru.user_id = _user_id
54
- and cr.kind = any(_roles)
55
-
56
- );
49
+ from app_portal.domain_users
50
+ where domain_id = _domain_id
51
+ and user_id = _user_id
52
+ and role = any(_roles)
53
+ )
57
54
  $$ language sql
58
55
  security definer;
59
56
  ).align
60
57
  puts
61
58
 
62
- signature = "#{acl_schema}.user_is_role(_user_id integer, _domain_id integer, role text)"
59
+ signature = "#{acl_schema}.user_is_role(_user_id integer, _object_id integer, role text)"
63
60
  puts %(
64
- -- Return true if the user has the given role on the domain record
61
+ -- Return true if the user has the given role on the object
65
62
  -- (cases, events, visits). Note that this function overloads the multi-role
66
63
  -- version
67
64
  --
68
65
  drop function if exists #{signature} cascade;
69
66
  create function #{signature} returns boolean as $$
70
- select #{acl_schema}.user_is_role(_user_id, _domain_id, array[role]);
67
+ select #{acl_schema}.user_is_role(_user_id, _object_id, array[role]);
71
68
  $$ language sql
72
69
  security definer;
73
70
  ).align
@@ -75,13 +72,13 @@ module MkAcl
75
72
 
76
73
  for domain, roles in DOMAINS.zip([CASE_ROLES, EVENT_ROLES, VISIT_ROLES])
77
74
  for role in roles
78
- signature = "#{acl_schema}.user_is_#{role.downcase}(_user_id integer, _domain_id integer)"
75
+ signature = "#{acl_schema}.user_is_#{role.downcase}(_user_id integer, _object_id integer)"
79
76
  puts %(
80
77
  -- Return true if the user possess the '#{role}' role
81
78
  --
82
79
  drop function if exists #{signature} cascade;
83
80
  create function #{signature} returns boolean as $$
84
- select #{acl_schema}.user_is_role(_user_id, _domain_id, '#{role}');
81
+ select #{acl_schema}.user_is_role(_user_id, _object_id, '#{role}');
85
82
  $$ language sql
86
83
  security definer;
87
84
  ).align
@@ -92,28 +89,28 @@ module MkAcl
92
89
 
93
90
  def generate_current_role_functions
94
91
  # TODO: Test. Then combine with per-table methods
95
- signature = "public.current_is_role(_domain_id integer, _roles text[])"
92
+ signature = "public.current_is_role(_object_id integer, _roles text[])"
96
93
  puts %(
97
94
  -- Return true if the current user possess one or more of the given roles on the
98
- -- domain record (cases, events, visits) with the given ID
95
+ -- object (cases, events, visits) with the given ID
99
96
  --
100
97
  drop function if exists #{signature} cascade;
101
98
  create function #{signature} returns boolean as $$
102
- select #{acl_schema}.user_is_role(public.current_user_id(), _domain_id, _roles);
99
+ select #{acl_schema}.user_is_role(public.current_user_id(), _object_id, _roles);
103
100
  $$ language sql
104
101
  security definer;
105
102
  ).align
106
103
  puts
107
104
 
108
- signature = "public.current_is_role(_domain_id integer, _role text)"
105
+ signature = "public.current_is_role(_object_id integer, _role text)"
109
106
  puts %(
110
- -- Return true if the current user possess the given role on the domain record
107
+ -- Return true if the current user possess the given role on the object
111
108
  -- (cases, events, visits). Note that this function overloads the multi-role
112
109
  -- version
113
110
  --
114
111
  drop function if exists #{signature} cascade;
115
112
  create function #{signature} returns boolean as $$
116
- select #{acl_schema}.user_is_role(public.current_user_id(), _domain_id, array[_role]);
113
+ select #{acl_schema}.user_is_role(public.current_user_id(), _object_id, array[_role]);
117
114
  $$ language sql
118
115
  security definer;
119
116
  ).align
@@ -121,13 +118,13 @@ module MkAcl
121
118
 
122
119
  for domain, roles in DOMAINS.zip([CASE_ROLES, EVENT_ROLES, VISIT_ROLES])
123
120
  for role in roles
124
- signature = "public.current_is_#{role.downcase}(_domain_id integer)"
121
+ signature = "public.current_is_#{role.downcase}(_object_id integer)"
125
122
  puts %(
126
123
  -- Return true if the current user possess the '#{role}' role
127
124
  --
128
125
  drop function if exists #{signature} cascade;
129
126
  create function #{signature} returns boolean as $$
130
- select #{acl_schema}.user_is_role(public.current_user_id(), _domain_id, '#{role}');
127
+ select #{acl_schema}.user_is_role(public.current_user_id(), _object_id, '#{role}');
131
128
  $$ language sql
132
129
  security definer;
133
130
  ).align
@@ -12,6 +12,7 @@ module MkAcl
12
12
  DOMAINS = %w(case event visit)
13
13
  DOMAIN_TABLES = DOMAINS.map { "#{_1}s" }
14
14
 
15
+ # TODO Read from database (or maybe not)
15
16
  CASE_ROLES = %w(LA TA KON AKK RLA CLA CTA)
16
17
  EVENT_ROLES = %w(ELA ETA)
17
18
  VISIT_ROLES = %w(VLA VTA)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MikrasUtils
4
- VERSION = "0.3.1"
4
+ VERSION = "0.3.3"
5
5
  end
@@ -1,8 +1,10 @@
1
1
 
2
+ drop function if exists public.current_role_ids() cascade;
3
+ drop function if exists public.current_domain_roles(integer) cascade;
4
+
2
5
  -- Returns an array of role IDs for the current user (from both auth and
3
6
  -- app_portal). This array is matched against the relevant ACL field in the RLS
4
7
  -- policies so performance is important (TODO)
5
- drop function if exists public.current_role_ids() cascade;
6
8
  create function public.current_role_ids() returns integer[] as $$
7
9
  select acl_portal.select_user_acl(public.current_user_id());
8
10
  $$ language sql
@@ -13,7 +15,6 @@ $$ language sql
13
15
 
14
16
  -- Returns an array of app_portal role names (kind) for the current user in the
15
17
  -- given domain
16
- drop function if exists public.current_domain_roles(integer) cascade;
17
18
  create function public.current_domain_roles(_domain_id integer) returns text[] as $$
18
19
  select coalesce(array_agg(dr.role), array[]::text[])
19
20
  from acl_portal.domain_users du
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mikras_utils
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Claus Rasmussen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-07-15 00:00:00.000000000 Z
11
+ date: 2024-08-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pg_conn