Skip to content

Commit

Permalink
feat(validators): added custom-collection-validators and @typedef for…
Browse files Browse the repository at this point in the history
… fileValidators and collectionValidators
  • Loading branch information
Egor Didenko committed May 29, 2024
1 parent bd436d2 commit 2741c59
Show file tree
Hide file tree
Showing 12 changed files with 104 additions and 110 deletions.
10 changes: 6 additions & 4 deletions abstract/UploaderBlock.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class UploaderBlock extends ActivityBlock {
});
this.$['*uploadCollection'] = uploadCollection;
}

//
if (!this.has('*validationManager')) {
this.add('*validationManager', new ValidationManager(this));
}
Expand Down Expand Up @@ -394,8 +394,10 @@ export class UploaderBlock extends ActivityBlock {
if (added.size || removed.size) {
this.$['*groupInfo'] = null;
}
this.validationManager._runFileValidators();
this.validationManager._runCollectionValidators();
if (this.validationManager) {
this.validationManager.runFileValidators();
this.validationManager.runCollectionValidators();
}

for (const entry of added) {
if (!entry.getValue('silent')) {
Expand Down Expand Up @@ -445,7 +447,7 @@ export class UploaderBlock extends ActivityBlock {
entriesToRunValidation.length > 0 &&
setTimeout(() => {
// We can't modify entry properties in the same tick, so we need to wait a bit
this.validationManager._runFileValidators(entriesToRunValidation);
if (this.validationManager) this.validationManager.runFileValidators(entriesToRunValidation);
});

if (changeMap.uploadProgress) {
Expand Down
59 changes: 28 additions & 31 deletions abstract/ValidationManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,35 @@ import {
} from '../utils/validators/file/index.js';
import { validateMultiple, validateCollectionUploadError } from '../utils/validators/collection/index.js';

/**
* @typedef {(
* outputEntry: import('../types').OutputFileEntry,
* internalEntry: import('./TypedData.js').TypedData,
* block: import('./UploaderBlock.js').UploaderBlock,
* ) => undefined | ReturnType<typeof import('../utils/buildOutputError.js').buildOutputFileError>} FuncFileValidator
*/

/**
* @typedef {(
* collection: import('./TypedCollection.js').TypedCollection,
* block: import('./UploaderBlock.js').UploaderBlock,
* ) =>
* | undefined
* | ReturnType<typeof import('../utils/buildOutputError.js').buildCollectionFileError>
* | ReturnType<typeof import('../utils/buildOutputError.js').buildCollectionFileError>[]} FuncCollectionValidator
*/

export class ValidationManager {
/**
* @private
* @type {import('./UploaderBlock.js').UploaderBlock}
*/
_blockInstance;

/**
* @private
* @type {((
* outputEntry: import('../types').OutputFileEntry,
* internalEntry: import('./TypedData.js').TypedData,
* block: import('./UploaderBlock.js').UploaderBlock,
* ) => undefined | ReturnType<typeof import('../utils/buildOutputError.js').buildOutputFileError>)[]}
*/
/** @type FuncFileValidator[] */
_fileValidators = [validateIsImage, validateFileType, validateMaxSizeLimit, validateUploadError];

/**
* @private
* @type {((
* collection: import('./TypedCollection.js').TypedCollection,
* block: import('./UploaderBlock.js').UploaderBlock,
* ) =>
* | undefined
* | ReturnType<typeof import('../utils/buildOutputError.js').buildCollectionFileError>
* | ReturnType<typeof import('../utils/buildOutputError.js').buildCollectionFileError>[])[]}
*/
/** @type FuncCollectionValidator[] */
_collectionValidators = [validateMultiple, validateCollectionUploadError];

/** @param {import('./UploaderBlock.js').UploaderBlock} blockInstance */
Expand All @@ -44,8 +46,8 @@ export class ValidationManager {
this._uploadCollection = this._blockInstance.uploadCollection;

const runAllValidators = () => {
this._runFileValidators();
this._runCollectionValidators();
this.runFileValidators();
this.runCollectionValidators();
};

this._blockInstance.subConfigValue('maxLocalFileSizeBytes', runAllValidators);
Expand All @@ -56,11 +58,8 @@ export class ValidationManager {
this._blockInstance.subConfigValue('accept', runAllValidators);
}

/**
* @private
* @param {string[]} [entryIds]
*/
_runFileValidators(entryIds) {
/** @param {string[]} [entryIds] */
runFileValidators(entryIds) {
const ids = entryIds ?? this._uploadCollection.items();
for (const id of ids) {
const entry = this._uploadCollection.read(id);
Expand All @@ -71,12 +70,11 @@ export class ValidationManager {
}
}

/** @private */
_runCollectionValidators() {
runCollectionValidators() {
const collection = this._uploadCollection;
const errors = [];

for (const validator of this._collectionValidators) {
for (const validator of [...this._collectionValidators, ...(this._blockInstance.cfg.collectionValidators ?? [])]) {
const errorOrErrors = validator(collection, this._blockInstance);
if (!errorOrErrors) {
continue;
Expand Down Expand Up @@ -115,18 +113,17 @@ export class ValidationManager {
* @param {import('./TypedData.js').TypedData} entry
*/
_runCustomValidatorsEntry(entry) {
this._commonValidation(entry, this._blockInstance.cfg.validators);
this._commonValidation(entry, this._blockInstance.cfg.fileValidators ?? []);
}

/**
* @private
* @param {import('./TypedData.js').TypedData} entry
* @param {?} validators
* @param {FuncFileValidator[]} validators
*/
_commonValidation(entry, validators) {
const outputEntry = this._blockInstance.getOutputItem(entry.uid);
const errors = [];

for (const validator of validators) {
const error = validator(outputEntry, entry, this._blockInstance);
if (error) {
Expand Down
6 changes: 4 additions & 2 deletions blocks/Config/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ const allConfigKeys = /** @type {(keyof import('../../types').ConfigType)[]} */
* 'secureUploadsSignatureResolver',
* 'secureDeliveryProxyUrlResolver',
* 'iconHrefResolver',
* 'validators'
* 'fileValidators',
* 'collectionValidators'
* ]}
*/
export const complexConfigKeys = [
Expand All @@ -28,7 +29,8 @@ export const complexConfigKeys = [
'secureUploadsSignatureResolver',
'secureDeliveryProxyUrlResolver',
'iconHrefResolver',
'validators'
'fileValidators',
'collectionValidators'
];

/** @type {(key: keyof import('../../types').ConfigType) => key is keyof import('../../types').ConfigComplexType} */
Expand Down
3 changes: 2 additions & 1 deletion blocks/Config/initialConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,6 @@ export const initialConfig = {
secureUploadsSignatureResolver: null,
secureDeliveryProxyUrlResolver: null,
iconHrefResolver: null,
validators: null,
fileValidators: [],
collectionValidators: [],
};
11 changes: 8 additions & 3 deletions blocks/Config/normalizeConfigValue.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,14 @@ const asFunction = (value) => {
throw new Error('Invalid value. Must be a function.');
};

/** @param {unknown} value */
/**
* @template {Array<T>}T
* @param {unknown} value
* @returns {T}
*/
const asValidators = (value) => {
if (Array.isArray(value)) {
return value;
return /** @type {T} */ (value);
}

throw new Error('Invalid validators value. Must be an array.');
Expand Down Expand Up @@ -149,7 +153,8 @@ const mapping = {
secureDeliveryProxyUrlResolver:
/** @type {typeof asFunction<import('../../types').SecureDeliveryProxyUrlResolver>} */ (asFunction),
iconHrefResolver: /** @type {typeof asFunction<import('../../types').IconHrefResolver>} */ (asFunction),
validators: asValidators,
fileValidators: /** @type {typeof asValidators<import('../../types').FileValidator[]>} */ (asValidators),
collectionValidators: /** @type {typeof asValidators<import('../../types').CollectionValidator[]>} */ (asValidators),
};

/**
Expand Down
94 changes: 49 additions & 45 deletions types/exported.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { LocaleDefinition } from '../abstract/localeRegistry';
import { complexConfigKeys } from '../blocks/Config/Config';
import { FuncFileValidator, FuncCollectionValidator } from '../abstract/ValidationManager';

export type UploadError = import('@uploadcare/upload-client').UploadError;
export type UploadcareFile = import('@uploadcare/upload-client').UploadcareFile;
Expand All @@ -15,6 +16,8 @@ export type SecureDeliveryProxyUrlResolver = (
export type SecureUploadsSignatureAndExpire = { secureSignature: string; secureExpire: string };
export type SecureUploadsSignatureResolver = () => Promise<SecureUploadsSignatureAndExpire | null>;
export type IconHrefResolver = (iconName: string) => string;
export type FileValidator = FuncFileValidator | any;
export type CollectionValidator = FuncCollectionValidator | any;

export type ConfigType = {
pubkey: string;
Expand Down Expand Up @@ -68,17 +71,18 @@ export type ConfigType = {
secureUploadsSignatureResolver: SecureUploadsSignatureResolver | null;
secureDeliveryProxyUrlResolver: SecureDeliveryProxyUrlResolver | null;
iconHrefResolver: IconHrefResolver | null;
validators: string | null | [];

fileValidators: FileValidator[] | null;
collectionValidators: CollectionValidator[] | null;
};
export type ConfigComplexType = Pick<ConfigType, (typeof complexConfigKeys)[number]>;
export type ConfigPlainType = Omit<ConfigType, keyof ConfigComplexType>;
export type ConfigAttributesType = KebabCaseKeys<ConfigPlainType> & LowerCaseKeys<ConfigPlainType>;

export type KebabCase<S extends string> = S extends `${infer C}${infer T}`
? T extends Uncapitalize<T>
? `${Uncapitalize<C>}${KebabCase<T>}`
: `${Uncapitalize<C>}-${KebabCase<T>}`
? `${Uncapitalize<C>}${KebabCase<T>}`
: `${Uncapitalize<C>}-${KebabCase<T>}`
: S;
export type KebabCaseKeys<T extends Record<string, unknown>> = { [Key in keyof T as KebabCase<Key & string>]: T[Key] };
export type LowerCase<S extends string> = Lowercase<S>;
Expand Down Expand Up @@ -128,11 +132,11 @@ export type OutputErrorTypePayload = {

export type OutputError<T extends OutputFileErrorType | OutputCollectionErrorType> =
T extends keyof OutputErrorTypePayload
? {
type: T;
message: string;
} & OutputErrorTypePayload[T]
: never;
? {
type: T;
message: string;
} & OutputErrorTypePayload[T]
: never;

export type OutputFileEntry<TStatus extends OutputFileStatus = OutputFileStatus> = {
status: TStatus;
Expand All @@ -149,7 +153,7 @@ export type OutputFileEntry<TStatus extends OutputFileStatus = OutputFileStatus>
uploadProgress: number;
fullPath: string | null;
} & (
| {
| {
status: 'success';
fileInfo: UploadcareFile;
uuid: string;
Expand All @@ -161,7 +165,7 @@ export type OutputFileEntry<TStatus extends OutputFileStatus = OutputFileStatus>
isRemoved: false;
errors: [];
}
| {
| {
status: 'failed';
fileInfo: UploadcareFile | null;
uuid: string | null;
Expand All @@ -173,7 +177,7 @@ export type OutputFileEntry<TStatus extends OutputFileStatus = OutputFileStatus>
isRemoved: false;
errors: OutputError<OutputFileErrorType>[];
}
| {
| {
status: 'uploading';
fileInfo: null;
uuid: null;
Expand All @@ -185,7 +189,7 @@ export type OutputFileEntry<TStatus extends OutputFileStatus = OutputFileStatus>
isRemoved: false;
errors: [];
}
| {
| {
status: 'removed';
fileInfo: UploadcareFile | null;
uuid: string | null;
Expand All @@ -197,7 +201,7 @@ export type OutputFileEntry<TStatus extends OutputFileStatus = OutputFileStatus>
isRemoved: true;
errors: OutputError<OutputFileErrorType>[];
}
| {
| {
status: 'idle';
fileInfo: null;
uuid: null;
Expand All @@ -209,7 +213,7 @@ export type OutputFileEntry<TStatus extends OutputFileStatus = OutputFileStatus>
isRemoved: false;
errors: [];
}
);
);

export type OutputCollectionStatus = 'idle' | 'uploading' | 'success' | 'failed';

Expand All @@ -233,43 +237,43 @@ export type OutputCollectionState<
} & (TGroupFlag extends 'has-group'
? { group: UploadcareGroup }
: TGroupFlag extends 'maybe-has-group'
? { group: UploadcareGroup | null }
: never) &
? { group: UploadcareGroup | null }
: never) &
(
| {
status: 'idle';
isFailed: false;
isUploading: false;
isSuccess: false;
errors: [];
allEntries: OutputFileEntry<'idle' | 'success'>[];
}
status: 'idle';
isFailed: false;
isUploading: false;
isSuccess: false;
errors: [];
allEntries: OutputFileEntry<'idle' | 'success'>[];
}
| {
status: 'uploading';
isFailed: false;
isUploading: true;
isSuccess: false;
errors: [];
allEntries: OutputFileEntry[];
}
status: 'uploading';
isFailed: false;
isUploading: true;
isSuccess: false;
errors: [];
allEntries: OutputFileEntry[];
}
| {
status: 'success';
isFailed: false;
isUploading: false;
isSuccess: true;
errors: [];
allEntries: OutputFileEntry<'success'>[];
}
status: 'success';
isFailed: false;
isUploading: false;
isSuccess: true;
errors: [];
allEntries: OutputFileEntry<'success'>[];
}
| {
status: 'failed';
isFailed: true;
isUploading: false;
isSuccess: false;
errors: OutputError<OutputCollectionErrorType>[];
allEntries: OutputFileEntry[];
}
status: 'failed';
isFailed: true;
isUploading: false;
isSuccess: false;
errors: OutputError<OutputCollectionErrorType>[];
allEntries: OutputFileEntry[];
}
);

export { EventType, EventPayload } from '../blocks/UploadCtxProvider/EventEmitter';

export { };
export {};
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { buildCollectionFileError } from '../../buildOutputError.js';

/** @type import('../../../abstract/ValidationManager.js').FuncCollectionValidator */
export const validateCollectionUploadError = (collection, block) => {
if (collection.items().some((id) => collection.readProp(id, 'errors').length > 0)) {
return buildCollectionFileError({
Expand Down
1 change: 1 addition & 0 deletions utils/validators/collection/validateMultiple.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { buildCollectionFileError } from '../../buildOutputError.js';

/** @type import('../../../abstract/ValidationManager.js').FuncCollectionValidator */
export const validateMultiple = (collection, block) => {
const total = collection.size;
const multipleMin = block.cfg.multiple ? block.cfg.multipleMin : 0;
Expand Down
Loading

0 comments on commit 2741c59

Please sign in to comment.