Browse Source

rename columns to include

Maurits van der Schee 6 years ago
parent
commit
c352c18c47

+ 15
- 15
api.php View File

3428
     }
3428
     }
3429
 }
3429
 }
3430
 
3430
 
3431
-// file: src/Tqdev/PhpCrudApi/Record/ColumnSelector.php
3431
+// file: src/Tqdev/PhpCrudApi/Record/ColumnIncluder.php
3432
 
3432
 
3433
-class ColumnSelector
3433
+class ColumnIncluder
3434
 {
3434
 {
3435
 
3435
 
3436
     private function isMandatory(String $tableName, String $columnName, array $params): bool
3436
     private function isMandatory(String $tableName, String $columnName, array $params): bool
3473
     {
3473
     {
3474
         $tableName = $table->getName();
3474
         $tableName = $table->getName();
3475
         $results = $table->columnNames();
3475
         $results = $table->columnNames();
3476
-        $results = $this->select($tableName, $primaryTable, $params, 'columns', $results, true);
3476
+        $results = $this->select($tableName, $primaryTable, $params, 'include', $results, true);
3477
         $results = $this->select($tableName, $primaryTable, $params, 'exclude', $results, false);
3477
         $results = $this->select($tableName, $primaryTable, $params, 'exclude', $results, false);
3478
         return $results;
3478
         return $results;
3479
     }
3479
     }
3772
     {
3772
     {
3773
         $this->db = $db;
3773
         $this->db = $db;
3774
         $this->tables = $reflection->getDatabase();
3774
         $this->tables = $reflection->getDatabase();
3775
-        $this->columns = new ColumnSelector();
3775
+        $this->columns = new ColumnIncluder();
3776
         $this->joiner = new RelationJoiner($this->columns);
3776
         $this->joiner = new RelationJoiner($this->columns);
3777
         $this->filters = new FilterInfo();
3777
         $this->filters = new FilterInfo();
3778
         $this->ordering = new OrderingInfo();
3778
         $this->ordering = new OrderingInfo();
3876
 
3876
 
3877
     private $columns;
3877
     private $columns;
3878
 
3878
 
3879
-    public function __construct(ColumnSelector $columns)
3879
+    public function __construct(ColumnIncluder $columns)
3880
     {
3880
     {
3881
         $this->columns = $columns;
3881
         $this->columns = $columns;
3882
     }
3882
     }
3883
 
3883
 
3884
-    public function addMandatoryColumns(ReflectedTable $table, ReflectedDatabase $tables, array &$params)/*: void*/
3884
+    public function addMandatoryColumns(ReflectedTable $table, ReflectedDatabase $tables, array &$params) /*: void*/
3885
     {
3885
     {
3886
-        if (!isset($params['join']) || !isset($params['columns'])) {
3886
+        if (!isset($params['join']) || !isset($params['include'])) {
3887
             return;
3887
             return;
3888
         }
3888
         }
3889
         $params['mandatory'] = array();
3889
         $params['mandatory'] = array();
3933
     }
3933
     }
3934
 
3934
 
3935
     public function addJoins(ReflectedTable $table, array &$records, ReflectedDatabase $tables, array $params,
3935
     public function addJoins(ReflectedTable $table, array &$records, ReflectedDatabase $tables, array $params,
3936
-        GenericDB $db)/*: void*/{
3936
+        GenericDB $db) /*: void*/ {
3937
 
3937
 
3938
         $joins = $this->getJoinsAsPathTree($tables, $params);
3938
         $joins = $this->getJoinsAsPathTree($tables, $params);
3939
         $this->addJoinsForTables($table, $joins, $records, $tables, $params, $db);
3939
         $this->addJoinsForTables($table, $joins, $records, $tables, $params, $db);
4013
         return $fkValues;
4013
         return $fkValues;
4014
     }
4014
     }
4015
 
4015
 
4016
-    private function addFkRecords(ReflectedTable $t2, array $fkValues, array $params, GenericDB $db, array &$records)/*: void*/
4016
+    private function addFkRecords(ReflectedTable $t2, array $fkValues, array $params, GenericDB $db, array &$records) /*: void*/
4017
     {
4017
     {
4018
         $pk = $t2->getPk();
4018
         $pk = $t2->getPk();
4019
         $columnNames = $this->columns->getNames($t2, false, $params);
4019
         $columnNames = $this->columns->getNames($t2, false, $params);
4024
         }
4024
         }
4025
     }
4025
     }
4026
 
4026
 
4027
-    private function fillFkValues(ReflectedTable $t2, array $fkRecords, array &$fkValues)/*: void*/
4027
+    private function fillFkValues(ReflectedTable $t2, array $fkRecords, array &$fkValues) /*: void*/
4028
     {
4028
     {
4029
         $pkName = $t2->getPk()->getName();
4029
         $pkName = $t2->getPk()->getName();
4030
         foreach ($fkRecords as $fkRecord) {
4030
         foreach ($fkRecords as $fkRecord) {
4033
         }
4033
         }
4034
     }
4034
     }
4035
 
4035
 
4036
-    private function setFkValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, array $fkValues)/*: void*/
4036
+    private function setFkValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, array $fkValues) /*: void*/
4037
     {
4037
     {
4038
         $fks = $t1->getFksTo($t2->getName());
4038
         $fks = $t1->getFksTo($t2->getName());
4039
         foreach ($fks as $fk) {
4039
         foreach ($fks as $fk) {
4058
         return $pkValues;
4058
         return $pkValues;
4059
     }
4059
     }
4060
 
4060
 
4061
-    private function addPkRecords(ReflectedTable $t1, ReflectedTable $t2, array $pkValues, array $params, GenericDB $db, array &$records)/*: void*/
4061
+    private function addPkRecords(ReflectedTable $t1, ReflectedTable $t2, array $pkValues, array $params, GenericDB $db, array &$records) /*: void*/
4062
     {
4062
     {
4063
         $fks = $t2->getFksTo($t1->getName());
4063
         $fks = $t2->getFksTo($t1->getName());
4064
         $columnNames = $this->columns->getNames($t2, false, $params);
4064
         $columnNames = $this->columns->getNames($t2, false, $params);
4073
         }
4073
         }
4074
     }
4074
     }
