diff --git a/packages/embedder/indexing/orgIdsWithFeatureFlag.ts b/packages/embedder/indexing/orgIdsWithFeatureFlag.ts deleted file mode 100644 index 82d86702e67..00000000000 --- a/packages/embedder/indexing/orgIdsWithFeatureFlag.ts +++ /dev/null @@ -1,15 +0,0 @@ -import getRethink from 'parabol-server/database/rethinkDriver' -import {RDatum} from 'parabol-server/database/stricterR' - -export const orgIdsWithFeatureFlag = async () => { - // I had to add a secondary index to the Organization table to get - // this query to be cheap - const r = await getRethink() - return await r - .table('Organization') - .getAll('relatedDiscussions', {index: 'featureFlagsIndex' as any}) - .filter((r: RDatum) => r('featureFlags').contains('relatedDiscussions')) - .map((r: RDatum) => r('id')) - .coerceTo('array') - .run() -} diff --git a/packages/server/billing/helpers/adjustUserCount.ts b/packages/server/billing/helpers/adjustUserCount.ts index 42618bd4261..649f568f161 100644 --- a/packages/server/billing/helpers/adjustUserCount.ts +++ b/packages/server/billing/helpers/adjustUserCount.ts @@ -1,9 +1,10 @@ import {InvoiceItemType} from 'parabol-client/types/constEnums' import getRethink from '../../database/rethinkDriver' import {RDatum} from '../../database/stricterR' -import Organization from '../../database/types/Organization' import OrganizationUser from '../../database/types/OrganizationUser' import {DataLoaderWorker} from '../../graphql/graphql' +import isValid from '../../graphql/isValid' +import getKysely from '../../postgres/getKysely' import insertOrgUserAudit from '../../postgres/helpers/insertOrgUserAudit' import {OrganizationUserAuditEventTypeEnum} from '../../postgres/queries/generated/insertOrgUserAuditQuery' import {getUserById} from '../../postgres/queries/getUsersByIds' @@ -22,7 +23,7 @@ const maybeUpdateOrganizationActiveDomain = async ( dataLoader: DataLoaderWorker ) => { const r = await getRethink() - const organization = await r.table('Organization').get(orgId).run() + const organization = await dataLoader.get('organizations').load(orgId) const {isActiveDomainTouched, activeDomain} = organization // don't modify if the domain was set manually if (isActiveDomainTouched) return @@ -38,14 +39,18 @@ const maybeUpdateOrganizationActiveDomain = async ( // don't modify if we can't guess the domain or the domain we guess is the current domain const domain = await getActiveDomainForOrgId(orgId) if (!domain || domain === activeDomain) return - - await r - .table('Organization') - .get(orgId) - .update({ - activeDomain: domain - }) - .run() + organization.activeDomain = domain + const pg = getKysely() + await Promise.all([ + pg.updateTable('Organization').set({activeDomain: domain}).where('id', '=', orgId).execute(), + r + .table('Organization') + .get(orgId) + .update({ + activeDomain: domain + }) + .run() + ]) } const changePause = (inactive: boolean) => async (_orgIds: string[], user: IUser) => { @@ -76,18 +81,16 @@ const changePause = (inactive: boolean) => async (_orgIds: string[], user: IUser const addUser = async (orgIds: string[], user: IUser, dataLoader: DataLoaderWorker) => { const {id: userId} = user const r = await getRethink() - const {organizations, organizationUsers} = await r({ - organizationUsers: r + const [rawOrganizations, organizationUsers] = await Promise.all([ + dataLoader.get('organizations').loadMany(orgIds), + r .table('OrganizationUser') .getAll(userId, {index: 'userId'}) .orderBy(r.desc('newUserUntil')) - .coerceTo('array') as unknown as OrganizationUser[], - organizations: r - .table('Organization') - .getAll(r.args(orgIds)) - .coerceTo('array') as unknown as Organization[] - }).run() - + .coerceTo('array') + .run() + ]) + const organizations = rawOrganizations.filter(isValid) const docs = orgIds.map((orgId) => { const oldOrganizationUser = organizationUsers.find( (organizationUser) => organizationUser.orgId === orgId @@ -153,7 +156,6 @@ export default async function adjustUserCount( type: InvoiceItemType, dataLoader: DataLoaderWorker ) { - const r = await getRethink() const orgIds = Array.isArray(orgInput) ? orgInput : [orgInput] const user = (await getUserById(userId))! @@ -164,11 +166,8 @@ export default async function adjustUserCount( const auditEventType = auditEventTypeLookup[type] await insertOrgUserAudit(orgIds, userId, auditEventType) - const paidOrgs = await r - .table('Organization') - .getAll(r.args(orgIds), {index: 'id'}) - .filter((org: RDatum) => org('stripeSubscriptionId').default(null).ne(null)) - .run() + const organizations = await dataLoader.get('organizations').loadMany(orgIds) + const paidOrgs = organizations.filter(isValid).filter((org) => org.stripeSubscriptionId) handleEnterpriseOrgQuantityChanges(paidOrgs, dataLoader).catch() handleTeamOrgQuantityChanges(paidOrgs).catch(Logger.error) diff --git a/packages/server/billing/helpers/generateInvoice.ts b/packages/server/billing/helpers/generateInvoice.ts index 3785a7d6303..1b75d4a6537 100644 --- a/packages/server/billing/helpers/generateInvoice.ts +++ b/packages/server/billing/helpers/generateInvoice.ts @@ -8,7 +8,6 @@ import {InvoiceLineItemEnum} from '../../database/types/InvoiceLineItem' import InvoiceLineItemDetail from '../../database/types/InvoiceLineItemDetail' import InvoiceLineItemOtherAdjustments from '../../database/types/InvoiceLineItemOtherAdjustments' import NextPeriodCharges from '../../database/types/NextPeriodCharges' -import Organization from '../../database/types/Organization' import QuantityChangeLineItem from '../../database/types/QuantityChangeLineItem' import generateUID from '../../generateUID' import {DataLoaderWorker} from '../../graphql/graphql' @@ -354,16 +353,16 @@ export default async function generateInvoice( invoice.status === 'paid' && invoice.status_transitions.paid_at ? fromEpochSeconds(invoice.status_transitions.paid_at) : undefined - - const {organization, billingLeaderIds} = await r({ - organization: r.table('Organization').get(orgId) as unknown as Organization, - billingLeaderIds: r + const [organization, billingLeaderIds] = await Promise.all([ + dataLoader.get('organizations').load(orgId), + r .table('OrganizationUser') .getAll(orgId, {index: 'orgId'}) .filter({removedAt: null}) .filter((row: RDatum) => r.expr(['BILLING_LEADER', 'ORG_ADMIN']).contains(row('role'))) - .coerceTo('array')('userId') as unknown as string[] - }).run() + .coerceTo('array')('userId') + .run() as any as string[] + ]) const billingLeaders = (await dataLoader.get('users').loadMany(billingLeaderIds)).filter(isValid) const billingLeaderEmails = billingLeaders.map((user) => user.email) diff --git a/packages/server/billing/helpers/generateUpcomingInvoice.ts b/packages/server/billing/helpers/generateUpcomingInvoice.ts index 189ab930a18..7980c3871df 100644 --- a/packages/server/billing/helpers/generateUpcomingInvoice.ts +++ b/packages/server/billing/helpers/generateUpcomingInvoice.ts @@ -1,4 +1,3 @@ -import getRethink from '../../database/rethinkDriver' import {DataLoaderWorker} from '../../graphql/graphql' import getUpcomingInvoiceId from '../../utils/getUpcomingInvoiceId' import {getStripeManager} from '../../utils/stripe' @@ -6,9 +5,9 @@ import fetchAllLines from './fetchAllLines' import generateInvoice from './generateInvoice' const generateUpcomingInvoice = async (orgId: string, dataLoader: DataLoaderWorker) => { - const r = await getRethink() const invoiceId = getUpcomingInvoiceId(orgId) - const {stripeId} = await r.table('Organization').get(orgId).pluck('stripeId').run() + const organization = await dataLoader.get('organizations').load(orgId) + const {stripeId} = organization const manager = getStripeManager() const [stripeLineItems, upcomingInvoice] = await Promise.all([ fetchAllLines('upcoming', stripeId), diff --git a/packages/server/billing/helpers/teamLimitsCheck.ts b/packages/server/billing/helpers/teamLimitsCheck.ts index 31103db959b..fff842326db 100644 --- a/packages/server/billing/helpers/teamLimitsCheck.ts +++ b/packages/server/billing/helpers/teamLimitsCheck.ts @@ -80,7 +80,13 @@ export const maybeRemoveRestrictions = async (orgId: string, dataLoader: DataLoa if (!(await isLimitExceeded(orgId))) { const billingLeadersIds = await dataLoader.get('billingLeadersIdsByOrgId').load(orgId) + const pg = getKysely() await Promise.all([ + pg + .updateTable('Organization') + .set({tierLimitExceededAt: null, scheduledLockAt: null, lockedAt: null}) + .where('id', '=', orgId) + .execute(), r .table('Organization') .get(orgId) @@ -129,16 +135,26 @@ export const checkTeamsLimit = async (orgId: string, dataLoader: DataLoaderWorke const now = new Date() const scheduledLockAt = new Date(now.getTime() + ms(`${Threshold.STARTER_TIER_LOCK_AFTER_DAYS}d`)) - - await r - .table('Organization') - .get(orgId) - .update({ - tierLimitExceededAt: now, - scheduledLockAt, - updatedAt: now - }) - .run() + const pg = getKysely() + await Promise.all([ + pg + .updateTable('Organization') + .set({ + tierLimitExceededAt: now, + scheduledLockAt + }) + .where('id', '=', orgId) + .execute(), + r + .table('Organization') + .get(orgId) + .update({ + tierLimitExceededAt: now, + scheduledLockAt, + updatedAt: now + }) + .run() + ]) dataLoader.get('organizations').clear(orgId) const billingLeaders = await getBillingLeadersByOrgId(orgId, dataLoader) diff --git a/packages/server/billing/helpers/terminateSubscription.ts b/packages/server/billing/helpers/terminateSubscription.ts index 0b1c6d3f4c5..1850a614828 100644 --- a/packages/server/billing/helpers/terminateSubscription.ts +++ b/packages/server/billing/helpers/terminateSubscription.ts @@ -1,31 +1,45 @@ import getRethink from '../../database/rethinkDriver' import Organization from '../../database/types/Organization' +import getKysely from '../../postgres/getKysely' import {Logger} from '../../utils/Logger' +import sendToSentry from '../../utils/sendToSentry' import {getStripeManager} from '../../utils/stripe' const terminateSubscription = async (orgId: string) => { const r = await getRethink() + const pg = getKysely() const now = new Date() // flag teams as unpaid - const [rethinkResult] = await Promise.all([ - r({ - organization: r - .table('Organization') - .get(orgId) - .update( - { - // periodEnd should always be redundant, but useful for testing purposes - periodEnd: now, - stripeSubscriptionId: null - }, - {returnChanges: true} - )('changes')(0)('old_val') - .default(null) as unknown as Organization - }).run() + const [pgOrganization, organization] = await Promise.all([ + pg + .with('OldOrg', (qc) => + qc.selectFrom('Organization').select('stripeSubscriptionId').where('id', '=', orgId) + ) + .updateTable('Organization') + .set({periodEnd: now, stripeSubscriptionId: null}) + .where('id', '=', orgId) + .returning((qc) => + qc.selectFrom('OldOrg').select('stripeSubscriptionId').as('stripeSubscriptionId') + ) + .executeTakeFirst(), + r + .table('Organization') + .get(orgId) + .update( + { + // periodEnd should always be redundant, but useful for testing purposes + periodEnd: now, + stripeSubscriptionId: null + }, + {returnChanges: true} + )('changes')(0)('old_val') + .default(null) + .run() as unknown as Organization ]) - const {organization} = rethinkResult const {stripeSubscriptionId} = organization - + if (stripeSubscriptionId !== pgOrganization?.stripeSubscriptionId) { + sendToSentry(new Error(`stripeSubscriptionId mismatch for orgId ${orgId}`)) + } if (stripeSubscriptionId) { const manager = getStripeManager() try { diff --git a/packages/server/database/types/GoogleAnalyzedEntity.ts b/packages/server/database/types/GoogleAnalyzedEntity.ts index e66ce25f7c3..b148d69967e 100644 --- a/packages/server/database/types/GoogleAnalyzedEntity.ts +++ b/packages/server/database/types/GoogleAnalyzedEntity.ts @@ -1,5 +1,3 @@ -import {sql} from 'kysely' - interface Input { lemma?: string name: string @@ -17,8 +15,3 @@ export default class GoogleAnalyzedEntity { this.salience = salience } } - -export const toGoogleAnalyzedEntityPG = (entities: GoogleAnalyzedEntity[]) => - sql< - string[] - >`(select coalesce(array_agg((name, salience, lemma)::"GoogleAnalyzedEntity"), '{}') from json_populate_recordset(null::"GoogleAnalyzedEntity", ${JSON.stringify(entities)}))` diff --git a/packages/server/database/types/processTeamsLimitsJob.ts b/packages/server/database/types/processTeamsLimitsJob.ts index 57beb49c170..562a9698028 100644 --- a/packages/server/database/types/processTeamsLimitsJob.ts +++ b/packages/server/database/types/processTeamsLimitsJob.ts @@ -3,6 +3,7 @@ import sendTeamsLimitEmail from '../../billing/helpers/sendTeamsLimitEmail' import {DataLoaderWorker} from '../../graphql/graphql' import isValid from '../../graphql/isValid' import publishNotification from '../../graphql/public/mutations/helpers/publishNotification' +import getKysely from '../../postgres/getKysely' import NotificationTeamsLimitReminder from './NotificationTeamsLimitReminder' import ScheduledTeamLimitsJob from './ScheduledTeamLimitsJob' @@ -27,7 +28,14 @@ const processTeamsLimitsJob = async (job: ScheduledTeamLimitsJob, dataLoader: Da if (type === 'LOCK_ORGANIZATION') { const now = new Date() - await r.table('Organization').get(orgId).update({lockedAt: now}).run() + await Promise.all([ + getKysely() + .updateTable('Organization') + .set({lockedAt: now}) + .where('id', '=', 'orgId') + .execute(), + r.table('Organization').get(orgId).update({lockedAt: now}).run() + ]) organization.lockedAt = lockedAt } else if (type === 'WARN_ORGANIZATION') { const notificationsToInsert = billingLeadersIds.map((userId) => { diff --git a/packages/server/graphql/mutations/addOrg.ts b/packages/server/graphql/mutations/addOrg.ts index d3c9ca3df03..d6ce8215734 100644 --- a/packages/server/graphql/mutations/addOrg.ts +++ b/packages/server/graphql/mutations/addOrg.ts @@ -62,7 +62,11 @@ export default { const teamId = generateUID() const {email} = viewer await createNewOrg(orgId, orgName, viewerId, email, dataLoader) - await createTeamAndLeader(viewer, {id: teamId, orgId, isOnboardTeam: false, ...newTeam}) + await createTeamAndLeader( + viewer, + {id: teamId, orgId, isOnboardTeam: false, ...newTeam}, + dataLoader + ) const {tms} = authToken // MUTATIVE diff --git a/packages/server/graphql/mutations/addTeam.ts b/packages/server/graphql/mutations/addTeam.ts index 132926cd46c..3b9fb4f5bdb 100644 --- a/packages/server/graphql/mutations/addTeam.ts +++ b/packages/server/graphql/mutations/addTeam.ts @@ -85,7 +85,7 @@ export default { // RESOLUTION const teamId = generateUID() - await createTeamAndLeader(viewer, {id: teamId, isOnboardTeam: false, ...newTeam}) + await createTeamAndLeader(viewer, {id: teamId, isOnboardTeam: false, ...newTeam}, dataLoader) const {tms} = authToken // MUTATIVE diff --git a/packages/server/graphql/mutations/createReflection.ts b/packages/server/graphql/mutations/createReflection.ts index 43b55797825..de7171d3705 100644 --- a/packages/server/graphql/mutations/createReflection.ts +++ b/packages/server/graphql/mutations/createReflection.ts @@ -6,10 +6,10 @@ import getGroupSmartTitle from 'parabol-client/utils/smartGroup/getGroupSmartTit import unlockAllStagesForPhase from 'parabol-client/utils/unlockAllStagesForPhase' import normalizeRawDraftJS from 'parabol-client/validation/normalizeRawDraftJS' import getRethink from '../../database/rethinkDriver' -import {toGoogleAnalyzedEntityPG} from '../../database/types/GoogleAnalyzedEntity' import ReflectionGroup from '../../database/types/ReflectionGroup' import generateUID from '../../generateUID' import getKysely from '../../postgres/getKysely' +import {toGoogleAnalyzedEntity} from '../../postgres/helpers/toGoogleAnalyzedEntity' import {analytics} from '../../utils/analytics/analytics' import {getUserId} from '../../utils/authorization' import publish from '../../utils/publish' @@ -102,7 +102,7 @@ export default { await pg .with('Group', (qc) => qc.insertInto('RetroReflectionGroup').values(reflectionGroup)) .insertInto('RetroReflection') - .values({...reflection, entities: toGoogleAnalyzedEntityPG(entities)}) + .values({...reflection, entities: toGoogleAnalyzedEntity(entities)}) .execute() const groupPhase = phases.find((phase) => phase.phaseType === 'group')! diff --git a/packages/server/graphql/mutations/downgradeToStarter.ts b/packages/server/graphql/mutations/downgradeToStarter.ts index 90f948e3d0c..bbed44fd08c 100644 --- a/packages/server/graphql/mutations/downgradeToStarter.ts +++ b/packages/server/graphql/mutations/downgradeToStarter.ts @@ -1,6 +1,5 @@ import {GraphQLID, GraphQLList, GraphQLNonNull, GraphQLString} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../database/rethinkDriver' import {getUserId, isSuperUser, isUserBillingLeader} from '../../utils/authorization' import publish from '../../utils/publish' import standardError from '../../utils/standardError' @@ -37,7 +36,6 @@ export default { }: {orgId: string; reasonsForLeaving?: TReasonToDowngradeEnum[]; otherTool?: string}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -56,7 +54,8 @@ export default { return standardError(new Error('Other tool name is too long'), {userId: viewerId}) } - const {stripeSubscriptionId, tier} = await r.table('Organization').get(orgId).run() + const {stripeSubscriptionId, tier} = await dataLoader.get('organizations').load(orgId) + dataLoader.get('organizations').clear(orgId) if (tier === 'starter') { return standardError(new Error('Already on free tier'), {userId: viewerId}) @@ -68,6 +67,7 @@ export default { orgId, stripeSubscriptionId!, viewer, + dataLoader, reasonsForLeaving, otherTool ) diff --git a/packages/server/graphql/mutations/helpers/bootstrapNewUser.ts b/packages/server/graphql/mutations/helpers/bootstrapNewUser.ts index 9fb461504f6..8d1d09d3cbd 100644 --- a/packages/server/graphql/mutations/helpers/bootstrapNewUser.ts +++ b/packages/server/graphql/mutations/helpers/bootstrapNewUser.ts @@ -128,7 +128,7 @@ const bootstrapNewUser = async ( const orgName = `${newUser.preferredName}’s Org` await createNewOrg(orgId, orgName, userId, email, dataLoader) await Promise.all([ - createTeamAndLeader(newUser as IUser, validNewTeam), + createTeamAndLeader(newUser as IUser, validNewTeam, dataLoader), addSeedTasks(userId, teamId), r.table('SuggestedAction').insert(new SuggestedActionInviteYourTeam({userId, teamId})).run(), sendPromptToJoinOrg(newUser, dataLoader) diff --git a/packages/server/graphql/mutations/helpers/createNewOrg.ts b/packages/server/graphql/mutations/helpers/createNewOrg.ts index 26eee3a436c..b5fa95ea827 100644 --- a/packages/server/graphql/mutations/helpers/createNewOrg.ts +++ b/packages/server/graphql/mutations/helpers/createNewOrg.ts @@ -1,6 +1,7 @@ import getRethink from '../../../database/rethinkDriver' import Organization from '../../../database/types/Organization' import OrganizationUser from '../../../database/types/OrganizationUser' +import getKysely from '../../../postgres/getKysely' import insertOrgUserAudit from '../../../postgres/helpers/insertOrgUserAudit' import getDomainFromEmail from '../../../utils/getDomainFromEmail' import {DataLoaderWorker} from '../../graphql' @@ -28,6 +29,10 @@ export default async function createNewOrg( tier: org.tier }) await insertOrgUserAudit([orgId], leaderUserId, 'added') + await getKysely() + .insertInto('Organization') + .values({...org, creditCard: null}) + .execute() return r({ org: r.table('Organization').insert(org), organizationUser: r.table('OrganizationUser').insert(orgUser) diff --git a/packages/server/graphql/mutations/helpers/createTeamAndLeader.ts b/packages/server/graphql/mutations/helpers/createTeamAndLeader.ts index f76a2540d27..1019dd0b789 100644 --- a/packages/server/graphql/mutations/helpers/createTeamAndLeader.ts +++ b/packages/server/graphql/mutations/helpers/createTeamAndLeader.ts @@ -4,6 +4,7 @@ import MeetingSettingsPoker from '../../../database/types/MeetingSettingsPoker' import MeetingSettingsRetrospective from '../../../database/types/MeetingSettingsRetrospective' import Team from '../../../database/types/Team' import TimelineEventCreatedTeam from '../../../database/types/TimelineEventCreatedTeam' +import {DataLoaderInstance} from '../../../dataloader/RootDataLoader' import getKysely from '../../../postgres/getKysely' import IUser from '../../../postgres/types/IUser' import addTeamIdToTMS from '../../../safeMutations/addTeamIdToTMS' @@ -17,11 +18,15 @@ interface ValidNewTeam { } // used for addorg, addTeam -export default async function createTeamAndLeader(user: IUser, newTeam: ValidNewTeam) { +export default async function createTeamAndLeader( + user: IUser, + newTeam: ValidNewTeam, + dataLoader: DataLoaderInstance +) { const r = await getRethink() const {id: userId} = user const {id: teamId, orgId} = newTeam - const organization = await r.table('Organization').get(orgId).run() + const organization = await dataLoader.get('organizations').load(orgId) const {tier, trialStartDate} = organization const verifiedTeam = new Team({...newTeam, createdBy: userId, tier, trialStartDate}) const meetingSettings = [ diff --git a/packages/server/graphql/mutations/helpers/hideConversionModal.ts b/packages/server/graphql/mutations/helpers/hideConversionModal.ts index c71115516d5..3e186459c44 100644 --- a/packages/server/graphql/mutations/helpers/hideConversionModal.ts +++ b/packages/server/graphql/mutations/helpers/hideConversionModal.ts @@ -1,4 +1,5 @@ import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' import errorFilter from '../../errorFilter' import {DataLoaderWorker} from '../../graphql' @@ -7,6 +8,11 @@ const hideConversionModal = async (orgId: string, dataLoader: DataLoaderWorker) const {showConversionModal} = organization if (showConversionModal) { const r = await getRethink() + await getKysely() + .updateTable('Organization') + .set({showConversionModal: false}) + .where('id', '=', orgId) + .execute() await r .table('Organization') .get(orgId) diff --git a/packages/server/graphql/mutations/helpers/removeFromOrg.ts b/packages/server/graphql/mutations/helpers/removeFromOrg.ts index 945fc3d4a83..ac4fb4ccca5 100644 --- a/packages/server/graphql/mutations/helpers/removeFromOrg.ts +++ b/packages/server/graphql/mutations/helpers/removeFromOrg.ts @@ -57,7 +57,7 @@ const removeFromOrg = async ( // need to make sure the org doc is updated before adjusting this const {role} = organizationUser if (role && ['BILLING_LEADER', 'ORG_ADMIN'].includes(role)) { - const organization = await r.table('Organization').get(orgId).run() + const organization = await dataLoader.get('organizations').load(orgId) // if no other billing leader, promote the oldest // if team tier & no other member, downgrade to starter const otherBillingLeaders = await r @@ -84,7 +84,7 @@ const removeFromOrg = async ( }) .run() } else if (organization.tier !== 'starter') { - await resolveDowngradeToStarter(orgId, organization.stripeSubscriptionId!, user) + await resolveDowngradeToStarter(orgId, organization.stripeSubscriptionId!, user, dataLoader) } } } diff --git a/packages/server/graphql/mutations/helpers/resolveDowngradeToStarter.ts b/packages/server/graphql/mutations/helpers/resolveDowngradeToStarter.ts index ce82907ecfb..803c767e1e7 100644 --- a/packages/server/graphql/mutations/helpers/resolveDowngradeToStarter.ts +++ b/packages/server/graphql/mutations/helpers/resolveDowngradeToStarter.ts @@ -1,5 +1,5 @@ import getRethink from '../../../database/rethinkDriver' -import Organization from '../../../database/types/Organization' +import {DataLoaderInstance} from '../../../dataloader/RootDataLoader' import getKysely from '../../../postgres/getKysely' import updateTeamByOrgId from '../../../postgres/queries/updateTeamByOrgId' import {analytics} from '../../../utils/analytics/analytics' @@ -13,6 +13,7 @@ const resolveDowngradeToStarter = async ( orgId: string, stripeSubscriptionId: string, user: {id: string; email: string}, + dataLoader: DataLoaderInstance, reasonsForLeaving?: ReasonToDowngradeEnum[], otherTool?: string ) => { @@ -27,7 +28,16 @@ const resolveDowngradeToStarter = async ( } const [org] = await Promise.all([ - r.table('Organization').get(orgId).run() as unknown as Organization, + dataLoader.get('organizations').load(orgId), + pg + .updateTable('Organization') + .set({ + tier: 'starter', + periodEnd: now, + stripeSubscriptionId: null + }) + .where('id', '=', orgId) + .execute(), pg .updateTable('SAML') .set({metadata: null, lastUpdatedBy: user.id}) @@ -49,7 +59,7 @@ const resolveDowngradeToStarter = async ( orgId ) ]) - + dataLoader.get('organizations').clear(orgId) await Promise.all([setUserTierForOrgId(orgId), setTierForOrgUsers(orgId)]) analytics.organizationDowngraded(user, { orgId, diff --git a/packages/server/graphql/mutations/helpers/safeCreateRetrospective.ts b/packages/server/graphql/mutations/helpers/safeCreateRetrospective.ts index 7e49609a4df..c803811cc56 100644 --- a/packages/server/graphql/mutations/helpers/safeCreateRetrospective.ts +++ b/packages/server/graphql/mutations/helpers/safeCreateRetrospective.ts @@ -34,7 +34,7 @@ const safeCreateRetrospective = async ( dataLoader.get('teams').loadNonNull(teamId) ]) - const organization = await r.table('Organization').get(team.orgId).run() + const organization = await dataLoader.get('organizations').load(team.orgId) const {showConversionModal} = organization const meetingId = generateUID() diff --git a/packages/server/graphql/mutations/moveTeamToOrg.ts b/packages/server/graphql/mutations/moveTeamToOrg.ts index 18196676f41..bef33207dd1 100644 --- a/packages/server/graphql/mutations/moveTeamToOrg.ts +++ b/packages/server/graphql/mutations/moveTeamToOrg.ts @@ -30,7 +30,7 @@ const moveToOrg = async ( const su = isSuperUser(authToken) // VALIDATION const [org, teams, isPaidResult] = await Promise.all([ - r.table('Organization').get(orgId).run(), + dataLoader.get('organizations').load(orgId), getTeamsByIds([teamId]), pg .selectFrom('Team') @@ -117,7 +117,7 @@ const moveToOrg = async ( const {newToOrgUserIds} = rethinkResult // if no teams remain on the org, remove it - await safeArchiveEmptyStarterOrganization(currentOrgId) + await safeArchiveEmptyStarterOrganization(currentOrgId, dataLoader) await Promise.all( newToOrgUserIds.map((newUserId) => { diff --git a/packages/server/graphql/mutations/payLater.ts b/packages/server/graphql/mutations/payLater.ts index 54af091b0d2..49a4b6ae794 100644 --- a/packages/server/graphql/mutations/payLater.ts +++ b/packages/server/graphql/mutations/payLater.ts @@ -2,6 +2,7 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import getRethink from '../../database/rethinkDriver' import {RValue} from '../../database/stricterR' +import getKysely from '../../postgres/getKysely' import getPg from '../../postgres/getPg' import {incrementUserPayLaterClickCountQuery} from '../../postgres/queries/generated/incrementUserPayLaterClickCountQuery' import {analytics} from '../../utils/analytics/analytics' @@ -49,6 +50,13 @@ export default { // RESOLUTION const team = await dataLoader.get('teams').loadNonNull(teamId) const {orgId} = team + await getKysely() + .updateTable('Organization') + .set((eb) => ({ + payLaterClickCount: eb('payLaterClickCount', '+', 1) + })) + .where('id', '=', orgId) + .execute() await r .table('Organization') .get(orgId) diff --git a/packages/server/graphql/mutations/updateReflectionContent.ts b/packages/server/graphql/mutations/updateReflectionContent.ts index c716f776884..36b79bfb958 100644 --- a/packages/server/graphql/mutations/updateReflectionContent.ts +++ b/packages/server/graphql/mutations/updateReflectionContent.ts @@ -5,8 +5,8 @@ import isPhaseComplete from 'parabol-client/utils/meetings/isPhaseComplete' import getGroupSmartTitle from 'parabol-client/utils/smartGroup/getGroupSmartTitle' import normalizeRawDraftJS from 'parabol-client/validation/normalizeRawDraftJS' import stringSimilarity from 'string-similarity' -import {toGoogleAnalyzedEntityPG} from '../../database/types/GoogleAnalyzedEntity' import getKysely from '../../postgres/getKysely' +import {toGoogleAnalyzedEntity} from '../../postgres/helpers/toGoogleAnalyzedEntity' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' import standardError from '../../utils/standardError' @@ -88,7 +88,7 @@ export default { .updateTable('RetroReflection') .set({ content: normalizedContent, - entities: toGoogleAnalyzedEntityPG(entities), + entities: toGoogleAnalyzedEntity(entities), sentimentScore, plaintextContent }) diff --git a/packages/server/graphql/private/mutations/changeEmailDomain.ts b/packages/server/graphql/private/mutations/changeEmailDomain.ts index dab120dd709..351bbe85af4 100644 --- a/packages/server/graphql/private/mutations/changeEmailDomain.ts +++ b/packages/server/graphql/private/mutations/changeEmailDomain.ts @@ -55,6 +55,11 @@ const changeEmailDomain: MutationResolvers['changeEmailDomain'] = async ( }) .where('domain', 'like', normalizedOldDomain) .execute(), + pg + .updateTable('Organization') + .set({activeDomain: normalizedNewDomain}) + .where('activeDomain', '=', normalizedOldDomain) + .execute(), r .table('Organization') .filter((row: RDatum) => row('activeDomain').eq(normalizedOldDomain)) diff --git a/packages/server/graphql/private/mutations/draftEnterpriseInvoice.ts b/packages/server/graphql/private/mutations/draftEnterpriseInvoice.ts index 258c70bdeaf..ab2da48431c 100644 --- a/packages/server/graphql/private/mutations/draftEnterpriseInvoice.ts +++ b/packages/server/graphql/private/mutations/draftEnterpriseInvoice.ts @@ -102,6 +102,11 @@ const draftEnterpriseInvoice: MutationResolvers['draftEnterpriseInvoice'] = asyn // create the customer const customer = await manager.createCustomer(orgId, apEmail || user.email) if (customer instanceof Error) throw customer + await getKysely() + .updateTable('Organization') + .set({stripeId: customer.id}) + .where('id', '=', orgId) + .execute() await r.table('Organization').get(orgId).update({stripeId: customer.id}).run() customerId = customer.id } else { @@ -116,6 +121,21 @@ const draftEnterpriseInvoice: MutationResolvers['draftEnterpriseInvoice'] = asyn ) await Promise.all([ + pg + .updateTable('Organization') + .set({ + periodEnd: fromEpochSeconds(subscription.current_period_end), + periodStart: fromEpochSeconds(subscription.current_period_start), + stripeSubscriptionId: subscription.id, + tier: 'enterprise', + tierLimitExceededAt: null, + scheduledLockAt: null, + lockedAt: null, + updatedAt: now, + trialStartDate: null + }) + .where('id', '=', orgId) + .execute(), r({ updatedOrg: r .table('Organization') diff --git a/packages/server/graphql/private/mutations/endTrial.ts b/packages/server/graphql/private/mutations/endTrial.ts index fce952a0eaa..890ffdb0015 100644 --- a/packages/server/graphql/private/mutations/endTrial.ts +++ b/packages/server/graphql/private/mutations/endTrial.ts @@ -19,6 +19,7 @@ const endTrial: MutationResolvers['endTrial'] = async (_source, {orgId}, {dataLo // RESOLUTION await Promise.all([ + pg.updateTable('Organization').set({trialStartDate: null}).where('id', '=', orgId).execute(), r({ orgUpdate: r.table('Organization').get(orgId).update({ trialStartDate: null, diff --git a/packages/server/graphql/private/mutations/flagConversionModal.ts b/packages/server/graphql/private/mutations/flagConversionModal.ts index b46548d4e92..4ba7bd4b4f1 100644 --- a/packages/server/graphql/private/mutations/flagConversionModal.ts +++ b/packages/server/graphql/private/mutations/flagConversionModal.ts @@ -1,19 +1,27 @@ import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' import {MutationResolvers} from '../resolverTypes' const flagConversionModal: MutationResolvers['flagConversionModal'] = async ( _source, - {active, orgId} + {active, orgId}, + {dataLoader} ) => { const r = await getRethink() // VALIDATION - const organization = await r.table('Organization').get(orgId).run() + const organization = await dataLoader.get('organizations').load(orgId) if (!organization) { return {error: {message: 'Invalid orgId'}} } // RESOLUTION + organization.showConversionModal = active + await getKysely() + .updateTable('Organization') + .set({showConversionModal: active}) + .where('id', '=', orgId) + .execute() await r .table('Organization') .get(orgId) diff --git a/packages/server/graphql/private/mutations/sendUpcomingInvoiceEmails.ts b/packages/server/graphql/private/mutations/sendUpcomingInvoiceEmails.ts index 01642c464df..d49383b0486 100644 --- a/packages/server/graphql/private/mutations/sendUpcomingInvoiceEmails.ts +++ b/packages/server/graphql/private/mutations/sendUpcomingInvoiceEmails.ts @@ -8,6 +8,7 @@ import getRethink from '../../../database/rethinkDriver' import {RDatum, RValue} from '../../../database/stricterR' import UpcomingInvoiceEmailTemplate from '../../../email/UpcomingInvoiceEmailTemplate' import getMailManager from '../../../email/getMailManager' +import getKysely from '../../../postgres/getKysely' import IUser from '../../../postgres/types/IUser' import {MutationResolvers} from '../resolverTypes' @@ -134,6 +135,11 @@ const sendUpcomingInvoiceEmails: MutationResolvers['sendUpcomingInvoiceEmails'] }) ) const orgIds = organizations.map(({id}) => id) + await getKysely() + .updateTable('Organization') + .set({upcomingInvoiceEmailSentAt: now}) + .where('id', 'in', orgIds) + .execute() await r .table('Organization') .getAll(r.args(orgIds)) diff --git a/packages/server/graphql/private/mutations/setOrganizationDomain.ts b/packages/server/graphql/private/mutations/setOrganizationDomain.ts index cefc6fa5274..b374ed7ce35 100644 --- a/packages/server/graphql/private/mutations/setOrganizationDomain.ts +++ b/packages/server/graphql/private/mutations/setOrganizationDomain.ts @@ -1,19 +1,26 @@ import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' import {MutationResolvers} from '../resolverTypes' const setOrganizationDomain: MutationResolvers['setOrganizationDomain'] = async ( _source, - {orgId, domain} + {orgId, domain}, + {dataLoader} ) => { const r = await getRethink() // VALIDATION - const organization = await r.table('Organization').get(orgId).run() - + const organization = await dataLoader.get('organizations').load(orgId) + dataLoader.get('organizations').clear(orgId) if (!organization) { throw new Error('Organization not found') } // RESOLUTION + await getKysely() + .updateTable('Organization') + .set({activeDomain: domain, isActiveDomainTouched: true}) + .where('id', '=', orgId) + .execute() await r .table('Organization') .get(orgId) diff --git a/packages/server/graphql/private/mutations/startTrial.ts b/packages/server/graphql/private/mutations/startTrial.ts index af4e52efb61..cff3800cb7b 100644 --- a/packages/server/graphql/private/mutations/startTrial.ts +++ b/packages/server/graphql/private/mutations/startTrial.ts @@ -25,6 +25,11 @@ const startTrial: MutationResolvers['startTrial'] = async (_source, {orgId}, {da // RESOLUTION await Promise.all([ + pg + .updateTable('Organization') + .set({trialStartDate: now, tierLimitExceededAt: null, scheduledLockAt: null, lockedAt: null}) + .where('id', '=', orgId) + .execute(), r({ updatedOrg: r.table('Organization').get(orgId).update({ trialStartDate: now, diff --git a/packages/server/graphql/private/mutations/stripeCreateSubscription.ts b/packages/server/graphql/private/mutations/stripeCreateSubscription.ts index 55d0a872078..4acbd075ce5 100644 --- a/packages/server/graphql/private/mutations/stripeCreateSubscription.ts +++ b/packages/server/graphql/private/mutations/stripeCreateSubscription.ts @@ -1,5 +1,6 @@ import Stripe from 'stripe' import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' import {isSuperUser} from '../../../utils/authorization' import {getStripeManager} from '../../../utils/stripe' import {MutationResolvers} from '../resolverTypes' @@ -38,6 +39,14 @@ const stripeCreateSubscription: MutationResolvers['stripeCreateSubscription'] = const isSubscriptionInvalid = invalidStatuses.some((status) => (subscription.status = status)) if (isSubscriptionInvalid) return false + await getKysely() + .updateTable('Organization') + .set({ + stripeSubscriptionId: subscriptionId + }) + .where('id', '=', orgId) + .execute() + await r .table('Organization') .get(orgId) diff --git a/packages/server/graphql/private/mutations/stripeDeleteSubscription.ts b/packages/server/graphql/private/mutations/stripeDeleteSubscription.ts index b42338b2371..3b8d2dd1f7f 100644 --- a/packages/server/graphql/private/mutations/stripeDeleteSubscription.ts +++ b/packages/server/graphql/private/mutations/stripeDeleteSubscription.ts @@ -1,5 +1,6 @@ import getRethink from '../../../database/rethinkDriver' import Organization from '../../../database/types/Organization' +import getKysely from '../../../postgres/getKysely' import {isSuperUser} from '../../../utils/authorization' import {getStripeManager} from '../../../utils/stripe' import {MutationResolvers} from '../resolverTypes' @@ -36,7 +37,11 @@ const stripeDeleteSubscription: MutationResolvers['stripeDeleteSubscription'] = if (stripeSubscriptionId !== subscriptionId) { throw new Error(`Subscription ID does not match: ${stripeSubscriptionId} vs ${subscriptionId}`) } - + await getKysely() + .updateTable('Organization') + .set({stripeSubscriptionId: null}) + .where('id', '=', orgId) + .execute() await r .table('Organization') .get(orgId) diff --git a/packages/server/graphql/private/mutations/stripeInvoiceFinalized.ts b/packages/server/graphql/private/mutations/stripeInvoiceFinalized.ts index 480a6bfef12..6450f9dc57f 100644 --- a/packages/server/graphql/private/mutations/stripeInvoiceFinalized.ts +++ b/packages/server/graphql/private/mutations/stripeInvoiceFinalized.ts @@ -6,7 +6,7 @@ import {MutationResolvers} from '../resolverTypes' const stripeInvoiceFinalized: MutationResolvers['stripeInvoiceFinalized'] = async ( _source, {invoiceId}, - {authToken} + {authToken, dataLoader} ) => { const r = await getRethink() const now = new Date() @@ -29,7 +29,7 @@ const stripeInvoiceFinalized: MutationResolvers['stripeInvoiceFinalized'] = asyn livemode, metadata: {orgId} } = customer - const org = await r.table('Organization').get(orgId).run() + const org = await dataLoader.get('organizations').load(orgId!) if (!org) { if (livemode) { throw new Error( diff --git a/packages/server/graphql/private/mutations/stripeInvoicePaid.ts b/packages/server/graphql/private/mutations/stripeInvoicePaid.ts index 67956661c62..527dd1ec1fa 100644 --- a/packages/server/graphql/private/mutations/stripeInvoicePaid.ts +++ b/packages/server/graphql/private/mutations/stripeInvoicePaid.ts @@ -7,7 +7,7 @@ import {MutationResolvers} from '../resolverTypes' const stripeInvoicePaid: MutationResolvers['stripeInvoicePaid'] = async ( _source, {invoiceId}, - {authToken} + {authToken, dataLoader} ) => { const r = await getRethink() const now = new Date() @@ -30,8 +30,11 @@ const stripeInvoicePaid: MutationResolvers['stripeInvoicePaid'] = async ( livemode, metadata: {orgId} } = stripeCustomer - const org = await r.table('Organization').get(orgId).run() - if (!org || !orgId) { + if (!orgId) { + throw new Error(`Payment cannot succeed. Org ${orgId} does not exist for invoice ${invoiceId}`) + } + const org = await dataLoader.get('organizations').load(orgId) + if (!org) { if (livemode) { throw new Error( `Payment cannot succeed. Org ${orgId} does not exist for invoice ${invoiceId}` diff --git a/packages/server/graphql/private/mutations/stripeSucceedPayment.ts b/packages/server/graphql/private/mutations/stripeSucceedPayment.ts index 30334a7dc7e..03a62264dae 100644 --- a/packages/server/graphql/private/mutations/stripeSucceedPayment.ts +++ b/packages/server/graphql/private/mutations/stripeSucceedPayment.ts @@ -7,7 +7,7 @@ import {MutationResolvers} from '../resolverTypes' const stripeSucceedPayment: MutationResolvers['stripeSucceedPayment'] = async ( _source, {invoiceId}, - {authToken} + {authToken, dataLoader} ) => { const r = await getRethink() const now = new Date() @@ -30,8 +30,11 @@ const stripeSucceedPayment: MutationResolvers['stripeSucceedPayment'] = async ( livemode, metadata: {orgId} } = customer - const org = await r.table('Organization').get(orgId).run() - if (!org || !orgId) { + if (!orgId) { + throw new Error(`Payment cannot succeed. Org ${orgId} does not exist for invoice ${invoiceId}`) + } + const org = await dataLoader.get('organizations').load(orgId) + if (!org) { if (livemode) { throw new Error( `Payment cannot succeed. Org ${orgId} does not exist for invoice ${invoiceId}` diff --git a/packages/server/graphql/private/mutations/stripeUpdateCreditCard.ts b/packages/server/graphql/private/mutations/stripeUpdateCreditCard.ts index 3fc305757a1..2772c922b30 100644 --- a/packages/server/graphql/private/mutations/stripeUpdateCreditCard.ts +++ b/packages/server/graphql/private/mutations/stripeUpdateCreditCard.ts @@ -1,4 +1,6 @@ import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' +import {toCreditCard} from '../../../postgres/helpers/toCreditCard' import {isSuperUser} from '../../../utils/authorization' import {getStripeManager} from '../../../utils/stripe' import getCCFromCustomer from '../../mutations/helpers/getCCFromCustomer' @@ -23,6 +25,14 @@ const stripeUpdateCreditCard: MutationResolvers['stripeUpdateCreditCard'] = asyn const { metadata: {orgId} } = customer + if (!orgId) { + throw new Error('Unable to update credit card as customer does not have an orgId') + } + await getKysely() + .updateTable('Organization') + .set({creditCard: toCreditCard(creditCard)}) + .where('id', '=', orgId) + .execute() await r.table('Organization').get(orgId).update({creditCard}).run() return true } diff --git a/packages/server/graphql/private/mutations/updateOrgFeatureFlag.ts b/packages/server/graphql/private/mutations/updateOrgFeatureFlag.ts index 86cede3f0b7..733d03ae10b 100644 --- a/packages/server/graphql/private/mutations/updateOrgFeatureFlag.ts +++ b/packages/server/graphql/private/mutations/updateOrgFeatureFlag.ts @@ -1,14 +1,18 @@ +import {sql} from 'kysely' import {RValue} from 'rethinkdb-ts' import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' +import isValid from '../../isValid' import {MutationResolvers} from '../resolverTypes' const updateOrgFeatureFlag: MutationResolvers['updateOrgFeatureFlag'] = async ( _source, - {orgIds, flag, addFlag} + {orgIds, flag, addFlag}, + {dataLoader} ) => { const r = await getRethink() - - const existingIds = (await r.table('Organization').getAll(r.args(orgIds))('id').run()) as string[] + const existingOrgs = (await dataLoader.get('organizations').loadMany(orgIds)).filter(isValid) + const existingIds = existingOrgs.map(({id}) => id) const nonExistingIds = orgIds.filter((x) => !existingIds.includes(x)) @@ -17,6 +21,17 @@ const updateOrgFeatureFlag: MutationResolvers['updateOrgFeatureFlag'] = async ( } // RESOLUTION + await getKysely() + .updateTable('Organization') + .$if(addFlag, (db) => db.set({featureFlags: sql`arr_append_uniq("featureFlags",${flag})`})) + .$if(!addFlag, (db) => + db.set({ + featureFlags: sql`ARRAY_REMOVE("featureFlags",${flag})` + }) + ) + .where('id', 'in', orgIds) + .returning('id') + .execute() const updatedOrgIds = (await r .table('Organization') .getAll(r.args(orgIds)) diff --git a/packages/server/graphql/private/mutations/upgradeToTeamTier.ts b/packages/server/graphql/private/mutations/upgradeToTeamTier.ts index e7fd466a0ad..aef8652f492 100644 --- a/packages/server/graphql/private/mutations/upgradeToTeamTier.ts +++ b/packages/server/graphql/private/mutations/upgradeToTeamTier.ts @@ -2,6 +2,7 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import removeTeamsLimitObjects from '../../../billing/helpers/removeTeamsLimitObjects' import getRethink from '../../../database/rethinkDriver' import getKysely from '../../../postgres/getKysely' +import {toCreditCard} from '../../../postgres/helpers/toCreditCard' import {analytics} from '../../../utils/analytics/analytics' import {getUserId} from '../../../utils/authorization' import publish from '../../../utils/publish' @@ -66,22 +67,34 @@ const upgradeToTeamTier: MutationResolvers['upgradeToTeamTier'] = async ( } // RESOLUTION + const creditCard = await getCCFromCustomer(customer) await Promise.all([ + pg + .updateTable('Organization') + .set({ + creditCard: toCreditCard(creditCard), + tier: 'team', + tierLimitExceededAt: null, + scheduledLockAt: null, + lockedAt: null, + trialStartDate: null, + stripeId, + stripeSubscriptionId + }) + .where('id', '=', orgId) + .execute(), r({ - updatedOrg: r - .table('Organization') - .get(orgId) - .update({ - creditCard: await getCCFromCustomer(customer), - tier: 'team', - tierLimitExceededAt: null, - scheduledLockAt: null, - lockedAt: null, - updatedAt: now, - trialStartDate: null, - stripeId, - stripeSubscriptionId - }) + updatedOrg: r.table('Organization').get(orgId).update({ + creditCard, + tier: 'team', + tierLimitExceededAt: null, + scheduledLockAt: null, + lockedAt: null, + updatedAt: now, + trialStartDate: null, + stripeId, + stripeSubscriptionId + }) }).run(), pg .updateTable('Team') diff --git a/packages/server/graphql/public/mutations/acceptRequestToJoinDomain.ts b/packages/server/graphql/public/mutations/acceptRequestToJoinDomain.ts index db1ea25044e..324c4404ecd 100644 --- a/packages/server/graphql/public/mutations/acceptRequestToJoinDomain.ts +++ b/packages/server/graphql/public/mutations/acceptRequestToJoinDomain.ts @@ -56,11 +56,10 @@ const acceptRequestToJoinDomain: MutationResolvers['acceptRequestToJoinDomain'] // Provided request domain should match team's organizations activeDomain const leadTeams = await getTeamsByIds(validTeamMembers.map((teamMember) => teamMember.teamId)) - const validOrgIds = await r - .table('Organization') - .getAll(r.args(leadTeams.map((team) => team.orgId))) - .filter({activeDomain: domain})('id') - .run() + const teamOrgs = await Promise.all( + leadTeams.map((t) => dataLoader.get('organizations').load(t.orgId)) + ) + const validOrgIds = teamOrgs.filter((org) => org.activeDomain === domain).map(({id}) => id) if (!validOrgIds.length) { return standardError(new Error('Invalid organizations')) diff --git a/packages/server/graphql/public/mutations/updateCreditCard.ts b/packages/server/graphql/public/mutations/updateCreditCard.ts index d607f16cbe0..984c9758e17 100644 --- a/packages/server/graphql/public/mutations/updateCreditCard.ts +++ b/packages/server/graphql/public/mutations/updateCreditCard.ts @@ -2,6 +2,8 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import Stripe from 'stripe' import removeTeamsLimitObjects from '../../../billing/helpers/removeTeamsLimitObjects' import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' +import {toCreditCard} from '../../../postgres/helpers/toCreditCard' import updateTeamByOrgId from '../../../postgres/queries/updateTeamByOrgId' import {getUserId, isUserBillingLeader} from '../../../utils/authorization' import publish from '../../../utils/publish' @@ -56,6 +58,18 @@ const updateCreditCard: MutationResolvers['updateCreditCard'] = async ( const creditCard = stripeCardToDBCard(stripeCard) await Promise.all([ + getKysely() + .updateTable('Organization') + .set({ + creditCard: toCreditCard(creditCard), + tier: 'team', + stripeId: customer.id, + tierLimitExceededAt: null, + scheduledLockAt: null, + lockedAt: null + }) + .where('id', '=', orgId) + .execute(), r({ updatedOrg: r.table('Organization').get(orgId).update({ creditCard, diff --git a/packages/server/graphql/public/mutations/updateOrg.ts b/packages/server/graphql/public/mutations/updateOrg.ts index e0be5066410..3aedaf7d82c 100644 --- a/packages/server/graphql/public/mutations/updateOrg.ts +++ b/packages/server/graphql/public/mutations/updateOrg.ts @@ -1,5 +1,6 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' import {getUserId, isUserBillingLeader} from '../../../utils/authorization' import publish from '../../../utils/publish' import standardError from '../../../utils/standardError' @@ -41,6 +42,11 @@ const updateOrg: MutationResolvers['updateOrg'] = async ( name: normalizedName, updatedAt: now } + await getKysely() + .updateTable('Organization') + .set({name: normalizedName}) + .where('id', '=', orgId) + .execute() await r.table('Organization').get(orgId).update(dbUpdate).run() const data = {orgId} diff --git a/packages/server/graphql/public/mutations/uploadOrgImage.ts b/packages/server/graphql/public/mutations/uploadOrgImage.ts index 22f4812cf9f..e03a52f2ce7 100644 --- a/packages/server/graphql/public/mutations/uploadOrgImage.ts +++ b/packages/server/graphql/public/mutations/uploadOrgImage.ts @@ -3,6 +3,7 @@ import getRethink from '../../../database/rethinkDriver' import getFileStoreManager from '../../../fileStorage/getFileStoreManager' import normalizeAvatarUpload from '../../../fileStorage/normalizeAvatarUpload' import validateAvatarUpload from '../../../fileStorage/validateAvatarUpload' +import getKysely from '../../../postgres/getKysely' import {getUserId, isUserBillingLeader} from '../../../utils/authorization' import publish from '../../../utils/publish' import standardError from '../../../utils/standardError' @@ -33,7 +34,11 @@ const uploadOrgImage: MutationResolvers['uploadOrgImage'] = async ( const [normalExt, normalBuffer] = await normalizeAvatarUpload(validExt, validBuffer) const manager = getFileStoreManager() const publicLocation = await manager.putOrgAvatar(normalBuffer, orgId, normalExt) - + await getKysely() + .updateTable('Organization') + .set({picture: publicLocation}) + .where('id', '=', orgId) + .execute() await r .table('Organization') .get(orgId) diff --git a/packages/server/graphql/public/types/DomainJoinRequest.ts b/packages/server/graphql/public/types/DomainJoinRequest.ts index ffe93556170..0b030ce8d79 100644 --- a/packages/server/graphql/public/types/DomainJoinRequest.ts +++ b/packages/server/graphql/public/types/DomainJoinRequest.ts @@ -31,11 +31,10 @@ const DomainJoinRequest: DomainJoinRequestResolvers = { const leadTeamIds = leadTeamMembers.map((teamMember) => teamMember.teamId) const leadTeams = (await dataLoader.get('teams').loadMany(leadTeamIds)).filter(isValid) - const validOrgIds = await r - .table('Organization') - .getAll(r.args(leadTeams.map((team) => team.orgId))) - .filter({activeDomain: domain})('id') - .run() + const teamOrgs = await Promise.all( + leadTeams.map((t) => dataLoader.get('organizations').load(t.orgId)) + ) + const validOrgIds = teamOrgs.filter((org) => org.activeDomain === domain).map(({id}) => id) const validTeams = leadTeams.filter((team) => validOrgIds.includes(team.orgId)) return validTeams diff --git a/packages/server/graphql/queries/invoices.ts b/packages/server/graphql/queries/invoices.ts index 9454ec627c7..c9e043efd8c 100644 --- a/packages/server/graphql/queries/invoices.ts +++ b/packages/server/graphql/queries/invoices.ts @@ -38,7 +38,7 @@ export default { } // RESOLUTION - const {stripeId} = await r.table('Organization').get(orgId).pluck('stripeId').run() + const {stripeId} = await dataLoader.get('organizations').load(orgId) const dbAfter = after ? new Date(after) : r.maxval const [tooManyInvoices, orgUserCount] = await Promise.all([ r diff --git a/packages/server/postgres/helpers/toCreditCard.ts b/packages/server/postgres/helpers/toCreditCard.ts new file mode 100644 index 00000000000..2f4e4c02b42 --- /dev/null +++ b/packages/server/postgres/helpers/toCreditCard.ts @@ -0,0 +1,6 @@ +import {sql} from 'kysely' +import CreditCard from '../../database/types/CreditCard' +export const toCreditCard = (creditCard: CreditCard | undefined | null) => { + if (!creditCard) return null + return sql`(select json_populate_record(null::"CreditCard", ${JSON.stringify(creditCard)}))` +} diff --git a/packages/server/postgres/helpers/toGoogleAnalyzedEntity.ts b/packages/server/postgres/helpers/toGoogleAnalyzedEntity.ts new file mode 100644 index 00000000000..7fac73eefbd --- /dev/null +++ b/packages/server/postgres/helpers/toGoogleAnalyzedEntity.ts @@ -0,0 +1,6 @@ +import {sql} from 'kysely' +import GoogleAnalyzedEntity from '../../database/types/GoogleAnalyzedEntity' +export const toGoogleAnalyzedEntity = (entities: GoogleAnalyzedEntity[]) => + sql< + string[] + >`(select coalesce(array_agg((name, salience, lemma)::"GoogleAnalyzedEntity"), '{}') from json_populate_recordset(null::"GoogleAnalyzedEntity", ${JSON.stringify(entities)}))` diff --git a/packages/server/postgres/migrations/1719435764047_Organization-phase1.ts b/packages/server/postgres/migrations/1719435764047_Organization-phase1.ts new file mode 100644 index 00000000000..719d7a6953b --- /dev/null +++ b/packages/server/postgres/migrations/1719435764047_Organization-phase1.ts @@ -0,0 +1,55 @@ +import {Client} from 'pg' +import getPgConfig from '../getPgConfig' + +export async function up() { + const client = new Client(getPgConfig()) + await client.connect() + //activeDomain has a few that are longer than 100 + await client.query(` + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'CreditCard') THEN + CREATE TYPE "CreditCard" AS (brand text, expiry text, last4 smallint); + END IF; + + CREATE TABLE IF NOT EXISTS "Organization" ( + "id" VARCHAR(100) PRIMARY KEY, + "activeDomain" VARCHAR(100), + "isActiveDomainTouched" BOOLEAN NOT NULL DEFAULT FALSE, + "creditCard" "CreditCard", + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "name" VARCHAR(100) NOT NULL, + "payLaterClickCount" SMALLINT NOT NULL DEFAULT 0, + "periodEnd" TIMESTAMP WITH TIME ZONE, + "periodStart" TIMESTAMP WITH TIME ZONE, + "picture" VARCHAR(2056), + "showConversionModal" BOOLEAN NOT NULL DEFAULT FALSE, + "stripeId" VARCHAR(100), + "stripeSubscriptionId" VARCHAR(100), + "upcomingInvoiceEmailSentAt" TIMESTAMP WITH TIME ZONE, + "tier" "TierEnum" NOT NULL DEFAULT 'starter', + "tierLimitExceededAt" TIMESTAMP WITH TIME ZONE, + "trialStartDate" TIMESTAMP WITH TIME ZONE, + "scheduledLockAt" TIMESTAMP WITH TIME ZONE, + "lockedAt" TIMESTAMP WITH TIME ZONE, + "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "featureFlags" TEXT[] NOT NULL DEFAULT '{}' + ); + CREATE INDEX IF NOT EXISTS "idx_Organization_activeDomain" ON "Organization"("activeDomain"); + CREATE INDEX IF NOT EXISTS "idx_Organization_tier" ON "Organization"("tier"); + DROP TRIGGER IF EXISTS "update_Organization_updatedAt" ON "Organization"; + CREATE TRIGGER "update_Organization_updatedAt" BEFORE UPDATE ON "Organization" FOR EACH ROW EXECUTE PROCEDURE "set_updatedAt"(); + END $$; +`) + await client.end() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DROP TABLE "Organization"; + DROP TYPE "CreditCard"; + ` /* Do undo magic */) + await client.end() +} diff --git a/packages/server/safeMutations/safeArchiveEmptyStarterOrganization.ts b/packages/server/safeMutations/safeArchiveEmptyStarterOrganization.ts index e4eba005f50..e53f082ffce 100644 --- a/packages/server/safeMutations/safeArchiveEmptyStarterOrganization.ts +++ b/packages/server/safeMutations/safeArchiveEmptyStarterOrganization.ts @@ -1,17 +1,21 @@ import getRethink from '../database/rethinkDriver' +import {DataLoaderInstance} from '../dataloader/RootDataLoader' import getTeamsByOrgIds from '../postgres/queries/getTeamsByOrgIds' // Only does something if the organization is empty & not paid // safeArchiveTeam & downgradeToStarter should be called before calling this -const safeArchiveEmptyStarterOrganization = async (orgId: string) => { +const safeArchiveEmptyStarterOrganization = async ( + orgId: string, + dataLoader: DataLoaderInstance +) => { const r = await getRethink() const now = new Date() const orgTeams = await getTeamsByOrgIds([orgId]) const teamCountRemainingOnOldOrg = orgTeams.length if (teamCountRemainingOnOldOrg > 0) return - const org = await r.table('Organization').get(orgId).run() + const org = await dataLoader.get('organizations').load(orgId) if (org.tier !== 'starter') return await r diff --git a/packages/server/utils/__tests__/isRequestToJoinDomainAllowed.test.ts b/packages/server/utils/__tests__/isRequestToJoinDomainAllowed.test.ts index 6858d2854cf..1a152cfa456 100644 --- a/packages/server/utils/__tests__/isRequestToJoinDomainAllowed.test.ts +++ b/packages/server/utils/__tests__/isRequestToJoinDomainAllowed.test.ts @@ -2,9 +2,11 @@ import {r} from 'rethinkdb-ts' import getRethinkConfig from '../../database/getRethinkConfig' import getRethink from '../../database/rethinkDriver' +import {TierEnum} from '../../database/types/Invoice' import OrganizationUser from '../../database/types/OrganizationUser' import generateUID from '../../generateUID' import {DataLoaderWorker} from '../../graphql/graphql' +import getKysely from '../../postgres/getKysely' import getRedis from '../getRedis' import {getEligibleOrgIdsByDomain} from '../isRequestToJoinDomainAllowed' jest.mock('../../database/rethinkDriver') @@ -44,7 +46,7 @@ type TestOrganizationUser = Partial< const addOrg = async ( activeDomain: string | null, members: TestOrganizationUser[], - rest?: {featureFlags?: string[]; tier?: string} + rest?: {featureFlags?: string[]; tier?: TierEnum} ) => { const {featureFlags, tier} = rest ?? {} const orgId = generateUID() @@ -52,6 +54,7 @@ const addOrg = async ( id: orgId, activeDomain, featureFlags, + name: 'foog', tier: tier ?? 'starter' } @@ -63,7 +66,7 @@ const addOrg = async ( role: member.role ?? null, removedAt: member.removedAt ?? null })) - + await getKysely().insertInto('Organization').values(org).execute() await r.table('Organization').insert(org).run() await r.table('OrganizationUser').insert(orgUsers).run() return orgId diff --git a/scripts/toolboxSrc/setIsEnterprise.ts b/scripts/toolboxSrc/setIsEnterprise.ts index 9aab65a8674..4fe84a5242b 100644 --- a/scripts/toolboxSrc/setIsEnterprise.ts +++ b/scripts/toolboxSrc/setIsEnterprise.ts @@ -1,22 +1,25 @@ +import getKysely from 'parabol-server/postgres/getKysely' import getRethink from '../../packages/server/database/rethinkDriver' import getPg from '../../packages/server/postgres/getPg' import {defaultTier} from '../../packages/server/utils/defaultTier' export default async function setIsEnterprise() { if (defaultTier !== 'enterprise') { - throw new Error('Environment variable IS_ENTERPRISE is not set to true. Exiting without updating tiers.') + throw new Error( + 'Environment variable IS_ENTERPRISE is not set to true. Exiting without updating tiers.' + ) } - + const r = await getRethink() console.log( 'Updating tier to "enterprise" for Organization and OrganizationUser tables in RethinkDB' ) - + type RethinkTableKey = 'Organization' | 'OrganizationUser' const tablesToUpdate: RethinkTableKey[] = ['Organization', 'OrganizationUser'] - + await getKysely().updateTable('Organization').set({tier: 'enterprise'}).execute() const rethinkPromises = tablesToUpdate.map(async (table) => { const result = await r .table(table)