iknow_view_models 3.7.7 → 3.8.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 849a8c0a38a21b8d8d8c706aae9800c2d5b009a81e59432d501cc0cef8329db3
4
- data.tar.gz: b9cfa60ace9c9f6af4c711f7ae11fd8aa76934bcef7d6e7b360b749fe667d4b4
3
+ metadata.gz: 10ac2bba9965a5eb683cdbb972d1a7c8b31a6fd86304b1e52816cdd5649818d9
4
+ data.tar.gz: dcd13cba67c3b27beb4491b23d6a487ff30a3a3b930a8d2188a5503f77298c89
5
5
  SHA512:
6
- metadata.gz: 6e510f3a65791a27df5be2eabb889bd5a5addb84ee17f4e1b2f1ff7b76dfb1630978e3fc1b90bc1c26b36a83d9bb6892e9c3eb7e235b7f762946ab0c3b9b4ab4
7
- data.tar.gz: 67f12f88ce24394ddcaa639008a66ea0120fa5e062463fed5c4d30e0f20a75e7dda7823f025304b3cf4d1cda9e5640cbe281f459aab22f84ed5a4d920eb7f9b6
6
+ metadata.gz: a0faccad196615ad236d93016b3268fe50c3eac86bfc52750445a53655fc24d24ff068442071d4ea0a6d1d19e86cdaba8ae24ad9cba5d4feedfbb007333217e9
7
+ data.tar.gz: 253dea949f2c1a79360969fbd662a93d072ef93ff43554b5b08bc65344e2cc96c58b0eea2a71ff734cd8d3507c07b2569638be4d483c116d7b498ea98aca5fd5
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IknowViewModels
4
- VERSION = '3.7.7'
4
+ VERSION = '3.8.0'
5
5
  end
@@ -134,7 +134,15 @@ class ViewModel::ActiveRecord
134
134
 
135
135
  updates.each do |ref, update_data|
136
136
  viewmodel =
137
- if update_data.new?
137
+ if update_data.auto_child_update?
138
+ raise ViewModel::DeserializationError::InvalidStructure.new(
139
+ 'Cannot make an automatic child update to a root node',
140
+ ViewModel::Reference.new(update_data.viewmodel_class, nil))
141
+ elsif update_data.child_update?
142
+ raise ViewModel::DeserializationError::InvalidStructure.new(
143
+ 'Cannot update an existing root node without a specified id',
144
+ ViewModel::Reference.new(update_data.viewmodel_class, nil))
145
+ elsif update_data.new?
138
146
  viewmodel_class.for_new_model(id: update_data.id)
139
147
  else
140
148
  viewmodel_class.new(existing_models[update_data.id])
@@ -520,9 +520,7 @@ class ViewModel::ActiveRecord
520
520
  end
521
521
  end
522
522
 
523
- def new?
524
- id.nil? || metadata.new?
525
- end
523
+ delegate :new?, :child_update?, :auto_child_update?, to: :metadata
526
524
 
527
525
  def self.parse_hashes(root_subtree_hashes, referenced_subtree_hashes = {})
528
526
  valid_reference_keys = referenced_subtree_hashes.keys.to_set
@@ -300,6 +300,26 @@ class ViewModel::ActiveRecord
300
300
  case
301
301
  when update_data.new?
302
302
  child_viewmodel_class.for_new_model(id: update_data.id)
303
+ when update_data.child_update?
304
+ if association_data.collection?
305
+ raise ViewModel::DeserializationError::InvalidStructure.new(
306
+ 'Cannot update existing children of a collection association without specified ids',
307
+ ViewModel::Reference.new(update_data.viewmodel_class, nil))
308
+ end
309
+
310
+ child = previous_child_viewmodels[0]
311
+
312
+ if child.nil?
313
+ unless update_data.auto_child_update?
314
+ raise ViewModel::DeserializationError::PreviousChildNotFound.new(
315
+ association_data.association_name.to_s,
316
+ self.blame_reference)
317
+ end
318
+
319
+ child = child_viewmodel_class.for_new_model
320
+ end
321
+
322
+ child
303
323
  when existing_child = previous_by_key[key]
