@justair/justair-library 7.5.0-alpha.d0806df → 7.5.0-alpha.ed6e6e7
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.
- package/dist/constants/deviceTypes.d.ts +0 -6
- package/dist/constants/deviceTypes.d.ts.map +1 -1
- package/dist/utils/subscribedDevices.d.ts +1 -2
- package/dist/utils/subscribedDevices.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants/deviceTypes.js +0 -8
- package/src/index.js +1 -2
- package/src/utils/subscribedDevices.js +49 -63
- package/src/utils/tests/subscribedDevices.test.js +5 -87
|
@@ -5,10 +5,4 @@ export const DEVICE_TYPES: Readonly<{
|
|
|
5
5
|
}>;
|
|
6
6
|
export const DEVICE_TYPE_VALUES: readonly ("monitor" | "sample-site" | "linked-device")[];
|
|
7
7
|
export function isAqiCapableDeviceType(deviceType: any): boolean;
|
|
8
|
-
/**
|
|
9
|
-
* The device-type vocabulary as a type, so consumers get autocomplete and a
|
|
10
|
-
* compile error on a mis-cased value like "sampleSite" — the exact bug the
|
|
11
|
-
* runtime comments in utils/subscribedDevices.js warn about at length.
|
|
12
|
-
*/
|
|
13
|
-
export type DeviceType = (typeof DEVICE_TYPES)[keyof typeof DEVICE_TYPES];
|
|
14
8
|
//# sourceMappingURL=deviceTypes.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deviceTypes.d.ts","sourceRoot":"","sources":["../../src/constants/deviceTypes.js"],"names":[],"mappings":"AA0BA;;;;GAIG;AAEH,0FAA6E;
|
|
1
|
+
{"version":3,"file":"deviceTypes.d.ts","sourceRoot":"","sources":["../../src/constants/deviceTypes.js"],"names":[],"mappings":"AA0BA;;;;GAIG;AAEH,0FAA6E;AAStE,iEAEoC"}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
export const SUBSCRIBED_DEVICES_ERRORS: Readonly<{
|
|
2
2
|
MISSING_SITE_RESOLVER: "missing_site_resolver";
|
|
3
3
|
RESOLVER_RETURNED_NON_ARRAY: "resolver_returned_non_array";
|
|
4
|
-
RESOLVER_RETURNED_NESTED_ARRAYS: "resolver_returned_nested_arrays";
|
|
5
4
|
RESOLVER_RETURNED_DOCUMENTS: "resolver_returned_documents";
|
|
6
5
|
}>;
|
|
7
6
|
export function projectSubscribedMonitors(subscribedDevices?: Array<{
|
|
8
7
|
deviceId: import("mongoose").Types.ObjectId;
|
|
9
|
-
deviceType:
|
|
8
|
+
deviceType: string;
|
|
10
9
|
}>, { resolveSiteMonitorIds }?: {
|
|
11
10
|
resolveSiteMonitorIds?: (siteIds: import("mongoose").Types.ObjectId[]) => Promise<Array<import("mongoose").Types.ObjectId | null | undefined>>;
|
|
12
11
|
}): Promise<import("mongoose").Types.ObjectId[]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscribedDevices.d.ts","sourceRoot":"","sources":["../../src/utils/subscribedDevices.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"subscribedDevices.d.ts","sourceRoot":"","sources":["../../src/utils/subscribedDevices.js"],"names":[],"mappings":"AA4EA;;;;GAIG;AAwII,8DALI,KAAK,CAAC;IAAE,QAAQ,EAAE,OAAO,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,8BAC1E;IAAE,qBAAqB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,UAAU,EAAE,KAAK,CAAC,QAAQ,EAAE,KAC7E,OAAO,CAAC,KAAK,CAAC,OAAO,UAAU,EAAE,KAAK,CAAC,QAAQ,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC,CAAA;CAAE,GAC9D,OAAO,CAAC,OAAO,UAAU,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CA0BxD"}
|
package/package.json
CHANGED
|
@@ -32,14 +32,6 @@ export const DEVICE_TYPES = Object.freeze({
|
|
|
32
32
|
|
|
33
33
|
export const DEVICE_TYPE_VALUES = Object.freeze(Object.values(DEVICE_TYPES));
|
|
34
34
|
|
|
35
|
-
/**
|
|
36
|
-
* The device-type vocabulary as a type, so consumers get autocomplete and a
|
|
37
|
-
* compile error on a mis-cased value like "sampleSite" — the exact bug the
|
|
38
|
-
* runtime comments in utils/subscribedDevices.js warn about at length.
|
|
39
|
-
*
|
|
40
|
-
* @typedef {typeof DEVICE_TYPES[keyof typeof DEVICE_TYPES]} DeviceType
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
35
|
// Whether a device of this type can ever report AQI, and therefore whether a
|
|
44
36
|
// subscription to it can contribute to AQI alert delivery.
|
|
45
37
|
//
|
package/src/index.js
CHANGED
|
@@ -276,8 +276,7 @@ export {
|
|
|
276
276
|
// Every consumer that writes a subscription recomputes the AQI audience with
|
|
277
277
|
// this instead of re-implementing the per-device-type rule.
|
|
278
278
|
// SUBSCRIBED_DEVICES_ERRORS is the stable `err.code` vocabulary for the
|
|
279
|
-
//
|
|
280
|
-
// than on message wording.
|
|
279
|
+
// throw above; consumers branch on it rather than on message wording.
|
|
281
280
|
projectSubscribedMonitors,
|
|
282
281
|
SUBSCRIBED_DEVICES_ERRORS,
|
|
283
282
|
// Utilities
|
|
@@ -42,20 +42,11 @@
|
|
|
42
42
|
// { deviceId, deviceType: "monitor" } entry per existing id), then
|
|
43
43
|
// overwrite freely. This is the target state.
|
|
44
44
|
// 2. Until that backfill has run for the user, union the projection with the
|
|
45
|
-
// existing subscribedMonitors instead of overwriting
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// migrated (subscribedDevices is non-empty) but is not, so the obvious
|
|
51
|
-
// "has this user been backfilled?" check answers yes and the next
|
|
52
|
-
// unsubscribe overwrites subscribedMonitors with a projection missing
|
|
53
|
-
// every monitor they had before. A half-migrated user must never exist.
|
|
54
|
-
//
|
|
55
|
-
// This path is also SUBSCRIBE-ONLY. A union can only ever add ids, so
|
|
56
|
-
// using it for an unsubscribe makes the removal a silent no-op. There is
|
|
57
|
-
// deliberately no third option: an unsubscribe on a user who predates
|
|
58
|
-
// subscribedDevices MUST be preceded by the backfill, and only then
|
|
45
|
+
// existing subscribedMonitors instead of overwriting. THIS IS
|
|
46
|
+
// SUBSCRIBE-ONLY. A union can only ever add ids, so using it for an
|
|
47
|
+
// unsubscribe makes the removal a silent no-op. There is deliberately no
|
|
48
|
+
// third option: an unsubscribe on a user who predates subscribedDevices
|
|
49
|
+
// MUST run the per-user backfill in option 1 first, and only then
|
|
59
50
|
// overwrite. Removing a subscription is not correct any other way.
|
|
60
51
|
//
|
|
61
52
|
// This module deliberately does NOT offer a `union: true` flag. A blanket
|
|
@@ -86,7 +77,6 @@ import { DEVICE_TYPES } from "../constants/deviceTypes.js";
|
|
|
86
77
|
export const SUBSCRIBED_DEVICES_ERRORS = Object.freeze({
|
|
87
78
|
MISSING_SITE_RESOLVER: "missing_site_resolver",
|
|
88
79
|
RESOLVER_RETURNED_NON_ARRAY: "resolver_returned_non_array",
|
|
89
|
-
RESOLVER_RETURNED_NESTED_ARRAYS: "resolver_returned_nested_arrays",
|
|
90
80
|
RESOLVER_RETURNED_DOCUMENTS: "resolver_returned_documents",
|
|
91
81
|
});
|
|
92
82
|
|
|
@@ -135,6 +125,38 @@ const linkedSiteIds = (entries) =>
|
|
|
135
125
|
.filter((entry) => entry.deviceType === DEVICE_TYPES.LINKED_DEVICE)
|
|
136
126
|
.map((entry) => entry.deviceId);
|
|
137
127
|
|
|
128
|
+
// Returns the monitor ids that should be stored in Users.subscribedMonitors for
|
|
129
|
+
// the given subscribedDevices array. Order is deterministic — direct `monitor`
|
|
130
|
+
// subscriptions in array order, then Site-derived ids in resolver order, first
|
|
131
|
+
// occurrence wins — but carries no meaning; treat the result as a set.
|
|
132
|
+
//
|
|
133
|
+
// `subscribedDevices` tolerates undefined/null: a user row written before this
|
|
134
|
+
// field existed has no key at all on a `.lean()` read (pinned in
|
|
135
|
+
// models/tests/users.test.js), and a projection over "no subscriptions" is an
|
|
136
|
+
// empty audience, not a crash.
|
|
137
|
+
//
|
|
138
|
+
// `resolveSiteMonitorIds(siteIds)` is called at most once, with every
|
|
139
|
+
// linked-device id at a time, and must resolve to the flat union of those
|
|
140
|
+
// Sites' monitorIds — the ids themselves, not the Site documents:
|
|
141
|
+
//
|
|
142
|
+
// const sites = await Sites.find({ _id: { $in: siteIds } }, "monitorIds").lean();
|
|
143
|
+
// return sites.flatMap((site) => site.monitorIds ?? []);
|
|
144
|
+
//
|
|
145
|
+
// Returning the documents is the easy mistake, so it is rejected rather than
|
|
146
|
+
// merely documented: the return is validated and a document-shaped entry
|
|
147
|
+
// throws RESOLVER_RETURNED_DOCUMENTS, as a non-array return throws
|
|
148
|
+
// RESOLVER_RETURNED_NON_ARRAY. Left unchecked it would fail silently — each
|
|
149
|
+
// Site doc casts to its own _id on write and subscribedMonitors fills with
|
|
150
|
+
// Site ids, exactly the polymorphic id this field split exists to keep out.
|
|
151
|
+
//
|
|
152
|
+
// It is not called at all when nothing is subscribed to a linked-device, so a
|
|
153
|
+
// consumer that only handles monitors pays no query. A resolver may return
|
|
154
|
+
// nullish entries — Sites.monitorIds is `default: undefined`, so a sample-only
|
|
155
|
+
// Site has no array — and they are filtered out here rather than trusted to
|
|
156
|
+
// the caller.
|
|
157
|
+
//
|
|
158
|
+
// Read the PRECONDITION and STALENESS notes at the top of this file before
|
|
159
|
+
// writing the result to Users.subscribedMonitors.
|
|
138
160
|
// The resolver is consumer-supplied, so its return is the one input this
|
|
139
161
|
// module cannot check at the type boundary. Validate it rather than trust it:
|
|
140
162
|
// both bad shapes below would otherwise corrupt subscribedMonitors silently,
|
|
@@ -154,29 +176,26 @@ const validateResolvedSiteMonitorIds = (resolved) => {
|
|
|
154
176
|
throw error;
|
|
155
177
|
}
|
|
156
178
|
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
179
|
+
// Discriminating a document from an id without importing mongoose: both
|
|
180
|
+
// expose `_id`, because mongoose defines `_id` on ObjectId.prototype as a
|
|
181
|
+
// self-reference (`oid._id === oid`). A document's `_id` is a *different*
|
|
182
|
+
// object, so the self-reference is the test — checking merely for the
|
|
183
|
+
// presence of `_id` would reject every ordinary ObjectId.
|
|
184
|
+
// `.map` instead of `.flatMap` yields [[m1, m2]]. It is not caught by the
|
|
185
|
+
// document check (an array is not a plain object) and fails inconsistently
|
|
186
|
+
// downstream: one monitor per Site casts back to the id and silently works,
|
|
187
|
+
// two throws a CastError at write time, far from the cause.
|
|
161
188
|
if (resolved.some(Array.isArray)) {
|
|
162
189
|
const error = new TypeError(
|
|
163
190
|
"resolveSiteMonitorIds must resolve to a flat array of monitor ids"
|
|
164
191
|
);
|
|
165
|
-
error.code = SUBSCRIBED_DEVICES_ERRORS.
|
|
192
|
+
error.code = SUBSCRIBED_DEVICES_ERRORS.RESOLVER_RETURNED_NON_ARRAY;
|
|
166
193
|
throw error;
|
|
167
194
|
}
|
|
168
195
|
|
|
169
|
-
// Discriminating a document from an id without importing mongoose: test for
|
|
170
|
-
// what only an id has. `toHexString` is on ObjectId (mongoose-patched or raw
|
|
171
|
-
// bson) and on nothing a Sites query returns, hydrated or lean.
|
|
172
|
-
//
|
|
173
|
-
// Deliberately not keyed on `_id`, which is wrong in both directions: every
|
|
174
|
-
// ObjectId appears to carry one (mongoose defines `_id` on ObjectId.prototype
|
|
175
|
-
// as a self-reference), and a lean read that projects it away
|
|
176
|
-
// (`Sites.find(..., "monitorIds -_id").lean()`) carries none.
|
|
177
196
|
if (
|
|
178
197
|
resolved.some(
|
|
179
|
-
(id) => isPlainObject(id) &&
|
|
198
|
+
(id) => isPlainObject(id) && id._id !== undefined && id._id !== id
|
|
180
199
|
)
|
|
181
200
|
) {
|
|
182
201
|
const error = new TypeError(
|
|
@@ -189,41 +208,8 @@ const validateResolvedSiteMonitorIds = (resolved) => {
|
|
|
189
208
|
return resolved;
|
|
190
209
|
};
|
|
191
210
|
|
|
192
|
-
// Returns the monitor ids that should be stored in Users.subscribedMonitors for
|
|
193
|
-
// the given subscribedDevices array. Order is deterministic — direct `monitor`
|
|
194
|
-
// subscriptions in array order, then Site-derived ids in resolver order, first
|
|
195
|
-
// occurrence wins — but carries no meaning; treat the result as a set.
|
|
196
|
-
//
|
|
197
|
-
// `subscribedDevices` tolerates undefined/null: a user row written before this
|
|
198
|
-
// field existed has no key at all on a `.lean()` read (pinned in
|
|
199
|
-
// models/tests/users.test.js), and a projection over "no subscriptions" is an
|
|
200
|
-
// empty audience, not a crash.
|
|
201
|
-
//
|
|
202
|
-
// `resolveSiteMonitorIds(siteIds)` is called at most once, with every
|
|
203
|
-
// linked-device id at a time, and must resolve to the flat union of those
|
|
204
|
-
// Sites' monitorIds — the ids themselves, not the Site documents:
|
|
205
|
-
//
|
|
206
|
-
// const sites = await Sites.find({ _id: { $in: siteIds } }, "monitorIds").lean();
|
|
207
|
-
// return sites.flatMap((site) => site.monitorIds ?? []);
|
|
208
|
-
//
|
|
209
|
-
// Returning the documents is the easy mistake, so it is rejected rather than
|
|
210
|
-
// merely documented: the return is validated and a document-shaped entry
|
|
211
|
-
// throws RESOLVER_RETURNED_DOCUMENTS, as a non-array return throws
|
|
212
|
-
// RESOLVER_RETURNED_NON_ARRAY. Left unchecked it would fail silently — each
|
|
213
|
-
// Site doc casts to its own _id on write and subscribedMonitors fills with
|
|
214
|
-
// Site ids, exactly the polymorphic id this field split exists to keep out.
|
|
215
|
-
//
|
|
216
|
-
// It is not called at all when nothing is subscribed to a linked-device, so a
|
|
217
|
-
// consumer that only handles monitors pays no query. A resolver may return
|
|
218
|
-
// nullish entries — Sites.monitorIds is `default: undefined`, so a sample-only
|
|
219
|
-
// Site has no array — and they are filtered out here rather than trusted to
|
|
220
|
-
// the caller.
|
|
221
|
-
//
|
|
222
|
-
// Read the PRECONDITION and STALENESS notes at the top of this file before
|
|
223
|
-
// writing the result to Users.subscribedMonitors.
|
|
224
211
|
/**
|
|
225
|
-
* @param {Array<{ deviceId: import("mongoose").Types.ObjectId,
|
|
226
|
-
* deviceType: import("../constants/deviceTypes.js").DeviceType }>} [subscribedDevices]
|
|
212
|
+
* @param {Array<{ deviceId: import("mongoose").Types.ObjectId, deviceType: string }>} [subscribedDevices]
|
|
227
213
|
* @param {{ resolveSiteMonitorIds?: (siteIds: import("mongoose").Types.ObjectId[]) =>
|
|
228
214
|
* Promise<Array<import("mongoose").Types.ObjectId | null | undefined>> }} [options]
|
|
229
215
|
* @returns {Promise<import("mongoose").Types.ObjectId[]>}
|
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
import mongoose from 'mongoose';
|
|
2
|
-
import { Users } from '../../models/users.js';
|
|
3
|
-
import {
|
|
4
|
-
DEVICE_TYPE_VALUES,
|
|
5
|
-
isAqiCapableDeviceType,
|
|
6
|
-
} from '../../constants/deviceTypes.js';
|
|
7
2
|
import {
|
|
8
3
|
projectSubscribedMonitors,
|
|
9
4
|
SUBSCRIBED_DEVICES_ERRORS,
|
|
@@ -145,46 +140,15 @@ describe('projectSubscribedMonitors', () => {
|
|
|
145
140
|
resolveSiteMonitorIds: async () => [[oid(), oid()]],
|
|
146
141
|
}),
|
|
147
142
|
).rejects.toMatchObject({
|
|
148
|
-
code: SUBSCRIBED_DEVICES_ERRORS.
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test('rejects a lean Site read that projected _id away', async () => {
|
|
153
|
-
// The `_id`-based discriminator missed this: `Sites.find(..., "monitorIds
|
|
154
|
-
// -_id").lean()` yields objects carrying no _id at all, which would
|
|
155
|
-
// otherwise key as "[object Object]" in dedupeById and land in
|
|
156
|
-
// subscribedMonitors as junk.
|
|
157
|
-
await expect(
|
|
158
|
-
projectSubscribedMonitors([{ deviceId: oid(), deviceType: 'linked-device' }], {
|
|
159
|
-
resolveSiteMonitorIds: async () => [{ monitorIds: [oid()] }],
|
|
160
|
-
}),
|
|
161
|
-
).rejects.toMatchObject({
|
|
162
|
-
code: SUBSCRIBED_DEVICES_ERRORS.RESOLVER_RETURNED_DOCUMENTS,
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
test('rejects a hydrated Site document (the forgot-.lean() case)', async () => {
|
|
167
|
-
// The likeliest real-world shape of this mistake. A throwaway model keeps
|
|
168
|
-
// this file off the real Sites schema; hydration needs no connection.
|
|
169
|
-
const HydratedSite = mongoose.model(
|
|
170
|
-
'SubscribedDevicesProbeSite',
|
|
171
|
-
new mongoose.Schema({ monitorIds: [mongoose.Schema.Types.ObjectId] }),
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
await expect(
|
|
175
|
-
projectSubscribedMonitors([{ deviceId: oid(), deviceType: 'linked-device' }], {
|
|
176
|
-
resolveSiteMonitorIds: async () => [new HydratedSite({ monitorIds: [oid()] })],
|
|
177
|
-
}),
|
|
178
|
-
).rejects.toMatchObject({
|
|
179
|
-
code: SUBSCRIBED_DEVICES_ERRORS.RESOLVER_RETURNED_DOCUMENTS,
|
|
143
|
+
code: SUBSCRIBED_DEVICES_ERRORS.RESOLVER_RETURNED_NON_ARRAY,
|
|
180
144
|
});
|
|
181
145
|
});
|
|
182
146
|
|
|
183
147
|
test('does not mistake ordinary ObjectIds for documents', async () => {
|
|
184
|
-
// Regression guard for the discriminator:
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
148
|
+
// Regression guard for the discriminator: mongoose defines `_id` on
|
|
149
|
+
// ObjectId.prototype as a self-reference, so a naive "has _id" check
|
|
150
|
+
// rejects every legitimate resolver result. Only a differing `_id` marks
|
|
151
|
+
// an actual document.
|
|
188
152
|
const siteId = oid();
|
|
189
153
|
const monitorId = oid();
|
|
190
154
|
|
|
@@ -367,49 +331,3 @@ describe('projectSubscribedMonitors', () => {
|
|
|
367
331
|
expect(after).toEqual([shared]);
|
|
368
332
|
});
|
|
369
333
|
});
|
|
370
|
-
|
|
371
|
-
// Two encodings of "which device types can produce AQI" exist: the predicate
|
|
372
|
-
// isAqiCapableDeviceType, and the selectors inside projectSubscribedMonitors.
|
|
373
|
-
// Nothing structural keeps them in step, so a fourth type added to one and not
|
|
374
|
-
// the other would report as AQI-capable while contributing no audience — the
|
|
375
|
-
// silent failure this module exists to prevent. This pins them together.
|
|
376
|
-
// Every other case here passes plain object literals, but the real call site
|
|
377
|
-
// hands in user.subscribedDevices — a mongoose DocumentArray of subdocuments,
|
|
378
|
-
// which has to survive the isPlainObject filter. If it ever stopped doing so
|
|
379
|
-
// every entry would be dropped and the projection would return an empty
|
|
380
|
-
// audience with no error. Hydration needs no connection, so this stays DB-free.
|
|
381
|
-
describe('accepts real mongoose subdocuments, not just object literals', () => {
|
|
382
|
-
test('projects a hydrated user.subscribedDevices the same as plain objects', async () => {
|
|
383
|
-
const monitorId = oid();
|
|
384
|
-
const siteId = oid();
|
|
385
|
-
const siteMonitorId = oid();
|
|
386
|
-
|
|
387
|
-
const user = new Users({
|
|
388
|
-
subscribedDevices: [
|
|
389
|
-
{ deviceId: monitorId, deviceType: 'monitor' },
|
|
390
|
-
{ deviceId: siteId, deviceType: 'linked-device' },
|
|
391
|
-
{ deviceId: oid(), deviceType: 'sample-site' },
|
|
392
|
-
],
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
await expect(
|
|
396
|
-
projectSubscribedMonitors(user.subscribedDevices, {
|
|
397
|
-
resolveSiteMonitorIds: async () => [siteMonitorId],
|
|
398
|
-
}),
|
|
399
|
-
).resolves.toEqual([monitorId, siteMonitorId]);
|
|
400
|
-
});
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
describe('isAqiCapableDeviceType agrees with the projection', () => {
|
|
404
|
-
test.each(DEVICE_TYPE_VALUES)('%s', async (deviceType) => {
|
|
405
|
-
const deviceId = oid();
|
|
406
|
-
const projected = await projectSubscribedMonitors(
|
|
407
|
-
[{ deviceId, deviceType }],
|
|
408
|
-
// Resolves as though the id were a Site linking exactly one monitor, so
|
|
409
|
-
// a linked-device yields an audience and a sample-site still yields none.
|
|
410
|
-
{ resolveSiteMonitorIds: async () => [oid()] },
|
|
411
|
-
);
|
|
412
|
-
|
|
413
|
-
expect(projected.length > 0).toBe(isAqiCapableDeviceType(deviceType));
|
|
414
|
-
});
|
|
415
|
-
});
|