Compare commits

...
Author SHA1 Message Date
Quinlan Jung 4f2247f19b [eas-cli] fetch entire relay compliant dataset (#1953)
* [eas-cli] fetch entire relay compliant dataset

* update CHANGELOG.md
2023-07-26 20:23:23 -07:00
Quinlan Jung 7a3c782bb5 [eas-ci] revert UpdateBranchWithCurrentGroupFragment 2023-07-25 18:14:22 -07:00
10 changed files with 763 additions and 41 deletions

No files matched your search

+1
View File
@@ -13,6 +13,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🧹 Chores
- Unify channel graphql query types. ([#1949](https://github.com/expo/eas-cli/pull/1949) by [@quinlanj](https://github.com/quinlanj))
- Fetch entire relay compliant dataset. ([#1953](https://github.com/expo/eas-cli/pull/1953) by [@quinlanj](https://github.com/quinlanj))
## [3.17.0](https://github.com/expo/eas-cli/releases/tag/v3.17.0) - 2023-07-24
+464
View File
@@ -15554,6 +15554,80 @@
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "BuildLimitThresholdExceededMetadata",
"description": null,
"fields": [
{
"name": "account",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Account",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "thresholdsExceeded",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "NotificationThresholdExceeded",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "BuildLimitThresholdExceededMetadataType",
"description": null,
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "IOS",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "TOTAL",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "BuildLogs",
@@ -16520,6 +16594,81 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "BuildPlanCreditThresholdExceededMetadata",
"description": null,
"fields": [
{
"name": "account",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Account",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "buildCreditUsage",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "planLimit",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "threshold",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "BuildPriority",
@@ -25965,6 +26114,109 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Notification",
"description": null,
"fields": [
{
"name": "createdAt",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "event",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "NotificationEvent",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "metadata",
"description": null,
"args": [],
"type": {
"kind": "UNION",
"name": "NotificationMetadata",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "type",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "NotificationType",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updatedAt",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "NotificationEvent",
@@ -25996,10 +26248,42 @@
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "TEST",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "UNION",
"name": "NotificationMetadata",
"description": null,
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": [
{
"kind": "OBJECT",
"name": "BuildLimitThresholdExceededMetadata",
"ofType": null
},
{
"kind": "OBJECT",
"name": "BuildPlanCreditThresholdExceededMetadata",
"ofType": null
},
{
"kind": "OBJECT",
"name": "TestNotificationMetadata",
"ofType": null
}
]
},
{
"kind": "OBJECT",
"name": "NotificationSubscription",
@@ -26280,6 +26564,81 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "NotificationThresholdExceeded",
"description": null,
"fields": [
{
"name": "count",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "limit",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "threshold",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "type",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "BuildLimitThresholdExceededMetadataType",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "NotificationType",
@@ -26293,6 +26652,12 @@
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "WEB",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
@@ -30078,6 +30443,30 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "webNotifications",
"description": "Web notifications linked to a user",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Notification",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@@ -32672,6 +33061,33 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "TestNotificationMetadata",
"description": null,
"fields": [
{
"name": "message",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "TimelineActivityConnection",
@@ -35289,6 +35705,30 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "webNotifications",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Notification",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@@ -35892,6 +36332,30 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "webNotifications",
"description": "Web notifications linked to a user",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Notification",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@@ -1,7 +1,6 @@
import { UpdateBranchWithCurrentGroupFragment } from '../../graphql/generated';
import { UpdateChannelObject } from '../../graphql/queries/ChannelQuery';
import { UpdateBranchObject, UpdateChannelObject } from '../../graphql/queries/ChannelQuery';
export const testUpdateBranch1: UpdateBranchWithCurrentGroupFragment = {
export const testUpdateBranch1: UpdateBranchObject = {
id: '754bf17f-efc0-46ab-8a59-a03f20e53e9b',
name: 'wrong-channel',
updateGroups: [
@@ -61,7 +60,7 @@ export const testUpdateBranch1: UpdateBranchWithCurrentGroupFragment = {
__typename: 'UpdateBranch',
};
const testUpdateBranch2: UpdateBranchWithCurrentGroupFragment = {
const testUpdateBranch2: UpdateBranchObject = {
id: '6941a8dd-5c0a-48bc-8876-f49c88ed419f',
name: 'production',
updateGroups: [
+47
View File
@@ -8,6 +8,8 @@ import { withErrorHandlingAsync } from '../graphql/client';
import {
CreateUpdateChannelOnAppMutation,
CreateUpdateChannelOnAppMutationVariables,
PageInfo,
UpdateChannelBasicInfoFragment,
ViewBranchesOnUpdateChannelQueryVariables,
ViewUpdateChannelsOnAppQueryVariables,
} from '../graphql/generated';
@@ -15,12 +17,14 @@ import { BranchQuery, UpdateBranchOnChannelObject } from '../graphql/queries/Bra
import { ChannelQuery, UpdateChannelObject } from '../graphql/queries/ChannelQuery';
import { UpdateChannelBasicInfoFragmentNode } from '../graphql/types/UpdateChannelBasicInfo';
import Log from '../log';
import { ora } from '../ora';
import formatFields from '../utils/formatFields';
import { printJsonOnlyOutput } from '../utils/json';
import {
paginatedQueryWithConfirmPromptAsync,
paginatedQueryWithSelectPromptAsync,
} from '../utils/queries';
import { Connection, QueryParams, getPaginatedDatasetAsync } from '../utils/relay';
import { logChannelDetails } from './utils';
export const CHANNELS_LIMIT = 25;
@@ -264,3 +268,46 @@ export async function ensureChannelExistsAsync(
}
}
}
export async function getChannelsDatasetAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
filterPredicate,
batchSize = 100,
}: {
appId: string;
filterPredicate?: (channelInfo: UpdateChannelBasicInfoFragment) => boolean;
batchSize?: number;
}
): Promise<UpdateChannelBasicInfoFragment[]> {
const queryAsync = async ({
first,
after,
}: QueryParams): Promise<Connection<UpdateChannelBasicInfoFragment>> =>
await ChannelQuery.viewUpdateChannelsBasicInfoPaginatedOnAppAsync(graphqlClient, {
appId,
first,
after,
});
const assetSpinner = ora().start('Fetching channels...');
const afterEachQuery = (
totalNodesFetched: number,
_dataset: UpdateChannelBasicInfoFragment[],
_batch: UpdateChannelBasicInfoFragment[],
pageInfo: PageInfo
): void => {
if (pageInfo.hasNextPage) {
assetSpinner.text = `Fetched ${totalNodesFetched} channels`;
}
};
const dataset = await getPaginatedDatasetAsync({
queryAsync,
afterEachQuery,
filterPredicate,
batchSize,
});
assetSpinner.succeed(`Fetched all channels`);
return dataset;
}
+3 -4
View File
@@ -1,8 +1,7 @@
import assert from 'assert';
import chalk from 'chalk';
import { UpdateBranchWithCurrentGroupFragment } from '../graphql/generated';
import { UpdateChannelObject } from '../graphql/queries/ChannelQuery';
import { UpdateBranchObject, UpdateChannelObject } from '../graphql/queries/ChannelQuery';
import Log from '../log';
import {
FormattedBranchDescription,
@@ -114,7 +113,7 @@ export function logChannelDetails(channel: UpdateChannelObject): void {
function getUpdateBranchNullable(
channel: UpdateChannelObject,
branchId: string
): UpdateBranchWithCurrentGroupFragment | null {
): UpdateBranchObject | null {
const updateBranches = channel.updateBranches;
const updateBranch = updateBranches.find(branch => branch.id === branchId);
return updateBranch ?? null;
@@ -123,7 +122,7 @@ function getUpdateBranchNullable(
export function getUpdateBranch(
channel: UpdateChannelObject,
branchId: string
): UpdateBranchWithCurrentGroupFragment {
): UpdateBranchObject {
const updateBranch = getUpdateBranchNullable(channel, branchId);
if (!updateBranch) {
throw new Error(
+54 -4
View File
@@ -2236,6 +2236,17 @@ export enum BuildJobStatus {
Started = 'STARTED'
}
export type BuildLimitThresholdExceededMetadata = {
__typename?: 'BuildLimitThresholdExceededMetadata';
account: Account;
thresholdsExceeded: Array<NotificationThresholdExceeded>;
};
export enum BuildLimitThresholdExceededMetadataType {
Ios = 'IOS',
Total = 'TOTAL'
}
export type BuildLogs = {
__typename?: 'BuildLogs';
format?: Maybe<BuildJobLogsFormat>;
@@ -2381,6 +2392,14 @@ export type BuildParamsInput = {
sdkVersion?: InputMaybe<Scalars['String']>;
};
export type BuildPlanCreditThresholdExceededMetadata = {
__typename?: 'BuildPlanCreditThresholdExceededMetadata';
account: Account;
buildCreditUsage: Scalars['Int'];
planLimit: Scalars['Int'];
threshold: Scalars['Int'];
};
export enum BuildPriority {
High = 'HIGH',
Normal = 'NORMAL',
@@ -3758,13 +3777,26 @@ export type MeteredBillingStatus = {
EAS_UPDATE: Scalars['Boolean'];
};
export type Notification = {
__typename?: 'Notification';
createdAt: Scalars['DateTime'];
event: NotificationEvent;
id: Scalars['ID'];
metadata?: Maybe<NotificationMetadata>;
type: NotificationType;
updatedAt: Scalars['DateTime'];
};
export enum NotificationEvent {
BuildComplete = 'BUILD_COMPLETE',
BuildLimitThresholdExceeded = 'BUILD_LIMIT_THRESHOLD_EXCEEDED',
BuildPlanCreditThresholdExceeded = 'BUILD_PLAN_CREDIT_THRESHOLD_EXCEEDED',
SubmissionComplete = 'SUBMISSION_COMPLETE'
SubmissionComplete = 'SUBMISSION_COMPLETE',
Test = 'TEST'
}
export type NotificationMetadata = BuildLimitThresholdExceededMetadata | BuildPlanCreditThresholdExceededMetadata | TestNotificationMetadata;
export type NotificationSubscription = {
__typename?: 'NotificationSubscription';
account?: Maybe<Account>;
@@ -3805,8 +3837,17 @@ export type NotificationSubscriptionMutationUnsubscribeArgs = {
id: Scalars['ID'];
};
export type NotificationThresholdExceeded = {
__typename?: 'NotificationThresholdExceeded';
count: Scalars['Int'];
limit: Scalars['Int'];
threshold: Scalars['Int'];
type: BuildLimitThresholdExceededMetadataType;
};
export enum NotificationType {
Email = 'EMAIL'
Email = 'EMAIL',
Web = 'WEB'
}
export type Offer = {
@@ -4309,6 +4350,8 @@ export type SsoUser = Actor & UserActor & {
/** @deprecated No longer supported */
twitterUsername?: Maybe<Scalars['String']>;
username: Scalars['String'];
/** Web notifications linked to a user */
webNotifications: Array<Notification>;
};
@@ -4745,6 +4788,11 @@ export type SubscriptionDetailsPlanEnablementArgs = {
serviceMetric: EasServiceMetric;
};
export type TestNotificationMetadata = {
__typename?: 'TestNotificationMetadata';
message: Scalars['String'];
};
export type TimelineActivityConnection = {
__typename?: 'TimelineActivityConnection';
edges: Array<TimelineActivityEdge>;
@@ -5062,6 +5110,7 @@ export type User = Actor & UserActor & {
/** @deprecated No longer supported */
twitterUsername?: Maybe<Scalars['String']>;
username: Scalars['String'];
webNotifications: Array<Notification>;
};
@@ -5150,6 +5199,8 @@ export type UserActor = {
/** @deprecated No longer supported */
twitterUsername?: Maybe<Scalars['String']>;
username: Scalars['String'];
/** Web notifications linked to a user */
webNotifications: Array<Notification>;
};
@@ -6173,6 +6224,7 @@ export type ViewBuildsOnAppQuery = { __typename?: 'RootQuery', app: { __typename
export type ViewUpdateChannelOnAppQueryVariables = Exact<{
appId: Scalars['String'];
channelName: Scalars['String'];
filter?: InputMaybe<UpdatesFilter>;
}>;
@@ -6308,8 +6360,6 @@ export type UpdateFragment = { __typename?: 'Update', id: string, group: string,
export type UpdateBranchFragment = { __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }> };
export type UpdateBranchWithCurrentGroupFragment = { __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array<Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }>> };
export type UpdateChannelBasicInfoFragment = { __typename?: 'UpdateChannel', id: string, name: string, branchMapping: string };
export type WebhookFragment = { __typename?: 'Webhook', id: string, event: WebhookType, url: string, createdAt: any, updatedAt: any };
@@ -5,8 +5,6 @@ import { ChannelNotFoundError } from '../../channel/errors';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
PageInfo,
UpdateChannelBasicInfoFragment,
ViewUpdateChannelOnAppQuery,
ViewUpdateChannelOnAppQueryVariables,
ViewUpdateChannelsOnAppQuery,
@@ -14,7 +12,7 @@ import {
ViewUpdateChannelsPaginatedOnAppQuery,
ViewUpdateChannelsPaginatedOnAppQueryVariables,
} from '../generated';
import { UpdateBranchWithCurrentGroupFragmentNode } from '../types/UpdateBranchWithCurrentUpdate';
import { UpdateFragmentNode } from '../types/Update';
import { UpdateChannelBasicInfoFragmentNode } from '../types/UpdateChannelBasicInfo';
type ViewUpdateChannelsOnAppObject = NonNullable<
@@ -28,16 +26,22 @@ type UpdateChannelByNameObject = NonNullable<
// these types should have the same fields
export type UpdateChannelObject = ViewUpdateChannelsOnAppObject & UpdateChannelByNameObject;
export type UpdateBranchObject = UpdateChannelObject['updateBranches'][number];
export const ChannelQuery = {
async viewUpdateChannelAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, channelName }: ViewUpdateChannelOnAppQueryVariables
{ appId, channelName, filter }: ViewUpdateChannelOnAppQueryVariables
): Promise<UpdateChannelByNameObject> {
const response = await withErrorHandlingAsync(
graphqlClient
.query<ViewUpdateChannelOnAppQuery, ViewUpdateChannelOnAppQueryVariables>(
gql`
query ViewUpdateChannelOnApp($appId: String!, $channelName: String!) {
query ViewUpdateChannelOnApp(
$appId: String!
$channelName: String!
$filter: UpdatesFilter
) {
app {
byId(appId: $appId) {
id
@@ -48,15 +52,19 @@ export const ChannelQuery = {
branchMapping
updateBranches(offset: 0, limit: 5) {
id
...UpdateBranchWithCurrentGroupFragment
name
updateGroups(offset: 0, limit: 1, filter: $filter) {
id
...UpdateFragment
}
}
}
}
}
}
${print(UpdateBranchWithCurrentGroupFragmentNode)}
${print(UpdateFragmentNode)}
`,
{ appId, channelName },
{ appId, channelName, filter },
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch', 'Update'] }
)
.toPromise()
@@ -88,13 +96,17 @@ export const ChannelQuery = {
branchMapping
updateBranches(offset: 0, limit: 5) {
id
...UpdateBranchWithCurrentGroupFragment
name
updateGroups(offset: 0, limit: 1) {
id
...UpdateFragment
}
}
}
}
}
}
${print(UpdateBranchWithCurrentGroupFragmentNode)}
${print(UpdateFragmentNode)}
`,
{ appId, offset, limit },
{ additionalTypenames: ['UpdateChannel', 'UpdateBranch', 'Update'] }
@@ -112,7 +124,7 @@ export const ChannelQuery = {
async viewUpdateChannelsBasicInfoPaginatedOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, first, after }: ViewUpdateChannelsPaginatedOnAppQueryVariables
): Promise<[UpdateChannelBasicInfoFragment[], PageInfo]> {
): Promise<ViewUpdateChannelsPaginatedOnAppQuery['app']['byId']['channelsPaginated']> {
const response = await withErrorHandlingAsync(
graphqlClient
.query<
@@ -153,7 +165,6 @@ export const ChannelQuery = {
if (!channelsPaginated) {
throw new Error(`Could not find channels on project with id ${appId}`);
}
return [channelsPaginated.edges.map(edge => edge.node) ?? [], channelsPaginated.pageInfo];
return channelsPaginated;
},
};
@@ -1,16 +0,0 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { UpdateFragmentNode } from './Update';
export const UpdateBranchWithCurrentGroupFragmentNode = gql`
fragment UpdateBranchWithCurrentGroupFragment on UpdateBranch {
id
name
updateGroups(offset: 0, limit: 1) {
id
...UpdateFragment
}
}
${print(UpdateFragmentNode)}
`;
@@ -0,0 +1,94 @@
import { getPaginatedDatasetAsync } from '../relay';
describe('getPaginatedDatasetAsync', () => {
const mockQueryAsync = jest.fn(({ first, after }) =>
Promise.resolve({
edges: Array(first).fill({ node: 'node', cursor: 'cursor' }),
pageInfo: {
hasNextPage: after !== 'next',
endCursor: after ? null : 'next',
hasPreviousPage: false,
},
})
);
beforeEach(() => {
jest.clearAllMocks();
});
it('should query data page by page until no more data', async () => {
const dataset = await getPaginatedDatasetAsync({
queryAsync: mockQueryAsync,
batchSize: 1,
});
expect(mockQueryAsync).toHaveBeenCalledTimes(2);
expect(mockQueryAsync).toHaveBeenCalledWith({ first: 1, after: undefined });
expect(mockQueryAsync).toHaveBeenCalledWith({ first: 1, after: 'next' });
expect(dataset).toHaveLength(2);
});
it('should call beforeEachQuery and afterEachQuery with correct parameters', async () => {
// dataset is pass by reference, so we make a copy at each call
const datasetAtNthCall_beforeEachQuery = [] as any;
const mockBeforeEachQuery = jest.fn((totalNodesFetched, dataset) => {
datasetAtNthCall_beforeEachQuery.push([...dataset]);
});
// dataset is pass by reference, so we make a copy at each call
const datasetAtNthCall_afterEachQuery = [] as any;
const mockAfterEachQuery = jest.fn((totalNodesFetched, dataset, batch, pageInfo) => {
datasetAtNthCall_afterEachQuery.push([...dataset]);
});
await getPaginatedDatasetAsync({
queryAsync: mockQueryAsync,
beforeEachQuery: mockBeforeEachQuery,
afterEachQuery: mockAfterEachQuery,
batchSize: 1,
});
expect(mockBeforeEachQuery).toHaveBeenCalledTimes(2);
expect(mockBeforeEachQuery).toHaveBeenNthCalledWith(1, 0, expect.any(Array));
expect(datasetAtNthCall_beforeEachQuery[0]).toStrictEqual([]);
expect(mockBeforeEachQuery).toHaveBeenNthCalledWith(2, 1, expect.any(Array));
expect(datasetAtNthCall_beforeEachQuery[1]).toStrictEqual(['node']);
expect(mockAfterEachQuery).toHaveBeenCalledTimes(2);
expect(mockAfterEachQuery).toHaveBeenNthCalledWith(1, 1, expect.any(Array), ['node'], {
hasNextPage: true,
endCursor: 'next',
hasPreviousPage: false,
});
expect(datasetAtNthCall_afterEachQuery[0]).toStrictEqual(['node']);
expect(mockAfterEachQuery).toHaveBeenNthCalledWith(2, 2, expect.any(Array), ['node'], {
hasNextPage: false,
endCursor: null,
hasPreviousPage: false,
});
expect(datasetAtNthCall_afterEachQuery[1]).toStrictEqual(['node', 'node']);
});
it('should filter nodes if filterPredicate is provided', async () => {
const filterPredicate = (node: string): boolean => node !== 'node';
const dataset = await getPaginatedDatasetAsync({
queryAsync: mockQueryAsync,
filterPredicate,
batchSize: 1,
});
expect(dataset).toEqual([]); // expecting an empty array because our filterPredicate will filter out 'node'
});
it('should stop fetching when maxNodesFetched limit is reached', async () => {
const dataset = await getPaginatedDatasetAsync({
queryAsync: mockQueryAsync,
batchSize: 2,
maxNodesFetched: 2,
});
expect(dataset).toHaveLength(2);
expect(mockQueryAsync).toHaveBeenCalledTimes(1);
});
});
+73
View File
@@ -0,0 +1,73 @@
import { PageInfo } from '../graphql/generated';
export type Connection<T> = {
edges: Edge<T>[];
pageInfo: PageInfo;
};
type Edge<T> = {
node: T;
};
export type QueryParams = {
first: number;
after?: string;
};
/**
* Fetches dataset in paginated manner (batch by batch) using GraphQL queries.
*
* @param queryAsync A promise based function for querying.
* @param beforeEachQuery Optional. A callback function to be called before each query
* @param afterEachQuery Optional. A callback function to be called after each query.
* @param filterPredicate Optional. A predicate function to filter the node.
* @param batchSize Optional. The batch size of the pagination. Defaults to 100.
*
* @return {Promise<T[]>} - A promise that resolves to an array (the dataset).
* @throws {Error} - If an error occurs during execution of the query or pagination.
*/
export async function getPaginatedDatasetAsync<T>({
queryAsync,
beforeEachQuery,
afterEachQuery,
filterPredicate,
batchSize = 100,
maxNodesFetched = 10_000,
}: {
queryAsync: ({ first, after }: QueryParams) => Promise<Connection<T>>;
beforeEachQuery?: (totalNodesFetched: number, dataset: T[]) => void;
afterEachQuery?: (
totalNodesFetched: number,
dataset: T[],
batch: T[],
pageInfo: PageInfo
) => void;
filterPredicate?: (node: T) => boolean;
batchSize?: number;
maxNodesFetched?: number;
}): Promise<T[]> {
const dataset = [];
let hasMore = true;
let after: string | undefined;
let totalNodesFetched = 0;
while (hasMore) {
if (beforeEachQuery) {
beforeEachQuery(totalNodesFetched, dataset);
}
const result = await queryAsync({ first: batchSize, after });
const { edges, pageInfo } = result;
const nodes = edges.map(edge => edge.node);
const batch = filterPredicate ? nodes.filter(filterPredicate) : nodes;
dataset.push(...batch);
hasMore = pageInfo.hasNextPage;
after = pageInfo.endCursor ?? undefined;
totalNodesFetched += nodes.length;
if (afterEachQuery) {
afterEachQuery(totalNodesFetched, dataset, batch, pageInfo);
}
if (totalNodesFetched >= maxNodesFetched) {
return dataset;
}
}
return dataset;
}