From e9092162e05089b3c110e39098c16d62a83cae20 Mon Sep 17 00:00:00 2001 From: martmull Date: Thu, 19 Oct 2023 22:48:34 +0200 Subject: [PATCH] 2049 timebox 1j zapier integration 4 define and implement a first trigger for zapier app (#2132) * Add create company trigger * Refactor * Add operation in subscribe * Add create hook api endpoint * Add import of hook module * Add a test for hook subscribe * Add delete hook api endpoint * Add delete hook test * Add findMany hook route --------- Co-authored-by: Charles Bochet --- .../src/creates/create_company.ts | 28 ++---- .../src/creates/create_person.ts | 28 ++---- packages/twenty-zapier/src/index.ts | 6 +- .../src/test/authentication.test.ts | 31 +++---- .../src/test/triggers/company.test.ts | 90 +++++++++++++++++++ .../twenty-zapier/src/triggers/company.ts | 74 +++++++++++++++ packages/twenty-zapier/src/utils/getBundle.ts | 17 +++- packages/twenty-zapier/src/utils/requestDb.ts | 2 + server/src/ability/ability.factory.ts | 7 ++ server/src/ability/ability.module.ts | 13 +++ .../ability/handlers/hook.ability-handler.ts | 57 ++++++++++++ server/src/core/core.module.ts | 3 + server/src/core/hook/hook.module.ts | 12 +++ server/src/core/hook/hook.resolver.ts | 72 +++++++++++++++ .../migration.sql | 15 ++++ server/src/database/schema.prisma | 19 ++++ .../utils/prisma-select/model-select-map.ts | 1 + 17 files changed, 413 insertions(+), 62 deletions(-) create mode 100644 packages/twenty-zapier/src/test/triggers/company.test.ts create mode 100644 packages/twenty-zapier/src/triggers/company.ts create mode 100644 server/src/ability/handlers/hook.ability-handler.ts create mode 100644 server/src/core/hook/hook.module.ts create mode 100644 server/src/core/hook/hook.resolver.ts create mode 100644 server/src/database/migrations/20231019144904_complete_comment_thread_migration/migration.sql diff --git a/packages/twenty-zapier/src/creates/create_company.ts b/packages/twenty-zapier/src/creates/create_company.ts index e18df1f8d..1b36dc091 100644 --- a/packages/twenty-zapier/src/creates/create_company.ts +++ b/packages/twenty-zapier/src/creates/create_company.ts @@ -1,26 +1,16 @@ import { Bundle, ZObject } from 'zapier-platform-core'; import handleQueryParams from '../utils/handleQueryParams'; +import requestDb from '../utils/requestDb'; const perform = async (z: ZObject, bundle: Bundle) => { - const response = await z.request({ - body: { - query: ` - mutation CreateCompany { - createOneCompany( - data:{${handleQueryParams(bundle.inputData)}} - ) - {id} - }`, - }, - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - Authorization: `Bearer ${bundle.authData.apiKey}`, - }, - method: 'POST', - url: `${process.env.SERVER_BASE_URL}/graphql`, - }); - return response.json; + const query = ` + mutation CreateCompany { + createOneCompany( + data:{${handleQueryParams(bundle.inputData)}} + ) + {id} + }`; + return await requestDb(z, bundle, query); }; export default { display: { diff --git a/packages/twenty-zapier/src/creates/create_person.ts b/packages/twenty-zapier/src/creates/create_person.ts index 1fe799a5f..1b0a4102d 100644 --- a/packages/twenty-zapier/src/creates/create_person.ts +++ b/packages/twenty-zapier/src/creates/create_person.ts @@ -1,26 +1,16 @@ import { Bundle, ZObject } from 'zapier-platform-core'; import handleQueryParams from '../utils/handleQueryParams'; +import requestDb from '../utils/requestDb'; const perform = async (z: ZObject, bundle: Bundle) => { - const response = await z.request({ - body: { - query: ` - mutation CreatePerson { - createOnePerson( - data:{${handleQueryParams(bundle.inputData)}} - ) - {id} - }`, - }, - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - Authorization: `Bearer ${bundle.authData.apiKey}`, - }, - method: 'POST', - url: `${process.env.SERVER_BASE_URL}/graphql`, - }); - return response.json; + const query = ` + mutation CreatePerson { + createOnePerson( + data:{${handleQueryParams(bundle.inputData)}} + ) + {id} + }`; + return await requestDb(z, bundle, query); }; export default { display: { diff --git a/packages/twenty-zapier/src/index.ts b/packages/twenty-zapier/src/index.ts index 11329623b..f6c0317db 100644 --- a/packages/twenty-zapier/src/index.ts +++ b/packages/twenty-zapier/src/index.ts @@ -2,13 +2,17 @@ const { version } = require('../package.json'); import { version as platformVersion } from 'zapier-platform-core'; import createPerson from './creates/create_person'; import createCompany from './creates/create_company'; +import company from './triggers/company'; import authentication from './authentication'; -import 'dotenv/config' +import 'dotenv/config'; export default { version, platformVersion, authentication: authentication, + triggers: { + [company.key]: company, + }, creates: { [createPerson.key]: createPerson, [createCompany.key]: createCompany, diff --git a/packages/twenty-zapier/src/test/authentication.test.ts b/packages/twenty-zapier/src/test/authentication.test.ts index 89a680cb2..54c499119 100644 --- a/packages/twenty-zapier/src/test/authentication.test.ts +++ b/packages/twenty-zapier/src/test/authentication.test.ts @@ -7,33 +7,22 @@ import { ZObject, } from 'zapier-platform-core'; import getBundle from '../utils/getBundle'; +import handleQueryParams from '../utils/handleQueryParams'; +import requestDb from '../utils/requestDb'; const appTester = createAppTester(App); tools.env.inject(); const generateKey = async (z: ZObject, bundle: Bundle) => { - const options = { - url: `${process.env.SERVER_BASE_URL}/graphql`, - method: 'POST', - headers: { - Authorization: `Bearer ${bundle.authData.apiKey}`, - }, - body: { - query: `mutation - CreateApiKey { - createOneApiKey(data:{ - name:"${bundle.inputData.name}", - expiresAt: "${bundle.inputData.expiresAt}" - }) {token}}`, - }, - } satisfies HttpRequestOptions; - return z.request(options).then((response) => { - const results = response.json; - return results.data.createOneApiKey.token; - }); + const query = ` + mutation CreateApiKey { + createOneApiKey( + data:{${handleQueryParams(bundle.inputData)}} + ) + {token} + }`; + return (await requestDb(z, bundle, query)).data.createOneApiKey.token; }; -const apiKey = String(process.env.API_KEY); - describe('custom auth', () => { it('passes authentication and returns json', async () => { const bundle = getBundle(); diff --git a/packages/twenty-zapier/src/test/triggers/company.test.ts b/packages/twenty-zapier/src/test/triggers/company.test.ts new file mode 100644 index 000000000..5df4259af --- /dev/null +++ b/packages/twenty-zapier/src/test/triggers/company.test.ts @@ -0,0 +1,90 @@ +import { Bundle, createAppTester, ZObject } from 'zapier-platform-core'; +import App from '../../index'; +import getBundle from '../../utils/getBundle'; +import requestDb from '../../utils/requestDb'; +const appTester = createAppTester(App); + +describe('triggers.company', () => { + test('should succeed to subscribe', async () => { + const bundle = getBundle({}); + bundle.targetUrl = 'https://test.com'; + const result = await appTester( + App.triggers.company.operation.performSubscribe, + bundle, + ); + expect(result).toBeDefined(); + expect(result.id).toBeDefined(); + const checkDbResult = await appTester( + (z: ZObject, bundle: Bundle) => + requestDb( + z, + bundle, + `query findManyHook {findManyHook(where: {id: {equals: "${result.id}"}}){id operation}}`, + ), + bundle, + ); + expect(checkDbResult.data.findManyHook.length).toEqual(1); + expect(checkDbResult.data.findManyHook[0].operation).toEqual( + 'createOneCompany', + ); + }); + test('should succeed to unsubscribe', async () => { + const bundle = getBundle({}); + bundle.targetUrl = 'https://test.com'; + const result = await appTester( + App.triggers.company.operation.performSubscribe, + bundle, + ); + const unsubscribeBundle = getBundle({}); + unsubscribeBundle.subscribeData = { id: result.id }; + const unsubscribeResult = await appTester( + App.triggers.company.operation.performUnsubscribe, + unsubscribeBundle, + ); + expect(unsubscribeResult).toBeDefined(); + expect(unsubscribeResult.id).toEqual(result.id); + const checkDbResult = await appTester( + (z: ZObject, bundle: Bundle) => + requestDb( + z, + bundle, + `query findManyHook {findManyHook(where: {id: {equals: "${result.id}"}}){id}}`, + ), + bundle, + ); + expect(checkDbResult.data.findManyHook.length).toEqual(0); + }); + test('should load company from hook', async () => { + const bundle = { + cleanedRequest: { + id: 'd6ccb1d1-a90b-4822-a992-a0dd946592c9', + name: '', + domainName: '', + createdAt: '2023-10-19 10:10:12.490', + address: '', + employees: null, + linkedinUrl: null, + xUrl: null, + annualRecurringRevenue: null, + idealCustomerProfile: false, + }, + }; + const results = await appTester( + App.triggers.company.operation.perform, + bundle, + ); + expect(results.length).toEqual(1); + const company = results[0]; + expect(company.id).toEqual('d6ccb1d1-a90b-4822-a992-a0dd946592c9'); + }); + it('should load companies from list', async () => { + const bundle = getBundle({}); + const results = await appTester( + App.triggers.company.operation.performList, + bundle, + ); + expect(results.length).toBeGreaterThan(1); + const firstCompany = results[0]; + expect(firstCompany.id).toBeDefined(); + }); +}); diff --git a/packages/twenty-zapier/src/triggers/company.ts b/packages/twenty-zapier/src/triggers/company.ts new file mode 100644 index 000000000..96464f94f --- /dev/null +++ b/packages/twenty-zapier/src/triggers/company.ts @@ -0,0 +1,74 @@ +import { Bundle, ZObject } from 'zapier-platform-core'; +import requestDb from '../utils/requestDb'; +import handleQueryParams from '../utils/handleQueryParams'; + +const performSubscribe = async (z: ZObject, bundle: Bundle) => { + const data = { targetUrl: bundle.targetUrl, operation: 'createOneCompany' }; + const result = await requestDb( + z, + bundle, + `mutation createOneHook {createOneHook(data:{${handleQueryParams( + data, + )}}) {id}}`, + ); + return result.data.createOneHook; +}; +const performUnsubscribe = async (z: ZObject, bundle: Bundle) => { + const data = { id: bundle.subscribeData?.id }; + const result = await requestDb( + z, + bundle, + `mutation deleteOneHook {deleteOneHook(where:{${handleQueryParams( + data, + )}}) {id}}`, + ); + return result.data.deleteOneHook; +}; +const perform = (z: ZObject, bundle: Bundle) => { + return [bundle.cleanedRequest]; +}; +const performList = async (z: ZObject, bundle: Bundle) => { + const results = await requestDb( + z, + bundle, + `query FindManyCompany {findManyCompany { + id + name + domainName + createdAt + address + employees + linkedinUrl + xUrl + annualRecurringRevenue + idealCustomerProfile + }}`, + ); + return results.data.findManyCompany; +}; +export default { + key: 'company', + noun: 'Company', + display: { + label: 'New Company', + description: 'Triggers when a new company is created.', + }, + operation: { + inputFields: [], + type: 'hook', + performSubscribe, + performUnsubscribe, + perform, + performList, + sample: { + id: 'f75f6b2e-9442-4c72-aa95-47d8e5ec8cb3', + createdAt: '2023-10-19 07:37:25.306', + workspaceId: 'c8b070fc-c969-4ca5-837a-e7c3735734d2', + }, + outputFields: [ + { key: 'id', label: 'ID' }, + { key: 'createdAt', label: 'Created At' }, + { key: 'workspaceId', label: 'Workspace ID' }, + ], + }, +}; diff --git a/packages/twenty-zapier/src/utils/getBundle.ts b/packages/twenty-zapier/src/utils/getBundle.ts index 318b5bf7a..b20ae832a 100644 --- a/packages/twenty-zapier/src/utils/getBundle.ts +++ b/packages/twenty-zapier/src/utils/getBundle.ts @@ -1,7 +1,20 @@ -const getBundle = (inputData?: object) => { +import { Bundle } from 'zapier-platform-core'; + +const getBundle = (inputData?: { [x: string]: any }): Bundle => { return { authData: { apiKey: String(process.env.API_KEY) }, - inputData, + inputData: inputData || {}, + cleanedRequest: {}, + inputDataRaw: {}, + meta: { + isBulkRead: false, + isFillingDynamicDropdown: false, + isLoadingSample: false, + isPopulatingDedupe: false, + isTestingAuth: false, + limit: 1, + page: 1, + }, }; }; export default getBundle; diff --git a/packages/twenty-zapier/src/utils/requestDb.ts b/packages/twenty-zapier/src/utils/requestDb.ts index 9d81a07ee..cc0b21e04 100644 --- a/packages/twenty-zapier/src/utils/requestDb.ts +++ b/packages/twenty-zapier/src/utils/requestDb.ts @@ -5,6 +5,8 @@ const requestDb = async (z: ZObject, bundle: Bundle, query: string) => { url: `${process.env.SERVER_BASE_URL}/graphql`, method: 'POST', headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', Authorization: `Bearer ${bundle.authData.apiKey}`, }, body: { diff --git a/server/src/ability/ability.factory.ts b/server/src/ability/ability.factory.ts index a956c7cf1..d4cdcd936 100644 --- a/server/src/ability/ability.factory.ts +++ b/server/src/ability/ability.factory.ts @@ -7,6 +7,7 @@ import { ActivityTarget, Attachment, ApiKey, + Hook, Comment, Company, Favorite, @@ -35,6 +36,7 @@ type SubjectsAbility = Subjects<{ Comment: Comment; Company: Company; Favorite: Favorite; + Hook: Hook; Person: Person; Pipeline: Pipeline; PipelineProgress: PipelineProgress; @@ -81,6 +83,11 @@ export class AbilityFactory { can(AbilityAction.Create, 'ApiKey'); can(AbilityAction.Update, 'ApiKey', { workspaceId: workspace.id }); + // Hook + can(AbilityAction.Read, 'Hook', { workspaceId: workspace.id }); + can(AbilityAction.Create, 'Hook'); + can(AbilityAction.Delete, 'Hook', { workspaceId: workspace.id }); + // Workspace can(AbilityAction.Read, 'Workspace'); can(AbilityAction.Update, 'Workspace'); diff --git a/server/src/ability/ability.module.ts b/server/src/ability/ability.module.ts index 2ff46a844..05dca18ca 100644 --- a/server/src/ability/ability.module.ts +++ b/server/src/ability/ability.module.ts @@ -128,6 +128,11 @@ import { ManageApiKeyAbilityHandler, ReadApiKeyAbilityHandler, } from './handlers/api-key.ability-handler'; +import { + CreateHookAbilityHandler, + DeleteHookAbilityHandler, + ReadHookAbilityHandler, +} from './handlers/hook.ability-handler'; @Module({ providers: [ @@ -239,6 +244,10 @@ import { ManageApiKeyAbilityHandler, CreateApiKeyAbilityHandler, UpdateApiKeyAbilityHandler, + // Hook + CreateHookAbilityHandler, + DeleteHookAbilityHandler, + ReadHookAbilityHandler, ], exports: [ AbilityFactory, @@ -348,6 +357,10 @@ import { ManageApiKeyAbilityHandler, CreateApiKeyAbilityHandler, UpdateApiKeyAbilityHandler, + // Hook + CreateHookAbilityHandler, + DeleteHookAbilityHandler, + ReadHookAbilityHandler, ], }) export class AbilityModule {} diff --git a/server/src/ability/handlers/hook.ability-handler.ts b/server/src/ability/handlers/hook.ability-handler.ts new file mode 100644 index 000000000..1c2574b55 --- /dev/null +++ b/server/src/ability/handlers/hook.ability-handler.ts @@ -0,0 +1,57 @@ +import { + Injectable, + ExecutionContext, + NotFoundException, +} from '@nestjs/common'; +import { GqlExecutionContext } from '@nestjs/graphql'; + +import { subject } from '@casl/ability'; + +import { IAbilityHandler } from 'src/ability/interfaces/ability-handler.interface'; + +import { AppAbility } from 'src/ability/ability.factory'; +import { relationAbilityChecker } from 'src/ability/ability.util'; +import { PrismaService } from 'src/database/prisma.service'; +import { AbilityAction } from 'src/ability/ability.action'; +import { assert } from 'src/utils/assert'; + +@Injectable() +export class CreateHookAbilityHandler implements IAbilityHandler { + constructor(private readonly prismaService: PrismaService) {} + + async handle(ability: AppAbility, context: ExecutionContext) { + const gqlContext = GqlExecutionContext.create(context); + const args = gqlContext.getArgs(); + const allowed = await relationAbilityChecker( + 'Hook', + ability, + this.prismaService.client, + args, + ); + if (!allowed) { + return false; + } + return ability.can(AbilityAction.Create, 'Hook'); + } +} + +@Injectable() +export class DeleteHookAbilityHandler implements IAbilityHandler { + constructor(private readonly prismaService: PrismaService) {} + async handle(ability: AppAbility, context: ExecutionContext) { + const gqlContext = GqlExecutionContext.create(context); + const args = gqlContext.getArgs(); + const hook = await this.prismaService.client.hook.findFirst({ + where: args.where, + }); + assert(hook, '', NotFoundException); + return ability.can(AbilityAction.Delete, subject('Hook', hook)); + } +} + +@Injectable() +export class ReadHookAbilityHandler implements IAbilityHandler { + async handle(ability: AppAbility) { + return ability.can(AbilityAction.Read, 'Hook'); + } +} diff --git a/server/src/core/core.module.ts b/server/src/core/core.module.ts index 19044ad64..fec8c726d 100644 --- a/server/src/core/core.module.ts +++ b/server/src/core/core.module.ts @@ -15,6 +15,7 @@ import { ActivityModule } from './activity/activity.module'; import { ViewModule } from './view/view.module'; import { FavoriteModule } from './favorite/favorite.module'; import { ApiKeyModule } from './api-key/api-key.module'; +import { HookModule } from './hook/hook.module'; @Module({ imports: [ @@ -33,6 +34,7 @@ import { ApiKeyModule } from './api-key/api-key.module'; ViewModule, FavoriteModule, ApiKeyModule, + HookModule, ], exports: [ AuthModule, @@ -46,6 +48,7 @@ import { ApiKeyModule } from './api-key/api-key.module'; AttachmentModule, FavoriteModule, ApiKeyModule, + HookModule, ], }) export class CoreModule {} diff --git a/server/src/core/hook/hook.module.ts b/server/src/core/hook/hook.module.ts new file mode 100644 index 000000000..c7ee4f69a --- /dev/null +++ b/server/src/core/hook/hook.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; + +import { PrismaModule } from 'src/database/prisma.module'; +import { AbilityModule } from 'src/ability/ability.module'; + +import { HookResolver } from './hook.resolver'; + +@Module({ + imports: [PrismaModule, AbilityModule], + providers: [HookResolver], +}) +export class HookModule {} diff --git a/server/src/core/hook/hook.resolver.ts b/server/src/core/hook/hook.resolver.ts new file mode 100644 index 000000000..06c928776 --- /dev/null +++ b/server/src/core/hook/hook.resolver.ts @@ -0,0 +1,72 @@ +import { NotFoundException, UseGuards } from '@nestjs/common'; +import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; + +import { accessibleBy } from '@casl/prisma'; + +import { JwtAuthGuard } from 'src/guards/jwt.auth.guard'; +import { Hook } from 'src/core/@generated/hook/hook.model'; +import { AbilityGuard } from 'src/guards/ability.guard'; +import { CheckAbilities } from 'src/decorators/check-abilities.decorator'; +import { + CreateHookAbilityHandler, + DeleteHookAbilityHandler, + ReadHookAbilityHandler, +} from 'src/ability/handlers/hook.ability-handler'; +import { CreateOneHookArgs } from 'src/core/@generated/hook/create-one-hook.args'; +import { PrismaService } from 'src/database/prisma.service'; +import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator'; +import { Workspace } from 'src/core/@generated/workspace/workspace.model'; +import { DeleteOneHookArgs } from 'src/core/@generated/hook/delete-one-hook.args'; +import { FindManyHookArgs } from 'src/core/@generated/hook/find-many-hook.args'; +import { UserAbility } from 'src/decorators/user-ability.decorator'; +import { AppAbility } from 'src/ability/ability.factory'; + +@UseGuards(JwtAuthGuard) +@Resolver(() => Hook) +export class HookResolver { + constructor(private readonly prismaService: PrismaService) {} + @Mutation(() => Hook) + @UseGuards(AbilityGuard) + @CheckAbilities(CreateHookAbilityHandler) + async createOneHook( + @Args() args: CreateOneHookArgs, + @AuthWorkspace() { id: workspaceId }: Workspace, + ): Promise { + return this.prismaService.client.hook.create({ + data: { + ...args.data, + ...{ workspace: { connect: { id: workspaceId } } }, + }, + }); + } + + @Mutation(() => Hook, { nullable: false }) + @UseGuards(AbilityGuard) + @CheckAbilities(DeleteHookAbilityHandler) + async deleteOneHook(@Args() args: DeleteOneHookArgs): Promise { + const hookToDelete = this.prismaService.client.hook.findUnique({ + where: args.where, + }); + if (!hookToDelete) { + throw new NotFoundException(); + } + return await this.prismaService.client.hook.delete({ + where: args.where, + }); + } + + @Query(() => [Hook]) + @UseGuards(AbilityGuard) + @CheckAbilities(ReadHookAbilityHandler) + async findManyHook( + @Args() args: FindManyHookArgs, + @UserAbility() ability: AppAbility, + ) { + const filterOptions = [accessibleBy(ability).WorkspaceMember]; + if (args.where) filterOptions.push(args.where); + return this.prismaService.client.hook.findMany({ + ...args, + where: { AND: filterOptions }, + }); + } +} diff --git a/server/src/database/migrations/20231019144904_complete_comment_thread_migration/migration.sql b/server/src/database/migrations/20231019144904_complete_comment_thread_migration/migration.sql new file mode 100644 index 000000000..183a15ae3 --- /dev/null +++ b/server/src/database/migrations/20231019144904_complete_comment_thread_migration/migration.sql @@ -0,0 +1,15 @@ +-- CreateTable +CREATE TABLE "hooks" ( + "id" TEXT NOT NULL, + "workspaceId" TEXT NOT NULL, + "targetUrl" TEXT NOT NULL, + "operation" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + + CONSTRAINT "hooks_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "hooks" ADD CONSTRAINT "hooks_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/server/src/database/schema.prisma b/server/src/database/schema.prisma index 3d6a60362..c19e0e6d8 100644 --- a/server/src/database/schema.prisma +++ b/server/src/database/schema.prisma @@ -179,6 +179,7 @@ model Workspace { views View[] viewSorts ViewSort[] apiKeys ApiKey[] + hooks Hook[] /// @TypeGraphQL.omit(input: true, output: true) deletedAt DateTime? @@ -907,3 +908,21 @@ model ApiKey { @@map("api_keys") } + +model Hook { + /// @Validator.IsString() + /// @Validator.IsOptional() + id String @id @default(uuid()) + /// @TypeGraphQL.omit(input: true, output: true) + workspace Workspace @relation(fields: [workspaceId], references: [id]) + /// @TypeGraphQL.omit(input: true, output: true) + workspaceId String + targetUrl String + operation String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + /// @TypeGraphQL.omit(input: true, output: true) + deletedAt DateTime? + + @@map("hooks") +} diff --git a/server/src/utils/prisma-select/model-select-map.ts b/server/src/utils/prisma-select/model-select-map.ts index f61cf2a91..11026d502 100644 --- a/server/src/utils/prisma-select/model-select-map.ts +++ b/server/src/utils/prisma-select/model-select-map.ts @@ -22,4 +22,5 @@ export type ModelSelectMap = { ViewSort: Prisma.ViewSortSelect; ViewField: Prisma.ViewFieldSelect; ApiKey: Prisma.ApiKeySelect; + Hook: Prisma.HookSelect; };