robodev 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/package.json +1 -1
  2. package/src/emit-starter.test.ts +2 -2
  3. package/src/index.ts +14 -4
  4. package/src/print-live-url.test.ts +31 -0
  5. package/src/print-live-url.ts +6 -0
  6. package/templates/auth-chat/.rulesync/skills/media-feature/SKILL.md +6 -6
  7. package/templates/auth-chat/package.json +1 -1
  8. package/templates/backend/api/_lib.ts +40 -15
  9. package/templates/backend/api/planets/[id].ts +23 -8
  10. package/templates/backend/api/planets.ts +14 -9
  11. package/templates/backend/package.json +1 -1
  12. package/templates/empty/package.json +1 -1
  13. package/templates/space/.rulesync/skills/media-feature/SKILL.md +6 -6
  14. package/templates/space/api/_lib.ts +40 -15
  15. package/templates/space/api/planets/[id].ts +23 -8
  16. package/templates/space/api/planets.ts +14 -9
  17. package/templates/space/apps/fe/src/assets/locales/en/translation.json +1 -0
  18. package/templates/space/apps/fe/src/assets/locales/sl/translation.json +1 -0
  19. package/templates/space/apps/fe/src/components/features/planets/details/PlanetEditPage.tsx +69 -33
  20. package/templates/space/apps/fe/src/components/features/planets/list/PlanetCreateModal.tsx +23 -20
  21. package/templates/space/apps/fe/src/utils/planet-form.test.ts +68 -0
  22. package/templates/space/apps/fe/src/utils/planet-form.ts +49 -0
  23. package/templates/space/apps/fe/src/utils/planet-submit.ts +25 -0
  24. package/templates/space/openapi.json +14 -131
  25. package/templates/space/package.json +1 -1
  26. package/templates/backend/api/files/upload-request.ts +0 -48
  27. package/templates/backend/api/files/upload.ts +0 -29
  28. package/templates/space/api/files/upload-request.ts +0 -48
  29. package/templates/space/api/files/upload.ts +0 -29
  30. package/templates/space/apps/fe/src/utils/media-upload-headers.ts +0 -8
  31. package/templates/space/apps/fe/src/utils/media-upload.test.ts +0 -19
  32. package/templates/space/apps/fe/src/utils/media-upload.ts +0 -72
@@ -9,9 +9,9 @@ import {
9
9
  TextInput,
10
10
  Typography,
11
11
  useForm,
12
- useFormValue,
13
12
  useToast,
14
13
  } from "@povio/ui/tanstack";
14
+ import { useQueryClient } from "@tanstack/react-query";
15
15
  import { useNavigate } from "@tanstack/react-router";
16
16
  import { Trash } from "lucide-react";
17
17
  import { useEffect, useState } from "react";
@@ -19,10 +19,10 @@ import { useTranslation } from "react-i18next";
19
19
 
20
20
  import { RowInputWrapper } from "@/components/shared/forms/RowInputWrapper";
21
21
  import { AlienQueries } from "@/openapi/alien/alien.queries";
22
- import { MediaModels } from "@/openapi/media/media.models";
23
22
  import { PlanetModels } from "@/openapi/planet/planet.models";
24
23
  import { PlanetQueries } from "@/openapi/planet/planet.queries";
25
- import { useMediaUploadHandler } from "@/utils/media-upload";
24
+ import { bindLocalPlanetImage } from "@/utils/planet-form";
25
+ import { updatePlanet } from "@/utils/planet-submit";
26
26
 