304
324
  existing_child
305
325
  when taken_child = update_context.try_take_released_viewmodel(key)
@@ -211,6 +211,23 @@ class ViewModel
211
211
  end
212
212
  end
213
213
 
214
+ class PreviousChildNotFound < NotFound
215
+ attr_reader :association
216
+
217
+ def initialize(association, blame_nodes)
218
+ @association = association
219
+ super(blame_nodes)
220
+ end
221
+
222
+ def detail
223
+ "No child currently exists to update for association '#{association}'"
224
+ end
225
+
226
+ def meta
227
+ super.merge(association: association)
228
+ end
229
+ end
230
+
214
231
  class DuplicateNodes < InvalidRequest
215
232
  attr_reader :type
216
233
 
@@ -21,7 +21,7 @@ class ViewModel::Schemas
21
21
  'description' => 'viewmodel update',
22
22
  'properties' => { ViewModel::TYPE_ATTRIBUTE => { 'type' => 'string' },
23
23
  ViewModel::ID_ATTRIBUTE => ID_SCHEMA,
24
- ViewModel::NEW_ATTRIBUTE => { 'type' => 'boolean' },
24
+ ViewModel::NEW_ATTRIBUTE => { 'oneOf' => [{ 'type' => 'boolean' }, { 'type' => 'string', 'enum' => ['auto'] }] },
25
25
  ViewModel::VERSION_ATTRIBUTE => { 'type' => 'integer' } },
26
26
  'required' => [ViewModel::TYPE_ATTRIBUTE],
27
27
  }.freeze
data/lib/view_model.rb CHANGED
@@ -11,6 +11,22 @@ class ViewModel
11
11
  ID_ATTRIBUTE = 'id'
12
12
  TYPE_ATTRIBUTE = '_type'
13
13
  VERSION_ATTRIBUTE = '_version'
14
+
15
+ # The optional _new attribute specifies explicitly whether a deserialization
16
+ # is to an new or existing model. The behaviour of _new is as follows:
17
+ # * true - Always create a new model, using the id if provided, error if
18
+ # a model with that id already exists
19
+ # * false - Never create a new model. If id isn’t provided, it's an error
20
+ # unless the viewmodel is being deserialized in the context of a
21
+ # singular association, in which case it's implicitly referring
22
+ # to the current child of that association, and is an error if
23
+ # that child doesn't exist.
24
+ # * nil - Create a new model if `id` is not specified, otherwise update the
25
+ # existing record with `id`, and error if it doesn't exist.
26
+ # * 'auto' - Can only be used when the viewmodel is being deserialized in the
27
+ # context of a singular association. `id` must not be specified. If
28
+ # the association currently has a child, update it, otherwise
29
+ # create a new one.
14
30
  NEW_ATTRIBUTE = '_new'
15
31
 
16
32
  BULK_UPDATE_TYPE = '_bulk_update'
@@ -20,10 +36,36 @@ class ViewModel
20
36
  # Migrations leave a metadata attribute _migrated on any views that they
21
37
  # alter. This attribute is accessible as metadata when deserializing migrated
22
38
  # input, and is included in the output serialization sent to clients.
23
- MIGRATED_ATTRIBUTE = '_migrated'
39
+ MIGRATED_ATTRIBUTE = '_migrated'
24
40
 
25
41
  Metadata = Struct.new(:id, :view_name, :schema_version, :new, :migrated) do
26
- alias_method :new?, :new
42
+ # Does this metadata describe deserialization to a new model, either by
43
+ # {new: true} or {new: nil, id: nil}
44
+ def new?
45
+ if new.nil?
46
+ id.nil?
47
+ else
48
+ new == true
49
+ end
50
+ end
51
+
52
+ # Does this metadata describe an change to the anonymous child of a singular
53
+ # association
54
+ def child_update?
55
+ explicit_child_update? || auto_child_update?
56
+ end
57
+
58
+ # Does this metadata describe an update to the already-existing anonymous
59
+ # child of a singular association
60
+ def explicit_child_update?
61
+ new == false && id.nil?
62
+ end
63
+
64
+ # Does this child metadata describe an create-or-update to the anonymous
65
+ # child of a singular association
66
+ def auto_child_update?
67
+ new == 'auto'
68
+ end
27
69
  end
