Pagination fixes (#2347)

* `offset` -> `pageOffset`, `limit`-> `pageLimit`

* Be backward compatible in API

* Cleanup types and handle pagination qs

* Cleanup unused code

* Import type

* Cleanup and fix lint error

* Align params for sso-tracer

* Move parsing to a common util function

* pageLimit shouldn't be optional

* Cap pageLimit to max value, split the boolean

* Revert typings and assert non null

* Refactor var name

* Use util function to normalize pagination params across getAll and getByIndex

* Normalize offset/limit for dynamo/mongo

* Update query params in `FederatedSAMLApps`

* Cap to max limit if passed limit is 0

* Sync lock file

* Add a 3rd record and supply opts.pageLimit

* Normalize offset/limit for mem/redis

* Save the 3rd record in the store

* Fix getAll tests

* Give precedence to standard params over legacy

* Use util function

* Parse using util function

* Refactor

* Standardise pagination for `api/v1/dsync/events`

* Standardise pagination for api/admin/connections

* Standardise pagination for api/admin/directory-sync

* Standardise pagination for `api/v1/dsync/groups`

* Standardise pagination for `v1/dsync/users`, `v1/dsync/product`

* Standardise pagination in fetchByProduct APIs

* Update swagger for groups

* Fix pagination params definition, add the params for users api

* More swagger updates

* Swagger spec update for dsync events

* Add pagination params to apis fetching by product

* Update qs in internal-ui

* Remove type assertion

* [Swagger WIP] Fix response format for paginated APIs

* Add dsync events to swagger spec

* Fix swagger spec for sso tracer

* Fix swagger spec for federated-saml apps of a product

* Update pageLimit to 50

* Use pageLimit value from internal-ui

* Update UI SDK

* Cleanup local pagination component

* Update swagger version

* Remove unused keys from locale

* Fix tag for trace api spec

* Fix param name for swagger

* Fix swagger tag for trace

* updated package-lock

* updated package-lock

---------

Co-authored-by: Deepak Prabhakara <deepak@boxyhq.com>
This commit is contained in:
Aswin V 2024-03-07 01:44:14 +05:30 committed by GitHub
parent 06c7d38b37
commit 1188dd6396
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 806 additions and 344 deletions

View File

@ -1,44 +0,0 @@
import ArrowLeftIcon from '@heroicons/react/24/outline/ArrowLeftIcon';
import ArrowRightIcon from '@heroicons/react/24/outline/ArrowRightIcon';
import { useTranslation } from 'next-i18next';
import { ButtonOutline } from './ButtonOutline';
type PaginationProps = {
itemsCount: number;
offset: number;
onPrevClick: () => void;
onNextClick: () => void;
};
export const pageLimit = 15;
export const Pagination = ({ itemsCount, offset, onPrevClick, onNextClick }: PaginationProps) => {
const { t } = useTranslation('common');
// Hide pagination if there are no items to paginate.
if ((itemsCount === 0 && offset === 0) || (itemsCount < pageLimit && offset === 0)) {
return null;
}
const prevDisabled = offset === 0;
const nextDisabled = itemsCount < pageLimit || itemsCount === 0;
return (
<div className='flex justify-center space-x-4 py-4'>
<ButtonOutline
Icon={ArrowLeftIcon}
aria-label={t('previous')}
onClick={onPrevClick}
disabled={prevDisabled}>
{t('prev')}
</ButtonOutline>
<ButtonOutline
Icon={ArrowRightIcon}
aria-label={t('previous')}
onClick={onNextClick}
disabled={nextDisabled}>
{t('next')}
</ButtonOutline>
</div>
);
};

View File

@ -3,8 +3,8 @@ import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
import { LinkPrimary } from '@components/LinkPrimary';
import { InputWithCopyButton } from '@components/ClipboardButton';
import { pageLimit } from '@components/Pagination';
import { ConnectionList } from '@boxyhq/react-ui/sso';
import { pageLimit } from '@boxyhq/internal-ui';
const SSOConnectionList = ({
setupLinkToken,

View File

@ -2,8 +2,8 @@ import LinkIcon from '@heroicons/react/24/outline/LinkIcon';
import { useTranslation } from 'next-i18next';
import { LinkPrimary } from '@components/LinkPrimary';
import { useRouter } from 'next/router';
import { pageLimit } from '@components/Pagination';
import { DirectoryList } from '@boxyhq/react-ui/dsync';
import { pageLimit } from '@boxyhq/internal-ui';
const DSyncDirectoryList = ({ setupLinkToken }: { setupLinkToken?: string }) => {
const { t } = useTranslation('common');

View File

@ -1,6 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import jackson from '@lib/jackson';
import { parsePaginateApiParams } from '@lib/utils';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { method } = req;
@ -35,12 +36,13 @@ const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
const { samlFederatedController } = await jackson();
const { offset, limit, pageToken } = req.query as { offset: string; limit: string; pageToken?: string };
const { pageOffset, pageLimit, pageToken } = parsePaginateApiParams(req.query);
const pageOffset = parseInt(offset);
const pageLimit = parseInt(limit);
const apps = await samlFederatedController.app.getAll({ pageOffset, pageLimit, pageToken });
const apps = await samlFederatedController.app.getAll({
pageOffset,
pageLimit,
pageToken,
});
if (apps.pageToken) {
res.setHeader('jackson-pagetoken', apps.pageToken);

View File

@ -1,6 +1,7 @@
import { NextApiRequest, NextApiResponse } from 'next';
import jackson from '@lib/jackson';
import { parsePaginateApiParams } from '@lib/utils';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
@ -22,17 +23,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
const { samlFederatedController } = await jackson();
const { product, pageOffset, pageLimit, pageToken } = req.query as {
const { product } = req.query as {
product: string;
pageOffset: string;
pageLimit: string;
pageToken?: string;
};
const { pageOffset, pageLimit, pageToken } = parsePaginateApiParams(req.query);
const apps = await samlFederatedController.app.getByProduct({
product,
pageOffset: parseInt(pageOffset),
pageLimit: parseInt(pageLimit),
pageOffset,
pageLimit,
pageToken,
});

View File

@ -21,8 +21,8 @@ export const DirectoryGroups = ({
const { paginate, setPaginate, pageTokenMap } = usePaginate(router!);
const params = {
offset: paginate.offset,
limit: pageLimit,
pageOffset: paginate.offset,
pageLimit,
};
// For DynamoDB

View File

@ -21,8 +21,8 @@ export const DirectoryUsers = ({
const { paginate, setPaginate, pageTokenMap } = usePaginate(router!);
const params = {
offset: paginate.offset,
limit: pageLimit,
pageOffset: paginate.offset,
pageLimit,
};
// For DynamoDB

View File

@ -35,8 +35,8 @@ export const DirectoryWebhookLogs = ({
const { paginate, setPaginate, pageTokenMap } = usePaginate(router!);
const params = {
offset: paginate.offset,
limit: pageLimit,
pageOffset: paginate.offset,
pageLimit,
};
// For DynamoDB

View File

@ -37,7 +37,7 @@ export const FederatedSAMLApps = ({
const { t } = useTranslation('common');
const { paginate, setPaginate, pageTokenMap } = usePaginate(router!);
let getAppsUrl = `${urls.getApps}?offset=${paginate.offset}&limit=${pageLimit}`;
let getAppsUrl = `${urls.getApps}?pageOffset=${paginate.offset}&pageLimit=${pageLimit}`;
// For DynamoDB
if (paginate.offset > 0 && pageTokenMap[paginate.offset - pageLimit]) {

View File

@ -3,7 +3,7 @@ import ArrowLeftIcon from '@heroicons/react/24/outline/ArrowLeftIcon';
import ArrowRightIcon from '@heroicons/react/24/outline/ArrowRightIcon';
import { ButtonOutline } from './ButtonOutline';
export const pageLimit = 15;
export const pageLimit = 50;
export const Pagination = ({
itemsCount,

View File

@ -20,8 +20,8 @@ export const SSOTracers = ({
const { paginate, setPaginate, pageTokenMap, setPageTokenMap } = usePaginate(router!);
const params = {
offset: paginate.offset,
limit: pageLimit,
pageOffset: paginate.offset,
pageLimit,
};
// For DynamoDB

View File

@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import micromatch from 'micromatch';
import type { OIDCSSOConnectionWithDiscoveryUrl, OIDCSSOConnectionWithMetadata } from '@boxyhq/saml-jackson';
import { JacksonError } from 'npm/src/controller/error';
import type { PaginateApiParams } from 'types';
export const validateEmailWithACL = (email: string) => {
const NEXTAUTH_ACL = process.env.NEXTAUTH_ACL || undefined;
@ -73,3 +74,29 @@ export const oidcMetadataParse = (
}
return body;
};
export const parsePaginateApiParams = (params: NextApiRequest['query']): PaginateApiParams => {
let pageOffset, pageLimit;
if ('pageOffset' in params) {
pageOffset = params.pageOffset;
} else if ('offset' in params) {
pageOffset = params.offset;
}
if ('pageLimit' in params) {
pageLimit = params.pageLimit;
} else if ('limit' in params) {
pageLimit = params.limit;
}
pageOffset = parseInt(pageOffset);
pageLimit = parseInt(pageLimit);
const pageToken = params.pageToken as string;
return {
pageOffset,
pageLimit,
pageToken,
};
};

View File

@ -30,15 +30,12 @@
"name": "Name",
"new_directory": "New Directory",
"new_setup_link": "New Setup Link",
"next": "Next",
"connections": "Connections",
"new_connection": "New Connection",
"no_projects_found": "No projects found.",
"oidc": "OIDC",
"open_menu": "Open menu",
"open_sidebar": "Open sidebar",
"prev": "Prev",
"previous": "Previous",
"product": "Product",
"save_changes": "Save Changes",
"saved": "Saved",

View File

@ -806,12 +806,20 @@ export class ConnectionAPIController implements IConnectionAPIController {
* type: object
* description: OIDC IdP metadata
* responses:
* '200Get':
* '200GetByProduct':
* description: Success
* schema:
* type: array
* items:
* $ref: '#/definitions/Connection'
* content:
* application/json:
* schema:
* type: object
* properties:
* data:
* type: array
* items:
* $ref: '#/definitions/Connection'
* pageToken:
* type: string
* description: token for pagination
* '400Get':
* description: Please provide a `product`.
* '401Get':
@ -821,11 +829,14 @@ export class ConnectionAPIController implements IConnectionAPIController {
* summary: Get SSO Connections by product
* parameters:
* - $ref: '#/parameters/productParamGet'
* - $ref: '#/parameters/pageOffset'
* - $ref: '#/parameters/pageLimit'
* - $ref: '#/parameters/pageToken'
* operationId: get-connections-by-product
* tags: [Single Sign On]
* responses:
* '200':
* $ref: '#/responses/200Get'
* $ref: '#/responses/200GetByProduct'
* '400':
* $ref: '#/responses/400Get'
* '401':

View File

@ -393,6 +393,9 @@ export class SetupLinkController {
* summary: Get the Setup Links by product
* parameters:
* - $ref: '#/parameters/productParamGet'
* - $ref: '#/parameters/pageOffset'
* - $ref: '#/parameters/pageLimit'
* - $ref: '#/parameters/pageToken'
* operationId: get-sso-setup-link-by-product
* tags: [Setup Links | Single Sign On]
* responses:
@ -407,6 +410,9 @@ export class SetupLinkController {
* summary: Get the Setup Links by product
* parameters:
* - $ref: '#/parameters/productParamGet'
* - $ref: '#/parameters/pageOffset'
* - $ref: '#/parameters/pageLimit'
* - $ref: '#/parameters/pageToken'
* operationId: get-dsync-setup-link-by-product
* tags: [Setup Links | Directory Sync]
* responses:

View File

@ -183,6 +183,10 @@ class DynamoDB implements DatabaseDriver {
}
async getAll(namespace: string, _?: number, pageLimit?: number, pageToken?: string): Promise<Records> {
const { limit: Limit } = dbutils.normalizeOffsetAndLimit({
pageLimit,
maxLimit: this.options.pageLimit!,
});
const res = await this.client.send(
new QueryCommand({
KeyConditionExpression: 'namespace = :namespace',
@ -190,7 +194,7 @@ class DynamoDB implements DatabaseDriver {
':namespace': { S: namespace },
},
TableName: tableName,
Limit: pageLimit && pageLimit > 0 ? pageLimit : undefined,
Limit,
ExclusiveStartKey: pageToken ? JSON.parse(Buffer.from(pageToken, 'base64').toString()) : undefined,
})
);

View File

@ -59,14 +59,18 @@ class Mem implements DatabaseDriver {
_?: string,
sortOrder?: SortOrder
): Promise<Records> {
const offsetAndLimitValueCheck = !dbutils.isNumeric(pageOffset) && !dbutils.isNumeric(pageLimit);
const returnValue: string[] = [];
const skip = Number(offsetAndLimitValueCheck ? 0 : pageOffset);
let take = Number(offsetAndLimitValueCheck ? this.options.pageLimit : pageLimit);
const { offset: skip, limit } = dbutils.normalizeOffsetAndLimit({
pageOffset,
pageLimit,
maxLimit: this.options.pageLimit!,
});
let take = limit;
let count = 0;
take += skip;
take! += skip!;
if (namespace) {
const index = dbutils.keyFromParts(dbutils.createdAtPrefix, namespace);
@ -79,11 +83,11 @@ class Mem implements DatabaseDriver {
const iterator: IterableIterator<string> = sortOrder === 'ASC' ? val.values() : val.reverse().values();
for (const value of iterator) {
if (count >= take) {
if (count >= take!) {
break;
}
if (count >= skip) {
if (count >= skip!) {
returnValue.push(this.store[dbutils.keyFromParts(namespace, value)]);
}
@ -103,30 +107,30 @@ class Mem implements DatabaseDriver {
_?: string,
sortOrder?: SortOrder
): Promise<Records> {
const offsetAndLimitValueCheck = !dbutils.isNumeric(pageOffset) && !dbutils.isNumeric(pageLimit);
const skip = Number(offsetAndLimitValueCheck ? 0 : pageOffset);
const { offset: skip, limit } = dbutils.normalizeOffsetAndLimit({
pageOffset,
pageLimit,
maxLimit: this.options.pageLimit!,
});
let take = limit;
let take = Number(offsetAndLimitValueCheck ? this.options.pageLimit : pageLimit);
let count = 0;
take += skip;
take! += skip!;
const dbKeys = Array.from((await this.indexes[dbutils.keyForIndex(namespace, idx)]) || []) as string[];
const iterator: IterableIterator<string> =
sortOrder === 'ASC' ? dbKeys.values() : dbKeys.reverse().values();
const ret: string[] = [];
for (const dbKey of iterator || []) {
if (offsetAndLimitValueCheck) {
ret.push(await this.get(namespace, dbKey));
} else {
if (count >= take) {
break;
}
if (count >= skip) {
ret.push(await this.get(namespace, dbKey));
}
count++;
if (count >= take!) {
break;
}
if (count >= skip!) {
ret.push(await this.get(namespace, dbKey));
}
count++;
}
return { data: ret };

View File

@ -86,10 +86,20 @@ class Mongo implements DatabaseDriver {
_?: string,
sortOrder?: SortOrder
): Promise<Records> {
const { offset: skip, limit } = dbutils.normalizeOffsetAndLimit({
pageOffset,
pageLimit,
maxLimit: this.options.pageLimit!,
});
const docs = await this.collection
.find(
{ namespace: namespace },
{ sort: { createdAt: sortOrder === 'ASC' ? 1 : -1 }, skip: pageOffset, limit: pageLimit }
{
sort: { createdAt: sortOrder === 'ASC' ? 1 : -1 },
skip,
limit,
}
)
.toArray();
@ -103,31 +113,31 @@ class Mongo implements DatabaseDriver {
async getByIndex(
namespace: string,
idx: Index,
offset?: number,
limit?: number,
pageOffset?: number,
pageLimit?: number,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_?: string,
sortOrder?: SortOrder
): Promise<Records> {
const sort: Sort = { createdAt: sortOrder === 'ASC' ? 'asc' : 'desc' };
const docs =
dbutils.isNumeric(offset) && dbutils.isNumeric(limit)
? await this.collection
.find(
{
indexes: dbutils.keyForIndex(namespace, idx),
},
{ sort, skip: offset, limit: limit }
)
.toArray()
: await this.collection
.find(
{
indexes: dbutils.keyForIndex(namespace, idx),
},
{ sort }
)
.toArray();
const { offset: skip, limit } = dbutils.normalizeOffsetAndLimit({
pageOffset,
pageLimit,
maxLimit: this.options.pageLimit!,
});
const docs = await this.collection
.find(
{
indexes: dbutils.keyForIndex(namespace, idx),
},
{
sort,
skip,
limit,
}
)
.toArray();
const ret: string[] = [];
for (const doc of docs || []) {

View File

@ -44,10 +44,14 @@ class Redis implements DatabaseDriver {
_?: string,
sortOrder?: SortOrder
): Promise<Records> {
const offset = pageOffset ? Number(pageOffset) : 0;
const count = pageLimit ? Number(pageLimit) : this.options.pageLimit;
const sortedSetKey = dbutils.keyFromParts(dbutils.createdAtPrefix, namespace);
const { offset, limit: count } = dbutils.normalizeOffsetAndLimit({
pageOffset,
pageLimit,
maxLimit: this.options.pageLimit!,
});
const zRangeOptions = {
BY: 'SCORE',
REV: sortOrder === 'ASC',
@ -92,49 +96,43 @@ class Redis implements DatabaseDriver {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_?: string
): Promise<Records> {
const offsetAndLimitValueCheck = !dbutils.isNumeric(pageOffset) && !dbutils.isNumeric(pageLimit);
let take = Number(offsetAndLimitValueCheck ? this.options.pageLimit : pageLimit);
const skip = Number(offsetAndLimitValueCheck ? 0 : pageOffset);
const { offset: skip, limit } = dbutils.normalizeOffsetAndLimit({
pageOffset,
pageLimit,
maxLimit: this.options.pageLimit!,
});
let take = limit;
let count = 0;
take += skip;
take! += skip!;
const returnValue: string[] = [];
const keyArray: string[] = [];
const idxKey = dbutils.keyForIndex(namespace, idx);
const dbKeys = await this.client.sMembers(dbutils.keyFromParts(dbutils.indexPrefix, idxKey));
if (!offsetAndLimitValueCheck) {
for await (const { value } of this.client.zScanIterator(
dbutils.keyFromParts(dbutils.createdAtPrefix, namespace),
count + 1
)) {
if (dbKeys.indexOf(value) !== -1) {
if (count >= take) {
break;
}
if (count >= skip) {
keyArray.push(dbutils.keyFromParts(namespace, value));
}
count++;
for await (const { value } of this.client.zScanIterator(
dbutils.keyFromParts(dbutils.createdAtPrefix, namespace),
count + 1
)) {
if (dbKeys.indexOf(value) !== -1) {
if (count >= take!) {
break;
}
}
if (keyArray.length > 0) {
const value = await this.client.MGET(keyArray);
for (let i = 0; i < value.length; i++) {
const valueObject = JSON.parse(value[i].toString());
if (valueObject !== null && valueObject !== '') {
returnValue.push(valueObject);
}
if (count >= skip!) {
keyArray.push(dbutils.keyFromParts(namespace, value));
}
count++;
}
return { data: returnValue || [] };
} else {
const ret: string[] = [];
for (const dbKey of dbKeys || []) {
if (offsetAndLimitValueCheck) {
ret.push(await this.get(namespace, dbKey));
}
}
return { data: ret };
}
if (keyArray.length > 0) {
const value = await this.client.MGET(keyArray);
for (let i = 0; i < value.length; i++) {
const valueObject = JSON.parse(value[i].toString());
if (valueObject !== null && valueObject !== '') {
returnValue.push(valueObject);
}
}
}
return { data: returnValue || [] };
}
async put(namespace: string, key: string, val: Encrypted, ttl = 0, ...indexes: any[]): Promise<void> {

View File

@ -171,7 +171,11 @@ class Sql implements DatabaseDriver {
_?: string,
sortOrder?: SortOrder
): Promise<Records> {
const skipOffsetAndLimitValue = !dbutils.isNumeric(pageOffset) && !dbutils.isNumeric(pageLimit);
const { offset: skip, limit: take } = dbutils.normalizeOffsetAndLimit({
pageOffset,
pageLimit,
maxLimit: this.options.pageLimit!,
});
const res = await this.storeRepository.find({
where: { namespace: namespace },
@ -179,8 +183,8 @@ class Sql implements DatabaseDriver {
order: {
['createdAt']: sortOrder || 'DESC',
},
take: skipOffsetAndLimitValue ? this.options.pageLimit : pageLimit,
skip: skipOffsetAndLimitValue ? 0 : pageOffset,
take,
skip,
});
return { data: res || [] };
@ -195,21 +199,20 @@ class Sql implements DatabaseDriver {
_?: string,
sortOrder?: SortOrder
): Promise<Records> {
const skipOffsetAndLimitValue = !dbutils.isNumeric(pageOffset) && !dbutils.isNumeric(pageLimit);
const { offset: skip, limit: take } = dbutils.normalizeOffsetAndLimit({
pageOffset,
pageLimit,
maxLimit: this.options.pageLimit!,
});
const sort = {
id: sortOrder || 'DESC',
};
const res = skipOffsetAndLimitValue
? await this.indexRepository.find({
where: { key: dbutils.keyForIndex(namespace, idx) },
order: sort,
})
: await this.indexRepository.find({
where: { key: dbutils.keyForIndex(namespace, idx) },
take: skipOffsetAndLimitValue ? this.options.pageLimit : pageLimit,
skip: skipOffsetAndLimitValue ? 0 : pageOffset,
order: sort,
});
const res = await this.indexRepository.find({
where: { key: dbutils.keyForIndex(namespace, idx) },
take,
skip,
order: sort,
});
const ret: Encrypted[] = [];
if (res) {

View File

@ -23,6 +23,22 @@ export const sleep = (ms: number): Promise<void> => {
export function isNumeric(num) {
return !isNaN(num);
}
export const normalizeOffsetAndLimit = ({
pageLimit,
pageOffset,
maxLimit,
}: {
pageOffset?: number;
pageLimit?: number;
maxLimit: number;
}) => {
const skipOffset = pageOffset === undefined || !isNumeric(pageOffset);
// maxLimit capped to 50 by default unless set from env (db.options.pageLimit)
const capToMaxLimit =
pageLimit === undefined || pageLimit === 0 || !isNumeric(pageLimit) || pageLimit > maxLimit;
return { offset: skipOffset ? 0 : pageOffset, limit: capToMaxLimit ? maxLimit : pageLimit };
};
export const indexPrefix = '_index';
export const createdAtPrefix = '_createdAt';
export const modifiedAtPrefix = '_modifiedAt';

View File

@ -133,6 +133,24 @@ export class DirectoryConfig {
* in: query
* required: false
* type: string
* pageOffset:
* name: pageOffset
* description: Starting point from which the set of records are retrieved
* in: query
* required: false
* type: string
* pageLimit:
* name: pageLimit
* description: Number of records to be fetched for the page
* in: query
* required: false
* type: string
* pageToken:
* name: pageToken
* description: Token used for DynamoDB pagination
* in: query
* required: false
* type: string
*/
/**
@ -602,6 +620,9 @@ export class DirectoryConfig {
* summary: Get directory connections by product
* parameters:
* - $ref: '#/parameters/product'
* - $ref: '#/parameters/pageOffset'
* - $ref: '#/parameters/pageLimit'
* - $ref: '#/parameters/pageToken'
* tags:
* - Directory Sync
* produces:
@ -609,10 +630,18 @@ export class DirectoryConfig {
* responses:
* '200':
* description: Success
* schema:
* type: array
* items:
* $ref: '#/definitions/Directory'
* content:
* application/json:
* schema:
* type: object
* properties:
* data:
* type: array
* items:
* $ref: '#/definitions/Directory'
* pageToken:
* type: string
* description: token for pagination
*/
public async filterBy(
params: FilterByParams = {}

View File

@ -207,6 +207,9 @@ export class Groups extends Base {
* - $ref: '#/parameters/tenant'
* - $ref: '#/parameters/product'
* - $ref: '#/parameters/directoryId'
* - $ref: '#/parameters/pageOffset'
* - $ref: '#/parameters/pageLimit'
* - $ref: '#/parameters/pageToken'
* tags:
* - Directory Sync
* produces:
@ -214,10 +217,18 @@ export class Groups extends Base {
* responses:
* 200:
* description: Success
* schema:
* type: array
* items:
* $ref: '#/definitions/Group'
* content:
* application/json:
* schema:
* type: object
* properties:
* data:
* type: array
* items:
* $ref: '#/definitions/Group'
* pageToken:
* type: string
* description: token for pagination
*/
public async getAll(
params: PaginationParams & {

View File

@ -197,6 +197,9 @@ export class Users extends Base {
* - $ref: '#/parameters/tenant'
* - $ref: '#/parameters/product'
* - $ref: '#/parameters/directoryId'
* - $ref: '#/parameters/pageOffset'
* - $ref: '#/parameters/pageLimit'
* - $ref: '#/parameters/pageToken'
* tags:
* - Directory Sync
* produces:
@ -204,10 +207,18 @@ export class Users extends Base {
* responses:
* 200:
* description: Success
* schema:
* type: array
* items:
* $ref: '#/definitions/User'
* content:
* application/json:
* schema:
* type: object
* properties:
* data:
* type: array
* items:
* $ref: '#/definitions/User'
* pageToken:
* type: string
* description: token for pagination
*/
public async getAll({
pageOffset,

View File

@ -14,6 +14,55 @@ type GetAllParams = PaginationParams & {
directoryId?: string;
};
/**
* @swagger
* definitions:
* Event:
* type: object
* example:
* {
* "id": "id1",
* "webhook_endpoint": "https://example.com/webhook",
* "created_at": "2024-03-05T17:06:26.074Z",
* "status_code": 200,
* "delivered": true,
* "payload": {
* "directory_id": "58b5cd9dfaa39d47eb8f5f88631f9a629a232016",
* "event": "user.created",
* "tenant": "boxyhq",
* "product": "jackson",
* "data": {
* "id": "038e767b-9bc6-4dbd-975e-fbc38a8e7d82",
* "first_name": "Deepak",
* "last_name": "Prabhakara",
* "email": "deepak@boxyhq.com",
* "active": true,
* "raw": {
* "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
* "userName": "deepak@boxyhq.com",
* "name": {
* "givenName": "Deepak",
* "familyName": "Prabhakara"
* },
* "emails": [
* {
* "primary": true,
* "value": "deepak@boxyhq.com",
* "type": "work"
* }
* ],
* "title": "CEO",
* "displayName": "Deepak Prabhakara",
* "locale": "en-US",
* "externalId": "00u1ldzzogFkXFmvT5d7",
* "groups": [],
* "active": true,
* "id": "038e767b-9bc6-4dbd-975e-fbc38a8e7d82"
* }
* }
* }
* }
*/
export class WebhookEventsLogger extends Base {
constructor({ db }: { db: DatabaseStore }) {
super({ db });
@ -43,6 +92,36 @@ export class WebhookEventsLogger extends Base {
return await this.eventStore().get(id);
}
/**
* @swagger
* /api/v1/dsync/events:
* get:
* summary: Get event logs for a directory
* parameters:
* - $ref: '#/parameters/directoryId'
* - $ref: '#/parameters/pageOffset'
* - $ref: '#/parameters/pageLimit'
* - $ref: '#/parameters/pageToken'
* tags:
* - Directory Sync
* produces:
* - application/json
* responses:
* 200:
* description: Success
* content:
* application/json:
* schema:
* type: object
* properties:
* data:
* type: array
* items:
* $ref: '#/definitions/Event'
* pageToken:
* type: string
* description: token for pagination
*/
// Get the event logs for a directory paginated
public async getAll(params: GetAllParams = {}) {
const { pageOffset, pageLimit, directoryId } = params;

View File

@ -325,6 +325,9 @@ export class App {
* in: query
* required: true
* type: string
* - $ref: '#/parameters/pageOffset'
* - $ref: '#/parameters/pageLimit'
* - $ref: '#/parameters/pageToken'
* tags:
* - Identity Federation
* produces:
@ -332,10 +335,18 @@ export class App {
* responses:
* 200:
* description: Success
* schema:
* type: array
* items:
* $ref: '#/definitions/SAMLFederationApp'
* content:
* application/json:
* schema:
* type: object
* properties:
* data:
* type: array
* items:
* $ref: '#/definitions/SAMLFederationApp'
* pageToken:
* type: string
* description: token for pagination
*/
public async getByProduct({ product, pageOffset, pageLimit, pageToken }: GetByProductParams) {
await throwIfInvalidLicense(this.opts.boxyhqLicenseKey);

View File

@ -113,7 +113,7 @@ class SSOTracer {
* required: true
* type: string
* tags:
* - SAML Traces
* - SSO Traces
* produces:
* - application/json
* responses:
@ -164,17 +164,28 @@ class SSOTracer {
* summary: Get all traces for a product
* parameters:
* - $ref: '#/parameters/product'
* - $ref: '#/parameters/pageOffset'
* - $ref: '#/parameters/pageLimit'
* - $ref: '#/parameters/pageToken'
* tags:
* - SAML Traces
* - SSO Traces
* produces:
* - application/json
* responses:
* '200':
* description: Success
* schema:
* type: array
* items:
* $ref: '#/definitions/SSOTrace'
* content:
* application/json:
* schema:
* type: object
* properties:
* data:
* type: array
* items:
* $ref: '#/definitions/SSOTrace'
* pageToken:
* type: string
* description: token for pagination
*/
public async getTracesByProduct(params: GetByProductParams) {
const { product, pageOffset, pageLimit, pageToken } = params;

View File

@ -22,17 +22,24 @@ const record2 = {
city: 'London',
};
const records = [record1, record2];
const record3 = {
id: '3',
name: 'Samuel Jackson',
city: 'Delhi',
};
const records = [record1, record2, record3];
const memDbConfig = <DatabaseOption>{
engine: 'mem',
ttl: 1,
pageLimit: 2,
};
const redisDbConfig = <DatabaseOption>{
engine: 'redis',
url: 'redis://localhost:6379',
pageLimit: 50,
pageLimit: 2,
};
const postgresDbConfig = <DatabaseOption>{
@ -41,11 +48,13 @@ const postgresDbConfig = <DatabaseOption>{
type: 'postgres',
ttl: 1,
cleanupLimit: 10,
pageLimit: 2,
};
const mongoDbConfig = <DatabaseOption>{
engine: 'mongo',
url: 'mongodb://localhost:27017/jackson',
pageLimit: 2,
};
const mysqlDbConfig = <DatabaseOption>{
@ -54,6 +63,7 @@ const mysqlDbConfig = <DatabaseOption>{
type: 'mysql',
ttl: 1,
cleanupLimit: 10,
pageLimit: 2,
};
const planetscaleDbConfig = <DatabaseOption>{
@ -61,6 +71,7 @@ const planetscaleDbConfig = <DatabaseOption>{
url: process.env.PLANETSCALE_URL,
ttl: 1,
cleanupLimit: 10,
pageLimit: 2,
// ssl: {
// rejectUnauthorized: true,
// },
@ -72,6 +83,7 @@ const mariadbDbConfig = <DatabaseOption>{
type: 'mariadb',
ttl: 1,
cleanupLimit: 10,
pageLimit: 2,
};
const mssqlDbConfig = <DatabaseOption>{
@ -80,6 +92,7 @@ const mssqlDbConfig = <DatabaseOption>{
url: 'sqlserver://localhost:1433;database=master;username=sa;password=123ABabc!',
ttl: 1,
cleanupLimit: 10,
pageLimit: 2,
};
const dynamoDbConfig = <DatabaseOption>{
@ -87,6 +100,7 @@ const dynamoDbConfig = <DatabaseOption>{
url: process.env.DYNAMODB_URL,
ttl: 1,
cleanupLimit: 10,
pageLimit: 2,
dynamodb: {
region: 'us-east-1',
readCapacityUnits: 5,
@ -229,6 +243,24 @@ tap.test('dbs', async () => {
value: record2.name,
}
);
// wait 100ms to ensure that the record is written with a different timestamp
await new Promise((resolve) => setTimeout(resolve, 100));
await connectionStore.put(
record3.id,
record3,
{
// secondary index on city
name: 'city',
value: record3.city,
},
{
// secondary index on name
name: 'name',
value: record3.name,
}
);
});
tap.test('get(): ' + dbType, async (t) => {
@ -240,33 +272,23 @@ tap.test('dbs', async () => {
});
tap.test('getAll(): ' + dbType, async (t) => {
const allRecords = await connectionStore.getAll();
const allRecordOutput = {};
let allRecordInput = {};
for (const keyValue in records) {
const keyVal = records[keyValue.toString()];
allRecordOutput[keyVal];
}
for (const keyValue in allRecords.data) {
const keyVal = records[keyValue.toString()];
allRecordInput[allRecords.data[keyVal]];
}
t.same(allRecordInput, allRecordOutput, 'unable to getAll records');
allRecordInput = {};
let allRecordsWithPagination = await connectionStore.getAll(0, 2);
for (const keyValue in allRecordsWithPagination.data) {
const keyVal = records[keyValue.toString()];
allRecordInput[allRecordsWithPagination.data[keyVal]];
}
t.same(allRecordInput, allRecordOutput, 'unable to getAll records');
allRecordsWithPagination = await connectionStore.getAll(0, 0);
for (const keyValue in allRecordsWithPagination.data) {
const keyVal = records[keyValue.toString()];
allRecordInput[allRecordsWithPagination.data[keyVal]];
}
t.same(allRecordInput, allRecordOutput, 'unable to getAll records');
const testMessage =
dbType === 'dynamodb' // dynamodb doesn't support sort order
? 'should return all the records upto options.pageLimit'
: 'should return all the records upto options.pageLimit in DESC order by creation time';
const wanted = dbType === 'dynamodb' ? records.slice(1, 3) : [...records].reverse().slice(0, 2);
// getAll without pagination params
t.same((await connectionStore.getAll()).data, wanted, `without pagination params ` + testMessage);
// getAll with pagination params
t.same((await connectionStore.getAll(0, 2)).data, wanted, `with pagination params ` + testMessage);
// getAll with pageLimit set to 0
t.same((await connectionStore.getAll(0, 0)).data, wanted, `with pageLimit set to 0 ` + testMessage);
// getAll with pageLimit > options.pageLimit
t.same(
(await connectionStore.getAll(0, 3)).data,
wanted,
`with pageLimit > options.pageLimit ` + testMessage
);
const oneRecordWithPagination = await connectionStore.getAll(0, 1);
t.same(
@ -291,7 +313,7 @@ tap.test('dbs', async () => {
t.match(sortedRecordsAsc, [record1, record2], 'records are sorted in ASC order');
const { data: sortedRecordsDesc } = await connectionStore.getAll(0, 2, undefined, 'DESC');
t.match(sortedRecordsDesc, [record2, record1], 'records are sorted in DESC order');
t.match(sortedRecordsDesc, [record3, record2], 'records are sorted in DESC order');
}
});

View File

@ -7,7 +7,7 @@ import { useProjects } from '@lib/ui/retraced';
import Loading from '@components/Loading';
import { useTranslation } from 'next-i18next';
import router from 'next/router';
import { Pagination, pageLimit } from '@components/Pagination';
import { Pagination, pageLimit } from '@boxyhq/internal-ui';
import usePaginate from '@lib/ui/hooks/usePaginate';
import { LinkPrimary } from '@components/LinkPrimary';
import { errorToast } from '@components/Toaster';

View File

@ -1,7 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import jackson from '@lib/jackson';
import { oidcMetadataParse, strategyChecker } from '@lib/utils';
import { oidcMetadataParse, parsePaginateApiParams, strategyChecker } from '@lib/utils';
import { adminPortalSSODefaults } from '@lib/env';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
@ -26,20 +26,15 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
const { adminController, connectionAPIController } = await jackson();
const { pageOffset, pageLimit, isSystemSSO, pageToken } = req.query as {
pageOffset: string;
pageLimit: string;
const { isSystemSSO } = req.query as {
isSystemSSO?: string; // if present will be '' else undefined
pageToken?: string;
};
const { pageOffset, pageLimit, pageToken } = parsePaginateApiParams(req.query);
const { tenant: adminPortalSSOTenant, product: adminPortalSSOProduct } = adminPortalSSODefaults;
const paginatedConnectionList = await adminController.getAllConnection(
+(pageOffset || 0),
+(pageLimit || 0),
pageToken
);
const paginatedConnectionList = await adminController.getAllConnection(pageOffset, pageLimit, pageToken);
const connections =
isSystemSSO === undefined

View File

@ -1,6 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { DirectoryType } from '@boxyhq/saml-jackson';
import jackson from '@lib/jackson';
import { parsePaginateApiParams } from '@lib/utils';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { method } = req;
@ -43,19 +44,15 @@ const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
const { directorySyncController } = await jackson();
const { pageOffset, pageLimit, pageToken } = req.query as {
pageOffset: string;
pageLimit: string;
pageToken?: string;
};
const { pageOffset, pageLimit, pageToken } = parsePaginateApiParams(req.query);
const {
data,
error,
pageToken: nextPageToken,
} = await directorySyncController.directories.getAll({
pageOffset: +(pageOffset || 0),
pageLimit: +(pageLimit || 0),
pageOffset,
pageLimit,
pageToken,
});

View File

@ -1,5 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import jackson from '@lib/jackson';
import { parsePaginateApiParams } from '@lib/utils';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { method } = req;
@ -45,14 +46,13 @@ const handleDELETE = async (req: NextApiRequest, res: NextApiResponse) => {
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
const { setupLinkController } = await jackson();
const { token, service, pageOffset, pageLimit, pageToken } = req.query as {
pageOffset: string;
pageLimit: string;
pageToken?: string;
const { token, service } = req.query as {
token: string;
service: string;
};
const { pageOffset, pageLimit, pageToken } = parsePaginateApiParams(req.query);
if (!token && !service) {
return res.status(404).json({
error: {
@ -73,8 +73,8 @@ const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
if (service) {
const setupLinksPaginated = await setupLinkController.filterBy({
service: service as any,
pageLimit: parseInt(pageLimit),
pageOffset: parseInt(pageOffset),
pageOffset,
pageLimit,
pageToken,
});

View File

@ -1,6 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import jackson from '@lib/jackson';
import type { IAdminController } from '@boxyhq/saml-jackson';
import { parsePaginateApiParams } from '@lib/utils';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { method } = req;
@ -23,10 +24,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
// Get SAML Traces
const handleGET = async (req: NextApiRequest, res: NextApiResponse, adminController: IAdminController) => {
const { offset, limit, pageToken } = req.query as { offset: string; limit: string; pageToken?: string };
const pageOffset = parseInt(offset);
const pageLimit = parseInt(limit);
const params = req.query;
const { pageOffset, pageLimit, pageToken } = parsePaginateApiParams(params);
const tracesPaginated = await adminController.getAllSSOTraces(pageOffset, pageLimit, pageToken);

View File

@ -1,5 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import jackson from '@lib/jackson';
import { parsePaginateApiParams } from '@lib/utils';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
@ -28,14 +29,13 @@ const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
tenant: string;
product: string;
directoryId: string;
offset: string;
limit: string;
pageToken: string;
};
let tenant = searchParams.tenant || '';
let product = searchParams.product || '';
const { pageOffset, pageLimit, pageToken } = parsePaginateApiParams(req.query);
// If tenant and product are not provided, retrieve the from directory
if ((!tenant || !product) && searchParams.directoryId) {
const { data: directory } = await directorySyncController.directories.get(searchParams.directoryId);
@ -49,9 +49,9 @@ const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
}
const events = await directorySyncController.webhookLogs.setTenantAndProduct(tenant, product).getAll({
pageOffset: parseInt(searchParams.offset || '0'),
pageLimit: parseInt(searchParams.limit || '15'),
pageToken: searchParams.pageToken || undefined,
pageOffset,
pageLimit,
pageToken,
directoryId: searchParams.directoryId,
});

View File

@ -1,5 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import jackson from '@lib/jackson';
import { parsePaginateApiParams } from '@lib/utils';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { method } = req;
@ -21,14 +22,13 @@ const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
tenant: string;
product: string;
directoryId: string;
offset: string;
limit: string;
pageToken: string;
};
let tenant = searchParams.tenant || '';
let product = searchParams.product || '';
const { pageOffset, pageLimit, pageToken } = parsePaginateApiParams(req.query);
// If tenant and product are not provided, retrieve the from directory
if ((!tenant || !product) && searchParams.directoryId) {
const { data: directory } = await directorySyncController.directories.get(searchParams.directoryId);
@ -42,9 +42,9 @@ const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
}
const { data, error } = await directorySyncController.groups.setTenantAndProduct(tenant, product).getAll({
pageOffset: parseInt(searchParams.offset || '0'),
pageLimit: parseInt(searchParams.limit || '15'),
pageToken: searchParams.pageToken || undefined,
pageOffset,
pageLimit,
pageToken,
directoryId: searchParams.directoryId,
});

View File

@ -1,4 +1,5 @@
import jackson from '@lib/jackson';
import { parsePaginateApiParams } from '@lib/utils';
import { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@ -23,21 +24,20 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
const { directorySyncController } = await jackson();
const { product, pageOffset, pageLimit, pageToken } = req.query as {
const { product } = req.query as {
product: string;
pageOffset: string;
pageLimit: string;
pageToken?: string;
};
if (!product) {
throw new Error('Please provide a product');
}
const { pageOffset, pageLimit, pageToken } = parsePaginateApiParams(req.query);
const connections = await directorySyncController.directories.filterBy({
product,
pageOffset: parseInt(pageOffset),
pageLimit: parseInt(pageLimit),
pageOffset,
pageLimit,
pageToken,
});

View File

@ -1,6 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { SetupLinkService } from '@boxyhq/saml-jackson';
import jackson from '@lib/jackson';
import { parsePaginateApiParams } from '@lib/utils';
const service: SetupLinkService = 'dsync';
@ -27,22 +28,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
const { setupLinkController } = await jackson();
const { product, pageOffset, pageLimit, pageToken } = req.query as {
const { product } = req.query as {
product: string;
pageOffset: string;
pageLimit: string;
pageToken?: string;
};
if (!product) {
throw new Error('Please provide a product');
}
const { pageOffset, pageLimit, pageToken } = parsePaginateApiParams(req.query);
const setupLinks = await setupLinkController.filterBy({
product,
service,
pageOffset: parseInt(pageOffset),
pageLimit: parseInt(pageLimit),
pageOffset,
pageLimit,
pageToken,
});

View File

@ -1,5 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import jackson from '@lib/jackson';
import { parsePaginateApiParams } from '@lib/utils';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { method } = req;
@ -21,14 +22,13 @@ const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
tenant: string;
product: string;
directoryId: string;
offset: string;
limit: string;
pageToken: string;
};
let tenant = searchParams.tenant || '';
let product = searchParams.product || '';
const { pageOffset, pageLimit, pageToken } = parsePaginateApiParams(req.query);
// If tenant and product are not provided, retrieve the from directory
if ((!tenant || !product) && searchParams.directoryId) {
const { data: directory } = await directorySyncController.directories.get(searchParams.directoryId);
@ -42,9 +42,9 @@ const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
}
const { data, error } = await directorySyncController.users.setTenantAndProduct(tenant, product).getAll({
pageOffset: parseInt(searchParams.offset || '0'),
pageLimit: parseInt(searchParams.limit || '15'),
pageToken: searchParams.pageToken || undefined,
pageOffset,
pageLimit,
pageToken,
directoryId: searchParams.directoryId,
});

View File

@ -1,4 +1,5 @@
import jackson from '@lib/jackson';
import { parsePaginateApiParams } from '@lib/utils';
import { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@ -17,23 +18,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
}
// Get the saml traces filtered by the product
// Get the sso traces filtered by the product
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
const { adminController } = await jackson();
const { product, pageOffset, pageLimit, pageToken } = req.query as {
const { product } = req.query as {
product: string;
pageOffset: string;
pageLimit: string;
pageToken?: string;
};
const traces = await adminController.getTracesByProduct(
product,
parseInt(pageOffset),
parseInt(pageLimit),
pageToken
);
const { pageOffset, pageLimit, pageToken } = parsePaginateApiParams(req.query);
const traces = await adminController.getTracesByProduct(product, pageOffset, pageLimit, pageToken);
res.json(traces);
};

View File

@ -1,4 +1,5 @@
import jackson from '@lib/jackson';
import { parsePaginateApiParams } from '@lib/utils';
import { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@ -23,17 +24,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
const { connectionAPIController } = await jackson();
const { product, pageOffset, pageLimit, pageToken } = req.query as {
const { product } = req.query as {
product: string;
pageOffset: string;
pageLimit: string;
pageToken?: string;
};
const { pageOffset, pageLimit, pageToken } = parsePaginateApiParams(req.query);
const connections = await connectionAPIController.getConnectionsByProduct({
product,
pageOffset: parseInt(pageOffset),
pageLimit: parseInt(pageLimit),
pageOffset,
pageLimit,
pageToken,
});

View File

@ -1,6 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { SetupLinkService } from '@boxyhq/saml-jackson';
import jackson from '@lib/jackson';
import { parsePaginateApiParams } from '@lib/utils';
const service: SetupLinkService = 'sso';
@ -27,22 +28,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
const { setupLinkController } = await jackson();
const { product, pageOffset, pageLimit, pageToken } = req.query as {
const { product } = req.query as {
product: string;
pageOffset: string;
pageLimit: string;
pageToken?: string;
};
if (!product) {
throw new Error('Please provide a product');
}
const { pageOffset, pageLimit, pageToken } = parsePaginateApiParams(req.query);
const setupLinks = await setupLinkController.filterBy({
product,
service,
pageOffset: parseInt(pageOffset),
pageLimit: parseInt(pageLimit),
pageOffset,
pageLimit,
pageToken,
});

View File

@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "Enterprise SSO & Directory Sync",
"version": "1.19.2",
"version": "1.20.5",
"description": "This is the API documentation for SAML Jackson service.",
"termsOfService": "https://boxyhq.com/terms.html",
"contact": {
@ -278,6 +278,15 @@
"parameters": [
{
"$ref": "#/parameters/productParamGet"
},
{
"$ref": "#/parameters/pageOffset"
},
{
"$ref": "#/parameters/pageLimit"
},
{
"$ref": "#/parameters/pageToken"
}
],
"operationId": "get-connections-by-product",
@ -286,7 +295,7 @@
],
"responses": {
"200": {
"$ref": "#/responses/200Get"
"$ref": "#/responses/200GetByProduct"
},
"400": {
"$ref": "#/responses/400Get"
@ -644,6 +653,15 @@
"parameters": [
{
"$ref": "#/parameters/productParamGet"
},
{
"$ref": "#/parameters/pageOffset"
},
{
"$ref": "#/parameters/pageLimit"
},
{
"$ref": "#/parameters/pageToken"
}
],
"operationId": "get-sso-setup-link-by-product",
@ -669,6 +687,15 @@
"parameters": [
{
"$ref": "#/parameters/productParamGet"
},
{
"$ref": "#/parameters/pageOffset"
},
{
"$ref": "#/parameters/pageLimit"
},
{
"$ref": "#/parameters/pageToken"
}
],
"operationId": "get-dsync-setup-link-by-product",
@ -701,7 +728,7 @@
}
],
"tags": [
"SAML Traces"
"SSO Traces"
],
"produces": [
"application/json"
@ -722,10 +749,19 @@
"parameters": [
{
"$ref": "#/parameters/product"
},
{
"$ref": "#/parameters/pageOffset"
},
{
"$ref": "#/parameters/pageLimit"
},
{
"$ref": "#/parameters/pageToken"
}
],
"tags": [
"SAML Traces"
"SSO Traces"
],
"produces": [
"application/json"
@ -733,10 +769,23 @@
"responses": {
"200": {
"description": "Success",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/SSOTrace"
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/SSOTrace"
}
},
"pageToken": {
"type": "string",
"description": "token for pagination"
}
}
}
}
}
}
@ -985,6 +1034,15 @@
"parameters": [
{
"$ref": "#/parameters/product"
},
{
"$ref": "#/parameters/pageOffset"
},
{
"$ref": "#/parameters/pageLimit"
},
{
"$ref": "#/parameters/pageToken"
}
],
"tags": [
@ -996,10 +1054,23 @@
"responses": {
"200": {
"description": "Success",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/Directory"
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/Directory"
}
},
"pageToken": {
"type": "string",
"description": "token for pagination"
}
}
}
}
}
}
@ -1052,6 +1123,15 @@
},
{
"$ref": "#/parameters/directoryId"
},
{
"$ref": "#/parameters/pageOffset"
},
{
"$ref": "#/parameters/pageLimit"
},
{
"$ref": "#/parameters/pageToken"
}
],
"tags": [
@ -1063,10 +1143,23 @@
"responses": {
"200": {
"description": "Success",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/Group"
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/Group"
}
},
"pageToken": {
"type": "string",
"description": "token for pagination"
}
}
}
}
}
}
@ -1119,6 +1212,15 @@
},
{
"$ref": "#/parameters/directoryId"
},
{
"$ref": "#/parameters/pageOffset"
},
{
"$ref": "#/parameters/pageLimit"
},
{
"$ref": "#/parameters/pageToken"
}
],
"tags": [
@ -1130,10 +1232,72 @@
"responses": {
"200": {
"description": "Success",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/User"
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/User"
}
},
"pageToken": {
"type": "string",
"description": "token for pagination"
}
}
}
}
}
}
}
}
},
"/api/v1/dsync/events": {
"get": {
"summary": "Get event logs for a directory",
"parameters": [
{
"$ref": "#/parameters/directoryId"
},
{
"$ref": "#/parameters/pageOffset"
},
{
"$ref": "#/parameters/pageLimit"
},
{
"$ref": "#/parameters/pageToken"
}
],
"tags": [
"Directory Sync"
],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/Event"
}
},
"pageToken": {
"type": "string",
"description": "token for pagination"
}
}
}
}
}
}
@ -1435,6 +1599,15 @@
"in": "query",
"required": true,
"type": "string"
},
{
"$ref": "#/parameters/pageOffset"
},
{
"$ref": "#/parameters/pageLimit"
},
{
"$ref": "#/parameters/pageToken"
}
],
"tags": [
@ -1446,10 +1619,23 @@
"responses": {
"200": {
"description": "Success",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/SAMLFederationApp"
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/SAMLFederationApp"
}
},
"pageToken": {
"type": "string",
"description": "token for pagination"
}
}
}
}
}
}
@ -1742,6 +1928,53 @@
}
}
},
"Event": {
"type": "object",
"example": {
"id": "id1",
"webhook_endpoint": "https://example.com/webhook",
"created_at": "2024-03-05T17:06:26.074Z",
"status_code": 200,
"delivered": true,
"payload": {
"directory_id": "58b5cd9dfaa39d47eb8f5f88631f9a629a232016",
"event": "user.created",
"tenant": "boxyhq",
"product": "jackson",
"data": {
"id": "038e767b-9bc6-4dbd-975e-fbc38a8e7d82",
"first_name": "Deepak",
"last_name": "Prabhakara",
"email": "deepak@boxyhq.com",
"active": true,
"raw": {
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"userName": "deepak@boxyhq.com",
"name": {
"givenName": "Deepak",
"familyName": "Prabhakara"
},
"emails": [
{
"primary": true,
"value": "deepak@boxyhq.com",
"type": "work"
}
],
"title": "CEO",
"displayName": "Deepak Prabhakara",
"locale": "en-US",
"externalId": "00u1ldzzogFkXFmvT5d7",
"groups": [],
"active": true,
"id": "038e767b-9bc6-4dbd-975e-fbc38a8e7d82"
}
}
}
}
},
"SAMLFederationApp": {
"type": "object",
"properties": {
@ -1799,6 +2032,28 @@
},
"401Get": {
"description": "Unauthorized"
},
"200GetByProduct": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/Connection"
}
},
"pageToken": {
"type": "string",
"description": "token for pagination"
}
}
}
}
}
}
},
"parameters": {
@ -2138,6 +2393,27 @@
"in": "query",
"required": false,
"type": "string"
},
"pageOffset": {
"name": "pageOffset",
"description": "Starting point from which the set of records are retrieved",
"in": "query",
"required": false,
"type": "string"
},
"pageLimit": {
"name": "pageLimit",
"description": "Number of records to be fetched for the page",
"in": "query",
"required": false,
"type": "string"
},
"pageToken": {
"name": "pageToken",
"description": "Token used for DynamoDB pagination",
"in": "query",
"required": false,
"type": "string"
}
},
"components": {},

View File

@ -1,8 +0,0 @@
export type ApiSuccess<T> = { data: T; pageToken?: string };
export interface ApiError extends Error {
info?: string;
status: number;
}
export type ApiResponse<T = any> = ApiSuccess<T> | { error: ApiError };

View File

@ -1,10 +1,10 @@
export type ApiError = {
code?: string;
message: string;
values: { [key: string]: string };
};
export type ApiSuccess<T> = { data: T; pageToken?: string };
export type ApiResponse<T> = {
data: T | null;
error: ApiError | null;
};
export interface ApiError extends Error {
info?: string;
status: number;
}
export type ApiResponse<T = any> = ApiSuccess<T> | { error: ApiError };
export type PaginateApiParams = { pageOffset: number; pageLimit: number } & { pageToken?: string };