Add updating devices to apiv1.
We needed a way to be able to update devices after they were created. This is
supported in the devices package, we just needed to expose it using apiv1
endpoints.
In doing so, it became apparent that allowing users to change the Owner of their
Devices wasn't properly thought through, and pending a reason to use it, I'm
just removing it. The biggest issue came when trying to return usable error
messages; we couldn't distinguish between "you don't own the device you're
trying to update" and "you're not allowed to change the owner of the device". I
also couldn't figure out _who should be able to_ change the owner of the device,
which is generally an indication that I'm building a feature before I have a use
case for it.
To support this change, the apiv1.DeviceChange type needed its Owner property
removed.
I also needed to add deviceFromAPI and devicesFromAPI helpers to return
devices.Device types from apiv1.Device types.
There's now a new validateDeviceUpdate helper that checks to ensure that a
device update request is valid and the user has the appropriate permissions.
The createRequest type now accepts a slice of Devices, not a slice of
DeviceChanges, because we want to pass the Owner in.
A new updateRequest type is created, which accepts a DeviceChange to apply.
A new handleUpdateDevice handler is created, which is assigned to the endpoint
for PATCH requests against a device ID. It checks that the user is logged in,
the Device they're trying to update exists, and that it's a valid update. If all
of that is true, the device is updated and the updated device is returned.
Finally, we had to add two new scopes to support new functionality:
ScopeUpdateOtherUserDevices allows a user to update other user's devices, and
ScopeUpdateLastSeen allows a user to update the LastSeen property of a device.
Pending some better error messages, this should be a full implementation of
updating a device, which leaves only the deletion endpoint to deal with.
9 "golang.org/x/net/context"
11 "code.secondbit.org/uuid.hg"
15 // ErrDeviceNotFound is returned when the specified device couldn't be found.
16 ErrDeviceNotFound = errors.New("device not found")
20 // MinDeviceNameLength is the minimum length a Device name can be.
21 MinDeviceNameLength = 1
22 // MaxDeviceNameLength is the maximum length a Device name can be.
23 MaxDeviceNameLength = 64
26 // Device represents a specific device that updates can be pushed to.
37 // ApplyChange returns a Device that is a copy of the passed Device,
38 // but with the passed DeviceChange applied.
39 func ApplyChange(d Device, change DeviceChange) Device {
41 if change.Name != nil {
42 result.Name = *change.Name
44 if change.Owner != nil {
45 result.Owner = *change.Owner
47 // We don't want to accidentally leave a slice that
48 // is owned by both behind.
49 result.Owner = d.Owner.Copy()
51 if change.Type != nil {
52 result.Type = *change.Type
54 if change.LastSeen != nil {
55 result.LastSeen = *change.LastSeen
57 if change.PushToken != nil {
58 result.PushToken = *change.PushToken
63 // DeviceChange represents a set of changes to a Device that will be used
64 // to update a Device.
65 type DeviceChange struct {
74 // Storer is an interface to control how data is stored in and retrieved from
76 type Storer interface {
77 GetDevices(ids []uuid.ID, c context.Context) (map[string]Device, error)
78 UpdateDevice(change DeviceChange, c context.Context) error
79 DeleteDevices(ids []uuid.ID, c context.Context) error
80 CreateDevices(devices []Device, c context.Context) error
81 ListDevicesByOwner(user uuid.ID, c context.Context) ([]Device, error)
84 // ErrDeviceAlreadyExists is returned when a Device is added to a Storer, but a
85 // Device with the same ID already exists in the Storer. The ID underlying
86 // ErrDeviceAlreadyExists will hold the ID that already exists.
87 type ErrDeviceAlreadyExists uuid.ID
89 func (e ErrDeviceAlreadyExists) Error() string {
90 return fmt.Sprintf("device with ID %s already exists in datastore", uuid.ID(e).String())
93 // GetMany returns as many of the Devices specified by the passed IDs as possible.
94 // They are returned as a map, with the key being the string version of the ID.
95 // No error will be returned if a Device can't be found.
96 func GetMany(ids []uuid.ID, c context.Context) (map[string]Device, error) {
97 results := map[string]Device{}
98 storer, err := GetStorer(c)
100 log.Printf("Error retrieving Storer: %+v\n", err)
103 results, err = storer.GetDevices(ids, c)
105 log.Printf("Error retrieving Devices from %T: %+v\n", storer, err)
111 // Get returns the Device specified by the passed ID. If the Device can't be found,
112 // an ErrDeviceNotFound error is returned.
113 func Get(id uuid.ID, c context.Context) (Device, error) {
114 results, err := GetMany([]uuid.ID{id}, c)
118 result, ok := results[id.String()]
120 return Device{}, ErrDeviceNotFound
125 // Update applies the DeviceChange to the passed Device, and returns the result. If
126 // the Device can't be found, an ErrDeviceNotFound error was returned.
127 func Update(device Device, change DeviceChange, c context.Context) (Device, error) {
128 storer, err := GetStorer(c)
130 log.Printf("Error retrieving Storer: %+v\n", err)
133 change.DeviceID = device.ID
134 err = storer.UpdateDevice(change, c)
138 return ApplyChange(device, change), nil
141 // DeleteMany removes the passed IDs from the datastore. No error is returned if the
142 // ID doesn't correspond to a Device in the datastore.
143 func DeleteMany(ids []uuid.ID, c context.Context) error {
144 storer, err := GetStorer(c)
146 log.Printf("Error retrieving Storer: %+v\n", err)
149 return storer.DeleteDevices(ids, c)
152 // Delete removes the passed ID from the datastore. No error is returned if the ID doesn't
153 // correspond to a Device in the datastore.
154 func Delete(id uuid.ID, c context.Context) error {
155 return DeleteMany([]uuid.ID{id}, c)
158 // CreateMany stores the passed Devices in the datastore, assigning default values if
159 // necessary. The Devices that were ultimately stored (including any default values, if
160 // applicable) are returned.
161 func CreateMany(devices []Device, c context.Context) ([]Device, error) {
162 storer, err := GetStorer(c)
164 log.Printf("Error retrieving Storer: %+v\n", err)
165 return []Device{}, err
167 modified := make([]Device, 0, len(devices))
168 for _, device := range devices {
169 if device.ID.IsZero() {
170 device.ID = uuid.NewID()
172 if device.Created.IsZero() {
173 device.Created = time.Now()
175 if device.LastSeen.IsZero() {
176 device.LastSeen = time.Now()
178 modified = append(modified, device)
180 err = storer.CreateDevices(modified, c)
182 return []Device{}, err
187 // Create stores the passed Device in the datastore, assigning default values if
188 // necessary. The Devices that were ultimately stored (including any default values, if
189 // applicable) are returned.
190 func Create(device Device, c context.Context) (Device, error) {
191 devices, err := CreateMany([]Device{device}, c)
195 // There should never be a case where we don't return a result.
196 // Ideally, we'd return an error here instead of letting the panic
197 // happen, but seeing as I can't come up with a reason the error would
198 // occur, I'm having trouble coming up with a reasonable error to return.
199 return devices[0], nil
202 // ListByOwner returns a slice of all the Devices with an Owner property that
203 // matches the passed ID. There's no guarantee on the order the Devices will be
205 func ListByOwner(user uuid.ID, c context.Context) ([]Device, error) {
206 // BUG(paddy): Eventually, we'll need to support paging for devices. But right now, I don't foresee any user creating enough of them to make pagination worthwhile.
207 storer, err := GetStorer(c)
209 log.Printf("Error retrieving Storer: %+v\n", err)
210 return []Device{}, err
212 devices, err := storer.ListDevicesByOwner(user, c)
216 // ToMap transforms the passed slice into a map by using the String method of
217 // each Device's ID as the key, and the Device as the value.
218 func ToMap(devices []Device) map[string]Device {
219 results := make(map[string]Device, len(devices))
220 for _, device := range devices {
221 results[device.ID.String()] = device
226 // ToSlice transforms the passed map of Devices into a slice of Devices.
227 func ToSlice(devices map[string]Device) []Device {
228 results := make([]Device, 0, len(devices))
229 for _, device := range devices {
230 results = append(results, device)