Conversation
There was a problem hiding this comment.
Pull request overview
Adds context-entity slug fields (starting with organizationSlug) to membership records so the frontend can build context routes from membership data without fetching full entity details, while keeping the setup extensible for forks that add more context entity types.
Changes:
- Extend shared config with
entitySlugColumnKeysand persist slug columns onmemberships/inactive_memberships. - Populate slug columns when creating memberships/invitations and update mocks accordingly.
- Update frontend route resolution and regenerate OpenAPI + client types/docs to include
organizationSlugon membership schemas.
Reviewed changes
Copilot reviewed 20 out of 22 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| shared/src/builder/types.ts | Adds entitySlugColumnKeys to the required shared config shape. |
| shared/default-config.ts | Defines default entitySlugColumnKeys mapping (organization → organizationSlug). |
| frontend/src/routes-resolver.ts | Uses membership-provided slug (when available) to build context entity routes. |
| frontend/src/api.gen/zod.gen.ts | Regenerated Zod schemas now include organizationSlug on membership types. |
| frontend/src/api.gen/types.gen.ts | Regenerated TypeScript client types now include organizationSlug on membership types. |
| frontend/public/static/openapi.json | Regenerated OpenAPI spec includes organizationSlug on membership schemas/examples. |
| frontend/public/static/docs.gen/schemas.gen.json | Regenerated schema docs include organizationSlug. |
| frontend/public/static/docs.gen/details.gen/users.gen.json | Regenerated endpoint docs/examples include membership organizationSlug. |
| frontend/public/static/docs.gen/details.gen/organizations.gen.json | Regenerated endpoint docs/examples include membership organizationSlug. |
| frontend/public/static/docs.gen/details.gen/memberships.gen.json | Regenerated endpoint docs/examples include membership organizationSlug. |
| frontend/public/static/docs.gen/details.gen/me.gen.json | Regenerated endpoint docs/examples include membership organizationSlug. |
| backend/src/modules/memberships/memberships-handlers.ts | Includes slug fields when inserting inactive memberships. |
| backend/src/modules/memberships/helpers/index.ts | Adds slug extraction helper and includes slug columns in inserted membership rows. |
| backend/src/db/schema/memberships.ts | Adds dynamic slug columns and indexes to the memberships table schema. |
| backend/src/db/schema/inactive-memberships.ts | Adds dynamic slug columns and indexes to the inactive memberships table schema. |
| backend/scripts/drizzle-studio/start.ts | Fixes .env lookup path for Drizzle Studio startup script. |
| backend/mocks/utils/mock-context-entity-id-columns.ts | Adds mock slug-column generation/types for context entities. |
| backend/mocks/utils/index.ts | Re-exports new mock slug helpers/types. |
| backend/mocks/mock-membership.ts | Extends membership mocks to include generated slug columns. |
| backend/drizzle/20260216112941_careful_sir_ram/snapshot.json | Drizzle snapshot reflecting new membership slug columns and indexes. |
| backend/drizzle/20260216112941_careful_sir_ram/migration.sql | Migration adding organization_slug columns + indexes. |
| /** Maps context entity types to their slug column names - must match contextEntityTypes */ | ||
| entitySlugColumnKeys: { | ||
| organization: 'organizationSlug', | ||
| } as const, |
There was a problem hiding this comment.
entitySlugColumnKeys is documented as “must match contextEntityTypes”, but unlike entityIdColumnKeys there’s no compile-time validation enforcing this. Consider adding a config-validation check (similar to the existing ExpectedIdColumnKeys check) so forks can’t add a context entity type without providing its slug column key mapping.
frontend/src/routes-resolver.ts
Outdated
| const getEntitySlugFromMembership = ( | ||
| entityType: string, | ||
| membership: MembershipBase | null | undefined, | ||
| ): string | null => { | ||
| if (!membership) return null; | ||
| const slugColumnKey = (appConfig.entitySlugColumnKeys as Record<string, string | undefined>)[entityType]; | ||
| if (!slugColumnKey) return null; | ||
| return (membership as unknown as Record<string, string | null>)[slugColumnKey] ?? null; |
There was a problem hiding this comment.
getEntitySlugFromMembership relies on string + unknown casts to index into appConfig.entitySlugColumnKeys and membership. This loses type safety and can mask misconfigurations. Consider tightening entityType to the known context entity types and indexing membership via a typed key (e.g. keyof MembershipBase) to avoid the as unknown as Record<...> pattern.
| const getEntitySlugFromMembership = ( | |
| entityType: string, | |
| membership: MembershipBase | null | undefined, | |
| ): string | null => { | |
| if (!membership) return null; | |
| const slugColumnKey = (appConfig.entitySlugColumnKeys as Record<string, string | undefined>)[entityType]; | |
| if (!slugColumnKey) return null; | |
| return (membership as unknown as Record<string, string | null>)[slugColumnKey] ?? null; | |
| type MembershipSlugKey = Extract<keyof MembershipBase, string>; | |
| const getEntitySlugFromMembership = ( | |
| entityType: ContextEntityBase['entityType'], | |
| membership: MembershipBase | null | undefined, | |
| ): string | null => { | |
| if (!membership) return null; | |
| const slugColumnKey = | |
| appConfig.entitySlugColumnKeys[ | |
| entityType as keyof typeof appConfig.entitySlugColumnKeys | |
| ]; | |
| if (!slugColumnKey) return null; | |
| const membershipKey = slugColumnKey as MembershipSlugKey; | |
| if (!(membershipKey in membership)) { | |
| return null; | |
| } | |
| const value = membership[membershipKey]; | |
| if (typeof value === 'string') { | |
| return value; | |
| } | |
| return value === null ? null : null; |
| acc[entityFieldSlugName] = (entity as unknown as Record<string, string>).slug ?? null; | ||
| } else if (entityFieldSlugName in entity) { | ||
| // For parent entities, the slug may be stored in the entity | ||
| acc[entityFieldSlugName] = entity[entityFieldSlugName as keyof typeof entity] as string | null; |
There was a problem hiding this comment.
getBaseMembershipEntitySlugs reads slug via (entity as unknown as Record<string, string>).slug, which bypasses the fact that not all entity tables necessarily have a slug column. Consider using a safer runtime check (e.g. 'slug' in entity and typeof entity.slug === 'string') and/or adjusting the entity typing so the slug access doesn’t require a broad cast.
| acc[entityFieldSlugName] = (entity as unknown as Record<string, string>).slug ?? null; | |
| } else if (entityFieldSlugName in entity) { | |
| // For parent entities, the slug may be stored in the entity | |
| acc[entityFieldSlugName] = entity[entityFieldSlugName as keyof typeof entity] as string | null; | |
| const maybeEntityWithSlug = entity as unknown as Record<string, unknown>; | |
| const slugValue = 'slug' in maybeEntityWithSlug ? maybeEntityWithSlug.slug : null; | |
| acc[entityFieldSlugName] = typeof slugValue === 'string' || slugValue === null ? slugValue : null; | |
| } else if (entityFieldSlugName in entity) { | |
| // For parent entities, the slug may be stored in the entity | |
| const maybeEntityWithSlugField = entity as unknown as Record<string, unknown>; | |
| const value = maybeEntityWithSlugField[entityFieldSlugName]; | |
| acc[entityFieldSlugName] = typeof value === 'string' || value === null ? value : null; |
| .map((t) => | ||
| index(`memberships_${appConfig.entitySlugColumnKeys[t]}_idx`).on( | ||
| table[appConfig.entitySlugColumnKeys[t] as keyof typeof table], | ||
| ), | ||
| ), |
There was a problem hiding this comment.
The dynamic index names use the camelCase slug key (e.g. memberships_organizationSlug_idx), while the rest of the indexes in this file follow snake_case (e.g. memberships_user_id_idx). To keep index naming consistent (and avoid churn if casing changes), consider normalizing the generated index name to snake_case while still indexing the correct column.
| .map((t) => | |
| index(`memberships_${appConfig.entitySlugColumnKeys[t]}_idx`).on( | |
| table[appConfig.entitySlugColumnKeys[t] as keyof typeof table], | |
| ), | |
| ), | |
| .map((t) => { | |
| const slugColumnKey = appConfig.entitySlugColumnKeys[t] as string; | |
| const slugIndexKey = slugColumnKey.replace(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`); | |
| return index(`memberships_${slugIndexKey}_idx`).on( | |
| table[slugColumnKey as keyof typeof table], | |
| ); | |
| }), |
| // Indexes for slug columns | ||
| ...appConfig.contextEntityTypes | ||
| .filter((t) => appConfig.entitySlugColumnKeys[t]) | ||
| .map((t) => | ||
| index(`inactive_memberships_${appConfig.entitySlugColumnKeys[t]}_idx`).on( | ||
| table[appConfig.entitySlugColumnKeys[t] as keyof typeof table], | ||
| ), | ||
| ), |
There was a problem hiding this comment.
The dynamic index names use the camelCase slug key (e.g. inactive_memberships_organizationSlug_idx), but other indexes here use snake_case (e.g. inactive_memberships_user_id_idx). Consider normalizing the generated index name to snake_case for consistency and to keep migrations stable.
No description provided.