Skip to content

Commit

Permalink
feat(envited.ascs.digital): get profile functionality
Browse files Browse the repository at this point in the history
Signed-off-by: Roy Scheeren <[email protected]>
  • Loading branch information
royscheeren committed Jan 31, 2024
1 parent 8f36504 commit fa2388e
Show file tree
Hide file tree
Showing 21 changed files with 2,482 additions and 41 deletions.
14 changes: 14 additions & 0 deletions apps/envited.ascs.digital/app/companies/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { getProfile } from '../../../common/serverActions'
import { Header } from '../../../modules/Header'

export default async function Index({ params }: { params: { slug: string } }) {
const { slug } = params
const profile = await getProfile(slug)

return (
<>
<Header />
<main>{JSON.stringify(profile)}</main>
</>
)
}
3 changes: 3 additions & 0 deletions apps/envited.ascs.digital/common/database/queries/profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ export const maybeUpdatePublishedState = (db: DatabaseConnection) => async (data

return db.update(profile).set({ isPublished, updatedAt: new Date() }).where(eq(profile.name, data.name)).returning()
}

export const getProfileBySlug = (db: DatabaseConnection) => async (slug: string) =>
db.select().from(profile).where(eq(profile.slug, slug))
13 changes: 11 additions & 2 deletions apps/envited.ascs.digital/common/database/queries/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@ import { fromPairs, map, pipe, toPairs } from 'ramda'
import { connectDb } from '../database'
import * as schema from '../schema'
import { fetchTables } from './common'
import { maybeUpdatePublishedState, update as updateProfile } from './profiles'
import { deleteUserById, getUserById, getUserWithProfileById, getUsersByIssuerId, insertUserTx } from './users'
import { getProfileBySlug, maybeUpdatePublishedState, update as updateProfile } from './profiles'
import {
deleteUserById,
getUserById,
getUserByIssuerId,
getUserWithProfileById,
getUsersByIssuerId,
insertUserTx,
} from './users'

