Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(server): core IoC #40 - favoriteStreamFactory #3252

Merged
merged 1 commit into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/frontend/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"typescript.suggest.autoImports": true,
"typescript.preferences.importModuleSpecifier": "non-relative",
"javascript.preferences.importModuleSpecifier": "non-relative",
"volar.completion.preferredTagNameCase": "kebab"
"volar.completion.preferredTagNameCase": "kebab",
"vitest.disableWorkspaceWarning": true
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto-added by VSCode extension

}
3 changes: 3 additions & 0 deletions packages/preview-service/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"vitest.disableWorkspaceWarning": true
}
18 changes: 18 additions & 0 deletions packages/server/modules/core/domain/streams/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,17 @@ export type StoreStream = (
}>
) => Promise<Stream>

export type SetStreamFavorited = (params: {
streamId: string
userId: string
favorited?: boolean
}) => Promise<void>

export type CanUserFavoriteStream = (params: {
userId: string
streamId: string
}) => Promise<boolean>

export type DeleteStreamRecords = (streamId: string) => Promise<number>

export type GetOnboardingBaseStream = (version: string) => Promise<Optional<Stream>>
Expand Down Expand Up @@ -224,3 +235,10 @@ export type GetFavoriteStreamsCollection = (params: {
cursor?: string | null | undefined
streamIdWhitelist?: string[] | undefined
}) => Promise<{ totalCount: number; cursor: Nullable<string>; items: Stream[] }>

