@ram_28/kf-ai-sdk 2.0.6 → 2.0.9

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 (79) hide show
  1. package/dist/api/client.d.ts +21 -1
  2. package/dist/api/client.d.ts.map +1 -1
  3. package/dist/api/index.d.ts +1 -1
  4. package/dist/api/index.d.ts.map +1 -1
  5. package/dist/api.cjs +1 -1
  6. package/dist/api.mjs +2 -2
  7. package/dist/api.types.d.ts +1 -1
  8. package/dist/api.types.d.ts.map +1 -1
  9. package/dist/auth.cjs +1 -1
  10. package/dist/auth.mjs +1 -1
  11. package/dist/bdo/core/BaseBdo.d.ts +21 -1
  12. package/dist/bdo/core/BaseBdo.d.ts.map +1 -1
  13. package/dist/bdo/core/Item.d.ts +22 -3
  14. package/dist/bdo/core/Item.d.ts.map +1 -1
  15. package/dist/bdo/core/types.d.ts +28 -0
  16. package/dist/bdo/core/types.d.ts.map +1 -1
  17. package/dist/bdo/fields/FileField.d.ts +2 -2
  18. package/dist/bdo/fields/FileField.d.ts.map +1 -1
  19. package/dist/bdo/fields/ImageField.d.ts +18 -0
  20. package/dist/bdo/fields/ImageField.d.ts.map +1 -0
  21. package/dist/bdo/fields/attachment-constants.d.ts +9 -0
  22. package/dist/bdo/fields/attachment-constants.d.ts.map +1 -0
  23. package/dist/bdo/fields/index.d.ts +1 -0
  24. package/dist/bdo/fields/index.d.ts.map +1 -1
  25. package/dist/bdo/index.d.ts +2 -2
  26. package/dist/bdo/index.d.ts.map +1 -1
  27. package/dist/bdo.cjs +1 -1
  28. package/dist/bdo.d.ts +1 -1
  29. package/dist/bdo.d.ts.map +1 -1
  30. package/dist/bdo.mjs +381 -157
  31. package/dist/bdo.types.d.ts +2 -2
  32. package/dist/bdo.types.d.ts.map +1 -1
  33. package/dist/client-BnVxSHAm.cjs +1 -0
  34. package/dist/client-CMERmrC-.js +279 -0
  35. package/dist/form.cjs +1 -1
  36. package/dist/form.mjs +14 -14
  37. package/dist/{metadata-BJWukIqS.cjs → metadata-BfJtHz84.cjs} +1 -1
  38. package/dist/{metadata-CJuFxytC.js → metadata-CwAo6a8e.js} +1 -1
  39. package/dist/table.cjs +1 -1
  40. package/dist/table.mjs +1 -1
  41. package/dist/types/base-fields.d.ts +19 -3
  42. package/dist/types/base-fields.d.ts.map +1 -1
  43. package/dist/types/common.d.ts +40 -0
  44. package/dist/types/common.d.ts.map +1 -1
  45. package/dist/types/constants.d.ts +8 -0
  46. package/dist/types/constants.d.ts.map +1 -1
  47. package/dist/workflow/Activity.d.ts +11 -9
  48. package/dist/workflow/Activity.d.ts.map +1 -1
  49. package/dist/workflow/client.d.ts +12 -10
  50. package/dist/workflow/client.d.ts.map +1 -1
  51. package/dist/workflow/types.d.ts +12 -11
  52. package/dist/workflow/types.d.ts.map +1 -1
  53. package/dist/workflow.cjs +1 -1
  54. package/dist/workflow.mjs +201 -213
  55. package/docs/useForm.md +174 -0
  56. package/docs/workflow.md +38 -76
  57. package/package.json +1 -1
  58. package/sdk/api/client.ts +145 -0
  59. package/sdk/api/index.ts +4 -0
  60. package/sdk/api.types.ts +5 -0
  61. package/sdk/bdo/core/BaseBdo.ts +60 -0
  62. package/sdk/bdo/core/Item.ts +231 -3
  63. package/sdk/bdo/core/types.ts +63 -0
  64. package/sdk/bdo/fields/FileField.ts +14 -5
  65. package/sdk/bdo/fields/ImageField.ts +46 -0
  66. package/sdk/bdo/fields/attachment-constants.ts +72 -0
  67. package/sdk/bdo/fields/index.ts +1 -0
  68. package/sdk/bdo/index.ts +6 -0
  69. package/sdk/bdo.ts +1 -0
  70. package/sdk/bdo.types.ts +7 -0
  71. package/sdk/components/hooks/useForm/createResolver.ts +1 -1
  72. package/sdk/types/base-fields.ts +21 -3
  73. package/sdk/types/common.ts +45 -0
  74. package/sdk/types/constants.ts +8 -0
  75. package/sdk/workflow/Activity.ts +18 -26
  76. package/sdk/workflow/client.ts +25 -47
  77. package/sdk/workflow/types.ts +11 -12
  78. package/dist/client-BULEEaCP.js +0 -222
  79. package/dist/client-DtPpfJc1.cjs +0 -1