4075
 
4075
 
4076
-    private function fillPkValues(ReflectedTable $t1, ReflectedTable $t2, array $pkRecords, array &$pkValues)/*: void*/
4076
+    private function fillPkValues(ReflectedTable $t1, ReflectedTable $t2, array $pkRecords, array &$pkValues) /*: void*/
4077
     {
4077
     {
4078
         $fks = $t2->getFksTo($t1->getName());
4078
         $fks = $t2->getFksTo($t1->getName());
4079
         foreach ($fks as $fk) {
4079
         foreach ($fks as $fk) {
4087
         }
4087
         }
4088
     }
4088
     }
4089
 
4089
 
4090
-    private function setPkValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, array $pkValues)/*: void*/
4090
+    private function setPkValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, array $pkValues) /*: void*/
4091
     {
4091
     {
4092
         $pkName = $t1->getPk()->getName();
4092
         $pkName = $t1->getPk()->getName();
4093
         $t2Name = $t2->getName();
4093
         $t2Name = $t2->getName();
4125
         return new HabtmValues($pkValues, $fkValues);
4125
         return new HabtmValues($pkValues, $fkValues);
4126
     }
4126
     }
4127
 
4127
 
4128
-    private function setHabtmValues(ReflectedTable $t1, ReflectedTable $t3, array &$records, HabtmValues $habtmValues)/*: void*/
4128
+    private function setHabtmValues(ReflectedTable $t1, ReflectedTable $t3, array &$records, HabtmValues $habtmValues) /*: void*/
4129
     {
4129
     {
4130
         $pkName = $t1->getPk()->getName();
4130
         $pkName = $t1->getPk()->getName();
4131
         $t3Name = $t3->getName();
4131
         $t3Name = $t3->getName();

+ 0
- 66
src/Tqdev/PhpCrudApi/Record/ColumnSelector.php View File

1
-<?php
2
-namespace Tqdev\PhpCrudApi\Record;
3
-
4
-use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
5
-
6
-class ColumnSelector
7
-{
8
-
9
-    private function isMandatory(String $tableName, String $columnName, array $params): bool
10
-    {
11
-        return isset($params['mandatory']) && in_array($tableName . "." . $columnName, $params['mandatory']);
12
-    }
13
-
14
-    private function select(String $tableName, bool $primaryTable, array $params, String $paramName,
15
-        array $columnNames, bool $include): array{
16
-        if (!isset($params[$paramName])) {
17
-            return $columnNames;
18
-        }
19
-        $columns = array();
20
-        foreach (explode(',', $params[$paramName][0]) as $columnName) {
21
-            $columns[$columnName] = true;
22
-        }
23
-        $result = array();
24
-        foreach ($columnNames as $columnName) {
25
-            $match = isset($columns['*.*']);
26
-            if (!$match) {
27
-                $match = isset($columns[$tableName . '.*']) || isset($columns[$tableName . '.' . $columnName]);
28
-            }
29
-            if ($primaryTable && !$match) {
30
-                $match = isset($columns['*']) || isset($columns[$columnName]);
31
-            }
32
-            if ($match) {
33
-                if ($include || $this->isMandatory($tableName, $columnName, $params)) {
34
-                    $result[] = $columnName;
35
-                }
36
-            } else {
37
-                if (!$include || $this->isMandatory($tableName, $columnName, $params)) {
38
-                    $result[] = $columnName;
39
-                }
40
-            }
41
-        }
42
-        return $result;
43
-    }
44
-
45
-    public function getNames(ReflectedTable $table, bool $primaryTable, array $params): array
46
-    {
47
-        $tableName = $table->getName();
48
-        $results = $table->columnNames();
49
-        $results = $this->select($tableName, $primaryTable, $params, 'columns', $results, true);
50
-        $results = $this->select($tableName, $primaryTable, $params, 'exclude', $results, false);
51
-        return $results;
52
-    }
53
-
54
-    public function getValues(ReflectedTable $table, bool $primaryTable, /* object */ $record, array $params): array
55
-    {
56
-        $results = array();
57
-        $columnNames = $this->getNames($table, $primaryTable, $params);
58
-        foreach ($columnNames as $columnName) {
59
-            if (property_exists($record, $columnName)) {
60
-                $results[$columnName] = $record->$columnName;
61
-            }
62
-        }
63
-        return $results;
64
-    }
65
-
66
-}

+ 1
- 1
src/Tqdev/PhpCrudApi/Record/RecordService.php View File

19
     {
19
     {
20
         $this->db = $db;
20
         $this->db = $db;
21
         $this->tables = $reflection->getDatabase();
21
         $this->tables = $reflection->getDatabase();
22
-        $this->columns = new ColumnSelector();
22
+        $this->columns = new ColumnIncluder();
23
         $this->joiner = new RelationJoiner($this->columns);
23
         $this->joiner = new RelationJoiner($this->columns);
24
         $this->filters = new FilterInfo();
24
         $this->filters = new FilterInfo();
25
         $this->ordering = new OrderingInfo();
25
         $this->ordering = new OrderingInfo();

+ 13
- 13
src/Tqdev/PhpCrudApi/Record/RelationJoiner.php View File

1
 <?php
1
 <?php
2
 namespace Tqdev\PhpCrudApi\Record;
2
 namespace Tqdev\PhpCrudApi\Record;
3
 
3
 
4
+use Tqdev\PhpCrudApi\Column\Reflection\ReflectedDatabase;
5
+use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
4
 use Tqdev\PhpCrudApi\Database\GenericDB;
6
 use Tqdev\PhpCrudApi\Database\GenericDB;
5
 use Tqdev\PhpCrudApi\Record\Condition\ColumnCondition;
7
 use Tqdev\PhpCrudApi\Record\Condition\ColumnCondition;
6
 use Tqdev\PhpCrudApi\Record\Condition\OrCondition;
8
 use Tqdev\PhpCrudApi\Record\Condition\OrCondition;
7
-use Tqdev\PhpCrudApi\Column\Reflection\ReflectedDatabase;
8
-use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
9
 
9
 
10
 class RelationJoiner
10
 class RelationJoiner
11
 {
11
 {
12
 
12
 
13
     private $columns;
13
     private $columns;
14
 
14
 
15
-    public function __construct(ColumnSelector $columns)
15
+    public function __construct(ColumnIncluder $columns)
16
     {
16
     {
17
         $this->columns = $columns;
17
         $this->columns = $columns;
18
     }
18
     }
19
 
19
 
20
-    public function addMandatoryColumns(ReflectedTable $table, ReflectedDatabase $tables, array &$params)/*: void*/
20
+    public function addMandatoryColumns(ReflectedTable $table, ReflectedDatabase $tables, array &$params) /*: void*/
21
     {
21
     {
22
-        if (!isset($params['join']) || !isset($params['columns'])) {
22
+        if (!isset($params['join']) || !isset($params['include'])) {
23
             return;
23
             return;
24
         }
24
         }
25
         $params['mandatory'] = array();
25
         $params['mandatory'] = array();
69
     }
69
     }
70
 
70
 
71
     public function addJoins(ReflectedTable $table, array &$records, ReflectedDatabase $tables, array $params,
71
     public function addJoins(ReflectedTable $table, array &$records, ReflectedDatabase $tables, array $params,
72
-        GenericDB $db)/*: void*/{
72
+        GenericDB $db) /*: void*/ {
73
 
73
 
74
         $joins = $this->getJoinsAsPathTree($tables, $params);
74
         $joins = $this->getJoinsAsPathTree($tables, $params);
75
         $this->addJoinsForTables($table, $joins, $records, $tables, $params, $db);
75
         $this->addJoinsForTables($table, $joins, $records, $tables, $params, $db);
149
         return $fkValues;
149
         return $fkValues;
150
     }
150
     }
151
 
151
 
152
-    private function addFkRecords(ReflectedTable $t2, array $fkValues, array $params, GenericDB $db, array &$records)/*: void*/
152
+    private function addFkRecords(ReflectedTable $t2, array $fkValues, array $params, GenericDB $db, array &$records) /*: void*/
153
     {
153
     {
154
         $pk = $t2->getPk();
154
         $pk = $t2->getPk();
155
         $columnNames = $this->columns->getNames($t2, false, $params);
155
         $columnNames = $this->columns->getNames($t2, false, $params);
160
         }
160
         }
161
     }
161
     }
162
 
162
 
163
-    private function fillFkValues(ReflectedTable $t2, array $fkRecords, array &$fkValues)/*: void*/
163
+    private function fillFkValues(ReflectedTable $t2, array $fkRecords, array &$fkValues) /*: void*/
164
     {
164
     {
165
         $pkName = $t2->getPk()->getName();
165
         $pkName = $t2->getPk()->getName();
166
         foreach ($fkRecords as $fkRecord) {
166
         foreach ($fkRecords as $fkRecord) {
169
         }
169
         }
170
     }
170
     }
171
 
171
 
172
-    private function setFkValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, array $fkValues)/*: void*/
172
+    private function setFkValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, array $fkValues) /*: void*/
173
     {
173
     {
174
         $fks = $t1->getFksTo($t2->getName());
174
         $fks = $t1->getFksTo($t2->getName());
175
         foreach ($fks as $fk) {
175
         foreach ($fks as $fk) {
194
         return $pkValues;
194
         return $pkValues;
195
     }
195
     }
196
 
196
 
197
-    private function addPkRecords(ReflectedTable $t1, ReflectedTable $t2, array $pkValues, array $params, GenericDB $db, array &$records)/*: void*/
197
+    private function addPkRecords(ReflectedTable $t1, ReflectedTable $t2, array $pkValues, array $params, GenericDB $db, array &$records) /*: void*/
198
     {
198
     {
199
         $fks = $t2->getFksTo($t1->getName());
199
         $fks = $t2->getFksTo($t1->getName());
200
         $columnNames = $this->columns->getNames($t2, false, $params);
200
         $columnNames = $this->columns->getNames($t2, false, $params);
209
         }
209
         }
210
     }
210
     }
211
 
211
 
212
-    private function fillPkValues(ReflectedTable $t1, ReflectedTable $t2, array $pkRecords, array &$pkValues)/*: void*/
212
+    private function fillPkValues(ReflectedTable $t1, ReflectedTable $t2, array $pkRecords, array &$pkValues) /*: void*/
213
     {
213
     {
214
         $fks = $t2->getFksTo($t1->getName());
214
         $fks = $t2->getFksTo($t1->getName());
215
         foreach ($fks as $fk) {
215
         foreach ($fks as $fk) {
223
         }
223
         }
224
     }
224
     }
225
 
225
 
226
-    private function setPkValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, array $pkValues)/*: void*/
226
+    private function setPkValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, array $pkValues) /*: void*/
227
     {
227
     {
228
         $pkName = $t1->getPk()->getName();
228
         $pkName = $t1->getPk()->getName();
229
         $t2Name = $t2->getName();
229
         $t2Name = $t2->getName();
261
         return new HabtmValues($pkValues, $fkValues);
261
         return new HabtmValues($pkValues, $fkValues);
262
     }
262
     }