export type FavoriteStream = (params: {
userId: string
streamId: string
favorited?: boolean | undefined
userResourceAccessRules?: ContextResourceAccessRules
}) => Promise<Stream>
15 changes: 12 additions & 3 deletions packages/server/modules/core/graph/resolvers/streams.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
getStreamUsers,
favoriteStream,
getActiveUserStreamFavoriteDate,
getStreamFavoritesCount,
getOwnedFavoritesCount
Expand Down Expand Up @@ -35,7 +34,9 @@ import {
legacyGetStreamsFactory,
getFavoritedStreamsCountFactory,
getFavoritedStreamsPageFactory,
getStreamCollaboratorsFactory
getStreamCollaboratorsFactory,
canUserFavoriteStreamFactory,
setStreamFavoritedFactory
} from '@/modules/core/repositories/streams'
import {
createStreamReturnRecordFactory,
Expand Down Expand Up @@ -87,7 +88,10 @@ import {
validateStreamAccessFactory
} from '@/modules/core/services/streams/access'
import { getDiscoverableStreamsFactory } from '@/modules/core/services/streams/discoverableStreams'
import { getFavoriteStreamsCollectionFactory } from '@/modules/core/services/streams/favorite'
import {
favoriteStreamFactory,
getFavoriteStreamsCollectionFactory
} from '@/modules/core/services/streams/favorite'

const getFavoriteStreamsCollection = getFavoriteStreamsCollectionFactory({
getFavoritedStreamsCount: getFavoritedStreamsCountFactory({ db }),
Expand Down Expand Up @@ -173,6 +177,11 @@ const getDiscoverableStreams = getDiscoverableStreamsFactory({
countDiscoverableStreams: countDiscoverableStreamsFactory({ db })
})
const getStreams = legacyGetStreamsFactory({ db })
const favoriteStream = favoriteStreamFactory({
canUserFavoriteStream: canUserFavoriteStreamFactory({ db }),
setStreamFavorited: setStreamFavoritedFactory({ db }),
getStream
})

const getUserStreamsCore = async (
forOtherUser: boolean,
Expand Down
113 changes: 55 additions & 58 deletions packages/server/modules/core/repositories/streams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ import {
GetDiscoverableStreamsPage,
LegacyGetStreams,
GetFavoritedStreamsPage,
GetFavoritedStreamsCount
GetFavoritedStreamsCount,
SetStreamFavorited,
CanUserFavoriteStream
} from '@/modules/core/domain/streams/operations'
export type { StreamWithOptionalRole, StreamWithCommitId }

Expand Down Expand Up @@ -341,40 +343,39 @@ export const getFavoritedStreamsCountFactory =
* @param {boolean} [p.favorited] By default favorites the stream, but you can set this
* to false to unfavorite it
*/
export async function setStreamFavorited(params: {
streamId: string
userId: string
favorited?: boolean
}) {
const { streamId, userId, favorited = true } = params
export const setStreamFavoritedFactory =
(deps: { db: Knex }): SetStreamFavorited =>
async (params: { streamId: string; userId: string; favorited?: boolean }) => {
const { streamId, userId, favorited = true } = params

if (!userId || !streamId)
throw new InvalidArgumentError('Invalid stream or user ID', {
info: { userId, streamId }
})

if (!userId || !streamId)
throw new InvalidArgumentError('Invalid stream or user ID', {
info: { userId, streamId }
const favoriteQuery = tables.streamFavorites(deps.db).where({
streamId,
userId
})

const favoriteQuery = StreamFavorites.knex().where({
streamId,
userId
})
if (!favorited) {
await favoriteQuery.del()
return
}

// Upserting the favorite
await tables
.streamFavorites(deps.db)
.insert({
userId,
streamId
})
.onConflict(['streamId', 'userId'])
.ignore()

if (!favorited) {
await favoriteQuery.del()
return
}

// Upserting the favorite
await StreamFavorites.knex()
.insert({
userId,
streamId
})
.onConflict(['streamId', 'userId'])
.ignore()

return
}

/**
* Get favorite metadata for specified user and all specified stream IDs
* @param {Object} p
Expand Down Expand Up @@ -421,39 +422,35 @@ export async function getBatchStreamFavoritesCounts(streamIds: string[]) {

/**
* Check if user can favorite a stream
* @param {Object} p
* @param {string} userId
* @param {string} streamId
* @returns {Promise<boolean>}
*/
export async function canUserFavoriteStream(params: {
userId: string
streamId: string
}) {
const { userId, streamId } = params

if (!userId || !streamId)
throw new InvalidArgumentError('Invalid stream or user ID', {
info: { userId, streamId }
})
export const canUserFavoriteStreamFactory =
(deps: { db: Knex }): CanUserFavoriteStream =>
async (params: { userId: string; streamId: string }) => {
const { userId, streamId } = params

if (!userId || !streamId)
throw new InvalidArgumentError('Invalid stream or user ID', {
info: { userId, streamId }
})

const query = Streams.knex()
.select<Array<Pick<StreamRecord, 'id'>>>([Streams.col.id])
.leftJoin(StreamAcl.name, function () {
this.on(StreamAcl.col.resourceId, Streams.col.id).andOnVal(
StreamAcl.col.userId,
userId
)
})
.where(Streams.col.id, streamId)
.andWhere(function () {
this.where(Streams.col.isPublic, true).orWhereNotNull(StreamAcl.col.resourceId)
})
.limit(1)
const query = tables
.streams(deps.db)
.select<Array<Pick<StreamRecord, 'id'>>>([Streams.col.id])
.leftJoin(StreamAcl.name, function () {
this.on(StreamAcl.col.resourceId, Streams.col.id).andOnVal(
StreamAcl.col.userId,
userId
)
})
.where(Streams.col.id, streamId)
.andWhere(function () {
this.where(Streams.col.isPublic, true).orWhereNotNull(StreamAcl.col.resourceId)
})
.limit(1)

const result = await query
return result?.length > 0
}
const result = await query
return result?.length > 0
}

/**
* Find total favorites of owned streams for specified users
Expand Down
47 changes: 2 additions & 45 deletions packages/server/modules/core/services/streams.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
const { StreamAcl, knex } = require('@/modules/core/dbSchema')
const {
setStreamFavorited,
canUserFavoriteStream,
getStreamFactory
} = require('@/modules/core/repositories/streams')
const { UnauthorizedError, InvalidArgumentError } = require('@/modules/shared/errors')
const { isResourceAllowed } = require('@/modules/core/helpers/token')
const {
TokenResourceIdentifierType
} = require('@/modules/core/graph/generated/graphql')
const { StreamAcl } = require('@/modules/core/dbSchema')
const { InvalidArgumentError } = require('@/modules/shared/errors')

/**
* NOTE: Stop adding stuff to this service, create specialized service modules instead for various domains
Expand All @@ -18,8 +9,6 @@ const {
*/

module.exports = {
setStreamFavorited,

/**
* @returns {Promise<{role: string, id: string, name: string, company: string, avatar: string}[]>}
*/
Expand All @@ -35,38 +24,6 @@ module.exports = {
return await query
},

/**
* Favorite or unfavorite a stream
* @param {Object} p
* @param {string} p.userId
* @param {string} p.streamId
* @param {boolean} [p.favorited] Whether to favorite or unfavorite (true by default)
* @param {import('@/modules/core/helpers/token').ContextResourceAccessRules} [p.userResourceAccessRules] Resource access rules (if any) for the user doing the favoriting
* @returns {Promise<import('@/modules/core/helpers/types').StreamRecord>} Updated stream
*/
async favoriteStream({ userId, streamId, favorited, userResourceAccessRules }) {
// Check if user has access to stream
const canFavorite = await canUserFavoriteStream({ userId, streamId })
const hasResourceAccess = isResourceAllowed({
resourceId: streamId,
resourceAccessRules: userResourceAccessRules,
resourceType: TokenResourceIdentifierType.Project
})
if (!canFavorite || !hasResourceAccess) {
throw new UnauthorizedError("User doesn't have access to the specified stream", {
info: { userId, streamId }
})
}

// Favorite/unfavorite the stream
await setStreamFavorited({ streamId, userId, favorited })

const getStream = getStreamFactory({ db: knex })

// Get updated stream info
return await getStream({ streamId, userId })
},

/**
* Get active user stream favorite date (using dataloader)
* @param {Object} p
Expand Down
40 changes: 39 additions & 1 deletion packages/server/modules/core/services/streams/favorite.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import {
CanUserFavoriteStream,
FavoriteStream,
GetFavoritedStreamsCount,
GetFavoritedStreamsPage,
GetFavoriteStreamsCollection
GetFavoriteStreamsCollection,
GetStream,
SetStreamFavorited
} from '@/modules/core/domain/streams/operations'
import { TokenResourceIdentifierType } from '@/modules/core/domain/tokens/types'
import { isResourceAllowed } from '@/modules/core/helpers/token'
import { UnauthorizedError } from '@/modules/shared/errors'
import { clamp } from 'lodash'

/**
Expand Down Expand Up @@ -35,3 +42,34 @@ export const getFavoriteStreamsCollectionFactory =

return { totalCount, cursor: finalCursor, items: streams }
}

/**
* Favorite or unfavorite a stream
*/
export const favoriteStreamFactory =
(deps: {
canUserFavoriteStream: CanUserFavoriteStream
setStreamFavorited: SetStreamFavorited
getStream: GetStream
}): FavoriteStream =>
async ({ userId, streamId, favorited, userResourceAccessRules }) => {
// Check if user has access to stream
const canFavorite = await deps.canUserFavoriteStream({ userId, streamId })
const hasResourceAccess = isResourceAllowed({
resourceId: streamId,
resourceAccessRules: userResourceAccessRules,
resourceType: TokenResourceIdentifierType.Project
})
if (!canFavorite || !hasResourceAccess) {
throw new UnauthorizedError("User doesn't have access to the specified stream", {
info: { userId, streamId }
})
}

// Favorite/unfavorite the stream
await deps.setStreamFavorited({ streamId, userId, favorited })

// Get updated stream info
const stream = await deps.getStream({ streamId, userId })
return stream! // It should exist, cause we already checked that it does
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { db } from '@/db/knex'
import { Streams, Users } from '@/modules/core/dbSchema'
import {
getStreamFactory,
setStreamFavorited
setStreamFavoritedFactory
} from '@/modules/core/repositories/streams'
import { Nullable, Optional } from '@/modules/shared/helpers/typeHelper'
import { BasicTestUser, createTestUsers } from '@/test/authHelper'
Expand Down Expand Up @@ -32,6 +32,7 @@ const READABLE_DISCOVERABLE_STREAM_COUNT = 15

const cleanup = async () => await truncateTables([Streams.name, Users.name])
const getStream = getStreamFactory({ db })
const setStreamFavorited = setStreamFavoritedFactory({ db })

describe('Discoverable streams', () => {
let apollo: ServerAndContext
Expand Down
3 changes: 3 additions & 0 deletions packages/ui-components/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"vitest.disableWorkspaceWarning": true
}
3 changes: 3 additions & 0 deletions packages/viewer/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"vitest.disableWorkspaceWarning": true
}
Loading