@@ -12,6 +12,10 @@ import type {
12
12
  PivotOptionsType,
13
13
  PivotResponseType,
14
14
  DraftResponseType,
15
+ FileUploadRequestType,
16
+ FileUploadResponseType,
17
+ FileDownloadResponseType,
18
+ AttachmentViewType,
15
19
  } from "../../types/common";
16
20
  import type { SystemFields } from "../../types/base-fields";
17
21
  import { api } from "../../api/client";
@@ -62,6 +66,13 @@ export abstract class BaseBdo<
62
66
  // FIELD DEFINITIONS (auto-discovered)
63
67
  // ============================================================
64
68
 
69
+ /**
70
+ * Get the Business Object ID for API calls
71
+ */
72
+ getBoId(): string {
73
+ return this.meta._id;
74
+ }
75
+
65
76
  /**
66
77
  * Whether fields have been bound to this BDO
67
78
  */
@@ -299,4 +310,53 @@ export abstract class BaseBdo<
299
310
  ): Promise<PivotResponseType> {
300
311
  return api<TEntity>(this.meta._id).pivot(options);
301
312
  }
313
+
314
+ // ============================================================
315
+ // ATTACHMENT OPERATIONS
316
+ // ============================================================
317
+
318
+ /**
319
+ * Get signed upload URLs for file/image attachments
320
+ */
321
+ protected async getUploadUrl(
322
+ instanceId: string,
323
+ fieldId: string,
324
+ files: FileUploadRequestType[],
325
+ ): Promise<FileUploadResponseType[]> {
326
+ return api<TEntity>(this.meta._id).getUploadUrl(instanceId, fieldId, files);
327
+ }
328
+
329
+ /**
330
+ * Get signed download URL for a single attachment
331
+ */
332
+ protected async getDownloadUrl(
333
+ instanceId: string,
334
+ fieldId: string,
335
+ attachmentId: string,
336
+ viewType?: AttachmentViewType,
337
+ ): Promise<FileDownloadResponseType> {
338
+ return api<TEntity>(this.meta._id).getDownloadUrl(instanceId, fieldId, attachmentId, viewType);
339
+ }
340
+
341
+ /**
342
+ * Get signed download URLs for all attachments on a field
343
+ */
344
+ protected async getDownloadUrls(
345
+ instanceId: string,
346
+ fieldId: string,
347
+ viewType?: AttachmentViewType,
348
+ ): Promise<FileDownloadResponseType[]> {
349
+ return api<TEntity>(this.meta._id).getDownloadUrls(instanceId, fieldId, viewType);
350
+ }
351
+
352
+ /**
353
+ * Delete an attachment
354
+ */
355
+ protected async deleteAttachment(
356
+ instanceId: string,
357
+ fieldId: string,
358
+ attachmentId: string,
359
+ ): Promise<void> {
360
+ return api<TEntity>(this.meta._id).deleteAttachment(instanceId, fieldId, attachmentId);
361
+ }
302
362
  }
@@ -9,8 +9,27 @@ import type {
9
9
  EditableFieldAccessorType,
10
10
  ReadonlyFieldAccessorType,
11
11
  FieldAccessorType,
12
+ EditableImageFieldAccessorType,
13
+ ReadonlyImageFieldAccessorType,
14
+ EditableFileFieldAccessorType,
15
+ ReadonlyFileFieldAccessorType,
12
16
  } from "./types";
13
17
  import type { BaseField } from "../fields/BaseField";
18
+ import type {
19
+ FileType,
20
+ ImageFieldType,
21
+ FileFieldType,
22
+ } from "../../types/base-fields";
23
+ import type {
24
+ FileUploadRequestType,
25
+ FileDownloadResponseType,
26
+ AttachmentViewType,
27
+ } from "../../types/common";
28
+ import {
29
+ validateFileExtension,
30
+ extractFileExtension,
31
+ } from "../fields/attachment-constants";
32
+ import { api } from "../../api/client";
14
33
 
15
34
  /**
16
35
  * Interface for BDO that Item needs
@@ -23,23 +42,44 @@ interface BdoLike {
23
42
  value: unknown,
24
43
  allValues: Record<string, unknown>
25
44
  ): ValidationResultType;
45
+ getBoId(): string;
26
46
  }
27
47
 
28
48
  // Re-export accessor types for convenience
29
49
  export type { EditableFieldAccessorType, ReadonlyFieldAccessorType, FieldAccessorType, BaseFieldMetaType };
30
50
 
51
+ /**
52
+ * Resolve editable accessor type — specialized for File/Image fields
53
+ */
54
+ type ResolveEditableAccessor<T> =
55
+ [T] extends [ImageFieldType]
56
+ ? EditableImageFieldAccessorType
57
+ : [T] extends [FileFieldType]
58
+ ? EditableFileFieldAccessorType
59
+ : EditableFieldAccessorType<T>;
60
+
61
+ /**
62
+ * Resolve readonly accessor type — specialized for File/Image fields
63
+ */
64
+ type ResolveReadonlyAccessor<T> =
65
+ [T] extends [ImageFieldType]
66
+ ? ReadonlyImageFieldAccessorType
67
+ : [T] extends [FileFieldType]
68
+ ? ReadonlyFileFieldAccessorType
69
+ : ReadonlyFieldAccessorType<T>;
70
+
31
71
  /**
32
72
  * Create editable accessor type for each field in TEditable
33
73
  */
34
74
  type EditableAccessors<T> = {
35
- [K in keyof T as K extends "_id" ? never : K]: EditableFieldAccessorType<T[K]>;
75
+ [K in keyof T as K extends "_id" ? never : K]: ResolveEditableAccessor<T[K]>;
36
76
  };
37
77
 
38
78
  /**
39
79
  * Create readonly accessor type for each field in TReadonly
40
80
  */
41
81
  type ReadonlyAccessors<T> = {
42
- [K in keyof T as K extends "_id" ? never : K]: ReadonlyFieldAccessorType<T[K]>;
82
+ [K in keyof T as K extends "_id" ? never : K]: ResolveReadonlyAccessor<T[K]>;
43
83
  };
44
84
 
45
85
  /**
@@ -90,7 +130,8 @@ export class Item<T extends Record<string, unknown>> {
90
130
  prop === "_bdo" ||
91
131
  prop === "_data" ||
92
132
  prop === "_accessorCache" ||
93
- prop === "_getAccessor"
133
+ prop === "_getAccessor" ||
134
+ prop === "_requireInstanceId"
94
135
  ) {
95
136
  return Reflect.get(target, prop, receiver);
96
137
  }
@@ -155,6 +196,20 @@ export class Item<T extends Record<string, unknown>> {
155
196
  }) as Item<T>;
156
197
  }
157
198
 
199
+ /**
200
+ * Require instanceId or throw.
201
+ * TODO: Support create flow via draftInteraction to get temp _id
202
+ */
203
+ private _requireInstanceId(): string {
204
+ const id = this._data._id as string | undefined;
205
+ if (!id) {
206
+ throw new Error(
207
+ "Cannot perform attachment operation: item has no _id. Save the item first.",
208
+ );
209
+ }
210
+ return id;
211
+ }
212
+
158
213
  /**
159
214
  * Get or create a field accessor for the given field.
160
215
  * Editable fields get set(), readonly fields do not.
@@ -234,6 +289,179 @@ export class Item<T extends Record<string, unknown>> {
234
289
  };
235
290
  }
236
291
 
292
+ // Enrich File/Image field accessors with attachment methods
293
+ if (meta.Type === "Image" || meta.Type === "File") {
294
+ const boId = this._bdo.getBoId();
295
+ const acc = accessor as unknown as Record<string, unknown>;
296
+
297
+ if (meta.Type === "Image") {
298
+ // Image field — single file
299
+
300
+ // getDownloadUrl — always available (editable + readonly)
301
+ acc.getDownloadUrl = async (
302
+ viewType?: AttachmentViewType,
303
+ ): Promise<FileDownloadResponseType> => {
304
+ const instanceId = this._requireInstanceId();
305
+ const value = this._data[fieldId as keyof T] as
306
+ | FileType
307
+ | null
308
+ | undefined;
309
+ if (!value?._id) {
310
+ throw new Error(`${fieldId} has no image to download`);
311
+ }
312
+ return api(boId).getDownloadUrl(
313
+ instanceId,
314
+ fieldId,
315
+ value._id,
316
+ viewType,
317
+ );
318
+ };
319
+
320
+ // upload + deleteAttachment — editable only
321
+ if (!isReadOnly) {
322
+ /**
323
+ * Upload to storage and update local field value.
324
+ * Does NOT persist to backend — call save()/update() after uploading.
325
+ */
326
+ acc.upload = async (file: File): Promise<FileType> => {
327
+ validateFileExtension(file.name, "Image");
328
+ const instanceId = this._requireInstanceId();
329
+ const request: FileUploadRequestType = {
330
+ FileName: file.name,
331
+ Size: file.size,
332
+ FileExtension: extractFileExtension(file.name),
333
+ };
334
+ const [uploadInfo] = await api(boId).getUploadUrl(
335
+ instanceId,
336
+ fieldId,
337
+ [request],
338
+ );
339
+
340
+ await fetch(uploadInfo.UploadUrl.URL, {
341
+ method: "PUT",
342
+ headers: { "Content-Type": uploadInfo.ContentType },
343
+ body: file,
344
+ });
345
+
346
+ const metadata: FileType = {
347
+ _id: uploadInfo._id,
348
+ _name: uploadInfo._name,
349
+ FileName: uploadInfo.FileName,
350
+ FileExtension: uploadInfo.FileExtension,
351
+ Size: uploadInfo.Size,
352
+ ContentType: uploadInfo.ContentType,
353
+ };
354
+
355
+ this._data[fieldId as keyof T] = metadata as T[keyof T];
356
+ return metadata;
357
+ };
358
+
359
+ acc.deleteAttachment = async (): Promise<void> => {
360
+ const instanceId = this._requireInstanceId();
361
+ const value = this._data[fieldId as keyof T] as
362
+ | FileType
363
+ | null
364
+ | undefined;
365
+ if (!value?._id) {
366
+ throw new Error(`${fieldId} has no image to delete`);
367
+ }
368
+ await api(boId).deleteAttachment(instanceId, fieldId, value._id);
369
+ this._data[fieldId as keyof T] = null as T[keyof T];
370
+ };
371
+ }
372
+ } else {
373
+ // File field — multi-file
374
+
375
+ // getDownloadUrl + getDownloadUrls — always available (editable + readonly)
376
+ acc.getDownloadUrl = async (
377
+ attachmentId: string,
378
+ viewType?: AttachmentViewType,
379
+ ): Promise<FileDownloadResponseType> => {
380
+ const instanceId = this._requireInstanceId();
381
+ return api(boId).getDownloadUrl(
382
+ instanceId,
383
+ fieldId,
384
+ attachmentId,
385
+ viewType,
386
+ );
387
+ };
388
+
389
+ acc.getDownloadUrls = async (
390
+ viewType?: AttachmentViewType,
391
+ ): Promise<FileDownloadResponseType[]> => {
392
+ const instanceId = this._requireInstanceId();
393
+ return api(boId).getDownloadUrls(instanceId, fieldId, viewType);
394
+ };
395
+
396
+ // upload + deleteAttachment — editable only
397
+ if (!isReadOnly) {
398
+ /**
399
+ * Upload to storage and update local field value.
400
+ * Does NOT persist to backend — call save()/update() after uploading.
401
+ * (deleteAttachment is atomic — backend handles storage + DB in one call)
402
+ */
403
+ acc.upload = async (files: File[]): Promise<FileType[]> => {
404
+ for (const file of files) {
405
+ validateFileExtension(file.name, "File");
406
+ }
407
+ const instanceId = this._requireInstanceId();
408
+ const requests: FileUploadRequestType[] = files.map((file) => ({
409
+ FileName: file.name,
410
+ Size: file.size,
411
+ FileExtension: extractFileExtension(file.name),
412
+ }));
413
+ const uploadInfos = await api(boId).getUploadUrl(
414
+ instanceId,
415
+ fieldId,
416
+ requests,
417
+ );
418
+
419
+ const uploaded: FileType[] = await Promise.all(
420
+ files.map(async (file, i) => {
421
+ await fetch(uploadInfos[i].UploadUrl.URL, {
422
+ method: "PUT",
423
+ headers: { "Content-Type": uploadInfos[i].ContentType },
424
+ body: file,
425
+ });
426
+ return {
427
+ _id: uploadInfos[i]._id,
428
+ _name: uploadInfos[i]._name,
429
+ FileName: uploadInfos[i].FileName,
430
+ FileExtension: uploadInfos[i].FileExtension,
431
+ Size: uploadInfos[i].Size,
432
+ ContentType: uploadInfos[i].ContentType,
433
+ };
434
+ }),
435
+ );
436
+
437
+ const current =
438
+ (this._data[fieldId as keyof T] as FileType[] | undefined) ?? [];
439
+ this._data[fieldId as keyof T] = [
440
+ ...current,
441
+ ...uploaded,
442
+ ] as T[keyof T];
443
+ return uploaded;
444
+ };
445
+
446
+ acc.deleteAttachment = async (
447
+ attachmentId: string,
448
+ ): Promise<void> => {
449
+ const instanceId = this._requireInstanceId();
450
+ await api(boId).deleteAttachment(
451
+ instanceId,
452
+ fieldId,
453
+ attachmentId,
454
+ );
455
+ const current =
456
+ (this._data[fieldId as keyof T] as FileType[] | undefined) ?? [];
457
+ this._data[fieldId as keyof T] = current.filter(
458
+ (f) => f._id !== attachmentId,
459
+ ) as T[keyof T];
460
+ };
461
+ }
462
+ }
463
+ }
464
+
237
465
  // Cache and return
238
466
  this._accessorCache.set(fieldId, accessor);
239
467
  return accessor;
@@ -142,6 +142,11 @@ export interface FileFieldMetaType extends BaseFieldMetaType {
142
142
  Constraint?: BaseConstraintType;
143
143
  }
144
144
 
145
+ export interface ImageFieldMetaType extends BaseFieldMetaType {
146
+ Type: "Image";
147
+ Constraint?: BaseConstraintType;
148
+ }
149
+
145
150
  // ============================================================
146
151
  // RUNTIME ACCESSOR TYPES
147
152
  // These represent what item.Title looks like at runtime
@@ -170,6 +175,64 @@ export type ReadonlyFieldAccessorType<T> = BaseFieldAccessorType<T>;
170
175
  /** Union of editable or readonly accessor */
171
176
  export type FieldAccessorType<T> = EditableFieldAccessorType<T> | ReadonlyFieldAccessorType<T>;
172
177
 
178
+ // ============================================================
179
+ // FILE/IMAGE FIELD ACCESSOR TYPES
180
+ // ============================================================
181
+
182
+ import type {
183
+ FileType,
184
+ ImageFieldType,
185
+ FileFieldType,
186
+ } from "../../types/base-fields";
187
+ import type {
188
+ FileDownloadResponseType,
189
+ AttachmentViewType,
190
+ } from "../../types/common";
191
+
192
+ /** Editable Image field accessor — adds upload, download, delete */
193
+ export interface EditableImageFieldAccessorType
194
+ extends EditableFieldAccessorType<ImageFieldType> {
195
+ upload(file: File): Promise<FileType>;
196
+ getDownloadUrl(
197
+ viewType?: AttachmentViewType,
198
+ ): Promise<FileDownloadResponseType>;
199
+ deleteAttachment(): Promise<void>;
200
+ }
201
+
202
+ /** Readonly Image field accessor — download only */
203
+ export interface ReadonlyImageFieldAccessorType
204
+ extends ReadonlyFieldAccessorType<ImageFieldType> {
205
+ getDownloadUrl(
206
+ viewType?: AttachmentViewType,
207
+ ): Promise<FileDownloadResponseType>;
208
+ }
209
+
210
+ /** Editable File field accessor — adds upload, download, delete */
211
+ export interface EditableFileFieldAccessorType
212
+ extends EditableFieldAccessorType<FileFieldType> {
213
+ upload(files: File[]): Promise<FileType[]>;
214
+ getDownloadUrl(
215
+ attachmentId: string,
216
+ viewType?: AttachmentViewType,
217
+ ): Promise<FileDownloadResponseType>;
218
+ getDownloadUrls(
219
+ viewType?: AttachmentViewType,
220
+ ): Promise<FileDownloadResponseType[]>;
221
+ deleteAttachment(attachmentId: string): Promise<void>;
222
+ }
223
+
224
+ /** Readonly File field accessor — download only */
225
+ export interface ReadonlyFileFieldAccessorType
226
+ extends ReadonlyFieldAccessorType<FileFieldType> {
227
+ getDownloadUrl(
228
+ attachmentId: string,
229
+ viewType?: AttachmentViewType,
230
+ ): Promise<FileDownloadResponseType>;
231
+ getDownloadUrls(
232
+ viewType?: AttachmentViewType,
233
+ ): Promise<FileDownloadResponseType[]>;
234
+ }
235
+
173
236
  // ============================================================
174
237
  // SELECT FIELD OPTIONS
175
238
  // ============================================================
@@ -1,6 +1,6 @@
1
1
  // ============================================================
2
2
  // FILE FIELD
3
- // Field for file attachments
3
+ // Field for file attachments (array of files)
4
4
  // ============================================================
5
5
 
6
6
  import type { FileFieldType } from "../../types/base-fields";
@@ -12,8 +12,8 @@ import { BaseField } from "./BaseField";
12
12
  *
13
13
  * @example
14
14
  * ```typescript
15
- * readonly Attachment = new FileField({
16
- * _id: "Attachment", Name: "Attachment", Type: "File",
15
+ * readonly Attachments = new FileField({
16
+ * _id: "Attachments", Name: "Attachments", Type: "File",
17
17
  * });
18
18
  * ```
19
19
  */
@@ -27,13 +27,22 @@ export class FileField extends BaseField<FileFieldType> {
27
27
  return { valid: true, errors: [] };
28
28
  }
29
29
 
30
- if (typeof value !== "object" || Array.isArray(value)) {
30
+ if (!Array.isArray(value)) {
31
31
  return {
32
32
  valid: false,
33
- errors: [`${this.label} must be a valid file object`],
33
+ errors: [`${this.label} must be an array of file objects`],
34
34
  };
35
35
  }
36
36
 
37
+ for (const item of value) {
38
+ if (!item || typeof item !== "object" || !item._id) {
39
+ return {
40
+ valid: false,
41
+ errors: [`Each file in ${this.label} must have an _id`],
42
+ };
43
+ }
44
+ }
45
+
37
46
  return { valid: true, errors: [] };
38
47
  }
39
48
  }
@@ -0,0 +1,46 @@
1
+ // ============================================================
2
+ // IMAGE FIELD
3
+ // Field for single image attachments (nullable)
4
+ // ============================================================
5
+
6
+ import type { ImageFieldType } from "../../types/base-fields";
7
+ import type { ImageFieldMetaType, ValidationResultType } from "../core/types";
8
+ import { BaseField } from "./BaseField";
9
+
10
+ /**
11
+ * Field definition for single image attachment fields
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * readonly Avatar = new ImageField({
16
+ * _id: "Avatar", Name: "Avatar", Type: "Image",
17
+ * });
18
+ * ```
19
+ */
20
+ export class ImageField extends BaseField<ImageFieldType> {
21
+ constructor(meta: ImageFieldMetaType) {
22
+ super(meta);
23
+ }
24
+
25
+ validate(value: ImageFieldType | undefined): ValidationResultType {
26
+ if (value === undefined || value === null) {
27
+ return { valid: true, errors: [] };
28
+ }
29
+
30
+ if (typeof value !== "object" || Array.isArray(value)) {
31
+ return {
32
+ valid: false,
33
+ errors: [`${this.label} must be a valid image object`],
34
+ };
35
+ }
36
+
37
+ if (!value._id || !value.FileName) {
38
+ return {
39
+ valid: false,
40
+ errors: [`${this.label} must have _id and FileName`],
41
+ };
42
+ }
43
+
44
+ return { valid: true, errors: [] };
45
+ }
46
+ }
@@ -0,0 +1,72 @@
1
+ /** Supported image extensions — matches backend SupportFileExtensions.IMAGE_EXTENSIONS */
2
+ export const IMAGE_EXTENSIONS = new Set([
3
+ "jpg",
4
+ "jpeg",
5
+ "png",
6
+ "gif",
7
+ "webp",
8
+ "bmp",
9
+ "tiff",
10
+ "tif",
11
+ "heic",
12
+ "heif",
13
+ ]);
14
+
15
+ /** Supported file extensions — matches backend SupportFileExtensions.FILE_EXTENSIONS */
16
+ export const FILE_EXTENSIONS = new Set([
17
+ // Images
18
+ "jpg",
19
+ "jpeg",
20
+ "png",
21
+ "gif",
22
+ "webp",
23
+ "bmp",
24
+ "tiff",
25
+ "tif",
26
+ "heic",
27
+ "heif",
28
+ // Videos
29
+ "mp4",
30
+ "mov",
31
+ "avi",
32
+ "webm",
33
+ "mkv",
34
+ "m4v",
35
+ "wmv",
36
+ "flv",
37
+ // Documents
38
+ "pdf",
39
+ "doc",
40
+ "docx",
41
+ "xls",
42
+ "xlsx",
43
+ "ppt",
44
+ "pptx",
45
+ // Other
46
+ "txt",
47
+ "csv",
48
+ "zip",
49
+ ]);
50
+
51
+ /** Extract and normalize file extension from a filename. Returns lowercase without dot. */
52
+ export function extractFileExtension(fileName: string): string {
53
+ if (!fileName.includes(".")) return "";
54
+ const parts = fileName.split(".");
55
+ return (parts[parts.length - 1] ?? "").toLowerCase();
56
+ }
57
+
58
+ /** Validate file extension against backend whitelist. Throws on invalid. */
59
+ export function validateFileExtension(
60
+ fileName: string,
61
+ fieldType: "File" | "Image",
62
+ ): void {
63
+ const ext = extractFileExtension(fileName);
64
+ const allowed = fieldType === "Image" ? IMAGE_EXTENSIONS : FILE_EXTENSIONS;
65
+ if (!ext || !allowed.has(ext)) {
66
+ const types = [...allowed].sort().join(", ");
67
+ throw new Error(
68
+ `File "${fileName}" has unsupported extension "${ext || "(none)"}". ` +
69
+ `Supported for ${fieldType} fields: ${types}`,
70
+ );
71
+ }
72
+ }
@@ -15,4 +15,5 @@ export { ObjectField } from "./ObjectField";
15
15
  export { TextField } from "./TextField";
16
16
  export { UserField } from "./UserField";
17
17
  export { FileField } from "./FileField";
18
+ export { ImageField } from "./ImageField";
18
19
  export { TextAreaField } from "./TextAreaField";
package/sdk/bdo/index.ts CHANGED
@@ -32,7 +32,12 @@ export type {
32
32
  ArrayFieldMetaType,
33
33
  ObjectFieldMetaType,
34
34
  FileFieldMetaType,
35
+ ImageFieldMetaType,
35
36
  BaseFieldAccessorType,
37
+ EditableImageFieldAccessorType,
38
+ ReadonlyImageFieldAccessorType,
39
+ EditableFileFieldAccessorType,
40
+ ReadonlyFileFieldAccessorType,
36
41
  } from "./core/types";
37
42
 
38
43
  // Field classes
@@ -51,4 +56,5 @@ export {
51
56
  ObjectField,
52
57
  UserField,
53
58
  FileField,
59
+ ImageField,
54
60
  } from "./fields";
package/sdk/bdo.ts CHANGED
@@ -22,6 +22,7 @@ export {
22
22
  ObjectField,
23
23
  UserField,
24
24
  FileField,
25
+ ImageField,
25
26
  } from "./bdo/fields";
26
27
 
27
28
  // Constants
package/sdk/bdo.types.ts CHANGED
@@ -23,10 +23,15 @@ export type {
23
23
  ArrayFieldMetaType,
24
24
  ObjectFieldMetaType,
25
25
  FileFieldMetaType,
26
+ ImageFieldMetaType,
26
27
  BaseFieldAccessorType,
27
28
  EditableFieldAccessorType,
28
29
  ReadonlyFieldAccessorType,
29
30
  FieldAccessorType,
31
+ EditableImageFieldAccessorType,
32
+ ReadonlyImageFieldAccessorType,
33
+ EditableFileFieldAccessorType,
34
+ ReadonlyFileFieldAccessorType,
30
35
  } from "./bdo/core/types";
31
36
 
32
37
  // Re-export SDK field types
@@ -43,6 +48,8 @@ export type {
43
48
  ArrayFieldType,
44
49
  ObjectFieldType,
45
50
  UserFieldType,
51
+ FileType,
52
+ ImageFieldType,
46
53
  FileFieldType,
47
54
  SystemFieldsType,
48
55
  UserRefType,
@@ -10,7 +10,7 @@ export function validateConstraints(field: BaseField<unknown>, value: unknown):
10
10
  const errors: string[] = [];
11
11
 
12
12
  // Required
13
- if (field.required && (value === undefined || value === null || value === "")) {
13
+ if (field.required && (value === undefined || value === null || value === "" || (Array.isArray(value) && value.length === 0))) {
14
14
  errors.push(`${field.label} is required`);
15
15
  return { valid: false, errors };
16
16
  }