@fonoster/sipnet 0.16.8 → 0.17.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.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Fonoster Inc
3
+ Copyright (c) 2026 Fonoster Inc
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
21
+ SOFTWARE.
@@ -30,21 +30,29 @@ exports.listNumbers = listNumbers;
30
30
  */
31
31
  const common_1 = require("@fonoster/common");
32
32
  const logger_1 = require("@fonoster/logger");
33
+ const paginationUtils_1 = require("../resources/paginationUtils");
33
34
  const convertToFonosterNumber_1 = require("./convertToFonosterNumber");
34
35
  const logger = (0, logger_1.getLogger)({ service: "sipnet", filePath: __filename });
35
36
  function listNumbers(api) {
36
37
  const fn = (call, callback) => __awaiter(this, void 0, void 0, function* () {
37
38
  const { request } = call;
38
39
  logger.verbose("call to listNumbers", Object.assign({}, request));
39
- const response = yield api.listNumbers(request);
40
40
  const accessKeyId = (0, common_1.getAccessKeyIdFromCall)(call);
41
- const items = response.items
42
- .filter((item) => item.extended.accessKeyId === accessKeyId)
43
- .map(convertToFonosterNumber_1.convertToFonosterNumber);
44
- callback(null, {
45
- items: items,
46
- nextPageToken: response.nextPageToken
41
+ const requestWithPageToken = request;
42
+ const pageSize = requestWithPageToken.pageSize || 20;
43
+ const response = yield (0, paginationUtils_1.paginateWithFiltering)({
44
+ pageSize,
45
+ pageToken: requestWithPageToken.pageToken,
46
+ fetchPage: (pageToken, fetchPageSize) => __awaiter(this, void 0, void 0, function* () {
47
+ const normalizedRequest = Object.assign(Object.assign({}, request), { pageToken, pageSize: fetchPageSize });
48
+ return yield api.listNumbers(normalizedRequest);
49
+ }),
50
+ filterItems: (items) => {
51
+ // Filter by accessKeyId and convert to Fonoster number format
52
+ return (0, paginationUtils_1.filterByAccessKeyId)(items, accessKeyId).map(convertToFonosterNumber_1.convertToFonosterNumber);
53
+ }
47
54
  });
55
+ callback(null, response);
48
56
  });
49
57
  return (0, common_1.withErrorHandlingAndValidation)(fn, common_1.Validators.listRequestSchema);
50
58
  }
@@ -30,6 +30,7 @@ exports.listResources = listResources;
30
30
  */
31
31
  const common_1 = require("@fonoster/common");
32
32
  const logger_1 = require("@fonoster/logger");
33
+ const paginationUtils_1 = require("./paginationUtils");
33
34
  const logger = (0, logger_1.getLogger)({ service: "sipnet", filePath: __filename });
34
35
  function listResources(api, resource) {
35
36
  const fn = (call, callback) => __awaiter(this, void 0, void 0, function* () {
@@ -37,12 +38,18 @@ function listResources(api, resource) {
37
38
  const res = resource === "Credentials" ? "Credential" : resource;
38
39
  logger.verbose(`call to list${res}s`, { request });
39
40
  const accessKeyId = (0, common_1.getAccessKeyIdFromCall)(call);
40
- const response = yield api[`list${res}s`](request);
41
- const items = response.items.filter((item) => { var _a; return ((_a = item.extended) === null || _a === void 0 ? void 0 : _a.accessKeyId) === accessKeyId; });
42
- callback(null, {
43
- items,
44
- nextPageToken: response.nextPageToken
41
+ const requestWithPageToken = request;
42
+ const pageSize = requestWithPageToken.pageSize || 20;
43
+ const response = yield (0, paginationUtils_1.paginateWithFiltering)({
44
+ pageSize,
45
+ pageToken: requestWithPageToken.pageToken,
46
+ fetchPage: (pageToken, fetchPageSize) => __awaiter(this, void 0, void 0, function* () {
47
+ const normalizedRequest = Object.assign(Object.assign({}, request), { pageToken, pageSize: fetchPageSize });
48
+ return yield api[`list${res}s`](normalizedRequest);
49
+ }),
50
+ filterItems: (items) => (0, paginationUtils_1.filterByAccessKeyId)(items, accessKeyId)
45
51
  });
52
+ callback(null, response);
46
53
  });
47
54
  return (0, common_1.withErrorHandlingAndValidation)(fn, common_1.Validators.listRequestSchema);
48
55
  }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
3
+ * http://github.com/fonoster/fonoster
4
+ *
5
+ * This file is part of Fonoster
6
+ *
7
+ * Licensed under the MIT License (the "License");
8
+ * you may not use this file except in compliance with
9
+ * the License. You may obtain a copy of the License at
10
+ *
11
+ * https://opensource.org/licenses/MIT
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+ type ListResponse<T> = {
20
+ items: T[];
21
+ nextPageToken?: string;
22
+ };
23
+ type PaginatedListOptions<TInput, TOutput = TInput> = {
24
+ pageSize: number;
25
+ pageToken?: string;
26
+ fetchPage: (pageToken: string | undefined, pageSize: number) => Promise<ListResponse<TInput>>;
27
+ filterItems: (items: TInput[]) => TOutput[];
28
+ maxIterations?: number;
29
+ };
30
+ /**
31
+ * Normalizes empty string pageToken to undefined.
32
+ * Empty strings are falsy in JS, but gRPC/protobuf may send them as empty strings.
33
+ */
34
+ export declare function normalizePageToken(pageToken?: string): string | undefined;
35
+ /**
36
+ * Normalizes empty string nextPageToken from response to undefined.
37
+ */
38
+ export declare function normalizeNextPageToken(nextPageToken?: string): string | undefined;
39
+ /**
40
+ * Filters items by accessKeyId from extended field.
41
+ */
42
+ export declare function filterByAccessKeyId<T extends {
43
+ extended?: {
44
+ accessKeyId?: string;
45
+ };
46
+ }>(items: T[], accessKeyId: string): T[];
47
+ /**
48
+ * Handles pagination with filtering, continuing to fetch pages until:
49
+ * - We have enough items (pageSize)
50
+ * - We run out of pages
51
+ * - We hit the max iterations limit
52
+ *
53
+ * This handles the case where the first pages contain items from other customers.
54
+ *
55
+ * @template TInput - The type of items returned from the API
56
+ * @template TOutput - The type of items after filtering/transformation (defaults to TInput)
57
+ */
58
+ export declare function paginateWithFiltering<TInput, TOutput = TInput>(options: PaginatedListOptions<TInput, TOutput>): Promise<ListResponse<TOutput>>;
59
+ export {};
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.normalizePageToken = normalizePageToken;
13
+ exports.normalizeNextPageToken = normalizeNextPageToken;
14
+ exports.filterByAccessKeyId = filterByAccessKeyId;
15
+ exports.paginateWithFiltering = paginateWithFiltering;
16
+ /**
17
+ * Normalizes empty string pageToken to undefined.
18
+ * Empty strings are falsy in JS, but gRPC/protobuf may send them as empty strings.
19
+ */
20
+ function normalizePageToken(pageToken) {
21
+ return pageToken === "" ? undefined : pageToken;
22
+ }
23
+ /**
24
+ * Normalizes empty string nextPageToken from response to undefined.
25
+ */
26
+ function normalizeNextPageToken(nextPageToken) {
27
+ return nextPageToken && nextPageToken !== "" ? nextPageToken : undefined;
28
+ }
29
+ /**
30
+ * Filters items by accessKeyId from extended field.
31
+ */
32
+ function filterByAccessKeyId(items, accessKeyId) {
33
+ return items.filter((item) => { var _a; return ((_a = item.extended) === null || _a === void 0 ? void 0 : _a.accessKeyId) === accessKeyId; });
34
+ }
35
+ /**
36
+ * Handles pagination with filtering, continuing to fetch pages until:
37
+ * - We have enough items (pageSize)
38
+ * - We run out of pages
39
+ * - We hit the max iterations limit
40
+ *
41
+ * This handles the case where the first pages contain items from other customers.
42
+ *
43
+ * @template TInput - The type of items returned from the API
44
+ * @template TOutput - The type of items after filtering/transformation (defaults to TInput)
45
+ */
46
+ function paginateWithFiltering(options) {
47
+ return __awaiter(this, void 0, void 0, function* () {
48
+ const { pageSize, pageToken, fetchPage, filterItems, maxIterations = 10 } = options;
49
+ const normalizedPageToken = normalizePageToken(pageToken);
50
+ const items = [];
51
+ let nextPageToken;
52
+ let currentPageToken = normalizedPageToken;
53
+ let hasMorePages = true;
54
+ let iterations = 0;
55
+ while (items.length < pageSize &&
56
+ hasMorePages &&
57
+ iterations < maxIterations) {
58
+ iterations++;
59
+ // Request more items to account for filtering
60
+ const backendRequestedSize = pageSize * 2;
61
+ const response = yield fetchPage(currentPageToken, backendRequestedSize);
62
+ if (!response || !response.items) {
63
+ hasMorePages = false;
64
+ nextPageToken = undefined;
65
+ break;
66
+ }
67
+ const filteredItems = filterItems(response.items);
68
+ items.push(...filteredItems);
69
+ const backendNextPageToken = normalizeNextPageToken(response.nextPageToken);
70
+ // If we got no items from backend or no nextPageToken, we're done
71
+ if (response.items.length === 0 || !backendNextPageToken) {
72
+ hasMorePages = false;
73
+ nextPageToken = undefined;
74
+ break;
75
+ }
76
+ // Check if backend returned fewer items than requested (indicating last page)
77
+ // If backend returned exactly what we asked for or more, there might be more items
78
+ const backendHasMoreItems = response.items.length >= backendRequestedSize;
79
+ // If we've filled the page, we're done
80
+ if (items.length >= pageSize) {
81
+ // We filled the page, so there might be more items
82
+ // Only return nextPageToken if backend indicates there are more items
83
+ nextPageToken = backendHasMoreItems ? backendNextPageToken : undefined;
84
+ break;
85
+ }
86
+ // If we got filtered items in this iteration, return them
87
+ // Only continue if we got 0 filtered items (meaning all items were from other customers)
88
+ if (filteredItems.length === 0) {
89
+ // No items matched in this page, continue to next page
90
+ currentPageToken = backendNextPageToken;
91
+ nextPageToken = backendNextPageToken;
92
+ }
93
+ else {
94
+ nextPageToken = backendHasMoreItems ? backendNextPageToken : undefined;
95
+ break;
96
+ }
97
+ }
98
+ return {
99
+ items: items.slice(0, pageSize),
100
+ nextPageToken
101
+ };
102
+ });
103
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fonoster/sipnet",
3
- "version": "0.16.8",
3
+ "version": "0.17.0",
4
4
  "description": "Routr-based SIP stack",
5
5
  "author": "Pedro Sanders <psanders@fonoster.com>",
6
6
  "homepage": "https://github.com/fonoster/fonoster#readme",
@@ -20,10 +20,10 @@
20
20
  "fonoster": "./dist/index.js"
21
21
  },
22
22
  "dependencies": {
23
- "@fonoster/common": "^0.16.8",
24
- "@fonoster/identity": "^0.16.8",
25
- "@fonoster/logger": "^0.16.7",
26
- "@fonoster/types": "^0.16.7",
23
+ "@fonoster/common": "^0.17.0",
24
+ "@fonoster/identity": "^0.17.0",
25
+ "@fonoster/logger": "^0.17.0",
26
+ "@fonoster/types": "^0.17.0",
27
27
  "@grpc/grpc-js": "~1.10.6",
28
28
  "@routr/sdk": "2.13.1",
29
29
  "zod": "^3.23.8"
@@ -41,5 +41,5 @@
41
41
  "bugs": {
42
42
  "url": "https://github.com/fonoster/fonoster/issues"
43
43
  },
44
- "gitHead": "5972440bc8106a5db4b3536129b60d1df30fe6b3"
44
+ "gitHead": "4d1a9afaec6f294184386e009d1a4e292fb3583b"
45
45
  }