263
 
263
 
264
-    private function setHabtmValues(ReflectedTable $t1, ReflectedTable $t3, array &$records, HabtmValues $habtmValues)/*: void*/
264
+    private function setHabtmValues(ReflectedTable $t1, ReflectedTable $t3, array &$records, HabtmValues $habtmValues) /*: void*/
265
     {
265
     {
266
         $pkName = $t1->getPk()->getName();
266
         $pkName = $t1->getPk()->getName();
267
         $t3Name = $t3->getName();
267
         $t3Name = $t3->getName();

+ 1
- 1
tests/functional/001_records/002_list_post_columns.log View File

1
-GET /records/posts?columns=id,content
1
+GET /records/posts?include=id,content
2
 ===
2
 ===
3
 200
3
 200
4
 Content-Type: application/json
4
 Content-Type: application/json

+ 1
- 1
tests/functional/001_records/005_read_post_columns.log View File

1
-GET /records/posts/2?columns=id,content
1
+GET /records/posts/2?include=id,content
2
 ===
2
 ===
3
 200
3
 200
4
 Content-Type: application/json
4
 Content-Type: application/json

+ 1
- 1
tests/functional/001_records/008_edit_post_columns_missing_field.log View File

1
-PUT /records/posts/3?columns=id,content
1
+PUT /records/posts/3?include=id,content
2
 
2
 
3
 {"content":"test (edited 2)"}
3
 {"content":"test (edited 2)"}
4
 ===
4
 ===

+ 1
- 1
tests/functional/001_records/009_edit_post_columns_extra_field.log View File

1
-PUT /records/posts/3?columns=id,content
1
+PUT /records/posts/3?include=id,content
2
 
2
 
3
 {"user_id":2,"content":"test (edited 3)"}
3
 {"user_id":2,"content":"test (edited 3)"}
4
 ===
4
 ===

+ 1
- 1
tests/functional/001_records/015_delete_post_ignore_columns.log View File

1
-DELETE /records/posts/4?columns=id,content
1
+DELETE /records/posts/4?include=id,content
2
 ===
2
 ===
3
 200
3
 200
4
 Content-Type: application/json
4
 Content-Type: application/json

+ 1
- 1
tests/functional/001_records/033_list_example_from_readme_tag_name_only.log View File

1
-GET /records/posts?columns=tags.name&join=categories&join=post_tags,tags&join=comments&filter=id,eq,1
1
+GET /records/posts?include=tags.name&join=categories&join=post_tags,tags&join=comments&filter=id,eq,1
2
 ===
2
 ===
3
 200
3
 200
4
 Content-Type: application/json
4
 Content-Type: application/json

+ 1
- 1
tests/functional/001_records/041_cors_pre_flight.log View File

1
-OPTIONS /records/posts/1?columns=id
1
+OPTIONS /records/posts/1?include=id
2
 Origin: http://example.com
2
 Origin: http://example.com
3
 Access-Control-Request-Method: POST
3
 Access-Control-Request-Method: POST
4
 Access-Control-Request-Headers: X-XSRF-TOKEN, X-Requested-With
4
 Access-Control-Request-Headers: X-XSRF-TOKEN, X-Requested-With

+ 1
- 1
tests/functional/001_records/042_cors_headers.log View File

1
-GET /records/posts/1?columns=id
1
+GET /records/posts/1?include=id
2
 Origin: http://example.com
2
 Origin: http://example.com
3
 ===
3
 ===
4
 200
4
 200

+ 1
- 1
tests/functional/001_records/052_edit_user_location.log View File

8
 
8
 
9
 1
9
 1
10
 ===
10
 ===
11
-GET /records/users/1?columns=id,location
11
+GET /records/users/1?include=id,location
12
 ===
12
 ===
13
 200
13
 200
14
 Content-Type: application/json
14
 Content-Type: application/json

+ 1
- 1
tests/functional/001_records/053_list_user_locations.log View File

1
-GET /records/users?columns=id,location
1
+GET /records/users?include=id,location
2
 ===
2
 ===
3
 200
3
 200
4
 Content-Type: application/json
4
 Content-Type: application/json

+ 1
- 1
tests/functional/001_records/054_edit_user_with_id.log View File

8
 
8
 
9
 1
9
 1
10
 ===
10
 ===
11
-GET /records/users/1?columns=id,username,password
11
+GET /records/users/1?include=id,username,password
12
 ===
12
 ===
13
 200
13
 200
14
 Content-Type: application/json
14
 Content-Type: application/json

+ 1
- 1
tests/functional/001_records/057_filter_on_and.log View File

1
-GET /records/posts?columns=id&filter=id,ge,1&filter=id,le,2
1
+GET /records/posts?include=id&filter=id,ge,1&filter=id,le,2
2
 ===
2
 ===
3
 200
3
 200
4
 Content-Type: application/json
4
 Content-Type: application/json

+ 1
- 1
tests/functional/001_records/058_filter_on_or.log View File

1
-GET /records/posts?columns=id&filter1=id,eq,1&filter2=id,eq,2
1
+GET /records/posts?include=id&filter1=id,eq,1&filter2=id,eq,2
2
 ===
2
 ===
3
 200
3
 200
4
 Content-Type: application/json
4
 Content-Type: application/json

+ 1
- 1
tests/functional/001_records/059_filter_on_and_plus_or.log View File

1
-GET /records/posts?columns=id&filter1=id,eq,1&filter2=id,gt,1&filter2=id,lt,3
1
+GET /records/posts?include=id&filter1=id,eq,1&filter2=id,gt,1&filter2=id,lt,3
2
 ===
2
 ===
3
 200
3
 200
4
 Content-Type: application/json
4
 Content-Type: application/json

+ 1
- 1
tests/functional/001_records/060_filter_on_or_plus_and.log View File

1
-GET /records/posts?columns=id&filter1=id,eq,1&filter2=id,eq,2&filter=user_id,eq,1
1
+GET /records/posts?include=id&filter1=id,eq,1&filter2=id,eq,2&filter=user_id,eq,1
2
 ===
2
 ===
3
 200
3
 200
4
 Content-Type: application/json
4
 Content-Type: application/json

+ 1
- 1
tests/functional/001_records/061_get_post_content_with_included_tag_names.log View File

1
-GET /records/posts/1?columns=content,tags.name&join=tags
1
+GET /records/posts/1?include=content,tags.name&join=tags
2
 ===
2
 ===
3
 200
3
 200
4
 Content-Type: application/json
4
 Content-Type: application/json

+ 3
- 3
tests/functional/001_records/069_increment_event_visitors.log View File

1
-GET /records/events/1?columns=visitors
1
+GET /records/events/1?include=visitors
2
 ===
2
 ===
3
 200
3
 200
4
 Content-Type: application/json
4
 Content-Type: application/json
36
 
36
 
37
 [1,1]
37
 [1,1]
38
 ===
38
 ===
39
-GET /records/events/1?columns=visitors
39
+GET /records/events/1?include=visitors
40
 ===
40
 ===
41
 200
41
 200
42
 Content-Type: application/json
42
 Content-Type: application/json
54
 
54
 
55
 1
55
 1
56
 ===
56
 ===
57
-GET /records/events/1?columns=visitors
57
+GET /records/events/1?include=visitors
58
 ===
58
 ===
59
 200
59
 200
60
 Content-Type: application/json
60
 Content-Type: application/json

Loading…
Cancel
Save