diff --git a/graphql/codegen/src/__tests__/codegen/__snapshots__/query-keys-factory.test.ts.snap b/graphql/codegen/src/__tests__/codegen/__snapshots__/query-keys-factory.test.ts.snap new file mode 100644 index 000000000..0959320eb --- /dev/null +++ b/graphql/codegen/src/__tests__/codegen/__snapshots__/query-keys-factory.test.ts.snap @@ -0,0 +1,1670 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`generateInvalidationFile generates invalidation helpers for a single table without relationships 1`] = ` +"/** + * Cache invalidation helpers + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ + +// ============================================================================ +// Type-safe cache invalidation utilities +// +// Features: +// - Simple invalidation helpers per entity +// - Cascade invalidation for parent-child relationships +// - Remove helpers for delete operations +// ============================================================================ + +import type { QueryClient } from '@tanstack/react-query'; +import { userKeys } from './query-keys'; + +// ============================================================================ +// Invalidation Helpers +// ============================================================================ + +/** + * Type-safe query invalidation helpers + * + * @example + * \`\`\`ts + * // Invalidate all user queries + * invalidate.user.all(queryClient); + * + * // Invalidate user lists + * invalidate.user.lists(queryClient); + * + * // Invalidate specific user + * invalidate.user.detail(queryClient, userId); + * \`\`\` + */ +export const invalidate = { + /** + * Invalidate user queries + */ + user: { + /** Invalidate all user queries */ + all: (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: userKeys.all }), + + /** Invalidate user list queries */ + lists: (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: userKeys.lists() }), + + /** Invalidate a specific user */ + detail: (queryClient: QueryClient, id: string | number) => + queryClient.invalidateQueries({ queryKey: userKeys.detail(id) }), + }, +} as const; + +// ============================================================================ +// Remove Helpers (for delete operations) +// ============================================================================ + +/** + * Remove queries from cache (for delete operations) + * + * Use these when an entity is deleted to remove it from cache + * instead of just invalidating (which would trigger a refetch). + */ +export const remove = { + /** Remove user from cache */ + user: (queryClient: QueryClient, id: string | number) => { + queryClient.removeQueries({ queryKey: userKeys.detail(id) }); + }, +} as const; +" +`; + +exports[`generateInvalidationFile generates invalidation helpers for multiple tables 1`] = ` +"/** + * Cache invalidation helpers + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ + +// ============================================================================ +// Type-safe cache invalidation utilities +// +// Features: +// - Simple invalidation helpers per entity +// - Cascade invalidation for parent-child relationships +// - Remove helpers for delete operations +// ============================================================================ + +import type { QueryClient } from '@tanstack/react-query'; +import { userKeys, postKeys } from './query-keys'; + +// ============================================================================ +// Invalidation Helpers +// ============================================================================ + +/** + * Type-safe query invalidation helpers + * + * @example + * \`\`\`ts + * // Invalidate all user queries + * invalidate.user.all(queryClient); + * + * // Invalidate user lists + * invalidate.user.lists(queryClient); + * + * // Invalidate specific user + * invalidate.user.detail(queryClient, userId); + * \`\`\` + */ +export const invalidate = { + /** + * Invalidate user queries + */ + user: { + /** Invalidate all user queries */ + all: (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: userKeys.all }), + + /** Invalidate user list queries */ + lists: (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: userKeys.lists() }), + + /** Invalidate a specific user */ + detail: (queryClient: QueryClient, id: string | number) => + queryClient.invalidateQueries({ queryKey: userKeys.detail(id) }), + }, + + /** + * Invalidate post queries + */ + post: { + /** Invalidate all post queries */ + all: (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: postKeys.all }), + + /** Invalidate post list queries */ + lists: (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: postKeys.lists() }), + + /** Invalidate a specific post */ + detail: (queryClient: QueryClient, id: string | number) => + queryClient.invalidateQueries({ queryKey: postKeys.detail(id) }), + }, +} as const; + +// ============================================================================ +// Remove Helpers (for delete operations) +// ============================================================================ + +/** + * Remove queries from cache (for delete operations) + * + * Use these when an entity is deleted to remove it from cache + * instead of just invalidating (which would trigger a refetch). + */ +export const remove = { + /** Remove user from cache */ + user: (queryClient: QueryClient, id: string | number) => { + queryClient.removeQueries({ queryKey: userKeys.detail(id) }); + }, + + /** Remove post from cache */ + post: (queryClient: QueryClient, id: string | number) => { + queryClient.removeQueries({ queryKey: postKeys.detail(id) }); + }, +} as const; +" +`; + +exports[`generateInvalidationFile generates invalidation helpers with cascade support for hierarchical relationships 1`] = ` +"/** + * Cache invalidation helpers + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ + +// ============================================================================ +// Type-safe cache invalidation utilities +// +// Features: +// - Simple invalidation helpers per entity +// - Cascade invalidation for parent-child relationships +// - Remove helpers for delete operations +// ============================================================================ + +import type { QueryClient } from '@tanstack/react-query'; +import { organizationKeys, databaseKeys, tableKeys, fieldKeys } from './query-keys'; +import type { DatabaseScope, TableScope, FieldScope } from './query-keys'; + +// ============================================================================ +// Invalidation Helpers +// ============================================================================ + +/** + * Type-safe query invalidation helpers + * + * @example + * \`\`\`ts + * // Invalidate all user queries + * invalidate.user.all(queryClient); + * + * // Invalidate user lists + * invalidate.user.lists(queryClient); + * + * // Invalidate specific user + * invalidate.user.detail(queryClient, userId); + * + * // Cascade invalidate (entity + all children) + * invalidate.database.withChildren(queryClient, databaseId); + * \`\`\` + */ +export const invalidate = { + /** + * Invalidate organization queries + */ + organization: { + /** Invalidate all organization queries */ + all: (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: organizationKeys.all }), + + /** Invalidate organization list queries */ + lists: (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: organizationKeys.lists() }), + + /** Invalidate a specific organization */ + detail: (queryClient: QueryClient, id: string | number) => + queryClient.invalidateQueries({ queryKey: organizationKeys.detail(id) }), + + /** + * Invalidate organization and all child entities + * Cascades to: database, table, field + */ + withChildren: (queryClient: QueryClient, id: string | number) => { + // Invalidate this organization + queryClient.invalidateQueries({ queryKey: organizationKeys.detail(id) }); + queryClient.invalidateQueries({ queryKey: organizationKeys.lists() }); + + // Cascade to child entities + queryClient.invalidateQueries({ queryKey: databaseKeys.byOrganization(id) }); + queryClient.invalidateQueries({ queryKey: tableKeys.byOrganization(id) }); + queryClient.invalidateQueries({ queryKey: fieldKeys.byOrganization(id) }); + }, + }, + + /** + * Invalidate database queries + */ + database: { + /** Invalidate all database queries */ + all: (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: databaseKeys.all }), + + /** Invalidate database list queries */ + lists: (queryClient: QueryClient, scope?: DatabaseScope) => + queryClient.invalidateQueries({ queryKey: databaseKeys.lists(scope) }), + + /** Invalidate a specific database */ + detail: (queryClient: QueryClient, id: string | number, scope?: DatabaseScope) => + queryClient.invalidateQueries({ queryKey: databaseKeys.detail(id, scope) }), + + /** + * Invalidate database and all child entities + * Cascades to: table, field + */ + withChildren: (queryClient: QueryClient, id: string | number) => { + // Invalidate this database + queryClient.invalidateQueries({ queryKey: databaseKeys.detail(id) }); + queryClient.invalidateQueries({ queryKey: databaseKeys.lists() }); + + // Cascade to child entities + queryClient.invalidateQueries({ queryKey: tableKeys.byDatabase(id) }); + queryClient.invalidateQueries({ queryKey: fieldKeys.byDatabase(id) }); + }, + }, + + /** + * Invalidate table queries + */ + table: { + /** Invalidate all table queries */ + all: (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: tableKeys.all }), + + /** Invalidate table list queries */ + lists: (queryClient: QueryClient, scope?: TableScope) => + queryClient.invalidateQueries({ queryKey: tableKeys.lists(scope) }), + + /** Invalidate a specific table */ + detail: (queryClient: QueryClient, id: string | number, scope?: TableScope) => + queryClient.invalidateQueries({ queryKey: tableKeys.detail(id, scope) }), + + /** + * Invalidate table and all child entities + * Cascades to: field + */ + withChildren: (queryClient: QueryClient, id: string | number) => { + // Invalidate this table + queryClient.invalidateQueries({ queryKey: tableKeys.detail(id) }); + queryClient.invalidateQueries({ queryKey: tableKeys.lists() }); + + // Cascade to child entities + queryClient.invalidateQueries({ queryKey: fieldKeys.byTable(id) }); + }, + }, + + /** + * Invalidate field queries + */ + field: { + /** Invalidate all field queries */ + all: (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: fieldKeys.all }), + + /** Invalidate field list queries */ + lists: (queryClient: QueryClient, scope?: FieldScope) => + queryClient.invalidateQueries({ queryKey: fieldKeys.lists(scope) }), + + /** Invalidate a specific field */ + detail: (queryClient: QueryClient, id: string | number, scope?: FieldScope) => + queryClient.invalidateQueries({ queryKey: fieldKeys.detail(id, scope) }), + }, +} as const; + +// ============================================================================ +// Remove Helpers (for delete operations) +// ============================================================================ + +/** + * Remove queries from cache (for delete operations) + * + * Use these when an entity is deleted to remove it from cache + * instead of just invalidating (which would trigger a refetch). + */ +export const remove = { + /** Remove organization from cache */ + organization: (queryClient: QueryClient, id: string | number) => { + queryClient.removeQueries({ queryKey: organizationKeys.detail(id) }); + }, + + /** Remove database from cache */ + database: (queryClient: QueryClient, id: string | number, scope?: DatabaseScope) => { + queryClient.removeQueries({ queryKey: databaseKeys.detail(id, scope) }); + }, + + /** Remove table from cache */ + table: (queryClient: QueryClient, id: string | number, scope?: TableScope) => { + queryClient.removeQueries({ queryKey: tableKeys.detail(id, scope) }); + }, + + /** Remove field from cache */ + field: (queryClient: QueryClient, id: string | number, scope?: FieldScope) => { + queryClient.removeQueries({ queryKey: fieldKeys.detail(id, scope) }); + }, +} as const; +" +`; + +exports[`generateInvalidationFile generates invalidation helpers with cascade support for parent-child relationship 1`] = ` +"/** + * Cache invalidation helpers + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ + +// ============================================================================ +// Type-safe cache invalidation utilities +// +// Features: +// - Simple invalidation helpers per entity +// - Cascade invalidation for parent-child relationships +// - Remove helpers for delete operations +// ============================================================================ + +import type { QueryClient } from '@tanstack/react-query'; +import { userKeys, postKeys } from './query-keys'; +import type { PostScope } from './query-keys'; + +// ============================================================================ +// Invalidation Helpers +// ============================================================================ + +/** + * Type-safe query invalidation helpers + * + * @example + * \`\`\`ts + * // Invalidate all user queries + * invalidate.user.all(queryClient); + * + * // Invalidate user lists + * invalidate.user.lists(queryClient); + * + * // Invalidate specific user + * invalidate.user.detail(queryClient, userId); + * + * // Cascade invalidate (entity + all children) + * invalidate.database.withChildren(queryClient, databaseId); + * \`\`\` + */ +export const invalidate = { + /** + * Invalidate user queries + */ + user: { + /** Invalidate all user queries */ + all: (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: userKeys.all }), + + /** Invalidate user list queries */ + lists: (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: userKeys.lists() }), + + /** Invalidate a specific user */ + detail: (queryClient: QueryClient, id: string | number) => + queryClient.invalidateQueries({ queryKey: userKeys.detail(id) }), + + /** + * Invalidate user and all child entities + * Cascades to: post + */ + withChildren: (queryClient: QueryClient, id: string | number) => { + // Invalidate this user + queryClient.invalidateQueries({ queryKey: userKeys.detail(id) }); + queryClient.invalidateQueries({ queryKey: userKeys.lists() }); + + // Cascade to child entities + queryClient.invalidateQueries({ queryKey: postKeys.byUser(id) }); + }, + }, + + /** + * Invalidate post queries + */ + post: { + /** Invalidate all post queries */ + all: (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: postKeys.all }), + + /** Invalidate post list queries */ + lists: (queryClient: QueryClient, scope?: PostScope) => + queryClient.invalidateQueries({ queryKey: postKeys.lists(scope) }), + + /** Invalidate a specific post */ + detail: (queryClient: QueryClient, id: string | number, scope?: PostScope) => + queryClient.invalidateQueries({ queryKey: postKeys.detail(id, scope) }), + }, +} as const; + +// ============================================================================ +// Remove Helpers (for delete operations) +// ============================================================================ + +/** + * Remove queries from cache (for delete operations) + * + * Use these when an entity is deleted to remove it from cache + * instead of just invalidating (which would trigger a refetch). + */ +export const remove = { + /** Remove user from cache */ + user: (queryClient: QueryClient, id: string | number) => { + queryClient.removeQueries({ queryKey: userKeys.detail(id) }); + }, + + /** Remove post from cache */ + post: (queryClient: QueryClient, id: string | number, scope?: PostScope) => { + queryClient.removeQueries({ queryKey: postKeys.detail(id, scope) }); + }, +} as const; +" +`; + +exports[`generateInvalidationFile generates invalidation helpers without cascade when disabled 1`] = ` +"/** + * Cache invalidation helpers + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ + +// ============================================================================ +// Type-safe cache invalidation utilities +// +// Features: +// - Simple invalidation helpers per entity +// - Cascade invalidation for parent-child relationships +// - Remove helpers for delete operations +// ============================================================================ + +import type { QueryClient } from '@tanstack/react-query'; +import { userKeys, postKeys } from './query-keys'; +import type { PostScope } from './query-keys'; + +// ============================================================================ +// Invalidation Helpers +// ============================================================================ + +/** + * Type-safe query invalidation helpers + * + * @example + * \`\`\`ts + * // Invalidate all user queries + * invalidate.user.all(queryClient); + * + * // Invalidate user lists + * invalidate.user.lists(queryClient); + * + * // Invalidate specific user + * invalidate.user.detail(queryClient, userId); + * \`\`\` + */ +export const invalidate = { + /** + * Invalidate user queries + */ + user: { + /** Invalidate all user queries */ + all: (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: userKeys.all }), + + /** Invalidate user list queries */ + lists: (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: userKeys.lists() }), + + /** Invalidate a specific user */ + detail: (queryClient: QueryClient, id: string | number) => + queryClient.invalidateQueries({ queryKey: userKeys.detail(id) }), + + /** + * Invalidate user and all child entities + * Cascades to: post + */ + withChildren: (queryClient: QueryClient, id: string | number) => { + // Invalidate this user + queryClient.invalidateQueries({ queryKey: userKeys.detail(id) }); + queryClient.invalidateQueries({ queryKey: userKeys.lists() }); + + // Cascade to child entities + queryClient.invalidateQueries({ queryKey: postKeys.byUser(id) }); + }, + }, + + /** + * Invalidate post queries + */ + post: { + /** Invalidate all post queries */ + all: (queryClient: QueryClient) => + queryClient.invalidateQueries({ queryKey: postKeys.all }), + + /** Invalidate post list queries */ + lists: (queryClient: QueryClient, scope?: PostScope) => + queryClient.invalidateQueries({ queryKey: postKeys.lists(scope) }), + + /** Invalidate a specific post */ + detail: (queryClient: QueryClient, id: string | number, scope?: PostScope) => + queryClient.invalidateQueries({ queryKey: postKeys.detail(id, scope) }), + }, +} as const; + +// ============================================================================ +// Remove Helpers (for delete operations) +// ============================================================================ + +/** + * Remove queries from cache (for delete operations) + * + * Use these when an entity is deleted to remove it from cache + * instead of just invalidating (which would trigger a refetch). + */ +export const remove = { + /** Remove user from cache */ + user: (queryClient: QueryClient, id: string | number) => { + queryClient.removeQueries({ queryKey: userKeys.detail(id) }); + }, + + /** Remove post from cache */ + post: (queryClient: QueryClient, id: string | number, scope?: PostScope) => { + queryClient.removeQueries({ queryKey: postKeys.detail(id, scope) }); + }, +} as const; +" +`; + +exports[`generateMutationKeysFile generates mutation keys for a single table without relationships 1`] = ` +"/** + * Centralized mutation key factory + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ + +// ============================================================================ +// Mutation keys for tracking in-flight mutations +// +// Benefits: +// - Track mutation state with useIsMutating +// - Implement optimistic updates with proper rollback +// - Deduplicate identical mutations +// - Coordinate related mutations +// ============================================================================ + +// ============================================================================ +// Entity Mutation Keys +// ============================================================================ + +export const userMutationKeys = { + /** All user mutation keys */ + all: ['mutation', 'user'] as const, + + /** Create user mutation key */ + create: () => ['mutation', 'user', 'create'] as const, + + /** Update user mutation key */ + update: (id: string | number) => + ['mutation', 'user', 'update', id] as const, + + /** Delete user mutation key */ + delete: (id: string | number) => + ['mutation', 'user', 'delete', id] as const, +} as const; + +// ============================================================================ +// Unified Mutation Key Store +// ============================================================================ + +/** + * Unified mutation key store + * + * Use this for tracking in-flight mutations with useIsMutating. + * + * @example + * \`\`\`ts + * import { useIsMutating } from '@tanstack/react-query'; + * import { mutationKeys } from './generated'; + * + * // Check if any user mutations are in progress + * const isMutatingUser = useIsMutating({ mutationKey: mutationKeys.user.all }); + * + * // Check if a specific user is being updated + * const isUpdating = useIsMutating({ mutationKey: mutationKeys.user.update(userId) }); + * \`\`\` + */ +export const mutationKeys = { + user: userMutationKeys, +} as const; +" +`; + +exports[`generateMutationKeysFile generates mutation keys for hierarchical relationships 1`] = ` +"/** + * Centralized mutation key factory + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ + +// ============================================================================ +// Mutation keys for tracking in-flight mutations +// +// Benefits: +// - Track mutation state with useIsMutating +// - Implement optimistic updates with proper rollback +// - Deduplicate identical mutations +// - Coordinate related mutations +// ============================================================================ + +// ============================================================================ +// Entity Mutation Keys +// ============================================================================ + +export const organizationMutationKeys = { + /** All organization mutation keys */ + all: ['mutation', 'organization'] as const, + + /** Create organization mutation key */ + create: () => ['mutation', 'organization', 'create'] as const, + + /** Update organization mutation key */ + update: (id: string | number) => + ['mutation', 'organization', 'update', id] as const, + + /** Delete organization mutation key */ + delete: (id: string | number) => + ['mutation', 'organization', 'delete', id] as const, +} as const; + +export const databaseMutationKeys = { + /** All database mutation keys */ + all: ['mutation', 'database'] as const, + + /** Create database mutation key */ + create: (organizationId?: string) => + organizationId + ? ['mutation', 'database', 'create', { organizationId }] as const + : ['mutation', 'database', 'create'] as const, + + /** Update database mutation key */ + update: (id: string | number) => + ['mutation', 'database', 'update', id] as const, + + /** Delete database mutation key */ + delete: (id: string | number) => + ['mutation', 'database', 'delete', id] as const, +} as const; + +export const tableMutationKeys = { + /** All table mutation keys */ + all: ['mutation', 'table'] as const, + + /** Create table mutation key */ + create: (databaseId?: string) => + databaseId + ? ['mutation', 'table', 'create', { databaseId }] as const + : ['mutation', 'table', 'create'] as const, + + /** Update table mutation key */ + update: (id: string | number) => + ['mutation', 'table', 'update', id] as const, + + /** Delete table mutation key */ + delete: (id: string | number) => + ['mutation', 'table', 'delete', id] as const, +} as const; + +export const fieldMutationKeys = { + /** All field mutation keys */ + all: ['mutation', 'field'] as const, + + /** Create field mutation key */ + create: (tableId?: string) => + tableId + ? ['mutation', 'field', 'create', { tableId }] as const + : ['mutation', 'field', 'create'] as const, + + /** Update field mutation key */ + update: (id: string | number) => + ['mutation', 'field', 'update', id] as const, + + /** Delete field mutation key */ + delete: (id: string | number) => + ['mutation', 'field', 'delete', id] as const, +} as const; + +// ============================================================================ +// Unified Mutation Key Store +// ============================================================================ + +/** + * Unified mutation key store + * + * Use this for tracking in-flight mutations with useIsMutating. + * + * @example + * \`\`\`ts + * import { useIsMutating } from '@tanstack/react-query'; + * import { mutationKeys } from './generated'; + * + * // Check if any user mutations are in progress + * const isMutatingUser = useIsMutating({ mutationKey: mutationKeys.user.all }); + * + * // Check if a specific user is being updated + * const isUpdating = useIsMutating({ mutationKey: mutationKeys.user.update(userId) }); + * \`\`\` + */ +export const mutationKeys = { + organization: organizationMutationKeys, + database: databaseMutationKeys, + table: tableMutationKeys, + field: fieldMutationKeys, +} as const; +" +`; + +exports[`generateMutationKeysFile generates mutation keys for multiple tables 1`] = ` +"/** + * Centralized mutation key factory + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ + +// ============================================================================ +// Mutation keys for tracking in-flight mutations +// +// Benefits: +// - Track mutation state with useIsMutating +// - Implement optimistic updates with proper rollback +// - Deduplicate identical mutations +// - Coordinate related mutations +// ============================================================================ + +// ============================================================================ +// Entity Mutation Keys +// ============================================================================ + +export const userMutationKeys = { + /** All user mutation keys */ + all: ['mutation', 'user'] as const, + + /** Create user mutation key */ + create: () => ['mutation', 'user', 'create'] as const, + + /** Update user mutation key */ + update: (id: string | number) => + ['mutation', 'user', 'update', id] as const, + + /** Delete user mutation key */ + delete: (id: string | number) => + ['mutation', 'user', 'delete', id] as const, +} as const; + +export const postMutationKeys = { + /** All post mutation keys */ + all: ['mutation', 'post'] as const, + + /** Create post mutation key */ + create: () => ['mutation', 'post', 'create'] as const, + + /** Update post mutation key */ + update: (id: string | number) => + ['mutation', 'post', 'update', id] as const, + + /** Delete post mutation key */ + delete: (id: string | number) => + ['mutation', 'post', 'delete', id] as const, +} as const; + +// ============================================================================ +// Unified Mutation Key Store +// ============================================================================ + +/** + * Unified mutation key store + * + * Use this for tracking in-flight mutations with useIsMutating. + * + * @example + * \`\`\`ts + * import { useIsMutating } from '@tanstack/react-query'; + * import { mutationKeys } from './generated'; + * + * // Check if any user mutations are in progress + * const isMutatingUser = useIsMutating({ mutationKey: mutationKeys.user.all }); + * + * // Check if a specific user is being updated + * const isUpdating = useIsMutating({ mutationKey: mutationKeys.user.update(userId) }); + * \`\`\` + */ +export const mutationKeys = { + user: userMutationKeys, + post: postMutationKeys, +} as const; +" +`; + +exports[`generateMutationKeysFile generates mutation keys with custom mutations 1`] = ` +"/** + * Centralized mutation key factory + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ + +// ============================================================================ +// Mutation keys for tracking in-flight mutations +// +// Benefits: +// - Track mutation state with useIsMutating +// - Implement optimistic updates with proper rollback +// - Deduplicate identical mutations +// - Coordinate related mutations +// ============================================================================ + +// ============================================================================ +// Entity Mutation Keys +// ============================================================================ + +export const userMutationKeys = { + /** All user mutation keys */ + all: ['mutation', 'user'] as const, + + /** Create user mutation key */ + create: () => ['mutation', 'user', 'create'] as const, + + /** Update user mutation key */ + update: (id: string | number) => + ['mutation', 'user', 'update', id] as const, + + /** Delete user mutation key */ + delete: (id: string | number) => + ['mutation', 'user', 'delete', id] as const, +} as const; + +// ============================================================================ +// Custom Mutation Keys +// ============================================================================ + +export const customMutationKeys = { + /** Mutation key for login */ + login: (identifier?: string) => + identifier + ? ['mutation', 'login', identifier] as const + : ['mutation', 'login'] as const, + + /** Mutation key for logout */ + logout: () => ['mutation', 'logout'] as const, +} as const; + +// ============================================================================ +// Unified Mutation Key Store +// ============================================================================ + +/** + * Unified mutation key store + * + * Use this for tracking in-flight mutations with useIsMutating. + * + * @example + * \`\`\`ts + * import { useIsMutating } from '@tanstack/react-query'; + * import { mutationKeys } from './generated'; + * + * // Check if any user mutations are in progress + * const isMutatingUser = useIsMutating({ mutationKey: mutationKeys.user.all }); + * + * // Check if a specific user is being updated + * const isUpdating = useIsMutating({ mutationKey: mutationKeys.user.update(userId) }); + * \`\`\` + */ +export const mutationKeys = { + user: userMutationKeys, + custom: customMutationKeys, +} as const; +" +`; + +exports[`generateMutationKeysFile generates mutation keys with parent-child relationship 1`] = ` +"/** + * Centralized mutation key factory + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ + +// ============================================================================ +// Mutation keys for tracking in-flight mutations +// +// Benefits: +// - Track mutation state with useIsMutating +// - Implement optimistic updates with proper rollback +// - Deduplicate identical mutations +// - Coordinate related mutations +// ============================================================================ + +// ============================================================================ +// Entity Mutation Keys +// ============================================================================ + +export const userMutationKeys = { + /** All user mutation keys */ + all: ['mutation', 'user'] as const, + + /** Create user mutation key */ + create: () => ['mutation', 'user', 'create'] as const, + + /** Update user mutation key */ + update: (id: string | number) => + ['mutation', 'user', 'update', id] as const, + + /** Delete user mutation key */ + delete: (id: string | number) => + ['mutation', 'user', 'delete', id] as const, +} as const; + +export const postMutationKeys = { + /** All post mutation keys */ + all: ['mutation', 'post'] as const, + + /** Create post mutation key */ + create: (authorId?: string) => + authorId + ? ['mutation', 'post', 'create', { authorId }] as const + : ['mutation', 'post', 'create'] as const, + + /** Update post mutation key */ + update: (id: string | number) => + ['mutation', 'post', 'update', id] as const, + + /** Delete post mutation key */ + delete: (id: string | number) => + ['mutation', 'post', 'delete', id] as const, +} as const; + +// ============================================================================ +// Unified Mutation Key Store +// ============================================================================ + +/** + * Unified mutation key store + * + * Use this for tracking in-flight mutations with useIsMutating. + * + * @example + * \`\`\`ts + * import { useIsMutating } from '@tanstack/react-query'; + * import { mutationKeys } from './generated'; + * + * // Check if any user mutations are in progress + * const isMutatingUser = useIsMutating({ mutationKey: mutationKeys.user.all }); + * + * // Check if a specific user is being updated + * const isUpdating = useIsMutating({ mutationKey: mutationKeys.user.update(userId) }); + * \`\`\` + */ +export const mutationKeys = { + user: userMutationKeys, + post: postMutationKeys, +} as const; +" +`; + +exports[`generateQueryKeysFile generates query keys for a single table without relationships 1`] = ` +"/** + * Centralized query key factory + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ + +// ============================================================================ +// This file provides a centralized, type-safe query key factory following +// the lukemorales query-key-factory pattern for React Query. +// +// Benefits: +// - Single source of truth for all query keys +// - Type-safe key access with autocomplete +// - Hierarchical invalidation (invalidate all 'user.*' queries) +// - Scoped keys for parent-child relationships +// ============================================================================ + +// ============================================================================ +// Entity Query Keys +// ============================================================================ + +export const userKeys = { + /** All user queries */ + all: ['user'] as const, + + /** List query keys */ + lists: () => [...userKeys.all, 'list'] as const, + + /** List query key with variables */ + list: (variables?: object) => + [...userKeys.lists(), variables] as const, + + /** Detail query keys */ + details: () => [...userKeys.all, 'detail'] as const, + + /** Detail query key for specific item */ + detail: (id: string | number) => + [...userKeys.details(), id] as const, +} as const; + +// ============================================================================ +// Unified Query Key Store +// ============================================================================ + +/** + * Unified query key store + * + * Use this for type-safe query key access across your application. + * + * @example + * \`\`\`ts + * // Invalidate all user queries + * queryClient.invalidateQueries({ queryKey: queryKeys.user.all }); + * + * // Invalidate user list queries + * queryClient.invalidateQueries({ queryKey: queryKeys.user.lists() }); + * + * // Invalidate specific user + * queryClient.invalidateQueries({ queryKey: queryKeys.user.detail(userId) }); + * \`\`\` + */ +export const queryKeys = { + user: userKeys, +} as const; + +/** Type representing all available query key scopes */ +export type QueryKeyScope = keyof typeof queryKeys; +" +`; + +exports[`generateQueryKeysFile generates query keys for multiple tables without relationships 1`] = ` +"/** + * Centralized query key factory + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ + +// ============================================================================ +// This file provides a centralized, type-safe query key factory following +// the lukemorales query-key-factory pattern for React Query. +// +// Benefits: +// - Single source of truth for all query keys +// - Type-safe key access with autocomplete +// - Hierarchical invalidation (invalidate all 'user.*' queries) +// - Scoped keys for parent-child relationships +// ============================================================================ + +// ============================================================================ +// Entity Query Keys +// ============================================================================ + +export const userKeys = { + /** All user queries */ + all: ['user'] as const, + + /** List query keys */ + lists: () => [...userKeys.all, 'list'] as const, + + /** List query key with variables */ + list: (variables?: object) => + [...userKeys.lists(), variables] as const, + + /** Detail query keys */ + details: () => [...userKeys.all, 'detail'] as const, + + /** Detail query key for specific item */ + detail: (id: string | number) => + [...userKeys.details(), id] as const, +} as const; + +export const postKeys = { + /** All post queries */ + all: ['post'] as const, + + /** List query keys */ + lists: () => [...postKeys.all, 'list'] as const, + + /** List query key with variables */ + list: (variables?: object) => + [...postKeys.lists(), variables] as const, + + /** Detail query keys */ + details: () => [...postKeys.all, 'detail'] as const, + + /** Detail query key for specific item */ + detail: (id: string | number) => + [...postKeys.details(), id] as const, +} as const; + +// ============================================================================ +// Unified Query Key Store +// ============================================================================ + +/** + * Unified query key store + * + * Use this for type-safe query key access across your application. + * + * @example + * \`\`\`ts + * // Invalidate all user queries + * queryClient.invalidateQueries({ queryKey: queryKeys.user.all }); + * + * // Invalidate user list queries + * queryClient.invalidateQueries({ queryKey: queryKeys.user.lists() }); + * + * // Invalidate specific user + * queryClient.invalidateQueries({ queryKey: queryKeys.user.detail(userId) }); + * \`\`\` + */ +export const queryKeys = { + user: userKeys, + post: postKeys, +} as const; + +/** Type representing all available query key scopes */ +export type QueryKeyScope = keyof typeof queryKeys; +" +`; + +exports[`generateQueryKeysFile generates query keys with custom queries 1`] = ` +"/** + * Centralized query key factory + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ + +// ============================================================================ +// This file provides a centralized, type-safe query key factory following +// the lukemorales query-key-factory pattern for React Query. +// +// Benefits: +// - Single source of truth for all query keys +// - Type-safe key access with autocomplete +// - Hierarchical invalidation (invalidate all 'user.*' queries) +// - Scoped keys for parent-child relationships +// ============================================================================ + +// ============================================================================ +// Entity Query Keys +// ============================================================================ + +export const userKeys = { + /** All user queries */ + all: ['user'] as const, + + /** List query keys */ + lists: () => [...userKeys.all, 'list'] as const, + + /** List query key with variables */ + list: (variables?: object) => + [...userKeys.lists(), variables] as const, + + /** Detail query keys */ + details: () => [...userKeys.all, 'detail'] as const, + + /** Detail query key for specific item */ + detail: (id: string | number) => + [...userKeys.details(), id] as const, +} as const; + +// ============================================================================ +// Custom Query Keys +// ============================================================================ + +export const customQueryKeys = { + /** Query key for currentUser */ + currentUser: () => ['currentUser'] as const, + + /** Query key for searchUsers */ + searchUsers: (variables: object) => + ['searchUsers', variables] as const, +} as const; + +// ============================================================================ +// Unified Query Key Store +// ============================================================================ + +/** + * Unified query key store + * + * Use this for type-safe query key access across your application. + * + * @example + * \`\`\`ts + * // Invalidate all user queries + * queryClient.invalidateQueries({ queryKey: queryKeys.user.all }); + * + * // Invalidate user list queries + * queryClient.invalidateQueries({ queryKey: queryKeys.user.lists() }); + * + * // Invalidate specific user + * queryClient.invalidateQueries({ queryKey: queryKeys.user.detail(userId) }); + * \`\`\` + */ +export const queryKeys = { + user: userKeys, + custom: customQueryKeys, +} as const; + +/** Type representing all available query key scopes */ +export type QueryKeyScope = keyof typeof queryKeys; +" +`; + +exports[`generateQueryKeysFile generates query keys with hierarchical relationships (org -> db -> table -> field) 1`] = ` +"/** + * Centralized query key factory + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ + +// ============================================================================ +// This file provides a centralized, type-safe query key factory following +// the lukemorales query-key-factory pattern for React Query. +// +// Benefits: +// - Single source of truth for all query keys +// - Type-safe key access with autocomplete +// - Hierarchical invalidation (invalidate all 'user.*' queries) +// - Scoped keys for parent-child relationships +// ============================================================================ + +// ============================================================================ +// Scope Types +// ============================================================================ + +export type DatabaseScope = { organizationId?: string; organizationId?: string }; +export type TableScope = { databaseId?: string; organizationId?: string }; +export type FieldScope = { tableId?: string; databaseId?: string; organizationId?: string }; + +// ============================================================================ +// Entity Query Keys +// ============================================================================ + +export const organizationKeys = { + /** All organization queries */ + all: ['organization'] as const, + + /** List query keys */ + lists: () => [...organizationKeys.all, 'list'] as const, + + /** List query key with variables */ + list: (variables?: object) => + [...organizationKeys.lists(), variables] as const, + + /** Detail query keys */ + details: () => [...organizationKeys.all, 'detail'] as const, + + /** Detail query key for specific item */ + detail: (id: string | number) => + [...organizationKeys.details(), id] as const, +} as const; + +export const databaseKeys = { + /** All database queries */ + all: ['database'] as const, + + /** Database queries scoped to a specific organization */ + byOrganization: (organizationId: string) => + ['database', { organizationId }] as const, + + /** Database queries scoped to a specific organization */ + byOrganization: (organizationId: string) => + ['database', { organizationId }] as const, + + /** Get scope-aware base key */ + scoped: (scope?: DatabaseScope) => { + if (scope?.organizationId) return databaseKeys.byOrganization(scope.organizationId); + if (scope?.organizationId) return databaseKeys.byOrganization(scope.organizationId); + return databaseKeys.all; + }, + + /** List query keys (optionally scoped) */ + lists: (scope?: DatabaseScope) => + [...databaseKeys.scoped(scope), 'list'] as const, + + /** List query key with variables */ + list: (variables?: object, scope?: DatabaseScope) => + [...databaseKeys.lists(scope), variables] as const, + + /** Detail query keys (optionally scoped) */ + details: (scope?: DatabaseScope) => + [...databaseKeys.scoped(scope), 'detail'] as const, + + /** Detail query key for specific item */ + detail: (id: string | number, scope?: DatabaseScope) => + [...databaseKeys.details(scope), id] as const, +} as const; + +export const tableKeys = { + /** All table queries */ + all: ['table'] as const, + + /** Table queries scoped to a specific database */ + byDatabase: (databaseId: string) => + ['table', { databaseId }] as const, + + /** Table queries scoped to a specific organization */ + byOrganization: (organizationId: string) => + ['table', { organizationId }] as const, + + /** Get scope-aware base key */ + scoped: (scope?: TableScope) => { + if (scope?.databaseId) return tableKeys.byDatabase(scope.databaseId); + if (scope?.organizationId) return tableKeys.byOrganization(scope.organizationId); + return tableKeys.all; + }, + + /** List query keys (optionally scoped) */ + lists: (scope?: TableScope) => + [...tableKeys.scoped(scope), 'list'] as const, + + /** List query key with variables */ + list: (variables?: object, scope?: TableScope) => + [...tableKeys.lists(scope), variables] as const, + + /** Detail query keys (optionally scoped) */ + details: (scope?: TableScope) => + [...tableKeys.scoped(scope), 'detail'] as const, + + /** Detail query key for specific item */ + detail: (id: string | number, scope?: TableScope) => + [...tableKeys.details(scope), id] as const, +} as const; + +export const fieldKeys = { + /** All field queries */ + all: ['field'] as const, + + /** Field queries scoped to a specific table */ + byTable: (tableId: string) => + ['field', { tableId }] as const, + + /** Field queries scoped to a specific database */ + byDatabase: (databaseId: string) => + ['field', { databaseId }] as const, + + /** Field queries scoped to a specific organization */ + byOrganization: (organizationId: string) => + ['field', { organizationId }] as const, + + /** Get scope-aware base key */ + scoped: (scope?: FieldScope) => { + if (scope?.tableId) return fieldKeys.byTable(scope.tableId); + if (scope?.databaseId) return fieldKeys.byDatabase(scope.databaseId); + if (scope?.organizationId) return fieldKeys.byOrganization(scope.organizationId); + return fieldKeys.all; + }, + + /** List query keys (optionally scoped) */ + lists: (scope?: FieldScope) => + [...fieldKeys.scoped(scope), 'list'] as const, + + /** List query key with variables */ + list: (variables?: object, scope?: FieldScope) => + [...fieldKeys.lists(scope), variables] as const, + + /** Detail query keys (optionally scoped) */ + details: (scope?: FieldScope) => + [...fieldKeys.scoped(scope), 'detail'] as const, + + /** Detail query key for specific item */ + detail: (id: string | number, scope?: FieldScope) => + [...fieldKeys.details(scope), id] as const, +} as const; + +// ============================================================================ +// Unified Query Key Store +// ============================================================================ + +/** + * Unified query key store + * + * Use this for type-safe query key access across your application. + * + * @example + * \`\`\`ts + * // Invalidate all user queries + * queryClient.invalidateQueries({ queryKey: queryKeys.user.all }); + * + * // Invalidate user list queries + * queryClient.invalidateQueries({ queryKey: queryKeys.user.lists() }); + * + * // Invalidate specific user + * queryClient.invalidateQueries({ queryKey: queryKeys.user.detail(userId) }); + * \`\`\` + */ +export const queryKeys = { + organization: organizationKeys, + database: databaseKeys, + table: tableKeys, + field: fieldKeys, +} as const; + +/** Type representing all available query key scopes */ +export type QueryKeyScope = keyof typeof queryKeys; +" +`; + +exports[`generateQueryKeysFile generates query keys with simple parent-child relationship 1`] = ` +"/** + * Centralized query key factory + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ + +// ============================================================================ +// This file provides a centralized, type-safe query key factory following +// the lukemorales query-key-factory pattern for React Query. +// +// Benefits: +// - Single source of truth for all query keys +// - Type-safe key access with autocomplete +// - Hierarchical invalidation (invalidate all 'user.*' queries) +// - Scoped keys for parent-child relationships +// ============================================================================ + +// ============================================================================ +// Scope Types +// ============================================================================ + +export type PostScope = { authorId?: string; authorId?: string }; + +// ============================================================================ +// Entity Query Keys +// ============================================================================ + +export const userKeys = { + /** All user queries */ + all: ['user'] as const, + + /** List query keys */ + lists: () => [...userKeys.all, 'list'] as const, + + /** List query key with variables */ + list: (variables?: object) => + [...userKeys.lists(), variables] as const, + + /** Detail query keys */ + details: () => [...userKeys.all, 'detail'] as const, + + /** Detail query key for specific item */ + detail: (id: string | number) => + [...userKeys.details(), id] as const, +} as const; + +export const postKeys = { + /** All post queries */ + all: ['post'] as const, + + /** Post queries scoped to a specific user */ + byUser: (authorId: string) => + ['post', { authorId }] as const, + + /** Post queries scoped to a specific user */ + byUser: (authorId: string) => + ['post', { authorId }] as const, + + /** Get scope-aware base key */ + scoped: (scope?: PostScope) => { + if (scope?.authorId) return postKeys.byUser(scope.authorId); + if (scope?.userId) return postKeys.byUser(scope.userId); + return postKeys.all; + }, + + /** List query keys (optionally scoped) */ + lists: (scope?: PostScope) => + [...postKeys.scoped(scope), 'list'] as const, + + /** List query key with variables */ + list: (variables?: object, scope?: PostScope) => + [...postKeys.lists(scope), variables] as const, + + /** Detail query keys (optionally scoped) */ + details: (scope?: PostScope) => + [...postKeys.scoped(scope), 'detail'] as const, + + /** Detail query key for specific item */ + detail: (id: string | number, scope?: PostScope) => + [...postKeys.details(scope), id] as const, +} as const; + +// ============================================================================ +// Unified Query Key Store +// ============================================================================ + +/** + * Unified query key store + * + * Use this for type-safe query key access across your application. + * + * @example + * \`\`\`ts + * // Invalidate all user queries + * queryClient.invalidateQueries({ queryKey: queryKeys.user.all }); + * + * // Invalidate user list queries + * queryClient.invalidateQueries({ queryKey: queryKeys.user.lists() }); + * + * // Invalidate specific user + * queryClient.invalidateQueries({ queryKey: queryKeys.user.detail(userId) }); + * \`\`\` + */ +export const queryKeys = { + user: userKeys, + post: postKeys, +} as const; + +/** Type representing all available query key scopes */ +export type QueryKeyScope = keyof typeof queryKeys; +" +`; + +exports[`generateQueryKeysFile generates query keys without scoped keys when disabled 1`] = ` +"/** + * Centralized query key factory + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ + +// ============================================================================ +// This file provides a centralized, type-safe query key factory following +// the lukemorales query-key-factory pattern for React Query. +// +// Benefits: +// - Single source of truth for all query keys +// - Type-safe key access with autocomplete +// - Hierarchical invalidation (invalidate all 'user.*' queries) +// - Scoped keys for parent-child relationships +// ============================================================================ + +// ============================================================================ +// Entity Query Keys +// ============================================================================ + +export const userKeys = { + /** All user queries */ + all: ['user'] as const, + + /** List query keys */ + lists: () => [...userKeys.all, 'list'] as const, + + /** List query key with variables */ + list: (variables?: object) => + [...userKeys.lists(), variables] as const, + + /** Detail query keys */ + details: () => [...userKeys.all, 'detail'] as const, + + /** Detail query key for specific item */ + detail: (id: string | number) => + [...userKeys.details(), id] as const, +} as const; + +export const postKeys = { + /** All post queries */ + all: ['post'] as const, + + /** List query keys */ + lists: () => [...postKeys.all, 'list'] as const, + + /** List query key with variables */ + list: (variables?: object) => + [...postKeys.lists(), variables] as const, + + /** Detail query keys */ + details: () => [...postKeys.all, 'detail'] as const, + + /** Detail query key for specific item */ + detail: (id: string | number) => + [...postKeys.details(), id] as const, +} as const; + +// ============================================================================ +// Unified Query Key Store +// ============================================================================ + +/** + * Unified query key store + * + * Use this for type-safe query key access across your application. + * + * @example + * \`\`\`ts + * // Invalidate all user queries + * queryClient.invalidateQueries({ queryKey: queryKeys.user.all }); + * + * // Invalidate user list queries + * queryClient.invalidateQueries({ queryKey: queryKeys.user.lists() }); + * + * // Invalidate specific user + * queryClient.invalidateQueries({ queryKey: queryKeys.user.detail(userId) }); + * \`\`\` + */ +export const queryKeys = { + user: userKeys, + post: postKeys, +} as const; + +/** Type representing all available query key scopes */ +export type QueryKeyScope = keyof typeof queryKeys; +" +`; diff --git a/graphql/codegen/src/__tests__/codegen/query-keys-factory.test.ts b/graphql/codegen/src/__tests__/codegen/query-keys-factory.test.ts new file mode 100644 index 000000000..177cc2dc1 --- /dev/null +++ b/graphql/codegen/src/__tests__/codegen/query-keys-factory.test.ts @@ -0,0 +1,374 @@ +/** + * Tests for query key factory pattern generators + * + * Uses snapshot testing to validate generated TypeScript output for: + * - Query keys factory (query-keys.ts) + * - Mutation keys factory (mutation-keys.ts) + * - Cache invalidation helpers (invalidation.ts) + */ +import { generateQueryKeysFile } from '../../cli/codegen/query-keys'; +import { generateMutationKeysFile } from '../../cli/codegen/mutation-keys'; +import { generateInvalidationFile } from '../../cli/codegen/invalidation'; +import type { CleanTable, CleanFieldType, CleanRelations, CleanOperation, CleanTypeRef } from '../../types/schema'; +import type { ResolvedQueryKeyConfig, EntityRelationship } from '../../types/config'; + +const fieldTypes = { + uuid: { gqlType: 'UUID', isArray: false } as CleanFieldType, + string: { gqlType: 'String', isArray: false } as CleanFieldType, + int: { gqlType: 'Int', isArray: false } as CleanFieldType, + datetime: { gqlType: 'Datetime', isArray: false } as CleanFieldType, +}; + +const emptyRelations: CleanRelations = { + belongsTo: [], + hasOne: [], + hasMany: [], + manyToMany: [], +}; + +function createTable(partial: Partial & { name: string }): CleanTable { + return { + name: partial.name, + fields: partial.fields ?? [], + relations: partial.relations ?? emptyRelations, + query: partial.query, + inflection: partial.inflection, + constraints: partial.constraints, + }; +} + +function createTypeRef(kind: CleanTypeRef['kind'], name: string | null, ofType?: CleanTypeRef): CleanTypeRef { + return { kind, name, ofType }; +} + +const simpleUserTable = createTable({ + name: 'User', + fields: [ + { name: 'id', type: fieldTypes.uuid }, + { name: 'email', type: fieldTypes.string }, + { name: 'name', type: fieldTypes.string }, + { name: 'createdAt', type: fieldTypes.datetime }, + ], + query: { + all: 'users', + one: 'user', + create: 'createUser', + update: 'updateUser', + delete: 'deleteUser', + }, +}); + +const postTable = createTable({ + name: 'Post', + fields: [ + { name: 'id', type: fieldTypes.uuid }, + { name: 'title', type: fieldTypes.string }, + { name: 'content', type: fieldTypes.string }, + { name: 'authorId', type: fieldTypes.uuid }, + { name: 'createdAt', type: fieldTypes.datetime }, + ], + query: { + all: 'posts', + one: 'post', + create: 'createPost', + update: 'updatePost', + delete: 'deletePost', + }, +}); + +const organizationTable = createTable({ + name: 'Organization', + fields: [ + { name: 'id', type: fieldTypes.uuid }, + { name: 'name', type: fieldTypes.string }, + { name: 'slug', type: fieldTypes.string }, + ], + query: { + all: 'organizations', + one: 'organization', + create: 'createOrganization', + update: 'updateOrganization', + delete: 'deleteOrganization', + }, +}); + +const databaseTable = createTable({ + name: 'Database', + fields: [ + { name: 'id', type: fieldTypes.uuid }, + { name: 'name', type: fieldTypes.string }, + { name: 'organizationId', type: fieldTypes.uuid }, + ], + query: { + all: 'databases', + one: 'database', + create: 'createDatabase', + update: 'updateDatabase', + delete: 'deleteDatabase', + }, +}); + +const tableEntityTable = createTable({ + name: 'Table', + fields: [ + { name: 'id', type: fieldTypes.uuid }, + { name: 'name', type: fieldTypes.string }, + { name: 'databaseId', type: fieldTypes.uuid }, + ], + query: { + all: 'tables', + one: 'table', + create: 'createTable', + update: 'updateTable', + delete: 'deleteTable', + }, +}); + +const fieldTable = createTable({ + name: 'Field', + fields: [ + { name: 'id', type: fieldTypes.uuid }, + { name: 'name', type: fieldTypes.string }, + { name: 'tableId', type: fieldTypes.uuid }, + { name: 'type', type: fieldTypes.string }, + ], + query: { + all: 'fields', + one: 'field', + create: 'createField', + update: 'updateField', + delete: 'deleteField', + }, +}); + +const simpleConfig: ResolvedQueryKeyConfig = { + style: 'hierarchical', + relationships: {}, + generateScopedKeys: true, + generateCascadeHelpers: true, + generateMutationKeys: true, +}; + +const simpleRelationships: Record = { + post: { parent: 'User', foreignKey: 'authorId' }, +}; + +const hierarchicalRelationships: Record = { + database: { parent: 'Organization', foreignKey: 'organizationId' }, + table: { parent: 'Database', foreignKey: 'databaseId', ancestors: ['organization'] }, + field: { parent: 'Table', foreignKey: 'tableId', ancestors: ['database', 'organization'] }, +}; + +const sampleCustomQueries: CleanOperation[] = [ + { + name: 'currentUser', + kind: 'query', + args: [], + returnType: createTypeRef('OBJECT', 'User'), + description: 'Get the current authenticated user', + }, + { + name: 'searchUsers', + kind: 'query', + args: [ + { name: 'query', type: createTypeRef('NON_NULL', null, createTypeRef('SCALAR', 'String')) }, + { name: 'limit', type: createTypeRef('SCALAR', 'Int') }, + ], + returnType: createTypeRef('LIST', null, createTypeRef('OBJECT', 'User')), + description: 'Search users by name or email', + }, +]; + +const sampleCustomMutations: CleanOperation[] = [ + { + name: 'login', + kind: 'mutation', + args: [ + { name: 'email', type: createTypeRef('NON_NULL', null, createTypeRef('SCALAR', 'String')) }, + { name: 'password', type: createTypeRef('NON_NULL', null, createTypeRef('SCALAR', 'String')) }, + ], + returnType: createTypeRef('OBJECT', 'LoginPayload'), + description: 'Authenticate user', + }, + { + name: 'logout', + kind: 'mutation', + args: [], + returnType: createTypeRef('OBJECT', 'LogoutPayload'), + description: 'Log out current user', + }, +]; + +describe('generateQueryKeysFile', () => { + it('generates query keys for a single table without relationships', () => { + const result = generateQueryKeysFile({ + tables: [simpleUserTable], + customQueries: [], + config: simpleConfig, + }); + expect(result.fileName).toBe('query-keys.ts'); + expect(result.content).toMatchSnapshot(); + }); + + it('generates query keys for multiple tables without relationships', () => { + const result = generateQueryKeysFile({ + tables: [simpleUserTable, postTable], + customQueries: [], + config: simpleConfig, + }); + expect(result.content).toMatchSnapshot(); + }); + + it('generates query keys with simple parent-child relationship', () => { + const result = generateQueryKeysFile({ + tables: [simpleUserTable, postTable], + customQueries: [], + config: { + ...simpleConfig, + relationships: simpleRelationships, + }, + }); + expect(result.content).toMatchSnapshot(); + }); + + it('generates query keys with hierarchical relationships (org -> db -> table -> field)', () => { + const result = generateQueryKeysFile({ + tables: [organizationTable, databaseTable, tableEntityTable, fieldTable], + customQueries: [], + config: { + ...simpleConfig, + relationships: hierarchicalRelationships, + }, + }); + expect(result.content).toMatchSnapshot(); + }); + + it('generates query keys with custom queries', () => { + const result = generateQueryKeysFile({ + tables: [simpleUserTable], + customQueries: sampleCustomQueries, + config: simpleConfig, + }); + expect(result.content).toMatchSnapshot(); + }); + + it('generates query keys without scoped keys when disabled', () => { + const result = generateQueryKeysFile({ + tables: [simpleUserTable, postTable], + customQueries: [], + config: { + ...simpleConfig, + relationships: simpleRelationships, + generateScopedKeys: false, + }, + }); + expect(result.content).toMatchSnapshot(); + }); +}); + +describe('generateMutationKeysFile', () => { + it('generates mutation keys for a single table without relationships', () => { + const result = generateMutationKeysFile({ + tables: [simpleUserTable], + customMutations: [], + config: simpleConfig, + }); + expect(result.fileName).toBe('mutation-keys.ts'); + expect(result.content).toMatchSnapshot(); + }); + + it('generates mutation keys for multiple tables', () => { + const result = generateMutationKeysFile({ + tables: [simpleUserTable, postTable], + customMutations: [], + config: simpleConfig, + }); + expect(result.content).toMatchSnapshot(); + }); + + it('generates mutation keys with parent-child relationship', () => { + const result = generateMutationKeysFile({ + tables: [simpleUserTable, postTable], + customMutations: [], + config: { + ...simpleConfig, + relationships: simpleRelationships, + }, + }); + expect(result.content).toMatchSnapshot(); + }); + + it('generates mutation keys with custom mutations', () => { + const result = generateMutationKeysFile({ + tables: [simpleUserTable], + customMutations: sampleCustomMutations, + config: simpleConfig, + }); + expect(result.content).toMatchSnapshot(); + }); + + it('generates mutation keys for hierarchical relationships', () => { + const result = generateMutationKeysFile({ + tables: [organizationTable, databaseTable, tableEntityTable, fieldTable], + customMutations: [], + config: { + ...simpleConfig, + relationships: hierarchicalRelationships, + }, + }); + expect(result.content).toMatchSnapshot(); + }); +}); + +describe('generateInvalidationFile', () => { + it('generates invalidation helpers for a single table without relationships', () => { + const result = generateInvalidationFile({ + tables: [simpleUserTable], + config: simpleConfig, + }); + expect(result.fileName).toBe('invalidation.ts'); + expect(result.content).toMatchSnapshot(); + }); + + it('generates invalidation helpers for multiple tables', () => { + const result = generateInvalidationFile({ + tables: [simpleUserTable, postTable], + config: simpleConfig, + }); + expect(result.content).toMatchSnapshot(); + }); + + it('generates invalidation helpers with cascade support for parent-child relationship', () => { + const result = generateInvalidationFile({ + tables: [simpleUserTable, postTable], + config: { + ...simpleConfig, + relationships: simpleRelationships, + }, + }); + expect(result.content).toMatchSnapshot(); + }); + + it('generates invalidation helpers with cascade support for hierarchical relationships', () => { + const result = generateInvalidationFile({ + tables: [organizationTable, databaseTable, tableEntityTable, fieldTable], + config: { + ...simpleConfig, + relationships: hierarchicalRelationships, + }, + }); + expect(result.content).toMatchSnapshot(); + }); + + it('generates invalidation helpers without cascade when disabled', () => { + const result = generateInvalidationFile({ + tables: [simpleUserTable, postTable], + config: { + ...simpleConfig, + relationships: simpleRelationships, + generateCascadeHelpers: false, + }, + }); + expect(result.content).toMatchSnapshot(); + }); +});