diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index 440df3b19..adf7d2e74 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -614,6 +614,23 @@ export type DeviceToken = { token: Scalars['String']; }; +export type DeviceTokensError = { + __typename?: 'DeviceTokensError'; + errorCodes: Array; +}; + +export enum DeviceTokensErrorCode { + BadRequest = 'BAD_REQUEST', + Unauthorized = 'UNAUTHORIZED' +} + +export type DeviceTokensResult = DeviceTokensError | DeviceTokensSuccess; + +export type DeviceTokensSuccess = { + __typename?: 'DeviceTokensSuccess'; + deviceTokens: Array; +}; + export type Feature = { __typename?: 'Feature'; createdAt: Scalars['Date']; @@ -1416,6 +1433,7 @@ export type Query = { article: ArticleResult; articleSavingRequest: ArticleSavingRequestResult; articles: ArticlesResult; + deviceTokens: DeviceTokensResult; feedArticles: FeedArticlesResult; getFollowers: GetFollowersResult; getFollowing: GetFollowingResult; @@ -2844,6 +2862,10 @@ export type ResolversTypes = { DeleteWebhookResult: ResolversTypes['DeleteWebhookError'] | ResolversTypes['DeleteWebhookSuccess']; DeleteWebhookSuccess: ResolverTypeWrapper; DeviceToken: ResolverTypeWrapper; + DeviceTokensError: ResolverTypeWrapper; + DeviceTokensErrorCode: DeviceTokensErrorCode; + DeviceTokensResult: ResolversTypes['DeviceTokensError'] | ResolversTypes['DeviceTokensSuccess']; + DeviceTokensSuccess: ResolverTypeWrapper; Feature: ResolverTypeWrapper; FeedArticle: ResolverTypeWrapper; FeedArticleEdge: ResolverTypeWrapper; @@ -3227,6 +3249,9 @@ export type ResolversParentTypes = { DeleteWebhookResult: ResolversParentTypes['DeleteWebhookError'] | ResolversParentTypes['DeleteWebhookSuccess']; DeleteWebhookSuccess: DeleteWebhookSuccess; DeviceToken: DeviceToken; + DeviceTokensError: DeviceTokensError; + DeviceTokensResult: ResolversParentTypes['DeviceTokensError'] | ResolversParentTypes['DeviceTokensSuccess']; + DeviceTokensSuccess: DeviceTokensSuccess; Feature: Feature; FeedArticle: FeedArticle; FeedArticleEdge: FeedArticleEdge; @@ -3888,6 +3913,20 @@ export type DeviceTokenResolvers; }; +export type DeviceTokensErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type DeviceTokensResultResolvers = { + __resolveType: TypeResolveFn<'DeviceTokensError' | 'DeviceTokensSuccess', ParentType, ContextType>; +}; + +export type DeviceTokensSuccessResolvers = { + deviceTokens?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type FeatureResolvers = { createdAt?: Resolver; expiresAt?: Resolver, ParentType, ContextType>; @@ -4303,6 +4342,7 @@ export type QueryResolvers>; articleSavingRequest?: Resolver>; articles?: Resolver>; + deviceTokens?: Resolver; feedArticles?: Resolver>; getFollowers?: Resolver>; getFollowing?: Resolver>; @@ -5127,6 +5167,9 @@ export type Resolvers = { DeleteWebhookResult?: DeleteWebhookResultResolvers; DeleteWebhookSuccess?: DeleteWebhookSuccessResolvers; DeviceToken?: DeviceTokenResolvers; + DeviceTokensError?: DeviceTokensErrorResolvers; + DeviceTokensResult?: DeviceTokensResultResolvers; + DeviceTokensSuccess?: DeviceTokensSuccessResolvers; Feature?: FeatureResolvers; FeedArticle?: FeedArticleResolvers; FeedArticleEdge?: FeedArticleEdgeResolvers; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index ac7c546b5..c1eaec68d 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -540,6 +540,21 @@ type DeviceToken { token: String! } +type DeviceTokensError { + errorCodes: [DeviceTokensErrorCode!]! +} + +enum DeviceTokensErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +union DeviceTokensResult = DeviceTokensError | DeviceTokensSuccess + +type DeviceTokensSuccess { + deviceTokens: [DeviceToken!]! +} + type Feature { createdAt: Date! expiresAt: Date @@ -1028,6 +1043,7 @@ type Query { article(slug: String!, username: String!): ArticleResult! articleSavingRequest(id: ID!): ArticleSavingRequestResult! articles(after: String, first: Int, includePending: Boolean, query: String, sharedOnly: Boolean, sort: SortParams): ArticlesResult! + deviceTokens: DeviceTokensResult! feedArticles(after: String, first: Int, sharedByUser: ID, sort: SortParams): FeedArticlesResult! getFollowers(userId: ID): GetFollowersResult! getFollowing(userId: ID): GetFollowingResult! diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index 4571292ad..204d41616 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -37,6 +37,7 @@ import { deleteNewsletterEmailResolver, deleteReminderResolver, deleteWebhookResolver, + deviceTokensResolver, generateApiKeyResolver, getAllUsersResolver, getArticleResolver, @@ -204,6 +205,7 @@ export const functionResolvers = { integrations: integrationsResolver, recentSearches: recentSearchesResolver, rules: rulesResolver, + deviceTokens: deviceTokensResolver, }, User: { async sharedArticles( @@ -616,4 +618,5 @@ export const functionResolvers = { ...resultResolveTypeResolver('OptInFeature'), ...resultResolveTypeResolver('SetRule'), ...resultResolveTypeResolver('Rules'), + ...resultResolveTypeResolver('DeviceTokens'), } diff --git a/packages/api/src/resolvers/user_device_tokens/index.ts b/packages/api/src/resolvers/user_device_tokens/index.ts index c83b7041e..83b5e65a8 100644 --- a/packages/api/src/resolvers/user_device_tokens/index.ts +++ b/packages/api/src/resolvers/user_device_tokens/index.ts @@ -1,6 +1,9 @@ import { authorized } from '../../utils/helpers' import { DeviceToken, + DeviceTokensError, + DeviceTokensErrorCode, + DeviceTokensSuccess, MutationSetDeviceTokenArgs, SetDeviceTokenError, SetDeviceTokenErrorCode, @@ -13,6 +16,7 @@ import { deleteDeviceToken, getDeviceToken, getDeviceTokenByToken, + getDeviceTokensByUserId, } from '../../services/user_device_tokens' import { UserDeviceToken } from '../../entity/user_device_tokens' import { QueryFailedError } from 'typeorm' @@ -122,6 +126,41 @@ export const setDeviceTokenResolver = authorized< } }) +export const deviceTokensResolver = authorized< + DeviceTokensSuccess, + DeviceTokensError +>(async (_parent, _args, { claims: { uid }, log }) => { + try { + log.info('deviceTokensResolver', { + labels: { + source: 'resolver', + resolver: 'deviceTokensResolver', + uid, + }, + }) + + const deviceTokens = await getDeviceTokensByUserId(uid) + console.log('deviceTokens', deviceTokens) + + return { + deviceTokens: deviceTokens.map(deviceTokenToData), + } + } catch (e) { + log.error('Error getting device tokens', { + e, + labels: { + source: 'resolver', + resolver: 'rulesResolver', + uid, + }, + }) + + return { + errorCodes: [DeviceTokensErrorCode.BadRequest], + } + } +}) + const deviceTokenToData = (deviceToken: UserDeviceToken): DeviceToken => { return { id: deviceToken.id, diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 6c5e96644..371037663 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -2033,6 +2033,21 @@ const schema = gql` NOT_FOUND } + union DeviceTokensResult = DeviceTokensSuccess | DeviceTokensError + + type DeviceTokensSuccess { + deviceTokens: [DeviceToken!]! + } + + type DeviceTokensError { + errorCodes: [DeviceTokensErrorCode!]! + } + + enum DeviceTokensErrorCode { + UNAUTHORIZED + BAD_REQUEST + } + # Mutations type Mutation { googleLogin(input: GoogleLoginInput!): LoginResult! @@ -2155,6 +2170,7 @@ const schema = gql` integrations: IntegrationsResult! recentSearches: RecentSearchesResult! rules(enabled: Boolean): RulesResult! + deviceTokens: DeviceTokensResult! } ` diff --git a/packages/api/src/services/user_device_tokens.ts b/packages/api/src/services/user_device_tokens.ts index 66942d380..4cc740a4b 100644 --- a/packages/api/src/services/user_device_tokens.ts +++ b/packages/api/src/services/user_device_tokens.ts @@ -20,7 +20,7 @@ export const getDeviceTokenByToken = async ( export const getDeviceTokensByUserId = async ( userId: string -): Promise => { +): Promise => { return getRepository(UserDeviceToken).find({ where: { user: { id: userId } }, }) diff --git a/packages/api/test/resolvers/user_device_tokens.test.ts b/packages/api/test/resolvers/user_device_tokens.test.ts index 001ddc923..cdedd4bc3 100644 --- a/packages/api/test/resolvers/user_device_tokens.test.ts +++ b/packages/api/test/resolvers/user_device_tokens.test.ts @@ -9,16 +9,19 @@ import { expect } from 'chai' import { UserDeviceToken } from '../../src/entity/user_device_tokens' import { SetDeviceTokenErrorCode } from '../../src/generated/graphql' import 'mocha' +import { User } from '../../src/entity/user' +import { getRepository } from '../../src/entity/utils' describe('Device tokens API', () => { const username = 'fakeUser' let authToken: string let deviceToken: UserDeviceToken + let user: User before(async () => { // create test user and login - const user = await createTestUser(username) + user = await createTestUser(username) const res = await request .post('/local/debug/fake-user-login') .send({ fakeEmail: user.email }) @@ -63,6 +66,11 @@ describe('Device tokens API', () => { ` }) + after(async () => { + // clean up + await getRepository(UserDeviceToken).delete({ user: { id: user.id } }) + }) + context('when id in input is not null', () => { context('when token exists', () => { before(() => { @@ -137,4 +145,43 @@ describe('Device tokens API', () => { return graphqlRequest(query, invalidAuthToken).expect(500) }) }) + + describe('Get device tokens', () => { + const token = 'Some token' + + const query = ` + query { + deviceTokens { + ... on DeviceTokensSuccess { + deviceTokens { + id + token + createdAt + } + } + ... on DeviceTokensError { + errorCodes + } + } + } + ` + + before(async () => { + // create test device token + await getRepository(UserDeviceToken).save({ + user: { id: user.id }, + token, + }) + }) + + after(async () => { + // clean up + await getRepository(UserDeviceToken).delete({ token }) + }) + + it('responds with status code 200 and returns all device tokens', async () => { + const response = await graphqlRequest(query, authToken).expect(200) + expect(response.body.data.deviceTokens.deviceTokens).to.have.lengthOf(1) + }) + }) })