mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #1035 from omnivore-app/feature/sync-api
feature/sync api
This commit is contained in:
commit
92d3ab0195
11 changed files with 427 additions and 70 deletions
|
|
@ -18,7 +18,7 @@ export const addLabelInPage = async (
|
|||
ctx._source.labels = [params.label];
|
||||
ctx._source.updatedAt = params.updatedAt
|
||||
} else if (!ctx._source.labels.any(label -> label.name == params.label.name)) {
|
||||
ctx._source.labels.add(params.label) ;
|
||||
ctx._source.labels.add(params.label);
|
||||
ctx._source.updatedAt = params.updatedAt
|
||||
} else { ctx.op = 'none' }`,
|
||||
lang: 'painless',
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import {
|
|||
ArticleSavingRequestStatus,
|
||||
Page,
|
||||
PageContext,
|
||||
PageSearchArgs,
|
||||
PageType,
|
||||
ParamSet,
|
||||
SearchBody,
|
||||
|
|
@ -17,7 +18,6 @@ import {
|
|||
ReadFilter,
|
||||
SortBy,
|
||||
SortOrder,
|
||||
SortParams,
|
||||
} from '../utils/search'
|
||||
import { client, INDEX_ALIAS } from './index'
|
||||
import { EntityType } from '../datalayer/pubsub'
|
||||
|
|
@ -224,12 +224,16 @@ export const updatePage = async (
|
|||
|
||||
if (body.result !== 'updated') return false
|
||||
|
||||
if (page.state === ArticleSavingRequestStatus.Deleted) {
|
||||
await ctx.pubsub.entityDeleted(EntityType.PAGE, id, ctx.uid)
|
||||
return true
|
||||
}
|
||||
|
||||
await ctx.pubsub.entityUpdated<Partial<Page>>(
|
||||
EntityType.PAGE,
|
||||
{ ...page, id },
|
||||
ctx.uid
|
||||
)
|
||||
|
||||
return true
|
||||
} catch (e) {
|
||||
if (
|
||||
|
|
@ -255,11 +259,7 @@ export const deletePage = async (
|
|||
refresh: ctx.refresh,
|
||||
})
|
||||
|
||||
if (body.deleted === 0) return false
|
||||
|
||||
await ctx.pubsub.entityDeleted(EntityType.PAGE, id, ctx.uid)
|
||||
|
||||
return true
|
||||
return body.deleted !== 0
|
||||
} catch (e) {
|
||||
if (
|
||||
e instanceof ResponseError &&
|
||||
|
|
@ -337,21 +337,7 @@ export const getPageById = async (id: string): Promise<Page | undefined> => {
|
|||
}
|
||||
|
||||
export const searchPages = async (
|
||||
args: {
|
||||
from?: number
|
||||
size?: number
|
||||
sort?: SortParams
|
||||
query?: string
|
||||
inFilter?: InFilter
|
||||
readFilter?: ReadFilter
|
||||
typeFilter?: PageType
|
||||
labelFilters: LabelFilter[]
|
||||
hasFilters: HasFilter[]
|
||||
dateFilters: DateFilter[]
|
||||
termFilters?: FieldFilter[]
|
||||
matchFilters?: FieldFilter[]
|
||||
includePending?: boolean | null
|
||||
},
|
||||
args: PageSearchArgs,
|
||||
userId: string
|
||||
): Promise<[Page[], number] | undefined> => {
|
||||
try {
|
||||
|
|
@ -373,10 +359,10 @@ export const searchPages = async (
|
|||
const sortOrder = sort?.order || SortOrder.DESCENDING
|
||||
// default sort by saved_at
|
||||
const sortField = sort?.by || SortBy.SAVED
|
||||
const includeLabels = labelFilters.filter(
|
||||
const includeLabels = labelFilters?.filter(
|
||||
(filter) => filter.type === LabelFilterType.INCLUDE
|
||||
)
|
||||
const excludeLabels = labelFilters.filter(
|
||||
const excludeLabels = labelFilters?.filter(
|
||||
(filter) => filter.type === LabelFilterType.EXCLUDE
|
||||
)
|
||||
|
||||
|
|
@ -421,16 +407,16 @@ export const searchPages = async (
|
|||
if (readFilter !== ReadFilter.ALL) {
|
||||
appendReadFilter(body, readFilter)
|
||||
}
|
||||
if (hasFilters.length > 0) {
|
||||
if (hasFilters && hasFilters.length > 0) {
|
||||
appendHasFilters(body, hasFilters)
|
||||
}
|
||||
if (includeLabels.length > 0) {
|
||||
if (includeLabels && includeLabels.length > 0) {
|
||||
appendIncludeLabelFilter(body, includeLabels)
|
||||
}
|
||||
if (excludeLabels.length > 0) {
|
||||
if (excludeLabels && excludeLabels.length > 0) {
|
||||
appendExcludeLabelFilter(body, excludeLabels)
|
||||
}
|
||||
if (dateFilters.length > 0) {
|
||||
if (dateFilters && dateFilters.length > 0) {
|
||||
appendDateFilters(body, dateFilters)
|
||||
}
|
||||
if (termFilters) {
|
||||
|
|
@ -448,6 +434,14 @@ export const searchPages = async (
|
|||
})
|
||||
}
|
||||
|
||||
if (!args.includeDeleted) {
|
||||
body.query.bool.must_not.push({
|
||||
term: {
|
||||
state: ArticleSavingRequestStatus.Deleted,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
console.log('searching pages in elastic', JSON.stringify(body))
|
||||
|
||||
const response = await client.search<SearchResponse<Page>, SearchBody>({
|
||||
|
|
@ -533,6 +527,7 @@ export const deletePagesByParam = async <K extends keyof ParamSet>(
|
|||
const { body } = await client.deleteByQuery({
|
||||
index: INDEX_ALIAS,
|
||||
body: params,
|
||||
conflicts: 'proceed',
|
||||
})
|
||||
|
||||
if (body.deleted > 0) {
|
||||
|
|
@ -566,6 +561,11 @@ export const searchAsYouType = async (
|
|||
userId,
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
state: ArticleSavingRequestStatus.Succeeded,
|
||||
},
|
||||
},
|
||||
{
|
||||
multi_match: {
|
||||
query,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
// Define the type of the body for the Search request
|
||||
import { PickTuple } from '../util'
|
||||
import { PubsubClient } from '../datalayer/pubsub'
|
||||
import {
|
||||
DateFilter,
|
||||
FieldFilter,
|
||||
HasFilter,
|
||||
InFilter,
|
||||
LabelFilter,
|
||||
ReadFilter,
|
||||
SortParams,
|
||||
} from '../utils/search'
|
||||
|
||||
export interface SearchBody {
|
||||
query: {
|
||||
|
|
@ -146,6 +155,7 @@ export enum ArticleSavingRequestStatus {
|
|||
Failed = 'FAILED',
|
||||
Processing = 'PROCESSING',
|
||||
Succeeded = 'SUCCEEDED',
|
||||
Deleted = 'DELETED',
|
||||
}
|
||||
|
||||
export interface Label {
|
||||
|
|
@ -245,3 +255,20 @@ export interface PageContext {
|
|||
refresh?: boolean
|
||||
uid: string
|
||||
}
|
||||
|
||||
export interface PageSearchArgs {
|
||||
from?: number
|
||||
size?: number
|
||||
sort?: SortParams
|
||||
query?: string
|
||||
inFilter?: InFilter
|
||||
readFilter?: ReadFilter
|
||||
typeFilter?: PageType
|
||||
labelFilters?: LabelFilter[]
|
||||
hasFilters?: HasFilter[]
|
||||
dateFilters?: DateFilter[]
|
||||
termFilters?: FieldFilter[]
|
||||
matchFilters?: FieldFilter[]
|
||||
includePending?: boolean | null
|
||||
includeDeleted?: boolean
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,6 +183,7 @@ export enum ArticleSavingRequestErrorCode {
|
|||
export type ArticleSavingRequestResult = ArticleSavingRequestError | ArticleSavingRequestSuccess;
|
||||
|
||||
export enum ArticleSavingRequestStatus {
|
||||
Deleted = 'DELETED',
|
||||
Failed = 'FAILED',
|
||||
Processing = 'PROCESSING',
|
||||
Succeeded = 'SUCCEEDED'
|
||||
|
|
@ -1264,6 +1265,7 @@ export type Query = {
|
|||
sharedArticle: SharedArticleResult;
|
||||
subscriptions: SubscriptionsResult;
|
||||
typeaheadSearch: TypeaheadSearchResult;
|
||||
updatesSince: UpdatesSinceResult;
|
||||
user: UserResult;
|
||||
users: UsersResult;
|
||||
validateUsername: Scalars['Boolean'];
|
||||
|
|
@ -1341,6 +1343,13 @@ export type QueryTypeaheadSearchArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type QueryUpdatesSinceArgs = {
|
||||
after?: InputMaybe<Scalars['String']>;
|
||||
first?: InputMaybe<Scalars['Int']>;
|
||||
since: Scalars['Date'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryUserArgs = {
|
||||
userId?: InputMaybe<Scalars['ID']>;
|
||||
username?: InputMaybe<Scalars['String']>;
|
||||
|
|
@ -1892,6 +1901,14 @@ export type SubscriptionsSuccess = {
|
|||
subscriptions: Array<Subscription>;
|
||||
};
|
||||
|
||||
export type SyncUpdatedItemEdge = {
|
||||
__typename?: 'SyncUpdatedItemEdge';
|
||||
cursor: Scalars['String'];
|
||||
itemID: Scalars['ID'];
|
||||
node?: Maybe<SearchItem>;
|
||||
updateReason: UpdateReason;
|
||||
};
|
||||
|
||||
export type TypeaheadSearchError = {
|
||||
__typename?: 'TypeaheadSearchError';
|
||||
errorCodes: Array<TypeaheadSearchErrorCode>;
|
||||
|
|
@ -2059,6 +2076,12 @@ export type UpdatePageSuccess = {
|
|||
updatedPage: Article;
|
||||
};
|
||||
|
||||
export enum UpdateReason {
|
||||
Created = 'CREATED',
|
||||
Deleted = 'DELETED',
|
||||
Updated = 'UPDATED'
|
||||
}
|
||||
|
||||
export type UpdateReminderError = {
|
||||
__typename?: 'UpdateReminderError';
|
||||
errorCodes: Array<UpdateReminderErrorCode>;
|
||||
|
|
@ -2158,6 +2181,23 @@ export type UpdateUserSuccess = {
|
|||
user: User;
|
||||
};
|
||||
|
||||
export type UpdatesSinceError = {
|
||||
__typename?: 'UpdatesSinceError';
|
||||
errorCodes: Array<UpdatesSinceErrorCode>;
|
||||
};
|
||||
|
||||
export enum UpdatesSinceErrorCode {
|
||||
Unauthorized = 'UNAUTHORIZED'
|
||||
}
|
||||
|
||||
export type UpdatesSinceResult = UpdatesSinceError | UpdatesSinceSuccess;
|
||||
|
||||
export type UpdatesSinceSuccess = {
|
||||
__typename?: 'UpdatesSinceSuccess';
|
||||
edges: Array<SyncUpdatedItemEdge>;
|
||||
pageInfo: PageInfo;
|
||||
};
|
||||
|
||||
export type UploadFileRequestError = {
|
||||
__typename?: 'UploadFileRequestError';
|
||||
errorCodes: Array<UploadFileRequestErrorCode>;
|
||||
|
|
@ -2653,6 +2693,7 @@ export type ResolversTypes = {
|
|||
SubscriptionsErrorCode: SubscriptionsErrorCode;
|
||||
SubscriptionsResult: ResolversTypes['SubscriptionsError'] | ResolversTypes['SubscriptionsSuccess'];
|
||||
SubscriptionsSuccess: ResolverTypeWrapper<SubscriptionsSuccess>;
|
||||
SyncUpdatedItemEdge: ResolverTypeWrapper<SyncUpdatedItemEdge>;
|
||||
TypeaheadSearchError: ResolverTypeWrapper<TypeaheadSearchError>;
|
||||
TypeaheadSearchErrorCode: TypeaheadSearchErrorCode;
|
||||
TypeaheadSearchItem: ResolverTypeWrapper<TypeaheadSearchItem>;
|
||||
|
|
@ -2687,6 +2728,7 @@ export type ResolversTypes = {
|
|||
UpdatePageInput: UpdatePageInput;
|
||||
UpdatePageResult: ResolversTypes['UpdatePageError'] | ResolversTypes['UpdatePageSuccess'];
|
||||
UpdatePageSuccess: ResolverTypeWrapper<UpdatePageSuccess>;
|
||||
UpdateReason: UpdateReason;
|
||||
UpdateReminderError: ResolverTypeWrapper<UpdateReminderError>;
|
||||
UpdateReminderErrorCode: UpdateReminderErrorCode;
|
||||
UpdateReminderInput: UpdateReminderInput;
|
||||
|
|
@ -2707,6 +2749,10 @@ export type ResolversTypes = {
|
|||
UpdateUserProfileSuccess: ResolverTypeWrapper<UpdateUserProfileSuccess>;
|
||||
UpdateUserResult: ResolversTypes['UpdateUserError'] | ResolversTypes['UpdateUserSuccess'];
|
||||
UpdateUserSuccess: ResolverTypeWrapper<UpdateUserSuccess>;
|
||||
UpdatesSinceError: ResolverTypeWrapper<UpdatesSinceError>;
|
||||
UpdatesSinceErrorCode: UpdatesSinceErrorCode;
|
||||
UpdatesSinceResult: ResolversTypes['UpdatesSinceError'] | ResolversTypes['UpdatesSinceSuccess'];
|
||||
UpdatesSinceSuccess: ResolverTypeWrapper<UpdatesSinceSuccess>;
|
||||
UploadFileRequestError: ResolverTypeWrapper<UploadFileRequestError>;
|
||||
UploadFileRequestErrorCode: UploadFileRequestErrorCode;
|
||||
UploadFileRequestInput: UploadFileRequestInput;
|
||||
|
|
@ -2950,6 +2996,7 @@ export type ResolversParentTypes = {
|
|||
SubscriptionsError: SubscriptionsError;
|
||||
SubscriptionsResult: ResolversParentTypes['SubscriptionsError'] | ResolversParentTypes['SubscriptionsSuccess'];
|
||||
SubscriptionsSuccess: SubscriptionsSuccess;
|
||||
SyncUpdatedItemEdge: SyncUpdatedItemEdge;
|
||||
TypeaheadSearchError: TypeaheadSearchError;
|
||||
TypeaheadSearchItem: TypeaheadSearchItem;
|
||||
TypeaheadSearchResult: ResolversParentTypes['TypeaheadSearchError'] | ResolversParentTypes['TypeaheadSearchSuccess'];
|
||||
|
|
@ -2993,6 +3040,9 @@ export type ResolversParentTypes = {
|
|||
UpdateUserProfileSuccess: UpdateUserProfileSuccess;
|
||||
UpdateUserResult: ResolversParentTypes['UpdateUserError'] | ResolversParentTypes['UpdateUserSuccess'];
|
||||
UpdateUserSuccess: UpdateUserSuccess;
|
||||
UpdatesSinceError: UpdatesSinceError;
|
||||
UpdatesSinceResult: ResolversParentTypes['UpdatesSinceError'] | ResolversParentTypes['UpdatesSinceSuccess'];
|
||||
UpdatesSinceSuccess: UpdatesSinceSuccess;
|
||||
UploadFileRequestError: UploadFileRequestError;
|
||||
UploadFileRequestInput: UploadFileRequestInput;
|
||||
UploadFileRequestResult: ResolversParentTypes['UploadFileRequestError'] | ResolversParentTypes['UploadFileRequestSuccess'];
|
||||
|
|
@ -3774,6 +3824,7 @@ export type QueryResolvers<ContextType = ResolverContext, ParentType extends Res
|
|||
sharedArticle?: Resolver<ResolversTypes['SharedArticleResult'], ParentType, ContextType, RequireFields<QuerySharedArticleArgs, 'slug' | 'username'>>;
|
||||
subscriptions?: Resolver<ResolversTypes['SubscriptionsResult'], ParentType, ContextType, Partial<QuerySubscriptionsArgs>>;
|
||||
typeaheadSearch?: Resolver<ResolversTypes['TypeaheadSearchResult'], ParentType, ContextType, RequireFields<QueryTypeaheadSearchArgs, 'query'>>;
|
||||
updatesSince?: Resolver<ResolversTypes['UpdatesSinceResult'], ParentType, ContextType, RequireFields<QueryUpdatesSinceArgs, 'since'>>;
|
||||
user?: Resolver<ResolversTypes['UserResult'], ParentType, ContextType, Partial<QueryUserArgs>>;
|
||||
users?: Resolver<ResolversTypes['UsersResult'], ParentType, ContextType>;
|
||||
validateUsername?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<QueryValidateUsernameArgs, 'username'>>;
|
||||
|
|
@ -4116,6 +4167,14 @@ export type SubscriptionsSuccessResolvers<ContextType = ResolverContext, ParentT
|
|||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type SyncUpdatedItemEdgeResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['SyncUpdatedItemEdge'] = ResolversParentTypes['SyncUpdatedItemEdge']> = {
|
||||
cursor?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
itemID?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
node?: Resolver<Maybe<ResolversTypes['SearchItem']>, ParentType, ContextType>;
|
||||
updateReason?: Resolver<ResolversTypes['UpdateReason'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type TypeaheadSearchErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['TypeaheadSearchError'] = ResolversParentTypes['TypeaheadSearchError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['TypeaheadSearchErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
|
|
@ -4279,6 +4338,21 @@ export type UpdateUserSuccessResolvers<ContextType = ResolverContext, ParentType
|
|||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type UpdatesSinceErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdatesSinceError'] = ResolversParentTypes['UpdatesSinceError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['UpdatesSinceErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type UpdatesSinceResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdatesSinceResult'] = ResolversParentTypes['UpdatesSinceResult']> = {
|
||||
__resolveType: TypeResolveFn<'UpdatesSinceError' | 'UpdatesSinceSuccess', ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type UpdatesSinceSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdatesSinceSuccess'] = ResolversParentTypes['UpdatesSinceSuccess']> = {
|
||||
edges?: Resolver<Array<ResolversTypes['SyncUpdatedItemEdge']>, ParentType, ContextType>;
|
||||
pageInfo?: Resolver<ResolversTypes['PageInfo'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type UploadFileRequestErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UploadFileRequestError'] = ResolversParentTypes['UploadFileRequestError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['UploadFileRequestErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
|
|
@ -4571,6 +4645,7 @@ export type Resolvers<ContextType = ResolverContext> = {
|
|||
SubscriptionsError?: SubscriptionsErrorResolvers<ContextType>;
|
||||
SubscriptionsResult?: SubscriptionsResultResolvers<ContextType>;
|
||||
SubscriptionsSuccess?: SubscriptionsSuccessResolvers<ContextType>;
|
||||
SyncUpdatedItemEdge?: SyncUpdatedItemEdgeResolvers<ContextType>;
|
||||
TypeaheadSearchError?: TypeaheadSearchErrorResolvers<ContextType>;
|
||||
TypeaheadSearchItem?: TypeaheadSearchItemResolvers<ContextType>;
|
||||
TypeaheadSearchResult?: TypeaheadSearchResultResolvers<ContextType>;
|
||||
|
|
@ -4605,6 +4680,9 @@ export type Resolvers<ContextType = ResolverContext> = {
|
|||
UpdateUserProfileSuccess?: UpdateUserProfileSuccessResolvers<ContextType>;
|
||||
UpdateUserResult?: UpdateUserResultResolvers<ContextType>;
|
||||
UpdateUserSuccess?: UpdateUserSuccessResolvers<ContextType>;
|
||||
UpdatesSinceError?: UpdatesSinceErrorResolvers<ContextType>;
|
||||
UpdatesSinceResult?: UpdatesSinceResultResolvers<ContextType>;
|
||||
UpdatesSinceSuccess?: UpdatesSinceSuccessResolvers<ContextType>;
|
||||
UploadFileRequestError?: UploadFileRequestErrorResolvers<ContextType>;
|
||||
UploadFileRequestResult?: UploadFileRequestResultResolvers<ContextType>;
|
||||
UploadFileRequestSuccess?: UploadFileRequestSuccessResolvers<ContextType>;
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ enum ArticleSavingRequestErrorCode {
|
|||
union ArticleSavingRequestResult = ArticleSavingRequestError | ArticleSavingRequestSuccess
|
||||
|
||||
enum ArticleSavingRequestStatus {
|
||||
DELETED
|
||||
FAILED
|
||||
PROCESSING
|
||||
SUCCEEDED
|
||||
|
|
@ -918,6 +919,7 @@ type Query {
|
|||
sharedArticle(selectedHighlightId: String, slug: String!, username: String!): SharedArticleResult!
|
||||
subscriptions(sort: SortParams): SubscriptionsResult!
|
||||
typeaheadSearch(first: Int, query: String!): TypeaheadSearchResult!
|
||||
updatesSince(after: String, first: Int, since: Date!): UpdatesSinceResult!
|
||||
user(userId: ID, username: String): UserResult!
|
||||
users: UsersResult!
|
||||
validateUsername(username: String!): Boolean!
|
||||
|
|
@ -1419,6 +1421,13 @@ type SubscriptionsSuccess {
|
|||
subscriptions: [Subscription!]!
|
||||
}
|
||||
|
||||
type SyncUpdatedItemEdge {
|
||||
cursor: String!
|
||||
itemID: ID!
|
||||
node: SearchItem
|
||||
updateReason: UpdateReason!
|
||||
}
|
||||
|
||||
type TypeaheadSearchError {
|
||||
errorCodes: [TypeaheadSearchErrorCode!]!
|
||||
}
|
||||
|
|
@ -1571,6 +1580,12 @@ type UpdatePageSuccess {
|
|||
updatedPage: Article!
|
||||
}
|
||||
|
||||
enum UpdateReason {
|
||||
CREATED
|
||||
DELETED
|
||||
UPDATED
|
||||
}
|
||||
|
||||
type UpdateReminderError {
|
||||
errorCodes: [UpdateReminderErrorCode!]!
|
||||
}
|
||||
|
|
@ -1662,6 +1677,21 @@ type UpdateUserSuccess {
|
|||
user: User!
|
||||
}
|
||||
|
||||
type UpdatesSinceError {
|
||||
errorCodes: [UpdatesSinceErrorCode!]!
|
||||
}
|
||||
|
||||
enum UpdatesSinceErrorCode {
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union UpdatesSinceResult = UpdatesSinceError | UpdatesSinceSuccess
|
||||
|
||||
type UpdatesSinceSuccess {
|
||||
edges: [SyncUpdatedItemEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type UploadFileRequestError {
|
||||
errorCodes: [UploadFileRequestErrorCode!]!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
QueryArticlesArgs,
|
||||
QuerySearchArgs,
|
||||
QueryTypeaheadSearchArgs,
|
||||
QueryUpdatesSinceArgs,
|
||||
ResolverFn,
|
||||
SaveArticleReadingProgressError,
|
||||
SaveArticleReadingProgressErrorCode,
|
||||
|
|
@ -39,8 +40,11 @@ import {
|
|||
TypeaheadSearchError,
|
||||
TypeaheadSearchErrorCode,
|
||||
TypeaheadSearchSuccess,
|
||||
UpdateReason,
|
||||
UpdatesSinceError,
|
||||
UpdatesSinceErrorCode,
|
||||
UpdatesSinceSuccess,
|
||||
} from '../../generated/graphql'
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Merge } from '../../util'
|
||||
import {
|
||||
getStorageFileDetails,
|
||||
|
|
@ -69,7 +73,7 @@ import { createImageProxyUrl } from '../../utils/imageproxy'
|
|||
import normalizeUrl from 'normalize-url'
|
||||
import { WithDataSourcesContext } from '../types'
|
||||
|
||||
import { parseSearchQuery } from '../../utils/search'
|
||||
import { parseSearchQuery, SortBy, SortOrder } from '../../utils/search'
|
||||
import { createPageSaveRequest } from '../../services/create_page_save_request'
|
||||
import { analytics } from '../../utils/analytics'
|
||||
import { env } from '../../env'
|
||||
|
|
@ -83,7 +87,6 @@ import {
|
|||
} from '../../elastic/types'
|
||||
import {
|
||||
createPage,
|
||||
deletePage,
|
||||
getPageById,
|
||||
getPageByParam,
|
||||
searchAsYouType,
|
||||
|
|
@ -620,7 +623,7 @@ export const setBookmarkArticleResolver = authorized<
|
|||
async (
|
||||
_,
|
||||
{ input: { articleID, bookmark } },
|
||||
{ models, authTrx, claims: { uid }, log, pubsub }
|
||||
{ claims: { uid }, log, pubsub }
|
||||
) => {
|
||||
const page = await getPageById(articleID)
|
||||
if (!page) {
|
||||
|
|
@ -637,11 +640,15 @@ export const setBookmarkArticleResolver = authorized<
|
|||
return { errorCodes: [SetBookmarkArticleErrorCode.NotFound] }
|
||||
}
|
||||
|
||||
await deletePage(pageRemoved.id, { pubsub, uid })
|
||||
|
||||
const highlightsUnshared = await authTrx(async (tx) => {
|
||||
return models.highlight.unshareAllHighlights(articleID, uid, tx)
|
||||
})
|
||||
// delete the page
|
||||
const deleted = await updatePage(
|
||||
pageRemoved.id,
|
||||
{ state: ArticleSavingRequestStatus.Deleted },
|
||||
{ pubsub, uid }
|
||||
)
|
||||
if (!deleted) {
|
||||
return { errorCodes: [SetBookmarkArticleErrorCode.NotFound] }
|
||||
}
|
||||
|
||||
analytics.track({
|
||||
userId: uid,
|
||||
|
|
@ -657,7 +664,6 @@ export const setBookmarkArticleResolver = authorized<
|
|||
content: undefined,
|
||||
originalHtml: undefined,
|
||||
}),
|
||||
highlightsUnshared: highlightsUnshared.length,
|
||||
labels: {
|
||||
source: 'resolver',
|
||||
resolver: 'setBookmarkArticleResolver',
|
||||
|
|
@ -680,7 +686,13 @@ export const setBookmarkArticleResolver = authorized<
|
|||
userId: uid,
|
||||
slug: generateSlug(page.title),
|
||||
}
|
||||
await updatePage(articleID, pageUpdated, { pubsub, uid })
|
||||
const updated = await updatePage(articleID, pageUpdated, {
|
||||
pubsub,
|
||||
uid,
|
||||
})
|
||||
if (!updated) {
|
||||
return { errorCodes: [SetBookmarkArticleErrorCode.NotFound] }
|
||||
}
|
||||
|
||||
log.info('Article bookmarked', {
|
||||
page: Object.assign({}, page, {
|
||||
|
|
@ -924,3 +936,91 @@ export const typeaheadSearchResolver = authorized<
|
|||
|
||||
return { items: await searchAsYouType(claims.uid, query, first || undefined) }
|
||||
})
|
||||
|
||||
export const updatesSinceResolver = authorized<
|
||||
UpdatesSinceSuccess,
|
||||
UpdatesSinceError,
|
||||
QueryUpdatesSinceArgs
|
||||
>(async (_obj, { since, first, after }, { claims: { uid } }) => {
|
||||
if (!uid) {
|
||||
return { errorCodes: [UpdatesSinceErrorCode.Unauthorized] }
|
||||
}
|
||||
|
||||
analytics.track({
|
||||
userId: uid,
|
||||
event: 'updatesSince',
|
||||
properties: {
|
||||
env: env.server.apiEnv,
|
||||
since,
|
||||
first,
|
||||
after,
|
||||
},
|
||||
})
|
||||
|
||||
const startCursor = after || ''
|
||||
const size = first || 10
|
||||
const startDate = new Date(since)
|
||||
const [pages, totalCount] = (await searchPages(
|
||||
{
|
||||
from: Number(startCursor),
|
||||
size: size + 1, // fetch one more item to get next cursor
|
||||
includeDeleted: true,
|
||||
dateFilters: [{ field: 'updatedAt', startDate }],
|
||||
sort: { by: SortBy.UPDATED, order: SortOrder.ASCENDING },
|
||||
},
|
||||
uid
|
||||
)) || [[], 0]
|
||||
|
||||
const start =
|
||||
startCursor && !isNaN(Number(startCursor)) ? Number(startCursor) : 0
|
||||
const hasNextPage = pages.length > size
|
||||
const endCursor = String(start + pages.length - (hasNextPage ? 1 : 0))
|
||||
|
||||
//TODO: refactor so that the lastCursor included
|
||||
if (hasNextPage) {
|
||||
// remove an extra if exists
|
||||
pages.pop()
|
||||
}
|
||||
|
||||
const edges = pages.map((p) => {
|
||||
const updateReason = getUpdateReason(p, startDate)
|
||||
return {
|
||||
node:
|
||||
updateReason === UpdateReason.Deleted
|
||||
? null
|
||||
: ({
|
||||
...p,
|
||||
image: p.image && createImageProxyUrl(p.image, 88, 88),
|
||||
isArchived: !!p.archivedAt,
|
||||
contentReader:
|
||||
p.pageType === PageType.File
|
||||
? ContentReader.Pdf
|
||||
: ContentReader.Web,
|
||||
} as SearchItem),
|
||||
cursor: endCursor,
|
||||
itemID: p.id,
|
||||
updateReason,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
edges,
|
||||
pageInfo: {
|
||||
hasPreviousPage: false,
|
||||
startCursor,
|
||||
hasNextPage,
|
||||
endCursor,
|
||||
totalCount,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const getUpdateReason = (page: Page, since: Date) => {
|
||||
if (page.state === ArticleSavingRequestStatus.Deleted) {
|
||||
return UpdateReason.Deleted
|
||||
}
|
||||
if (page.createdAt >= since) {
|
||||
return UpdateReason.Created
|
||||
}
|
||||
return UpdateReason.Updated
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ import {
|
|||
updatePageResolver,
|
||||
updateReminderResolver,
|
||||
updateSharedCommentResolver,
|
||||
updatesSinceResolver,
|
||||
updateUserProfileResolver,
|
||||
updateUserResolver,
|
||||
uploadFileRequestResolver,
|
||||
|
|
@ -186,6 +187,7 @@ export const functionResolvers = {
|
|||
webhook: webhookResolver,
|
||||
apiKeys: apiKeysResolver,
|
||||
typeaheadSearch: typeaheadSearchResolver,
|
||||
updatesSince: updatesSinceResolver,
|
||||
},
|
||||
User: {
|
||||
async sharedArticles(
|
||||
|
|
@ -586,4 +588,5 @@ export const functionResolvers = {
|
|||
...resultResolveTypeResolver('RevokeApiKey'),
|
||||
...resultResolveTypeResolver('DeleteAccount'),
|
||||
...resultResolveTypeResolver('TypeaheadSearch'),
|
||||
...resultResolveTypeResolver('UpdatesSince'),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1000,6 +1000,7 @@ const schema = gql`
|
|||
PROCESSING
|
||||
SUCCEEDED
|
||||
FAILED
|
||||
DELETED
|
||||
}
|
||||
|
||||
type ArticleSavingRequest {
|
||||
|
|
@ -1761,6 +1762,34 @@ const schema = gql`
|
|||
siteName: String
|
||||
}
|
||||
|
||||
union UpdatesSinceResult = UpdatesSinceSuccess | UpdatesSinceError
|
||||
|
||||
type UpdatesSinceSuccess {
|
||||
edges: [SyncUpdatedItemEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type SyncUpdatedItemEdge {
|
||||
cursor: String!
|
||||
updateReason: UpdateReason!
|
||||
itemID: ID!
|
||||
node: SearchItem # for created or updated items, null for deletions */
|
||||
}
|
||||
|
||||
enum UpdateReason {
|
||||
CREATED
|
||||
UPDATED
|
||||
DELETED
|
||||
}
|
||||
|
||||
type UpdatesSinceError {
|
||||
errorCodes: [UpdatesSinceErrorCode!]!
|
||||
}
|
||||
|
||||
enum UpdatesSinceErrorCode {
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
# Mutations
|
||||
type Mutation {
|
||||
googleLogin(input: GoogleLoginInput!): LoginResult!
|
||||
|
|
@ -1873,6 +1902,7 @@ const schema = gql`
|
|||
webhook(id: ID!): WebhookResult!
|
||||
apiKeys: ApiKeysResult!
|
||||
typeaheadSearch(query: String!, first: Int): TypeaheadSearchResult!
|
||||
updatesSince(after: String, first: Int, since: Date!): UpdatesSinceResult!
|
||||
}
|
||||
`
|
||||
|
||||
|
|
|
|||
|
|
@ -70,11 +70,7 @@ describe('elastic api', () => {
|
|||
],
|
||||
state: ArticleSavingRequestStatus.Succeeded,
|
||||
}
|
||||
const pageId = await createPage(page, ctx)
|
||||
if (!pageId) {
|
||||
expect.fail('Failed to create page')
|
||||
}
|
||||
page.id = pageId
|
||||
page.id = (await createPage(page, ctx))!
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
|
|
@ -83,12 +79,10 @@ describe('elastic api', () => {
|
|||
})
|
||||
|
||||
describe('createPage', () => {
|
||||
let newPageId: string | undefined
|
||||
let newPageId: string
|
||||
|
||||
after(async () => {
|
||||
if (newPageId) {
|
||||
await deletePage(newPageId, ctx)
|
||||
}
|
||||
await deletePage(newPageId, ctx)
|
||||
})
|
||||
|
||||
it('creates a page', async () => {
|
||||
|
|
@ -108,9 +102,7 @@ describe('elastic api', () => {
|
|||
url: 'https://blog.omnivore.app/testUrl',
|
||||
state: ArticleSavingRequestStatus.Succeeded,
|
||||
}
|
||||
|
||||
newPageId = await createPage(newPageData, ctx)
|
||||
|
||||
newPageId = (await createPage(newPageData, ctx))!
|
||||
expect(newPageId).to.be.a('string')
|
||||
})
|
||||
})
|
||||
|
|
@ -344,9 +336,11 @@ describe('elastic api', () => {
|
|||
})
|
||||
|
||||
describe('searchAsYouType', () => {
|
||||
let pageId: string
|
||||
|
||||
before(async () => {
|
||||
// create a testing page
|
||||
await createPage(
|
||||
pageId = (await createPage(
|
||||
{
|
||||
content: '',
|
||||
createdAt: new Date(),
|
||||
|
|
@ -363,12 +357,12 @@ describe('elastic api', () => {
|
|||
userId,
|
||||
},
|
||||
ctx
|
||||
)
|
||||
))!
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
// delete the testing page
|
||||
await deletePagesByParam({ userId }, ctx)
|
||||
await deletePage(pageId, ctx)
|
||||
})
|
||||
|
||||
it('searches pages', async () => {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { expect } from 'chai'
|
|||
import 'mocha'
|
||||
import { User } from '../../src/entity/user'
|
||||
import chaiString from 'chai-string'
|
||||
import { UploadFileStatus } from '../../src/generated/graphql'
|
||||
import { UpdateReason, UploadFileStatus } from '../../src/generated/graphql'
|
||||
import {
|
||||
ArticleSavingRequestStatus,
|
||||
Highlight,
|
||||
|
|
@ -649,7 +649,7 @@ describe('Article API', () => {
|
|||
let query = ''
|
||||
let articleId = ''
|
||||
let bookmark = true
|
||||
let pageId = ''
|
||||
let pageId: string
|
||||
|
||||
before(async () => {
|
||||
const page: Page = {
|
||||
|
|
@ -667,16 +667,11 @@ describe('Article API', () => {
|
|||
readingProgressAnchorIndex: 0,
|
||||
state: ArticleSavingRequestStatus.Succeeded,
|
||||
}
|
||||
const newPageId = await createPage(page, ctx)
|
||||
if (newPageId) {
|
||||
pageId = newPageId
|
||||
}
|
||||
pageId = (await createPage(page, ctx))!
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
if (pageId) {
|
||||
await deletePage(pageId, ctx)
|
||||
}
|
||||
await deletePage(pageId, ctx)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -684,7 +679,7 @@ describe('Article API', () => {
|
|||
})
|
||||
|
||||
context('when we set a bookmark on an article', () => {
|
||||
before(async () => {
|
||||
before(() => {
|
||||
articleId = pageId
|
||||
bookmark = true
|
||||
})
|
||||
|
|
@ -698,15 +693,15 @@ describe('Article API', () => {
|
|||
})
|
||||
|
||||
context('when we unset a bookmark on an article', () => {
|
||||
before(async () => {
|
||||
before(() => {
|
||||
articleId = pageId
|
||||
bookmark = false
|
||||
})
|
||||
|
||||
it('should delete an article', async () => {
|
||||
await graphqlRequest(query, authToken).expect(200)
|
||||
const pageId = await getPageById(articleId)
|
||||
expect(pageId).to.undefined
|
||||
const page = await getPageById(articleId)
|
||||
expect(page?.state).to.eql(ArticleSavingRequestStatus.Deleted)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -977,4 +972,104 @@ describe('Article API', () => {
|
|||
expect(res.body.data.typeaheadSearch.items[4].id).to.eq(pages[4].id)
|
||||
})
|
||||
})
|
||||
|
||||
describe('UpdatesSince API', () => {
|
||||
const updatesSinceQuery = (since: string) => `
|
||||
query {
|
||||
updatesSince(
|
||||
since: "${since}") {
|
||||
... on UpdatesSinceSuccess {
|
||||
edges {
|
||||
cursor
|
||||
node {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
itemID
|
||||
updateReason
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
endCursor
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
... on UpdatesSinceError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
let since: string
|
||||
let pages: Page[] = []
|
||||
let deletedPages: Page[] = []
|
||||
|
||||
before(async () => {
|
||||
// Create some test pages
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const page: Page = {
|
||||
id: '',
|
||||
hash: '',
|
||||
userId: user.id,
|
||||
pageType: PageType.Article,
|
||||
title: 'test page',
|
||||
content: '',
|
||||
slug: '',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
readingProgressPercent: 0,
|
||||
readingProgressAnchorIndex: 0,
|
||||
url: '',
|
||||
savedAt: new Date(),
|
||||
state: ArticleSavingRequestStatus.Succeeded,
|
||||
}
|
||||
page.id = (await createPage(page, ctx))!
|
||||
pages.push(page)
|
||||
}
|
||||
|
||||
// set the since to be the timestamp before deletion
|
||||
since = pages[4].createdAt.toISOString()
|
||||
|
||||
// Delete some pages
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await updatePage(
|
||||
pages[i].id,
|
||||
{ state: ArticleSavingRequestStatus.Deleted },
|
||||
ctx
|
||||
)
|
||||
deletedPages.push(pages[i])
|
||||
}
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
// Delete all pages
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
await deletePage(pages[i].id, ctx)
|
||||
}
|
||||
})
|
||||
|
||||
it('returns pages deleted after since', async () => {
|
||||
const res = await graphqlRequest(
|
||||
updatesSinceQuery(since),
|
||||
authToken
|
||||
).expect(200)
|
||||
|
||||
expect(res.body.data.updatesSince.edges.length).to.eql(3)
|
||||
expect(res.body.data.updatesSince.edges[0].itemID).to.eq(
|
||||
deletedPages[0].id
|
||||
)
|
||||
expect(res.body.data.updatesSince.edges[1].itemID).to.eq(
|
||||
deletedPages[1].id
|
||||
)
|
||||
expect(res.body.data.updatesSince.edges[2].itemID).to.eq(
|
||||
deletedPages[2].id
|
||||
)
|
||||
expect(res.body.data.updatesSince.edges[2].updateReason).to.eq(
|
||||
UpdateReason.Deleted
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import 'mocha'
|
|||
import { User } from '../../src/entity/user'
|
||||
import { Highlight, Page, PageContext } from '../../src/elastic/types'
|
||||
import { getRepository } from '../../src/entity/utils'
|
||||
import { deletePagesByParam, getPageById } from '../../src/elastic/pages'
|
||||
import { deletePage, getPageById } from '../../src/elastic/pages'
|
||||
import { addLabelInPage } from '../../src/elastic/labels'
|
||||
import { createPubSubClient } from '../../src/datalayer/pubsub'
|
||||
import {
|
||||
|
|
@ -66,7 +66,7 @@ describe('Labels API', () => {
|
|||
|
||||
after(async () => {
|
||||
// clean up
|
||||
await deletePagesByParam({ userId: user.id }, ctx)
|
||||
await deletePage(page.id, ctx)
|
||||
await deleteTestUser(username)
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue