feat(api): add updateCount field to user (#55349)

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Shaun Hamilton
2024-07-01 17:13:10 +02:00
committed by GitHub
parent 766e982040
commit 1de602814f
3 changed files with 153 additions and 15 deletions
+9 -8
View File
@@ -55,11 +55,6 @@ type ProfileUI {
showTimeLine Boolean? // Undefined
}
// Currently unused, so commented out.
// type ProgressTimestamp {
// timestamp Float
// }
type SavedChallengeFile {
contents String
ext String
@@ -74,11 +69,11 @@ type SavedChallenge {
lastSavedDate Float
}
/// Corresponds to the `user` collection.
model user {
id String @id @default(auto()) @map("_id") @db.ObjectId
about String
acceptedPrivacyTerms Boolean
// badges Json? // Undefined | { coreTeam [][] } removed, as new API never uses it.
completedChallenges CompletedChallenge[]
completedExams CompletedExam[] // Undefined
currentChallengeId String?
@@ -88,7 +83,6 @@ model user {
emailVerified Boolean
emailVerifyTTL DateTime? // Null | Undefined
externalId String
// github String? Removed, because value was only ever found to be Null
githubProfile String? // Undefined
isApisMicroservicesCert Boolean? // Undefined
isBackEndCert Boolean? // Undefined
@@ -127,13 +121,20 @@ model user {
portfolio Portfolio[]
profileUI ProfileUI? // Undefined
progressTimestamps Json? // ProgressTimestamp[] | Null[] | Int64[] | Double[] - TODO: NORMALIZE
// rand Float? // Undefined removed, as new API never uses it.
/// A random number between 0 and 1.
///
/// Valuable for selectively performing random logic.
rand Float?
savedChallenges SavedChallenge[] // Undefined | SavedChallenge[]
sendQuincyEmail Boolean
theme String? // Undefined
timezone String? // Undefined
twitter String? // Null | Undefined
unsubscribeId String
/// Used to track the number of times the user's record was written to.
///
/// This has the main benefit of allowing concurrent ops to check for race conditions.
updateCount Int? @default(0)
username String // TODO(Post-MVP): make this unique
usernameDisplay String? // Undefined
verificationToken String? // Undefined
+107
View File
@@ -0,0 +1,107 @@
import { defaultUserEmail, setupServer } from '../../jest.utils';
import { createUserInput } from '../utils/create-user';
describe('prisma client extensions', () => {
setupServer();
beforeEach(async () => {
await fastifyTestInstance.prisma.user.deleteMany({
where: { email: defaultUserEmail }
});
});
afterAll(async () => {
await fastifyTestInstance.prisma.user.deleteMany({
where: { email: defaultUserEmail }
});
});
describe('updateCount', () => {
it('should default to 0', async () => {
const user = await fastifyTestInstance.prisma.user.create({
data: createUserInput(defaultUserEmail)
});
expect(user).toMatchObject({
updateCount: 0
});
});
it('should increment by one for updates and creates', async () => {
const user = await fastifyTestInstance.prisma.user.create({
data: createUserInput(defaultUserEmail)
});
const updateUser = await fastifyTestInstance.prisma.user.update({
where: { id: user.id },
data: { username: 'any-change' }
});
expect(updateUser).toMatchObject({
username: 'any-change',
updateCount: 1
});
await fastifyTestInstance.prisma.user.updateMany({
where: { id: user.id },
// Even no change to values updates the updateCount
data: { username: 'any-change' }
});
const updateManyUser = await fastifyTestInstance.prisma.user.findUnique({
where: { id: user.id }
});
expect(updateManyUser).toMatchObject({
username: 'any-change',
updateCount: 2
});
const upsertUser = await fastifyTestInstance.prisma.user.upsert({
where: { id: user.id },
create: createUserInput(defaultUserEmail),
update: { username: 'upser-user' }
});
expect(upsertUser).toMatchObject({
username: 'upser-user',
updateCount: 3
});
});
it("should not increment for 'find' queries", async () => {
const user = await fastifyTestInstance.prisma.user.create({
data: createUserInput(defaultUserEmail)
});
const findUniqueUser = await fastifyTestInstance.prisma.user.findUnique({
where: { id: user.id }
});
expect(findUniqueUser).toMatchObject({
updateCount: 0
});
const findManyUsers = await fastifyTestInstance.prisma.user.findMany();
expect(findManyUsers).toHaveLength(1);
expect(findManyUsers[0]).toMatchObject({
updateCount: 0
});
const findFirstUser = await fastifyTestInstance.prisma.user.findFirst();
expect(findFirstUser).toMatchObject({
updateCount: 0
});
const findRawUser = await fastifyTestInstance.prisma.user.findRaw({
filter: { email: defaultUserEmail }
});
expect(findRawUser[0]).toMatchObject({
updateCount: 0
});
});
});
});
+37 -7
View File
@@ -7,18 +7,20 @@ import { MONGOHQ_URL } from '../utils/env';
declare module 'fastify' {
interface FastifyInstance {
prisma: PrismaClient;
prisma: ReturnType<typeof extendClient>;
}
}
const prismaPlugin: FastifyPluginAsync = fp(async (server, _options) => {
const prisma = new PrismaClient({
datasources: {
db: {
url: MONGOHQ_URL
const prisma = extendClient(
new PrismaClient({
datasources: {
db: {
url: MONGOHQ_URL
}
}
}
});
})
);
await prisma.$connect();
@@ -29,4 +31,32 @@ const prismaPlugin: FastifyPluginAsync = fp(async (server, _options) => {
});
});
// TODO: It would be nice to split this up into multiple update functions,
// but the types are a pain.
// TODO: Multiple extended clients can be used for different restrictions (e.g. session vs non-session users)
// TODO: Could be used to add other _easily forgotten_ fields like `progressTimestamp`
function extendClient(prisma: PrismaClient) {
return prisma.$extends({
query: {
user: {
async update({ args, query }) {
args.data.updateCount = { increment: 1 };
return query(args);
},
async updateMany({ args, query }) {
args.data.updateCount = { increment: 1 };
return query(args);
},
async upsert({ args, query }) {
args.update.updateCount = { increment: 1 };
return query(args);
}
// NOTE: raw ops are untouched, as it is meant to be a direct passthrough to mongodb
// async findRaw({ model, operation, args, query }) {}
// async aggregateRaw({ model, operation, args, query }) {}
}
}
});
}
export default prismaPlugin;