const queries = {
deleteUserById,
Expand All @@ -17,6 +24,8 @@ const queries = {
insertUserTx,
updateProfile,
maybeUpdatePublishedState,
getProfileBySlug,
getUserByIssuerId,
}

export const init =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,14 @@ describe('common/database/users', () => {

const result = await SUT.insertCompanyProfileTx(tx)({
name: 'NAME',
slug: 'name',
isPublished: false,
})

expect(tx.insert).toHaveBeenCalledWith(profile)
expect(tx.insert().values).toHaveBeenCalledWith({
name: 'NAME',
slug: 'name',
isPublished: false,
createdAt: new Date(),
updatedAt: new Date(),
Expand Down
5 changes: 5 additions & 0 deletions apps/envited.ascs.digital/common/database/queries/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import postgres from 'postgres'
import { isEmpty, prop, propOr } from 'ramda'

import { Profile } from '../../types'
import { slugify } from '../../utils/utils'
import * as schema from '../schema'
import {
addressType,
Expand All @@ -32,6 +33,9 @@ export const getUserById = (db: DatabaseConnection) => async (id: string) =>
export const getUserWithProfileById = (db: DatabaseConnection) => async (id: string) =>
db.select().from(user).where(eq(user.id, id)).leftJoin(profile, eq(user.name, profile.name))

export const getUserByIssuerId = (db: DatabaseConnection) => async (issuerId: string) =>
db.select().from(user).where(eq(user.id, issuerId))

export const getUsersByIssuerId = (db: DatabaseConnection) => async (issuerId: string) =>
db.select().from(user).where(eq(user.issuerId, issuerId))

Expand Down Expand Up @@ -200,6 +204,7 @@ export const _txn =

await insertCompanyProfileTx(tx)({
name: credentialSubject.name,
slug: slugify(credentialSubject.name),
streetAddress: credentialSubject.address.streetAddress,
postalCode: credentialSubject.address.postalCode,
addressLocality: credentialSubject.address.addressLocality,
Expand Down
1 change: 1 addition & 0 deletions apps/envited.ascs.digital/common/database/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export const profile = pgTable('profile', {
.unique()
.notNull()
.references(() => user.name),
slug: text('slug').unique().notNull(),
description: text('description'),
logo: text('logo'),
streetAddress: text('street_address'),
Expand Down
7 changes: 3 additions & 4 deletions apps/envited.ascs.digital/common/guards/guards.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { equals, path, pathOr, prop } from 'ramda'
import { equals, pathOr, prop } from 'ramda'

import { Role, Session, User } from '../../common/types/types'

Expand All @@ -13,7 +13,6 @@ export const userIsIssuedByLoggedInUser = (user: User) => (session: Session) =>
equals(prop('issuerId')(user))(pathOr('', ['user', 'pkh'])(session))

export const isOwnProfile = (user: User) => (profile: { name: string }) =>
equals(path(['profile', 'name'])(user))(prop('name')(profile))
equals(prop('name')(user))(prop('name')(profile))

export const isUsersCompanyProfile = (principal: User) => (profile: { name: string }) =>
equals(prop('name')(principal))(prop('name')(profile))
export const isUsersCompanyProfile = isOwnProfile
2 changes: 1 addition & 1 deletion apps/envited.ascs.digital/common/serverActions/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { getUserById, getUsersByIssuerId, insertUser } from './users'
export { updateProfile } from './profiles'
export { updateProfile, getProfile } from './profiles'
157 changes: 130 additions & 27 deletions apps/envited.ascs.digital/common/serverActions/profiles/get.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,94 @@ import * as SUT from './get'

describe('serverActions/profiles/get', () => {
describe('get', () => {
it('should get the profile as expected', async () => {
it('should get the own full profile as expected', async () => {
// when ... we want to get the full profile for a principal user
// then ... it should get the profile as expected
const getServerSessionStub = jest.fn().mockResolvedValue({
user: {
id: 'USER_PKH',
id: 'USER_PRINCIPAL_PKH',
role: Role.principal,
},
})

const dbStub = jest.fn().mockResolvedValue({
getProfileById: jest.fn().mockResolvedValue({
name: 'USER_NAME',
description: 'USER_DESCRIPTION',
principalName: 'USER_PRINCIPAL_NAME',
}),
getUserById: jest.fn().mockResolvedValue({
name: 'USER_NAME',
profile: {
getProfileBySlug: jest.fn().mockResolvedValue([
{
name: 'USER_PRINCIPAL_NAME',
description: 'USER_DESCRIPTION',
principalName: 'USER_PRINCIPAL_NAME',
},
]),
getUserById: jest.fn().mockResolvedValue([
{
name: 'USER_PRINCIPAL_NAME',
},
]),
getUserByIssuerId: jest.fn().mockResolvedValue([
{
name: 'USER_PRINCIPAL_NAME',
},
]),
})

const slug = 'PROFILE_SLUG'

const result = await SUT._get({ db: dbStub, getServerSession: getServerSessionStub })(slug)
const db = await dbStub()
expect(result).toEqual({
name: 'USER_PRINCIPAL_NAME',
description: 'USER_DESCRIPTION',
principalName: 'USER_PRINCIPAL_NAME',
})
expect(getServerSessionStub).toHaveBeenCalledWith()
expect(db.getProfileBySlug).toHaveBeenCalledWith(slug)
expect(db.getUserById).toHaveBeenCalledWith('USER_PRINCIPAL_PKH')
expect(db.getUserByIssuerId).not.toHaveBeenCalled()
})

it('should get the full profile for a principals user as expected', async () => {
// when ... we want to get the full profile for the user of a certain principal
// then ... it should get the profile as expected
const getServerSessionStub = jest.fn().mockResolvedValue({
user: {
id: 'USER_PKH',
role: Role.user,
},
})

const dbStub = jest.fn().mockResolvedValue({
getProfileBySlug: jest.fn().mockResolvedValue([
{
name: 'USER_PRINCIPAL_NAME',
description: 'USER_DESCRIPTION',
principalName: 'USER_PRINCIPAL_NAME',
},
]),
getUserById: jest.fn().mockResolvedValue([
{
name: 'USER_NAME',
},
}),
]),
getUserByIssuerId: jest.fn().mockResolvedValue([
{
name: 'USER_PRINCIPAL_NAME',
},
]),
})

const id = 'PROFILE_ID'
const slug = 'PROFILE_SLUG'

const result = await SUT._get({ db: dbStub, getServerSession: getServerSessionStub })(id)
const result = await SUT._get({ db: dbStub, getServerSession: getServerSessionStub })(slug)
const db = await dbStub()
expect(result).toEqual({
name: 'USER_NAME',
name: 'USER_PRINCIPAL_NAME',
description: 'USER_DESCRIPTION',
principalName: 'USER_PRINCIPAL_NAME',
})
expect(getServerSessionStub).toHaveBeenCalledWith()
expect(db.getProfileById).toHaveBeenCalledWith(id)
expect(db.getProfileBySlug).toHaveBeenCalledWith(slug)
expect(db.getUserById).toHaveBeenCalledWith('USER_PKH')
expect(db.getUserByIssuerId).toHaveBeenCalled()
})

it('should return a limited profile when there is no session', async () => {
Expand All @@ -47,27 +99,78 @@ describe('serverActions/profiles/get', () => {
const getServerSessionStub = jest.fn().mockResolvedValue(null)

const dbStub = jest.fn().mockResolvedValue({
getProfileById: jest.fn().mockResolvedValue({
name: 'USER_NAME',
description: 'USER_DESCRIPTION',
principalName: 'USER_PRINCIPAL_NAME',
}),
getUserById: jest.fn().mockResolvedValue({
name: 'USER_NAME',
profile: {
getProfileBySlug: jest.fn().mockResolvedValue([
{
name: 'USER_NAME',
description: 'USER_DESCRIPTION',
principalName: 'USER_PRINCIPAL_NAME',
},
]),
getUserById: jest.fn().mockResolvedValue([
{
name: 'USER_NAME',
profile: {
name: 'USER_NAME',
},
},
}),
]),
})

const id = 'PROFILE_ID'

const result = await SUT._get({ db: dbStub, getServerSession: getServerSessionStub })(id)
const slug = 'PROFILE_SLUG'

const result = await SUT._get({ db: dbStub, getServerSession: getServerSessionStub })(slug)
const db = await dbStub()
expect(result).toEqual({
name: 'USER_NAME',
description: 'USER_DESCRIPTION',
})
expect(getServerSessionStub).toHaveBeenCalledWith()
expect(db.getProfileBySlug).toHaveBeenCalledWith(slug)
expect(db.getUserById).not.toHaveBeenCalled()
})

it('should throw an error when the profile id is missing', async () => {
// when ... we want to get a profile but the id is missing
// then ... it should throw an error
const getServerSessionStub = jest.fn().mockResolvedValue([
{
user: {
id: 'USER_PRINCIPAL_PKH',
role: Role.principal,
},
},
])

const dbStub = jest.fn().mockResolvedValue({})

const slug = ''

expect.assertions(3)
await expect(() => SUT._get({ db: dbStub, getServerSession: getServerSessionStub })(slug)).rejects.toThrow()
expect(getServerSessionStub).not.toHaveBeenCalledWith()
expect(dbStub).not.toHaveBeenCalled()
})
})

it('should throw an error when the profile is not found', async () => {
// when ... we want to get a non existant profile
// then ... it should throw an error
const getServerSessionStub = jest.fn().mockResolvedValue({
user: {
id: 'USER_PRINCIPAL_PKH',
role: Role.principal,
},
})

const dbStub = jest.fn().mockResolvedValue({
getProfileBySlug: jest.fn().mockResolvedValue(null),
})

const slug = 'NON_EXISTANT_PROFILE_SLUG'
const db = await dbStub()
await expect(() => SUT._get({ db: dbStub, getServerSession: getServerSessionStub })(slug)).rejects.toThrow()
expect(getServerSessionStub).toHaveBeenCalledWith()
expect(dbStub).toHaveBeenCalledWith()
expect(db.getProfileBySlug).toHaveBeenCalledWith(slug)
})
})
13 changes: 6 additions & 7 deletions apps/envited.ascs.digital/common/serverActions/profiles/get.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use server'

import { error } from 'console'
import { isEmpty, isNil, omit } from 'ramda'

import { getServerSession } from '../../auth'
Expand All @@ -9,32 +8,32 @@ import { db } from '../../database/queries'
import { Database } from '../../database/types'
import { isOwnProfile, isUsersCompanyProfile } from '../../guards'
import { Session } from '../../types'
import { badRequestError, notFoundError } from '../../utils'
import { badRequestError, error, notFoundError } from '../../utils'

export const _get =
({ db, getServerSession }: { db: Database; getServerSession: () => Promise<Session | null> }) =>
async (id: string) => {
async (slug: string) => {
try {
if (isNil(id) || isEmpty(id)) {
if (isNil(slug) || isEmpty(slug)) {
throw badRequestError('Missing profile id')
}

const session = await getServerSession()
const connection = await db()
const profile = await connection.getProfileById(id)
const [profile] = await connection.getProfileBySlug(slug)

if (isNil(profile) || isEmpty(profile)) {
throw notFoundError()
}

if (!isNil(session)) {
const user = await connection.getUserById(session.user.id)
const [user] = await connection.getUserById(session.user.id)

if (isOwnProfile(user)(profile)) {
return profile
}

const principal = await connection.getPrincipalByUserId(user.id)
const [principal] = await connection.getUserByIssuerId(user.issuerId)
if (isUsersCompanyProfile(principal)(profile)) {
return profile
}
Expand Down
1 change: 1 addition & 0 deletions apps/envited.ascs.digital/common/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export interface User {
export interface Profile {
id?: string
name: string
slug: string
description?: string | null
logo?: string | null
streetAddress?: string | null
Expand Down
10 changes: 10 additions & 0 deletions apps/envited.ascs.digital/common/utils/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,14 @@ describe('common/utils', () => {
expect(result).toEqual('AscsUser')
})
})

describe('slugify', () => {
it('should create a slug of the string as expected', () => {
// when ... we want to create a slug of the string
// then ... we should get the slug as expected
const result = SUT.slugify('This is a test')

expect(result).toEqual('this-is-a-test')
})
})
})
8 changes: 8 additions & 0 deletions apps/envited.ascs.digital/common/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,11 @@ export const extractIdFromCredential = pathOr('', ['credentialSubject', 'id'])
export const extractIssuerIdFromCredential = pathOr('', ['issuer', 'id'])

export const extractTypeFromCredential = pathOr('', ['credentialSubject', 'type'])

export const slugify = (string: string) =>
string
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '')
Loading

0 comments on commit fa2388e

Please sign in to comment.