UNPKG

@proventuslabs/nestjs-multipart-form

Version:

A lightweight and efficient NestJS package for handling multipart form data and file uploads with RxJS streaming support and type safety.

222 lines 7.78 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.associateFields = associateFields; exports.collectAssociatives = collectAssociatives; exports.collectToRecord = collectToRecord; exports.filterFieldsByPatterns = filterFieldsByPatterns; exports.validateRequiredFields = validateRequiredFields; const qs_1 = __importDefault(require("qs")); const rxjs_1 = require("rxjs"); const errors_1 = require("../core/errors"); /** * @internal * Checks if a field name matches a pattern. * * @param fieldName The actual field name from the multipart form * @param pattern The pattern to match against (may include ^ prefix for "starts with") * @returns True if the field matches the pattern */ function matchesFieldPattern(fieldName, pattern) { if (pattern.startsWith("^")) { return fieldName.startsWith(pattern.slice(1)); } return fieldName === pattern; } /** * @internal * Parses a field name to determine if it uses associative syntax. * Supports both single-level `field[key]` and nested `field[key1][key2]` syntax patterns. * * @param fieldname The field name to parse * @returns Parsed field name information with basename and associations, or undefined if not associative */ function parseAssociations(fieldname) { const len = fieldname.length; if (!len) return undefined; const associations = []; let basename = ""; let buffer = ""; let inBracket = false; for (let i = 0; i < len; i++) { const char = fieldname[i]; if (char === "[") { if (!inBracket) { if (!basename) basename = buffer; buffer = ""; inBracket = true; } else { // nested [ inside bracket → invalid return undefined; } } else if (char === "]") { if (!inBracket) return undefined; // unmatched ] associations.push(buffer); buffer = ""; inBracket = false; } else { buffer += char; } } if (inBracket) return undefined; // unclosed bracket if (!basename) basename = buffer; if (associations.length === 0) return undefined; return { basename, associations, }; } /** * RxJS operator that enriches multipart fields with associative syntax parsing. * * Transforms fields with associative syntax (e.g., "user[name]", "data[items][0]") * by adding parsed basename, associations array, and isAssociative flag. * Fields without associative syntax pass through unchanged. * * @returns RxJS operator function that transforms MultipartField observables * * @example * fields$.pipe( * associateFields() * ).subscribe(field => { * // For field name "user[name]" with value "John": * console.log(field.name); // "user[name]" * console.log(field.value); // "John" * console.log(field.isAssociative); // true * console.log(field.basename); // "user" * console.log(field.associations); // ["name"] * }); */ function associateFields() { return (source) => { return source.pipe((0, rxjs_1.map)((v) => { const parsed = parseAssociations(v.name); if (parsed) { const { basename, associations } = parsed; return { ...v, associations, basename, isAssociative: true, }; } return v; })); }; } /** * RxJS operator that collects associatives multipart fields into arrays. * Fields with array-like syntax (field[]) are collected into arrays while * fields with object-like syntax (field[name]) are collected into objects. * Uses `qs` under the hood. * * @example * fields$.pipe( * associateFields(), * collectAssociatives() * ).subscribe(fields => { * // For fields "name[first]=John" and "name[last]=Doe": * console.log(fields); // { "name": { "first": "John", "last": "Doe" } } * }); */ function collectAssociatives(options) { return (source) => { return source.pipe((0, rxjs_1.map)((v) => `${v.name}=${v.value}`), (0, rxjs_1.toArray)(), (0, rxjs_1.map)((q) => qs_1.default.parse(q.join("&"), options))); }; } /** * RxJS operator that converts multipart fields to a simple key-value record. * * @returns RxJS operator function that converts MultipartField observables to a record * * @example * fields$.pipe( * collectToRecord() * ).subscribe(record => { * console.log(record); // { name: "John", email: "john@example.com" } * }); */ function collectToRecord() { return (source) => { return source.pipe((0, rxjs_1.toArray)(), (0, rxjs_1.map)((fields) => Object.fromEntries(fields.map((field) => [field.name, field.value])))); }; } /** * RxJS operator that filters multipart fields by pattern matching. * * @param patterns Array of patterns to match against field names * @returns RxJS operator function that filters MultipartField observables * * @example * fields$.pipe( * filterFieldsByPatterns(['name', '^user_']) * ).subscribe(field => console.log('Matched field:', field.name)); */ function filterFieldsByPatterns(patterns) { return (source) => { return source.pipe((0, rxjs_1.filter)((field) => { // check if field matches any pattern return patterns.some((pattern) => matchesFieldPattern(field.name, pattern)); })); }; } /** * RxJS operator that validates required field patterns are present when stream completes. * * @param requiredPatterns Array of required field patterns * @returns RxJS operator function that validates MultipartField observables * * @example * fields$.pipe( * filterFieldsByPatterns(['name', '^user_', 'metadata']), * validateRequiredFields(['name', '^user_']) * ).subscribe(field => console.log('Valid field:', field.name)); */ function validateRequiredFields(requiredPatterns) { return (source) => { // early exit if no required patterns if (requiredPatterns.length === 0) return source; return new rxjs_1.Observable((subscriber) => { const remainingRequired = new Set(requiredPatterns); const subscription = source .pipe((0, rxjs_1.tap)((field) => { // track which required patterns have been matched and remove them for (const pattern of requiredPatterns) { if (remainingRequired.has(pattern) && matchesFieldPattern(field.name, pattern)) { remainingRequired.delete(pattern); // early exit if all requirements satisfied if (remainingRequired.size === 0) break; } } })) .subscribe({ next: (field) => subscriber.next(field), error: (err) => subscriber.error(err), complete: () => { // check for missing REQUIRED patterns when upstream completes if (remainingRequired.size > 0) { subscriber.error(new errors_1.MissingFieldsError(Array.from(remainingRequired))); } else { subscriber.complete(); } }, }); return () => subscription.unsubscribe(); }); }; } //# sourceMappingURL=operators.js.map