28
70
 
29
71
  class << self
@@ -119,9 +161,14 @@ class ViewModel
119
161
  id = hash.delete(ViewModel::ID_ATTRIBUTE)
120
162
  type_name = hash.delete(ViewModel::TYPE_ATTRIBUTE)
121
163
  schema_version = hash.delete(ViewModel::VERSION_ATTRIBUTE)
122
- new = hash.delete(ViewModel::NEW_ATTRIBUTE) { false }
164
+ new = hash.delete(ViewModel::NEW_ATTRIBUTE)
123
165
  migrated = hash.delete(ViewModel::MIGRATED_ATTRIBUTE) { false }
124
166
 
167
+ if id && new == 'auto'
168
+ raise ViewModel::DeserializationError::InvalidSyntax.new(
169
+ "An explicit id must not be specified with an 'auto' child update")
170
+ end
171
+
125
172
  Metadata.new(id, type_name, schema_version, new, migrated)
126
173
  end
127
174
 
@@ -85,7 +85,7 @@ class ViewModel::ActiveRecord::BelongsToTest < ActiveSupport::TestCase
85
85
  end
86
86
  end
87
87
 
88
- def test_belongs_to_create
88
+ def test_belongs_to_create_implicit_new
89
89
  @model1.update(child: nil)
90
90
 
91
91
  alter_by_view!(ModelView, @model1) do |view, _refs|
@@ -95,6 +95,108 @@ class ViewModel::ActiveRecord::BelongsToTest < ActiveSupport::TestCase
95
95
  assert_equal('cheese', @model1.child.name)
96
96
  end
97
97
 
98
+ def test_belongs_to_create
99
+ @model1.update(child: nil)
100
+
101
+ alter_by_view!(ModelView, @model1) do |view, _refs|
102
+ view['child'] = { '_type' => 'Child', '_new' => true, 'name' => 'cheese' }
103
+ end
104
+
105
+ assert_equal('cheese', @model1.child.name)
106
+ end
107
+
108
+ def test_belongs_to_create_with_id
109
+ @model1.update(child: nil)
110
+
111
+ alter_by_view!(ModelView, @model1) do |view, _refs|
112
+ view['child'] = { '_type' => 'Child', 'id' => 9999, '_new' => true, 'name' => 'cheese' }
113
+ end
114
+
115
+ assert_equal('cheese', @model1.child.name)
116
+ assert_equal(9999, @model1.child.id)
117
+ end
118
+
119
+ def test_belongs_to_create_with_colliding_id
120
+ child_id = @model1.child_id
121
+ @model1.update(child: nil)
122
+
123
+ ex = assert_raises(ViewModel::DeserializationError::UniqueViolation) do
124
+ alter_by_view!(ModelView, @model1) do |view, _refs|
125
+ view['child'] = { '_type' => 'Child', 'id' => child_id, '_new' => true, 'name' => 'cheese' }
126
+ end
127
+ end
128
+
129
+ assert_equal(['id'], ex.columns)
130
+ assert_equal([ViewModel::Reference.new(ChildView, child_id)], ex.nodes)
131
+ end
132
+
133
+ def test_belongs_to_create_child_with_auto
134
+ @model1.update(child: nil)
135
+
136
+ alter_by_view!(ModelView, @model1) do |view, _refs|
137
+ view['child'] = { '_type' => 'Child', '_new' => 'auto', 'name' => 'cheese' }
138
+ end
139
+
140
+ assert_equal('cheese', @model1.child.name)
141
+ end
142
+
143
+ def test_belongs_to_edit_implicit_new
144
+ child_id = @model1.child_id
145
+
146
+ alter_by_view!(ModelView, @model1) do |view, _refs|
147
+ view['child'] = { '_type' => 'Child', 'id' => child_id, 'name' => 'cheese' }
148
+ end
149
+
150
+ assert_equal('cheese', @model1.child.name)
151
+ assert_equal(child_id, @model1.child.id)
152
+ end
153
+
154
+ def test_belongs_to_edit
155
+ child_id = @model1.child_id
156
+
157
+ alter_by_view!(ModelView, @model1) do |view, _refs|
158
+ view['child'] = { '_type' => 'Child', 'id' => child_id, '_new' => false, 'name' => 'cheese' }
159
+ end
160
+
161
+ assert_equal('cheese', @model1.child.name)
162
+ assert_equal(child_id, @model1.child.id)
163
+ end
164
+
165
+ def test_belongs_to_edit_child_without_id
166
+ child_id = @model1.child_id
167
+
168
+ alter_by_view!(ModelView, @model1) do |view, _refs|
169
+ view['child'] = { '_type' => 'Child', '_new' => false, 'name' => 'cheese' }
170
+ end
171
+
172
+ assert_equal('cheese', @model1.child.name)
173
+ assert_equal(child_id, @model1.child.id)
174
+ end
175
+
176
+ def test_belongs_to_one_edit_without_id_no_child
177
+ @model1.update(child: nil)
178
+
179
+ ex = assert_raises(ViewModel::DeserializationError::PreviousChildNotFound) do
180
+ alter_by_view!(ModelView, @model1) do |view, _refs|
181
+ view['child'] = { '_type' => 'Child', '_new' => false, 'name' => 'cheese' }
182
+ end
183
+ end
184
+
185
+ assert_equal('child', ex.association)
186
+ assert_equal([ViewModel::Reference.new(ModelView, @model1.id)], ex.nodes)
187
+ end
188
+
189
+ def test_belongs_to_edit_child_with_auto
190
+ child_id = @model1.child_id
191
+
192
+ alter_by_view!(ModelView, @model1) do |view, _refs|
193
+ view['child'] = { '_type' => 'Child', '_new' => 'auto', 'name' => 'cheese' }
194
+ end
195
+
196
+ assert_equal('cheese', @model1.child.name)
197
+ assert_equal(child_id, @model1.child.id)
198
+ end
199
+
98
200
  def test_belongs_to_replace
