feat!: environment admin URLs use slug; validate and immutabilize slug
UUID-based admin paths were the only remaining UUID-in-URL pattern in
the API. Migrates /api/v1/admin/environments/{id} to /{envSlug} so
slugs are the single environment identifier in every URL. UUIDs stay
internal to the database.
- Controller: @PathVariable UUID id → @PathVariable String envSlug on
get/update/delete and the two nested endpoints (default-container-
config, jar-retention). Handlers resolve slug → Environment via
EnvironmentService.getBySlug, then delegate to existing UUID-based
service methods.
- Service: create() now validates slug against ^[a-z0-9][a-z0-9-]{0,63}$
and returns 400 on invalid slugs. Rationale documented in the class:
slugs are immutable after creation because they appear in URLs,
Docker network names, container names, and ClickHouse partition keys.
- UpdateEnvironmentRequest has no slug field and Jackson's default
ignore-unknown behavior drops any slug supplied in a PUT body;
regression test (updateEnvironment_withSlugInBody_ignoresSlug)
documents this invariant.
- SPA: mutation args change from { id } to { slug }. EnvironmentsPage
still uses env.id for local selection state (UUID from DB) but
passes env.slug to every mutation.
BREAKING CHANGE: /api/v1/admin/environments/{id:UUID}/... paths removed.
Clients must use /{envSlug}/... (slug from the environments list).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -46,8 +46,8 @@ export function useCreateEnvironment() {
|
||||
export function useUpdateEnvironment() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, ...req }: UpdateEnvironmentRequest & { id: string }) =>
|
||||
adminFetch<Environment>(`/environments/${id}`, {
|
||||
mutationFn: ({ slug, ...req }: UpdateEnvironmentRequest & { slug: string }) =>
|
||||
adminFetch<Environment>(`/environments/${slug}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(req),
|
||||
}),
|
||||
@@ -58,8 +58,8 @@ export function useUpdateEnvironment() {
|
||||
export function useUpdateDefaultContainerConfig() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, config }: { id: string; config: Record<string, unknown> }) =>
|
||||
adminFetch<Environment>(`/environments/${id}/default-container-config`, {
|
||||
mutationFn: ({ slug, config }: { slug: string; config: Record<string, unknown> }) =>
|
||||
adminFetch<Environment>(`/environments/${slug}/default-container-config`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(config),
|
||||
}),
|
||||
@@ -70,8 +70,8 @@ export function useUpdateDefaultContainerConfig() {
|
||||
export function useUpdateJarRetention() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, jarRetentionCount }: { id: string; jarRetentionCount: number | null }) =>
|
||||
adminFetch<Environment>(`/environments/${id}/jar-retention`, {
|
||||
mutationFn: ({ slug, jarRetentionCount }: { slug: string; jarRetentionCount: number | null }) =>
|
||||
adminFetch<Environment>(`/environments/${slug}/jar-retention`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ jarRetentionCount }),
|
||||
}),
|
||||
@@ -82,8 +82,8 @@ export function useUpdateJarRetention() {
|
||||
export function useDeleteEnvironment() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) =>
|
||||
adminFetch<void>(`/environments/${id}`, { method: 'DELETE' }),
|
||||
mutationFn: (slug: string) =>
|
||||
adminFetch<void>(`/environments/${slug}`, { method: 'DELETE' }),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'environments'] }),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user