@warlock.js/core 5.6.0 → 5.7.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/CHANGELOG.md +18 -0
- package/esm/benchmark/profiler.d.mts.map +1 -1
- package/esm/benchmark/profiler.mjs +4 -3
- package/esm/benchmark/profiler.mjs.map +1 -1
- package/esm/cli/cli-commands.manager.mjs +10 -7
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/commands/routes/format-routes-table.mjs +3 -3
- package/esm/cli/commands/routes/format-routes-table.mjs.map +1 -1
- package/esm/cli/parse-cli-args.mjs +1 -0
- package/esm/cli/parse-cli-args.mjs.map +1 -1
- package/esm/cli/string-similarity.mjs +13 -4
- package/esm/cli/string-similarity.mjs.map +1 -1
- package/esm/commands/cli-command.d.mts.map +1 -1
- package/esm/commands/cli-command.mjs +5 -2
- package/esm/commands/cli-command.mjs.map +1 -1
- package/esm/config/config-loader.mjs +3 -1
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/dev-server/dependency-graph.d.mts.map +1 -1
- package/esm/dev-server/dependency-graph.mjs +2 -1
- package/esm/dev-server/dependency-graph.mjs.map +1 -1
- package/esm/dev-server/dev-logger.mjs +7 -5
- package/esm/dev-server/dev-logger.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +1 -1
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/parse-imports.mjs +22 -8
- package/esm/dev-server/parse-imports.mjs.map +1 -1
- package/esm/encryption/encrypt.mjs +1 -0
- package/esm/encryption/encrypt.mjs.map +1 -1
- package/esm/generations/add-command.action.mjs +8 -2
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/features/shared/relocate-conflicting-home-route.mjs +6 -1
- package/esm/generations/features/shared/relocate-conflicting-home-route.mjs.map +1 -1
- package/esm/http/middleware/maintenance.middleware.mjs +1 -2
- package/esm/http/middleware/maintenance.middleware.mjs.map +1 -1
- package/esm/http/middleware/utils/cidr-match.d.mts.map +1 -1
- package/esm/http/middleware/utils/cidr-match.mjs +1 -0
- package/esm/http/middleware/utils/cidr-match.mjs.map +1 -1
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +14 -7
- package/esm/http/request.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/filter-applicator.mjs +4 -2
- package/esm/repositories/adapters/cascade/filter-applicator.mjs.map +1 -1
- package/esm/router/positional-handler-diagnostics.d.mts.map +1 -1
- package/esm/router/positional-handler-diagnostics.mjs +9 -3
- package/esm/router/positional-handler-diagnostics.mjs.map +1 -1
- package/esm/router/route-registry.mjs +1 -1
- package/esm/router/route-registry.mjs.map +1 -1
- package/esm/storage/scoped-storage.d.mts.map +1 -1
- package/esm/storage/scoped-storage.mjs +1 -0
- package/esm/storage/scoped-storage.mjs.map +1 -1
- package/esm/storage/storage.d.mts.map +1 -1
- package/esm/storage/storage.mjs +6 -2
- package/esm/storage/storage.mjs.map +1 -1
- package/esm/storage/utils/safe-fetch.d.mts.map +1 -1
- package/esm/storage/utils/safe-fetch.mjs +6 -1
- package/esm/storage/utils/safe-fetch.mjs.map +1 -1
- package/esm/utils/version-compare.d.mts.map +1 -1
- package/esm/utils/version-compare.mjs +12 -4
- package/esm/utils/version-compare.mjs.map +1 -1
- package/esm/vite/lower-stage3-decorators.mjs +1 -0
- package/esm/vite/lower-stage3-decorators.mjs.map +1 -1
- package/package.json +12 -12
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filter-applicator.mjs","names":[],"sources":["../../../../../../../../../core/src/repositories/adapters/cascade/filter-applicator.ts"],"sourcesContent":["import type {\n QueryBuilderContract as CascadeQueryBuilder,\n QueryBuilderContract,\n} from \"@warlock.js/cascade\";\nimport type { FilterOptions, FilterRule, FilterRules } from \"../../contracts\";\n\n/**\n * Applies repository filters to a Cascade-Next query builder\n * Translates repository filter rules into Cascade query builder method calls\n */\nexport class FilterApplicator {\n /**\n * Apply filters to a Cascade query builder\n *\n * @param query - Cascade query builder instance\n * @param filters - Filter structure defining how to filter\n * @param data - Data containing filter values\n * @param options - Additional filter options (date formats, etc.)\n */\n public apply(\n query: CascadeQueryBuilder<any>,\n filters: FilterRules,\n data: any,\n options: FilterOptions,\n ): void {\n for (const key in filters) {\n const value = data[key];\n if (value === undefined) continue;\n\n const rule = this.parseFilterRule(key, filters[key]);\n this.applyFilterRule(query, rule, value, data, options);\n }\n }\n\n /**\n * Parse a filter rule into a structured format\n */\n private parseFilterRule(key: string, rule: FilterRule) {\n // Handle custom function\n if (typeof rule === \"function\") {\n return { type: \"function\", fn: rule, column: key, key };\n }\n\n // Handle array format: [\"operator\"] or [\"operator\", \"column\"] or [\"operator\", [\"col1\", \"col2\"]]\n if (Array.isArray(rule)) {\n const [operator, target] = rule;\n\n if (target === undefined) {\n return { type: operator, column: key, columns: undefined, key };\n }\n\n if (Array.isArray(target)) {\n return { type: operator, column: undefined, columns: target, key };\n }\n\n return { type: operator, column: target, columns: undefined, key };\n }\n\n // Handle simple operator string\n return { type: rule, column: key, columns: undefined, key };\n }\n\n /**\n * Apply a single filter rule to the query\n */\n private applyFilterRule(\n query: CascadeQueryBuilder<any>,\n rule: any,\n value: any,\n data: any,\n options: FilterOptions,\n ): void {\n // 1. Custom function filter\n if (rule.type === \"function\") {\n rule.fn(value, query, data);\n return;\n }\n\n // 2. Predefined filter types\n const handler = this.getFilterHandler(rule.type);\n if (handler) {\n handler.call(this, query, rule.column, rule.columns, value, options);\n return;\n }\n\n // 3. Standard where operators\n this.applyWhereOperator(query, rule.type, rule.column, rule.columns, value, rule.key);\n }\n\n /**\n * Get filter handler for predefined types\n */\n private getFilterHandler(type: string): Function | undefined {\n const handlers: Record<string, Function> = {\n // Boolean filters\n bool: this.handleBoolean,\n boolean: this.handleBoolean,\n\n // Numeric filters\n int: this.handleInt,\n integer: this.handleInt,\n \"!int\": this.handleNotInt,\n \"int>\": (q: any, col: any, cols: any, val: any) =>\n this.handleIntComparison(q, col, cols, val, \">\"),\n \"int>=\": (q: any, col: any, cols: any, val: any) =>\n this.handleIntComparison(q, col, cols, val, \">=\"),\n \"int<\": (q: any, col: any, cols: any, val: any) =>\n this.handleIntComparison(q, col, cols, val, \"<\"),\n \"int<=\": (q: any, col: any, cols: any, val: any) =>\n this.handleIntComparison(q, col, cols, val, \"<=\"),\n inInt: this.handleInInt,\n number: this.handleNumber,\n inNumber: this.handleInNumber,\n float: this.handleFloat,\n double: this.handleFloat,\n inFloat: this.handleInNumber,\n\n // Null filters\n null: this.handleNull,\n notNull: this.handleNotNull,\n \"!null\": this.handleNotNull,\n\n // Date filters\n date: this.handleDate,\n \"date>\": (q: any, col: any, cols: any, val: any, opts: any) =>\n this.handleDateComparison(q, col, cols, val, opts, \">\"),\n \"date>=\": (q: any, col: any, cols: any, val: any, opts: any) =>\n this.handleDateComparison(q, col, cols, val, opts, \">=\"),\n \"date<\": (q: any, col: any, cols: any, val: any, opts: any) =>\n this.handleDateComparison(q, col, cols, val, opts, \"<\"),\n \"date<=\": (q: any, col: any, cols: any, val: any, opts: any) =>\n this.handleDateComparison(q, col, cols, val, opts, \"<=\"),\n dateBetween: this.handleDateBetween,\n inDate: this.handleInDate,\n\n // DateTime filters\n dateTime: this.handleDateTime,\n \"dateTime>\": (q: any, col: any, cols: any, val: any, opts: any) =>\n this.handleDateTimeComparison(q, col, cols, val, opts, \">\"),\n \"dateTime>=\": (q: any, col: any, cols: any, val: any, opts: any) =>\n this.handleDateTimeComparison(q, col, cols, val, opts, \">=\"),\n \"dateTime<\": (q: any, col: any, cols: any, val: any, opts: any) =>\n this.handleDateTimeComparison(q, col, cols, val, opts, \"<\"),\n \"dateTime<=\": (q: any, col: any, cols: any, val: any, opts: any) =>\n this.handleDateTimeComparison(q, col, cols, val, opts, \"<=\"),\n dateTimeBetween: this.handleDateTimeBetween,\n inDateTime: this.handleInDateTime,\n\n // Scope filter - applies local scope when value is truthy\n scope: this.handleScope,\n\n // With filter - eager-loads a relation when value is truthy\n with: this.handleWith,\n\n // JoinWith filter - eager-loads a relation via SQL JOIN when value is truthy\n joinWith: this.handleJoinWith,\n\n // Vector similarity search — calls similarTo(column, embedding[])\n similarTo: this.handleSimilarTo,\n };\n\n return handlers[type];\n }\n\n /**\n * Apply standard where operators\n */\n private applyWhereOperator(\n query: QueryBuilderContract,\n operator: string,\n column?: string,\n columns?: string[],\n value?: any,\n ruleName?: string,\n ): void {\n // Handle \"in\" prefix for array values\n if (operator.startsWith(\"in\") && operator !== \"int\" && !Array.isArray(value)) {\n value = [value];\n }\n\n // Single column\n if (column) {\n switch (operator) {\n case \"=\":\n query.where(column, value);\n break;\n case \"!=\":\n case \"<>\":\n query.where(column, \"!=\", value);\n break;\n case \">\":\n case \">=\":\n case \"<\":\n case \"<=\":\n query.where(column, operator, value);\n break;\n case \"in\":\n query.whereIn(column, Array.isArray(value) ? value : [value]);\n break;\n case \"not in\":\n query.whereNotIn(column, Array.isArray(value) ? value : [value]);\n break;\n case \"like\":\n query.whereLike(column, value);\n break;\n case \"not like\":\n query.whereNotLike(column, value);\n break;\n case \"between\":\n query.whereBetween(column, value);\n break;\n case \"not between\":\n query.whereNotBetween(column, value);\n break;\n }\n }\n // Multiple columns (OR condition)\n else if (columns) {\n if (operator === \"=\") {\n const conditions: any = {};\n for (const col of columns) {\n conditions[col] = value;\n }\n query.orWhere(conditions);\n } else if (operator === \"like\") {\n // (col0 LIKE value OR col1 LIKE value OR ...), OR'd with prior conditions —\n // the object form of orWhere() only ever emits \"=\", so \"like\" needs the\n // callback/group form to keep the operator instead of silently degrading\n // to equality (D1).\n query.orWhere((sub: QueryBuilderContract) => {\n columns.forEach((col, index) => {\n if (index === 0) {\n sub.where(col, \"like\", value);\n } else {\n sub.orWhere(col, \"like\", value);\n }\n });\n });\n } else {\n // Every other operator silently degraded to equality via the object form\n // of orWhere() — refuse instead of returning a correct-looking, wrong query.\n throw new Error(\n `Unsupported multi-column filter operator \"${operator}\" for rule \"${ruleName ?? columns.join(\",\")}\". ` +\n `Multi-column filters only support \"=\" and \"like\".`,\n );\n }\n }\n }\n\n // ============================================================================\n // BOOLEAN FILTERS\n // ============================================================================\n\n /**\n * Coerce a filter value to a boolean.\n *\n * Recognized truthy forms: `true`, `1`, `\"true\"`, `\"1\"`.\n * Recognized falsy forms: `false`, `0`, `\"false\"`, `\"0\"`.\n * Anything else falls back to `Boolean(value)`.\n *\n * NOTE: this intentionally does NOT treat \"non-empty\" as true — the previous\n * `|| !isEmpty(value)` fallback coerced `false`/`0` to `true`, inverting the\n * filter for explicitly-false values.\n */\n private coerceBoolean(value: any): boolean {\n if (value === true || value === 1 || value === \"true\" || value === \"1\") {\n return true;\n }\n\n if (value === false || value === 0 || value === \"false\" || value === \"0\") {\n return false;\n }\n\n return Boolean(value);\n }\n\n private handleBoolean(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n ) {\n const boolValue = this.coerceBoolean(value);\n if (column) {\n query.where(column, boolValue);\n } else if (columns) {\n const conditions: any = {};\n for (const col of columns) {\n conditions[col] = boolValue;\n }\n\n query.orWhere(conditions);\n }\n }\n\n // ============================================================================\n // NUMERIC FILTERS\n // ============================================================================\n private handleInt(query: QueryBuilderContract, column?: string, columns?: string[], value?: any) {\n const intValue = parseInt(value);\n if (column) {\n query.where(column, intValue);\n } else if (columns) {\n const conditions: any = {};\n for (const col of columns) {\n conditions[col] = intValue;\n }\n\n query.orWhere(conditions);\n }\n }\n\n private handleNotInt(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n ) {\n const intValue = parseInt(value);\n if (column) {\n query.where(column, \"!=\", intValue);\n } else if (columns) {\n // Use multiple orWhere calls for OR logic across columns\n for (const col of columns) {\n query.orWhere(col, \"!=\", intValue);\n }\n }\n }\n\n private handleIntComparison(\n query: QueryBuilderContract,\n column: any,\n columns: any,\n value: any,\n operator: string,\n ) {\n const intValue = parseInt(value);\n if (column) {\n query.where(column, operator, intValue);\n } else if (columns) {\n for (const col of columns) {\n query.orWhere(col, operator, intValue);\n }\n }\n }\n\n private handleInInt(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n ) {\n const values = (Array.isArray(value) ? value : [value]).map((v: any) => parseInt(v));\n if (column) {\n query.whereIn(column, values);\n } else if (columns) {\n // Use multiple orWhere calls with whereIn for each column\n for (const col of columns) {\n query.orWhere((q: any) => q.whereIn(col, values));\n }\n }\n }\n\n private handleNumber(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n ) {\n const numValue = Number(value);\n if (column) {\n query.where(column, numValue);\n } else if (columns) {\n const conditions: any = {};\n for (const col of columns) {\n conditions[col] = numValue;\n }\n\n query.orWhere(conditions);\n }\n }\n\n private handleInNumber(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n ) {\n const values = (Array.isArray(value) ? value : [value]).map((v: any) => Number(v));\n if (column) {\n query.whereIn(column, values);\n } else if (columns) {\n // Use multiple orWhere calls with whereIn for each column\n for (const col of columns) {\n query.orWhere((q: any) => q.whereIn(col, values));\n }\n }\n }\n\n private handleFloat(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n ) {\n const floatValue = parseFloat(value);\n if (column) {\n query.where(column, floatValue);\n } else if (columns) {\n const conditions: any = {};\n for (const col of columns) {\n conditions[col] = floatValue;\n }\n\n query.orWhere(conditions);\n }\n }\n\n // ============================================================================\n // NULL FILTERS\n // ============================================================================\n\n private handleNull(query: QueryBuilderContract, column?: string, columns?: string[]) {\n if (column) {\n query.whereNull(column);\n } else if (columns) {\n for (const col of columns) {\n query.orWhere({ [col]: null });\n }\n }\n }\n\n private handleNotNull(query: QueryBuilderContract, column?: string, columns?: string[]) {\n if (column) {\n query.whereNotNull(column);\n } else if (columns) {\n // Use whereNotNull for each column\n for (const col of columns) {\n query.orWhere((q: any) => q.whereNotNull(col));\n }\n }\n }\n\n // ============================================================================\n // SCOPE FILTER\n // ============================================================================\n\n /**\n * Handle scope filter - applies local scope and passes the filter value.\n *\n * Usage in filterBy:\n * ```typescript\n * filterBy: {\n * active: \"scope\", // Uses the filter key as scope name\n * isAdmin: [\"scope\", \"admin\"] // Uses custom scope name\n * }\n * ```\n *\n * When list({ active: true }) is called, it will call query.scope(\"active\", true)\n * When list({ status: \"pending\" }) is called, it will call query.scope(\"status\", \"pending\")\n */\n private handleScope(\n query: QueryBuilderContract,\n column?: string,\n _columns?: string[],\n value?: any,\n ) {\n // column holds the scope name (either the filter key or custom name from array format)\n if (column) {\n query.scope(column, value);\n }\n }\n\n // ============================================================================\n // WITH (EAGER LOAD) FILTER\n // ============================================================================\n\n /**\n * Handle with filter - eager-loads a relation when the filter value is truthy.\n *\n * Usage in filterBy:\n * ```typescript\n * filterBy: {\n * with_ai_model: [\"with\", \"ai_model\"], // load single relation\n * with_all: [\"with\", [\"ai_model\", \"unit\"]] // load multiple relations\n * }\n * ```\n *\n * When list({ with_ai_model: true }) is called, it will call query.with(\"ai_model\")\n */\n private handleWith(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n ) {\n if (!value) return;\n\n // Load a single named relation\n if (column) {\n if (query.with) {\n query.with(column);\n }\n return;\n }\n\n // Load multiple relations from the columns array\n if (columns) {\n for (const relation of columns) {\n if (query.with) {\n query.with(relation);\n }\n }\n }\n }\n\n /**\n * Handle joinWith filter - eager-loads a relation via SQL JOIN when the filter value is truthy.\n *\n * Usage in filterBy:\n * ```typescript\n * filterBy: {\n * with_ai_model: [\"joinWith\", \"ai_model\"], // load single relation via join\n * with_all: [\"joinWith\", [\"ai_model\", \"unit\"]] // load multiple relations via join\n * }\n * ```\n *\n * When list({ with_ai_model: true }) is called, it will call query.joinWith(\"ai_model\")\n */\n private handleJoinWith(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n ) {\n if (!value) return;\n\n if (!query.joinWith) {\n console.warn(\n \"[Repository] joinWith is not supported by the query builder. using with instead.\",\n );\n return this.handleWith(query, column, columns, value);\n }\n\n // Load a single named relation\n if (column) {\n query.joinWith(column);\n return;\n }\n\n // Load multiple relations from the columns array\n if (columns) {\n query.joinWith(...columns);\n }\n }\n\n // ============================================================================\n // VECTOR FILTER\n // ============================================================================\n\n /**\n * Handle similarTo filter — performs vector similarity search.\n *\n * The filter value must be a `number[]` (pre-computed embedding).\n * Delegates to `query.similarTo(column, embedding)` which is handled\n * driver-specifically (pgvector or MongoDB Atlas $vectorSearch).\n *\n * Usage in filterBy:\n * ```typescript\n * filterBy: {\n * organization_id: \"=\",\n * embedding: \"similarTo\",\n * }\n * ```\n *\n * Then in the service:\n * ```typescript\n * await vectorsRepository.list({ embedding: queryEmbedding, organization_id: orgId });\n * ```\n */\n private handleSimilarTo(\n query: QueryBuilderContract,\n column?: string,\n _columns?: string[],\n value?: any,\n ) {\n if (!column || !Array.isArray(value)) return;\n\n // Cast to any: Cascade's internal QueryBuilderContract still uses nearestTo;\n // our wrapper (CascadeQueryBuilder.similarTo) delegates to it correctly.\n (query as any).similarTo(column, value);\n }\n\n // ============================================================================\n // DATE FILTERS\n // ============================================================================\n\n private handleDate(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n options?: FilterOptions,\n ) {\n const dateValue = this.parseDate(value, options?.dateFormat);\n if (column) {\n query.whereDate(column, dateValue);\n } else if (columns) {\n for (const col of columns) {\n query.orWhere((q: any) => q.whereDate(col, dateValue));\n }\n }\n }\n\n private handleDateComparison(\n query: QueryBuilderContract,\n column: any,\n columns: any,\n value: any,\n options: any,\n operator: string,\n ) {\n const dateValue = this.parseDate(value, options?.dateFormat);\n if (column) {\n if (operator === \">\" || operator === \">=\") {\n query.whereDateAfter(column, dateValue);\n } else {\n query.whereDateBefore(column, dateValue);\n }\n } else if (columns) {\n for (const col of columns) {\n if (operator === \">\" || operator === \">=\") {\n query.orWhere((q: any) => q.whereDateAfter(col, dateValue));\n } else {\n query.orWhere((q: any) => q.whereDateBefore(col, dateValue));\n }\n }\n }\n }\n\n private handleDateBetween(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n options?: FilterOptions,\n ) {\n if (!Array.isArray(value) || value.length !== 2) return;\n const [start, end] = value.map((v: any) => this.parseDate(v, options?.dateFormat));\n if (column) {\n query.whereDateBetween(column, [start, end]);\n } else if (columns) {\n for (const col of columns) {\n query.orWhere((q: any) => q.whereDateBetween(col, [start, end]));\n }\n }\n }\n\n private handleInDate(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n options?: FilterOptions,\n ) {\n const dates = (Array.isArray(value) ? value : [value]).map((v: any) =>\n this.parseDate(v, options?.dateFormat),\n );\n if (column) {\n query.whereIn(column, dates);\n } else if (columns) {\n // Use multiple orWhere calls with whereIn for each column\n for (const col of columns) {\n query.orWhere((q: any) => q.whereIn(col, dates));\n }\n }\n }\n\n // ============================================================================\n // DATETIME FILTERS\n // ============================================================================\n\n private handleDateTime(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n options?: FilterOptions,\n ) {\n const dateValue = this.parseDateTime(value, options?.dateTimeFormat);\n if (column) {\n query.where(column, dateValue);\n } else if (columns) {\n const conditions: any = {};\n for (const col of columns) {\n conditions[col] = dateValue;\n }\n\n query.orWhere(conditions);\n }\n }\n\n private handleDateTimeComparison(\n query: QueryBuilderContract,\n column: any,\n columns: any,\n value: any,\n options: any,\n operator: string,\n ) {\n const dateValue = this.parseDateTime(value, options?.dateTimeFormat);\n if (column) {\n query.where(column, operator, dateValue);\n } else if (columns) {\n for (const col of columns) {\n query.orWhere(col, operator, dateValue);\n }\n }\n }\n\n private handleDateTimeBetween(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n options?: FilterOptions,\n ) {\n if (!Array.isArray(value) || value.length !== 2) return;\n const [start, end] = value.map((v: any) => this.parseDateTime(v, options?.dateTimeFormat));\n if (column) {\n query.whereBetween(column, [start, end]);\n } else if (columns) {\n for (const col of columns) {\n query.orWhere((q: any) => q.whereBetween(col, [start, end]));\n }\n }\n }\n\n private handleInDateTime(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n options?: FilterOptions,\n ) {\n const dates = (Array.isArray(value) ? value : [value]).map((v: any) =>\n this.parseDateTime(v, options?.dateTimeFormat),\n );\n if (column) {\n query.whereIn(column, dates);\n } else if (columns) {\n // Use multiple orWhere calls with whereIn for each column\n for (const col of columns) {\n query.orWhere((q: any) => q.whereIn(col, dates));\n }\n }\n }\n\n // ============================================================================\n // DATE PARSING UTILITIES\n // ============================================================================\n\n /**\n * Parse date string to Date object\n * TODO: Implement proper date parsing with format support\n */\n private parseDate(value: any, format?: string): Date {\n if (value instanceof Date) return value;\n return new Date(value);\n }\n\n /**\n * Parse datetime string to Date object\n * TODO: Implement proper datetime parsing with format support\n */\n private parseDateTime(value: any, format?: string): Date {\n if (value instanceof Date) return value;\n return new Date(value);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"filter-applicator.mjs","names":[],"sources":["../../../../../../../../../core/src/repositories/adapters/cascade/filter-applicator.ts"],"sourcesContent":["import type {\n QueryBuilderContract as CascadeQueryBuilder,\n QueryBuilderContract,\n} from \"@warlock.js/cascade\";\nimport type { FilterOptions, FilterRule, FilterRules } from \"../../contracts\";\n\n/**\n * Applies repository filters to a Cascade-Next query builder\n * Translates repository filter rules into Cascade query builder method calls\n */\nexport class FilterApplicator {\n /**\n * Apply filters to a Cascade query builder\n *\n * @param query - Cascade query builder instance\n * @param filters - Filter structure defining how to filter\n * @param data - Data containing filter values\n * @param options - Additional filter options (date formats, etc.)\n */\n public apply(\n query: CascadeQueryBuilder<any>,\n filters: FilterRules,\n data: any,\n options: FilterOptions,\n ): void {\n // `Object.entries` hands the rule over already narrowed; a `for...in` index\n // read is `FilterRule | undefined` under the strictness contract.\n for (const [key, filterRule] of Object.entries(filters)) {\n const value = data[key];\n\n if (value === undefined) continue;\n if (filterRule === undefined) continue;\n\n const rule = this.parseFilterRule(key, filterRule);\n this.applyFilterRule(query, rule, value, data, options);\n }\n }\n\n /**\n * Parse a filter rule into a structured format\n */\n private parseFilterRule(key: string, rule: FilterRule) {\n // Handle custom function\n if (typeof rule === \"function\") {\n return { type: \"function\", fn: rule, column: key, key };\n }\n\n // Handle array format: [\"operator\"] or [\"operator\", \"column\"] or [\"operator\", [\"col1\", \"col2\"]]\n if (Array.isArray(rule)) {\n const [operator, target] = rule;\n\n if (target === undefined) {\n return { type: operator, column: key, columns: undefined, key };\n }\n\n if (Array.isArray(target)) {\n return { type: operator, column: undefined, columns: target, key };\n }\n\n return { type: operator, column: target, columns: undefined, key };\n }\n\n // Handle simple operator string\n return { type: rule, column: key, columns: undefined, key };\n }\n\n /**\n * Apply a single filter rule to the query\n */\n private applyFilterRule(\n query: CascadeQueryBuilder<any>,\n rule: any,\n value: any,\n data: any,\n options: FilterOptions,\n ): void {\n // 1. Custom function filter\n if (rule.type === \"function\") {\n rule.fn(value, query, data);\n return;\n }\n\n // 2. Predefined filter types\n const handler = this.getFilterHandler(rule.type);\n if (handler) {\n handler.call(this, query, rule.column, rule.columns, value, options);\n return;\n }\n\n // 3. Standard where operators\n this.applyWhereOperator(query, rule.type, rule.column, rule.columns, value, rule.key);\n }\n\n /**\n * Get filter handler for predefined types\n */\n private getFilterHandler(type: string): Function | undefined {\n const handlers: Record<string, Function> = {\n // Boolean filters\n bool: this.handleBoolean,\n boolean: this.handleBoolean,\n\n // Numeric filters\n int: this.handleInt,\n integer: this.handleInt,\n \"!int\": this.handleNotInt,\n \"int>\": (q: any, col: any, cols: any, val: any) =>\n this.handleIntComparison(q, col, cols, val, \">\"),\n \"int>=\": (q: any, col: any, cols: any, val: any) =>\n this.handleIntComparison(q, col, cols, val, \">=\"),\n \"int<\": (q: any, col: any, cols: any, val: any) =>\n this.handleIntComparison(q, col, cols, val, \"<\"),\n \"int<=\": (q: any, col: any, cols: any, val: any) =>\n this.handleIntComparison(q, col, cols, val, \"<=\"),\n inInt: this.handleInInt,\n number: this.handleNumber,\n inNumber: this.handleInNumber,\n float: this.handleFloat,\n double: this.handleFloat,\n inFloat: this.handleInNumber,\n\n // Null filters\n null: this.handleNull,\n notNull: this.handleNotNull,\n \"!null\": this.handleNotNull,\n\n // Date filters\n date: this.handleDate,\n \"date>\": (q: any, col: any, cols: any, val: any, opts: any) =>\n this.handleDateComparison(q, col, cols, val, opts, \">\"),\n \"date>=\": (q: any, col: any, cols: any, val: any, opts: any) =>\n this.handleDateComparison(q, col, cols, val, opts, \">=\"),\n \"date<\": (q: any, col: any, cols: any, val: any, opts: any) =>\n this.handleDateComparison(q, col, cols, val, opts, \"<\"),\n \"date<=\": (q: any, col: any, cols: any, val: any, opts: any) =>\n this.handleDateComparison(q, col, cols, val, opts, \"<=\"),\n dateBetween: this.handleDateBetween,\n inDate: this.handleInDate,\n\n // DateTime filters\n dateTime: this.handleDateTime,\n \"dateTime>\": (q: any, col: any, cols: any, val: any, opts: any) =>\n this.handleDateTimeComparison(q, col, cols, val, opts, \">\"),\n \"dateTime>=\": (q: any, col: any, cols: any, val: any, opts: any) =>\n this.handleDateTimeComparison(q, col, cols, val, opts, \">=\"),\n \"dateTime<\": (q: any, col: any, cols: any, val: any, opts: any) =>\n this.handleDateTimeComparison(q, col, cols, val, opts, \"<\"),\n \"dateTime<=\": (q: any, col: any, cols: any, val: any, opts: any) =>\n this.handleDateTimeComparison(q, col, cols, val, opts, \"<=\"),\n dateTimeBetween: this.handleDateTimeBetween,\n inDateTime: this.handleInDateTime,\n\n // Scope filter - applies local scope when value is truthy\n scope: this.handleScope,\n\n // With filter - eager-loads a relation when value is truthy\n with: this.handleWith,\n\n // JoinWith filter - eager-loads a relation via SQL JOIN when value is truthy\n joinWith: this.handleJoinWith,\n\n // Vector similarity search — calls similarTo(column, embedding[])\n similarTo: this.handleSimilarTo,\n };\n\n return handlers[type];\n }\n\n /**\n * Apply standard where operators\n */\n private applyWhereOperator(\n query: QueryBuilderContract,\n operator: string,\n column?: string,\n columns?: string[],\n value?: any,\n ruleName?: string,\n ): void {\n // Handle \"in\" prefix for array values\n if (operator.startsWith(\"in\") && operator !== \"int\" && !Array.isArray(value)) {\n value = [value];\n }\n\n // Single column\n if (column) {\n switch (operator) {\n case \"=\":\n query.where(column, value);\n break;\n case \"!=\":\n case \"<>\":\n query.where(column, \"!=\", value);\n break;\n case \">\":\n case \">=\":\n case \"<\":\n case \"<=\":\n query.where(column, operator, value);\n break;\n case \"in\":\n query.whereIn(column, Array.isArray(value) ? value : [value]);\n break;\n case \"not in\":\n query.whereNotIn(column, Array.isArray(value) ? value : [value]);\n break;\n case \"like\":\n query.whereLike(column, value);\n break;\n case \"not like\":\n query.whereNotLike(column, value);\n break;\n case \"between\":\n query.whereBetween(column, value);\n break;\n case \"not between\":\n query.whereNotBetween(column, value);\n break;\n }\n }\n // Multiple columns (OR condition)\n else if (columns) {\n if (operator === \"=\") {\n const conditions: any = {};\n for (const col of columns) {\n conditions[col] = value;\n }\n query.orWhere(conditions);\n } else if (operator === \"like\") {\n // (col0 LIKE value OR col1 LIKE value OR ...), OR'd with prior conditions —\n // the object form of orWhere() only ever emits \"=\", so \"like\" needs the\n // callback/group form to keep the operator instead of silently degrading\n // to equality (D1).\n query.orWhere((sub: QueryBuilderContract) => {\n columns.forEach((col, index) => {\n if (index === 0) {\n sub.where(col, \"like\", value);\n } else {\n sub.orWhere(col, \"like\", value);\n }\n });\n });\n } else {\n // Every other operator silently degraded to equality via the object form\n // of orWhere() — refuse instead of returning a correct-looking, wrong query.\n throw new Error(\n `Unsupported multi-column filter operator \"${operator}\" for rule \"${ruleName ?? columns.join(\",\")}\". ` +\n `Multi-column filters only support \"=\" and \"like\".`,\n );\n }\n }\n }\n\n // ============================================================================\n // BOOLEAN FILTERS\n // ============================================================================\n\n /**\n * Coerce a filter value to a boolean.\n *\n * Recognized truthy forms: `true`, `1`, `\"true\"`, `\"1\"`.\n * Recognized falsy forms: `false`, `0`, `\"false\"`, `\"0\"`.\n * Anything else falls back to `Boolean(value)`.\n *\n * NOTE: this intentionally does NOT treat \"non-empty\" as true — the previous\n * `|| !isEmpty(value)` fallback coerced `false`/`0` to `true`, inverting the\n * filter for explicitly-false values.\n */\n private coerceBoolean(value: any): boolean {\n if (value === true || value === 1 || value === \"true\" || value === \"1\") {\n return true;\n }\n\n if (value === false || value === 0 || value === \"false\" || value === \"0\") {\n return false;\n }\n\n return Boolean(value);\n }\n\n private handleBoolean(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n ) {\n const boolValue = this.coerceBoolean(value);\n if (column) {\n query.where(column, boolValue);\n } else if (columns) {\n const conditions: any = {};\n for (const col of columns) {\n conditions[col] = boolValue;\n }\n\n query.orWhere(conditions);\n }\n }\n\n // ============================================================================\n // NUMERIC FILTERS\n // ============================================================================\n private handleInt(query: QueryBuilderContract, column?: string, columns?: string[], value?: any) {\n const intValue = parseInt(value);\n if (column) {\n query.where(column, intValue);\n } else if (columns) {\n const conditions: any = {};\n for (const col of columns) {\n conditions[col] = intValue;\n }\n\n query.orWhere(conditions);\n }\n }\n\n private handleNotInt(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n ) {\n const intValue = parseInt(value);\n if (column) {\n query.where(column, \"!=\", intValue);\n } else if (columns) {\n // Use multiple orWhere calls for OR logic across columns\n for (const col of columns) {\n query.orWhere(col, \"!=\", intValue);\n }\n }\n }\n\n private handleIntComparison(\n query: QueryBuilderContract,\n column: any,\n columns: any,\n value: any,\n operator: string,\n ) {\n const intValue = parseInt(value);\n if (column) {\n query.where(column, operator, intValue);\n } else if (columns) {\n for (const col of columns) {\n query.orWhere(col, operator, intValue);\n }\n }\n }\n\n private handleInInt(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n ) {\n const values = (Array.isArray(value) ? value : [value]).map((v: any) => parseInt(v));\n if (column) {\n query.whereIn(column, values);\n } else if (columns) {\n // Use multiple orWhere calls with whereIn for each column\n for (const col of columns) {\n query.orWhere((q: any) => q.whereIn(col, values));\n }\n }\n }\n\n private handleNumber(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n ) {\n const numValue = Number(value);\n if (column) {\n query.where(column, numValue);\n } else if (columns) {\n const conditions: any = {};\n for (const col of columns) {\n conditions[col] = numValue;\n }\n\n query.orWhere(conditions);\n }\n }\n\n private handleInNumber(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n ) {\n const values = (Array.isArray(value) ? value : [value]).map((v: any) => Number(v));\n if (column) {\n query.whereIn(column, values);\n } else if (columns) {\n // Use multiple orWhere calls with whereIn for each column\n for (const col of columns) {\n query.orWhere((q: any) => q.whereIn(col, values));\n }\n }\n }\n\n private handleFloat(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n ) {\n const floatValue = parseFloat(value);\n if (column) {\n query.where(column, floatValue);\n } else if (columns) {\n const conditions: any = {};\n for (const col of columns) {\n conditions[col] = floatValue;\n }\n\n query.orWhere(conditions);\n }\n }\n\n // ============================================================================\n // NULL FILTERS\n // ============================================================================\n\n private handleNull(query: QueryBuilderContract, column?: string, columns?: string[]) {\n if (column) {\n query.whereNull(column);\n } else if (columns) {\n for (const col of columns) {\n query.orWhere({ [col]: null });\n }\n }\n }\n\n private handleNotNull(query: QueryBuilderContract, column?: string, columns?: string[]) {\n if (column) {\n query.whereNotNull(column);\n } else if (columns) {\n // Use whereNotNull for each column\n for (const col of columns) {\n query.orWhere((q: any) => q.whereNotNull(col));\n }\n }\n }\n\n // ============================================================================\n // SCOPE FILTER\n // ============================================================================\n\n /**\n * Handle scope filter - applies local scope and passes the filter value.\n *\n * Usage in filterBy:\n * ```typescript\n * filterBy: {\n * active: \"scope\", // Uses the filter key as scope name\n * isAdmin: [\"scope\", \"admin\"] // Uses custom scope name\n * }\n * ```\n *\n * When list({ active: true }) is called, it will call query.scope(\"active\", true)\n * When list({ status: \"pending\" }) is called, it will call query.scope(\"status\", \"pending\")\n */\n private handleScope(\n query: QueryBuilderContract,\n column?: string,\n _columns?: string[],\n value?: any,\n ) {\n // column holds the scope name (either the filter key or custom name from array format)\n if (column) {\n query.scope(column, value);\n }\n }\n\n // ============================================================================\n // WITH (EAGER LOAD) FILTER\n // ============================================================================\n\n /**\n * Handle with filter - eager-loads a relation when the filter value is truthy.\n *\n * Usage in filterBy:\n * ```typescript\n * filterBy: {\n * with_ai_model: [\"with\", \"ai_model\"], // load single relation\n * with_all: [\"with\", [\"ai_model\", \"unit\"]] // load multiple relations\n * }\n * ```\n *\n * When list({ with_ai_model: true }) is called, it will call query.with(\"ai_model\")\n */\n private handleWith(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n ) {\n if (!value) return;\n\n // Load a single named relation\n if (column) {\n if (query.with) {\n query.with(column);\n }\n return;\n }\n\n // Load multiple relations from the columns array\n if (columns) {\n for (const relation of columns) {\n if (query.with) {\n query.with(relation);\n }\n }\n }\n }\n\n /**\n * Handle joinWith filter - eager-loads a relation via SQL JOIN when the filter value is truthy.\n *\n * Usage in filterBy:\n * ```typescript\n * filterBy: {\n * with_ai_model: [\"joinWith\", \"ai_model\"], // load single relation via join\n * with_all: [\"joinWith\", [\"ai_model\", \"unit\"]] // load multiple relations via join\n * }\n * ```\n *\n * When list({ with_ai_model: true }) is called, it will call query.joinWith(\"ai_model\")\n */\n private handleJoinWith(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n ) {\n if (!value) return;\n\n if (!query.joinWith) {\n console.warn(\n \"[Repository] joinWith is not supported by the query builder. using with instead.\",\n );\n return this.handleWith(query, column, columns, value);\n }\n\n // Load a single named relation\n if (column) {\n query.joinWith(column);\n return;\n }\n\n // Load multiple relations from the columns array\n if (columns) {\n query.joinWith(...columns);\n }\n }\n\n // ============================================================================\n // VECTOR FILTER\n // ============================================================================\n\n /**\n * Handle similarTo filter — performs vector similarity search.\n *\n * The filter value must be a `number[]` (pre-computed embedding).\n * Delegates to `query.similarTo(column, embedding)` which is handled\n * driver-specifically (pgvector or MongoDB Atlas $vectorSearch).\n *\n * Usage in filterBy:\n * ```typescript\n * filterBy: {\n * organization_id: \"=\",\n * embedding: \"similarTo\",\n * }\n * ```\n *\n * Then in the service:\n * ```typescript\n * await vectorsRepository.list({ embedding: queryEmbedding, organization_id: orgId });\n * ```\n */\n private handleSimilarTo(\n query: QueryBuilderContract,\n column?: string,\n _columns?: string[],\n value?: any,\n ) {\n if (!column || !Array.isArray(value)) return;\n\n // Cast to any: Cascade's internal QueryBuilderContract still uses nearestTo;\n // our wrapper (CascadeQueryBuilder.similarTo) delegates to it correctly.\n (query as any).similarTo(column, value);\n }\n\n // ============================================================================\n // DATE FILTERS\n // ============================================================================\n\n private handleDate(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n options?: FilterOptions,\n ) {\n const dateValue = this.parseDate(value, options?.dateFormat);\n if (column) {\n query.whereDate(column, dateValue);\n } else if (columns) {\n for (const col of columns) {\n query.orWhere((q: any) => q.whereDate(col, dateValue));\n }\n }\n }\n\n private handleDateComparison(\n query: QueryBuilderContract,\n column: any,\n columns: any,\n value: any,\n options: any,\n operator: string,\n ) {\n const dateValue = this.parseDate(value, options?.dateFormat);\n if (column) {\n if (operator === \">\" || operator === \">=\") {\n query.whereDateAfter(column, dateValue);\n } else {\n query.whereDateBefore(column, dateValue);\n }\n } else if (columns) {\n for (const col of columns) {\n if (operator === \">\" || operator === \">=\") {\n query.orWhere((q: any) => q.whereDateAfter(col, dateValue));\n } else {\n query.orWhere((q: any) => q.whereDateBefore(col, dateValue));\n }\n }\n }\n }\n\n private handleDateBetween(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n options?: FilterOptions,\n ) {\n if (!Array.isArray(value) || value.length !== 2) return;\n const [start, end] = value.map((v: any) => this.parseDate(v, options?.dateFormat));\n\n // `value.length === 2` is checked above, so both exist — but the compiler\n // does not narrow destructured elements from a length test. Returning\n // rather than defaulting: this builds a date RANGE for a query, and a\n // half-formed range would silently filter on a boundary the caller never\n // asked for.\n if (start === undefined || end === undefined) return;\n\n if (column) {\n query.whereDateBetween(column, [start, end]);\n } else if (columns) {\n for (const col of columns) {\n query.orWhere((q: any) => q.whereDateBetween(col, [start, end]));\n }\n }\n }\n\n private handleInDate(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n options?: FilterOptions,\n ) {\n const dates = (Array.isArray(value) ? value : [value]).map((v: any) =>\n this.parseDate(v, options?.dateFormat),\n );\n if (column) {\n query.whereIn(column, dates);\n } else if (columns) {\n // Use multiple orWhere calls with whereIn for each column\n for (const col of columns) {\n query.orWhere((q: any) => q.whereIn(col, dates));\n }\n }\n }\n\n // ============================================================================\n // DATETIME FILTERS\n // ============================================================================\n\n private handleDateTime(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n options?: FilterOptions,\n ) {\n const dateValue = this.parseDateTime(value, options?.dateTimeFormat);\n if (column) {\n query.where(column, dateValue);\n } else if (columns) {\n const conditions: any = {};\n for (const col of columns) {\n conditions[col] = dateValue;\n }\n\n query.orWhere(conditions);\n }\n }\n\n private handleDateTimeComparison(\n query: QueryBuilderContract,\n column: any,\n columns: any,\n value: any,\n options: any,\n operator: string,\n ) {\n const dateValue = this.parseDateTime(value, options?.dateTimeFormat);\n if (column) {\n query.where(column, operator, dateValue);\n } else if (columns) {\n for (const col of columns) {\n query.orWhere(col, operator, dateValue);\n }\n }\n }\n\n private handleDateTimeBetween(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n options?: FilterOptions,\n ) {\n if (!Array.isArray(value) || value.length !== 2) return;\n const [start, end] = value.map((v: any) => this.parseDateTime(v, options?.dateTimeFormat));\n if (column) {\n query.whereBetween(column, [start, end]);\n } else if (columns) {\n for (const col of columns) {\n query.orWhere((q: any) => q.whereBetween(col, [start, end]));\n }\n }\n }\n\n private handleInDateTime(\n query: QueryBuilderContract,\n column?: string,\n columns?: string[],\n value?: any,\n options?: FilterOptions,\n ) {\n const dates = (Array.isArray(value) ? value : [value]).map((v: any) =>\n this.parseDateTime(v, options?.dateTimeFormat),\n );\n if (column) {\n query.whereIn(column, dates);\n } else if (columns) {\n // Use multiple orWhere calls with whereIn for each column\n for (const col of columns) {\n query.orWhere((q: any) => q.whereIn(col, dates));\n }\n }\n }\n\n // ============================================================================\n // DATE PARSING UTILITIES\n // ============================================================================\n\n /**\n * Parse date string to Date object\n * TODO: Implement proper date parsing with format support\n */\n private parseDate(value: any, format?: string): Date {\n if (value instanceof Date) return value;\n return new Date(value);\n }\n\n /**\n * Parse datetime string to Date object\n * TODO: Implement proper datetime parsing with format support\n */\n private parseDateTime(value: any, format?: string): Date {\n if (value instanceof Date) return value;\n return new Date(value);\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"positional-handler-diagnostics.d.mts","names":[],"sources":["../../../../../../../core/src/router/positional-handler-diagnostics.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"positional-handler-diagnostics.d.mts","names":[],"sources":["../../../../../../../core/src/router/positional-handler-diagnostics.ts"],"mappings":";;;;AA+PsE;AAmBtE;;AAnBsE,KA3N1D,wBAAA;EA8OsE,kDA5OhF,MAAA,EAAQ,KAAK,YAqPC;EAlPd,IAAA;EAGA,WAAA,UA+OgE;EA5OhE,UAAA;AAAA;;;AA0P4C;AAS9C;;;;AAAkF;AAclF;;;;;;;;;;;;;AAAsF;;;;;;;;;;;iBApGtE,0BAAA,CAA2B,OAAgB;;;;;;;;;iBAiC3C,uBAAA,CACd,OAAA,WACA,KAAA;EAAS,MAAA,EAAQ,KAAK;EAAY,IAAA;EAAc,UAAA;AAAA;;;;iBAmBlC,6BAAA,aAA0C,wBAAwB;;;;;;iBASlE,+BAAA,CAAgC,UAAkB;;;;;iBAclD,8BAAA;;;;;;iBASA,gCAAA,CAAiC,OAAiC,EAAxB,wBAAwB;;;;;;;iBAclE,+BAAA,CAAgC,MAAA,GAAQ,IAAA,QAAY,GAAA,YAAkB,wBAAA"}
|
|
@@ -114,7 +114,10 @@ function readParameterList(handler) {
|
|
|
114
114
|
if (source.includes("[native code]")) return void 0;
|
|
115
115
|
if (/^\s*class[\s{]/.test(source)) return void 0;
|
|
116
116
|
const bareArrow = source.match(/^\s*(?:async\s+)?([A-Za-z_$][\w$]*)\s*=>/);
|
|
117
|
-
if (bareArrow)
|
|
117
|
+
if (bareArrow) {
|
|
118
|
+
const parameter = bareArrow[1];
|
|
119
|
+
return parameter === void 0 ? void 0 : [parameter];
|
|
120
|
+
}
|
|
118
121
|
const openIndex = source.indexOf("(");
|
|
119
122
|
if (openIndex === -1) return void 0;
|
|
120
123
|
return splitTopLevelParameters(source, openIndex);
|
|
@@ -162,7 +165,10 @@ function isDestructured(parameter) {
|
|
|
162
165
|
function looksLikePositionalHandler(handler) {
|
|
163
166
|
if (typeof handler !== "function") return false;
|
|
164
167
|
const parameters = readParameterList(handler);
|
|
165
|
-
if (parameters)
|
|
168
|
+
if (parameters) {
|
|
169
|
+
const firstParameter = parameters[0];
|
|
170
|
+
return parameters.length >= 2 && firstParameter !== void 0 && !isDestructured(firstParameter);
|
|
171
|
+
}
|
|
166
172
|
return handler.length >= 2;
|
|
167
173
|
}
|
|
168
174
|
/**
|
|
@@ -206,7 +212,7 @@ function listPositionalHandlerSuspects() {
|
|
|
206
212
|
* not keep reporting the version it replaced.
|
|
207
213
|
*/
|
|
208
214
|
function forgetPositionalHandlerSuspects(sourceFile) {
|
|
209
|
-
for (let index = suspects.length - 1; index >= 0; index--) if (suspects[index]
|
|
215
|
+
for (let index = suspects.length - 1; index >= 0; index--) if (suspects[index]?.sourceFile === sourceFile) suspects.splice(index, 1);
|
|
210
216
|
}
|
|
211
217
|
/**
|
|
212
218
|
* Drop every recorded suspect. Intended for tests, which share the router
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"positional-handler-diagnostics.mjs","names":[],"sources":["../../../../../../../core/src/router/positional-handler-diagnostics.ts"],"sourcesContent":["/**\n * Early detection of route handlers still written against the **v4 positional\n * signature** — `(request, response)` — after v5 moved to a single context\n * object, `({ request, response })`.\n *\n * ## Why this exists\n *\n * A v4 handler is not rejected anywhere. It registers fine, and the router\n * calls it with one argument, so `response` is simply `undefined`. The app only\n * breaks on the first request that reaches it, with:\n *\n * TypeError: Cannot read properties of undefined (reading 'success')\n *\n * That message names nothing the reader can act on — not the route, not the\n * handler, not the change to make. This module turns that late, opaque failure\n * into one explicit list printed at boot, and into a `warlock doctor` check.\n *\n * The break itself is intentional; nothing here restores the positional form.\n * Detection and diagnostics only.\n *\n * ## Why it warns and never throws\n *\n * The signal is a heuristic (see {@link looksLikePositionalHandler}), so a\n * false positive is possible in principle. Turning one into a boot failure\n * would take a working application down over a guess, which is strictly worse\n * than the problem being diagnosed. Everything on this path is therefore a\n * warning: nothing here throws, and nothing here changes how a handler is\n * called. A miss simply degrades to the old runtime `TypeError`.\n */\n\nimport { log } from \"@warlock.js/logger\";\nimport type { Route } from \"./types\";\n\n/**\n * One route whose handler looks like it was written for v4.\n */\nexport type PositionalHandlerSuspect = {\n /** HTTP method the route was registered under. */\n method: Route[\"method\"];\n\n /** Full route path, prefixes already applied. */\n path: string;\n\n /** Handler function name, or `\"(anonymous)\"` when it has none. */\n handlerName: string;\n\n /** Route file the route came from, when the router knows it. */\n sourceFile: string;\n};\n\n/**\n * Collected across the whole registration pass so the warning can be emitted\n * ONCE, as a single list, rather than one line per route (or — as before — one\n * mystery 500 per request).\n */\nconst suspects: PositionalHandlerSuspect[] = [];\n\n/**\n * Split the parameter list of a function's source into top-level parameters.\n *\n * Depth-aware and quote-aware, because a default value may itself contain\n * commas, parentheses, braces or strings — `(request, response = f(a, b))` is\n * two parameters, not three. Anything unbalanced returns `undefined`: refusing\n * to guess is what keeps a parse quirk from becoming a false warning.\n *\n * @param openIndex index of the `(` that opens the parameter list.\n */\nfunction splitTopLevelParameters(source: string, openIndex: number): string[] | undefined {\n const parameters: string[] = [];\n\n let depth = 0;\n let current = \"\";\n let quote = \"\";\n\n for (let index = openIndex; index < source.length; index++) {\n const char = source[index];\n\n if (quote) {\n if (char === \"\\\\\") {\n current += char + (source[index + 1] ?? \"\");\n index++;\n continue;\n }\n\n if (char === quote) {\n quote = \"\";\n }\n\n current += char;\n continue;\n }\n\n if (char === '\"' || char === \"'\" || char === \"`\") {\n quote = char;\n current += char;\n continue;\n }\n\n if (char === \"(\" || char === \"[\" || char === \"{\") {\n depth++;\n\n // The paren that opens the list is punctuation, not part of a parameter.\n if (depth === 1) continue;\n\n current += char;\n continue;\n }\n\n if (char === \")\" || char === \"]\" || char === \"}\") {\n depth--;\n\n if (depth === 0) {\n if (current.trim()) {\n parameters.push(current.trim());\n }\n\n return parameters;\n }\n\n current += char;\n continue;\n }\n\n if (char === \",\" && depth === 1) {\n parameters.push(current.trim());\n current = \"\";\n continue;\n }\n\n current += char;\n }\n\n // Never found the closing paren — treat the source as unreadable.\n return undefined;\n}\n\n/**\n * Read a function's declared parameters from its own source.\n *\n * Returns `undefined` whenever the source cannot be trusted, which callers must\n * treat as \"no information\" rather than \"no parameters\":\n *\n * - `Function.prototype.bind` replaces the body with `[native code]`, so every\n * `[Controller, \"method\"]` tuple handler lands here (the router binds them);\n * - native functions likewise;\n * - a class, whose first `(` belongs to a constructor or a method, not to a\n * parameter list;\n * - anything that fails to parse.\n */\nfunction readParameterList(handler: Function): string[] | undefined {\n let source: string;\n\n try {\n source = Function.prototype.toString.call(handler);\n } catch {\n return undefined;\n }\n\n if (source.includes(\"[native code]\")) return undefined;\n\n if (/^\\s*class[\\s{]/.test(source)) return undefined;\n\n // A single parameter needs no parentheses: `ctx => …`, `async ctx => …`.\n const bareArrow = source.match(/^\\s*(?:async\\s+)?([A-Za-z_$][\\w$]*)\\s*=>/);\n\n if (bareArrow) return [bareArrow[1]];\n\n const openIndex = source.indexOf(\"(\");\n\n if (openIndex === -1) return undefined;\n\n return splitTopLevelParameters(source, openIndex);\n}\n\n/**\n * Whether a parameter is written as a destructuring pattern — `{ request }` or\n * `[a, b]` — as opposed to a plain name.\n */\nfunction isDestructured(parameter: string) {\n const start = parameter.trimStart();\n\n return start.startsWith(\"{\") || start.startsWith(\"[\");\n}\n\n/**\n * Does this handler look like it was written for the v4 positional signature?\n *\n * ## The signal, and what it misses\n *\n * `Function.length` alone is not enough, because it stops counting at the first\n * defaulted parameter:\n *\n * | handler | `.length` | actually |\n * | ---------------------------------- | --------- | -------- |\n * | `({ request, response }) => …` | 1 | v5 |\n * | `(ctx) => …` | 1 | v5 |\n * | `(request, response) => …` | 2 | v4 |\n * | `(request, response = x) => …` | 1 | v4 |\n *\n * So the primary signal is the function's own source: a handler is suspect when\n * it declares **two or more** parameters and the first is **not** destructured.\n * Reading the source catches the defaulted row that arity cannot, and rules out\n * the `({ … }, extra)` shape that arity would wrongly flag.\n *\n * When the source is unreadable — a bound or native function — this falls back\n * to `handler.length >= 2`, which `bind` preserves. That is a weaker signal but\n * never a wrong one for the common cases; it only loses the defaulted row.\n *\n * ## What is still missed, by design\n *\n * A one-parameter v4 handler (`(request) => …`) is indistinguishable from a v5\n * `(ctx) => …` and is deliberately not flagged — guessing there would produce\n * false positives on correct code. False positives against the canonical\n * `({ … })` form are essentially nil, and every miss simply leaves today's\n * behaviour in place.\n */\nexport function looksLikePositionalHandler(handler: unknown): boolean {\n if (typeof handler !== \"function\") return false;\n\n const parameters = readParameterList(handler);\n\n if (parameters) {\n return parameters.length >= 2 && !isDestructured(parameters[0]);\n }\n\n return handler.length >= 2;\n}\n\n/**\n * The name to print for a handler.\n *\n * `Function.bind` prefixes the name with `\"bound \"`, and the router binds every\n * `[Controller, \"method\"]` tuple handler. The bare method name is what the user\n * actually wrote, so that is what gets printed.\n */\nfunction handlerDisplayName(handler: Function): string {\n return handler.name?.replace(/^bound /, \"\").trim() || \"(anonymous)\";\n}\n\n/**\n * Inspect one route's handler as it registers, recording it when it looks like\n * the v4 positional form. Called from `Router.add`, so the cost is paid once per\n * route at registration and never per request.\n *\n * Deliberately total: any unexpected failure in the heuristic is swallowed, so a\n * diagnostic can never be the reason a route fails to register.\n */\nexport function inspectHandlerSignature(\n handler: unknown,\n route: { method: Route[\"method\"]; path: string; sourceFile: string },\n) {\n try {\n if (!looksLikePositionalHandler(handler)) return;\n\n suspects.push({\n method: route.method,\n path: route.path,\n handlerName: handlerDisplayName(handler as Function),\n sourceFile: route.sourceFile,\n });\n } catch {\n // A diagnostic must never break registration.\n }\n}\n\n/**\n * Every suspect recorded so far, in registration order.\n */\nexport function listPositionalHandlerSuspects(): readonly PositionalHandlerSuspect[] {\n return suspects;\n}\n\n/**\n * Drop the suspects belonging to a route file. Kept in step with\n * `Router.removeRoutesBySourceFile` so an HMR reload that fixes a handler does\n * not keep reporting the version it replaced.\n */\nexport function forgetPositionalHandlerSuspects(sourceFile: string) {\n for (let index = suspects.length - 1; index >= 0; index--) {\n if (suspects[index].sourceFile === sourceFile) {\n suspects.splice(index, 1);\n }\n }\n}\n\n/**\n * Drop every recorded suspect. Intended for tests, which share the router\n * singleton.\n */\nexport function clearPositionalHandlerSuspects() {\n suspects.length = 0;\n}\n\n/**\n * The one-line diagnostic for a single route. Written for someone who has never\n * read Warlock's internals: it names the handler, the route, what is wrong, and\n * the exact edit that fixes it.\n */\nexport function describePositionalHandlerSuspect(suspect: PositionalHandlerSuspect): string {\n return (\n `Handler \"${suspect.handlerName}\" (${suspect.method} ${suspect.path}) looks like ` +\n `the v4 positional signature (request, response). ` +\n `v5 passes a single context object — change it to ({ request, response }).`\n );\n}\n\n/**\n * The full boot-time warning: every suspect, once, in one entry.\n *\n * One boot, one list — not one warning per route, and emphatically not one 500\n * per request. Returns the reported suspects so callers can assert on them.\n */\nexport function reportPositionalHandlerSuspects(logger: Pick<typeof log, \"warn\"> = log) {\n const reported = [...suspects];\n\n if (reported.length === 0) return reported;\n\n const headline =\n reported.length === 1\n ? \"1 route handler looks like the v4 positional signature.\"\n : `${reported.length} route handlers look like the v4 positional signature.`;\n\n logger.warn({\n module: \"router\",\n action: \"handlerSignature\",\n message:\n `${headline} Warlock v5 calls every route handler with a single context object, ` +\n `so a second parameter is always undefined and the request fails as soon as it is used:\\n` +\n reported.map((suspect) => ` - ${describePositionalHandlerSuspect(suspect)}`).join(\"\\n\"),\n context: { handlers: reported },\n });\n\n return reported;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"positional-handler-diagnostics.mjs","names":[],"sources":["../../../../../../../core/src/router/positional-handler-diagnostics.ts"],"sourcesContent":["/**\n * Early detection of route handlers still written against the **v4 positional\n * signature** — `(request, response)` — after v5 moved to a single context\n * object, `({ request, response })`.\n *\n * ## Why this exists\n *\n * A v4 handler is not rejected anywhere. It registers fine, and the router\n * calls it with one argument, so `response` is simply `undefined`. The app only\n * breaks on the first request that reaches it, with:\n *\n * TypeError: Cannot read properties of undefined (reading 'success')\n *\n * That message names nothing the reader can act on — not the route, not the\n * handler, not the change to make. This module turns that late, opaque failure\n * into one explicit list printed at boot, and into a `warlock doctor` check.\n *\n * The break itself is intentional; nothing here restores the positional form.\n * Detection and diagnostics only.\n *\n * ## Why it warns and never throws\n *\n * The signal is a heuristic (see {@link looksLikePositionalHandler}), so a\n * false positive is possible in principle. Turning one into a boot failure\n * would take a working application down over a guess, which is strictly worse\n * than the problem being diagnosed. Everything on this path is therefore a\n * warning: nothing here throws, and nothing here changes how a handler is\n * called. A miss simply degrades to the old runtime `TypeError`.\n */\n\nimport { log } from \"@warlock.js/logger\";\nimport type { Route } from \"./types\";\n\n/**\n * One route whose handler looks like it was written for v4.\n */\nexport type PositionalHandlerSuspect = {\n /** HTTP method the route was registered under. */\n method: Route[\"method\"];\n\n /** Full route path, prefixes already applied. */\n path: string;\n\n /** Handler function name, or `\"(anonymous)\"` when it has none. */\n handlerName: string;\n\n /** Route file the route came from, when the router knows it. */\n sourceFile: string;\n};\n\n/**\n * Collected across the whole registration pass so the warning can be emitted\n * ONCE, as a single list, rather than one line per route (or — as before — one\n * mystery 500 per request).\n */\nconst suspects: PositionalHandlerSuspect[] = [];\n\n/**\n * Split the parameter list of a function's source into top-level parameters.\n *\n * Depth-aware and quote-aware, because a default value may itself contain\n * commas, parentheses, braces or strings — `(request, response = f(a, b))` is\n * two parameters, not three. Anything unbalanced returns `undefined`: refusing\n * to guess is what keeps a parse quirk from becoming a false warning.\n *\n * @param openIndex index of the `(` that opens the parameter list.\n */\nfunction splitTopLevelParameters(source: string, openIndex: number): string[] | undefined {\n const parameters: string[] = [];\n\n let depth = 0;\n let current = \"\";\n let quote = \"\";\n\n for (let index = openIndex; index < source.length; index++) {\n const char = source[index];\n\n if (quote) {\n if (char === \"\\\\\") {\n current += char + (source[index + 1] ?? \"\");\n index++;\n continue;\n }\n\n if (char === quote) {\n quote = \"\";\n }\n\n current += char;\n continue;\n }\n\n if (char === '\"' || char === \"'\" || char === \"`\") {\n quote = char;\n current += char;\n continue;\n }\n\n if (char === \"(\" || char === \"[\" || char === \"{\") {\n depth++;\n\n // The paren that opens the list is punctuation, not part of a parameter.\n if (depth === 1) continue;\n\n current += char;\n continue;\n }\n\n if (char === \")\" || char === \"]\" || char === \"}\") {\n depth--;\n\n if (depth === 0) {\n if (current.trim()) {\n parameters.push(current.trim());\n }\n\n return parameters;\n }\n\n current += char;\n continue;\n }\n\n if (char === \",\" && depth === 1) {\n parameters.push(current.trim());\n current = \"\";\n continue;\n }\n\n current += char;\n }\n\n // Never found the closing paren — treat the source as unreadable.\n return undefined;\n}\n\n/**\n * Read a function's declared parameters from its own source.\n *\n * Returns `undefined` whenever the source cannot be trusted, which callers must\n * treat as \"no information\" rather than \"no parameters\":\n *\n * - `Function.prototype.bind` replaces the body with `[native code]`, so every\n * `[Controller, \"method\"]` tuple handler lands here (the router binds them);\n * - native functions likewise;\n * - a class, whose first `(` belongs to a constructor or a method, not to a\n * parameter list;\n * - anything that fails to parse.\n */\nfunction readParameterList(handler: Function): string[] | undefined {\n let source: string;\n\n try {\n source = Function.prototype.toString.call(handler);\n } catch {\n return undefined;\n }\n\n if (source.includes(\"[native code]\")) return undefined;\n\n if (/^\\s*class[\\s{]/.test(source)) return undefined;\n\n // A single parameter needs no parentheses: `ctx => …`, `async ctx => …`.\n const bareArrow = source.match(/^\\s*(?:async\\s+)?([A-Za-z_$][\\w$]*)\\s*=>/);\n\n if (bareArrow) {\n const parameter = bareArrow[1];\n\n return parameter === undefined ? undefined : [parameter];\n }\n\n const openIndex = source.indexOf(\"(\");\n\n if (openIndex === -1) return undefined;\n\n return splitTopLevelParameters(source, openIndex);\n}\n\n/**\n * Whether a parameter is written as a destructuring pattern — `{ request }` or\n * `[a, b]` — as opposed to a plain name.\n */\nfunction isDestructured(parameter: string) {\n const start = parameter.trimStart();\n\n return start.startsWith(\"{\") || start.startsWith(\"[\");\n}\n\n/**\n * Does this handler look like it was written for the v4 positional signature?\n *\n * ## The signal, and what it misses\n *\n * `Function.length` alone is not enough, because it stops counting at the first\n * defaulted parameter:\n *\n * | handler | `.length` | actually |\n * | ---------------------------------- | --------- | -------- |\n * | `({ request, response }) => …` | 1 | v5 |\n * | `(ctx) => …` | 1 | v5 |\n * | `(request, response) => …` | 2 | v4 |\n * | `(request, response = x) => …` | 1 | v4 |\n *\n * So the primary signal is the function's own source: a handler is suspect when\n * it declares **two or more** parameters and the first is **not** destructured.\n * Reading the source catches the defaulted row that arity cannot, and rules out\n * the `({ … }, extra)` shape that arity would wrongly flag.\n *\n * When the source is unreadable — a bound or native function — this falls back\n * to `handler.length >= 2`, which `bind` preserves. That is a weaker signal but\n * never a wrong one for the common cases; it only loses the defaulted row.\n *\n * ## What is still missed, by design\n *\n * A one-parameter v4 handler (`(request) => …`) is indistinguishable from a v5\n * `(ctx) => …` and is deliberately not flagged — guessing there would produce\n * false positives on correct code. False positives against the canonical\n * `({ … })` form are essentially nil, and every miss simply leaves today's\n * behaviour in place.\n */\nexport function looksLikePositionalHandler(handler: unknown): boolean {\n if (typeof handler !== \"function\") return false;\n\n const parameters = readParameterList(handler);\n\n if (parameters) {\n const firstParameter = parameters[0];\n\n return parameters.length >= 2 && firstParameter !== undefined && !isDestructured(firstParameter);\n }\n\n return handler.length >= 2;\n}\n\n/**\n * The name to print for a handler.\n *\n * `Function.bind` prefixes the name with `\"bound \"`, and the router binds every\n * `[Controller, \"method\"]` tuple handler. The bare method name is what the user\n * actually wrote, so that is what gets printed.\n */\nfunction handlerDisplayName(handler: Function): string {\n return handler.name?.replace(/^bound /, \"\").trim() || \"(anonymous)\";\n}\n\n/**\n * Inspect one route's handler as it registers, recording it when it looks like\n * the v4 positional form. Called from `Router.add`, so the cost is paid once per\n * route at registration and never per request.\n *\n * Deliberately total: any unexpected failure in the heuristic is swallowed, so a\n * diagnostic can never be the reason a route fails to register.\n */\nexport function inspectHandlerSignature(\n handler: unknown,\n route: { method: Route[\"method\"]; path: string; sourceFile: string },\n) {\n try {\n if (!looksLikePositionalHandler(handler)) return;\n\n suspects.push({\n method: route.method,\n path: route.path,\n handlerName: handlerDisplayName(handler as Function),\n sourceFile: route.sourceFile,\n });\n } catch {\n // A diagnostic must never break registration.\n }\n}\n\n/**\n * Every suspect recorded so far, in registration order.\n */\nexport function listPositionalHandlerSuspects(): readonly PositionalHandlerSuspect[] {\n return suspects;\n}\n\n/**\n * Drop the suspects belonging to a route file. Kept in step with\n * `Router.removeRoutesBySourceFile` so an HMR reload that fixes a handler does\n * not keep reporting the version it replaced.\n */\nexport function forgetPositionalHandlerSuspects(sourceFile: string) {\n for (let index = suspects.length - 1; index >= 0; index--) {\n const suspect = suspects[index];\n\n if (suspect?.sourceFile === sourceFile) {\n suspects.splice(index, 1);\n }\n }\n}\n\n/**\n * Drop every recorded suspect. Intended for tests, which share the router\n * singleton.\n */\nexport function clearPositionalHandlerSuspects() {\n suspects.length = 0;\n}\n\n/**\n * The one-line diagnostic for a single route. Written for someone who has never\n * read Warlock's internals: it names the handler, the route, what is wrong, and\n * the exact edit that fixes it.\n */\nexport function describePositionalHandlerSuspect(suspect: PositionalHandlerSuspect): string {\n return (\n `Handler \"${suspect.handlerName}\" (${suspect.method} ${suspect.path}) looks like ` +\n `the v4 positional signature (request, response). ` +\n `v5 passes a single context object — change it to ({ request, response }).`\n );\n}\n\n/**\n * The full boot-time warning: every suspect, once, in one entry.\n *\n * One boot, one list — not one warning per route, and emphatically not one 500\n * per request. Returns the reported suspects so callers can assert on them.\n */\nexport function reportPositionalHandlerSuspects(logger: Pick<typeof log, \"warn\"> = log) {\n const reported = [...suspects];\n\n if (reported.length === 0) return reported;\n\n const headline =\n reported.length === 1\n ? \"1 route handler looks like the v4 positional signature.\"\n : `${reported.length} route handlers look like the v4 positional signature.`;\n\n logger.warn({\n module: \"router\",\n action: \"handlerSignature\",\n message:\n `${headline} Warlock v5 calls every route handler with a single context object, ` +\n `so a second parameter is always undefined and the request fails as soon as it is used:\\n` +\n reported.map((suspect) => ` - ${describePositionalHandlerSuspect(suspect)}`).join(\"\\n\"),\n context: { handlers: reported },\n });\n\n return reported;\n}\n"],"mappings":"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"}
|
|
@@ -53,7 +53,7 @@ var RouteRegistry = class {
|
|
|
53
53
|
* @returns Matched route with extracted params, or null if no match
|
|
54
54
|
*/
|
|
55
55
|
find(method, url) {
|
|
56
|
-
const path = normalizeRequestPath(url.split("?")[0]);
|
|
56
|
+
const path = normalizeRequestPath(url.split("?")[0] ?? "/");
|
|
57
57
|
const match = this.router.find(method, path);
|
|
58
58
|
if (!match) return null;
|
|
59
59
|
return match.handler(null, null, match.params, match.store, {});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-registry.mjs","names":[],"sources":["../../../../../../../core/src/router/route-registry.ts"],"sourcesContent":["import FindMyWay, { type HTTPMethod, type Instance } from \"find-my-way\";\nimport { normalizeRequestPath } from \"./normalize-request-path\";\nimport type { RequestMethod, Route } from \"./types\";\n\n/**\n * The concrete HTTP verbs an `all` route expands into.\n * Mirrors the set Fastify's `.all()` answers in production, so `router.any()`\n * routes behave identically under the dev server and prod Fastify.\n */\nconst ALL_METHODS: Exclude<RequestMethod, \"all\">[] = [\n \"GET\",\n \"POST\",\n \"PUT\",\n \"PATCH\",\n \"DELETE\",\n \"OPTIONS\",\n \"HEAD\",\n];\n\n/**\n * Route Registry\n * Manages dynamic route matching using find-my-way for HMR support\n */\nexport class RouteRegistry {\n private router: Instance<any>;\n\n public constructor() {\n this.router = FindMyWay({\n // Request paths are normalized explicitly in `find`; leaving this off\n // makes that shared normalizer, rather than a second router option, the\n // reason development accepts a terminal slash.\n ignoreTrailingSlash: false,\n caseSensitive: false,\n });\n }\n\n /**\n * Register all routes from the router's internal list\n */\n public register(routes: Route[]): void {\n // Register each route\n for (const route of routes) {\n if (route.method === \"all\") {\n for (const method of ALL_METHODS) {\n this.registerRoute({\n ...route,\n method,\n });\n }\n } else {\n this.registerRoute(route);\n }\n }\n }\n\n /**\n * Register a single route\n */\n public registerRoute(route: Route): void {\n this.router.on(route.method as HTTPMethod, route.path, (req, res, params) => {\n // Store the route and params for later use\n return { route, params };\n });\n }\n\n /**\n * Find a matching route for the given method and URL\n * @returns Matched route with extracted params, or null if no match\n */\n public find(\n method: string,\n url: string,\n ): { route: Route; params: Record<string, string> } | null {\n // Strip query string from URL (find-my-way expects just the path)\n const path = normalizeRequestPath(url.split(\"?\")[0]);\n\n const match = this.router.find(method as HTTPMethod, path);\n\n if (!match) {\n return null;\n }\n\n // find-my-way handler expects (req, res, params, store, searchParams)\n // We only care about the return value which contains { route, params }\n return match.handler(null as any, null as any, match.params, match.store, {});\n }\n\n /**\n * Get all registered routes count (for debugging)\n */\n public getRouteCount(): number {\n return this.router\n .prettyPrint()\n .split(\"\\n\")\n .filter((line) => line.trim()).length;\n }\n}\n"],"mappings":";;;;;;;;;AASA,MAAM,cAA+C;CACnD;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;AAMA,IAAa,gBAAb,MAA2B;CAGzB,AAAO,cAAc;EACnB,KAAK,SAAS,UAAU;GAItB,qBAAqB;GACrB,eAAe;EACjB,CAAC;CACH;;;;CAKA,AAAO,SAAS,QAAuB;EAErC,KAAK,MAAM,SAAS,QAClB,IAAI,MAAM,WAAW,OACnB,KAAK,MAAM,UAAU,aACnB,KAAK,cAAc;GACjB,GAAG;GACH;EACF,CAAC;OAGH,KAAK,cAAc,KAAK;CAG9B;;;;CAKA,AAAO,cAAc,OAAoB;EACvC,KAAK,OAAO,GAAG,MAAM,QAAsB,MAAM,OAAO,KAAK,KAAK,WAAW;GAE3E,OAAO;IAAE;IAAO;GAAO;EACzB,CAAC;CACH;;;;;CAMA,AAAO,KACL,QACA,KACyD;EAEzD,MAAM,OAAO,qBAAqB,IAAI,MAAM,GAAG,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"route-registry.mjs","names":[],"sources":["../../../../../../../core/src/router/route-registry.ts"],"sourcesContent":["import FindMyWay, { type HTTPMethod, type Instance } from \"find-my-way\";\nimport { normalizeRequestPath } from \"./normalize-request-path\";\nimport type { RequestMethod, Route } from \"./types\";\n\n/**\n * The concrete HTTP verbs an `all` route expands into.\n * Mirrors the set Fastify's `.all()` answers in production, so `router.any()`\n * routes behave identically under the dev server and prod Fastify.\n */\nconst ALL_METHODS: Exclude<RequestMethod, \"all\">[] = [\n \"GET\",\n \"POST\",\n \"PUT\",\n \"PATCH\",\n \"DELETE\",\n \"OPTIONS\",\n \"HEAD\",\n];\n\n/**\n * Route Registry\n * Manages dynamic route matching using find-my-way for HMR support\n */\nexport class RouteRegistry {\n private router: Instance<any>;\n\n public constructor() {\n this.router = FindMyWay({\n // Request paths are normalized explicitly in `find`; leaving this off\n // makes that shared normalizer, rather than a second router option, the\n // reason development accepts a terminal slash.\n ignoreTrailingSlash: false,\n caseSensitive: false,\n });\n }\n\n /**\n * Register all routes from the router's internal list\n */\n public register(routes: Route[]): void {\n // Register each route\n for (const route of routes) {\n if (route.method === \"all\") {\n for (const method of ALL_METHODS) {\n this.registerRoute({\n ...route,\n method,\n });\n }\n } else {\n this.registerRoute(route);\n }\n }\n }\n\n /**\n * Register a single route\n */\n public registerRoute(route: Route): void {\n this.router.on(route.method as HTTPMethod, route.path, (req, res, params) => {\n // Store the route and params for later use\n return { route, params };\n });\n }\n\n /**\n * Find a matching route for the given method and URL\n * @returns Matched route with extracted params, or null if no match\n */\n public find(\n method: string,\n url: string,\n ): { route: Route; params: Record<string, string> } | null {\n // Strip query string from URL (find-my-way expects just the path)\n const path = normalizeRequestPath(url.split(\"?\")[0] ?? \"/\");\n\n const match = this.router.find(method as HTTPMethod, path);\n\n if (!match) {\n return null;\n }\n\n // find-my-way handler expects (req, res, params, store, searchParams)\n // We only care about the return value which contains { route, params }\n return match.handler(null as any, null as any, match.params, match.store, {});\n }\n\n /**\n * Get all registered routes count (for debugging)\n */\n public getRouteCount(): number {\n return this.router\n .prettyPrint()\n .split(\"\\n\")\n .filter((line) => line.trim()).length;\n }\n}\n"],"mappings":";;;;;;;;;AASA,MAAM,cAA+C;CACnD;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;AAMA,IAAa,gBAAb,MAA2B;CAGzB,AAAO,cAAc;EACnB,KAAK,SAAS,UAAU;GAItB,qBAAqB;GACrB,eAAe;EACjB,CAAC;CACH;;;;CAKA,AAAO,SAAS,QAAuB;EAErC,KAAK,MAAM,SAAS,QAClB,IAAI,MAAM,WAAW,OACnB,KAAK,MAAM,UAAU,aACnB,KAAK,cAAc;GACjB,GAAG;GACH;EACF,CAAC;OAGH,KAAK,cAAc,KAAK;CAG9B;;;;CAKA,AAAO,cAAc,OAAoB;EACvC,KAAK,OAAO,GAAG,MAAM,QAAsB,MAAM,OAAO,KAAK,KAAK,WAAW;GAE3E,OAAO;IAAE;IAAO;GAAO;EACzB,CAAC;CACH;;;;;CAMA,AAAO,KACL,QACA,KACyD;EAEzD,MAAM,OAAO,qBAAqB,IAAI,MAAM,GAAG,CAAC,CAAC,MAAM,GAAG;EAE1D,MAAM,QAAQ,KAAK,OAAO,KAAK,QAAsB,IAAI;EAEzD,IAAI,CAAC,OACH,OAAO;EAKT,OAAO,MAAM,QAAQ,MAAa,MAAa,MAAM,QAAQ,MAAM,OAAO,CAAC,CAAC;CAC9E;;;;CAKA,AAAO,gBAAwB;EAC7B,OAAO,KAAK,OACT,YAAY,CAAC,CACb,MAAM,IAAI,CAAC,CACX,QAAQ,SAAS,KAAK,KAAK,CAAC,CAAC,CAAC;CACnC;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scoped-storage.d.mts","names":[],"sources":["../../../../../../../core/src/storage/scoped-storage.ts"],"mappings":";;;;;;;;;AA0CA;;;;;;;;;;;;;;;;;;cAAa,aAAA,YAAyB,qBAAA;EAkJzB;;;;EAAA,UA7ID,OAAA,EAAS,qBAAA;
|
|
1
|
+
{"version":3,"file":"scoped-storage.d.mts","names":[],"sources":["../../../../../../../core/src/storage/scoped-storage.ts"],"mappings":"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"}
|
|
@@ -177,6 +177,7 @@ var ScopedStorage = class {
|
|
|
177
177
|
const matches = dataUrl.match(/^data:([^;]+);base64,(.+)$/);
|
|
178
178
|
if (!matches) throw new Error("Invalid base64 data URL format. Expected: data:mime/type;base64,<data>");
|
|
179
179
|
const [, mimeType, base64Data] = matches;
|
|
180
|
+
if (mimeType === void 0 || base64Data === void 0) throw new Error("Invalid base64 data URL format. Expected: data:mime/type;base64,<data>");
|
|
180
181
|
const buffer = Buffer.from(base64Data, "base64");
|
|
181
182
|
return this.put(buffer, location, {
|
|
182
183
|
...options,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scoped-storage.mjs","names":["fs"],"sources":["../../../../../../../core/src/storage/scoped-storage.ts"],"sourcesContent":["import { fileExistsAsync } from \"@warlock.js/fs\";\nimport { createReadStream } from \"fs\";\nimport fs from \"fs/promises\";\nimport path from \"path\";\nimport type { Readable } from \"stream\";\nimport type { UploadedFile } from \"../http\";\nimport { StorageFile } from \"./storage-file\";\nimport type {\n DeleteManyResult,\n ListOptions,\n PutDirectoryOptions,\n PutDirectoryResult,\n PutFromUrlOptions,\n PutOptions,\n ScopedStorageContract,\n StorageDriverContract,\n StorageDriverType,\n StorageFileInfo,\n} from \"./types\";\nimport { safeFetchToBuffer } from \"./utils/safe-fetch\";\n\n/**\n * ScopedStorage - Base class for storage operations\n *\n * Wraps a storage driver and provides a consistent, developer-friendly API\n * that returns `StorageFile` instances instead of raw data objects.\n *\n * This class serves as the base for both direct driver usage and the\n * full `Storage` manager class.\n *\n * @example\n * ```typescript\n * // Using via storage.use()\n * const s3Storage = storage.use(\"s3\");\n * const file = await s3Storage.put(buffer, \"images/photo.jpg\");\n *\n * // file is a StorageFile instance with rich API\n * console.log(file.name); // \"photo.jpg\"\n * console.log(file.url); // \"https://...\"\n * await file.copy(\"backup/photo.jpg\");\n * ```\n */\nexport class ScopedStorage implements ScopedStorageContract {\n /**\n * The underlying storage driver instance\n * @internal\n */\n protected _driver: StorageDriverContract;\n\n /**\n * Create a new ScopedStorage instance\n *\n * @param driver - The storage driver to wrap\n */\n public constructor(driver: StorageDriverContract) {\n this._driver = driver;\n }\n\n // ============================================================\n // Properties\n // ============================================================\n\n /**\n * Get the driver name\n *\n * @returns The name identifier of the underlying driver (e.g., \"local\", \"s3\", \"r2\")\n */\n public get name(): StorageDriverType {\n return this.activeDriver.name;\n }\n\n /**\n * Get the default driver instance\n *\n * Use this for advanced operations that require direct driver access.\n *\n * @returns The raw storage driver\n */\n public get defaultDriver(): StorageDriverContract {\n return this._driver;\n }\n\n /**\n * Get the currently active driver\n *\n * Returns the driver being used for storage operations.\n * Can be overridden in subclasses for dynamic driver resolution (e.g., multi-tenant contexts).\n *\n * @returns The active storage driver\n */\n public get activeDriver(): StorageDriverContract {\n return this._driver;\n }\n\n // ============================================================\n // File Operations\n // ============================================================\n\n /**\n * Store a file in storage\n *\n * Accepts multiple input types and stores the file at the specified location.\n * Returns a `StorageFile` instance for further operations.\n *\n * @param file - File content as Buffer, string path, UploadedFile, or Readable stream\n * @param location - Destination path in storage (e.g., \"uploads/images/photo.jpg\")\n * @param options - Optional storage options\n * @returns StorageFile instance with cached metadata\n *\n * @example\n * ```typescript\n * // From buffer\n * const file = await storage.put(buffer, \"documents/report.pdf\");\n *\n * // From uploaded file\n * const file = await storage.put(uploadedFile, \"avatars/user-123.jpg\");\n *\n * // With options\n * const file = await storage.put(buffer, \"images/photo.jpg\", {\n * mimeType: \"image/jpeg\",\n * cacheControl: \"max-age=31536000\"\n * });\n * ```\n */\n public async put(\n file: UploadedFile | Buffer | string | Readable,\n location: string,\n options?: PutOptions,\n ): Promise<StorageFile> {\n const buffer = await this.toBuffer(file);\n const data = await this.activeDriver.put(buffer, location, options);\n return StorageFile.fromData(data, this.activeDriver);\n }\n\n /**\n * Store a file from a readable stream\n *\n * Optimized for large files - streams data directly without full buffering.\n * Ideal for file uploads, remote file fetching, or processing pipelines.\n *\n * @param stream - Readable stream of file content\n * @param location - Destination path in storage\n * @param options - Optional storage options\n * @returns StorageFile instance with cached metadata\n *\n * @example\n * ```typescript\n * import { createReadStream } from \"fs\";\n *\n * const stream = createReadStream(\"./large-video.mp4\");\n * const file = await storage.putStream(stream, \"videos/upload.mp4\");\n * ```\n */\n public async putStream(\n stream: Readable,\n location: string,\n options?: PutOptions,\n ): Promise<StorageFile> {\n const data = await this.activeDriver.putStream(stream, location, options);\n return StorageFile.fromData(data, this.activeDriver);\n }\n\n /**\n * Store a file from a URL\n *\n * Downloads the file from the URL and stores it. The download is\n * SSRF-guarded by default: the URL scheme must be https/http, the host\n * must not resolve to a private / loopback / link-local / cloud-metadata\n * address, the body is capped, and the request times out. Tune or relax\n * via the {@link PutFromUrlOptions} guard fields.\n *\n * @param url - URL to download from\n * @param location - Destination path in storage\n * @param options - Storage + outbound-download guard options\n * @returns StorageFile instance\n *\n * @example\n * ```typescript\n * const file = await storage.putFromUrl(\n * \"https://example.com/image.jpg\",\n * \"images/downloaded.jpg\"\n * );\n * ```\n */\n public async putFromUrl(\n url: string,\n location: string,\n options?: PutFromUrlOptions,\n ): Promise<StorageFile> {\n const { allowPrivateHosts, maxBytes, timeoutMs, allowedSchemes, ...putOptions } = options ?? {};\n\n const result = await safeFetchToBuffer(url, {\n allowPrivateHosts,\n maxBytes,\n timeoutMs,\n allowedSchemes,\n });\n\n if (!result.ok) {\n throw new Error(`Failed to fetch file from ${url}: ${result.statusText}`);\n }\n\n if (!result.contentType) {\n throw new Error(`Failed to fetch file from ${url}: missing content-type header`);\n }\n\n const mimeType = putOptions.mimeType || result.contentType;\n\n return this.put(result.buffer, location, { ...putOptions, mimeType });\n }\n\n /**\n * Store a file from base64 data URL\n *\n * @param dataUrl - Data URL (data:image/png;base64,iVBORw0KG...)\n * @param location - Destination path in storage\n * @param options - Optional storage options\n * @returns StorageFile instance\n *\n * @example\n * ```typescript\n * const file = await storage.putFromBase64(\n * \"data:image/png;base64,iVBORw0KGgoAAAANS...\",\n * \"images/upload.png\"\n * );\n * ```\n */\n public async putFromBase64(\n dataUrl: string,\n location: string,\n options?: PutOptions,\n ): Promise<StorageFile> {\n // Parse data URL: data:image/png;base64,iVBORw0KG...\n const matches = dataUrl.match(/^data:([^;]+);base64,(.+)$/);\n\n if (!matches) {\n throw new Error(\"Invalid base64 data URL format. Expected: data:mime/type;base64,<data>\");\n }\n\n const [, mimeType, base64Data] = matches;\n const buffer = Buffer.from(base64Data, \"base64\");\n\n return this.put(buffer, location, {\n ...options,\n mimeType: options?.mimeType || mimeType,\n });\n }\n\n /**\n * Retrieve file contents as a Buffer\n *\n * Downloads the entire file into memory. For large files,\n * consider using `getStream()` instead.\n *\n * @param location - Path to the file in storage\n * @returns Buffer containing file contents\n * @throws Error if file not found\n *\n * @example\n * ```typescript\n * const buffer = await storage.get(\"documents/report.pdf\");\n * const content = buffer.toString(\"utf-8\");\n * ```\n */\n public async get(location: string): Promise<Buffer> {\n return this.activeDriver.get(location);\n }\n\n /**\n * Retrieve file contents as a readable stream\n *\n * Streams file data without loading entire file into memory.\n * Ideal for large files or when piping to a response.\n *\n * @param location - Path to the file in storage\n * @returns Readable stream of file contents\n * @throws Error if file not found\n *\n * @example\n * ```typescript\n * const stream = await storage.getStream(\"videos/large.mp4\");\n * stream.pipe(response.raw);\n * ```\n */\n public async getStream(location: string): Promise<Readable> {\n return this.activeDriver.getStream(location);\n }\n\n /**\n * Delete a file from storage\n *\n * @param location - Path to the file, or a StorageFile instance\n * @returns `true` if deleted, `false` if file not found\n *\n * @example\n * ```typescript\n * // By path\n * await storage.delete(\"temp/old-file.txt\");\n *\n * // From StorageFile instance\n * const file = await storage.put(buffer, \"temp/file.txt\");\n * await storage.delete(file);\n * ```\n */\n public async delete(location: string | StorageFile): Promise<boolean> {\n const path = typeof location === \"string\" ? location : location.path;\n return this.activeDriver.delete(path);\n }\n\n /**\n * Delete multiple files at once\n *\n * Performs batch deletion for efficiency. Returns results for each file\n * including success/failure status.\n *\n * @param locations - Array of file paths to delete\n * @returns Array of delete results with status for each file\n *\n * @example\n * ```typescript\n * const results = await storage.deleteMany([\n * \"temp/file1.txt\",\n * \"temp/file2.txt\",\n * \"temp/file3.txt\"\n * ]);\n *\n * for (const result of results) {\n * console.log(`${result.location}: ${result.deleted ? \"deleted\" : result.error}`);\n * }\n * ```\n */\n public async deleteMany(locations: string[]): Promise<DeleteManyResult[]> {\n return this.activeDriver.deleteMany(locations);\n }\n\n /**\n * Delete a directory\n *\n * @param directoryPath - Path to the directory\n */\n public async deleteDirectory(directoryPath: string): Promise<boolean> {\n return await this.activeDriver.deleteDirectory(directoryPath);\n }\n\n /**\n * Check if a file exists in storage\n *\n * @param location - Path to check\n * @returns `true` if file exists, `false` otherwise\n *\n * @example\n * ```typescript\n * if (await storage.exists(\"config/settings.json\")) {\n * const config = await storage.get(\"config/settings.json\");\n * }\n * ```\n */\n public async exists(location: string): Promise<boolean> {\n return this.activeDriver.exists(location);\n }\n\n /**\n * Copy a file to a new location\n *\n * Creates a copy of the file at the destination path.\n * The original file remains unchanged.\n *\n * @param from - Source path or StorageFile instance\n * @param to - Destination path\n * @returns StorageFile instance at the new location\n *\n * @example\n * ```typescript\n * // Copy by path\n * const backup = await storage.copy(\"documents/report.pdf\", \"backups/report.pdf\");\n *\n * // Copy from StorageFile\n * const original = await storage.file(\"documents/report.pdf\");\n * const backup = await storage.copy(original, \"backups/report.pdf\");\n * ```\n */\n public async copy(from: string | StorageFile, to: string): Promise<StorageFile> {\n const fromPath = typeof from === \"string\" ? from : from.path;\n const data = await this.activeDriver.copy(fromPath, to);\n return StorageFile.fromData(data, this.activeDriver);\n }\n\n /**\n * Move a file to a new location\n *\n * Moves the file to the destination path. The original file\n * is deleted after successful copy.\n *\n * @param from - Source path or StorageFile instance\n * @param to - Destination path\n * @returns StorageFile instance at the new location\n *\n * @example\n * ```typescript\n * // Move by path\n * const file = await storage.move(\"uploads/temp.jpg\", \"images/photo.jpg\");\n *\n * // Move from StorageFile\n * const temp = await storage.file(\"uploads/temp.jpg\");\n * const final = await storage.move(temp, \"images/photo.jpg\");\n * ```\n */\n public async move(from: string | StorageFile, to: string): Promise<StorageFile> {\n const fromPath = typeof from === \"string\" ? from : from.path;\n const data = await this.activeDriver.move(fromPath, to);\n return StorageFile.fromData(data, this.activeDriver);\n }\n\n /**\n * Copy an entire directory recursively\n *\n * Copies all files from the source directory to the destination directory,\n * preserving the directory structure.\n *\n * @param from - Source directory path\n * @param to - Destination directory path\n * @param options - Optional concurrency control\n * @returns Number of files copied\n *\n * @example\n * ```typescript\n * // Copy entire directory\n * const count = await storage.copyDirectory(\"uploads/temp\", \"uploads/final\");\n * console.log(`Copied ${count} files`);\n *\n * // With concurrency limit\n * const count = await storage.copyDirectory(\"large-dir\", \"backup\", {\n * concurrency: 10\n * });\n * ```\n */\n public async copyDirectory(\n from: string,\n to: string,\n options?: { concurrency?: number },\n ): Promise<number> {\n const concurrency = options?.concurrency || 5;\n\n // List all files recursively\n const files = await this.list(from, { recursive: true });\n const filesToCopy = files.filter((f) => !f.isDirectory);\n\n // Copy files in batches for efficiency\n let copied = 0;\n for (let i = 0; i < filesToCopy.length; i += concurrency) {\n const batch = filesToCopy.slice(i, i + concurrency);\n await Promise.all(\n batch.map(async (file) => {\n // Calculate relative path and new destination\n const relativePath = file.path.substring(from.length).replace(/^\\//, \"\");\n const newPath = `${to}/${relativePath}`;\n await this.copy(file.path, newPath);\n copied++;\n }),\n );\n }\n\n return copied;\n }\n\n /**\n * Move an entire directory recursively\n *\n * Moves all files from the source directory to the destination directory,\n * then deletes the source directory.\n *\n * @param from - Source directory path\n * @param to - Destination directory path\n * @param options - Optional concurrency control\n * @returns Number of files moved\n *\n * @example\n * ```typescript\n * const count = await storage.moveDirectory(\"uploads/temp\", \"uploads/final\");\n * console.log(`Moved ${count} files`);\n * ```\n */\n public async moveDirectory(\n from: string,\n to: string,\n options?: { concurrency?: number },\n ): Promise<number> {\n // Copy all files first\n const count = await this.copyDirectory(from, to, options);\n\n // Delete source directory\n await this.deleteDirectory(from);\n\n return count;\n }\n\n /**\n * Upload a local filesystem directory into storage\n *\n * Recursively walks the local directory, applies an optional filter, then\n * streams each file into storage. Uploads run in concurrent batches for\n * efficiency. Failures are collected — a single failed file never aborts\n * the entire operation (mirrors the contract of `deleteMany`).\n *\n * @param localDirPath - Absolute path of the local directory to upload\n * @param destination - Target prefix in storage (e.g. \"uploads/assets\")\n * @param options - Concurrency, filter, progress callback, put options\n * @returns - { uploaded, failed, total }\n *\n * @example\n * ```typescript\n * const result = await storage.putDirectory(\"./public/assets\", \"cdn/assets\", {\n * concurrency: 10,\n * filter: (_, rel) => !rel.startsWith(\".\"),\n * onProgress: (done, total) => console.log(`${done}/${total}`),\n * });\n *\n * console.log(`Uploaded: ${result.uploaded.length}, Failed: ${result.failed.length}`);\n * ```\n */\n public async putDirectory(\n localDirPath: string,\n destination: string,\n options?: PutDirectoryOptions,\n ): Promise<PutDirectoryResult> {\n const concurrency = options?.concurrency ?? 5;\n\n // Collect all local file paths recursively\n const localFiles = await this.walkLocalDirectory(localDirPath);\n\n // Apply the user-supplied filter if any\n const filteredFiles = options?.filter\n ? localFiles.filter(({ absolute, relative }) => options.filter!(absolute, relative))\n : localFiles;\n\n const total = filteredFiles.length;\n const uploaded: StorageFile[] = [];\n const failed: Array<{ localPath: string; error: Error }> = [];\n let doneCount = 0;\n\n // Upload in concurrent batches\n for (let i = 0; i < filteredFiles.length; i += concurrency) {\n const batch = filteredFiles.slice(i, i + concurrency);\n\n await Promise.all(\n batch.map(async ({ absolute, relative }) => {\n const storagePath = `${destination.replace(/\\/$/, \"\")}/${relative}`;\n\n try {\n const stream = createReadStream(absolute);\n const file = await this.putStream(stream, storagePath, options?.putOptions);\n uploaded.push(file);\n doneCount++;\n options?.onProgress?.(doneCount, total, file);\n } catch (err) {\n failed.push({\n localPath: absolute,\n error: err instanceof Error ? err : new Error(String(err)),\n });\n }\n }),\n );\n }\n\n return { uploaded, failed, total };\n }\n\n /**\n * Walk a local directory recursively and return all file paths\n *\n * @param dirPath - Absolute local directory path\n * @returns Array of { absolute, relative } file path pairs\n * @internal\n */\n private async walkLocalDirectory(\n dirPath: string,\n baseDir?: string,\n ): Promise<Array<{ absolute: string; relative: string }>> {\n const root = baseDir ?? dirPath;\n const entries = await fs.readdir(dirPath, { withFileTypes: true });\n const results: Array<{ absolute: string; relative: string }> = [];\n\n for (const entry of entries) {\n const absolute = path.join(dirPath, entry.name);\n const relative = path.relative(root, absolute).replace(/\\\\/g, \"/\");\n\n if (entry.isDirectory()) {\n const nested = await this.walkLocalDirectory(absolute, root);\n results.push(...nested);\n } else if (entry.isFile()) {\n results.push({ absolute, relative });\n }\n // Symlinks are intentionally skipped\n }\n\n return results;\n }\n\n /**\n * Empty a directory without deleting the directory itself\n *\n * Deletes all files within the directory but preserves the directory structure.\n *\n * @param path - Directory path to empty\n * @returns Number of files deleted\n *\n * @example\n * ```typescript\n * const count = await storage.emptyDirectory(\"uploads/temp\");\n * console.log(`Deleted ${count} files`);\n * ```\n */\n public async emptyDirectory(path: string): Promise<number> {\n // List all files in directory\n const files = await this.list(path, { recursive: true });\n const filePaths = files.filter((f) => !f.isDirectory).map((f) => f.path);\n\n if (filePaths.length === 0) {\n return 0;\n }\n\n // Delete all files\n await this.deleteMany(filePaths);\n\n return filePaths.length;\n }\n\n /**\n * List files in a directory\n *\n * Returns file information for all files in the specified directory.\n * Supports recursive listing and pagination.\n *\n * @param directory - Directory path (defaults to root)\n * @param options - List options (recursive, limit, cursor)\n * @returns Array of file information objects\n *\n * @example\n * ```typescript\n * // List all files in uploads\n * const files = await storage.list(\"uploads\");\n *\n * // Recursive listing with limit\n * const files = await storage.list(\"uploads\", {\n * recursive: true,\n * limit: 100\n * });\n * ```\n */\n public async list(directory?: string, options?: ListOptions): Promise<StorageFileInfo[]> {\n return this.activeDriver.list(directory || \"\", options);\n }\n\n // ============================================================\n // URL Operations\n // ============================================================\n\n /**\n * Get the public URL for a file\n *\n * Returns the URL where the file can be accessed. For local storage,\n * this is typically a path prefix. For cloud storage, this is the\n * bucket URL or CDN URL.\n *\n * @param location - File path\n * @returns Public URL string\n *\n * @example\n * ```typescript\n * const url = storage.url(\"images/photo.jpg\");\n * // Local: \"/uploads/images/photo.jpg\"\n * // S3: \"https://bucket.s3.amazonaws.com/images/photo.jpg\"\n * ```\n */\n public url(location: string): string {\n return this.activeDriver.url(location);\n }\n\n /**\n * Get a temporary signed URL with expiration\n *\n * Creates a URL that provides temporary access to the file.\n * For cloud storage, this uses presigned URLs.\n * For local storage, this uses HMAC-signed tokens.\n *\n * @param location - File path\n * @param expiresIn - Seconds until URL expires (default: 3600)\n * @returns Signed URL string\n *\n * @example\n * ```typescript\n * // URL valid for 1 hour\n * const url = await storage.temporaryUrl(\"private/document.pdf\");\n *\n * // URL valid for 24 hours\n * const url = await storage.temporaryUrl(\"private/document.pdf\", 86400);\n * ```\n */\n public async temporaryUrl(location: string, expiresIn?: number): Promise<string> {\n return this.activeDriver.temporaryUrl(location, expiresIn);\n }\n\n // ============================================================\n // Metadata Operations\n // ============================================================\n\n /**\n * Get file metadata without downloading the file\n *\n * Retrieves information about a file including size, last modified date,\n * and MIME type without downloading the file contents.\n *\n * @param location - File path\n * @returns File information object\n * @throws Error if file not found\n *\n * @example\n * ```typescript\n * const info = await storage.metadata(\"documents/report.pdf\");\n * console.log(`Size: ${info.size} bytes`);\n * console.log(`Type: ${info.mimeType}`);\n * console.log(`Modified: ${info.lastModified}`);\n * ```\n */\n public async metadata(location: string): Promise<StorageFileInfo> {\n return this.activeDriver.metadata(location);\n }\n\n /**\n * Get file size in bytes\n *\n * Shortcut for `metadata(location).size`.\n *\n * @param location - File path\n * @returns File size in bytes\n * @throws Error if file not found\n */\n public async size(location: string): Promise<number> {\n return this.activeDriver.size(location);\n }\n\n /**\n * Get a StorageFile instance for OOP-style operations\n *\n * Creates a `StorageFile` wrapper for the specified path,\n * allowing fluent method chaining for file operations.\n *\n * @param location - File path\n * @returns StorageFile instance\n *\n * @example\n * ```typescript\n * const file = await storage.file(\"uploads/image.jpg\");\n *\n * // Properties\n * console.log(file.name); // \"image.jpg\"\n * console.log(file.extension); // \"jpg\"\n *\n * // Operations\n * await file.copy(\"backup/image.jpg\");\n * await file.delete();\n * ```\n */\n public file(location: string): StorageFile {\n return new StorageFile(location, this.activeDriver);\n }\n\n // ============================================================\n // Utilities\n // ============================================================\n\n /**\n * Convert various input types to Buffer\n *\n * @param file - Input file in various formats\n * @returns Buffer containing file contents\n * @internal\n */\n protected async toBuffer(file: UploadedFile | Buffer | string | Readable): Promise<Buffer> {\n // Already a buffer\n if (Buffer.isBuffer(file)) {\n return file;\n }\n\n // Readable stream - collect into buffer\n if (this.isReadable(file)) {\n return this.streamToBuffer(file as Readable);\n }\n\n // String content\n if (typeof file === \"string\") {\n if (await fileExistsAsync(file)) {\n return fs.readFile(file);\n }\n\n return Buffer.from(file);\n }\n\n // UploadedFile\n return (file as UploadedFile).buffer();\n }\n\n /**\n * Check if value is a Readable stream\n *\n * @param value - Value to check\n * @returns True if value is a Readable stream\n * @internal\n */\n protected isReadable(value: unknown): value is Readable {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"pipe\" in value &&\n typeof (value as Readable).pipe === \"function\"\n );\n }\n\n /**\n * Convert a Readable stream to Buffer\n *\n * @param stream - Readable stream\n * @returns Buffer containing stream contents\n * @internal\n */\n protected async streamToBuffer(stream: Readable): Promise<Buffer> {\n const chunks: Buffer[] = [];\n for await (const chunk of stream) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\n }\n return Buffer.concat(chunks as unknown as Uint8Array[]);\n }\n\n /**\n * Prepend a prefix to a location path\n *\n * Useful for organizing files into directories.\n *\n * @param prefix - Prefix to add (e.g., \"uploads\")\n * @param location - Original location path\n * @returns Combined path with prefix\n *\n * @example\n * ```typescript\n * storage.prepend(\"uploads\", \"image.jpg\"); // \"uploads/image.jpg\"\n * storage.prepend(\"uploads/\", \"/image.jpg\"); // \"uploads/image.jpg\"\n * ```\n */\n public prepend(prefix: string, location: string): string {\n return `${prefix.replace(/\\/$/, \"\")}/${location.replace(/^\\//, \"\")}`;\n }\n\n /**\n * Append a suffix to a location path (before extension)\n *\n * Useful for creating variants of files (thumbnails, etc.).\n *\n * @param location - Original location path\n * @param suffix - Suffix to add before extension\n * @returns Path with suffix added before extension\n *\n * @example\n * ```typescript\n * storage.append(\"image.jpg\", \"_thumb\"); // \"image_thumb.jpg\"\n * storage.append(\"document.pdf\", \"_v2\"); // \"document_v2.pdf\"\n * ```\n */\n public append(location: string, suffix: string): string {\n const lastDot = location.lastIndexOf(\".\");\n if (lastDot === -1) {\n return `${location}${suffix}`;\n }\n return `${location.substring(0, lastDot)}${suffix}${location.substring(lastDot)}`;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"scoped-storage.mjs","names":["fs"],"sources":["../../../../../../../core/src/storage/scoped-storage.ts"],"sourcesContent":["import { fileExistsAsync } from \"@warlock.js/fs\";\nimport { createReadStream } from \"fs\";\nimport fs from \"fs/promises\";\nimport path from \"path\";\nimport type { Readable } from \"stream\";\nimport type { UploadedFile } from \"../http\";\nimport { StorageFile } from \"./storage-file\";\nimport type {\n DeleteManyResult,\n ListOptions,\n PutDirectoryOptions,\n PutDirectoryResult,\n PutFromUrlOptions,\n PutOptions,\n ScopedStorageContract,\n StorageDriverContract,\n StorageDriverType,\n StorageFileInfo,\n} from \"./types\";\nimport { safeFetchToBuffer } from \"./utils/safe-fetch\";\n\n/**\n * ScopedStorage - Base class for storage operations\n *\n * Wraps a storage driver and provides a consistent, developer-friendly API\n * that returns `StorageFile` instances instead of raw data objects.\n *\n * This class serves as the base for both direct driver usage and the\n * full `Storage` manager class.\n *\n * @example\n * ```typescript\n * // Using via storage.use()\n * const s3Storage = storage.use(\"s3\");\n * const file = await s3Storage.put(buffer, \"images/photo.jpg\");\n *\n * // file is a StorageFile instance with rich API\n * console.log(file.name); // \"photo.jpg\"\n * console.log(file.url); // \"https://...\"\n * await file.copy(\"backup/photo.jpg\");\n * ```\n */\nexport class ScopedStorage implements ScopedStorageContract {\n /**\n * The underlying storage driver instance\n * @internal\n */\n protected _driver: StorageDriverContract;\n\n /**\n * Create a new ScopedStorage instance\n *\n * @param driver - The storage driver to wrap\n */\n public constructor(driver: StorageDriverContract) {\n this._driver = driver;\n }\n\n // ============================================================\n // Properties\n // ============================================================\n\n /**\n * Get the driver name\n *\n * @returns The name identifier of the underlying driver (e.g., \"local\", \"s3\", \"r2\")\n */\n public get name(): StorageDriverType {\n return this.activeDriver.name;\n }\n\n /**\n * Get the default driver instance\n *\n * Use this for advanced operations that require direct driver access.\n *\n * @returns The raw storage driver\n */\n public get defaultDriver(): StorageDriverContract {\n return this._driver;\n }\n\n /**\n * Get the currently active driver\n *\n * Returns the driver being used for storage operations.\n * Can be overridden in subclasses for dynamic driver resolution (e.g., multi-tenant contexts).\n *\n * @returns The active storage driver\n */\n public get activeDriver(): StorageDriverContract {\n return this._driver;\n }\n\n // ============================================================\n // File Operations\n // ============================================================\n\n /**\n * Store a file in storage\n *\n * Accepts multiple input types and stores the file at the specified location.\n * Returns a `StorageFile` instance for further operations.\n *\n * @param file - File content as Buffer, string path, UploadedFile, or Readable stream\n * @param location - Destination path in storage (e.g., \"uploads/images/photo.jpg\")\n * @param options - Optional storage options\n * @returns StorageFile instance with cached metadata\n *\n * @example\n * ```typescript\n * // From buffer\n * const file = await storage.put(buffer, \"documents/report.pdf\");\n *\n * // From uploaded file\n * const file = await storage.put(uploadedFile, \"avatars/user-123.jpg\");\n *\n * // With options\n * const file = await storage.put(buffer, \"images/photo.jpg\", {\n * mimeType: \"image/jpeg\",\n * cacheControl: \"max-age=31536000\"\n * });\n * ```\n */\n public async put(\n file: UploadedFile | Buffer | string | Readable,\n location: string,\n options?: PutOptions,\n ): Promise<StorageFile> {\n const buffer = await this.toBuffer(file);\n const data = await this.activeDriver.put(buffer, location, options);\n return StorageFile.fromData(data, this.activeDriver);\n }\n\n /**\n * Store a file from a readable stream\n *\n * Optimized for large files - streams data directly without full buffering.\n * Ideal for file uploads, remote file fetching, or processing pipelines.\n *\n * @param stream - Readable stream of file content\n * @param location - Destination path in storage\n * @param options - Optional storage options\n * @returns StorageFile instance with cached metadata\n *\n * @example\n * ```typescript\n * import { createReadStream } from \"fs\";\n *\n * const stream = createReadStream(\"./large-video.mp4\");\n * const file = await storage.putStream(stream, \"videos/upload.mp4\");\n * ```\n */\n public async putStream(\n stream: Readable,\n location: string,\n options?: PutOptions,\n ): Promise<StorageFile> {\n const data = await this.activeDriver.putStream(stream, location, options);\n return StorageFile.fromData(data, this.activeDriver);\n }\n\n /**\n * Store a file from a URL\n *\n * Downloads the file from the URL and stores it. The download is\n * SSRF-guarded by default: the URL scheme must be https/http, the host\n * must not resolve to a private / loopback / link-local / cloud-metadata\n * address, the body is capped, and the request times out. Tune or relax\n * via the {@link PutFromUrlOptions} guard fields.\n *\n * @param url - URL to download from\n * @param location - Destination path in storage\n * @param options - Storage + outbound-download guard options\n * @returns StorageFile instance\n *\n * @example\n * ```typescript\n * const file = await storage.putFromUrl(\n * \"https://example.com/image.jpg\",\n * \"images/downloaded.jpg\"\n * );\n * ```\n */\n public async putFromUrl(\n url: string,\n location: string,\n options?: PutFromUrlOptions,\n ): Promise<StorageFile> {\n const { allowPrivateHosts, maxBytes, timeoutMs, allowedSchemes, ...putOptions } = options ?? {};\n\n const result = await safeFetchToBuffer(url, {\n allowPrivateHosts,\n maxBytes,\n timeoutMs,\n allowedSchemes,\n });\n\n if (!result.ok) {\n throw new Error(`Failed to fetch file from ${url}: ${result.statusText}`);\n }\n\n if (!result.contentType) {\n throw new Error(`Failed to fetch file from ${url}: missing content-type header`);\n }\n\n const mimeType = putOptions.mimeType || result.contentType;\n\n return this.put(result.buffer, location, { ...putOptions, mimeType });\n }\n\n /**\n * Store a file from base64 data URL\n *\n * @param dataUrl - Data URL (data:image/png;base64,iVBORw0KG...)\n * @param location - Destination path in storage\n * @param options - Optional storage options\n * @returns StorageFile instance\n *\n * @example\n * ```typescript\n * const file = await storage.putFromBase64(\n * \"data:image/png;base64,iVBORw0KGgoAAAANS...\",\n * \"images/upload.png\"\n * );\n * ```\n */\n public async putFromBase64(\n dataUrl: string,\n location: string,\n options?: PutOptions,\n ): Promise<StorageFile> {\n // Parse data URL: data:image/png;base64,iVBORw0KG...\n const matches = dataUrl.match(/^data:([^;]+);base64,(.+)$/);\n\n if (!matches) {\n throw new Error(\"Invalid base64 data URL format. Expected: data:mime/type;base64,<data>\");\n }\n\n const [, mimeType, base64Data] = matches;\n\n if (mimeType === undefined || base64Data === undefined) {\n throw new Error(\"Invalid base64 data URL format. Expected: data:mime/type;base64,<data>\");\n }\n\n const buffer = Buffer.from(base64Data, \"base64\");\n\n return this.put(buffer, location, {\n ...options,\n mimeType: options?.mimeType || mimeType,\n });\n }\n\n /**\n * Retrieve file contents as a Buffer\n *\n * Downloads the entire file into memory. For large files,\n * consider using `getStream()` instead.\n *\n * @param location - Path to the file in storage\n * @returns Buffer containing file contents\n * @throws Error if file not found\n *\n * @example\n * ```typescript\n * const buffer = await storage.get(\"documents/report.pdf\");\n * const content = buffer.toString(\"utf-8\");\n * ```\n */\n public async get(location: string): Promise<Buffer> {\n return this.activeDriver.get(location);\n }\n\n /**\n * Retrieve file contents as a readable stream\n *\n * Streams file data without loading entire file into memory.\n * Ideal for large files or when piping to a response.\n *\n * @param location - Path to the file in storage\n * @returns Readable stream of file contents\n * @throws Error if file not found\n *\n * @example\n * ```typescript\n * const stream = await storage.getStream(\"videos/large.mp4\");\n * stream.pipe(response.raw);\n * ```\n */\n public async getStream(location: string): Promise<Readable> {\n return this.activeDriver.getStream(location);\n }\n\n /**\n * Delete a file from storage\n *\n * @param location - Path to the file, or a StorageFile instance\n * @returns `true` if deleted, `false` if file not found\n *\n * @example\n * ```typescript\n * // By path\n * await storage.delete(\"temp/old-file.txt\");\n *\n * // From StorageFile instance\n * const file = await storage.put(buffer, \"temp/file.txt\");\n * await storage.delete(file);\n * ```\n */\n public async delete(location: string | StorageFile): Promise<boolean> {\n const path = typeof location === \"string\" ? location : location.path;\n return this.activeDriver.delete(path);\n }\n\n /**\n * Delete multiple files at once\n *\n * Performs batch deletion for efficiency. Returns results for each file\n * including success/failure status.\n *\n * @param locations - Array of file paths to delete\n * @returns Array of delete results with status for each file\n *\n * @example\n * ```typescript\n * const results = await storage.deleteMany([\n * \"temp/file1.txt\",\n * \"temp/file2.txt\",\n * \"temp/file3.txt\"\n * ]);\n *\n * for (const result of results) {\n * console.log(`${result.location}: ${result.deleted ? \"deleted\" : result.error}`);\n * }\n * ```\n */\n public async deleteMany(locations: string[]): Promise<DeleteManyResult[]> {\n return this.activeDriver.deleteMany(locations);\n }\n\n /**\n * Delete a directory\n *\n * @param directoryPath - Path to the directory\n */\n public async deleteDirectory(directoryPath: string): Promise<boolean> {\n return await this.activeDriver.deleteDirectory(directoryPath);\n }\n\n /**\n * Check if a file exists in storage\n *\n * @param location - Path to check\n * @returns `true` if file exists, `false` otherwise\n *\n * @example\n * ```typescript\n * if (await storage.exists(\"config/settings.json\")) {\n * const config = await storage.get(\"config/settings.json\");\n * }\n * ```\n */\n public async exists(location: string): Promise<boolean> {\n return this.activeDriver.exists(location);\n }\n\n /**\n * Copy a file to a new location\n *\n * Creates a copy of the file at the destination path.\n * The original file remains unchanged.\n *\n * @param from - Source path or StorageFile instance\n * @param to - Destination path\n * @returns StorageFile instance at the new location\n *\n * @example\n * ```typescript\n * // Copy by path\n * const backup = await storage.copy(\"documents/report.pdf\", \"backups/report.pdf\");\n *\n * // Copy from StorageFile\n * const original = await storage.file(\"documents/report.pdf\");\n * const backup = await storage.copy(original, \"backups/report.pdf\");\n * ```\n */\n public async copy(from: string | StorageFile, to: string): Promise<StorageFile> {\n const fromPath = typeof from === \"string\" ? from : from.path;\n const data = await this.activeDriver.copy(fromPath, to);\n return StorageFile.fromData(data, this.activeDriver);\n }\n\n /**\n * Move a file to a new location\n *\n * Moves the file to the destination path. The original file\n * is deleted after successful copy.\n *\n * @param from - Source path or StorageFile instance\n * @param to - Destination path\n * @returns StorageFile instance at the new location\n *\n * @example\n * ```typescript\n * // Move by path\n * const file = await storage.move(\"uploads/temp.jpg\", \"images/photo.jpg\");\n *\n * // Move from StorageFile\n * const temp = await storage.file(\"uploads/temp.jpg\");\n * const final = await storage.move(temp, \"images/photo.jpg\");\n * ```\n */\n public async move(from: string | StorageFile, to: string): Promise<StorageFile> {\n const fromPath = typeof from === \"string\" ? from : from.path;\n const data = await this.activeDriver.move(fromPath, to);\n return StorageFile.fromData(data, this.activeDriver);\n }\n\n /**\n * Copy an entire directory recursively\n *\n * Copies all files from the source directory to the destination directory,\n * preserving the directory structure.\n *\n * @param from - Source directory path\n * @param to - Destination directory path\n * @param options - Optional concurrency control\n * @returns Number of files copied\n *\n * @example\n * ```typescript\n * // Copy entire directory\n * const count = await storage.copyDirectory(\"uploads/temp\", \"uploads/final\");\n * console.log(`Copied ${count} files`);\n *\n * // With concurrency limit\n * const count = await storage.copyDirectory(\"large-dir\", \"backup\", {\n * concurrency: 10\n * });\n * ```\n */\n public async copyDirectory(\n from: string,\n to: string,\n options?: { concurrency?: number },\n ): Promise<number> {\n const concurrency = options?.concurrency || 5;\n\n // List all files recursively\n const files = await this.list(from, { recursive: true });\n const filesToCopy = files.filter((f) => !f.isDirectory);\n\n // Copy files in batches for efficiency\n let copied = 0;\n for (let i = 0; i < filesToCopy.length; i += concurrency) {\n const batch = filesToCopy.slice(i, i + concurrency);\n await Promise.all(\n batch.map(async (file) => {\n // Calculate relative path and new destination\n const relativePath = file.path.substring(from.length).replace(/^\\//, \"\");\n const newPath = `${to}/${relativePath}`;\n await this.copy(file.path, newPath);\n copied++;\n }),\n );\n }\n\n return copied;\n }\n\n /**\n * Move an entire directory recursively\n *\n * Moves all files from the source directory to the destination directory,\n * then deletes the source directory.\n *\n * @param from - Source directory path\n * @param to - Destination directory path\n * @param options - Optional concurrency control\n * @returns Number of files moved\n *\n * @example\n * ```typescript\n * const count = await storage.moveDirectory(\"uploads/temp\", \"uploads/final\");\n * console.log(`Moved ${count} files`);\n * ```\n */\n public async moveDirectory(\n from: string,\n to: string,\n options?: { concurrency?: number },\n ): Promise<number> {\n // Copy all files first\n const count = await this.copyDirectory(from, to, options);\n\n // Delete source directory\n await this.deleteDirectory(from);\n\n return count;\n }\n\n /**\n * Upload a local filesystem directory into storage\n *\n * Recursively walks the local directory, applies an optional filter, then\n * streams each file into storage. Uploads run in concurrent batches for\n * efficiency. Failures are collected — a single failed file never aborts\n * the entire operation (mirrors the contract of `deleteMany`).\n *\n * @param localDirPath - Absolute path of the local directory to upload\n * @param destination - Target prefix in storage (e.g. \"uploads/assets\")\n * @param options - Concurrency, filter, progress callback, put options\n * @returns - { uploaded, failed, total }\n *\n * @example\n * ```typescript\n * const result = await storage.putDirectory(\"./public/assets\", \"cdn/assets\", {\n * concurrency: 10,\n * filter: (_, rel) => !rel.startsWith(\".\"),\n * onProgress: (done, total) => console.log(`${done}/${total}`),\n * });\n *\n * console.log(`Uploaded: ${result.uploaded.length}, Failed: ${result.failed.length}`);\n * ```\n */\n public async putDirectory(\n localDirPath: string,\n destination: string,\n options?: PutDirectoryOptions,\n ): Promise<PutDirectoryResult> {\n const concurrency = options?.concurrency ?? 5;\n\n // Collect all local file paths recursively\n const localFiles = await this.walkLocalDirectory(localDirPath);\n\n // Apply the user-supplied filter if any\n const filteredFiles = options?.filter\n ? localFiles.filter(({ absolute, relative }) => options.filter!(absolute, relative))\n : localFiles;\n\n const total = filteredFiles.length;\n const uploaded: StorageFile[] = [];\n const failed: Array<{ localPath: string; error: Error }> = [];\n let doneCount = 0;\n\n // Upload in concurrent batches\n for (let i = 0; i < filteredFiles.length; i += concurrency) {\n const batch = filteredFiles.slice(i, i + concurrency);\n\n await Promise.all(\n batch.map(async ({ absolute, relative }) => {\n const storagePath = `${destination.replace(/\\/$/, \"\")}/${relative}`;\n\n try {\n const stream = createReadStream(absolute);\n const file = await this.putStream(stream, storagePath, options?.putOptions);\n uploaded.push(file);\n doneCount++;\n options?.onProgress?.(doneCount, total, file);\n } catch (err) {\n failed.push({\n localPath: absolute,\n error: err instanceof Error ? err : new Error(String(err)),\n });\n }\n }),\n );\n }\n\n return { uploaded, failed, total };\n }\n\n /**\n * Walk a local directory recursively and return all file paths\n *\n * @param dirPath - Absolute local directory path\n * @returns Array of { absolute, relative } file path pairs\n * @internal\n */\n private async walkLocalDirectory(\n dirPath: string,\n baseDir?: string,\n ): Promise<Array<{ absolute: string; relative: string }>> {\n const root = baseDir ?? dirPath;\n const entries = await fs.readdir(dirPath, { withFileTypes: true });\n const results: Array<{ absolute: string; relative: string }> = [];\n\n for (const entry of entries) {\n const absolute = path.join(dirPath, entry.name);\n const relative = path.relative(root, absolute).replace(/\\\\/g, \"/\");\n\n if (entry.isDirectory()) {\n const nested = await this.walkLocalDirectory(absolute, root);\n results.push(...nested);\n } else if (entry.isFile()) {\n results.push({ absolute, relative });\n }\n // Symlinks are intentionally skipped\n }\n\n return results;\n }\n\n /**\n * Empty a directory without deleting the directory itself\n *\n * Deletes all files within the directory but preserves the directory structure.\n *\n * @param path - Directory path to empty\n * @returns Number of files deleted\n *\n * @example\n * ```typescript\n * const count = await storage.emptyDirectory(\"uploads/temp\");\n * console.log(`Deleted ${count} files`);\n * ```\n */\n public async emptyDirectory(path: string): Promise<number> {\n // List all files in directory\n const files = await this.list(path, { recursive: true });\n const filePaths = files.filter((f) => !f.isDirectory).map((f) => f.path);\n\n if (filePaths.length === 0) {\n return 0;\n }\n\n // Delete all files\n await this.deleteMany(filePaths);\n\n return filePaths.length;\n }\n\n /**\n * List files in a directory\n *\n * Returns file information for all files in the specified directory.\n * Supports recursive listing and pagination.\n *\n * @param directory - Directory path (defaults to root)\n * @param options - List options (recursive, limit, cursor)\n * @returns Array of file information objects\n *\n * @example\n * ```typescript\n * // List all files in uploads\n * const files = await storage.list(\"uploads\");\n *\n * // Recursive listing with limit\n * const files = await storage.list(\"uploads\", {\n * recursive: true,\n * limit: 100\n * });\n * ```\n */\n public async list(directory?: string, options?: ListOptions): Promise<StorageFileInfo[]> {\n return this.activeDriver.list(directory || \"\", options);\n }\n\n // ============================================================\n // URL Operations\n // ============================================================\n\n /**\n * Get the public URL for a file\n *\n * Returns the URL where the file can be accessed. For local storage,\n * this is typically a path prefix. For cloud storage, this is the\n * bucket URL or CDN URL.\n *\n * @param location - File path\n * @returns Public URL string\n *\n * @example\n * ```typescript\n * const url = storage.url(\"images/photo.jpg\");\n * // Local: \"/uploads/images/photo.jpg\"\n * // S3: \"https://bucket.s3.amazonaws.com/images/photo.jpg\"\n * ```\n */\n public url(location: string): string {\n return this.activeDriver.url(location);\n }\n\n /**\n * Get a temporary signed URL with expiration\n *\n * Creates a URL that provides temporary access to the file.\n * For cloud storage, this uses presigned URLs.\n * For local storage, this uses HMAC-signed tokens.\n *\n * @param location - File path\n * @param expiresIn - Seconds until URL expires (default: 3600)\n * @returns Signed URL string\n *\n * @example\n * ```typescript\n * // URL valid for 1 hour\n * const url = await storage.temporaryUrl(\"private/document.pdf\");\n *\n * // URL valid for 24 hours\n * const url = await storage.temporaryUrl(\"private/document.pdf\", 86400);\n * ```\n */\n public async temporaryUrl(location: string, expiresIn?: number): Promise<string> {\n return this.activeDriver.temporaryUrl(location, expiresIn);\n }\n\n // ============================================================\n // Metadata Operations\n // ============================================================\n\n /**\n * Get file metadata without downloading the file\n *\n * Retrieves information about a file including size, last modified date,\n * and MIME type without downloading the file contents.\n *\n * @param location - File path\n * @returns File information object\n * @throws Error if file not found\n *\n * @example\n * ```typescript\n * const info = await storage.metadata(\"documents/report.pdf\");\n * console.log(`Size: ${info.size} bytes`);\n * console.log(`Type: ${info.mimeType}`);\n * console.log(`Modified: ${info.lastModified}`);\n * ```\n */\n public async metadata(location: string): Promise<StorageFileInfo> {\n return this.activeDriver.metadata(location);\n }\n\n /**\n * Get file size in bytes\n *\n * Shortcut for `metadata(location).size`.\n *\n * @param location - File path\n * @returns File size in bytes\n * @throws Error if file not found\n */\n public async size(location: string): Promise<number> {\n return this.activeDriver.size(location);\n }\n\n /**\n * Get a StorageFile instance for OOP-style operations\n *\n * Creates a `StorageFile` wrapper for the specified path,\n * allowing fluent method chaining for file operations.\n *\n * @param location - File path\n * @returns StorageFile instance\n *\n * @example\n * ```typescript\n * const file = await storage.file(\"uploads/image.jpg\");\n *\n * // Properties\n * console.log(file.name); // \"image.jpg\"\n * console.log(file.extension); // \"jpg\"\n *\n * // Operations\n * await file.copy(\"backup/image.jpg\");\n * await file.delete();\n * ```\n */\n public file(location: string): StorageFile {\n return new StorageFile(location, this.activeDriver);\n }\n\n // ============================================================\n // Utilities\n // ============================================================\n\n /**\n * Convert various input types to Buffer\n *\n * @param file - Input file in various formats\n * @returns Buffer containing file contents\n * @internal\n */\n protected async toBuffer(file: UploadedFile | Buffer | string | Readable): Promise<Buffer> {\n // Already a buffer\n if (Buffer.isBuffer(file)) {\n return file;\n }\n\n // Readable stream - collect into buffer\n if (this.isReadable(file)) {\n return this.streamToBuffer(file as Readable);\n }\n\n // String content\n if (typeof file === \"string\") {\n if (await fileExistsAsync(file)) {\n return fs.readFile(file);\n }\n\n return Buffer.from(file);\n }\n\n // UploadedFile\n return (file as UploadedFile).buffer();\n }\n\n /**\n * Check if value is a Readable stream\n *\n * @param value - Value to check\n * @returns True if value is a Readable stream\n * @internal\n */\n protected isReadable(value: unknown): value is Readable {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"pipe\" in value &&\n typeof (value as Readable).pipe === \"function\"\n );\n }\n\n /**\n * Convert a Readable stream to Buffer\n *\n * @param stream - Readable stream\n * @returns Buffer containing stream contents\n * @internal\n */\n protected async streamToBuffer(stream: Readable): Promise<Buffer> {\n const chunks: Buffer[] = [];\n for await (const chunk of stream) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\n }\n return Buffer.concat(chunks as unknown as Uint8Array[]);\n }\n\n /**\n * Prepend a prefix to a location path\n *\n * Useful for organizing files into directories.\n *\n * @param prefix - Prefix to add (e.g., \"uploads\")\n * @param location - Original location path\n * @returns Combined path with prefix\n *\n * @example\n * ```typescript\n * storage.prepend(\"uploads\", \"image.jpg\"); // \"uploads/image.jpg\"\n * storage.prepend(\"uploads/\", \"/image.jpg\"); // \"uploads/image.jpg\"\n * ```\n */\n public prepend(prefix: string, location: string): string {\n return `${prefix.replace(/\\/$/, \"\")}/${location.replace(/^\\//, \"\")}`;\n }\n\n /**\n * Append a suffix to a location path (before extension)\n *\n * Useful for creating variants of files (thumbnails, etc.).\n *\n * @param location - Original location path\n * @param suffix - Suffix to add before extension\n * @returns Path with suffix added before extension\n *\n * @example\n * ```typescript\n * storage.append(\"image.jpg\", \"_thumb\"); // \"image_thumb.jpg\"\n * storage.append(\"document.pdf\", \"_v2\"); // \"document_v2.pdf\"\n * ```\n */\n public append(location: string, suffix: string): string {\n const lastDot = location.lastIndexOf(\".\");\n if (lastDot === -1) {\n return `${location}${suffix}`;\n }\n return `${location.substring(0, lastDot)}${suffix}${location.substring(lastDot)}`;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,IAAa,gBAAb,MAA4D;;;;;;CAY1D,AAAO,YAAY,QAA+B;EAChD,KAAK,UAAU;CACjB;;;;;;CAWA,IAAW,OAA0B;EACnC,OAAO,KAAK,aAAa;CAC3B;;;;;;;;CASA,IAAW,gBAAuC;EAChD,OAAO,KAAK;CACd;;;;;;;;;CAUA,IAAW,eAAsC;EAC/C,OAAO,KAAK;CACd;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCA,MAAa,IACX,MACA,UACA,SACsB;EACtB,MAAM,SAAS,MAAM,KAAK,SAAS,IAAI;EACvC,MAAM,OAAO,MAAM,KAAK,aAAa,IAAI,QAAQ,UAAU,OAAO;EAClE,OAAO,YAAY,SAAS,MAAM,KAAK,YAAY;CACrD;;;;;;;;;;;;;;;;;;;;CAqBA,MAAa,UACX,QACA,UACA,SACsB;EACtB,MAAM,OAAO,MAAM,KAAK,aAAa,UAAU,QAAQ,UAAU,OAAO;EACxE,OAAO,YAAY,SAAS,MAAM,KAAK,YAAY;CACrD;;;;;;;;;;;;;;;;;;;;;;;CAwBA,MAAa,WACX,KACA,UACA,SACsB;EACtB,MAAM,EAAE,mBAAmB,UAAU,WAAW,gBAAgB,GAAG,eAAe,WAAW,CAAC;EAE9F,MAAM,SAAS,MAAM,kBAAkB,KAAK;GAC1C;GACA;GACA;GACA;EACF,CAAC;EAED,IAAI,CAAC,OAAO,IACV,MAAM,IAAI,MAAM,6BAA6B,IAAI,IAAI,OAAO,YAAY;EAG1E,IAAI,CAAC,OAAO,aACV,MAAM,IAAI,MAAM,6BAA6B,IAAI,8BAA8B;EAGjF,MAAM,WAAW,WAAW,YAAY,OAAO;EAE/C,OAAO,KAAK,IAAI,OAAO,QAAQ,UAAU;GAAE,GAAG;GAAY;EAAS,CAAC;CACtE;;;;;;;;;;;;;;;;;CAkBA,MAAa,cACX,SACA,UACA,SACsB;EAEtB,MAAM,UAAU,QAAQ,MAAM,4BAA4B;EAE1D,IAAI,CAAC,SACH,MAAM,IAAI,MAAM,wEAAwE;EAG1F,MAAM,GAAG,UAAU,cAAc;EAEjC,IAAI,aAAa,UAAa,eAAe,QAC3C,MAAM,IAAI,MAAM,wEAAwE;EAG1F,MAAM,SAAS,OAAO,KAAK,YAAY,QAAQ;EAE/C,OAAO,KAAK,IAAI,QAAQ,UAAU;GAChC,GAAG;GACH,UAAU,SAAS,YAAY;EACjC,CAAC;CACH;;;;;;;;;;;;;;;;;CAkBA,MAAa,IAAI,UAAmC;EAClD,OAAO,KAAK,aAAa,IAAI,QAAQ;CACvC;;;;;;;;;;;;;;;;;CAkBA,MAAa,UAAU,UAAqC;EAC1D,OAAO,KAAK,aAAa,UAAU,QAAQ;CAC7C;;;;;;;;;;;;;;;;;CAkBA,MAAa,OAAO,UAAkD;EACpE,MAAM,OAAO,OAAO,aAAa,WAAW,WAAW,SAAS;EAChE,OAAO,KAAK,aAAa,OAAO,IAAI;CACtC;;;;;;;;;;;;;;;;;;;;;;;CAwBA,MAAa,WAAW,WAAkD;EACxE,OAAO,KAAK,aAAa,WAAW,SAAS;CAC/C;;;;;;CAOA,MAAa,gBAAgB,eAAyC;EACpE,OAAO,MAAM,KAAK,aAAa,gBAAgB,aAAa;CAC9D;;;;;;;;;;;;;;CAeA,MAAa,OAAO,UAAoC;EACtD,OAAO,KAAK,aAAa,OAAO,QAAQ;CAC1C;;;;;;;;;;;;;;;;;;;;;CAsBA,MAAa,KAAK,MAA4B,IAAkC;EAC9E,MAAM,WAAW,OAAO,SAAS,WAAW,OAAO,KAAK;EACxD,MAAM,OAAO,MAAM,KAAK,aAAa,KAAK,UAAU,EAAE;EACtD,OAAO,YAAY,SAAS,MAAM,KAAK,YAAY;CACrD;;;;;;;;;;;;;;;;;;;;;CAsBA,MAAa,KAAK,MAA4B,IAAkC;EAC9E,MAAM,WAAW,OAAO,SAAS,WAAW,OAAO,KAAK;EACxD,MAAM,OAAO,MAAM,KAAK,aAAa,KAAK,UAAU,EAAE;EACtD,OAAO,YAAY,SAAS,MAAM,KAAK,YAAY;CACrD;;;;;;;;;;;;;;;;;;;;;;;;CAyBA,MAAa,cACX,MACA,IACA,SACiB;EACjB,MAAM,cAAc,SAAS,eAAe;EAI5C,MAAM,eAAc,MADA,KAAK,KAAK,MAAM,EAAE,WAAW,KAAK,CAAC,EAC9B,CAAC,QAAQ,MAAM,CAAC,EAAE,WAAW;EAGtD,IAAI,SAAS;EACb,KAAK,IAAI,IAAI,GAAG,IAAI,YAAY,QAAQ,KAAK,aAAa;GACxD,MAAM,QAAQ,YAAY,MAAM,GAAG,IAAI,WAAW;GAClD,MAAM,QAAQ,IACZ,MAAM,IAAI,OAAO,SAAS;IAGxB,MAAM,UAAU,GAAG,GAAG,GADD,KAAK,KAAK,UAAU,KAAK,MAAM,CAAC,CAAC,QAAQ,OAAO,EACjC;IACpC,MAAM,KAAK,KAAK,KAAK,MAAM,OAAO;IAClC;GACF,CAAC,CACH;EACF;EAEA,OAAO;CACT;;;;;;;;;;;;;;;;;;CAmBA,MAAa,cACX,MACA,IACA,SACiB;EAEjB,MAAM,QAAQ,MAAM,KAAK,cAAc,MAAM,IAAI,OAAO;EAGxD,MAAM,KAAK,gBAAgB,IAAI;EAE/B,OAAO;CACT;;;;;;;;;;;;;;;;;;;;;;;;;CA0BA,MAAa,aACX,cACA,aACA,SAC6B;EAC7B,MAAM,cAAc,SAAS,eAAe;EAG5C,MAAM,aAAa,MAAM,KAAK,mBAAmB,YAAY;EAG7D,MAAM,gBAAgB,SAAS,SAC3B,WAAW,QAAQ,EAAE,UAAU,eAAe,QAAQ,OAAQ,UAAU,QAAQ,CAAC,IACjF;EAEJ,MAAM,QAAQ,cAAc;EAC5B,MAAM,WAA0B,CAAC;EACjC,MAAM,SAAqD,CAAC;EAC5D,IAAI,YAAY;EAGhB,KAAK,IAAI,IAAI,GAAG,IAAI,cAAc,QAAQ,KAAK,aAAa;GAC1D,MAAM,QAAQ,cAAc,MAAM,GAAG,IAAI,WAAW;GAEpD,MAAM,QAAQ,IACZ,MAAM,IAAI,OAAO,EAAE,UAAU,eAAe;IAC1C,MAAM,cAAc,GAAG,YAAY,QAAQ,OAAO,EAAE,EAAE,GAAG;IAEzD,IAAI;KACF,MAAM,SAAS,iBAAiB,QAAQ;KACxC,MAAM,OAAO,MAAM,KAAK,UAAU,QAAQ,aAAa,SAAS,UAAU;KAC1E,SAAS,KAAK,IAAI;KAClB;KACA,SAAS,aAAa,WAAW,OAAO,IAAI;IAC9C,SAAS,KAAK;KACZ,OAAO,KAAK;MACV,WAAW;MACX,OAAO,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;KAC3D,CAAC;IACH;GACF,CAAC,CACH;EACF;EAEA,OAAO;GAAE;GAAU;GAAQ;EAAM;CACnC;;;;;;;;CASA,MAAc,mBACZ,SACA,SACwD;EACxD,MAAM,OAAO,WAAW;EACxB,MAAM,UAAU,MAAMA,KAAG,QAAQ,SAAS,EAAE,eAAe,KAAK,CAAC;EACjE,MAAM,UAAyD,CAAC;EAEhE,KAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,WAAW,KAAK,KAAK,SAAS,MAAM,IAAI;GAC9C,MAAM,WAAW,KAAK,SAAS,MAAM,QAAQ,CAAC,CAAC,QAAQ,OAAO,GAAG;GAEjE,IAAI,MAAM,YAAY,GAAG;IACvB,MAAM,SAAS,MAAM,KAAK,mBAAmB,UAAU,IAAI;IAC3D,QAAQ,KAAK,GAAG,MAAM;GACxB,OAAO,IAAI,MAAM,OAAO,GACtB,QAAQ,KAAK;IAAE;IAAU;GAAS,CAAC;EAGvC;EAEA,OAAO;CACT;;;;;;;;;;;;;;;CAgBA,MAAa,eAAe,MAA+B;EAGzD,MAAM,aAAY,MADE,KAAK,KAAK,MAAM,EAAE,WAAW,KAAK,CAAC,EAChC,CAAC,QAAQ,MAAM,CAAC,EAAE,WAAW,CAAC,CAAC,KAAK,MAAM,EAAE,IAAI;EAEvE,IAAI,UAAU,WAAW,GACvB,OAAO;EAIT,MAAM,KAAK,WAAW,SAAS;EAE/B,OAAO,UAAU;CACnB;;;;;;;;;;;;;;;;;;;;;;;CAwBA,MAAa,KAAK,WAAoB,SAAmD;EACvF,OAAO,KAAK,aAAa,KAAK,aAAa,IAAI,OAAO;CACxD;;;;;;;;;;;;;;;;;;CAuBA,AAAO,IAAI,UAA0B;EACnC,OAAO,KAAK,aAAa,IAAI,QAAQ;CACvC;;;;;;;;;;;;;;;;;;;;;CAsBA,MAAa,aAAa,UAAkB,WAAqC;EAC/E,OAAO,KAAK,aAAa,aAAa,UAAU,SAAS;CAC3D;;;;;;;;;;;;;;;;;;;CAwBA,MAAa,SAAS,UAA4C;EAChE,OAAO,KAAK,aAAa,SAAS,QAAQ;CAC5C;;;;;;;;;;CAWA,MAAa,KAAK,UAAmC;EACnD,OAAO,KAAK,aAAa,KAAK,QAAQ;CACxC;;;;;;;;;;;;;;;;;;;;;;;CAwBA,AAAO,KAAK,UAA+B;EACzC,OAAO,IAAI,YAAY,UAAU,KAAK,YAAY;CACpD;;;;;;;;CAaA,MAAgB,SAAS,MAAkE;EAEzF,IAAI,OAAO,SAAS,IAAI,GACtB,OAAO;EAIT,IAAI,KAAK,WAAW,IAAI,GACtB,OAAO,KAAK,eAAe,IAAgB;EAI7C,IAAI,OAAO,SAAS,UAAU;GAC5B,IAAI,MAAM,gBAAgB,IAAI,GAC5B,OAAOA,KAAG,SAAS,IAAI;GAGzB,OAAO,OAAO,KAAK,IAAI;EACzB;EAGA,OAAQ,KAAsB,OAAO;CACvC;;;;;;;;CASA,AAAU,WAAW,OAAmC;EACtD,OACE,OAAO,UAAU,YACjB,UAAU,QACV,UAAU,SACV,OAAQ,MAAmB,SAAS;CAExC;;;;;;;;CASA,MAAgB,eAAe,QAAmC;EAChE,MAAM,SAAmB,CAAC;EAC1B,WAAW,MAAM,SAAS,QACxB,OAAO,KAAK,OAAO,SAAS,KAAK,IAAI,QAAQ,OAAO,KAAK,KAAK,CAAC;EAEjE,OAAO,OAAO,OAAO,MAAiC;CACxD;;;;;;;;;;;;;;;;CAiBA,AAAO,QAAQ,QAAgB,UAA0B;EACvD,OAAO,GAAG,OAAO,QAAQ,OAAO,EAAE,EAAE,GAAG,SAAS,QAAQ,OAAO,EAAE;CACnE;;;;;;;;;;;;;;;;CAiBA,AAAO,OAAO,UAAkB,QAAwB;EACtD,MAAM,UAAU,SAAS,YAAY,GAAG;EACxC,IAAI,YAAY,IACd,OAAO,GAAG,WAAW;EAEvB,OAAO,GAAG,SAAS,UAAU,GAAG,OAAO,IAAI,SAAS,SAAS,UAAU,OAAO;CAChF;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.mts","names":[],"sources":["../../../../../../../core/src/storage/storage.ts"],"mappings":";;;;;;;;;;;AAiFA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAAa,OAAA,SAAgB,aAAA,YAAyB,sBAAA;EAgWjD;;;;EAAA,UA3VO,OAAA,EAAO,GAAA,SAAA,qBAAA;EAwbL;;;;EAAA,UAlbF,OAAA,EAAO,GAAA,SAAA,mBAAA;EAoed;;;;EAAA,UA9dO,iBAAA,EAAoB,iBAAA;
|
|
1
|
+
{"version":3,"file":"storage.d.mts","names":[],"sources":["../../../../../../../core/src/storage/storage.ts"],"mappings":"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"}
|
package/esm/storage/storage.mjs
CHANGED
|
@@ -432,8 +432,12 @@ var Storage = class extends ScopedStorage {
|
|
|
432
432
|
if (base64.startsWith("data:")) {
|
|
433
433
|
const match = base64.match(/^data:([^;]+);base64,(.+)$/);
|
|
434
434
|
if (match) {
|
|
435
|
-
|
|
436
|
-
|
|
435
|
+
const matchedMimeType = match[1];
|
|
436
|
+
const matchedData = match[2];
|
|
437
|
+
if (matchedMimeType !== void 0 && matchedData !== void 0) {
|
|
438
|
+
mimeType = mimeType || matchedMimeType;
|
|
439
|
+
data = matchedData;
|
|
440
|
+
}
|
|
437
441
|
}
|
|
438
442
|
}
|
|
439
443
|
const buffer = Buffer.from(data, "base64");
|