diff --git a/backend/src/graphql/model/ProjectBranding.ts b/backend/src/graphql/model/ProjectBranding.ts index 02984c62a..591199a63 100644 --- a/backend/src/graphql/model/ProjectBranding.ts +++ b/backend/src/graphql/model/ProjectBranding.ts @@ -1,10 +1,11 @@ -import { projectBrandingsTable } from 'database' +import { ProjectBrandingSelect } from 'database' +import { ProjectBranding as ProjectBrandingZodSchema } from 'shared' import { Field, Int, ObjectType } from 'type-graphql' @ObjectType() export class ProjectBranding { // TODO: replace with valibot schema - constructor(projectBranding: typeof projectBrandingsTable.$inferSelect) { + constructor(projectBranding: ProjectBrandingZodSchema | ProjectBrandingSelect) { Object.assign(this, projectBranding) } diff --git a/backend/src/graphql/resolver/ProjectBrandingResolver.ts b/backend/src/graphql/resolver/ProjectBrandingResolver.ts index 8e4a76c80..edfe3eef2 100644 --- a/backend/src/graphql/resolver/ProjectBrandingResolver.ts +++ b/backend/src/graphql/resolver/ProjectBrandingResolver.ts @@ -2,12 +2,13 @@ import { ProjectBrandingInput } from '@input/ProjectBrandingInput' import { ProjectBranding } from '@model/ProjectBranding' import { Space } from '@model/Space' import { SpaceList } from '@model/SpaceList' -import { - dbDeleteProjectBranding, - dbFindAllProjectBrandings, - dbFindProjectBrandingById, - dbGetProjectLogoURL, - projectBrandingsTable +import { + dbDeleteProjectBranding, + dbFindAllProjectBrandings, + dbFindProjectBrandingById, + dbGetProjectLogoURL, + dbUpsertProjectBranding, + projectBrandingsTable, } from 'database' import { getLogger } from 'log4js' import { Arg, Authorized, ID, Int, Mutation, Query, Resolver } from 'type-graphql' @@ -49,14 +50,7 @@ export class ProjectBrandingResolver { async upsertProjectBranding( @Arg('input') input: ProjectBrandingInput, ): Promise { - const projectBranding = input.id - ? await DbProjectBranding.findOneOrFail({ where: { id: input.id } }) - : new DbProjectBranding() - - Object.assign(projectBranding, input) - await projectBranding.save() - - return new ProjectBranding(projectBranding) + return new ProjectBranding(await dbUpsertProjectBranding(input)) } @Mutation(() => Boolean) diff --git a/bun.lock b/bun.lock index 9a4a4c2bb..984f39857 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "gradido", diff --git a/database/migration/migrations/0097-project_branding_add_unique_index_alias.ts b/database/migration/migrations/0097-project_branding_add_unique_index_alias.ts new file mode 100644 index 000000000..038f97f8a --- /dev/null +++ b/database/migration/migrations/0097-project_branding_add_unique_index_alias.ts @@ -0,0 +1,9 @@ +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn( + `ALTER TABLE project_brandings ADD UNIQUE INDEX project_brandings_alias_unique (alias);`, + ) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn(`ALTER TABLE project_brandings DROP INDEX project_brandings_alias_unique;`) +} diff --git a/database/src/entity/ProjectBranding.ts b/database/src/entity/ProjectBranding.ts deleted file mode 100644 index f836f5824..000000000 --- a/database/src/entity/ProjectBranding.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm' - -@Entity('project_brandings') -export class ProjectBranding extends BaseEntity { - @PrimaryGeneratedColumn('increment', { unsigned: true }) - id: number - - @Column({ name: 'name', type: 'varchar', length: 255 }) - name: string - - @Column({ name: 'alias', type: 'varchar', length: 32 }) - alias: string - - @Column({ name: 'description', type: 'text', nullable: true, default: null }) - description: string | null - - @Column({ name: 'space_id', type: 'int', unsigned: true, nullable: true, default: null }) - spaceId: number | null - - @Column({ name: 'space_url', type: 'varchar', length: 255, nullable: true, default: null }) - spaceUrl: string | null - - @Column({ name: 'new_user_to_space', type: 'tinyint', width: 1, default: 0 }) - newUserToSpace: boolean - - @Column({ name: 'logo_url', type: 'varchar', length: 255, nullable: true, default: null }) - logoUrl: string | null -} diff --git a/database/src/entity/index.ts b/database/src/entity/index.ts index d2534af86..c8d8782aa 100644 --- a/database/src/entity/index.ts +++ b/database/src/entity/index.ts @@ -9,7 +9,6 @@ import { FederatedCommunity } from './FederatedCommunity' import { LoginElopageBuys } from './LoginElopageBuys' import { Migration } from './Migration' import { PendingTransaction } from './PendingTransaction' -import { ProjectBranding } from './ProjectBranding' import { Transaction } from './Transaction' import { TransactionLink } from './TransactionLink' import { User } from './User' @@ -27,7 +26,6 @@ export { FederatedCommunity, LoginElopageBuys, Migration, - ProjectBranding, PendingTransaction, Transaction, TransactionLink, @@ -47,7 +45,6 @@ export const entities = [ FederatedCommunity, LoginElopageBuys, Migration, - ProjectBranding, PendingTransaction, Transaction, TransactionLink, diff --git a/database/src/queries/projectBranding.ts b/database/src/queries/projectBranding.ts index bee025efc..3583c7e1e 100644 --- a/database/src/queries/projectBranding.ts +++ b/database/src/queries/projectBranding.ts @@ -1,6 +1,52 @@ -import { eq } from 'drizzle-orm' +import { eq, sql } from 'drizzle-orm' +import { ProjectBranding } from 'shared/src/schema/projectBranding.schema' import { drizzleDb } from '../AppDatabase' -import { projectBrandingsTable } from '../schemas/drizzle.schema' +import { + ProjectBrandingInsert, + ProjectBrandingSelect, + projectBrandingsTable, +} from '../schemas/drizzle.schema' + +/** + * Needed because of TypeScript 4, in TypeScript 5 we can use valibot and auto deduct a valibot schema from drizzle db schema + * Converts a ProjectBranding object to a ProjectBrandingInsert object to be used in database operations. + * @param projectBranding - The ProjectBranding object to convert. + * @returns The converted ProjectBrandingInsert object. + */ +function toDbInsert(projectBranding: ProjectBranding): ProjectBrandingInsert { + return { + // Omit ID when inserting (autoincrement) or set it if it exists + id: projectBranding.id ?? undefined, + name: projectBranding.name, + alias: projectBranding.alias, + // Set null in DB if undefined/null + description: projectBranding.description ?? null, + spaceId: projectBranding.spaceId ?? null, + spaceUrl: projectBranding.spaceUrl ?? null, + // Convert boolean to tinyint (1/0) + newUserToSpace: projectBranding.newUserToSpace ? 1 : 0, + logoUrl: projectBranding.logoUrl ?? null, + } +} + +export async function dbUpsertProjectBranding( + projectBranding: ProjectBranding, +): Promise { + if (projectBranding.id) { + await drizzleDb() + .update(projectBrandingsTable) + .set(toDbInsert(projectBranding)) + .where(eq(projectBrandingsTable.id, projectBranding.id)) + + return projectBranding + } else { + const drizzleProjectBranding = toDbInsert(projectBranding) + const result = await drizzleDb().insert(projectBrandingsTable).values(drizzleProjectBranding) + + projectBranding.id = result[0].insertId + return projectBranding + } +} export async function dbFindProjectSpaceUrl(alias: string): Promise { const result = await drizzleDb() @@ -11,7 +57,7 @@ export async function dbFindProjectSpaceUrl(alias: string): Promise return firstEntry.logoUrl } -export async function dbFindAllProjectBrandings(): Promise { - const result = await drizzleDb() - .select() - .from(projectBrandingsTable) +export async function dbFindAllProjectBrandings(): Promise { + const result = await drizzleDb().select().from(projectBrandingsTable) return result } -export async function dbFindProjectBrandingById(id: number): Promise { +export async function dbFindProjectBrandingById( + id: number, +): Promise { const result = await drizzleDb() .select() .from(projectBrandingsTable) @@ -47,4 +93,4 @@ export async function dbFindProjectBrandingById(id: number): Promise { await drizzleDb().delete(projectBrandingsTable).where(eq(projectBrandingsTable.id, id)) -} \ No newline at end of file +} diff --git a/database/src/schemas/drizzle.schema.ts b/database/src/schemas/drizzle.schema.ts index 95435a9f3..734eda8f1 100644 --- a/database/src/schemas/drizzle.schema.ts +++ b/database/src/schemas/drizzle.schema.ts @@ -1,5 +1,14 @@ import { sql } from 'drizzle-orm' -import { int, mysqlTable, text, timestamp, tinyint, varchar } from 'drizzle-orm/mysql-core' +import { + int, + mysqlTable, + text, + timestamp, + tinyint, + uniqueIndex, + varchar, +} from 'drizzle-orm/mysql-core' +import { z } from 'zod' export const openaiThreadsTable = mysqlTable('openai_threads', { id: varchar({ length: 128 }).notNull(), @@ -8,13 +17,20 @@ export const openaiThreadsTable = mysqlTable('openai_threads', { userId: int('user_id').notNull(), }) -export const projectBrandingsTable = mysqlTable("project_brandings", { - id: int().autoincrement().notNull(), - name: varchar({ length: 255 }).notNull(), - alias: varchar({ length: 32 }).notNull(), - description: text().default(sql`NULL`), - spaceId: int("space_id").default(sql`NULL`), - spaceUrl: varchar("space_url", { length: 255 }).default(sql`NULL`), - newUserToSpace: tinyint("new_user_to_space").default(0).notNull(), - logoUrl: varchar("logo_url", { length: 255 }).default(sql`NULL`), -}) \ No newline at end of file +export const projectBrandingsTable = mysqlTable( + 'project_brandings', + { + id: int().autoincrement().notNull(), + name: varchar({ length: 255 }).notNull(), + alias: varchar({ length: 32 }).notNull(), + description: text().default(sql`NULL`), + spaceId: int('space_id').default(sql`NULL`), + spaceUrl: varchar('space_url', { length: 255 }).default(sql`NULL`), + newUserToSpace: tinyint('new_user_to_space').default(0).notNull(), + logoUrl: varchar('logo_url', { length: 255 }).default(sql`NULL`), + }, + (table) => [uniqueIndex('project_brandings_alias_unique').on(table.alias)], +) + +export type ProjectBrandingSelect = typeof projectBrandingsTable.$inferSelect +export type ProjectBrandingInsert = typeof projectBrandingsTable.$inferInsert diff --git a/shared/src/schema/index.ts b/shared/src/schema/index.ts index 8d1f75b22..bf94f5e43 100644 --- a/shared/src/schema/index.ts +++ b/shared/src/schema/index.ts @@ -1,3 +1,4 @@ export * from './base.schema' export * from './community.schema' +export * from './projectBranding.schema' export * from './user.schema' diff --git a/shared/src/schema/projectBranding.schema.ts b/shared/src/schema/projectBranding.schema.ts new file mode 100644 index 000000000..8879ce327 --- /dev/null +++ b/shared/src/schema/projectBranding.schema.ts @@ -0,0 +1,15 @@ +import { z } from 'zod' + +// will be auto-generated in future directly from Drizzle table schema, this need TypeScript 5 +export const projectBrandingSchema = z.object({ + id: z.number().optional().nullable(), + name: z.string(), + alias: z.string().max(32), + description: z.string().optional().nullable(), + spaceId: z.number().optional().nullable(), + spaceUrl: z.string().url().optional().nullable(), + newUserToSpace: z.boolean(), + logoUrl: z.string().url().optional().nullable(), +}) + +export type ProjectBranding = z.infer