27
27
  interface PlanetEditPageProps {
28
28
  planet: PlanetModels.PlanetsGetResponseDto;
@@ -34,11 +34,14 @@ interface PlanetEditPageProps {
34
34
  export function PlanetEditPage({ planet, onClose, onDeleted, onSaved }: PlanetEditPageProps) {
35
35
  const { t } = useTranslation();
36
36
  const navigate = useNavigate();
37
- const updatePlanet = PlanetQueries.useUpdate();
37
+ const queryClient = useQueryClient();
38
38
  const deletePlanet = PlanetQueries.useDeleteApiPlanetsById();
39
39
  const { successToast, errorToast } = useToast();
40
40
  const { confirm } = Confirmation.useConfirmation();
41
- const [previewImageUrl, setPreviewImageUrl] = useState<string | undefined>(planet.image?.url);
41
+ const [pickedFile, setPickedFile] = useState<File | null>(null);
42
+ const [clearedExisting, setClearedExisting] = useState(false);
43
+ const [previewObjectUrl, setPreviewObjectUrl] = useState<string | undefined>();
44
+ const [isPending, setIsPending] = useState(false);
42
45
  // Seed the form from the fetched record, then reset when route data changes between planet IDs.
43
46
  const form = useForm({
44
47
  zodSchema: PlanetModels.PlanetsCreateRequestDtoSchema,
@@ -47,7 +50,6 @@ export function PlanetEditPage({ planet, onClose, onDeleted, onSaved }: PlanetEd
47
50
  alienId: planet.alienId ?? undefined,
48
51
  discoveryDate: planet.discoveryDate ?? undefined,
49
52
  description: planet.description ?? "",
50
- image: planet.image ? { id: planet.image.id } : undefined,
51
53
  },
52
54
  });
53
55
 
@@ -57,30 +59,46 @@ export function PlanetEditPage({ planet, onClose, onDeleted, onSaved }: PlanetEd
57
59
  alienId: planet.alienId ?? undefined,
58
60
  discoveryDate: planet.discoveryDate ?? undefined,
59
61
  description: planet.description ?? "",
60
- image: planet.image ? { id: planet.image.id } : undefined,
61
62
  });
62
- setPreviewImageUrl(planet.image?.url);
63
+ setPickedFile(null);
64
+ setClearedExisting(false);
65
+ setPreviewObjectUrl((current) => {
66
+ if (current) URL.revokeObjectURL(current);
67
+ return undefined;
68
+ });
63
69
  }, [form, planet]);
64
70
 
65
- const handleImageUpload = useMediaUploadHandler({
66
- resourceName: MediaModels.MediaResourceName["planet-image"],
67
- onUploaded: (media) => {
68
- form.setFieldValue("image", { id: media.id });
69
- setPreviewImageUrl(media.previewUrl);
70
- },
71
- });
71
+ useEffect(() => {
72
+ return () => {
73
+ if (previewObjectUrl) URL.revokeObjectURL(previewObjectUrl);
74
+ };
75
+ }, [previewObjectUrl]);
72
76
 
