From 98755f982c0cef7f1f1431d16e5be4f932c8daba Mon Sep 17 00:00:00 2001 From: Jordan Husney Date: Fri, 1 Mar 2024 19:54:07 -0800 Subject: [PATCH 1/8] chore: migrate EmailVerification to pg --- packages/server/__tests__/autoJoin.test.ts | 22 +++--- .../database/types/EmailVerification.ts | 6 +- .../createEmailVerficationForExistingUser.ts | 6 +- .../server/email/createEmailVerification.ts | 6 +- .../public/mutations/signUpWithPassword.ts | 19 +++-- .../graphql/public/mutations/verifyEmail.ts | 16 ++-- .../1709351538000_addEmailVerification.ts | 31 ++++++++ .../1709351575000_moveEmailVerification.ts | 76 +++++++++++++++++++ 8 files changed, 143 insertions(+), 39 deletions(-) create mode 100644 packages/server/postgres/migrations/1709351538000_addEmailVerification.ts create mode 100644 packages/server/postgres/migrations/1709351575000_moveEmailVerification.ts diff --git a/packages/server/__tests__/autoJoin.test.ts b/packages/server/__tests__/autoJoin.test.ts index 85bc9e3e222..0683c6deada 100644 --- a/packages/server/__tests__/autoJoin.test.ts +++ b/packages/server/__tests__/autoJoin.test.ts @@ -1,6 +1,6 @@ import faker from 'faker' import {getUserTeams, sendPublic, sendIntranet, signUp, signUpWithEmail} from './common' -import getRethink from '../database/rethinkDriver' +import getKysely from '../postgres/getKysely' import createEmailVerification from '../email/createEmailVerification' const signUpVerified = async (email: string) => { @@ -23,12 +23,14 @@ const signUpVerified = async (email: string) => { // manually generate verification token so also the founder can be verified await createEmailVerification({email, password}) - const r = await getRethink() - const verificationToken = await r - .table('EmailVerification') - .getAll(email, {index: 'email'}) - .nth(0)('token') - .run() + const pg = getKysely() + const verificationToken = ( + await pg + .selectFrom('EmailVerification') + .select('token') + .where('email', '=', email) + .executeTakeFirstOrThrow(() => new Error(`No verification token found for ${email}`)) + ).token const verifyEmail = await sendPublic({ query: ` @@ -55,9 +57,9 @@ const signUpVerified = async (email: string) => { expect(verifyEmail).toMatchObject({ data: { verifyEmail: { - authToken: expect.toBeString(), + authToken: expect.any(String), user: { - id: expect.toBeString() + id: expect.any(String) } } } @@ -153,7 +155,7 @@ test.skip('autoJoin on multiple teams does not create duplicate `OrganizationUse const newEmail = `${faker.internet.userName()}@${domain}`.toLowerCase() const {user: newUser} = await signUpVerified(newEmail) - expect(newUser.tms).toIncludeSameMembers(teamIds) + expect(newUser.tms).toEqual(expect.arrayContaining(teamIds)) expect(newUser.organizations).toMatchObject([ { id: orgId diff --git a/packages/server/database/types/EmailVerification.ts b/packages/server/database/types/EmailVerification.ts index 0288eae2421..a7f88f45b17 100644 --- a/packages/server/database/types/EmailVerification.ts +++ b/packages/server/database/types/EmailVerification.ts @@ -1,8 +1,6 @@ import {Threshold} from 'parabol-client/types/constEnums' -import generateUID from '../../generateUID' interface Input { - id?: string token: string email: string expiration?: Date @@ -12,7 +10,6 @@ interface Input { } export default class EmailVerification { - id: string invitationToken?: string token: string email: string @@ -20,8 +17,7 @@ export default class EmailVerification { hashedPassword?: string pseudoId?: string constructor(input: Input) { - const {id, invitationToken, token, email, expiration, hashedPassword, pseudoId} = input - this.id = id || generateUID() + const {invitationToken, token, email, expiration, hashedPassword, pseudoId} = input this.invitationToken = invitationToken || undefined this.token = token this.email = email diff --git a/packages/server/email/createEmailVerficationForExistingUser.ts b/packages/server/email/createEmailVerficationForExistingUser.ts index c72337f2708..5f99422857e 100644 --- a/packages/server/email/createEmailVerficationForExistingUser.ts +++ b/packages/server/email/createEmailVerficationForExistingUser.ts @@ -1,6 +1,6 @@ import base64url from 'base64url' import crypto from 'crypto' -import getRethink from '../database/rethinkDriver' +import getKysely from '../postgres/getKysely' import AuthIdentityLocal from '../database/types/AuthIdentityLocal' import EmailVerification from '../database/types/EmailVerification' import {DataLoaderWorker} from '../graphql/graphql' @@ -34,7 +34,6 @@ const createEmailVerficationForExistingUser = async ( if (!success) { return new Error('Unable to send verification email') } - const r = await getRethink() const emailVerification = new EmailVerification({ email, token: verifiedEmailToken, @@ -42,7 +41,8 @@ const createEmailVerficationForExistingUser = async ( pseudoId, invitationToken }) - await r.table('EmailVerification').insert(emailVerification).run() + const pg = getKysely() + await pg.insertInto('EmailVerification').values(emailVerification).execute() return undefined } diff --git a/packages/server/email/createEmailVerification.ts b/packages/server/email/createEmailVerification.ts index 2271c82163a..7733c3f34e4 100644 --- a/packages/server/email/createEmailVerification.ts +++ b/packages/server/email/createEmailVerification.ts @@ -2,7 +2,7 @@ import base64url from 'base64url' import bcrypt from 'bcryptjs' import crypto from 'crypto' import {Security} from 'parabol-client/types/constEnums' -import getRethink from '../database/rethinkDriver' +import getKysely from '../postgres/getKysely' import EmailVerification from '../database/types/EmailVerification' import emailVerificationEmailCreator from './emailVerificationEmailCreator' import getMailManager from './getMailManager' @@ -35,7 +35,6 @@ const createEmailVerification = async (props: SignUpWithPasswordMutationVariable if (!success) { return {error: {message: 'Unable to send verification email'}} } - const r = await getRethink() const hashedPassword = await bcrypt.hash(password, Security.SALT_ROUNDS) const emailVerification = new EmailVerification({ email, @@ -44,7 +43,8 @@ const createEmailVerification = async (props: SignUpWithPasswordMutationVariable pseudoId, invitationToken }) - await r.table('EmailVerification').insert(emailVerification).run() + const pg = getKysely() + await pg.insertInto('EmailVerification').values(emailVerification).execute() return {error: {message: 'Verification required. Check your inbox.'}} } diff --git a/packages/server/graphql/public/mutations/signUpWithPassword.ts b/packages/server/graphql/public/mutations/signUpWithPassword.ts index ea335848a77..984d5f170b3 100644 --- a/packages/server/graphql/public/mutations/signUpWithPassword.ts +++ b/packages/server/graphql/public/mutations/signUpWithPassword.ts @@ -1,7 +1,6 @@ import bcrypt from 'bcryptjs' import {AuthenticationError, Security} from 'parabol-client/types/constEnums' -import getRethink from '../../../database/rethinkDriver' -import {RValue} from '../../../database/stricterR' +import getKysely from '../../../postgres/getKysely' import createEmailVerification from '../../../email/createEmailVerification' import {USER_PREFERRED_NAME_LIMIT} from '../../../postgres/constants' import createNewLocalUser from '../../../utils/createNewLocalUser' @@ -21,7 +20,7 @@ const signUpWithPassword: MutationResolvers['signUpWithPassword'] = async ( if (email.length > USER_PREFERRED_NAME_LIMIT) { return {error: {message: 'Email is too long'}} } - const r = await getRethink() + const pg = getKysely() const isOrganic = !invitationToken const {ip, dataLoader} = context const loginAttempt = await attemptLogin(email, password, ip) @@ -49,13 +48,13 @@ const signUpWithPassword: MutationResolvers['signUpWithPassword'] = async ( } const verificationRequired = await isEmailVerificationRequired(email, dataLoader) if (verificationRequired) { - const existingVerification = await r - .table('EmailVerification') - .getAll(email, {index: 'email'}) - .filter((row: RValue) => row('expiration').gt(new Date())) - .nth(0) - .default(null) - .run() + const existingVerification = + (await pg + .selectFrom('EmailVerification') + .selectAll() + .where('email', '=', email) + .where('expiration', '>', new Date()) + .executeTakeFirst()) || null if (existingVerification) { return {error: {message: 'Verification email already sent'}} } diff --git a/packages/server/graphql/public/mutations/verifyEmail.ts b/packages/server/graphql/public/mutations/verifyEmail.ts index 0b9485c13e2..07b53b6eec3 100644 --- a/packages/server/graphql/public/mutations/verifyEmail.ts +++ b/packages/server/graphql/public/mutations/verifyEmail.ts @@ -1,5 +1,5 @@ import {AuthIdentityTypeEnum} from '../../../../client/types/constEnums' -import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' import AuthIdentityLocal from '../../../database/types/AuthIdentityLocal' import AuthToken from '../../../database/types/AuthToken' import EmailVerification from '../../../database/types/EmailVerification' @@ -16,14 +16,14 @@ const verifyEmail: MutationResolvers['verifyEmail'] = async ( context ) => { const {dataLoader} = context - const r = await getRethink() + const pg = getKysely() const now = new Date() - const emailVerification = (await r - .table('EmailVerification') - .getAll(verificationToken, {index: 'token'}) - .nth(0) - .default(null) - .run()) as EmailVerification + const emailVerification = + ((await pg + .selectFrom('EmailVerification') + .selectAll() + .where('token', '=', verificationToken) + .executeTakeFirst()) as EmailVerification) || null if (!emailVerification) { return {error: {message: 'Invalid verification token'}} diff --git a/packages/server/postgres/migrations/1709351538000_addEmailVerification.ts b/packages/server/postgres/migrations/1709351538000_addEmailVerification.ts new file mode 100644 index 00000000000..a249ba55099 --- /dev/null +++ b/packages/server/postgres/migrations/1709351538000_addEmailVerification.ts @@ -0,0 +1,31 @@ +import {Client} from 'pg' +import getPgConfig from '../getPgConfig' + +export async function up() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + CREATE TABLE "EmailVerification" ( + "id" SERIAL PRIMARY KEY, + "email" "citext" NOT NULL, + "expiration" TIMESTAMP WITH TIME ZONE NOT NULL, + "token" VARCHAR(100) NOT NULL, + "hashedPassword" VARCHAR(100), + "invitationToken" VARCHAR(100), + "pseudoId" VARCHAR(100) + ); + + CREATE INDEX IF NOT EXISTS "idx_EmailVerification_email" ON "EmailVerification"("email"); + CREATE INDEX IF NOT EXISTS "idx_EmailVerification_token" ON "EmailVerification"("token"); + `) + await client.end() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DROP TABLE IF EXISTS "EmailVerification"; + `) + await client.end() +} diff --git a/packages/server/postgres/migrations/1709351575000_moveEmailVerification.ts b/packages/server/postgres/migrations/1709351575000_moveEmailVerification.ts new file mode 100644 index 00000000000..3591ed43b2a --- /dev/null +++ b/packages/server/postgres/migrations/1709351575000_moveEmailVerification.ts @@ -0,0 +1,76 @@ +import {FirstParam} from 'parabol-client/types/generics' +import {Client} from 'pg' +import {r} from 'rethinkdb-ts' +import getPgConfig from '../getPgConfig' +import connectRethinkDB from '../../database/connectRethinkDB' +import getPgp from '../getPgp' + +export async function up() { + await connectRethinkDB() + const {pgp, pg} = getPgp() + const batchSize = 1000 + + try { + await r.table('EmailVerification').indexCreate('expiration').run() + await r.table('EmailVerification').indexWait().run() + } catch {} + + const columnSet = new pgp.helpers.ColumnSet( + [ + 'email', + 'expiration', + 'hashedPassword', + 'token', + {name: 'invitationToken', def: null}, + {name: 'pseudoId', def: null} + ], + {table: 'EmailVerification'} + ) + + const getNextData = async (leftBoundCursor: Date | undefined) => { + const expiration = leftBoundCursor || r.minval + const nextBatch = await r + .table('EmailVerification') + .between(expiration, r.maxval, {index: 'expiration', leftBound: 'open'}) + .orderBy({index: 'expiration'}) + .limit(batchSize) + .run() + if (nextBatch.length === 0) return null + if (nextBatch.length < batchSize) return nextBatch + const lastItem = nextBatch.pop() + const lastMatchingExpiration = nextBatch.findLastIndex( + (item) => item.expiration !== lastItem!.expiration + ) + if (lastMatchingExpiration === -1) { + throw new Error( + 'batchSize is smaller than the number of items that share the same cursor. Increase batchSize' + ) + } + return nextBatch.slice(0, lastMatchingExpiration) + } + + await pg.tx('EmailVerification', (task) => { + const fetchAndProcess: FirstParam = async ( + _index, + leftBoundCursor: undefined | Date + ) => { + const nextData = await getNextData(leftBoundCursor) + if (!nextData) return undefined + const insert = pgp.helpers.insert(nextData, columnSet) + await task.none(insert) + return nextData.at(-1)!.runAt + } + return task.sequence(fetchAndProcess) + }) + await r.getPoolMaster()?.drain() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(`DELETE FROM "EmailVerification"`) + await client.end() + try { + await r.table('EmailVerification').indexDrop('expiration').run() + } catch {} +} From d171d43a9af7559f3884a870fc54c4b742052125 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Thu, 25 Jul 2024 13:37:35 -0700 Subject: [PATCH 2/8] fix: migrate all the rows Signed-off-by: Matt Krick --- .../database/types/EmailVerification.ts | 28 ------- .../createEmailVerficationForExistingUser.ts | 23 +++--- .../server/email/createEmailVerification.ts | 22 +++--- .../public/mutations/signUpWithPassword.ts | 13 ++-- .../graphql/public/mutations/verifyEmail.ts | 12 ++- .../1709351538000_addEmailVerification.ts | 31 -------- .../1709351575000_moveEmailVerification.ts | 76 ------------------- .../1721868364099_addEmailVerification.ts | 52 +++++++++++++ 8 files changed, 88 insertions(+), 169 deletions(-) delete mode 100644 packages/server/database/types/EmailVerification.ts delete mode 100644 packages/server/postgres/migrations/1709351538000_addEmailVerification.ts delete mode 100644 packages/server/postgres/migrations/1709351575000_moveEmailVerification.ts create mode 100644 packages/server/postgres/migrations/1721868364099_addEmailVerification.ts diff --git a/packages/server/database/types/EmailVerification.ts b/packages/server/database/types/EmailVerification.ts deleted file mode 100644 index a7f88f45b17..00000000000 --- a/packages/server/database/types/EmailVerification.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {Threshold} from 'parabol-client/types/constEnums' - -interface Input { - token: string - email: string - expiration?: Date - hashedPassword?: string - pseudoId?: string | null - invitationToken?: string | null -} - -export default class EmailVerification { - invitationToken?: string - token: string - email: string - expiration: Date - hashedPassword?: string - pseudoId?: string - constructor(input: Input) { - const {invitationToken, token, email, expiration, hashedPassword, pseudoId} = input - this.invitationToken = invitationToken || undefined - this.token = token - this.email = email - this.expiration = expiration || new Date(Date.now() + Threshold.EMAIL_VERIFICATION_LIFESPAN) - this.hashedPassword = hashedPassword - this.pseudoId = pseudoId || undefined - } -} diff --git a/packages/server/email/createEmailVerficationForExistingUser.ts b/packages/server/email/createEmailVerficationForExistingUser.ts index 5f99422857e..a54b4c33968 100644 --- a/packages/server/email/createEmailVerficationForExistingUser.ts +++ b/packages/server/email/createEmailVerficationForExistingUser.ts @@ -1,9 +1,9 @@ import base64url from 'base64url' import crypto from 'crypto' -import getKysely from '../postgres/getKysely' +import {Threshold} from 'parabol-client/types/constEnums' import AuthIdentityLocal from '../database/types/AuthIdentityLocal' -import EmailVerification from '../database/types/EmailVerification' import {DataLoaderWorker} from '../graphql/graphql' +import getKysely from '../postgres/getKysely' import emailVerificationEmailCreator from './emailVerificationEmailCreator' import getMailManager from './getMailManager' @@ -34,15 +34,18 @@ const createEmailVerficationForExistingUser = async ( if (!success) { return new Error('Unable to send verification email') } - const emailVerification = new EmailVerification({ - email, - token: verifiedEmailToken, - hashedPassword, - pseudoId, - invitationToken - }) const pg = getKysely() - await pg.insertInto('EmailVerification').values(emailVerification).execute() + await pg + .insertInto('EmailVerification') + .values({ + email, + token: verifiedEmailToken, + hashedPassword, + pseudoId, + invitationToken, + expiration: new Date(Date.now() + Threshold.EMAIL_VERIFICATION_LIFESPAN) + }) + .execute() return undefined } diff --git a/packages/server/email/createEmailVerification.ts b/packages/server/email/createEmailVerification.ts index 7733c3f34e4..5a73d4716bc 100644 --- a/packages/server/email/createEmailVerification.ts +++ b/packages/server/email/createEmailVerification.ts @@ -1,9 +1,8 @@ import base64url from 'base64url' import bcrypt from 'bcryptjs' import crypto from 'crypto' -import {Security} from 'parabol-client/types/constEnums' +import {Security, Threshold} from 'parabol-client/types/constEnums' import getKysely from '../postgres/getKysely' -import EmailVerification from '../database/types/EmailVerification' import emailVerificationEmailCreator from './emailVerificationEmailCreator' import getMailManager from './getMailManager' @@ -36,15 +35,18 @@ const createEmailVerification = async (props: SignUpWithPasswordMutationVariable return {error: {message: 'Unable to send verification email'}} } const hashedPassword = await bcrypt.hash(password, Security.SALT_ROUNDS) - const emailVerification = new EmailVerification({ - email, - token: verifiedEmailToken, - hashedPassword, - pseudoId, - invitationToken - }) const pg = getKysely() - await pg.insertInto('EmailVerification').values(emailVerification).execute() + await pg + .insertInto('EmailVerification') + .values({ + email, + token: verifiedEmailToken, + hashedPassword, + pseudoId, + invitationToken, + expiration: new Date(Date.now() + Threshold.EMAIL_VERIFICATION_LIFESPAN) + }) + .execute() return {error: {message: 'Verification required. Check your inbox.'}} } diff --git a/packages/server/graphql/public/mutations/signUpWithPassword.ts b/packages/server/graphql/public/mutations/signUpWithPassword.ts index bf19c0a487a..29828d789c9 100644 --- a/packages/server/graphql/public/mutations/signUpWithPassword.ts +++ b/packages/server/graphql/public/mutations/signUpWithPassword.ts @@ -47,13 +47,12 @@ const signUpWithPassword: MutationResolvers['signUpWithPassword'] = async ( } const verificationRequired = await isEmailVerificationRequired(email, dataLoader) if (verificationRequired) { - const existingVerification = - (await pg - .selectFrom('EmailVerification') - .selectAll() - .where('email', '=', email) - .where('expiration', '>', new Date()) - .executeTakeFirst()) || null + const existingVerification = await pg + .selectFrom('EmailVerification') + .selectAll() + .where('email', '=', email) + .where('expiration', '>', new Date()) + .executeTakeFirst() if (existingVerification) { return {error: {message: 'Verification email already sent'}} } diff --git a/packages/server/graphql/public/mutations/verifyEmail.ts b/packages/server/graphql/public/mutations/verifyEmail.ts index e76f0aede1e..f9b8be46b9d 100644 --- a/packages/server/graphql/public/mutations/verifyEmail.ts +++ b/packages/server/graphql/public/mutations/verifyEmail.ts @@ -1,7 +1,6 @@ import {AuthIdentityTypeEnum} from '../../../../client/types/constEnums' import AuthIdentityLocal from '../../../database/types/AuthIdentityLocal' import AuthToken from '../../../database/types/AuthToken' -import EmailVerification from '../../../database/types/EmailVerification' import getKysely from '../../../postgres/getKysely' import {getUserByEmail} from '../../../postgres/queries/getUsersByEmails' import updateUser from '../../../postgres/queries/updateUser' @@ -18,12 +17,11 @@ const verifyEmail: MutationResolvers['verifyEmail'] = async ( const {dataLoader} = context const pg = getKysely() const now = new Date() - const emailVerification = - ((await pg - .selectFrom('EmailVerification') - .selectAll() - .where('token', '=', verificationToken) - .executeTakeFirst()) as EmailVerification) || null + const emailVerification = await pg + .selectFrom('EmailVerification') + .selectAll() + .where('token', '=', verificationToken) + .executeTakeFirst() if (!emailVerification) { return {error: {message: 'Invalid verification token'}} diff --git a/packages/server/postgres/migrations/1709351538000_addEmailVerification.ts b/packages/server/postgres/migrations/1709351538000_addEmailVerification.ts deleted file mode 100644 index a249ba55099..00000000000 --- a/packages/server/postgres/migrations/1709351538000_addEmailVerification.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {Client} from 'pg' -import getPgConfig from '../getPgConfig' - -export async function up() { - const client = new Client(getPgConfig()) - await client.connect() - await client.query(` - CREATE TABLE "EmailVerification" ( - "id" SERIAL PRIMARY KEY, - "email" "citext" NOT NULL, - "expiration" TIMESTAMP WITH TIME ZONE NOT NULL, - "token" VARCHAR(100) NOT NULL, - "hashedPassword" VARCHAR(100), - "invitationToken" VARCHAR(100), - "pseudoId" VARCHAR(100) - ); - - CREATE INDEX IF NOT EXISTS "idx_EmailVerification_email" ON "EmailVerification"("email"); - CREATE INDEX IF NOT EXISTS "idx_EmailVerification_token" ON "EmailVerification"("token"); - `) - await client.end() -} - -export async function down() { - const client = new Client(getPgConfig()) - await client.connect() - await client.query(` - DROP TABLE IF EXISTS "EmailVerification"; - `) - await client.end() -} diff --git a/packages/server/postgres/migrations/1709351575000_moveEmailVerification.ts b/packages/server/postgres/migrations/1709351575000_moveEmailVerification.ts deleted file mode 100644 index 3591ed43b2a..00000000000 --- a/packages/server/postgres/migrations/1709351575000_moveEmailVerification.ts +++ /dev/null @@ -1,76 +0,0 @@ -import {FirstParam} from 'parabol-client/types/generics' -import {Client} from 'pg' -import {r} from 'rethinkdb-ts' -import getPgConfig from '../getPgConfig' -import connectRethinkDB from '../../database/connectRethinkDB' -import getPgp from '../getPgp' - -export async function up() { - await connectRethinkDB() - const {pgp, pg} = getPgp() - const batchSize = 1000 - - try { - await r.table('EmailVerification').indexCreate('expiration').run() - await r.table('EmailVerification').indexWait().run() - } catch {} - - const columnSet = new pgp.helpers.ColumnSet( - [ - 'email', - 'expiration', - 'hashedPassword', - 'token', - {name: 'invitationToken', def: null}, - {name: 'pseudoId', def: null} - ], - {table: 'EmailVerification'} - ) - - const getNextData = async (leftBoundCursor: Date | undefined) => { - const expiration = leftBoundCursor || r.minval - const nextBatch = await r - .table('EmailVerification') - .between(expiration, r.maxval, {index: 'expiration', leftBound: 'open'}) - .orderBy({index: 'expiration'}) - .limit(batchSize) - .run() - if (nextBatch.length === 0) return null - if (nextBatch.length < batchSize) return nextBatch - const lastItem = nextBatch.pop() - const lastMatchingExpiration = nextBatch.findLastIndex( - (item) => item.expiration !== lastItem!.expiration - ) - if (lastMatchingExpiration === -1) { - throw new Error( - 'batchSize is smaller than the number of items that share the same cursor. Increase batchSize' - ) - } - return nextBatch.slice(0, lastMatchingExpiration) - } - - await pg.tx('EmailVerification', (task) => { - const fetchAndProcess: FirstParam = async ( - _index, - leftBoundCursor: undefined | Date - ) => { - const nextData = await getNextData(leftBoundCursor) - if (!nextData) return undefined - const insert = pgp.helpers.insert(nextData, columnSet) - await task.none(insert) - return nextData.at(-1)!.runAt - } - return task.sequence(fetchAndProcess) - }) - await r.getPoolMaster()?.drain() -} - -export async function down() { - const client = new Client(getPgConfig()) - await client.connect() - await client.query(`DELETE FROM "EmailVerification"`) - await client.end() - try { - await r.table('EmailVerification').indexDrop('expiration').run() - } catch {} -} diff --git a/packages/server/postgres/migrations/1721868364099_addEmailVerification.ts b/packages/server/postgres/migrations/1721868364099_addEmailVerification.ts new file mode 100644 index 00000000000..52033bf4c01 --- /dev/null +++ b/packages/server/postgres/migrations/1721868364099_addEmailVerification.ts @@ -0,0 +1,52 @@ +import {Kysely, PostgresDialect, sql} from 'kysely' +import {Client} from 'pg' +import {r} from 'rethinkdb-ts' +import connectRethinkDB from '../../database/connectRethinkDB' +import getPg from '../getPg' +import getPgConfig from '../getPgConfig' + +export async function up() { + await connectRethinkDB() + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + await sql` + CREATE TABLE "EmailVerification" ( + "id" INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "email" "citext" NOT NULL, + "expiration" TIMESTAMP WITH TIME ZONE NOT NULL, + "token" VARCHAR(100) NOT NULL, + "hashedPassword" VARCHAR(100), + "invitationToken" VARCHAR(100), + "pseudoId" VARCHAR(100) + ); + + CREATE INDEX IF NOT EXISTS "idx_EmailVerification_email" ON "EmailVerification"("email"); + CREATE INDEX IF NOT EXISTS "idx_EmailVerification_token" ON "EmailVerification"("token"); + `.execute(pg) + + const rData = await r.table('EmailVerification').coerceTo('array').run() + const insertData = rData.map((row) => { + const {email, expiration, hashedPassword, token, invitationToken, pseudoId} = row + return { + email, + expiration, + hashedPassword, + token, + invitationToken, + pseudoId + } + }) + await pg.insertInto('EmailVerification').values(insertData).execute() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DROP TABLE IF EXISTS "EmailVerification"; + `) + await client.end() +} From d30454c0b4471788c1b0b4b64cc1e1f55f76ef99 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Thu, 25 Jul 2024 13:41:58 -0700 Subject: [PATCH 3/8] fix migration on empty db Signed-off-by: Matt Krick --- .../postgres/migrations/1721868364099_addEmailVerification.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/server/postgres/migrations/1721868364099_addEmailVerification.ts b/packages/server/postgres/migrations/1721868364099_addEmailVerification.ts index 52033bf4c01..b725ec8f65f 100644 --- a/packages/server/postgres/migrations/1721868364099_addEmailVerification.ts +++ b/packages/server/postgres/migrations/1721868364099_addEmailVerification.ts @@ -39,6 +39,7 @@ export async function up() { pseudoId } }) + if (insertData.length === 0) return await pg.insertInto('EmailVerification').values(insertData).execute() } From 1220d101aabce8508c5c69f61094d10426d06138 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Thu, 25 Jul 2024 14:25:56 -0700 Subject: [PATCH 4/8] write to PG --- .../dataloader/foreignKeyLoaderMakers.ts | 14 +++- .../dataloader/primaryKeyLoaderMakers.ts | 10 ++- .../mutations/dismissSuggestedAction.ts | 6 ++ .../mutations/helpers/bootstrapNewUser.ts | 58 ++++++++++------ .../private/mutations/hardDeleteUser.ts | 1 - .../1721940319404_SuggestedAction-phase1.ts | 51 ++++++++++++++ packages/server/postgres/select.ts | 4 ++ .../safeMutations/acceptTeamInvitation.ts | 66 +++++++++---------- .../safeMutations/removeSuggestedAction.ts | 12 +++- 9 files changed, 164 insertions(+), 58 deletions(-) create mode 100644 packages/server/postgres/migrations/1721940319404_SuggestedAction-phase1.ts diff --git a/packages/server/dataloader/foreignKeyLoaderMakers.ts b/packages/server/dataloader/foreignKeyLoaderMakers.ts index ce59a0b4126..44feb29b8ff 100644 --- a/packages/server/dataloader/foreignKeyLoaderMakers.ts +++ b/packages/server/dataloader/foreignKeyLoaderMakers.ts @@ -1,5 +1,9 @@ import getKysely from '../postgres/getKysely' -import {selectTemplateDimension, selectTemplateScale} from '../postgres/select' +import { + selectSuggestedAction, + selectTemplateDimension, + selectTemplateScale +} from '../postgres/select' import {foreignKeyLoaderMaker} from './foreignKeyLoaderMaker' import {selectOrganizations, selectRetroReflections, selectTeams} from './primaryKeyLoaderMakers' @@ -151,3 +155,11 @@ export const templateDimensionsByScaleId = foreignKeyLoaderMaker( return selectTemplateDimension().where('scaleId', 'in', scaleIds).orderBy('sortOrder').execute() } ) + +export const _suggestedActionsByUserId = foreignKeyLoaderMaker( + '_suggestedActions', + 'userId', + async (userIds) => { + return selectSuggestedAction().where('userId', 'in', userIds).execute() + } +) diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index 7f8f900fd37..91e6b04b1fd 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -7,7 +7,11 @@ import {getTeamPromptResponsesByIds} from '../postgres/queries/getTeamPromptResp import getTemplateRefsByIds from '../postgres/queries/getTemplateRefsByIds' import getTemplateScaleRefsByIds from '../postgres/queries/getTemplateScaleRefsByIds' import {getUsersByIds} from '../postgres/queries/getUsersByIds' -import {selectTemplateDimension, selectTemplateScale} from '../postgres/select' +import { + selectSuggestedAction, + selectTemplateDimension, + selectTemplateScale +} from '../postgres/select' import {primaryKeyLoaderMaker} from './primaryKeyLoaderMaker' export const users = primaryKeyLoaderMaker(getUsersByIds) @@ -159,3 +163,7 @@ export const templateScales = primaryKeyLoaderMaker((ids: readonly string[]) => export const templateDimensions = primaryKeyLoaderMaker((ids: readonly string[]) => { return selectTemplateDimension().where('id', 'in', ids).execute() }) + +export const _suggestedActions = primaryKeyLoaderMaker((ids: readonly string[]) => { + return selectSuggestedAction().where('id', 'in', ids).execute() +}) diff --git a/packages/server/graphql/mutations/dismissSuggestedAction.ts b/packages/server/graphql/mutations/dismissSuggestedAction.ts index 22a91062387..5c34d8ccf6c 100644 --- a/packages/server/graphql/mutations/dismissSuggestedAction.ts +++ b/packages/server/graphql/mutations/dismissSuggestedAction.ts @@ -1,5 +1,6 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import getRethink from '../../database/rethinkDriver' +import getKysely from '../../postgres/getKysely' import {getUserId} from '../../utils/authorization' import standardError from '../../utils/standardError' import {GQLContext} from '../graphql' @@ -34,6 +35,11 @@ export default { } // RESOLUTION + await getKysely() + .updateTable('SuggestedAction') + .set({removedAt: now}) + .where('id', '=', suggestedActionId) + .execute() await r.table('SuggestedAction').get(suggestedActionId).update({removedAt: now}).run() // no need to publish since that'll only affect their other open tabs diff --git a/packages/server/graphql/mutations/helpers/bootstrapNewUser.ts b/packages/server/graphql/mutations/helpers/bootstrapNewUser.ts index 8d1d09d3cbd..185e6aa701a 100644 --- a/packages/server/graphql/mutations/helpers/bootstrapNewUser.ts +++ b/packages/server/graphql/mutations/helpers/bootstrapNewUser.ts @@ -1,8 +1,5 @@ import getRethink from '../../../database/rethinkDriver' import AuthToken from '../../../database/types/AuthToken' -import SuggestedActionCreateNewTeam from '../../../database/types/SuggestedActionCreateNewTeam' -import SuggestedActionInviteYourTeam from '../../../database/types/SuggestedActionInviteYourTeam' -import SuggestedActionTryTheDemo from '../../../database/types/SuggestedActionTryTheDemo' import TimelineEventJoinedParabol from '../../../database/types/TimelineEventJoinedParabol' import User from '../../../database/types/User' import generateUID from '../../../generateUID' @@ -89,28 +86,43 @@ const bootstrapNewUser = async ( const teamsWithAutoJoin = teamsWithAutoJoinRes.flat().filter(isValid) const tms = [] as string[] - + const actions = [ + { + id: generateUID(), + userId, + type: 'tryTheDemo' as const, + priority: 1 + }, + { + id: generateUID(), + userId, + type: 'createNewTeam' as const, + priority: 4 + } + ] if (teamsWithAutoJoin.length > 0) { await Promise.all( teamsWithAutoJoin.map((team) => { const teamId = team.id tms.push(teamId) + const inviteYourTeam = { + id: generateUID(), + userId, + teamId, + type: 'inviteYourTeam' as const, + priority: 2 + } return Promise.all([ acceptTeamInvitation(team, userId, dataLoader), isOrganic ? Promise.all([ - r - .table('SuggestedAction') - .insert(new SuggestedActionInviteYourTeam({userId, teamId})) - .run() + pg.insertInto('SuggestedAction').values(inviteYourTeam).execute(), + r.table('SuggestedAction').insert(inviteYourTeam).run() ]) - : r - .table('SuggestedAction') - .insert([ - new SuggestedActionTryTheDemo({userId}), - new SuggestedActionCreateNewTeam({userId}) - ]) - .run(), + : Promise.all([ + pg.insertInto('SuggestedAction').values(actions).execute(), + r.table('SuggestedAction').insert(actions).run() + ]), analytics.autoJoined(newUser, teamId) ]) }) @@ -127,18 +139,24 @@ const bootstrapNewUser = async ( } const orgName = `${newUser.preferredName}’s Org` await createNewOrg(orgId, orgName, userId, email, dataLoader) + const inviteYourTeam = { + id: generateUID(), + userId, + teamId, + type: 'inviteYourTeam' as const, + priority: 2 + } await Promise.all([ createTeamAndLeader(newUser as IUser, validNewTeam, dataLoader), addSeedTasks(userId, teamId), - r.table('SuggestedAction').insert(new SuggestedActionInviteYourTeam({userId, teamId})).run(), + pg.insertInto('SuggestedAction').values(inviteYourTeam).execute(), + r.table('SuggestedAction').insert(inviteYourTeam).run(), sendPromptToJoinOrg(newUser, dataLoader) ]) analytics.newOrg(newUser, orgId, teamId, true) } else { - await r - .table('SuggestedAction') - .insert([new SuggestedActionTryTheDemo({userId}), new SuggestedActionCreateNewTeam({userId})]) - .run() + await pg.insertInto('SuggestedAction').values(actions).execute() + await r.table('SuggestedAction').insert(actions).run() } analytics.accountCreated(newUser, !isOrganic, isPatient0) diff --git a/packages/server/graphql/private/mutations/hardDeleteUser.ts b/packages/server/graphql/private/mutations/hardDeleteUser.ts index 9037ff5a653..7bf8932ca67 100644 --- a/packages/server/graphql/private/mutations/hardDeleteUser.ts +++ b/packages/server/graphql/private/mutations/hardDeleteUser.ts @@ -89,7 +89,6 @@ const hardDeleteUser: MutationResolvers['hardDeleteUser'] = async ( .run(), meetingMember: r.table('MeetingMember').getAll(userIdToDelete, {index: 'userId'}).delete(), notification: r.table('Notification').getAll(userIdToDelete, {index: 'userId'}).delete(), - suggestedAction: r.table('SuggestedAction').getAll(userIdToDelete, {index: 'userId'}).delete(), createdTasks: r .table('Task') .getAll(r.args(teamIds), {index: 'teamId'}) diff --git a/packages/server/postgres/migrations/1721940319404_SuggestedAction-phase1.ts b/packages/server/postgres/migrations/1721940319404_SuggestedAction-phase1.ts new file mode 100644 index 00000000000..b37e2842dd4 --- /dev/null +++ b/packages/server/postgres/migrations/1721940319404_SuggestedAction-phase1.ts @@ -0,0 +1,51 @@ +import {Client} from 'pg' +import getPgConfig from '../getPgConfig' + +export async function up() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'SuggestedActionTypeEnum') THEN + CREATE TYPE "SuggestedActionTypeEnum" AS ENUM ( + 'inviteYourTeam', + 'tryTheDemo', + 'createNewTeam', + 'tryRetroMeeting', + 'tryActionMeeting' + ); + END IF; + CREATE TABLE IF NOT EXISTS "SuggestedAction" ( + "id" VARCHAR(100) PRIMARY KEY, + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "priority" SMALLINT NOT NULL DEFAULT 0, + "removedAt" TIMESTAMP WITH TIME ZONE, + "type" "SuggestedActionTypeEnum" NOT NULL, + "teamId" VARCHAR(100), + "userId" VARCHAR(100) NOT NULL, + CONSTRAINT "fk_userId" + FOREIGN KEY("userId") + REFERENCES "User"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_teamId" + FOREIGN KEY("teamId") + REFERENCES "Team"("id") + ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS "idx_SuggestedAction_teamId" ON "SuggestedAction"("teamId"); + CREATE INDEX IF NOT EXISTS "idx_SuggestedAction_userId" ON "SuggestedAction"("userId"); + END $$; +`) + await client.end() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DROP TABLE "SuggestedAction"; + DROP TYPE "SuggestedActionTypeEnum"; + ` /* Do undo magic */) + await client.end() +} diff --git a/packages/server/postgres/select.ts b/packages/server/postgres/select.ts index e63cd74062e..520b0caab72 100644 --- a/packages/server/postgres/select.ts +++ b/packages/server/postgres/select.ts @@ -20,3 +20,7 @@ export const selectTemplateScale = () => { export const selectTemplateDimension = () => { return getKysely().selectFrom('TemplateDimension').selectAll().where('removedAt', 'is', null) } + +export const selectSuggestedAction = () => { + return getKysely().selectFrom('SuggestedAction').selectAll().where('removedAt', 'is', null) +} diff --git a/packages/server/safeMutations/acceptTeamInvitation.ts b/packages/server/safeMutations/acceptTeamInvitation.ts index 587a4b68fdf..ede8209aca3 100644 --- a/packages/server/safeMutations/acceptTeamInvitation.ts +++ b/packages/server/safeMutations/acceptTeamInvitation.ts @@ -3,7 +3,6 @@ import {InvoiceItemType} from 'parabol-client/types/constEnums' import TeamMemberId from '../../client/shared/gqlIds/TeamMemberId' import adjustUserCount from '../billing/helpers/adjustUserCount' import getRethink from '../database/rethinkDriver' -import SuggestedActionCreateNewTeam from '../database/types/SuggestedActionCreateNewTeam' import {DataLoaderInstance} from '../dataloader/RootDataLoader' import generateUID from '../generateUID' import {DataLoaderWorker} from '../graphql/graphql' @@ -23,38 +22,39 @@ const handleFirstAcceptedInvitation = async ( const teamMembers = await dataLoader.get('teamMembersByTeamId').load(teamId) const teamLead = teamMembers.find((tm) => tm.isLead)! const {userId} = teamLead - const isNewTeamLead = await r - .table('SuggestedAction') - .getAll(userId, {index: 'userId'}) - .filter({type: 'tryRetroMeeting'}) - .count() - .eq(0) - .run() - if (!isNewTeamLead) return null - await r - .table('SuggestedAction') - .insert([ - { - id: generateUID(), - createdAt: now, - priority: 3, - removedAt: null, - teamId, - type: 'tryRetroMeeting', - userId - }, - new SuggestedActionCreateNewTeam({userId}), - { - id: generateUID(), - createdAt: now, - priority: 5, - removedAt: null, - teamId, - type: 'tryActionMeeting', - userId - } - ]) - .run() + const suggestedActions = await dataLoader.get('suggestedActionsByUserId').load(userId) + const hasTryRetro = suggestedActions.some((sa) => sa.type === 'tryRetroMeeting') + if (hasTryRetro) return null + const actions = [ + { + id: generateUID(), + createdAt: now, + priority: 3, + removedAt: null, + teamId, + type: 'tryRetroMeeting', + userId + }, + { + id: generateUID(), + createdAt: now, + priority: 4, + removedAt: null, + type: 'createNewTeam', + userId + }, + { + id: generateUID(), + createdAt: now, + priority: 5, + removedAt: null, + teamId, + type: 'tryActionMeeting', + userId + } + ] + await getKysely().insertInto('SuggestedAction').values(actions).execute() + await r.table('SuggestedAction').insert(actions).run() return userId } diff --git a/packages/server/safeMutations/removeSuggestedAction.ts b/packages/server/safeMutations/removeSuggestedAction.ts index 76d7aca0ad4..7d1d944d65c 100644 --- a/packages/server/safeMutations/removeSuggestedAction.ts +++ b/packages/server/safeMutations/removeSuggestedAction.ts @@ -1,8 +1,16 @@ +import {sql} from 'kysely' import getRethink from '../database/rethinkDriver' -import {TSuggestedActionTypeEnum} from '../graphql/types/SuggestedActionTypeEnum' +import getKysely from '../postgres/getKysely' +import {SuggestedAction} from '../postgres/pg' -const removeSuggestedAction = async (userId: string, type: TSuggestedActionTypeEnum) => { +const removeSuggestedAction = async (userId: string, type: SuggestedAction['type']) => { const r = await getRethink() + await getKysely() + .updateTable('SuggestedAction') + .set({removedAt: sql`CURRENT_TIMESTAMP`}) + .where('userId', '=', userId) + .where('type', '=', type) + .execute() return r .table('SuggestedAction') .getAll(userId, {index: 'userId'}) From c7fb5da114a0070ee2213ad0a71e12087940a12e Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Fri, 26 Jul 2024 09:56:10 -0700 Subject: [PATCH 5/8] fix tsc errors Signed-off-by: Matt Krick --- packages/server/safeMutations/acceptTeamInvitation.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/server/safeMutations/acceptTeamInvitation.ts b/packages/server/safeMutations/acceptTeamInvitation.ts index ede8209aca3..c48cba5df64 100644 --- a/packages/server/safeMutations/acceptTeamInvitation.ts +++ b/packages/server/safeMutations/acceptTeamInvitation.ts @@ -32,7 +32,7 @@ const handleFirstAcceptedInvitation = async ( priority: 3, removedAt: null, teamId, - type: 'tryRetroMeeting', + type: 'tryRetroMeeting' as const, userId }, { @@ -40,7 +40,7 @@ const handleFirstAcceptedInvitation = async ( createdAt: now, priority: 4, removedAt: null, - type: 'createNewTeam', + type: 'createNewTeam' as const, userId }, { @@ -49,7 +49,7 @@ const handleFirstAcceptedInvitation = async ( priority: 5, removedAt: null, teamId, - type: 'tryActionMeeting', + type: 'tryActionMeeting' as const, userId } ] From 333ff79754569b57963d320ebde857d6d1b7b6ba Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Fri, 26 Jul 2024 10:57:36 -0700 Subject: [PATCH 6/8] chore: migrate existing rows to PG Signed-off-by: Matt Krick --- .../1722013208553_SuggestAction-phase2.ts | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 packages/server/postgres/migrations/1722013208553_SuggestAction-phase2.ts diff --git a/packages/server/postgres/migrations/1722013208553_SuggestAction-phase2.ts b/packages/server/postgres/migrations/1722013208553_SuggestAction-phase2.ts new file mode 100644 index 00000000000..7414c33de7e --- /dev/null +++ b/packages/server/postgres/migrations/1722013208553_SuggestAction-phase2.ts @@ -0,0 +1,100 @@ +import {Kysely, PostgresDialect, sql} from 'kysely' +import {r} from 'rethinkdb-ts' +import connectRethinkDB from '../../database/connectRethinkDB' +import getPg from '../getPg' + +const toCreditCard = (creditCard: any) => { + if (!creditCard) return null + return sql`(select json_populate_record(null::"CreditCard", ${JSON.stringify(creditCard)}))` +} + +export async function up() { + await connectRethinkDB() + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + try { + console.log('Adding index') + await r + .table('SuggestedAction') + .indexCreate('createdAtId', (row: any) => [row('createdAt'), row('id')]) + .run() + await r.table('SuggestedAction').indexWait().run() + } catch { + // index already exists + } + console.log('Adding index complete') + const MAX_PG_PARAMS = 65545 + const PG_COLS = ['id', 'createdAt', 'priority', 'removedAt', 'type', 'teamId', 'userId'] as const + type SuggestedAction = { + [K in (typeof PG_COLS)[number]]: any + } + const BATCH_SIZE = Math.trunc(MAX_PG_PARAMS / PG_COLS.length) + + let curcreatedAt = r.minval + let curId = r.minval + for (let i = 0; i < 1e6; i++) { + console.log('inserting row', i * BATCH_SIZE, curcreatedAt, curId) + const rawRowsToInsert = (await r + .table('SuggestedAction') + .between([curcreatedAt, curId], [r.maxval, r.maxval], { + index: 'createdAtId', + leftBound: 'open', + rightBound: 'closed' + }) + .orderBy({index: 'createdAtId'}) + .limit(BATCH_SIZE) + .pluck(...PG_COLS) + .run()) as SuggestedAction[] + + const rowsToInsert = rawRowsToInsert.map((row) => ({ + ...row + })) + if (rowsToInsert.length === 0) break + const lastRow = rowsToInsert[rowsToInsert.length - 1] + curcreatedAt = lastRow.createdAt + curId = lastRow.id + try { + await pg + .insertInto('SuggestedAction') + .values(rowsToInsert) + .onConflict((oc) => oc.doNothing()) + .execute() + } catch (e) { + await Promise.all( + rowsToInsert.map(async (row) => { + try { + await pg + .insertInto('SuggestedAction') + .values(row) + .onConflict((oc) => oc.doNothing()) + .execute() + } catch (e) { + if (e.constraint === 'fk_userId' || e.constraint === 'fk_teamId') { + console.log(`Skipping ${row.id} because it has no user/team`) + return + } + console.log(e, row) + } + }) + ) + } + } +} + +export async function down() { + await connectRethinkDB() + try { + await r.table('SuggestedAction').indexDrop('createdAtId').run() + } catch { + // index already dropped + } + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + await sql`TRUNCATE TABLE "SuggestedAction" CASCADE`.execute(pg) +} From 5aac571a51c807c4fb3377dc3b22db4fce5657e9 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Thu, 1 Aug 2024 15:31:46 -0700 Subject: [PATCH 7/8] rename migration --- ...gestAction-phase2.ts => 1722527208553_SuggestAction-phase2.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/server/postgres/migrations/{1722013208553_SuggestAction-phase2.ts => 1722527208553_SuggestAction-phase2.ts} (100%) diff --git a/packages/server/postgres/migrations/1722013208553_SuggestAction-phase2.ts b/packages/server/postgres/migrations/1722527208553_SuggestAction-phase2.ts similarity index 100% rename from packages/server/postgres/migrations/1722013208553_SuggestAction-phase2.ts rename to packages/server/postgres/migrations/1722527208553_SuggestAction-phase2.ts From 3391267de7e59df0bd7f45f1fe897caef11c053e Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Fri, 2 Aug 2024 14:24:15 -0700 Subject: [PATCH 8/8] rename migration Signed-off-by: Matt Krick --- ...gestAction-phase2.ts => 1722637208553_SuggestAction-phase2.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/server/postgres/migrations/{1722527208553_SuggestAction-phase2.ts => 1722637208553_SuggestAction-phase2.ts} (100%) diff --git a/packages/server/postgres/migrations/1722527208553_SuggestAction-phase2.ts b/packages/server/postgres/migrations/1722637208553_SuggestAction-phase2.ts similarity index 100% rename from packages/server/postgres/migrations/1722527208553_SuggestAction-phase2.ts rename to packages/server/postgres/migrations/1722637208553_SuggestAction-phase2.ts