@dszp/netsapiens-lib 0.3.1 → 0.5.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.
@@ -168,6 +168,52 @@ const ok = (c: boolean, msg: string) => {
168
168
  ok(noDevicesSnap.deviceReadFailures === undefined, 'without includeDevices, deviceReadFailures is absent entirely');
169
169
  }
170
170
 
171
+ // -- per-user SMS numbers: one call per REAL extension, opt-in, failures named like device reads --
172
+ {
173
+ const seen: string[] = [];
174
+ const client = { get: async (p: string) => {
175
+ seen.push(p);
176
+ if (p.endsWith('/users')) return [{ user: '100', 'service-code': '' }, { user: '101', 'service-code': '' }, { user: '700', 'service-code': 'system-aa' }];
177
+ if (p.endsWith('/users/100/smsnumbers')) return [{ number: '13175550100' }];
178
+ if (p.endsWith('/users/101/smsnumbers')) throw new NsApiError('GET .../smsnumbers → 500', 500, p, null);
179
+ if (/\/domains\/[^/]+$/.test(p)) return [{ domain: 'acme.example' }];
180
+ return [];
181
+ } } as unknown as NsClient;
182
+ const snap = await fetchDomainSnapshot(client, 'acme.example', { includeAttendantMenus: false, includeUserSmsNumbers: true });
183
+ ok(JSON.stringify(snap.smsNumbersByUser) === JSON.stringify({ '100': [{ number: '13175550100' }] }), 'per-user SMS numbers are filed under the extension');
184
+ ok(JSON.stringify(snap.smsReadFailures) === JSON.stringify(['101']), 'a failed per-user SMS read is named');
185
+ ok(!seen.some((p) => p.endsWith('/users/700/smsnumbers')), 'system users are not read');
186
+ const plain = await fetchDomainSnapshot(client, 'acme.example', { includeAttendantMenus: false });
187
+ ok(plain.smsNumbersByUser === undefined && plain.smsReadFailures === undefined, 'without the option neither field is present');
188
+
189
+ // PRESENT AND EMPTY when every read answered — the same contract deviceReadFailures has, and the
190
+ // distinction a consumer needs: absent means nobody asked, empty means nothing failed. An absent
191
+ // field read as "nothing failed" would be a page claiming a clean read it never performed.
192
+ const allOk = { get: async (p: string) => {
193
+ if (p.endsWith('/users')) return [{ user: '100', 'service-code': '' }];
194
+ if (p.endsWith('/users/100/smsnumbers')) return [{ number: '13175550100' }];
195
+ if (/\/domains\/[^/]+$/.test(p)) return [{ domain: 'acme.example' }];
196
+ return [];
197
+ } } as unknown as NsClient;
198
+ const clean = await fetchDomainSnapshot(allOk, 'acme.example', { includeAttendantMenus: false, includeUserSmsNumbers: true });
199
+ ok(Array.isArray(clean.smsReadFailures) && clean.smsReadFailures.length === 0,
200
+ 'no failures - smsReadFailures is an empty array, not absent');
201
+
202
+ // SORTED, not in completion order. These fill under mapLimit, so the fixture makes the FIRST-sorting
203
+ // extension answer last; an unsorted list would come back ['101','100'] and the panel that prints it
204
+ // would reshuffle between two reads of the same broken domain.
205
+ const twoFails = { get: async (p: string) => {
206
+ if (p.endsWith('/users')) return [{ user: '100', 'service-code': '' }, { user: '101', 'service-code': '' }];
207
+ if (p.endsWith('/users/100/smsnumbers')) { await new Promise((r) => setTimeout(r, 10)); throw new NsApiError('GET .../smsnumbers → 500', 500, p, null); }
208
+ if (p.endsWith('/users/101/smsnumbers')) throw new NsApiError('GET .../smsnumbers → 500', 500, p, null);
209
+ if (/\/domains\/[^/]+$/.test(p)) return [{ domain: 'acme.example' }];
210
+ return [];
211
+ } } as unknown as NsClient;
212
+ const sorted = await fetchDomainSnapshot(twoFails, 'acme.example', { includeAttendantMenus: false, includeUserSmsNumbers: true });
213
+ ok(JSON.stringify(sorted.smsReadFailures) === JSON.stringify(['100', '101']),
214
+ 'two failures come back sorted, whichever order they completed in');
215
+ }
216
+
171
217
  console.log(`\n${pass} passed, ${fail} failed`);