73
- const handleImageChange = (_file: File | null) => {
74
- // When clearing file, keep current image
75
- if (!_file) {
76
- form.setFieldValue("image", planet.image ? { id: planet.image.id } : undefined);
77
- setPreviewImageUrl(planet.image?.url);
77
+ const handleImageChange = (file: File | null) => {
78
+ setPickedFile(file);
79
+ setPreviewObjectUrl((current) => {
80
+ if (current) URL.revokeObjectURL(current);
81
+ return file ? URL.createObjectURL(file) : undefined;
82
+ });
83
+ if (file) {
84
+ setClearedExisting(false);
78
85
  }
79
86
  };
80
87
 
88
+ const localImage = bindLocalPlanetImage(handleImageChange);
89
+
81
90
  const onSubmit = async (data: PlanetModels.PlanetsCreateRequestDto) => {
82
91
  try {
83
- await updatePlanet.mutateAsync({ id: planet.id, data });
92
+ setIsPending(true);
93
+ await updatePlanet(planet.id, {
94
+ name: data.name,
95
+ alienId: data.alienId,
96
+ discoveryDate: data.discoveryDate,
97
+ description: data.description,
98
+ file: pickedFile,
99
+ clearImage: Boolean(clearedExisting && !pickedFile && planet.image),
100
+ });
101
+ await queryClient.invalidateQueries({ queryKey: PlanetQueries.keys.all });
84
102
  successToast({ text: t(($) => $.planets.edit.success) });
85
103
  if (onSaved) {
86
104
  onSaved();
@@ -89,6 +107,8 @@ export function PlanetEditPage({ planet, onClose, onDeleted, onSaved }: PlanetEd
89
107
  }
90
108
  } catch {
91
109
  errorToast({ text: t(($) => $.planets.edit.error) });
110
+ } finally {
111
+ setIsPending(false);
92
112
  }
93
113
  };
94
114
 
@@ -121,8 +141,11 @@ export function PlanetEditPage({ planet, onClose, onDeleted, onSaved }: PlanetEd
121
141
  }
122
142
  };
123
143
 
124
- const currentImage = useFormValue(form, (values) => values.image);
125
- const currentImageUrl = planet.image && currentImage?.id === planet.image.id ? planet.image.url : previewImageUrl;
144
+ const currentImageUrl = pickedFile
145
+ ? previewObjectUrl
146
+ : clearedExisting
147
+ ? undefined
148
+ : planet.image?.url;
126
149
  const handleClose = () => {
127
150
  if (onClose) {
128
151
  onClose();
@@ -222,16 +245,29 @@ export function PlanetEditPage({ planet, onClose, onDeleted, onSaved }: PlanetEd
222
245
  uploadText={t(($) => $.planets.edit.imageUploadText)}
223
246
  browseText={t(($) => $.planets.edit.imageBrowseText)}
224
247
  acceptedFileTypes={["image/jpeg", "image/png", "image/webp"]}
225
- onChange={handleImageChange}
226
- fileUpload={handleImageUpload}
248
+ {...localImage}
227
249
  />
228
250
  {currentImageUrl && (
229
- <div className="aspect-video w-full overflow-hidden rounded-sm border border-elevation-outline-default-1 bg-elevation-fill-default-2">
230
- <img
231
- src={currentImageUrl}
232
- alt=""
233
- className="h-full w-full object-cover"
234
- />
251
+ <div className="flex flex-col gap-2">
252
+ <div className="aspect-video w-full overflow-hidden rounded-sm border border-elevation-outline-default-1 bg-elevation-fill-default-2">
253
+ <img
254
+ src={currentImageUrl}
255
+ alt=""
256
+ className="h-full w-full object-cover"
257
+ />
258
+ </div>
259
+ {!pickedFile && planet.image && !clearedExisting && (
260
+ <Button
261
+ type="button"
262
+ variant="outlined"
263
+ size="xs"
264
+ color="secondary"
265
+ width="hug"
266
+ onPress={() => setClearedExisting(true)}
267
+ >
268
+ {t(($) => $.planets.edit.imageRemove)}
269
+ </Button>
270
+ )}
235
271
  </div>
236
272
  )}
237
273
  </div>
@@ -254,8 +290,8 @@ export function PlanetEditPage({ planet, onClose, onDeleted, onSaved }: PlanetEd
254
290
  size="xs"
255
291
  color="primary"
256
292
  width="hug"
257
- isDisabled={updatePlanet.isPending}
258
- isLoading={updatePlanet.isPending}
293
+ isDisabled={isPending}
294
+ isLoading={isPending}
259
295
  >
260
296
  {t(($) => $.planets.edit.submit)}
261
297
  </Button>
@@ -10,14 +10,16 @@ import {
10
10
  useForm,
11
11
  useToast,
12
12
  } from "@povio/ui/tanstack";
13
+ import { useQueryClient } from "@tanstack/react-query";
14
+ import { useState } from "react";
13
15
  import { useTranslation } from "react-i18next";
14
16
 
15
17
  import { RowInputWrapper } from "@/components/shared/forms/RowInputWrapper";
16
18
  import { AlienQueries } from "@/openapi/alien/alien.queries";
17
- import { MediaModels } from "@/openapi/media/media.models";
18
19
  import { PlanetModels } from "@/openapi/planet/planet.models";
19
20
  import { PlanetQueries } from "@/openapi/planet/planet.queries";
20
- import { useMediaUploadHandler } from "@/utils/media-upload";
21
+ import { bindLocalPlanetImage } from "@/utils/planet-form";
22
+ import { createPlanet } from "@/utils/planet-submit";
21
23
 
22
24
  interface PlanetCreateModalProps {
23
25
  isOpen: boolean;
@@ -28,8 +30,10 @@ const useAlienLabelsQuery = AlienQueries.useGetLabels as never;
28
30
 
29
31
  export function PlanetCreateModal({ isOpen, onClose }: PlanetCreateModalProps) {
30
32
  const { t } = useTranslation();
31
- const createPlanet = PlanetQueries.useCreate();
33
+ const queryClient = useQueryClient();
32
34
  const { successToast, errorToast } = useToast();
35
+ const [pickedFile, setPickedFile] = useState<File | null>(null);
36
+ const [isPending, setIsPending] = useState(false);
33
37
  // Generated schemas drive form typing and validation; do not import backend types in frontend code.
34
38
  const form = useForm({
35
39
  zodSchema: PlanetModels.PlanetsCreateRequestDtoSchema,
@@ -38,34 +42,34 @@ export function PlanetCreateModal({ isOpen, onClose }: PlanetCreateModalProps) {
38
42
  alienId: undefined,
39
43
  discoveryDate: undefined,
40
44
  description: "",
41
- image: undefined,
42
45
  },
43
46
  });
44
47
 
45
- const handleImageUpload = useMediaUploadHandler({
46
- resourceName: MediaModels.MediaResourceName["planet-image"],
47
- onUploaded: (media) => form.setFieldValue("image", { id: media.id }),
48
- });
49
-
50
- const handleImageChange = (file: File | null) => {
51
- if (!file) {
52
- form.setFieldValue("image", undefined);
53
- }
54
- };
55
-
56
48
  const onSubmit = async (data: PlanetModels.PlanetsCreateRequestDto) => {
57
49
  try {
58
- await createPlanet.mutateAsync({ data });
50
+ setIsPending(true);
51
+ await createPlanet({
52
+ name: data.name,
53
+ alienId: data.alienId,
54
+ discoveryDate: data.discoveryDate,
55
+ description: data.description,
56
+ file: pickedFile,
57
+ });
58
+ await queryClient.invalidateQueries({ queryKey: PlanetQueries.keys.all });
59
59
  successToast({ text: t(($) => $.planets.create.success) });
60
60
  form.reset();
61
+ setPickedFile(null);
61
62
  onClose();
62
63
  } catch {
63
64
  errorToast({ text: t(($) => $.planets.create.error) });
65
+ } finally {
66
+ setIsPending(false);
64
67
  }
65
68
  };
66
69
 
67
70
  const handleClose = () => {
68
71
  form.reset();
72
+ setPickedFile(null);
69
73
  onClose();
70
74
  };
71
75
 
@@ -145,8 +149,7 @@ export function PlanetCreateModal({ isOpen, onClose }: PlanetCreateModalProps) {
145
149
  uploadText={t(($) => $.planets.create.imageUploadText)}
146
150
  browseText={t(($) => $.planets.create.imageBrowseText)}
147
151
  acceptedFileTypes={["image/jpeg", "image/png", "image/webp"]}
148
- onChange={handleImageChange}
149
- fileUpload={handleImageUpload}
152
+ {...bindLocalPlanetImage((file) => setPickedFile(file))}
150
153
  />
151
154
 
152
155
  <div className="mt-6 flex items-center justify-end gap-2 self-end">
@@ -167,8 +170,8 @@ export function PlanetCreateModal({ isOpen, onClose }: PlanetCreateModalProps) {
167
170
  size="xs"
168
171
  color="primary"
169
172
  width="hug"
170
- isDisabled={createPlanet.isPending}
171
- isLoading={createPlanet.isPending}
173
+ isDisabled={isPending}
174
+ isLoading={isPending}
172
175
  >
173
176
  {t(($) => $.planets.create.submit)}
174
177
  </Button>
@@ -0,0 +1,68 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ bindLocalPlanetImage,
5
+ buildPlanetFormData,
6
+ localPlanetImageUpload,
7
+ planetFormRequestConfig,
8
+ } from "./planet-form";
9
+
10
+ describe("planet multipart submit", () => {
11
+ it("builds FormData with text fields and an optional file", () => {
12
+ const file = new File(["png"], "mars.png", { type: "image/png" });
13
+ const form = buildPlanetFormData({
14
+ name: "Mars",
15
+ description: "Cold desert",
16
+ file,
17
+ });
18
+
19
+ expect(form.get("name")).toBe("Mars");
20
+ expect(form.get("description")).toBe("Cold desert");
21
+ const uploaded = form.get("file") as File;
22
+ expect(uploaded).toBeInstanceOf(File);
23
+ expect(uploaded.name).toBe("mars.png");
24
+ expect(uploaded.type).toBe("image/png");
25
+ expect(form.get("clearImage")).toBeNull();
26
+ });
27
+
28
+ it("sends clearImage only when the existing image was cleared and no file was picked", () => {
29
+ const form = buildPlanetFormData({
30
+ name: "Mars",
31
+ clearImage: true,
32
+ });
33
+
34
+ expect(form.get("clearImage")).toBe("true");
35
+
36
+ const withFile = buildPlanetFormData({
37
+ name: "Mars",
38
+ clearImage: true,
39
+ file: new File(["png"], "mars.png", { type: "image/png" }),
40
+ });
41
+ expect(withFile.get("clearImage")).toBeNull();
42
+ });
43
+
44
+ it("does not set Content-Type so the browser can add the multipart boundary", () => {
45
+ const config = planetFormRequestConfig() as { headers?: Record<string, string> };
46
+ expect(config.headers?.["Content-Type"]).toBeUndefined();
47
+ expect("Content-Type" in (config.headers ?? {})).toBe(false);
48
+ });
49
+
50
+ it("local FileUpload handler never fetches", async () => {
51
+ const file = new File(["png"], "mars.png", { type: "image/png" });
52
+ const result = await localPlanetImageUpload({ data: {} } as never, file);
53
+ expect(result).toEqual({ id: "mars.png" });
54
+ });
55
+
56
+ it("bindLocalPlanetImage keeps the File locally and clears without fetching", async () => {
57
+ const seen: Array<File | null> = [];
58
+ const handlers = bindLocalPlanetImage((file) => seen.push(file));
59
+ const file = new File(["png"], "mars.png", { type: "image/png" });
60
+
61
+ await handlers.fileUpload({ data: {} }, file);
62
+ await handlers.fileRemove();
63
+
64
+ expect(seen).toHaveLength(2);
65
+ expect(seen[0]).toBe(file);
66
+ expect(seen[1]).toBeNull();
67
+ });
68
+ });
@@ -0,0 +1,49 @@
1
+ export type PlanetWriteFields = {
2
+ name: string;
3
+ alienId?: string | null;
4
+ discoveryDate?: string | Date | null;
5
+ description?: string | null;
6
+ file?: File | null;
7
+ clearImage?: boolean;
8
+ };
9
+
10
+ function appendOptional(form: FormData, key: string, value: unknown) {
11
+ if (value == null || value === "") return;
12
+ if (value instanceof Date) {
13
+ form.append(key, value.toISOString());
14
+ return;
15
+ }
16
+ form.append(key, String(value));
17
+ }
18
+
19
+ export function buildPlanetFormData(fields: PlanetWriteFields): FormData {
20
+ const form = new FormData();
21
+ form.append("name", fields.name);
22
+ appendOptional(form, "alienId", fields.alienId);
23
+ appendOptional(form, "discoveryDate", fields.discoveryDate);
24
+ appendOptional(form, "description", fields.description);
25
+ if (fields.file) form.append("file", fields.file);
26
+ if (fields.clearImage && !fields.file) form.append("clearImage", "true");
27
+ return form;
28
+ }
29
+
30
+ /** Axios/browser must set the multipart boundary. Never set Content-Type. */
31
+ export function planetFormRequestConfig() {
32
+ return {};
33
+ }
34
+
35
+ export async function localPlanetImageUpload(_request: unknown, file: File): Promise<{ id: string }> {
36
+ return { id: file.name };
37
+ }
38
+
39
+ export function bindLocalPlanetImage(onFile: (file: File | null) => void) {
40
+ return {
41
+ fileUpload: async (request: unknown, file: File) => {
42
+ onFile(file);
43
+ return localPlanetImageUpload(request, file);
44
+ },
45
+ fileRemove: async () => {
46
+ onFile(null);
47
+ },
48
+ };
49
+ }
@@ -0,0 +1,25 @@
1
+ import { AppRestClient } from "@/clients/app-rest-client";
2
+ import { PlanetModels } from "@/openapi/planet/planet.models";
3
+
4
+ import { buildPlanetFormData, planetFormRequestConfig, type PlanetWriteFields } from "./planet-form";
5
+
6
+ export async function createPlanet(fields: PlanetWriteFields): Promise<PlanetModels.PlanetsGetResponseDto> {
7
+ return AppRestClient.post(
8
+ { resSchema: PlanetModels.PlanetsGetResponseDtoSchema },
9
+ "/api/planets",
10
+ buildPlanetFormData(fields),
11
+ planetFormRequestConfig(),
12
+ );
13
+ }
14
+
15
+ export async function updatePlanet(
16
+ id: string,
17
+ fields: PlanetWriteFields,
18
+ ): Promise<PlanetModels.PlanetsGetResponseDto> {
19
+ return AppRestClient.put(
20
+ { resSchema: PlanetModels.PlanetsGetResponseDtoSchema },
21
+ `/api/planets/${id}`,
22
+ buildPlanetFormData(fields),
23
+ planetFormRequestConfig(),
24
+ );
25
+ }
@@ -282,56 +282,6 @@
282
282
  },
283
283
  "required": ["refreshToken"]
284
284
  },
285
- "MediaUploadRequest": {
286
- "type": "object",
287
- "properties": {
288
- "resourceName": {
289
- "$ref": "#/components/schemas/MediaResourceName"
290
- },
291
- "fileName": {
292
- "type": "string",
293
- "minLength": 1
294
- },
295
- "fileSize": {
296
- "type": "integer",
297
- "minimum": -9007199254740991,
298
- "maximum": 9007199254740991,
299
- "exclusiveMinimum": 0
300
- },
301
- "mimeType": {
302
- "type": "string",
303
- "minLength": 1
304
- },
305
- "method": {
306
- "$ref": "#/components/schemas/MediaUploadMethod"
307
- }
308
- },
309
- "required": ["fileName", "fileSize"]
310
- },
311
- "MediaUploadInstructionsResponse": {
312
- "type": "object",
313
- "properties": {
314
- "id": {
315
- "type": "string"
316
- },
317
- "method": {
318
- "$ref": "#/components/schemas/MediaUploadMethod"
319
- },
320
- "url": {
321
- "type": "string"
322
- },
323
- "fields": {
324
- "type": "array",
325
- "items": {
326
- "type": "array"
327
- }
328
- },
329
- "provider": {
330
- "type": "string"
331
- }
332
- },
333
- "required": ["id", "method", "url", "fields"]
334
- },
335
285
  "FakeMailSendDemoBody": {
336
286
  "type": "object",
337
287
  "properties": {
@@ -541,13 +491,9 @@
541
491
  "nullable": true,
542
492
  "type": "string"
543
493
  },
544
- "image": {
545
- "nullable": true,
546
- "allOf": [
547
- {
548
- "$ref": "#/components/schemas/PlanetImageRequestDto"
549
- }
550
- ]
494
+ "file": {
495
+ "type": "string",
496
+ "format": "binary"
551
497
  }
552
498
  },
553
499
  "required": ["name"]
@@ -572,13 +518,12 @@
572
518
  "nullable": true,
573
519
  "type": "string"
574
520
  },
575
- "image": {
576
- "nullable": true,
577
- "allOf": [
578
- {
579
- "$ref": "#/components/schemas/PlanetImageRequestDto"
580
- }
581
- ]
521
+ "file": {
522
+ "type": "string",
523
+ "format": "binary"
524
+ },
525
+ "clearImage": {
526
+ "type": "boolean"
582
527
  }
583
528
  },
584
529
  "required": ["name"]
@@ -668,15 +613,6 @@
668
613
  "type": "string",
669
614
  "x-enumNames": ["id", "name", "discoveryDate", "createdAt", "updatedAt"]
670
615
  },
671
- "MediaResourceName": {
672
- "type": "string",
673
- "minLength": 1,
674
- "enum": ["small-image", "large-image", "compressed-file", "document", "any", "planet-image"]
675
- },
676
- "MediaUploadMethod": {
677
- "type": "string",
678
- "minLength": 1
679
- },
680
616
  "FakeMailDemoVariant": {
681
617
  "enum": ["text", "html", "recipients", "sequence"],
682
618
  "type": "string",
@@ -774,15 +710,6 @@
774
710
  }
775
711
  },
776
712
  "required": ["id", "userId", "name", "createdAt", "updatedAt", "likesCount", "likedByMe"]
777
- },
778
- "PlanetImageRequestDto": {
779
- "type": "object",
780
- "properties": {
781
- "id": {
782
- "type": "string"
783
- }
784
- },
785
- "required": ["id"]
786
713
  }
787
714
  },
788
715
  "securitySchemes": {
@@ -1209,41 +1136,6 @@
1209
1136
  "x-bl": "Consumes a valid unused password reset code."
1210
1137
  }
1211
1138
  },
1212
- "/api/files/upload-request": {
1213
- "post": {
1214
- "operationId": "MediaLibraryController_uploadRequest",
1215
- "tags": ["Media"],
1216
- "requestBody": {
1217
- "required": true,
1218
- "content": {
1219
- "application/json": {
1220
- "schema": {
1221
- "$ref": "#/components/schemas/MediaUploadRequest"
1222
- }
1223
- }
1224
- }
1225
- },
1226
- "responses": {
1227
- "201": {
1228
- "description": "OK",
1229
- "content": {
1230
- "application/json": {
1231
- "schema": {
1232
- "$ref": "#/components/schemas/MediaUploadInstructionsResponse"
1233
- }
1234
- }
1235
- }
1236
- }
1237
- },
1238
- "security": [
1239
- {
1240
- "bearerAuth": []
1241
- }
1242
- ],
1243
- "x-media-upload": true,
1244
- "x-bl": "Validates an authenticated media upload request, creates a pending media record for an allowed resource, and returns upload instructions for sending the file bytes separately."
1245
- }
1246
- },
1247
1139
  "/api/fake-mail/demo": {
1248
1140
  "post": {
1249
1141
  "operationId": "FakeMailController_sendDemo",
@@ -1391,7 +1283,7 @@
1391
1283
  "requestBody": {
1392
1284
  "required": true,
1393
1285
  "content": {
1394
- "application/json": {
1286
+ "multipart/form-data": {
1395
1287
  "schema": {
1396
1288
  "$ref": "#/components/schemas/PlanetsCreateRequestDto"
1397
1289
  }
@@ -1421,7 +1313,7 @@
1421
1313
  "bearerAuth": []
1422
1314
  }
1423
1315
  ],
1424
- "x-bl": "Creates a planet owned by the authenticated user and optionally assigns an existing alien.",
1316
+ "x-bl": "Creates a planet owned by the authenticated user from multipart form fields and an optional image file.",
1425
1317
  "x-acl": [
1426
1318
  {
1427
1319
  "action": "create",
@@ -1597,7 +1489,7 @@
1597
1489
  "requestBody": {
1598
1490
  "required": true,
1599
1491
  "content": {
1600
- "application/json": {
1492
+ "multipart/form-data": {
1601
1493
  "schema": {
1602
1494
  "$ref": "#/components/schemas/PlanetControllerUpdateRequestDto"
1603
1495
  }
@@ -1627,7 +1519,7 @@
1627
1519
  "bearerAuth": []
1628
1520
  }
1629
1521
  ],
1630
- "x-bl": "Updates a planet only when the authenticated user owns it, optionally assigns an existing alien, and returns it with creator name and alien name.",
1522
+ "x-bl": "Updates a planet only when the authenticated user owns it from multipart form fields, optionally replacing or clearing the image, and returns it with creator name and alien name.",
1631
1523
  "x-acl": [
1632
1524
  {
1633
1525
  "action": "update",
@@ -1797,16 +1689,7 @@
1797
1689
  {
1798
1690
  "name": "Planet",
1799
1691
  "x-robodev-hidden": true,
1800
- "x-robodev-owned-tables": ["Planet", "PlanetLike"],
1801
- "x-robodev-media-resources": [
1802
- {
1803
- "name": "planet-image",
1804
- "field": "imageId",
1805
- "dtoField": "image",
1806
- "mimeTypes": ["image/jpeg", "image/png", "image/webp"],
1807
- "maxFileSize": 2097152
1808
- }
1809
- ]
1692
+ "x-robodev-owned-tables": ["Planet", "PlanetLike"]
1810
1693
  }
1811
1694
  ],
1812
1695
  "x-robodev-user-roles": [
@@ -11,7 +11,7 @@
11
11
  "@robodev-ai/sdk": "^0.6.0"
12
12
  },
13
13
  "devDependencies": {
14
- "robodev": "^0.21.0",
14
+ "robodev": "^0.22.0",
15
15
  "rulesync": "^8.18.0",
16
16
  "typescript": "^5.9.2"
17
17
  }