From 0d01dfcb4c08606b3db98b2fb4cb5eedef80e87d Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Wed, 16 Jul 2025 09:54:28 -0600 Subject: [PATCH] array parter --- apps/server/src/api/v2/users/GET.ts | 2 + .../queries/users/users-to-organizations.ts | 18 +++++-- .../server-shared/src/type-utilities/index.ts | 1 + .../query-array-preprocessor.ts | 54 +++++++++++++++++++ .../server-shared/src/user/request.types.ts | 8 ++- 5 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 packages/server-shared/src/type-utilities/query-array-preprocessor.ts diff --git a/apps/server/src/api/v2/users/GET.ts b/apps/server/src/api/v2/users/GET.ts index df6a993dd..dcf957c9b 100644 --- a/apps/server/src/api/v2/users/GET.ts +++ b/apps/server/src/api/v2/users/GET.ts @@ -14,6 +14,8 @@ const app = new Hono().get( const { id: userId } = c.get('busterUser'); const options = c.req.valid('query'); + console.log(options); + try { const result: GetUserToOrganizationResponse = await getUserToOrganization({ userId, diff --git a/packages/database/src/queries/users/users-to-organizations.ts b/packages/database/src/queries/users/users-to-organizations.ts index e5792dc08..ff1a6f951 100644 --- a/packages/database/src/queries/users/users-to-organizations.ts +++ b/packages/database/src/queries/users/users-to-organizations.ts @@ -1,7 +1,13 @@ -import { type InferSelectModel, and, asc, count, eq, isNull, like } from 'drizzle-orm'; +import { type InferSelectModel, and, asc, count, eq, inArray, isNull, like } from 'drizzle-orm'; +import { createSelectSchema } from 'drizzle-zod'; import { z } from 'zod'; import { db } from '../../connection'; -import { users, usersToOrganizations } from '../../schema'; +import { + userOrganizationRoleEnum, + userOrganizationStatusEnum, + users, + usersToOrganizations, +} from '../../schema'; import { getUserOrganizationId } from '../organizations/organizations'; import { type PaginatedResponse, createPaginatedResponse } from '../shared-types'; import { withPagination } from '../shared-types/with-pagination'; @@ -17,6 +23,8 @@ const GetUserToOrganizationInputSchema = z.object({ page_size: z.number().optional().default(250), user_name: z.string().optional(), email: z.string().optional(), + role: z.array(z.enum(userOrganizationRoleEnum.enumValues)).optional(), + status: z.array(z.enum(userOrganizationStatusEnum.enumValues)).optional(), }); type GetUserToOrganizationInput = z.infer; @@ -33,7 +41,7 @@ export const getUserToOrganization = async ( params: GetUserToOrganizationInput ): Promise> => { // Validate and destructure input - const { userId, page, page_size, user_name, email } = + const { userId, page, page_size, user_name, email, role, status } = GetUserToOrganizationInputSchema.parse(params); // Get the user's organization ID @@ -49,7 +57,9 @@ export const getUserToOrganization = async ( eq(usersToOrganizations.organizationId, organizationId), isNull(usersToOrganizations.deletedAt), user_name ? like(users.name, `%${user_name}%`) : undefined, - email ? like(users.email, `%${email}%`) : undefined + email ? like(users.email, `%${email}%`) : undefined, + role ? inArray(usersToOrganizations.role, role) : undefined, + status ? inArray(usersToOrganizations.status, status) : undefined ); const getData = withPagination( diff --git a/packages/server-shared/src/type-utilities/index.ts b/packages/server-shared/src/type-utilities/index.ts index b1c8f1e81..2e941bf2e 100644 --- a/packages/server-shared/src/type-utilities/index.ts +++ b/packages/server-shared/src/type-utilities/index.ts @@ -1,2 +1,3 @@ export * from './isEqual'; export * from './pagination'; +export * from './query-array-preprocessor'; diff --git a/packages/server-shared/src/type-utilities/query-array-preprocessor.ts b/packages/server-shared/src/type-utilities/query-array-preprocessor.ts new file mode 100644 index 000000000..39300f668 --- /dev/null +++ b/packages/server-shared/src/type-utilities/query-array-preprocessor.ts @@ -0,0 +1,54 @@ +import { z } from 'zod'; + +/** + * Creates a preprocessor that converts query parameter strings into arrays. + * Handles various input formats: + * - Single value: "admin" → ["admin"] + * - Comma-separated: "admin,member" → ["admin", "member"] + * - Already an array: ["admin", "member"] → ["admin", "member"] + * - No value: undefined → undefined + */ +export const createQueryArrayPreprocessor = (schema: z.ZodArray>) => { + return z.preprocess((val) => { + // Handle no value + if (!val) return undefined; + + // Already an array, pass through + if (Array.isArray(val)) return val; + + // Handle string input (single or comma-separated) + if (typeof val === 'string') { + return val + .split(',') + .map((item) => item.trim()) + .filter(Boolean); + } + + // Single value case (wrap in array) + return [val]; + }, schema); +}; + +/** + * Type-safe helper for creating optional query array preprocessors + */ +export const createOptionalQueryArrayPreprocessor = (itemSchema: z.ZodType) => { + return z.preprocess((val) => { + // Handle no value + if (!val) return undefined; + + // Already an array, pass through + if (Array.isArray(val)) return val; + + // Handle string input (single or comma-separated) + if (typeof val === 'string') { + return val + .split(',') + .map((item) => item.trim()) + .filter(Boolean); + } + + // Single value case (wrap in array) + return [val]; + }, z.array(itemSchema).optional()); +}; diff --git a/packages/server-shared/src/user/request.types.ts b/packages/server-shared/src/user/request.types.ts index 0709af5e4..7c53b4579 100644 --- a/packages/server-shared/src/user/request.types.ts +++ b/packages/server-shared/src/user/request.types.ts @@ -1,6 +1,8 @@ import { z } from 'zod'; +import { OrganizationStatusSchema } from '../organization'; import { OrganizationRoleSchema } from '../organization/roles.types'; import { ShareAssetTypeSchema } from '../share'; +import { createOptionalQueryArrayPreprocessor } from '../type-utilities'; export const UserRequestSchema = z.object({ user_id: z.string(), @@ -50,9 +52,13 @@ export type GetUserListRequest = z.infer; export const GetUserToOrganizationRequestSchema = z.object({ page: z.coerce.number().min(1).optional().default(1), - page_size: z.coerce.number().min(1).max(5000).optional().default(250), + page_size: z.coerce.number().min(1).max(5000).optional().default(25), user_name: z.string().optional(), email: z.string().optional(), + //We need this because the frontend sends the roles as a comma-separated string in the query params + role: createOptionalQueryArrayPreprocessor(OrganizationRoleSchema), + //We need this because the frontend sends the status as a comma-separated string in the query params + status: createOptionalQueryArrayPreprocessor(OrganizationStatusSchema), }); export type GetUserToOrganizationRequest = z.infer;