diff --git a/packages/api/src/entity/rule.ts b/packages/api/src/entity/rule.ts index bee6b0ed0..16ce007e1 100644 --- a/packages/api/src/entity/rule.ts +++ b/packages/api/src/entity/rule.ts @@ -9,6 +9,18 @@ import { } from 'typeorm' import { User } from './user' +export enum RuleActionType { + AddLabel = 'ADD_LABEL', + Archive = 'ARCHIVE', + MarkAsRead = 'MARK_AS_READ', + SendNotification = 'SEND_NOTIFICATION', +} + +export interface RuleAction { + type: RuleActionType + params: string[] +} + @Entity({ name: 'rules' }) export class Rule { @PrimaryGeneratedColumn('uuid') @@ -25,7 +37,7 @@ export class Rule { filter!: string @Column('simple-json') - actions!: { type: string; params: string[] }[] + actions!: RuleAction[] @Column('text', { nullable: true }) description?: string | null diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index b5e08b5aa..440df3b19 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -1486,6 +1486,11 @@ export type QueryReminderArgs = { }; +export type QueryRulesArgs = { + enabled?: InputMaybe; +}; + + export type QuerySearchArgs = { after?: InputMaybe; first?: InputMaybe; @@ -4309,7 +4314,7 @@ export type QueryResolvers; recentSearches?: Resolver; reminder?: Resolver>; - rules?: Resolver; + rules?: Resolver>; search?: Resolver>; sendInstallInstructions?: Resolver; sharedArticle?: Resolver>; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 44c004f0c..ac7c546b5 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -1039,7 +1039,7 @@ type Query { newsletterEmails: NewsletterEmailsResult! recentSearches: RecentSearchesResult! reminder(linkId: ID!): ReminderResult! - rules: RulesResult! + rules(enabled: Boolean): RulesResult! search(after: String, first: Int, query: String): SearchResult! sendInstallInstructions: SendInstallInstructionsResult! sharedArticle(selectedHighlightId: String, slug: String!, username: String!): SharedArticleResult! diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index 9ffdd901a..4571292ad 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -59,6 +59,7 @@ import { reminderResolver, reportItemResolver, revokeApiKeyResolver, + rulesResolver, saveArticleReadingProgressResolver, saveFileResolver, savePageResolver, @@ -72,6 +73,7 @@ import { setLabelsForHighlightResolver, setLabelsResolver, setLinkArchivedResolver, + setRuleResolver, setShareArticleResolver, setShareHighlightResolver, setUserPersonalizationResolver, @@ -102,7 +104,6 @@ import { import { getPageByParam } from '../elastic/pages' import { recentSearchesResolver } from './recent_searches' import { optInFeatureResolver } from './features' -import { setRuleResolver } from './rules' /* eslint-disable @typescript-eslint/naming-convention */ type ResultResolveType = { @@ -202,6 +203,7 @@ export const functionResolvers = { updatesSince: updatesSinceResolver, integrations: integrationsResolver, recentSearches: recentSearchesResolver, + rules: rulesResolver, }, User: { async sharedArticles( @@ -613,4 +615,5 @@ export const functionResolvers = { ...resultResolveTypeResolver('RecentSearches'), ...resultResolveTypeResolver('OptInFeature'), ...resultResolveTypeResolver('SetRule'), + ...resultResolveTypeResolver('Rules'), } diff --git a/packages/api/src/resolvers/index.ts b/packages/api/src/resolvers/index.ts index 2f595e336..f303cbc3d 100644 --- a/packages/api/src/resolvers/index.ts +++ b/packages/api/src/resolvers/index.ts @@ -21,3 +21,4 @@ export * from './popular_reads' export * from './webhooks' export * from './api_key' export * from './integrations' +export * from './rules' diff --git a/packages/api/src/resolvers/rules/index.ts b/packages/api/src/resolvers/rules/index.ts index 516e07e79..bbee1b0dc 100644 --- a/packages/api/src/resolvers/rules/index.ts +++ b/packages/api/src/resolvers/rules/index.ts @@ -1,6 +1,10 @@ import { authorized } from '../../utils/helpers' import { MutationSetRuleArgs, + QueryRulesArgs, + RulesError, + RulesErrorCode, + RulesSuccess, SetRuleError, SetRuleErrorCode, SetRuleSuccess, @@ -55,3 +59,49 @@ export const setRuleResolver = authorized< } } }) + +export const rulesResolver = authorized< + RulesSuccess, + RulesError, + QueryRulesArgs +>(async (_, { enabled }, { claims, log }) => { + log.info('Getting rules', { + enabled, + labels: { + source: 'resolver', + resolver: 'rulesResolver', + uid: claims.uid, + }, + }) + + try { + const user = await getRepository(User).findOneBy({ id: claims.uid }) + if (!user) { + return { + errorCodes: [RulesErrorCode.Unauthorized], + } + } + + const rules = await getRepository(Rule).findBy({ + user: { id: claims.uid }, + enabled: enabled === null ? undefined : enabled, + }) + + return { + rules, + } + } catch (error) { + log.error('Error getting rules', { + error, + labels: { + source: 'resolver', + resolver: 'rulesResolver', + uid: claims.uid, + }, + }) + + return { + errorCodes: [RulesErrorCode.BadRequest], + } + } +}) diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 1cf55942b..6c5e96644 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -2154,7 +2154,7 @@ const schema = gql` updatesSince(after: String, first: Int, since: Date!): UpdatesSinceResult! integrations: IntegrationsResult! recentSearches: RecentSearchesResult! - rules: RulesResult! + rules(enabled: Boolean): RulesResult! } ` diff --git a/packages/api/test/resolvers/rules.test.ts b/packages/api/test/resolvers/rules.test.ts index f11fabeec..a3179ea3b 100644 --- a/packages/api/test/resolvers/rules.test.ts +++ b/packages/api/test/resolvers/rules.test.ts @@ -4,7 +4,7 @@ import { graphqlRequest, request } from '../util' import { User } from '../../src/entity/user' import { createTestUser, deleteTestUser } from '../db' import { getRepository } from '../../src/entity/utils' -import { Rule } from '../../src/entity/rule' +import { Rule, RuleAction, RuleActionType } from '../../src/entity/rule' describe('Rules Resolver', () => { const username = 'fakeUser' @@ -31,7 +31,7 @@ describe('Rules Resolver', () => { const setRulesQuery = ( name: string, filter: string, - actions: { type: string; params: string[] }[], + actions: RuleAction[], enabled: boolean, id?: string ) => ` @@ -77,7 +77,7 @@ describe('Rules Resolver', () => { const query = setRulesQuery( 'test rule', 'test filter', - [{ type: 'ADD_LABEL', params: [] }], + [{ type: RuleActionType.SendNotification, params: [] }], true ) @@ -85,4 +85,49 @@ describe('Rules Resolver', () => { expect(res.body.data.setRule.rule.filter).to.equal('test filter') }) }) + + describe('get rules', () => { + before(async () => { + await getRepository(Rule).save({ + user: { id: user.id }, + name: 'test rule', + filter: 'test filter', + actions: [{ type: RuleActionType.SendNotification, params: [] }], + enabled: true, + }) + }) + + after(async () => { + await getRepository(Rule).delete({ user: { id: user.id } }) + }) + + const getRulesQuery = (enabled: boolean | null = null) => ` + query { + rules (enabled: ${enabled}) { + ... on RulesSuccess { + rules { + id + name + filter + actions { + type + params + } + enabled + createdAt + updatedAt + } + } + ... on RulesError { + errorCodes + } + } + } + ` + + it('should get rules', async () => { + const res = await graphqlRequest(getRulesQuery(), authToken).expect(200) + expect(res.body.data.rules.rules.length).to.equal(1) + }) + }) }) diff --git a/packages/rule-handler/src/db.ts b/packages/rule-handler/src/db.ts deleted file mode 100644 index 518c610e0..000000000 --- a/packages/rule-handler/src/db.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { DataSource, EntityTarget, Repository } from 'typeorm' -import { SnakeNamingStrategy } from 'typeorm-naming-strategies' -import * as dotenv from 'dotenv' - -dotenv.config() - -const AppDataSource = new DataSource({ - type: 'postgres', - host: process.env.PG_HOST, - port: Number(process.env.PG_PORT), - schema: 'omnivore', - username: process.env.PG_USER, - password: process.env.PG_PASSWORD, - database: process.env.PG_DB, - logging: ['query', 'info'], - entities: [__dirname + '/entity/**/*{.js,.ts}'], - namingStrategy: new SnakeNamingStrategy(), -}) - -export const createDBConnection = async () => { - await AppDataSource.initialize() -} - -export const closeDBConnection = async () => { - await AppDataSource.destroy() -} - -export const getRepository = (entity: EntityTarget): Repository => { - return AppDataSource.getRepository(entity) -} diff --git a/packages/rule-handler/src/entity/rules.ts b/packages/rule-handler/src/entity/rules.ts deleted file mode 100644 index d508be63f..000000000 --- a/packages/rule-handler/src/entity/rules.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - Column, - CreateDateColumn, - Entity, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm' -import { User } from './user' - -@Entity({ name: 'rules' }) -export class Rules { - @PrimaryGeneratedColumn('uuid') - id!: string - - @ManyToOne(() => User, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'user_id' }) - user!: User - - @Column('text') - name!: string - - @Column('text') - filter!: string - - @Column('simple-json') - actions!: { type: string; params: string[] }[] - - @Column('text', { nullable: true }) - description?: string | null - - @Column('boolean', { default: true }) - enabled!: boolean - - @CreateDateColumn({ default: () => 'CURRENT_TIMESTAMP' }) - createdAt!: Date - - @UpdateDateColumn({ default: () => 'CURRENT_TIMESTAMP' }) - updatedAt!: Date -} diff --git a/packages/rule-handler/src/entity/user.ts b/packages/rule-handler/src/entity/user.ts deleted file mode 100644 index f3ecf261f..000000000 --- a/packages/rule-handler/src/entity/user.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { - Column, - CreateDateColumn, - Entity, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm' - -@Entity() -export class User { - @PrimaryGeneratedColumn('uuid') - id!: string - - @Column('text') - name!: string - - @Column('text') - email!: string - - @Column('text') - sourceUserId!: string - - @CreateDateColumn() - createdAt!: Date - - @UpdateDateColumn() - updatedAt!: Date - - @Column('varchar', { length: 255, nullable: true }) - password?: string -} diff --git a/packages/rule-handler/src/entity/user_device_tokens.ts b/packages/rule-handler/src/entity/user_device_tokens.ts deleted file mode 100644 index 0f642d245..000000000 --- a/packages/rule-handler/src/entity/user_device_tokens.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { - Column, - CreateDateColumn, - Entity, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, -} from 'typeorm' -import { User } from './user' - -@Entity({ name: 'user_device_tokens' }) -export class UserDeviceToken { - @PrimaryGeneratedColumn('uuid') - id!: string - - @Column('text') - token!: string - - @ManyToOne(() => User) - @JoinColumn({ name: 'user_id' }) - user!: User - - @CreateDateColumn() - createdAt!: Date -}