Skip to content

Commit e301f23

Browse files
authored
Merge pull request #7909 from TheThingsNetwork/feature/end-device-count
2 parents aca2d66 + 49ec0ca commit e301f23

17 files changed

Lines changed: 1590 additions & 665 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ For details about compatibility between different releases, see the **Commitment
1111

1212
### Added
1313

14+
- `Count` RPC on `EndDeviceRegistry` to efficiently retrieve the number of end devices in an application.
1415
- Add tracing for LBS LNS and TTIGW protocol handlers.
1516
- TTGC LBS Root CUPS claiming support.
1617
- Configurable Identity Server user login session TTL via `is.user-login.session-ttl`:

api/ttn/lorawan/v3/api.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,8 @@
249249
- [Message `BatchUpdateEndDeviceLastSeenRequest.EndDeviceLastSeenUpdate`](#ttn.lorawan.v3.BatchUpdateEndDeviceLastSeenRequest.EndDeviceLastSeenUpdate)
250250
- [Message `BoolValue`](#ttn.lorawan.v3.BoolValue)
251251
- [Message `ConvertEndDeviceTemplateRequest`](#ttn.lorawan.v3.ConvertEndDeviceTemplateRequest)
252+
- [Message `CountEndDevicesRequest`](#ttn.lorawan.v3.CountEndDevicesRequest)
253+
- [Message `CountEndDevicesResponse`](#ttn.lorawan.v3.CountEndDevicesResponse)
252254
- [Message `CreateEndDeviceRequest`](#ttn.lorawan.v3.CreateEndDeviceRequest)
253255
- [Message `DevAddrPrefix`](#ttn.lorawan.v3.DevAddrPrefix)
254256
- [Message `EndDevice`](#ttn.lorawan.v3.EndDevice)
@@ -3984,6 +3986,25 @@ Configuration options for static ADR.
39843986
| ----- | ----------- |
39853987
| `format_id` | <p>`string.max_len`: `36`</p><p>`string.pattern`: `^[a-z0-9](?:[-]?[a-z0-9]){2,}$`</p> |
39863988

3989+
### <a name="ttn.lorawan.v3.CountEndDevicesRequest">Message `CountEndDevicesRequest`</a>
3990+
3991+
| Field | Type | Label | Description |
3992+
| ----- | ---- | ----- | ----------- |
3993+
| `application_ids` | [`ApplicationIdentifiers`](#ttn.lorawan.v3.ApplicationIdentifiers) | | |
3994+
| `filters` | [`ListEndDevicesRequest.Filter`](#ttn.lorawan.v3.ListEndDevicesRequest.Filter) | repeated | |
3995+
3996+
#### Field Rules
3997+
3998+
| Field | Validations |
3999+
| ----- | ----------- |
4000+
| `application_ids` | <p>`message.required`: `true`</p> |
4001+
4002+
### <a name="ttn.lorawan.v3.CountEndDevicesResponse">Message `CountEndDevicesResponse`</a>
4003+
4004+
| Field | Type | Label | Description |
4005+
| ----- | ---- | ----- | ----------- |
4006+
| `count` | [`uint64`](#uint64) | | |
4007+
39874008
### <a name="ttn.lorawan.v3.CreateEndDeviceRequest">Message `CreateEndDeviceRequest`</a>
39884009

39894010
| Field | Type | Label | Description |
@@ -4854,6 +4875,7 @@ NsEndDeviceRegistry, the AsEndDeviceRegistry and the JsEndDeviceRegistry.
48544875
| `Get` | [`GetEndDeviceRequest`](#ttn.lorawan.v3.GetEndDeviceRequest) | [`EndDevice`](#ttn.lorawan.v3.EndDevice) | Get the end device with the given identifiers, selecting the fields specified in the field mask. More or less fields may be returned, depending on the rights of the caller. |
48554876
| `GetIdentifiersForEUIs` | [`GetEndDeviceIdentifiersForEUIsRequest`](#ttn.lorawan.v3.GetEndDeviceIdentifiersForEUIsRequest) | [`EndDeviceIdentifiers`](#ttn.lorawan.v3.EndDeviceIdentifiers) | Get the identifiers of the end device that has the given EUIs registered. |
48564877
| `List` | [`ListEndDevicesRequest`](#ttn.lorawan.v3.ListEndDevicesRequest) | [`EndDevices`](#ttn.lorawan.v3.EndDevices) | List end devices in the given application. Similar to Get, this selects the fields given by the field mask. More or less fields may be returned, depending on the rights of the caller. |
4878+
| `Count` | [`CountEndDevicesRequest`](#ttn.lorawan.v3.CountEndDevicesRequest) | [`CountEndDevicesResponse`](#ttn.lorawan.v3.CountEndDevicesResponse) | Count end devices in the given application. |
48574879
| `Update` | [`UpdateEndDeviceRequest`](#ttn.lorawan.v3.UpdateEndDeviceRequest) | [`EndDevice`](#ttn.lorawan.v3.EndDevice) | Update the end device, changing the fields specified by the field mask to the provided values. |
48584880
| `BatchUpdateLastSeen` | [`BatchUpdateEndDeviceLastSeenRequest`](#ttn.lorawan.v3.BatchUpdateEndDeviceLastSeenRequest) | [`.google.protobuf.Empty`](#google.protobuf.Empty) | Update the last seen timestamp for a batch of end devices. |
48594881
| `Delete` | [`EndDeviceIdentifiers`](#ttn.lorawan.v3.EndDeviceIdentifiers) | [`.google.protobuf.Empty`](#google.protobuf.Empty) | Delete the end device with the given IDs. Before deleting an end device it first needs to be deleted from the NsEndDeviceRegistry, the AsEndDeviceRegistry and the JsEndDeviceRegistry. In addition, if the device claimed on a Join Server, it also needs to be unclaimed via the DeviceClaimingServer so it can be claimed in the future. This is NOT done automatically. |
@@ -4866,6 +4888,7 @@ NsEndDeviceRegistry, the AsEndDeviceRegistry and the JsEndDeviceRegistry.
48664888
| `Get` | `GET` | `/api/v3/applications/{end_device_ids.application_ids.application_id}/devices/{end_device_ids.device_id}` | |
48674889
| `List` | `GET` | `/api/v3/applications/{application_ids.application_id}/devices` | |
48684890
| `List` | `POST` | `/api/v3/applications/{application_ids.application_id}/devices/filter` | `*` |
4891+
| `Count` | `GET` | `/api/v3/applications/{application_ids.application_id}/devices/count` | |
48694892
| `Update` | `PUT` | `/api/v3/applications/{end_device.ids.application_ids.application_id}/devices/{end_device.ids.device_id}` | `*` |
48704893
| `Delete` | `DELETE` | `/api/v3/applications/{application_ids.application_id}/devices/{device_id}` | |
48714894

api/ttn/lorawan/v3/api.swagger.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1185,6 +1185,37 @@
11851185
]
11861186
}
11871187
},
1188+
"/applications/{application_ids.application_id}/devices/count": {
1189+
"get": {
1190+
"summary": "Count end devices in the given application.",
1191+
"operationId": "EndDeviceRegistry_Count",
1192+
"responses": {
1193+
"200": {
1194+
"description": "A successful response.",
1195+
"schema": {
1196+
"$ref": "#/definitions/v3CountEndDevicesResponse"
1197+
}
1198+
},
1199+
"default": {
1200+
"description": "An unexpected error response.",
1201+
"schema": {
1202+
"$ref": "#/definitions/googlerpcStatus"
1203+
}
1204+
}
1205+
},
1206+
"parameters": [
1207+
{
1208+
"name": "application_ids.application_id",
1209+
"in": "path",
1210+
"required": true,
1211+
"type": "string"
1212+
}
1213+
],
1214+
"tags": [
1215+
"EndDeviceRegistry"
1216+
]
1217+
}
1218+
},
11881219
"/applications/{application_ids.application_id}/devices/filter": {
11891220
"post": {
11901221
"summary": "List end devices in the given application.\nSimilar to Get, this selects the fields given by the field mask.\nMore or less fields may be returned, depending on the rights of the caller.",
@@ -22870,6 +22901,15 @@
2287022901
}
2287122902
}
2287222903
},
22904+
"v3CountEndDevicesResponse": {
22905+
"type": "object",
22906+
"properties": {
22907+
"count": {
22908+
"type": "string",
22909+
"format": "uint64"
22910+
}
22911+
}
22912+
},
2287322913
"v3CreateLoginTokenResponse": {
2287422914
"type": "object",
2287522915
"properties": {

api/ttn/lorawan/v3/end_device.proto

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,6 +1346,15 @@ message ListEndDevicesRequest {
13461346
repeated Filter filters = 6;
13471347
}
13481348

1349+
message CountEndDevicesRequest {
1350+
ApplicationIdentifiers application_ids = 1 [(validate.rules).message.required = true];
1351+
repeated ListEndDevicesRequest.Filter filters = 2;
1352+
}
1353+
1354+
message CountEndDevicesResponse {
1355+
uint64 count = 1;
1356+
}
1357+
13491358
message SetEndDeviceRequest {
13501359
EndDevice end_device = 1 [(validate.rules).message.required = true];
13511360
// The names of the end device fields that should be updated.

api/ttn/lorawan/v3/end_device_services.proto

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ service EndDeviceRegistry {
7272
};
7373
}
7474

75+
// Count end devices in the given application.
76+
rpc Count(CountEndDevicesRequest) returns (CountEndDevicesResponse) {
77+
option (google.api.http) = {get: "/applications/{application_ids.application_id}/devices/count"};
78+
}
79+
7580
// Update the end device, changing the fields specified by the field mask to the provided values.
7681
rpc Update(UpdateEndDeviceRequest) returns (EndDevice) {
7782
option (google.api.http) = {

pkg/identityserver/bunstore/end_device_store.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,9 @@ func (s *endDeviceStore) CountEndDevices(ctx context.Context, ids *ttnpb.Applica
439439
by = s.selectWithID(ctx, ids.GetApplicationId())
440440
}
441441

442-
selectQuery := s.newSelectModel(ctx, &EndDevice{}).Apply(by)
442+
selectQuery := s.newSelectModel(ctx, &EndDevice{}).
443+
Apply(by).
444+
Apply(selectWithFilterFromContext(ctx))
443445

444446
// Count the total number of results.
445447
count, err := selectQuery.Count(ctx)

pkg/identityserver/end_device_registry.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,34 @@ func (is *IdentityServer) listEndDevices(ctx context.Context, req *ttnpb.ListEnd
332332
return devs, nil
333333
}
334334

335+
func (is *IdentityServer) countEndDevices(
336+
ctx context.Context, req *ttnpb.CountEndDevicesRequest,
337+
) (*ttnpb.CountEndDevicesResponse, error) {
338+
if err := rights.RequireApplication(
339+
ctx, req.GetApplicationIds(), ttnpb.Right_RIGHT_APPLICATION_DEVICES_READ,
340+
); err != nil {
341+
return nil, err
342+
}
343+
if req.Filters != nil {
344+
for _, filter := range req.Filters {
345+
if _, ok := filter.GetField().(*ttnpb.ListEndDevicesRequest_Filter_UpdatedSince); ok {
346+
ctx = store.WithFilter(ctx, "updated_at", filter.GetUpdatedSince().AsTime().Format(time.RFC3339Nano))
347+
}
348+
}
349+
}
350+
var count uint64
351+
err := is.store.Transact(ctx, func(ctx context.Context, st store.Store) (err error) {
352+
count, err = st.CountEndDevices(ctx, req.GetApplicationIds())
353+
return err
354+
})
355+
if err != nil {
356+
return nil, err
357+
}
358+
return &ttnpb.CountEndDevicesResponse{
359+
Count: count,
360+
}, nil
361+
}
362+
335363
func (is *IdentityServer) setFullEndDevicePictureURL(ctx context.Context, dev *ttnpb.EndDevice) {
336364
bucketURL := is.configFromContext(ctx).EndDevicePicture.BucketURL
337365
if bucketURL == "" {
@@ -591,6 +619,12 @@ func (dr *endDeviceRegistry) Delete(ctx context.Context, req *ttnpb.EndDeviceIde
591619
return dr.deleteEndDevice(ctx, req)
592620
}
593621

622+
func (dr *endDeviceRegistry) Count(
623+
ctx context.Context, req *ttnpb.CountEndDevicesRequest,
624+
) (*ttnpb.CountEndDevicesResponse, error) {
625+
return dr.countEndDevices(ctx, req)
626+
}
627+
594628
func (reg *endDeviceBatchRegistry) Delete(
595629
ctx context.Context,
596630
req *ttnpb.BatchDeleteEndDevicesRequest,

pkg/identityserver/end_device_registry_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,113 @@ func TestEndDevicesPagination(t *testing.T) {
268268
}, withPrivateTestDatabase(p))
269269
}
270270

271+
func TestEndDevicesCount(t *testing.T) {
272+
p := &storetest.Population{}
273+
274+
usr1 := p.NewUser()
275+
app1 := p.NewApplication(usr1.GetOrganizationOrUserIdentifiers())
276+
for range 5 {
277+
p.NewEndDevice(app1.GetIds())
278+
}
279+
280+
key, _ := p.NewAPIKey(usr1.GetEntityIdentifiers(), ttnpb.Right_RIGHT_ALL)
281+
creds := rpcCreds(key)
282+
283+
readKey, _ := p.NewAPIKey(usr1.GetEntityIdentifiers(), ttnpb.Right_RIGHT_APPLICATION_DEVICES_READ)
284+
readCreds := rpcCreds(readKey)
285+
286+
t.Parallel()
287+
a, ctx := test.New(t)
288+
289+
testWithIdentityServer(t, func(_ *IdentityServer, cc *grpc.ClientConn) {
290+
reg := ttnpb.NewEndDeviceRegistryClient(cc)
291+
292+
t.Run("Permission denied without credentials", func(_ *testing.T) { // nolint:paralleltest
293+
_, err := reg.Count(ctx, &ttnpb.CountEndDevicesRequest{
294+
ApplicationIds: app1.GetIds(),
295+
})
296+
if a.So(err, should.NotBeNil) {
297+
a.So(errors.IsPermissionDenied(err), should.BeTrue)
298+
}
299+
})
300+
301+
t.Run("Count with read credentials", func(_ *testing.T) { // nolint:paralleltest
302+
resp, err := reg.Count(ctx, &ttnpb.CountEndDevicesRequest{
303+
ApplicationIds: app1.GetIds(),
304+
}, readCreds)
305+
if a.So(err, should.BeNil) && a.So(resp, should.NotBeNil) {
306+
a.So(resp.Count, should.Equal, uint64(5))
307+
}
308+
})
309+
310+
t.Run("Count after adding a device", func(_ *testing.T) { // nolint:paralleltest
311+
_, err := reg.Create(ctx, &ttnpb.CreateEndDeviceRequest{
312+
EndDevice: &ttnpb.EndDevice{
313+
Ids: &ttnpb.EndDeviceIdentifiers{
314+
ApplicationIds: app1.GetIds(),
315+
DeviceId: "count-test-dev",
316+
},
317+
},
318+
}, creds)
319+
a.So(err, should.BeNil)
320+
321+
resp, err := reg.Count(ctx, &ttnpb.CountEndDevicesRequest{
322+
ApplicationIds: app1.GetIds(),
323+
}, readCreds)
324+
if a.So(err, should.BeNil) && a.So(resp, should.NotBeNil) {
325+
a.So(resp.Count, should.Equal, uint64(6))
326+
}
327+
})
328+
329+
t.Run("Count after deleting a device", func(_ *testing.T) { // nolint:paralleltest
330+
_, err := reg.Delete(ctx, &ttnpb.EndDeviceIdentifiers{
331+
ApplicationIds: app1.GetIds(),
332+
DeviceId: "count-test-dev",
333+
}, creds)
334+
a.So(err, should.BeNil)
335+
336+
resp, err := reg.Count(ctx, &ttnpb.CountEndDevicesRequest{
337+
ApplicationIds: app1.GetIds(),
338+
}, readCreds)
339+
if a.So(err, should.BeNil) && a.So(resp, should.NotBeNil) {
340+
a.So(resp.Count, should.Equal, uint64(5))
341+
}
342+
})
343+
344+
t.Run("Count with updated_since filter", func(_ *testing.T) { // nolint:paralleltest
345+
// Filter by 1 hour ago - all devices should match.
346+
resp, err := reg.Count(ctx, &ttnpb.CountEndDevicesRequest{
347+
ApplicationIds: app1.GetIds(),
348+
Filters: []*ttnpb.ListEndDevicesRequest_Filter{
349+
{
350+
Field: &ttnpb.ListEndDevicesRequest_Filter_UpdatedSince{
351+
UpdatedSince: timestamppb.New(time.Now().Add(-time.Hour)),
352+
},
353+
},
354+
},
355+
}, readCreds)
356+
if a.So(err, should.BeNil) && a.So(resp, should.NotBeNil) {
357+
a.So(resp.Count, should.Equal, uint64(5))
358+
}
359+
360+
// Filter by now - no devices should match.
361+
resp, err = reg.Count(ctx, &ttnpb.CountEndDevicesRequest{
362+
ApplicationIds: app1.GetIds(),
363+
Filters: []*ttnpb.ListEndDevicesRequest_Filter{
364+
{
365+
Field: &ttnpb.ListEndDevicesRequest_Filter_UpdatedSince{
366+
UpdatedSince: timestamppb.New(time.Now()),
367+
},
368+
},
369+
},
370+
}, readCreds)
371+
if a.So(err, should.BeNil) && a.So(resp, should.NotBeNil) {
372+
a.So(resp.Count, should.Equal, uint64(0))
373+
}
374+
})
375+
}, withPrivateTestDatabase(p))
376+
}
377+
271378
func TestEndDevicesBatchOperationsPermissions(t *testing.T) {
272379
t.Parallel()
273380
a, ctx := test.New(t)

0 commit comments

Comments
 (0)