99
201
  old_child = @model1.child
100
202
 
@@ -107,7 +107,7 @@ class ViewModel::ActiveRecord::HasManyTest < ActiveSupport::TestCase
107
107
  assert(pv.model.children.blank?)
108
108
  end
109
109
 
110
- def test_create_has_many
110
+ def test_create_has_many_implicit_new
111
111
  view = { '_type' => 'Model',
112
112
  'name' => 'p',
113
113
  'children' => [{ '_type' => 'Child', 'name' => 'c1' },
@@ -123,6 +123,64 @@ class ViewModel::ActiveRecord::HasManyTest < ActiveSupport::TestCase
123
123
  assert_equal(%w[c1 c2], pv.model.children.map(&:name))
124
124
  end
125
125
 
126
+ def test_create_has_many
127
+ view = { '_type' => 'Model',
128
+ 'name' => 'p',
129
+ 'children' => [{ '_type' => 'Child', '_new' => true, 'name' => 'c1' },
130
+ { '_type' => 'Child', '_new' => true, 'name' => 'c2' },] }
131
+
132
+ context = viewmodel_class.new_deserialize_context
133
+ pv = viewmodel_class.deserialize_from_view(view, deserialize_context: context)
134
+
135
+ assert_contains_exactly(
136
+ [pv.to_reference, pv.children[0].to_reference, pv.children[1].to_reference],
137
+ context.valid_edit_refs)
138
+
139
+ assert_equal(%w[c1 c2], pv.model.children.map(&:name))
140
+ end
141
+
142
+ def test_create_has_many_with_id
143
+ view = { '_type' => 'Model',
144
+ 'name' => 'p',
145
+ 'children' => [{ '_type' => 'Child', 'id' => 9998, '_new' => true, 'name' => 'c1' },
146
+ { '_type' => 'Child', 'id' => 9999, '_new' => true, 'name' => 'c2' },] }
147
+
148
+ context = viewmodel_class.new_deserialize_context
149
+ pv = viewmodel_class.deserialize_from_view(view, deserialize_context: context)
150
+
151
+ assert_contains_exactly(
152
+ [pv.to_reference, pv.children[0].to_reference, pv.children[1].to_reference],
153
+ context.valid_edit_refs)
154
+
155
+ assert_equal(%w[c1 c2], pv.model.children.map(&:name))
156
+ end
157
+
158
+ def test_create_has_many_auto
159
+ view = { '_type' => 'Model',
160
+ 'name' => 'p',
161
+ 'children' => [{ '_type' => 'Child', '_new' => 'auto', 'name' => 'c1' }] }
162
+
163
+ context = viewmodel_class.new_deserialize_context
164
+ ex = assert_raises(ViewModel::DeserializationError::InvalidStructure) do
165
+ viewmodel_class.deserialize_from_view(view, deserialize_context: context)
166
+ end
167
+ assert_match(/existing children of a collection association without specified ids/, ex.message)
168
+ assert_equal([ViewModel::Reference.new(ChildView, nil)], ex.nodes)
169
+ end
170
+
171
+ def test_update_has_many_without_id
172
+ view = { '_type' => 'Model',
173
+ 'name' => 'p',
174
+ 'children' => [{ '_type' => 'Child', '_new' => false, 'name' => 'c1' }] }
175
+
176
+ context = viewmodel_class.new_deserialize_context
177
+ ex = assert_raises(ViewModel::DeserializationError::InvalidStructure) do
178
+ viewmodel_class.deserialize_from_view(view, deserialize_context: context)
179
+ end
180
+ assert_match(/existing children of a collection association without specified ids/, ex.message)
181
+ assert_equal([ViewModel::Reference.new(ChildView, nil)], ex.nodes)
182
+ end
183
+
126
184
  def test_nil_multiple_association
127
185
  view = {
128
186
  '_type' => 'Model',
@@ -98,7 +98,7 @@ class ViewModel::ActiveRecord::HasOneTest < ActiveSupport::TestCase
98
98
  assert_nil(pv.model.child)
99
99
  end
100
100
 
101
- def test_has_one_create
101
+ def test_has_one_create_implicit_new
102
102
  @model1.update(child: nil)
103
103
 
104
104
  alter_by_view!(ModelView, @model1) do |view, _refs|
@@ -108,12 +108,106 @@ class ViewModel::ActiveRecord::HasOneTest < ActiveSupport::TestCase
108
108
  assert_equal('t', @model1.child.name)
109
109
  end
110
110
 
111
- def test_has_one_update
111
+ def test_has_one_create
112
+ @model1.update(child: nil)
113
+
114
+ alter_by_view!(ModelView, @model1) do |view, _refs|
115
+ view['child'] = { '_type' => 'Child', '_new' => true, 'name' => 't' }
116
+ end
117
+
118
+ assert_equal('t', @model1.child.name)
119
+ end
120
+
121
+ def test_has_one_create_with_id
122
+ @model1.update(child: nil)
123
+
124
+ alter_by_view!(ModelView, @model1) do |view, _refs|
125
+ view['child'] = { '_type' => 'Child', 'id' => 9999, '_new' => true, 'name' => 't' }
126
+ end
127
+
128
+ assert_equal('t', @model1.child.name)
129
+ assert_equal(9999, @model1.child.id)
130
+ end
131
+
132
+ def test_has_one_create_with_colliding_id
133
+ child_id = @model2.child.id
134
+ @model1.update(child: nil)
135
+
136
+ ex = assert_raises(ViewModel::DeserializationError::UniqueViolation) do
137
+ alter_by_view!(ModelView, @model1) do |view, _refs|
138
+ view['child'] = { '_type' => 'Child', 'id' => child_id, '_new' => true, 'name' => 'cheese' }
139
+ end
140
+ end
141
+
142
+ assert_equal(['id'], ex.columns)
143
+ assert_equal([ViewModel::Reference.new(ChildView, child_id)], ex.nodes)
144
+ end
145
+
146
+ def test_has_one_create_child_with_auto
147
+ @model1.update(child: nil)
148
+
149
+ alter_by_view!(ModelView, @model1) do |view, _refs|
150
+ view['child'] = { '_type' => 'Child', '_new' => 'auto', 'name' => 'cheese' }
151
+ end
152
+
153
+ assert_equal('cheese', @model1.child.name)
154
+ end
155
+
156
+ def test_has_one_edit_implicit_new
157
+ child_id = @model1.child.id
158
+
159
+ alter_by_view!(ModelView, @model1) do |view, _refs|
160
+ view['child'] = { '_type' => 'Child', 'id' => child_id, 'name' => 'cheese' }
161
+ end
162
+
163
+ assert_equal('cheese', @model1.child.name)
164
+ assert_equal(child_id, @model1.child.id)
165
+ end
166
+
167
+ def test_has_one_edit
168
+ child_id = @model1.child.id
169
+
170
+ alter_by_view!(ModelView, @model1) do |view, _refs|
171
+ view['child'] = { '_type' => 'Child', 'id' => child_id, '_new' => false, 'name' => 'cheese' }
172
+ end
173
+
174
+ assert_equal('cheese', @model1.child.name)
175
+ assert_equal(child_id, @model1.child.id)
176
+ end
177
+
178
+ def test_has_one_edit_without_id
179
+ child_id = @model1.child.id
180
+
181
+ alter_by_view!(ModelView, @model1) do |view, _refs|
182
+ view['child'] = { '_type' => 'Child', '_new' => false, 'name' => 'cheese' }
183
+ end
184
+
185
+ assert_equal('cheese', @model1.child.name)
186
+ assert_equal(child_id, @model1.child.id)
187
+ end
188
+
189
+ def test_has_one_edit_without_id_no_child
190
+ @model1.update(child: nil)
191
+
192
+ ex = assert_raises(ViewModel::DeserializationError::PreviousChildNotFound) do
193
+ alter_by_view!(ModelView, @model1) do |view, _refs|
194
+ view['child'] = { '_type' => 'Child', '_new' => false, 'name' => 'cheese' }
195
+ end
196
+ end
197
+
198
+ assert_equal('child', ex.association)
199
+ assert_equal([ViewModel::Reference.new(ModelView, @model1.id)], ex.nodes)
200
+ end
201
+
202
+ def test_belongs_to_edit_child_with_auto
203
+ child_id = @model1.child.id
204
+
112
205
  alter_by_view!(ModelView, @model1) do |view, _refs|
113
- view['child']['name'] = 'hello'
206
+ view['child'] = { '_type' => 'Child', '_new' => 'auto', 'name' => 'cheese' }
114
207
  end
115
208
 
116
- assert_equal('hello', @model1.child.name)
209
+ assert_equal('cheese', @model1.child.name)
210
+ assert_equal(child_id, @model1.child.id)
117
211
  end
118
212
 
119
213
  def test_has_one_destroy
@@ -127,6 +127,30 @@ class ViewModel::ActiveRecordTest < ActiveSupport::TestCase
127
127
  assert_equal([ViewModel::Reference.new(ParentView, 9999)], ex.nodes)
128
128
  end
129
129
 
130
+ def test_create_implicit_id_raises
131
+ view = {
132
+ '_type' => 'Parent',
133
+ '_new' => false,
134
+ }
135
+ ex = assert_raises(ViewModel::DeserializationError::InvalidStructure) do
136
+ ParentView.deserialize_from_view(view)
137
+ end
138
+ assert_match(/existing root node without a specified id/, ex.message)
139
+ assert_equal([ViewModel::Reference.new(ParentView, nil)], ex.nodes)
140
+ end
141
+
142
+ def test_create_auto_raises
143
+ view = {
144
+ '_type' => 'Parent',
145
+ '_new' => 'auto',
146
+ }
147
+ ex = assert_raises(ViewModel::DeserializationError::InvalidStructure) do
148
+ ParentView.deserialize_from_view(view)
149
+ end
150
+ assert_match(/automatic child update to a root node/, ex.message)
151
+ assert_equal([ViewModel::Reference.new(ParentView, nil)], ex.nodes)
152
+ end
153
+
130
154
  def test_read_only_raises_with_id
131
155
  view = {
132
156
  '_type' => 'Parent',
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: iknow_view_models
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.7.7
4
+ version: 3.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - iKnow Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-15 00:00:00.000000000 Z
11
+ date: 2024-01-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack