@solidxai/core 0.1.14 → 0.1.15-beta.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidxai/core",
3
- "version": "0.1.14",
3
+ "version": "0.1.15-beta.0",
4
4
  "description": "This module is a NestJS module containing all the required core providers required by a Solid application",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -238,9 +238,25 @@ export function isDangerousMediaFile(file?: UploadedFileLike | null): boolean {
238
238
  * Single source of truth for extension -> coarse media category. Shared by upload-time
239
239
  * validation (MediaFieldCrudManager) and serve-time header hardening (ServeStaticModule), so
240
240
  * both agree on which extensions belong to which category and neither drifts out of sync.
241
- * webm/ogg are shared between AUDIO_EXTENSIONS and VIDEO_EXTENSIONS - last one wins here, they
242
- * default to 'video' since that's the more common upload case.
241
+ * Some extensions, such as ogg and webm, are valid for more than one category.
242
+ * Keep every matching category instead of allowing a later entry to overwrite an earlier one.
243
243
  */
244
+ // Example: { ogg: ['audio', 'video'], mp3: ['audio'], jpg: ['image'] }
245
+ export const EXT_TO_MEDIA_TYPES: Record<string, MediaCategory[]> = MEDIA_FILE_TYPES.reduce((acc, fileType) => {
246
+ // Create an empty category list the first time we see an extension.
247
+ if (!acc[fileType.extension]) {
248
+ acc[fileType.extension] = [];
249
+ }
250
+
251
+ // Add the category for this extension, but do not add duplicates.
252
+ if (!acc[fileType.extension].includes(fileType.mediaType)) {
253
+ acc[fileType.extension].push(fileType.mediaType);
254
+ }
255
+
256
+ // Return the map so the next media definition can be added to it.
257
+ return acc;
258
+ }, {} as Record<string, MediaCategory[]>);
259
+
244
260
  export const EXT_TO_MEDIA_TYPE: Record<string, MediaCategory> = {
245
261
  ...Object.fromEntries(MEDIA_FILE_TYPES.map((fileType) => [fileType.extension, fileType.mediaType])),
246
262
  };
@@ -1,5 +1,5 @@
1
1
  import { FieldCrudManager, ValidationError } from "src/interfaces";
2
- import { EXT_TO_MEDIA_TYPE, getLowercaseFileExtension, getUploadedFileName, isDangerousMediaFile, MediaCategory, MIME_TO_MEDIA_TYPE } from "src/constants/media-file-types";
2
+ import { EXT_TO_MEDIA_TYPES, getLowercaseFileExtension, getUploadedFileName, isDangerousMediaFile, MediaCategory, MIME_TO_MEDIA_TYPE } from "src/constants/media-file-types";
3
3
 
4
4
  export enum SolidMediaType {
5
5
  mediaSingle = 'mediaSingle',
@@ -40,9 +40,15 @@ export class MediaFieldCrudManager implements FieldCrudManager {
40
40
  // 'image', since the mimetype match short-circuited before the extension was ever
41
41
  // checked). The extension must independently resolve to a known-safe category before
42
42
  // anything is accepted - not merely "not on the denylist".
43
+ // Read the file extension from the uploaded filename, for example "song.ogg" -> "ogg".
43
44
  const ext = getLowercaseFileExtension(getUploadedFileName(file));
44
- const extType = ext ? EXT_TO_MEDIA_TYPE[ext] : undefined;
45
- if (!extType) {
45
+
46
+ // Find every media category that supports this extension.
47
+ // An extension can belong to multiple categories, for example .ogg can be audio or video.
48
+ const extToMediaTypes = ext ? EXT_TO_MEDIA_TYPES[ext] : undefined;
49
+
50
+ // Reject files whose extension is not part of the supported media definitions.
51
+ if (!extToMediaTypes || extToMediaTypes.length === 0) {
46
52
  return null;
47
53
  }
48
54
 
@@ -50,13 +56,15 @@ export class MediaFieldCrudManager implements FieldCrudManager {
50
56
  // grant a category on its own. A mimetype that doesn't resolve to anything (e.g.
51
57
  // application/octet-stream) has no opinion and is treated as agreeing.
52
58
  const mt = (file.mimetype || '').toLowerCase().trim();
53
- const mimeType = MIME_TO_MEDIA_TYPE[mt]
59
+ const mimeToMediaTypes = MIME_TO_MEDIA_TYPE[mt]
54
60
  ?? (mt.startsWith('image/') ? 'image' : mt.startsWith('audio/') ? 'audio' : mt.startsWith('video/') ? 'video' : undefined);
55
- if (mimeType && mimeType !== extType) {
61
+ // When the extension is shared, the MIME type identifies the actual category:
62
+ // audio/ogg becomes audio, while video/ogg becomes video.
63
+ if (mimeToMediaTypes && !extToMediaTypes.includes(mimeToMediaTypes)) {
56
64
  return null;
57
65
  }
58
66
 
59
- return extType;
67
+ return mimeToMediaTypes || extToMediaTypes[0];
60
68
  }
61
69
 
62
70
  validate(dto: any, files: Array<Express.Multer.File>): ValidationError[] {
@@ -9,6 +9,8 @@ export function getUserExcludedFields(): string[] {
9
9
  "googleId",
10
10
  "active",
11
11
  "forcePasswordChange",
12
+ "failedLoginAttempts",
13
+ "isAllowedToGenerateApiKeys",
12
14
  "lastLoginProvider",
13
15
  "googleProfilePicture",
14
16
  "facebookAccessToken",
@@ -643,9 +643,44 @@ export class ImportTransactionService extends CRUDService<ImportTransaction> {
643
643
  }
644
644
  }
645
645
  }
646
+
647
+ // Some fields are hidden from the import file because users should not
648
+ // have to enter internal values such as passwordSchemeVersion. The
649
+ // backend still checks those fields as required, so fill in their default
650
+ // values here before validation. This lets users import normal details
651
+ // without exposing or requiring internal fields in the template.
652
+ for (const field of modelMetadataWithFields.fields) {
653
+ if (
654
+ field.required &&
655
+ field.defaultValue !== null &&
656
+ field.defaultValue !== undefined &&
657
+ !(field.name in dtoRecord)
658
+ ) {
659
+ dtoRecord[field.name] = this.parseImportDefaultValue(field);
660
+ }
661
+ }
662
+
646
663
  return dtoRecord;
647
664
  }
648
665
 
666
+ // Defaults come from metadata as text. Convert them to the type expected by
667
+ // validation, so values like "1" and "true" are handled as a number and a
668
+ // boolean instead of being treated as plain text.
669
+ private parseImportDefaultValue(field: FieldMetadata): any {
670
+ const defaultValue = field.defaultValue;
671
+
672
+ switch (field.type) {
673
+ case SolidFieldType.boolean:
674
+ return ['true', '1', 'yes'].includes(String(defaultValue).toLowerCase());
675
+ case SolidFieldType.int:
676
+ case SolidFieldType.bigint:
677
+ case SolidFieldType.decimal:
678
+ return Number(defaultValue);
679
+ default:
680
+ return defaultValue;
681
+ }
682
+ }
683
+
649
684
  private async populateDtoForACell(dtoRecord: Record<string, any>, fieldMetadata: FieldMetadata, record: Record<string, any>, key: string): Promise<Record<string, any>> {
650
685
  const fieldType = fieldMetadata.type;
651
686
  // const userKeyFieldName = userKeyField?.name || 'id'; // Default to 'id' if not found