172
218
  process.exit(fail ? 1 : 0);
173
219
  })();
package/src/nsClient.ts CHANGED
@@ -183,6 +183,12 @@ export interface FetchSnapshotOptions {
183
183
  * auto attendants and queues, and they hold no seat.
184
184
  */
185
185
  includeDevices?: boolean;
186
+ /**
187
+ * Also read each REAL extension's SMS-enabled numbers into `smsNumbersByUser`. One call per
188
+ * extension, like `includeDevices`, and for the same reason: the domain-level list does not say
189
+ * which user a number belongs to, and a consumer splitting a domain by site needs to know.
190
+ */
191
+ includeUserSmsNumbers?: boolean;
186
192
  }
187
193
 
188
194
  /**
@@ -258,6 +264,30 @@ export async function fetchDomainSnapshot(client: NsClient, domain: string, opts
258
264
  });
259
265
  if (devs.length) devicesByUser![ext] = devs;
260
266
  });
267
+ // SORTED before it leaves. These fill under `mapLimit`, so the order is whichever read failed first —
268
+ // a list that reshuffles between two reads of the same broken domain reads as data that changed, and a
269
+ // consumer prints this straight at an operator.
270
+ deviceReadFailures.sort();
271
+ }
272
+
273
+ // Per-user SMS numbers, same shape and reason as devices above: the domain-level list (see
274
+ // includeSmsNumbers) does not say which user a number belongs to.
275
+ let smsNumbersByUser: Record<string, Rec[]> | undefined;
276
+ let smsReadFailures: string[] | undefined;
277
+ if (opts.includeUserSmsNumbers) {
278
+ smsNumbersByUser = {};
279
+ smsReadFailures = [];
280
+ const seats = users.filter((u) => !String(u['service-code'] ?? '').trim().toLowerCase().startsWith('system-'));
281
+ await mapLimit(seats, conc, async (u) => {
282
+ const ext = String(u.user ?? '');
283
+ if (!ext) return;
284
+ const list = await soft(`${base}/users/${enc(ext)}/smsnumbers`).catch(() => {
285
+ smsReadFailures!.push(ext);
286
+ return [];
287
+ });
288
+ if (list.length) smsNumbersByUser![ext] = list;
289
+ });
290
+ smsReadFailures.sort(); // same reason as deviceReadFailures above
261
291
  }
262
292
 
263
293
  const answerrulesByUser: Record<string, Rec[]> = {};
@@ -316,6 +346,8 @@ export async function fetchDomainSnapshot(client: NsClient, domain: string, opts
316
346
  ...(smsnumbers ? { smsnumbers } : {}),
317
347
  ...(devicesByUser ? { devicesByUser } : {}),
318
348
  ...(deviceReadFailures ? { deviceReadFailures } : {}),
349
+ ...(smsNumbersByUser ? { smsNumbersByUser } : {}),
350
+ ...(smsReadFailures ? { smsReadFailures } : {}),
319
351
  ...(attendantDetailsByUser && Object.keys(attendantDetailsByUser).length ? { attendantDetailsByUser } : {}),
320
352
  ...(attendantDialrulesByExt && Object.keys(attendantDialrulesByExt).length ? { attendantDialrulesByExt } : {}),
321
353
  ...(dialrulesByPlan ? { dialrulesByPlan } : {}),