@cyberskill/shared 3.17.0 → 3.18.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.
@@ -121,29 +121,24 @@ var f = {
121
121
  isDynamicVirtual(e) {
122
122
  return !!(e && typeof e.ref == "function");
123
123
  },
124
- async getNewRecords(e, t, n, r = {}) {
125
- let i = 1e3, a = 0, o = !1, s = [];
126
- for (; !o;) {
127
- let t = await e.findAll(r, {
128
- skip: a,
129
- limit: i
124
+ async fetchAllRecords(e, t = {}, n = 1e3) {
125
+ let r = [], i = 0, a = !1;
126
+ for (; !a;) {
127
+ let o = await e.findAll(t, {
128
+ skip: i,
129
+ limit: n
130
130
  });
131
- if (!t.success) throw Error(`Failed to query existing records on skip ${a}: ${t.message}`);
132
- s.push(...t.result), t.truncated && t.result.length === i ? a += i : o = !0;
131
+ if (!o.success) throw Error(`Failed to query records on skip ${i}: ${o.message}`);
132
+ r.push(...o.result), o.truncated && o.result.length === n ? i += n : a = !0;
133
133
  }
134
- return t.filter((e) => !s.some((t) => n(t, e)));
134
+ return r;
135
+ },
136
+ async getNewRecords(e, t, n, r = {}) {
137
+ let i = await f.fetchAllRecords(e, r);
138
+ return t.filter((e) => !i.some((t) => n(t, e)));
135
139
  },
136
140
  async getExistingRecords(e, t, n, r = {}) {
137
- let i = 1e3, a = 0, o = !1, s = [];
138
- for (; !o;) {
139
- let t = await e.findAll(r, {
140
- skip: a,
141
- limit: i
142
- });
143
- if (!t.success) throw Error(`Failed to query existing records on skip ${a}: ${t.message}`);
144
- s.push(...t.result), t.truncated && t.result.length === i ? a += i : o = !0;
145
- }
146
- return s.filter((e) => t.some((t) => n(e, t)));
141
+ return (await f.fetchAllRecords(e, r)).filter((e) => t.some((t) => n(e, t)));
147
142
  },
148
143
  health(e) {
149
144
  let t = e.connection, n = null;
@@ -1 +1 @@
1
- {"version":3,"file":"mongo.util.js","names":[],"sources":["../../../src/node/mongo/mongo.util.ts"],"sourcesContent":["import type migrate from 'migrate-mongo';\nimport type mongooseRaw from 'mongoose';\n\nimport aggregatePaginate from 'mongoose-aggregate-paginate-v2';\nimport mongoosePaginate from 'mongoose-paginate-v2';\nimport { randomUUID } from 'node:crypto';\n\nimport { getNestedValue, regexSearchMapper, setNestedValue } from '#util/index.js';\nimport { validate } from '#util/validate/index.js';\n\nimport type { MongoController } from './mongo.controller.js';\nimport type { C_Document, I_CreateModelOptions, I_CreateSchemaOptions, I_DynamicVirtualConfig, I_DynamicVirtualOptions, I_ExtendedModel, I_GenericDocument, I_MongooseModelMiddleware, T_Filter, T_MongoosePlugin, T_MongooseSchema, T_QueryFilter, T_VirtualOptions, T_WithId } from './mongo.type.js';\n\nimport { addGitIgnoreEntry, writeFileSync } from '../fs/index.js';\nimport { MIGRATE_MONGO_CONFIG, PATH } from '../path/index.js';\n/**\n * Converts enum values to proper model names.\n * Handles common naming conventions like converting 'USER' to 'User'.\n *\n * @param enumValue - The enum value to convert\n * @returns The converted model name\n */\nexport function convertEnumToModelName(enumValue: string): string {\n if (enumValue === enumValue.toUpperCase()) {\n return enumValue.charAt(0).toUpperCase() + enumValue.slice(1).toLowerCase();\n }\n\n return enumValue;\n}\n\n/**\n * Interface for the MongoDB utility object to enable explicit type annotation.\n * Required to avoid TS7056 (inferred type exceeds maximum serialization length).\n */\ninterface I_MongoUtils {\n createGenericFields: () => I_GenericDocument;\n applyPlugins: <T>(schema: T_MongooseSchema<T>, plugins: Array<T_MongoosePlugin | false>) => void;\n applyMiddlewares: <T extends Partial<C_Document>>(schema: T_MongooseSchema<T>, middlewares: I_MongooseModelMiddleware<T>[]) => void;\n createGenericSchema: (mongoose: typeof mongooseRaw) => T_MongooseSchema<I_GenericDocument>;\n createSchema: <T, R extends string = string>(options: I_CreateSchemaOptions<T, R>) => T_MongooseSchema<T>;\n createModel: <T extends Partial<C_Document>, R extends string = string>(options: I_CreateModelOptions<T, R>) => I_ExtendedModel<T>;\n validator: {\n isRequired: <T>() => (this: T, value: unknown) => Promise<boolean>;\n isUnique: <T extends { constructor: { exists: (query: { [key: string]: unknown }) => Promise<unknown> } }>(fields: string[]) => (this: T, value: unknown) => Promise<boolean>;\n matchesRegex: (regexArray: RegExp[]) => (value: string) => Promise<boolean>;\n };\n migrate: {\n getModule: () => Promise<typeof migrate>;\n setConfig: (options: Partial<migrate.config.Config> & { moduleSystem?: 'commonjs' | 'esm' }) => void;\n };\n regexify: <T>(filter?: T_QueryFilter<T>, fields?: (keyof T | string)[]) => T_QueryFilter<T>;\n isDynamicVirtual: <T, R extends string>(options?: T_VirtualOptions<T, R>) => options is I_DynamicVirtualOptions<T, R>;\n getNewRecords: <T extends I_GenericDocument>(controller: MongoController<T>, recordsToCheck: T[], filterFn: (existingRecord: T_WithId<T>, newRecord: T) => boolean, filter?: T_Filter<T>) => Promise<T[]>;\n getExistingRecords: <T extends I_GenericDocument>(controller: MongoController<T>, recordsToCheck: T[], filterFn: (existingRecord: T_WithId<T>, newRecord: T) => boolean, filter?: T_Filter<T>) => Promise<T_WithId<T>[]>;\n health: (mongooseInstance: typeof mongooseRaw) => Record<string, unknown>;\n}\n\n/**\n * MongoDB utility object providing comprehensive database operations and utilities.\n * This object contains methods for creating generic fields, applying plugins and middlewares,\n * creating schemas and models, validation functions, migration utilities, and regex filtering.\n */\nexport const mongo: I_MongoUtils = {\n /**\n * Creates generic fields that are commonly used across MongoDB documents.\n * This function generates standard fields including a UUID, deletion flag, and timestamps\n * that can be applied to any document schema.\n *\n * @returns An object containing generic document fields (id, isDel, createdAt, updatedAt).\n */\n createGenericFields(): I_GenericDocument {\n return {\n id: randomUUID(),\n isDel: false,\n createdAt: new Date(),\n updatedAt: new Date(),\n };\n },\n /**\n * Applies plugins to a Mongoose schema.\n * This function filters out falsy plugins and applies the remaining valid plugins\n * to the provided schema.\n *\n * @param schema - The Mongoose schema to apply plugins to.\n * @param plugins - An array of plugin functions or false values to filter and apply.\n */\n applyPlugins<T>(schema: T_MongooseSchema<T>, plugins: Array<T_MongoosePlugin | false>) {\n plugins\n .filter((plugin): plugin is T_MongoosePlugin => typeof plugin === 'function')\n .forEach(plugin => schema.plugin(plugin));\n },\n /**\n * Applies middleware functions to a Mongoose schema.\n * This function configures pre and post middleware for specified methods on the schema.\n *\n * @param schema - The Mongoose schema to apply middleware to.\n * @param middlewares - An array of middleware configurations with method, pre, and post functions.\n */\n applyMiddlewares<T extends Partial<C_Document>>(\n schema: T_MongooseSchema<T>,\n middlewares: I_MongooseModelMiddleware<T>[],\n ) {\n middlewares.forEach(({ method, pre, post }) => {\n if (method && pre) {\n schema.pre(method as RegExp, pre);\n }\n\n if (method && post) {\n schema.post(method as RegExp, post);\n }\n });\n },\n /**\n * Creates a generic Mongoose schema with common fields.\n * This function creates a base schema with UUID field and deletion flag,\n * configured with automatic timestamps.\n *\n * @param mongoose - The Mongoose instance to create the schema with.\n * @returns A Mongoose schema with generic document fields.\n */\n createGenericSchema(mongoose: typeof mongooseRaw) {\n return new mongoose.Schema<I_GenericDocument>(\n {\n id: { type: String, default: () => randomUUID(), unique: true },\n isDel: { type: Boolean, default: false },\n },\n { timestamps: true },\n );\n },\n /**\n * Creates a Mongoose schema with optional virtual fields and generic fields.\n * This function creates a new Mongoose schema from the provided schema definition,\n * optionally adds virtual fields, and includes generic fields (unless standalone is true).\n *\n * @param options - Configuration options including mongoose instance, schema definition, virtuals, and standalone flag.\n * @param options.mongoose - The Mongoose instance to use for schema creation.\n * @param options.schema - The schema definition object.\n * @param options.virtuals - Optional array of virtual field configurations.\n * @param options.standalone - Whether to exclude generic fields (default: false).\n * @returns A configured Mongoose schema.\n */\n createSchema<T, R extends string = string>({\n mongoose,\n schema,\n virtuals = [],\n standalone = false,\n }: I_CreateSchemaOptions<T, R>): T_MongooseSchema<T> {\n const createdSchema = new mongoose.Schema<T>(schema, {\n toJSON: { virtuals: true }, // So `res.json()` and other `JSON.stringify()` functions include virtuals\n toObject: { virtuals: true }, // So `console.log()` and other functions that use `toObject()` include virtuals\n });\n\n virtuals.forEach(({ name, options, get }) => {\n if (mongo.isDynamicVirtual<T, R>(options)) {\n const schemaStatics = createdSchema.statics as Record<string, unknown>;\n\n if (!schemaStatics['_dynamicVirtuals']) {\n schemaStatics['_dynamicVirtuals'] = [];\n }\n\n (schemaStatics['_dynamicVirtuals'] as I_DynamicVirtualConfig<T>[]).push({\n name: name as string,\n options,\n });\n\n const virtualInstance = createdSchema.virtual(name as string);\n\n if (get) {\n virtualInstance.get(get);\n }\n else {\n virtualInstance.get(function (this: T & { _populated?: { [key: string]: unknown } }) {\n return this._populated?.[name as string] || (options?.count ? 0 : (options?.justOne ? null : []));\n });\n }\n }\n else {\n const virtualInstance = createdSchema.virtual(name as string, options);\n\n if (get) {\n virtualInstance.get(get);\n }\n }\n });\n\n if (!standalone) {\n createdSchema.add(mongo.createGenericSchema(mongoose));\n }\n\n return createdSchema;\n },\n /**\n * Creates a Mongoose model with plugins, middleware, and pagination support.\n * This function creates a model from a schema with optional pagination and aggregation plugins,\n * and applies any specified middleware. If a model with the same name already exists, it returns the existing model.\n *\n * @param options - Configuration options including mongoose instance, model name, schema, and feature flags.\n * @param options.mongoose - The Mongoose instance to use for model creation.\n * @param options.name - The name of the model to create.\n * @param options.schema - The schema definition for the model.\n * @param options.pagination - Whether to enable pagination plugin (default: false).\n * @param options.aggregate - Whether to enable aggregation pagination plugin (default: false).\n * @param options.virtuals - Optional array of virtual field configurations.\n * @param options.middlewares - Optional array of middleware configurations.\n * @returns A configured Mongoose model with extended functionality.\n * @throws {Error} When the model name is not provided.\n */\n createModel<T extends Partial<C_Document>, R extends string = string>({\n mongoose: currentMongooseInstance,\n name,\n schema,\n virtuals = [],\n pagination = true,\n aggregate = true,\n middlewares = [],\n }: I_CreateModelOptions<T, R>): I_ExtendedModel<T> {\n if (!name) {\n throw new Error('Model name is required.');\n }\n\n if (currentMongooseInstance.models[name]) {\n return currentMongooseInstance.models[name] as I_ExtendedModel<T>;\n }\n\n const createdSchema = mongo.createSchema({ mongoose: currentMongooseInstance, schema, virtuals });\n\n if (pagination || aggregate) {\n mongo.applyPlugins<T>(createdSchema, [\n pagination && mongoosePaginate,\n aggregate && aggregatePaginate,\n ]);\n }\n\n mongo.applyMiddlewares<T>(createdSchema, middlewares);\n\n const model = currentMongooseInstance.model<T>(name, createdSchema) as I_ExtendedModel<T>;\n\n if (virtuals.length > 0) {\n (model as I_ExtendedModel<T> & { _virtualConfigs: typeof virtuals })._virtualConfigs = virtuals;\n }\n\n return model;\n },\n /**\n * Validation utilities for Mongoose schemas.\n * This object provides common validation functions that can be used in Mongoose schema definitions.\n */\n validator: {\n /**\n * Creates a required field validator.\n * This function returns a validator that checks if a field value is not empty\n * using the validate.isEmpty utility.\n *\n * @returns A validation function that returns true if the field is not empty.\n */\n isRequired<T>(): (this: T, value: unknown) => Promise<boolean> {\n return async function (this: T, value: unknown): Promise<boolean> {\n return !validate.isEmpty(value);\n };\n },\n /**\n * Creates a unique field validator.\n * This function returns a validator that checks if a field value is unique\n * across the specified fields in the collection.\n *\n * @param fields - An array of field names to check for uniqueness.\n * @returns A validation function that returns true if the value is unique across the specified fields.\n * @throws {Error} When fields is not a non-empty array of strings.\n */\n isUnique<T extends { constructor: { exists: (query: { [key: string]: unknown }) => Promise<unknown> } }>(fields: string[]) {\n return async function (this: T, value: unknown): Promise<boolean> {\n if (!Array.isArray(fields) || fields.length === 0) {\n throw new Error('Fields must be a non-empty array of strings.');\n }\n\n const query = { $or: fields.map(field => ({ [field]: { $eq: value } })) };\n const existingDocument = await this.constructor.exists(query);\n\n return !existingDocument;\n };\n },\n /**\n * Creates a regex pattern validator.\n * This function returns a validator that checks if a string value matches\n * all provided regular expressions.\n *\n * @param regexArray - An array of regular expressions to test against the value.\n * @returns A validation function that returns true if the value matches all regex patterns.\n * @throws {Error} When regexArray is not an array of valid RegExp objects.\n */\n matchesRegex(regexArray: RegExp[]): (value: string) => Promise<boolean> {\n return async (value: string): Promise<boolean> => {\n if (!Array.isArray(regexArray) || regexArray.some(r => !(r instanceof RegExp))) {\n throw new Error('regexArray must be an array of valid RegExp objects.');\n }\n\n return regexArray.every(regex => regex.test(value));\n };\n },\n },\n /**\n * Migration utilities for MongoDB.\n * This object extends the migrate-mongo library with additional configuration utilities.\n */\n migrate: {\n /**\n * Lazily loads the migrate-mongo module to avoid eager import overhead.\n * Use this to access migrate-mongo methods (up, down, status, create) programmatically.\n *\n * @returns A promise resolving to the migrate-mongo module.\n */\n async getModule(): Promise<typeof migrate> {\n return (await import('migrate-mongo')).default;\n },\n /**\n * Sets the migration configuration and updates .gitignore.\n * This function creates a migration configuration file and ensures it's properly\n * excluded from version control.\n *\n * @param options - Migration configuration options to write to the config file.\n */\n setConfig: (options: Partial<migrate.config.Config> & { moduleSystem?: 'commonjs' | 'esm' }) => {\n const optionsJS = `// This file is automatically generated by the Cyberskill CLI.\\nmodule.exports = ${JSON.stringify(options, null, 4)}`;\n\n writeFileSync(PATH.MIGRATE_MONGO_CONFIG, optionsJS);\n\n addGitIgnoreEntry(PATH.GIT_IGNORE, MIGRATE_MONGO_CONFIG);\n },\n },\n /**\n * Converts string values in a filter to regex patterns for case-insensitive search.\n * This function recursively processes a filter object and converts string values in specified fields\n * to MongoDB regex patterns that support accented character matching.\n *\n * @remarks\n * **Performance guard:** Input strings are capped at 200 characters to mitigate ReDoS risk.\n * The generated regex patterns include accented character alternation groups (e.g., `(a|à|á|...)`)\n * which can be polynomially complex for very long inputs. For production search on large collections,\n * consider using MongoDB `$text` search indexes instead of `$regex` for better performance.\n *\n * @param filter - The filter object to process.\n * @param fields - An array of field names to convert to regex patterns.\n * @returns A new filter object with string values converted to regex patterns.\n */\n regexify<T>(filter?: T_QueryFilter<T>, fields?: (keyof T | string)[]): T_QueryFilter<T> {\n if (!filter) {\n return {} as T_QueryFilter<T>;\n }\n\n let newFilter = { ...filter };\n\n if (!fields || fields.length === 0) {\n return newFilter;\n }\n\n const MAX_REGEX_INPUT_LENGTH = 200;\n\n for (const field of fields) {\n const path = field.toString().split('.');\n const value = getNestedValue(newFilter, path);\n\n if (typeof value === 'string' && value.length > 0 && value.length <= MAX_REGEX_INPUT_LENGTH) {\n const regexValue = {\n $regex: `.*${regexSearchMapper(value)}.*`,\n $options: 'i',\n };\n\n newFilter = setNestedValue(newFilter, path, regexValue);\n }\n }\n\n return newFilter;\n },\n /**\n * Checks if a virtual options object has a dynamic ref function.\n *\n * @param options - The virtual options to check.\n * @returns True if the options contain a dynamic ref function.\n */\n isDynamicVirtual<T, R extends string = string>(options?: T_VirtualOptions<T, R>): options is I_DynamicVirtualOptions<T, R> {\n return Boolean(options && typeof options.ref === 'function');\n },\n\n /**\n * Generic utility function to get new records from the database\n * @param controller - MongoController instance\n * @param recordsToCheck - Array of records to check\n * @param filterFn - Function to determine if a record already exists\n * @param filter - Optional filter to narrow the query\n * @returns Array of records that don't exist in the database\n * @since 3.13.0\n */\n async getNewRecords<T extends I_GenericDocument>(\n controller: MongoController<T>,\n recordsToCheck: T[],\n filterFn: (existingRecord: T_WithId<T>, newRecord: T) => boolean,\n filter: T_Filter<T> = {} as T_Filter<T>,\n ): Promise<T[]> {\n const batchSize = 1000;\n let skip = 0;\n let isComplete = false;\n\n const allExistingRecords: T_WithId<T>[] = [];\n\n while (!isComplete) {\n const pageResult = await controller.findAll(filter, { skip, limit: batchSize });\n\n if (!pageResult.success) {\n throw new Error(`Failed to query existing records on skip ${skip}: ${pageResult.message}`);\n }\n\n allExistingRecords.push(...pageResult.result);\n\n if (pageResult.truncated && pageResult.result.length === batchSize) {\n skip += batchSize;\n }\n else {\n isComplete = true;\n }\n }\n\n const filteredRecords = recordsToCheck.filter(newRecord =>\n !allExistingRecords.some((existingRecord: T_WithId<T>) =>\n filterFn(existingRecord, newRecord),\n ),\n );\n\n return filteredRecords;\n },\n\n /**\n * Generic utility function to get existing records that match the filter criteria\n * @param controller - MongoController instance\n * @param recordsToCheck - Array of records to check\n * @param filterFn - Function to determine if a record exists\n * @param filter - Optional filter to narrow the query\n * @returns Array of existing records that match the filter criteria\n * @throws {Error} When the database query fails — prevents silent data inconsistency.\n * @since 3.13.0\n */\n async getExistingRecords<T extends I_GenericDocument>(\n controller: MongoController<T>,\n recordsToCheck: T[],\n filterFn: (existingRecord: T_WithId<T>, newRecord: T) => boolean,\n filter: T_Filter<T> = {} as T_Filter<T>,\n ): Promise<T_WithId<T>[]> {\n const batchSize = 1000;\n let skip = 0;\n let isComplete = false;\n\n const allExistingRecords: T_WithId<T>[] = [];\n\n while (!isComplete) {\n const pageResult = await controller.findAll(filter, { skip, limit: batchSize });\n\n if (!pageResult.success) {\n throw new Error(`Failed to query existing records on skip ${skip}: ${pageResult.message}`);\n }\n\n allExistingRecords.push(...pageResult.result);\n\n if (pageResult.truncated && pageResult.result.length === batchSize) {\n skip += batchSize;\n }\n else {\n isComplete = true;\n }\n }\n\n const foundRecords = allExistingRecords.filter((existingRecord: T_WithId<T>) =>\n recordsToCheck.some((newRecord: T) =>\n filterFn(existingRecord, newRecord),\n ),\n );\n\n return foundRecords;\n },\n\n /**\n * Retrieves health and connection pool statistics for the Mongoose connection.\n * This utility helps monitor database connection health and observability.\n *\n * @param mongooseInstance - The Mongoose instance to check.\n * @returns An object containing connection health statistics.\n */\n health(mongooseInstance: typeof mongooseRaw): Record<string, unknown> {\n const conn = mongooseInstance.connection;\n\n let pool: { totalConnections: number; availableConnections: number } | null = null;\n\n try {\n // Prefer the public getClient() API; fall back to the internal .client\n // property for older mongoose versions that do not expose getClient().\n const client = (conn as any).getClient?.() ?? (conn as any).client;\n\n if (client?.topology?.s?.servers && client.topology.s.servers instanceof Map) {\n let totalConnections = 0;\n let availableConnections = 0;\n\n client.topology.s.servers.forEach((server: any) => {\n const serverPool = server.s?.pool;\n if (serverPool) {\n totalConnections += serverPool.totalConnectionCount ?? 0;\n availableConnections += serverPool.availableConnectionCount ?? 0;\n }\n });\n\n pool = { totalConnections, availableConnections };\n }\n }\n catch {\n // Pool metrics rely on driver internals that may change across upgrades\n }\n\n return {\n readyState: conn.readyState,\n host: conn.host,\n name: conn.name,\n models: Object.keys(mongooseInstance.models).length,\n pool,\n poolMetricsAvailable: pool !== null,\n };\n },\n};\n\nexport { applyNestedPopulate } from './mongo.populate.js';\n"],"mappings":"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"}
1
+ {"version":3,"file":"mongo.util.js","names":[],"sources":["../../../src/node/mongo/mongo.util.ts"],"sourcesContent":["import type migrate from 'migrate-mongo';\nimport type mongooseRaw from 'mongoose';\n\nimport aggregatePaginate from 'mongoose-aggregate-paginate-v2';\nimport mongoosePaginate from 'mongoose-paginate-v2';\nimport { randomUUID } from 'node:crypto';\n\nimport { getNestedValue, regexSearchMapper, setNestedValue } from '#util/index.js';\nimport { validate } from '#util/validate/index.js';\n\nimport type { MongoController } from './mongo.controller.js';\nimport type { C_Document, I_CreateModelOptions, I_CreateSchemaOptions, I_DynamicVirtualConfig, I_DynamicVirtualOptions, I_ExtendedModel, I_GenericDocument, I_MongooseModelMiddleware, T_Filter, T_MongoosePlugin, T_MongooseSchema, T_QueryFilter, T_VirtualOptions, T_WithId } from './mongo.type.js';\n\nimport { addGitIgnoreEntry, writeFileSync } from '../fs/index.js';\nimport { MIGRATE_MONGO_CONFIG, PATH } from '../path/index.js';\n/**\n * Converts enum values to proper model names.\n * Handles common naming conventions like converting 'USER' to 'User'.\n *\n * @param enumValue - The enum value to convert\n * @returns The converted model name\n */\nexport function convertEnumToModelName(enumValue: string): string {\n if (enumValue === enumValue.toUpperCase()) {\n return enumValue.charAt(0).toUpperCase() + enumValue.slice(1).toLowerCase();\n }\n\n return enumValue;\n}\n\n/**\n * Interface for the MongoDB utility object to enable explicit type annotation.\n * Required to avoid TS7056 (inferred type exceeds maximum serialization length).\n */\ninterface I_MongoUtils {\n createGenericFields: () => I_GenericDocument;\n applyPlugins: <T>(schema: T_MongooseSchema<T>, plugins: Array<T_MongoosePlugin | false>) => void;\n applyMiddlewares: <T extends Partial<C_Document>>(schema: T_MongooseSchema<T>, middlewares: I_MongooseModelMiddleware<T>[]) => void;\n createGenericSchema: (mongoose: typeof mongooseRaw) => T_MongooseSchema<I_GenericDocument>;\n createSchema: <T, R extends string = string>(options: I_CreateSchemaOptions<T, R>) => T_MongooseSchema<T>;\n createModel: <T extends Partial<C_Document>, R extends string = string>(options: I_CreateModelOptions<T, R>) => I_ExtendedModel<T>;\n validator: {\n isRequired: <T>() => (this: T, value: unknown) => Promise<boolean>;\n isUnique: <T extends { constructor: { exists: (query: { [key: string]: unknown }) => Promise<unknown> } }>(fields: string[]) => (this: T, value: unknown) => Promise<boolean>;\n matchesRegex: (regexArray: RegExp[]) => (value: string) => Promise<boolean>;\n };\n migrate: {\n getModule: () => Promise<typeof migrate>;\n setConfig: (options: Partial<migrate.config.Config> & { moduleSystem?: 'commonjs' | 'esm' }) => void;\n };\n regexify: <T>(filter?: T_QueryFilter<T>, fields?: (keyof T | string)[]) => T_QueryFilter<T>;\n isDynamicVirtual: <T, R extends string>(options?: T_VirtualOptions<T, R>) => options is I_DynamicVirtualOptions<T, R>;\n fetchAllRecords: <T extends I_GenericDocument>(controller: MongoController<T>, filter?: T_Filter<T>, batchSize?: number) => Promise<T_WithId<T>[]>;\n getNewRecords: <T extends I_GenericDocument>(controller: MongoController<T>, recordsToCheck: T[], filterFn: (existingRecord: T_WithId<T>, newRecord: T) => boolean, filter?: T_Filter<T>) => Promise<T[]>;\n getExistingRecords: <T extends I_GenericDocument>(controller: MongoController<T>, recordsToCheck: T[], filterFn: (existingRecord: T_WithId<T>, newRecord: T) => boolean, filter?: T_Filter<T>) => Promise<T_WithId<T>[]>;\n health: (mongooseInstance: typeof mongooseRaw) => Record<string, unknown>;\n}\n\n/**\n * MongoDB utility object providing comprehensive database operations and utilities.\n * This object contains methods for creating generic fields, applying plugins and middlewares,\n * creating schemas and models, validation functions, migration utilities, and regex filtering.\n */\nexport const mongo: I_MongoUtils = {\n /**\n * Creates generic fields that are commonly used across MongoDB documents.\n * This function generates standard fields including a UUID, deletion flag, and timestamps\n * that can be applied to any document schema.\n *\n * @returns An object containing generic document fields (id, isDel, createdAt, updatedAt).\n */\n createGenericFields(): I_GenericDocument {\n return {\n id: randomUUID(),\n isDel: false,\n createdAt: new Date(),\n updatedAt: new Date(),\n };\n },\n /**\n * Applies plugins to a Mongoose schema.\n * This function filters out falsy plugins and applies the remaining valid plugins\n * to the provided schema.\n *\n * @param schema - The Mongoose schema to apply plugins to.\n * @param plugins - An array of plugin functions or false values to filter and apply.\n */\n applyPlugins<T>(schema: T_MongooseSchema<T>, plugins: Array<T_MongoosePlugin | false>) {\n plugins\n .filter((plugin): plugin is T_MongoosePlugin => typeof plugin === 'function')\n .forEach(plugin => schema.plugin(plugin));\n },\n /**\n * Applies middleware functions to a Mongoose schema.\n * This function configures pre and post middleware for specified methods on the schema.\n *\n * @param schema - The Mongoose schema to apply middleware to.\n * @param middlewares - An array of middleware configurations with method, pre, and post functions.\n */\n applyMiddlewares<T extends Partial<C_Document>>(\n schema: T_MongooseSchema<T>,\n middlewares: I_MongooseModelMiddleware<T>[],\n ) {\n middlewares.forEach(({ method, pre, post }) => {\n if (method && pre) {\n schema.pre(method as RegExp, pre);\n }\n\n if (method && post) {\n schema.post(method as RegExp, post);\n }\n });\n },\n /**\n * Creates a generic Mongoose schema with common fields.\n * This function creates a base schema with UUID field and deletion flag,\n * configured with automatic timestamps.\n *\n * @param mongoose - The Mongoose instance to create the schema with.\n * @returns A Mongoose schema with generic document fields.\n */\n createGenericSchema(mongoose: typeof mongooseRaw) {\n return new mongoose.Schema<I_GenericDocument>(\n {\n id: { type: String, default: () => randomUUID(), unique: true },\n isDel: { type: Boolean, default: false },\n },\n { timestamps: true },\n );\n },\n /**\n * Creates a Mongoose schema with optional virtual fields and generic fields.\n * This function creates a new Mongoose schema from the provided schema definition,\n * optionally adds virtual fields, and includes generic fields (unless standalone is true).\n *\n * @param options - Configuration options including mongoose instance, schema definition, virtuals, and standalone flag.\n * @param options.mongoose - The Mongoose instance to use for schema creation.\n * @param options.schema - The schema definition object.\n * @param options.virtuals - Optional array of virtual field configurations.\n * @param options.standalone - Whether to exclude generic fields (default: false).\n * @returns A configured Mongoose schema.\n */\n createSchema<T, R extends string = string>({\n mongoose,\n schema,\n virtuals = [],\n standalone = false,\n }: I_CreateSchemaOptions<T, R>): T_MongooseSchema<T> {\n const createdSchema = new mongoose.Schema<T>(schema, {\n toJSON: { virtuals: true }, // So `res.json()` and other `JSON.stringify()` functions include virtuals\n toObject: { virtuals: true }, // So `console.log()` and other functions that use `toObject()` include virtuals\n });\n\n virtuals.forEach(({ name, options, get }) => {\n if (mongo.isDynamicVirtual<T, R>(options)) {\n const schemaStatics = createdSchema.statics as Record<string, unknown>;\n\n if (!schemaStatics['_dynamicVirtuals']) {\n schemaStatics['_dynamicVirtuals'] = [];\n }\n\n (schemaStatics['_dynamicVirtuals'] as I_DynamicVirtualConfig<T>[]).push({\n name: name as string,\n options,\n });\n\n const virtualInstance = createdSchema.virtual(name as string);\n\n if (get) {\n virtualInstance.get(get);\n }\n else {\n virtualInstance.get(function (this: T & { _populated?: { [key: string]: unknown } }) {\n return this._populated?.[name as string] || (options?.count ? 0 : (options?.justOne ? null : []));\n });\n }\n }\n else {\n const virtualInstance = createdSchema.virtual(name as string, options);\n\n if (get) {\n virtualInstance.get(get);\n }\n }\n });\n\n if (!standalone) {\n createdSchema.add(mongo.createGenericSchema(mongoose));\n }\n\n return createdSchema;\n },\n /**\n * Creates a Mongoose model with plugins, middleware, and pagination support.\n * This function creates a model from a schema with optional pagination and aggregation plugins,\n * and applies any specified middleware. If a model with the same name already exists, it returns the existing model.\n *\n * @param options - Configuration options including mongoose instance, model name, schema, and feature flags.\n * @param options.mongoose - The Mongoose instance to use for model creation.\n * @param options.name - The name of the model to create.\n * @param options.schema - The schema definition for the model.\n * @param options.pagination - Whether to enable pagination plugin (default: false).\n * @param options.aggregate - Whether to enable aggregation pagination plugin (default: false).\n * @param options.virtuals - Optional array of virtual field configurations.\n * @param options.middlewares - Optional array of middleware configurations.\n * @returns A configured Mongoose model with extended functionality.\n * @throws {Error} When the model name is not provided.\n */\n createModel<T extends Partial<C_Document>, R extends string = string>({\n mongoose: currentMongooseInstance,\n name,\n schema,\n virtuals = [],\n pagination = true,\n aggregate = true,\n middlewares = [],\n }: I_CreateModelOptions<T, R>): I_ExtendedModel<T> {\n if (!name) {\n throw new Error('Model name is required.');\n }\n\n if (currentMongooseInstance.models[name]) {\n return currentMongooseInstance.models[name] as I_ExtendedModel<T>;\n }\n\n const createdSchema = mongo.createSchema({ mongoose: currentMongooseInstance, schema, virtuals });\n\n if (pagination || aggregate) {\n mongo.applyPlugins<T>(createdSchema, [\n pagination && mongoosePaginate,\n aggregate && aggregatePaginate,\n ]);\n }\n\n mongo.applyMiddlewares<T>(createdSchema, middlewares);\n\n const model = currentMongooseInstance.model<T>(name, createdSchema) as I_ExtendedModel<T>;\n\n if (virtuals.length > 0) {\n (model as I_ExtendedModel<T> & { _virtualConfigs: typeof virtuals })._virtualConfigs = virtuals;\n }\n\n return model;\n },\n /**\n * Validation utilities for Mongoose schemas.\n * This object provides common validation functions that can be used in Mongoose schema definitions.\n */\n validator: {\n /**\n * Creates a required field validator.\n * This function returns a validator that checks if a field value is not empty\n * using the validate.isEmpty utility.\n *\n * @returns A validation function that returns true if the field is not empty.\n */\n isRequired<T>(): (this: T, value: unknown) => Promise<boolean> {\n return async function (this: T, value: unknown): Promise<boolean> {\n return !validate.isEmpty(value);\n };\n },\n /**\n * Creates a unique field validator.\n * This function returns a validator that checks if a field value is unique\n * across the specified fields in the collection.\n *\n * @param fields - An array of field names to check for uniqueness.\n * @returns A validation function that returns true if the value is unique across the specified fields.\n * @throws {Error} When fields is not a non-empty array of strings.\n */\n isUnique<T extends { constructor: { exists: (query: { [key: string]: unknown }) => Promise<unknown> } }>(fields: string[]) {\n return async function (this: T, value: unknown): Promise<boolean> {\n if (!Array.isArray(fields) || fields.length === 0) {\n throw new Error('Fields must be a non-empty array of strings.');\n }\n\n const query = { $or: fields.map(field => ({ [field]: { $eq: value } })) };\n const existingDocument = await this.constructor.exists(query);\n\n return !existingDocument;\n };\n },\n /**\n * Creates a regex pattern validator.\n * This function returns a validator that checks if a string value matches\n * all provided regular expressions.\n *\n * @param regexArray - An array of regular expressions to test against the value.\n * @returns A validation function that returns true if the value matches all regex patterns.\n * @throws {Error} When regexArray is not an array of valid RegExp objects.\n */\n matchesRegex(regexArray: RegExp[]): (value: string) => Promise<boolean> {\n return async (value: string): Promise<boolean> => {\n if (!Array.isArray(regexArray) || regexArray.some(r => !(r instanceof RegExp))) {\n throw new Error('regexArray must be an array of valid RegExp objects.');\n }\n\n return regexArray.every(regex => regex.test(value));\n };\n },\n },\n /**\n * Migration utilities for MongoDB.\n * This object extends the migrate-mongo library with additional configuration utilities.\n */\n migrate: {\n /**\n * Lazily loads the migrate-mongo module to avoid eager import overhead.\n * Use this to access migrate-mongo methods (up, down, status, create) programmatically.\n *\n * @returns A promise resolving to the migrate-mongo module.\n */\n async getModule(): Promise<typeof migrate> {\n return (await import('migrate-mongo')).default;\n },\n /**\n * Sets the migration configuration and updates .gitignore.\n * This function creates a migration configuration file and ensures it's properly\n * excluded from version control.\n *\n * @param options - Migration configuration options to write to the config file.\n */\n setConfig: (options: Partial<migrate.config.Config> & { moduleSystem?: 'commonjs' | 'esm' }) => {\n const optionsJS = `// This file is automatically generated by the Cyberskill CLI.\\nmodule.exports = ${JSON.stringify(options, null, 4)}`;\n\n writeFileSync(PATH.MIGRATE_MONGO_CONFIG, optionsJS);\n\n addGitIgnoreEntry(PATH.GIT_IGNORE, MIGRATE_MONGO_CONFIG);\n },\n },\n /**\n * Converts string values in a filter to regex patterns for case-insensitive search.\n * This function recursively processes a filter object and converts string values in specified fields\n * to MongoDB regex patterns that support accented character matching.\n *\n * @remarks\n * **Performance guard:** Input strings are capped at 200 characters to mitigate ReDoS risk.\n * The generated regex patterns include accented character alternation groups (e.g., `(a|à|á|...)`)\n * which can be polynomially complex for very long inputs. For production search on large collections,\n * consider using MongoDB `$text` search indexes instead of `$regex` for better performance.\n *\n * @param filter - The filter object to process.\n * @param fields - An array of field names to convert to regex patterns.\n * @returns A new filter object with string values converted to regex patterns.\n */\n regexify<T>(filter?: T_QueryFilter<T>, fields?: (keyof T | string)[]): T_QueryFilter<T> {\n if (!filter) {\n return {} as T_QueryFilter<T>;\n }\n\n let newFilter = { ...filter };\n\n if (!fields || fields.length === 0) {\n return newFilter;\n }\n\n const MAX_REGEX_INPUT_LENGTH = 200;\n\n for (const field of fields) {\n const path = field.toString().split('.');\n const value = getNestedValue(newFilter, path);\n\n if (typeof value === 'string' && value.length > 0 && value.length <= MAX_REGEX_INPUT_LENGTH) {\n const regexValue = {\n $regex: `.*${regexSearchMapper(value)}.*`,\n $options: 'i',\n };\n\n newFilter = setNestedValue(newFilter, path, regexValue);\n }\n }\n\n return newFilter;\n },\n /**\n * Checks if a virtual options object has a dynamic ref function.\n *\n * @param options - The virtual options to check.\n * @returns True if the options contain a dynamic ref function.\n */\n isDynamicVirtual<T, R extends string = string>(options?: T_VirtualOptions<T, R>): options is I_DynamicVirtualOptions<T, R> {\n return Boolean(options && typeof options.ref === 'function');\n },\n\n /**\n * Fetches all records from a collection by paginating through results in batches.\n * Handles the offset-based pagination loop internally so callers don't need\n * to reimplement it.\n *\n * **Warning:** All matched documents are accumulated into memory before returning.\n * This helper is intended for bounded datasets only. Avoid using it on large or\n * unbounded collections to prevent excessive memory usage or OOM errors.\n *\n * @param controller - MongoController instance\n * @param filter - Optional filter to narrow the query\n * @param batchSize - Number of records per page (default: 1000)\n * @returns All records matching the filter\n * @throws {Error} When a page query fails\n * @since 3.17.0\n */\n async fetchAllRecords<T extends I_GenericDocument>(\n controller: MongoController<T>,\n filter: T_Filter<T> = {} as T_Filter<T>,\n batchSize = 1000,\n ): Promise<T_WithId<T>[]> {\n const allRecords: T_WithId<T>[] = [];\n let skip = 0;\n let isComplete = false;\n\n while (!isComplete) {\n const pageResult = await controller.findAll(filter, { skip, limit: batchSize });\n\n if (!pageResult.success) {\n throw new Error(`Failed to query records on skip ${skip}: ${pageResult.message}`);\n }\n\n allRecords.push(...pageResult.result);\n\n if (pageResult.truncated && pageResult.result.length === batchSize) {\n skip += batchSize;\n }\n else {\n isComplete = true;\n }\n }\n\n return allRecords;\n },\n\n /**\n * Generic utility function to get new records from the database\n * @param controller - MongoController instance\n * @param recordsToCheck - Array of records to check\n * @param filterFn - Function to determine if a record already exists\n * @param filter - Optional filter to narrow the query\n * @returns Array of records that don't exist in the database\n * @since 3.13.0\n */\n async getNewRecords<T extends I_GenericDocument>(\n controller: MongoController<T>,\n recordsToCheck: T[],\n filterFn: (existingRecord: T_WithId<T>, newRecord: T) => boolean,\n filter: T_Filter<T> = {} as T_Filter<T>,\n ): Promise<T[]> {\n const allExistingRecords = await mongo.fetchAllRecords(controller, filter);\n\n return recordsToCheck.filter(newRecord =>\n !allExistingRecords.some((existingRecord: T_WithId<T>) =>\n filterFn(existingRecord, newRecord),\n ),\n );\n },\n\n /**\n * Generic utility function to get existing records that match the filter criteria\n * @param controller - MongoController instance\n * @param recordsToCheck - Array of records to check\n * @param filterFn - Function to determine if a record exists\n * @param filter - Optional filter to narrow the query\n * @returns Array of existing records that match the filter criteria\n * @throws {Error} When the database query fails — prevents silent data inconsistency.\n * @since 3.13.0\n */\n async getExistingRecords<T extends I_GenericDocument>(\n controller: MongoController<T>,\n recordsToCheck: T[],\n filterFn: (existingRecord: T_WithId<T>, newRecord: T) => boolean,\n filter: T_Filter<T> = {} as T_Filter<T>,\n ): Promise<T_WithId<T>[]> {\n const allExistingRecords = await mongo.fetchAllRecords(controller, filter);\n\n return allExistingRecords.filter((existingRecord: T_WithId<T>) =>\n recordsToCheck.some((newRecord: T) =>\n filterFn(existingRecord, newRecord),\n ),\n );\n },\n\n /**\n * Retrieves health and connection pool statistics for the Mongoose connection.\n * This utility helps monitor database connection health and observability.\n *\n * @param mongooseInstance - The Mongoose instance to check.\n * @returns An object containing connection health statistics.\n */\n health(mongooseInstance: typeof mongooseRaw): Record<string, unknown> {\n const conn = mongooseInstance.connection;\n\n let pool: { totalConnections: number; availableConnections: number } | null = null;\n\n try {\n // Prefer the public getClient() API; fall back to the internal .client\n // property for older mongoose versions that do not expose getClient().\n const client = (conn as any).getClient?.() ?? (conn as any).client;\n\n if (client?.topology?.s?.servers && client.topology.s.servers instanceof Map) {\n let totalConnections = 0;\n let availableConnections = 0;\n\n client.topology.s.servers.forEach((server: any) => {\n const serverPool = server.s?.pool;\n if (serverPool) {\n totalConnections += serverPool.totalConnectionCount ?? 0;\n availableConnections += serverPool.availableConnectionCount ?? 0;\n }\n });\n\n pool = { totalConnections, availableConnections };\n }\n }\n catch {\n // Pool metrics rely on driver internals that may change across upgrades\n }\n\n return {\n readyState: conn.readyState,\n host: conn.host,\n name: conn.name,\n models: Object.keys(mongooseInstance.models).length,\n pool,\n poolMetricsAvailable: pool !== null,\n };\n },\n};\n\nexport { applyNestedPopulate } from './mongo.populate.js';\n"],"mappings":";;;;;;;;;;AAsBA,SAAgB,EAAuB,GAA2B;AAK9D,QAJI,MAAc,EAAU,aAAa,GAC9B,EAAU,OAAO,EAAE,CAAC,aAAa,GAAG,EAAU,MAAM,EAAE,CAAC,aAAa,GAGxE;;AAoCX,IAAa,IAAsB;CAQ/B,sBAAyC;AACrC,SAAO;GACH,IAAI,GAAY;GAChB,OAAO;GACP,2BAAW,IAAI,MAAM;GACrB,2BAAW,IAAI,MAAM;GACxB;;CAUL,aAAgB,GAA6B,GAA0C;AACnF,IACK,QAAQ,MAAuC,OAAO,KAAW,WAAW,CAC5E,SAAQ,MAAU,EAAO,OAAO,EAAO,CAAC;;CASjD,iBACI,GACA,GACF;AACE,IAAY,SAAS,EAAE,WAAQ,QAAK,cAAW;AAK3C,GAJI,KAAU,KACV,EAAO,IAAI,GAAkB,EAAI,EAGjC,KAAU,KACV,EAAO,KAAK,GAAkB,EAAK;IAEzC;;CAUN,oBAAoB,GAA8B;AAC9C,SAAO,IAAI,EAAS,OAChB;GACI,IAAI;IAAE,MAAM;IAAQ,eAAe,GAAY;IAAE,QAAQ;IAAM;GAC/D,OAAO;IAAE,MAAM;IAAS,SAAS;IAAO;GAC3C,EACD,EAAE,YAAY,IAAM,CACvB;;CAcL,aAA2C,EACvC,aACA,WACA,cAAW,EAAE,EACb,gBAAa,MACoC;EACjD,IAAM,IAAgB,IAAI,EAAS,OAAU,GAAQ;GACjD,QAAQ,EAAE,UAAU,IAAM;GAC1B,UAAU,EAAE,UAAU,IAAM;GAC/B,CAAC;AAuCF,SArCA,EAAS,SAAS,EAAE,SAAM,YAAS,aAAU;AACzC,OAAI,EAAM,iBAAuB,EAAQ,EAAE;IACvC,IAAM,IAAgB,EAAc;AAapC,IAXA,AACI,EAAc,qBAAsB,EAAE,EAGzC,EAAc,iBAAoD,KAAK;KAC9D;KACN;KACH,CAAC,EAEsB,EAAc,QAAQ,EAAe,CAGzC,IADhB,KAIoB,WAAiE;AACjF,YAAO,KAAK,aAAa,OAAoB,GAAS,QAAQ,IAAK,GAAS,UAAU,OAAO,EAAE;MACjG;UAGL;IACD,IAAM,IAAkB,EAAc,QAAQ,GAAgB,EAAQ;AAEtE,IAAI,KACA,EAAgB,IAAI,EAAI;;IAGlC,EAEG,KACD,EAAc,IAAI,EAAM,oBAAoB,EAAS,CAAC,EAGnD;;CAkBX,YAAsE,EAClE,UAAU,GACV,SACA,WACA,cAAW,EAAE,EACb,gBAAa,IACb,eAAY,IACZ,iBAAc,EAAE,IAC+B;AAC/C,MAAI,CAAC,EACD,OAAU,MAAM,0BAA0B;AAG9C,MAAI,EAAwB,OAAO,GAC/B,QAAO,EAAwB,OAAO;EAG1C,IAAM,IAAgB,EAAM,aAAa;GAAE,UAAU;GAAyB;GAAQ;GAAU,CAAC;AASjG,GAPI,KAAc,MACd,EAAM,aAAgB,GAAe,CACjC,KAAc,GACd,KAAa,EAChB,CAAC,EAGN,EAAM,iBAAoB,GAAe,EAAY;EAErD,IAAM,IAAQ,EAAwB,MAAS,GAAM,EAAc;AAMnE,SAJI,EAAS,SAAS,MACjB,EAAoE,kBAAkB,IAGpF;;CAMX,WAAW;EAQP,aAA+D;AAC3D,UAAO,eAAyB,GAAkC;AAC9D,WAAO,CAAC,EAAS,QAAQ,EAAM;;;EAYvC,SAAyG,GAAkB;AACvH,UAAO,eAAyB,GAAkC;AAC9D,QAAI,CAAC,MAAM,QAAQ,EAAO,IAAI,EAAO,WAAW,EAC5C,OAAU,MAAM,+CAA+C;IAGnE,IAAM,IAAQ,EAAE,KAAK,EAAO,KAAI,OAAU,GAAG,IAAQ,EAAE,KAAK,GAAO,EAAE,EAAE,EAAE;AAGzE,WAAO,CAFkB,MAAM,KAAK,YAAY,OAAO,EAAM;;;EAcrE,aAAa,GAA2D;AACpE,UAAO,OAAO,MAAoC;AAC9C,QAAI,CAAC,MAAM,QAAQ,EAAW,IAAI,EAAW,MAAK,MAAK,EAAE,aAAa,QAAQ,CAC1E,OAAU,MAAM,uDAAuD;AAG3E,WAAO,EAAW,OAAM,MAAS,EAAM,KAAK,EAAM,CAAC;;;EAG9D;CAKD,SAAS;EAOL,MAAM,YAAqC;AACvC,WAAQ,MAAM,OAAO,kBAAkB;;EAS3C,YAAY,MAAoF;GAC5F,IAAM,IAAY,oFAAoF,KAAK,UAAU,GAAS,MAAM,EAAE;AAItI,GAFA,EAAc,EAAK,sBAAsB,EAAU,EAEnD,EAAkB,EAAK,YAAY,EAAqB;;EAE/D;CAgBD,SAAY,GAA2B,GAAiD;AACpF,MAAI,CAAC,EACD,QAAO,EAAE;EAGb,IAAI,IAAY,EAAE,GAAG,GAAQ;AAE7B,MAAI,CAAC,KAAU,EAAO,WAAW,EAC7B,QAAO;AAKX,OAAK,IAAM,KAAS,GAAQ;GACxB,IAAM,IAAO,EAAM,UAAU,CAAC,MAAM,IAAI,EAClC,IAAQ,EAAe,GAAW,EAAK;AAE7C,OAAI,OAAO,KAAU,YAAY,EAAM,SAAS,KAAK,EAAM,UAAU,KAAwB;IACzF,IAAM,IAAa;KACf,QAAQ,KAAK,EAAkB,EAAM,CAAC;KACtC,UAAU;KACb;AAED,QAAY,EAAe,GAAW,GAAM,EAAW;;;AAI/D,SAAO;;CAQX,iBAA+C,GAA4E;AACvH,SAAO,GAAQ,KAAW,OAAO,EAAQ,OAAQ;;CAmBrD,MAAM,gBACF,GACA,IAAsB,EAAE,EACxB,IAAY,KACU;EACtB,IAAM,IAA4B,EAAE,EAChC,IAAO,GACP,IAAa;AAEjB,SAAO,CAAC,IAAY;GAChB,IAAM,IAAa,MAAM,EAAW,QAAQ,GAAQ;IAAE;IAAM,OAAO;IAAW,CAAC;AAE/E,OAAI,CAAC,EAAW,QACZ,OAAU,MAAM,mCAAmC,EAAK,IAAI,EAAW,UAAU;AAKrF,GAFA,EAAW,KAAK,GAAG,EAAW,OAAO,EAEjC,EAAW,aAAa,EAAW,OAAO,WAAW,IACrD,KAAQ,IAGR,IAAa;;AAIrB,SAAO;;CAYX,MAAM,cACF,GACA,GACA,GACA,IAAsB,EAAE,EACZ;EACZ,IAAM,IAAqB,MAAM,EAAM,gBAAgB,GAAY,EAAO;AAE1E,SAAO,EAAe,QAAO,MACzB,CAAC,EAAmB,MAAM,MACtB,EAAS,GAAgB,EAAU,CACtC,CACJ;;CAaL,MAAM,mBACF,GACA,GACA,GACA,IAAsB,EAAE,EACF;AAGtB,UAF2B,MAAM,EAAM,gBAAgB,GAAY,EAAO,EAEhD,QAAQ,MAC9B,EAAe,MAAM,MACjB,EAAS,GAAgB,EAAU,CACtC,CACJ;;CAUL,OAAO,GAA+D;EAClE,IAAM,IAAO,EAAiB,YAE1B,IAA0E;AAE9E,MAAI;GAGA,IAAM,IAAU,EAAa,aAAa,IAAK,EAAa;AAE5D,OAAI,GAAQ,UAAU,GAAG,WAAW,EAAO,SAAS,EAAE,mBAAmB,KAAK;IAC1E,IAAI,IAAmB,GACnB,IAAuB;AAU3B,IARA,EAAO,SAAS,EAAE,QAAQ,SAAS,MAAgB;KAC/C,IAAM,IAAa,EAAO,GAAG;AAC7B,KAAI,MACA,KAAoB,EAAW,wBAAwB,GACvD,KAAwB,EAAW,4BAA4B;MAErE,EAEF,IAAO;KAAE;KAAkB;KAAsB;;UAGnD;AAIN,SAAO;GACH,YAAY,EAAK;GACjB,MAAM,EAAK;GACX,MAAM,EAAK;GACX,QAAQ,OAAO,KAAK,EAAiB,OAAO,CAAC;GAC7C;GACA,sBAAsB,MAAS;GAClC;;CAER"}
@@ -1 +1 @@
1
- {"version":3,"file":"package.util.js","names":[],"sources":["../../../src/node/package/package.util.ts"],"sourcesContent":["import type { I_Return } from '#typescript/index.js';\n\nimport { getEnv } from '#config/env/index.js';\n\nimport type { I_PackageInfo, I_PackageInput, I_PackageJson } from './package.type.js';\n\nimport { runCommand } from '../command/index.js';\nimport { pathExistsSync, readJsonSync, writeFileSync } from '../fs/index.js';\nimport { catchError, log } from '../log/index.js';\nimport { command, join, NODE_MODULES, PACKAGE_JSON, PATH } from '../path/index.js';\nimport { E_PackageType } from './package.type.js';\n\nconst env = getEnv();\n\n/**\n * Fetches the latest version of a package from the npm registry.\n * This function makes an HTTP request to the npm registry to get the latest version\n * information for a specified package.\n *\n * @param packageName - The name of the package to fetch the latest version for.\n * @returns A promise that resolves to a standardized response with the latest version string.\n */\nexport async function getLatestPackageVersion(packageName: string): Promise<I_Return<string>> {\n try {\n const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`);\n\n if (!res.ok) {\n throw new Error(`Failed to fetch latest version: ${res.status} ${res.statusText}`);\n }\n\n const { version } = await res.json() as { version: string };\n\n return {\n success: true,\n result: version,\n };\n }\n catch (error) {\n return catchError<string>(error);\n }\n}\n\n/**\n * Retrieves comprehensive information about a package.\n * This function analyzes package information including:\n * - Current and latest versions\n * - Installation status and location\n * - Dependency type (regular, dev, peer, etc.)\n * - Whether it's the current project or an external dependency\n *\n * @param inputPackage - Optional package input configuration. If not provided, returns current project info.\n * @returns A promise that resolves to a standardized response with detailed package information.\n */\nexport async function getPackage(inputPackage?: I_PackageInput): Promise<I_Return<I_PackageInfo>> {\n try {\n if (!inputPackage) {\n if (pathExistsSync(PATH.PACKAGE_JSON)) {\n const packageJson = readJsonSync(PATH.PACKAGE_JSON) as I_PackageJson;\n const { name = '', version = '' } = packageJson;\n\n return {\n success: true,\n result: {\n name,\n currentVersion: version,\n latestVersion: version,\n isCurrentProject: true,\n isInstalled: true,\n isUpToDate: true,\n isDependency: false,\n isDevDependency: false,\n installedPath: PATH.PACKAGE_JSON,\n file: packageJson,\n },\n };\n }\n return {\n success: true,\n result: {\n name: '',\n currentVersion: '',\n latestVersion: '',\n isCurrentProject: false,\n isInstalled: false,\n isUpToDate: false,\n isDependency: false,\n isDevDependency: false,\n installedPath: '',\n file: {},\n },\n };\n }\n\n // if package.json does not exist\n if (!pathExistsSync(PATH.PACKAGE_JSON)) {\n return {\n success: true,\n result: {\n name: inputPackage.name,\n currentVersion: '',\n latestVersion: '',\n isCurrentProject: false,\n isInstalled: false,\n isUpToDate: false,\n isDependency: inputPackage.type === E_PackageType.DEPENDENCY,\n isDevDependency: inputPackage.type === E_PackageType.DEV_DEPENDENCY,\n installedPath: '',\n file: {},\n },\n };\n }\n\n const packageJson = readJsonSync(PATH.PACKAGE_JSON) as I_PackageJson;\n\n const { name, version = '', dependencies = {}, devDependencies = {} } = packageJson;\n\n // if it's the current project\n if (inputPackage.name === name) {\n return {\n success: true,\n result: {\n name,\n currentVersion: version,\n latestVersion: version,\n isCurrentProject: true,\n isInstalled: true,\n isUpToDate: true,\n isDependency: inputPackage.type === E_PackageType.DEPENDENCY,\n isDevDependency: inputPackage.type === E_PackageType.DEV_DEPENDENCY,\n installedPath: PATH.PACKAGE_JSON,\n file: packageJson,\n },\n };\n }\n\n const isDependency = inputPackage.name in dependencies;\n\n const isDevDependency = inputPackage.name in devDependencies;\n\n const latestVersionFound = await getLatestPackageVersion(inputPackage.name);\n\n if (!latestVersionFound.success) {\n return {\n success: true,\n result: {\n name: inputPackage.name,\n currentVersion: '',\n latestVersion: '',\n isCurrentProject: false,\n isInstalled: false,\n isUpToDate: false,\n isDependency,\n isDevDependency,\n installedPath: '',\n file: {},\n },\n };\n }\n\n // if it's not a dependency or devDependency\n if (!isDependency && !isDevDependency) {\n return {\n success: true,\n result: {\n name: inputPackage.name,\n currentVersion: '',\n latestVersion: latestVersionFound.result,\n isCurrentProject: false,\n isInstalled: false,\n isUpToDate: false,\n isDependency: inputPackage.type === E_PackageType.DEPENDENCY,\n isDevDependency: inputPackage.type === E_PackageType.DEV_DEPENDENCY,\n installedPath: '',\n file: {},\n },\n };\n }\n\n const packageJsonVersion = dependencies[inputPackage.name] ?? devDependencies[inputPackage.name] ?? '';\n const dependencyPackageJsonPath = join(env.CWD, NODE_MODULES, inputPackage.name, PACKAGE_JSON);\n\n // if package does not exist in node_modules\n if (!pathExistsSync(dependencyPackageJsonPath)) {\n return {\n success: true,\n result: {\n name: inputPackage.name,\n currentVersion: '',\n latestVersion: latestVersionFound.result,\n isCurrentProject: false,\n isInstalled: false,\n isUpToDate: false,\n isDependency,\n isDevDependency,\n installedPath: '',\n file: {},\n },\n };\n }\n\n const dependencyPackageJson = readJsonSync(dependencyPackageJsonPath) as I_PackageJson;\n\n const { version: dependencyVersion = '' } = dependencyPackageJson;\n\n // if version in package.json is different from version in node_modules\n if (packageJsonVersion !== dependencyVersion) {\n return {\n success: true,\n result: {\n name: inputPackage.name,\n currentVersion: dependencyVersion || packageJsonVersion,\n latestVersion: latestVersionFound.result,\n isCurrentProject: false,\n isInstalled: true,\n isUpToDate: false,\n isDependency,\n isDevDependency,\n installedPath: dependencyPackageJsonPath,\n file: dependencyPackageJson,\n },\n };\n }\n\n // if version in package.json is the same as version in node_modules\n return {\n success: true,\n result: {\n name: inputPackage.name,\n currentVersion: packageJsonVersion,\n latestVersion: latestVersionFound.result,\n isCurrentProject: false,\n isInstalled: true,\n isUpToDate: packageJsonVersion === latestVersionFound.result,\n isDependency,\n isDevDependency,\n installedPath: dependencyPackageJsonPath,\n file: dependencyPackageJson,\n },\n };\n }\n catch (error) {\n return catchError<I_PackageInfo>(error);\n }\n}\n\n/**\n * Updates a package to its latest version in package.json.\n * This function modifies the package.json file to update the specified package\n * to its latest version and logs the update action.\n *\n * @param packageInfo - The package information containing the latest version to update to.\n * @returns A promise that resolves when the update is complete.\n */\nexport async function updatePackage(packageInfo: I_PackageInfo): Promise<void> {\n try {\n const packageJson = readJsonSync(PATH.PACKAGE_JSON) as I_PackageJson;\n\n const dependencies = packageJson.dependencies ?? {};\n const devDependencies = packageJson.devDependencies ?? {};\n\n if (packageInfo.isDependency) {\n dependencies[packageInfo.name] = packageInfo.latestVersion;\n }\n else if (packageInfo.isDevDependency) {\n devDependencies[packageInfo.name] = packageInfo.latestVersion;\n }\n\n writeFileSync(PATH.PACKAGE_JSON, JSON.stringify(packageJson, null, 4));\n\n log.info(`Updated \"${packageInfo.name}\" to version ${packageInfo.latestVersion}`);\n }\n catch (error) {\n catchError(error);\n }\n}\n\n/**\n * Installs project dependencies using pnpm with fallback strategies.\n * This function attempts to install dependencies using different pnpm strategies:\n * 1. Standard installation\n * 2. Legacy peer dependencies mode\n * 3. Force installation mode\n *\n * @returns A promise that resolves when the installation is complete.\n */\nexport async function installDependencies(): Promise<void> {\n try {\n try {\n const cmd = await command.pnpmInstallStandard();\n await runCommand(`Installing dependencies (standard) using: ${cmd}`, cmd);\n return;\n }\n catch (error) {\n catchError(error);\n }\n\n try {\n const cmd = await command.pnpmInstallLegacy();\n await runCommand(`Retrying with legacy peer dependencies using: ${cmd}`, cmd);\n return;\n }\n catch (error) {\n catchError(error);\n }\n\n try {\n const cmd = await command.pnpmInstallForce();\n await runCommand(`Retrying with force install using: ${cmd}`, cmd);\n }\n catch (error) {\n catchError(error);\n }\n }\n catch (error) {\n catchError(error);\n }\n}\n\n/**\n * Sets up multiple packages with optional installation and update operations.\n * This function provides a comprehensive package management workflow that can:\n * - Install missing packages\n * - Update outdated packages\n * - Execute custom callbacks after package operations\n * - Run ESLint fixes after package changes\n *\n * @param packages - An array of package inputs to set up.\n * @param options - Optional configuration for installation, updates, and callbacks.\n * @param options.install - Whether to install missing packages (default: false).\n * @param options.update - Whether to update outdated packages (default: false).\n * @param options.callback - Optional callback function to execute after package operations.\n * @returns A promise that resolves when all package operations are complete.\n */\nexport async function setupPackages(\n packages: I_PackageInput[],\n options?: {\n install?: boolean;\n update?: boolean;\n callback?: () => Promise<void>;\n },\n): Promise<void> {\n try {\n if (!pathExistsSync(PATH.PACKAGE_JSON)) {\n log.error('package.json not found. Aborting setup.');\n return;\n }\n\n const packagesData = await Promise.all(packages.map(getPackage));\n\n const validPackages = packagesData\n .filter((pkg): pkg is { success: true; result: I_PackageInfo } => pkg.success && Boolean(pkg.result) && !pkg.result.isCurrentProject)\n .map(pkg => pkg.result);\n\n const packagesToInstall = validPackages.filter(pkg => !pkg.isInstalled);\n const packagesToUpdate = validPackages.filter(pkg => !pkg.isUpToDate);\n\n const tasks: Promise<void>[] = [];\n\n if (options?.install && packagesToInstall.length > 0) {\n tasks.push(...packagesToInstall.map(updatePackage));\n }\n\n if (options?.update && packagesToUpdate.length > 0) {\n tasks.push(...packagesToUpdate.map(updatePackage));\n }\n\n if (tasks.length > 0) {\n await Promise.all(tasks);\n await installDependencies();\n await runCommand('Running ESLint with auto-fix', await command.eslintFix());\n }\n\n await options?.callback?.();\n }\n catch (error) {\n catchError(error);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"package.util.js","names":[],"sources":["../../../src/node/package/package.util.ts"],"sourcesContent":["import type { I_Return } from '#typescript/index.js';\n\nimport { getEnv } from '#config/env/index.js';\n\nimport type { I_PackageInfo, I_PackageInput, I_PackageJson } from './package.type.js';\n\nimport { runCommand } from '../command/index.js';\nimport { pathExistsSync, readJsonSync, writeFileSync } from '../fs/index.js';\nimport { catchError, log } from '../log/index.js';\nimport { command, join, NODE_MODULES, PACKAGE_JSON, PATH } from '../path/index.js';\nimport { E_PackageType } from './package.type.js';\n\nconst env = getEnv();\n\n/**\n * Fetches the latest version of a package from the npm registry.\n * This function makes an HTTP request to the npm registry to get the latest version\n * information for a specified package.\n *\n * @param packageName - The name of the package to fetch the latest version for.\n * @returns A promise that resolves to a standardized response with the latest version string.\n */\nexport async function getLatestPackageVersion(packageName: string): Promise<I_Return<string>> {\n try {\n const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`);\n\n if (!res.ok) {\n throw new Error(`Failed to fetch latest version: ${res.status} ${res.statusText}`);\n }\n\n const { version } = await res.json() as { version: string };\n\n return {\n success: true,\n result: version,\n };\n }\n catch (error: unknown) {\n return catchError<string>(error);\n }\n}\n\n/**\n * Retrieves comprehensive information about a package.\n * This function analyzes package information including:\n * - Current and latest versions\n * - Installation status and location\n * - Dependency type (regular, dev, peer, etc.)\n * - Whether it's the current project or an external dependency\n *\n * @param inputPackage - Optional package input configuration. If not provided, returns current project info.\n * @returns A promise that resolves to a standardized response with detailed package information.\n */\nexport async function getPackage(inputPackage?: I_PackageInput): Promise<I_Return<I_PackageInfo>> {\n try {\n if (!inputPackage) {\n if (pathExistsSync(PATH.PACKAGE_JSON)) {\n const packageJson = readJsonSync(PATH.PACKAGE_JSON) as I_PackageJson;\n const { name = '', version = '' } = packageJson;\n\n return {\n success: true,\n result: {\n name,\n currentVersion: version,\n latestVersion: version,\n isCurrentProject: true,\n isInstalled: true,\n isUpToDate: true,\n isDependency: false,\n isDevDependency: false,\n installedPath: PATH.PACKAGE_JSON,\n file: packageJson,\n },\n };\n }\n return {\n success: true,\n result: {\n name: '',\n currentVersion: '',\n latestVersion: '',\n isCurrentProject: false,\n isInstalled: false,\n isUpToDate: false,\n isDependency: false,\n isDevDependency: false,\n installedPath: '',\n file: {},\n },\n };\n }\n\n // if package.json does not exist\n if (!pathExistsSync(PATH.PACKAGE_JSON)) {\n return {\n success: true,\n result: {\n name: inputPackage.name,\n currentVersion: '',\n latestVersion: '',\n isCurrentProject: false,\n isInstalled: false,\n isUpToDate: false,\n isDependency: inputPackage.type === E_PackageType.DEPENDENCY,\n isDevDependency: inputPackage.type === E_PackageType.DEV_DEPENDENCY,\n installedPath: '',\n file: {},\n },\n };\n }\n\n const packageJson = readJsonSync(PATH.PACKAGE_JSON) as I_PackageJson;\n\n const { name, version = '', dependencies = {}, devDependencies = {} } = packageJson;\n\n // if it's the current project\n if (inputPackage.name === name) {\n return {\n success: true,\n result: {\n name,\n currentVersion: version,\n latestVersion: version,\n isCurrentProject: true,\n isInstalled: true,\n isUpToDate: true,\n isDependency: inputPackage.type === E_PackageType.DEPENDENCY,\n isDevDependency: inputPackage.type === E_PackageType.DEV_DEPENDENCY,\n installedPath: PATH.PACKAGE_JSON,\n file: packageJson,\n },\n };\n }\n\n const isDependency = inputPackage.name in dependencies;\n\n const isDevDependency = inputPackage.name in devDependencies;\n\n const latestVersionFound = await getLatestPackageVersion(inputPackage.name);\n\n if (!latestVersionFound.success) {\n return {\n success: true,\n result: {\n name: inputPackage.name,\n currentVersion: '',\n latestVersion: '',\n isCurrentProject: false,\n isInstalled: false,\n isUpToDate: false,\n isDependency,\n isDevDependency,\n installedPath: '',\n file: {},\n },\n };\n }\n\n // if it's not a dependency or devDependency\n if (!isDependency && !isDevDependency) {\n return {\n success: true,\n result: {\n name: inputPackage.name,\n currentVersion: '',\n latestVersion: latestVersionFound.result,\n isCurrentProject: false,\n isInstalled: false,\n isUpToDate: false,\n isDependency: inputPackage.type === E_PackageType.DEPENDENCY,\n isDevDependency: inputPackage.type === E_PackageType.DEV_DEPENDENCY,\n installedPath: '',\n file: {},\n },\n };\n }\n\n const packageJsonVersion = dependencies[inputPackage.name] ?? devDependencies[inputPackage.name] ?? '';\n const dependencyPackageJsonPath = join(env.CWD, NODE_MODULES, inputPackage.name, PACKAGE_JSON);\n\n // if package does not exist in node_modules\n if (!pathExistsSync(dependencyPackageJsonPath)) {\n return {\n success: true,\n result: {\n name: inputPackage.name,\n currentVersion: '',\n latestVersion: latestVersionFound.result,\n isCurrentProject: false,\n isInstalled: false,\n isUpToDate: false,\n isDependency,\n isDevDependency,\n installedPath: '',\n file: {},\n },\n };\n }\n\n const dependencyPackageJson = readJsonSync(dependencyPackageJsonPath) as I_PackageJson;\n\n const { version: dependencyVersion = '' } = dependencyPackageJson;\n\n // if version in package.json is different from version in node_modules\n if (packageJsonVersion !== dependencyVersion) {\n return {\n success: true,\n result: {\n name: inputPackage.name,\n currentVersion: dependencyVersion || packageJsonVersion,\n latestVersion: latestVersionFound.result,\n isCurrentProject: false,\n isInstalled: true,\n isUpToDate: false,\n isDependency,\n isDevDependency,\n installedPath: dependencyPackageJsonPath,\n file: dependencyPackageJson,\n },\n };\n }\n\n // if version in package.json is the same as version in node_modules\n return {\n success: true,\n result: {\n name: inputPackage.name,\n currentVersion: packageJsonVersion,\n latestVersion: latestVersionFound.result,\n isCurrentProject: false,\n isInstalled: true,\n isUpToDate: packageJsonVersion === latestVersionFound.result,\n isDependency,\n isDevDependency,\n installedPath: dependencyPackageJsonPath,\n file: dependencyPackageJson,\n },\n };\n }\n catch (error: unknown) {\n return catchError<I_PackageInfo>(error);\n }\n}\n\n/**\n * Updates a package to its latest version in package.json.\n * This function modifies the package.json file to update the specified package\n * to its latest version and logs the update action.\n *\n * @param packageInfo - The package information containing the latest version to update to.\n * @returns A promise that resolves when the update is complete.\n */\nexport async function updatePackage(packageInfo: I_PackageInfo): Promise<void> {\n try {\n const packageJson = readJsonSync(PATH.PACKAGE_JSON) as I_PackageJson;\n\n const dependencies = packageJson.dependencies ?? {};\n const devDependencies = packageJson.devDependencies ?? {};\n\n if (packageInfo.isDependency) {\n dependencies[packageInfo.name] = packageInfo.latestVersion;\n }\n else if (packageInfo.isDevDependency) {\n devDependencies[packageInfo.name] = packageInfo.latestVersion;\n }\n\n writeFileSync(PATH.PACKAGE_JSON, JSON.stringify(packageJson, null, 4));\n\n log.info(`Updated \"${packageInfo.name}\" to version ${packageInfo.latestVersion}`);\n }\n catch (error: unknown) {\n catchError(error);\n }\n}\n\n/**\n * Installs project dependencies using pnpm with fallback strategies.\n * This function attempts to install dependencies using different pnpm strategies:\n * 1. Standard installation\n * 2. Legacy peer dependencies mode\n * 3. Force installation mode\n *\n * @returns A promise that resolves when the installation is complete.\n */\nexport async function installDependencies(): Promise<void> {\n try {\n try {\n const cmd = await command.pnpmInstallStandard();\n await runCommand(`Installing dependencies (standard) using: ${cmd}`, cmd);\n return;\n }\n catch (error: unknown) {\n catchError(error);\n }\n\n try {\n const cmd = await command.pnpmInstallLegacy();\n await runCommand(`Retrying with legacy peer dependencies using: ${cmd}`, cmd);\n return;\n }\n catch (error: unknown) {\n catchError(error);\n }\n\n try {\n const cmd = await command.pnpmInstallForce();\n await runCommand(`Retrying with force install using: ${cmd}`, cmd);\n }\n catch (error: unknown) {\n catchError(error);\n }\n }\n catch (error: unknown) {\n catchError(error);\n }\n}\n\n/**\n * Sets up multiple packages with optional installation and update operations.\n * This function provides a comprehensive package management workflow that can:\n * - Install missing packages\n * - Update outdated packages\n * - Execute custom callbacks after package operations\n * - Run ESLint fixes after package changes\n *\n * @param packages - An array of package inputs to set up.\n * @param options - Optional configuration for installation, updates, and callbacks.\n * @param options.install - Whether to install missing packages (default: false).\n * @param options.update - Whether to update outdated packages (default: false).\n * @param options.callback - Optional callback function to execute after package operations.\n * @returns A promise that resolves when all package operations are complete.\n */\nexport async function setupPackages(\n packages: I_PackageInput[],\n options?: {\n install?: boolean;\n update?: boolean;\n callback?: () => Promise<void>;\n },\n): Promise<void> {\n try {\n if (!pathExistsSync(PATH.PACKAGE_JSON)) {\n log.error('package.json not found. Aborting setup.');\n return;\n }\n\n const packagesData = await Promise.all(packages.map(getPackage));\n\n const validPackages = packagesData\n .filter((pkg): pkg is { success: true; result: I_PackageInfo } => pkg.success && Boolean(pkg.result) && !pkg.result.isCurrentProject)\n .map(pkg => pkg.result);\n\n const packagesToInstall = validPackages.filter(pkg => !pkg.isInstalled);\n const packagesToUpdate = validPackages.filter(pkg => !pkg.isUpToDate);\n\n const tasks: Promise<void>[] = [];\n\n if (options?.install && packagesToInstall.length > 0) {\n tasks.push(...packagesToInstall.map(updatePackage));\n }\n\n if (options?.update && packagesToUpdate.length > 0) {\n tasks.push(...packagesToUpdate.map(updatePackage));\n }\n\n if (tasks.length > 0) {\n await Promise.all(tasks);\n await installDependencies();\n await runCommand('Running ESLint with auto-fix', await command.eslintFix());\n }\n\n await options?.callback?.();\n }\n catch (error: unknown) {\n catchError(error);\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"storage.util.js","names":[],"sources":["../../../src/node/storage/storage.util.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport { getEnv } from '#config/env/index.js';\n\nimport { createTtlEnvelope, isExpiredEnvelope, isTtlEnvelope } from '../../util/storage/storage-envelope.js';\nimport { catchError, log } from '../log/index.js';\nimport { STORAGE_KEY_EXTENSION } from './storage.constant.js';\n\nconst MAX_KEY_LENGTH = 200;\n\ninterface NodeFsDriverState {\n baseDir: string;\n}\n\nexport interface I_StorageDriver {\n init: (options?: unknown) => Promise<void>;\n clear: () => Promise<void>;\n getItem: <T>(key: string) => Promise<T | null>;\n keys: () => Promise<string[]>;\n removeItem: (key: string) => Promise<void>;\n setItem: <T>(key: string, value: T) => Promise<T>;\n}\n\nconst nodeFsDriverState: NodeFsDriverState = {\n baseDir: '',\n};\n\n/**\n * Encodes a storage key into a filename-safe string.\n * Validates key length before encoding to prevent OS filename limits.\n *\n * @throws {RangeError} When key exceeds maximum length.\n */\nfunction encodeKey(key: string): string {\n if (key.length > MAX_KEY_LENGTH) {\n throw new RangeError(`Storage key exceeds maximum length of ${MAX_KEY_LENGTH} characters`);\n }\n return `${encodeURIComponent(key)}${STORAGE_KEY_EXTENSION}`;\n}\n\n/**\n * Decodes a filename-safe key back to the original storage key.\n */\nfunction decodeKey(fileName: string): string {\n return decodeURIComponent(fileName.slice(0, -STORAGE_KEY_EXTENSION.length));\n}\n\n/**\n * Maps a storage key to an absolute file path inside the storage directory.\n */\nfunction getFilePath(key: string, baseDir: string): string {\n return path.join(baseDir, encodeKey(key));\n}\n\n/**\n * Filesystem-backed storage driver that stores JSON-encoded values on disk.\n * Directly implements all storage operations without any external dependencies.\n */\nconst fsDriver: I_StorageDriver = {\n /** Ensures the storage directory exists. */\n async init(baseDir?: unknown) {\n try {\n if (typeof baseDir === 'string' && baseDir.length > 0) {\n nodeFsDriverState.baseDir = baseDir;\n }\n else {\n nodeFsDriverState.baseDir = getEnv().CYBERSKILL_STORAGE_DIRECTORY;\n }\n\n await fs.mkdir(nodeFsDriverState.baseDir, { recursive: true });\n }\n catch (error) {\n log.error('[Storage:init]', error);\n throw error;\n }\n },\n /** Deletes all stored entries atomically by swapping to a fresh directory. */\n async clear() {\n const { baseDir } = nodeFsDriverState;\n\n if (!baseDir) {\n return;\n }\n\n // Atomic swap: create a fresh temp dir, rename old→trash, rename fresh→baseDir, remove trash\n const trashDir = `${baseDir}.trash.${Date.now()}`;\n const freshDir = `${baseDir}.fresh.${Date.now()}`;\n\n try {\n await fs.mkdir(freshDir, { recursive: true });\n // Try atomic rename swap\n try {\n await fs.rename(baseDir, trashDir);\n }\n catch {\n // baseDir might not exist yet; no-op\n }\n await fs.rename(freshDir, baseDir);\n // Clean up trash in the background (non-blocking)\n fs.rm(trashDir, { recursive: true, force: true }).catch(() => { });\n }\n catch {\n // Fallback: non-atomic clear (e.g., cross-device rename)\n await fs.rm(baseDir, { recursive: true, force: true });\n await fs.mkdir(baseDir, { recursive: true });\n // Clean up any leftover temp dirs\n fs.rm(freshDir, { recursive: true, force: true }).catch(() => { });\n fs.rm(trashDir, { recursive: true, force: true }).catch(() => { });\n }\n },\n /** Reads and parses a stored value; returns null when the file is missing. */\n async getItem<T>(key: string): Promise<T | null> {\n const { baseDir } = nodeFsDriverState;\n const filePath = getFilePath(key, baseDir);\n\n try {\n const content = await fs.readFile(filePath, 'utf8');\n\n return JSON.parse(content) as T;\n }\n catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n return null;\n }\n throw error;\n }\n },\n /** Lists all stored keys. */\n async keys(): Promise<string[]> {\n const { baseDir } = nodeFsDriverState;\n\n try {\n const files = await fs.readdir(baseDir);\n\n return files\n .filter(file => file.endsWith(STORAGE_KEY_EXTENSION))\n .map(decodeKey);\n }\n catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n return [];\n }\n throw error;\n }\n },\n /** Removes a stored value for the given key. */\n async removeItem(key: string): Promise<void> {\n const { baseDir } = nodeFsDriverState;\n const filePath = getFilePath(key, baseDir);\n\n await fs.rm(filePath, { force: true });\n },\n /** Stores a value as JSON on disk. */\n async setItem<T>(key: string, value: T): Promise<T> {\n const { baseDir } = nodeFsDriverState;\n const filePath = getFilePath(key, baseDir);\n\n await fs.mkdir(baseDir, { recursive: true });\n await fs.writeFile(filePath, JSON.stringify(value), 'utf8');\n\n return value;\n },\n};\n\nlet initPromise: Promise<void> | null = null;\nlet activeDriver: I_StorageDriver = fsDriver;\n\n/**\n * Initializes the storage driver (singleton, idempotent).\n */\nasync function ensureDriverReady(): Promise<I_StorageDriver> {\n if (initPromise) {\n await initPromise;\n return activeDriver;\n }\n\n initPromise = activeDriver.init().catch((error) => {\n initPromise = null;\n throw error;\n });\n\n await initPromise;\n\n return activeDriver;\n}\n\n/**\n * Persistent storage utility object for data persistence across application sessions.\n * Uses a filesystem-backed driver that stores JSON-encoded values on disk,\n * with automatic initialization and error handling.\n */\nexport const storage = {\n /**\n * Initializes the utility with a custom storage driver instead of the default filesystem driver.\n * This allows swapping to Redis, Memory, or cloud-based drivers.\n * Must optionally be called before the first read/write operation.\n *\n * @param driver - The custom storage driver object that adheres to I_StorageDriver.\n */\n async initDriver(driver: I_StorageDriver): Promise<void> {\n activeDriver = driver;\n initPromise = null;\n await ensureDriverReady();\n },\n /**\n * Retrieves a value from persistent storage by key.\n * This method fetches data that was previously stored using the set method.\n * Returns null if the key doesn't exist or if an error occurs.\n *\n * @param key - The unique identifier for the stored value.\n * @returns A promise that resolves to the stored value or null if not found.\n */\n async get<T = unknown>(key: string): Promise<T | null> {\n try {\n const driver = await ensureDriverReady();\n const result = await driver.getItem<unknown>(key);\n\n if (result === null) {\n return null;\n }\n\n if (isTtlEnvelope<T>(result)) {\n if (isExpiredEnvelope(result)) {\n driver.removeItem(key).catch(() => { });\n\n return null;\n }\n\n return result.value;\n }\n\n return result as T;\n }\n catch (error) {\n return catchError(error, { returnValue: null });\n }\n },\n /**\n * Stores a value in persistent storage with a unique key.\n * This method saves data that can be retrieved later using the get method.\n * The data is automatically serialized and stored in the configured storage directory.\n *\n * @param key - The unique identifier for the value to store.\n * @param value - The data to store (will be automatically serialized).\n * @param options - Optional settings, such as `ttlMs` for setting an expiration on the key.\n * @param options.ttlMs - The time-to-live in milliseconds.\n * @returns A promise that resolves when the storage operation is complete.\n */\n async set<T = unknown>(key: string, value: T, options?: { ttlMs?: number }): Promise<void> {\n try {\n const driver = await ensureDriverReady();\n\n let payloadToStore: unknown = value;\n\n if (options?.ttlMs) {\n payloadToStore = createTtlEnvelope(value, options.ttlMs);\n }\n\n await driver.setItem(key, payloadToStore);\n }\n catch (error) {\n catchError(error);\n throw error;\n }\n },\n /**\n * Removes a value from persistent storage by key.\n * This method permanently deletes the stored data associated with the specified key.\n *\n * @param key - The unique identifier of the value to remove.\n * @returns A promise that resolves when the removal operation is complete.\n */\n async remove(key: string): Promise<void> {\n try {\n const driver = await ensureDriverReady();\n\n await driver.removeItem(key);\n }\n catch (error) {\n catchError(error);\n }\n },\n /**\n * Checks if a key exists in persistent storage.\n * This method efficiently checks for key existence and respects TTL parsing.\n * Returns false if the key exists but has expired.\n *\n * @param key - The unique identifier to check.\n * @returns A promise that resolves to true if the key exists and has not expired.\n * @since 3.13.0\n */\n async has(key: string): Promise<boolean> {\n try {\n const driver = await ensureDriverReady();\n const result = await driver.getItem<unknown>(key);\n\n if (result === null) {\n return false;\n }\n\n if (isTtlEnvelope<unknown>(result)) {\n if (isExpiredEnvelope(result)) {\n driver.removeItem(key).catch(() => { });\n\n return false;\n }\n }\n\n return true;\n }\n catch (error) {\n return catchError(error, { returnValue: false });\n }\n },\n /**\n * Clears all entries from storage atomically.\n * @returns A promise that resolves when the clearing operation is complete.\n */\n async clear(): Promise<void> {\n try {\n const driver = await ensureDriverReady();\n await driver.clear();\n }\n catch (error) {\n catchError(error);\n }\n },\n /**\n * Retrieves all storage keys.\n * This method returns an array of all keys that currently have stored values.\n * Returns an empty array if no keys exist or if an error occurs.\n *\n * @returns A promise that resolves to an array of storage keys.\n */\n async keys(): Promise<string[]> {\n try {\n const driver = await ensureDriverReady();\n const keys = await driver.keys();\n\n if (!Array.isArray(keys)) {\n log.warn(`[Storage:keys] Invalid keys response:`, keys);\n return [];\n }\n\n return keys;\n }\n catch (error) {\n return catchError(error, { returnValue: [] });\n }\n },\n /**\n * Gets a human-readable log link for a storage key.\n * This method provides a formatted string that shows the storage directory path\n * and the key name for debugging and manual inspection purposes.\n *\n * @param key - The storage key to generate a log link for.\n * @returns A promise that resolves to a formatted log link string or null if an error occurs.\n */\n async getLogLink(key: string): Promise<string | null> {\n try {\n const storagePath = path.join(getEnv().CYBERSKILL_STORAGE_DIRECTORY, encodeKey(key));\n\n return `${storagePath} (key: ${key})`;\n }\n catch (error) {\n return catchError(error, { returnValue: null });\n }\n },\n /**\n * Retrieves a value from persistent storage, or creates and stores it if it doesn't exist.\n * This method combines check, creation, and storage into a single convenient operation.\n *\n * @param key - The unique identifier for the value.\n * @param factory - A function (sync or async) that generates the value if it's missing or expired.\n * @param options - Optional storage options.\n * @param options.ttlMs - The time-to-live in milliseconds.\n * @returns A promise that resolves to the retrieved or newly created value.\n */\n async getOrSet<T = unknown>(key: string, factory: () => T | Promise<T>, options?: { ttlMs?: number }): Promise<T> {\n let value = await this.get<T>(key);\n\n if (value === null) {\n value = await factory();\n await this.set(key, value, options);\n }\n\n return value;\n },\n};\n\n/**\n * Resets all module-level singleton state used by the storage module.\n * Intended for use in tests to ensure isolation between test cases.\n * Do NOT call this in production code.\n * @since 3.13.0\n */\nexport function resetStorageForTesting(): void {\n initPromise = null;\n activeDriver = fsDriver;\n nodeFsDriverState.baseDir = '';\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"storage.util.js","names":[],"sources":["../../../src/node/storage/storage.util.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport { getEnv } from '#config/env/index.js';\n\nimport { createTtlEnvelope, isExpiredEnvelope, isTtlEnvelope } from '../../util/storage/storage-envelope.js';\nimport { catchError, log } from '../log/index.js';\nimport { STORAGE_KEY_EXTENSION } from './storage.constant.js';\n\nconst MAX_KEY_LENGTH = 200;\n\ninterface NodeFsDriverState {\n baseDir: string;\n}\n\nexport interface I_StorageDriver {\n init: (options?: unknown) => Promise<void>;\n clear: () => Promise<void>;\n getItem: <T>(key: string) => Promise<T | null>;\n keys: () => Promise<string[]>;\n removeItem: (key: string) => Promise<void>;\n setItem: <T>(key: string, value: T) => Promise<T>;\n}\n\nconst nodeFsDriverState: NodeFsDriverState = {\n baseDir: '',\n};\n\n/**\n * Encodes a storage key into a filename-safe string.\n * Validates key length before encoding to prevent OS filename limits.\n *\n * @throws {RangeError} When key exceeds maximum length.\n */\nfunction encodeKey(key: string): string {\n if (key.length > MAX_KEY_LENGTH) {\n throw new RangeError(`Storage key exceeds maximum length of ${MAX_KEY_LENGTH} characters`);\n }\n return `${encodeURIComponent(key)}${STORAGE_KEY_EXTENSION}`;\n}\n\n/**\n * Decodes a filename-safe key back to the original storage key.\n */\nfunction decodeKey(fileName: string): string {\n return decodeURIComponent(fileName.slice(0, -STORAGE_KEY_EXTENSION.length));\n}\n\n/**\n * Maps a storage key to an absolute file path inside the storage directory.\n */\nfunction getFilePath(key: string, baseDir: string): string {\n return path.join(baseDir, encodeKey(key));\n}\n\n/**\n * Filesystem-backed storage driver that stores JSON-encoded values on disk.\n * Directly implements all storage operations without any external dependencies.\n */\nconst fsDriver: I_StorageDriver = {\n /** Ensures the storage directory exists. */\n async init(baseDir?: unknown) {\n try {\n if (typeof baseDir === 'string' && baseDir.length > 0) {\n nodeFsDriverState.baseDir = baseDir;\n }\n else {\n nodeFsDriverState.baseDir = getEnv().CYBERSKILL_STORAGE_DIRECTORY;\n }\n\n await fs.mkdir(nodeFsDriverState.baseDir, { recursive: true });\n }\n catch (error: unknown) {\n log.error('[Storage:init]', error);\n throw error;\n }\n },\n /** Deletes all stored entries atomically by swapping to a fresh directory. */\n async clear() {\n const { baseDir } = nodeFsDriverState;\n\n if (!baseDir) {\n return;\n }\n\n // Atomic swap: create a fresh temp dir, rename old→trash, rename fresh→baseDir, remove trash\n const trashDir = `${baseDir}.trash.${Date.now()}`;\n const freshDir = `${baseDir}.fresh.${Date.now()}`;\n\n try {\n await fs.mkdir(freshDir, { recursive: true });\n // Try atomic rename swap\n try {\n await fs.rename(baseDir, trashDir);\n }\n catch {\n // baseDir might not exist yet; no-op\n }\n await fs.rename(freshDir, baseDir);\n // Clean up trash in the background (non-blocking)\n fs.rm(trashDir, { recursive: true, force: true }).catch(() => { });\n }\n catch {\n // Fallback: non-atomic clear (e.g., cross-device rename)\n await fs.rm(baseDir, { recursive: true, force: true });\n await fs.mkdir(baseDir, { recursive: true });\n // Clean up any leftover temp dirs\n fs.rm(freshDir, { recursive: true, force: true }).catch(() => { });\n fs.rm(trashDir, { recursive: true, force: true }).catch(() => { });\n }\n },\n /** Reads and parses a stored value; returns null when the file is missing. */\n async getItem<T>(key: string): Promise<T | null> {\n const { baseDir } = nodeFsDriverState;\n const filePath = getFilePath(key, baseDir);\n\n try {\n const content = await fs.readFile(filePath, 'utf8');\n\n return JSON.parse(content) as T;\n }\n catch (error: unknown) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n return null;\n }\n throw error;\n }\n },\n /** Lists all stored keys. */\n async keys(): Promise<string[]> {\n const { baseDir } = nodeFsDriverState;\n\n try {\n const files = await fs.readdir(baseDir);\n\n return files\n .filter(file => file.endsWith(STORAGE_KEY_EXTENSION))\n .map(decodeKey);\n }\n catch (error: unknown) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n return [];\n }\n throw error;\n }\n },\n /** Removes a stored value for the given key. */\n async removeItem(key: string): Promise<void> {\n const { baseDir } = nodeFsDriverState;\n const filePath = getFilePath(key, baseDir);\n\n await fs.rm(filePath, { force: true });\n },\n /** Stores a value as JSON on disk. */\n async setItem<T>(key: string, value: T): Promise<T> {\n const { baseDir } = nodeFsDriverState;\n const filePath = getFilePath(key, baseDir);\n\n await fs.mkdir(baseDir, { recursive: true });\n await fs.writeFile(filePath, JSON.stringify(value), 'utf8');\n\n return value;\n },\n};\n\nlet initPromise: Promise<void> | null = null;\nlet activeDriver: I_StorageDriver = fsDriver;\n\n/**\n * Initializes the storage driver (singleton, idempotent).\n */\nasync function ensureDriverReady(): Promise<I_StorageDriver> {\n if (initPromise) {\n await initPromise;\n return activeDriver;\n }\n\n initPromise = activeDriver.init().catch((error) => {\n initPromise = null;\n throw error;\n });\n\n await initPromise;\n\n return activeDriver;\n}\n\n/**\n * Persistent storage utility object for data persistence across application sessions.\n * Uses a filesystem-backed driver that stores JSON-encoded values on disk,\n * with automatic initialization and error handling.\n */\nexport const storage = {\n /**\n * Initializes the utility with a custom storage driver instead of the default filesystem driver.\n * This allows swapping to Redis, Memory, or cloud-based drivers.\n * Must optionally be called before the first read/write operation.\n *\n * @param driver - The custom storage driver object that adheres to I_StorageDriver.\n */\n async initDriver(driver: I_StorageDriver): Promise<void> {\n activeDriver = driver;\n initPromise = null;\n await ensureDriverReady();\n },\n /**\n * Retrieves a value from persistent storage by key.\n * This method fetches data that was previously stored using the set method.\n * Returns null if the key doesn't exist or if an error occurs.\n *\n * @param key - The unique identifier for the stored value.\n * @returns A promise that resolves to the stored value or null if not found.\n */\n async get<T = unknown>(key: string): Promise<T | null> {\n try {\n const driver = await ensureDriverReady();\n const result = await driver.getItem<unknown>(key);\n\n if (result === null) {\n return null;\n }\n\n if (isTtlEnvelope<T>(result)) {\n if (isExpiredEnvelope(result)) {\n driver.removeItem(key).catch(() => { });\n\n return null;\n }\n\n return result.value;\n }\n\n return result as T;\n }\n catch (error: unknown) {\n return catchError(error, { returnValue: null });\n }\n },\n /**\n * Stores a value in persistent storage with a unique key.\n * This method saves data that can be retrieved later using the get method.\n * The data is automatically serialized and stored in the configured storage directory.\n *\n * @param key - The unique identifier for the value to store.\n * @param value - The data to store (will be automatically serialized).\n * @param options - Optional settings, such as `ttlMs` for setting an expiration on the key.\n * @param options.ttlMs - The time-to-live in milliseconds.\n * @returns A promise that resolves when the storage operation is complete.\n */\n async set<T = unknown>(key: string, value: T, options?: { ttlMs?: number }): Promise<void> {\n try {\n const driver = await ensureDriverReady();\n\n let payloadToStore: unknown = value;\n\n if (options?.ttlMs) {\n payloadToStore = createTtlEnvelope(value, options.ttlMs);\n }\n\n await driver.setItem(key, payloadToStore);\n }\n catch (error: unknown) {\n catchError(error);\n throw error;\n }\n },\n /**\n * Removes a value from persistent storage by key.\n * This method permanently deletes the stored data associated with the specified key.\n *\n * @param key - The unique identifier of the value to remove.\n * @returns A promise that resolves when the removal operation is complete.\n */\n async remove(key: string): Promise<void> {\n try {\n const driver = await ensureDriverReady();\n\n await driver.removeItem(key);\n }\n catch (error: unknown) {\n catchError(error);\n }\n },\n /**\n * Checks if a key exists in persistent storage.\n * This method efficiently checks for key existence and respects TTL parsing.\n * Returns false if the key exists but has expired.\n *\n * @param key - The unique identifier to check.\n * @returns A promise that resolves to true if the key exists and has not expired.\n * @since 3.13.0\n */\n async has(key: string): Promise<boolean> {\n try {\n const driver = await ensureDriverReady();\n const result = await driver.getItem<unknown>(key);\n\n if (result === null) {\n return false;\n }\n\n if (isTtlEnvelope<unknown>(result)) {\n if (isExpiredEnvelope(result)) {\n driver.removeItem(key).catch(() => { });\n\n return false;\n }\n }\n\n return true;\n }\n catch (error: unknown) {\n return catchError(error, { returnValue: false });\n }\n },\n /**\n * Clears all entries from storage atomically.\n * @returns A promise that resolves when the clearing operation is complete.\n */\n async clear(): Promise<void> {\n try {\n const driver = await ensureDriverReady();\n await driver.clear();\n }\n catch (error: unknown) {\n catchError(error);\n }\n },\n /**\n * Retrieves all storage keys.\n * This method returns an array of all keys that currently have stored values.\n * Returns an empty array if no keys exist or if an error occurs.\n *\n * @returns A promise that resolves to an array of storage keys.\n */\n async keys(): Promise<string[]> {\n try {\n const driver = await ensureDriverReady();\n const keys = await driver.keys();\n\n if (!Array.isArray(keys)) {\n log.warn(`[Storage:keys] Invalid keys response:`, keys);\n return [];\n }\n\n return keys;\n }\n catch (error: unknown) {\n return catchError(error, { returnValue: [] });\n }\n },\n /**\n * Gets a human-readable log link for a storage key.\n * This method provides a formatted string that shows the storage directory path\n * and the key name for debugging and manual inspection purposes.\n *\n * @param key - The storage key to generate a log link for.\n * @returns A promise that resolves to a formatted log link string or null if an error occurs.\n */\n async getLogLink(key: string): Promise<string | null> {\n try {\n const storagePath = path.join(getEnv().CYBERSKILL_STORAGE_DIRECTORY, encodeKey(key));\n\n return `${storagePath} (key: ${key})`;\n }\n catch (error: unknown) {\n return catchError(error, { returnValue: null });\n }\n },\n /**\n * Retrieves a value from persistent storage, or creates and stores it if it doesn't exist.\n * This method combines check, creation, and storage into a single convenient operation.\n *\n * @param key - The unique identifier for the value.\n * @param factory - A function (sync or async) that generates the value if it's missing or expired.\n * @param options - Optional storage options.\n * @param options.ttlMs - The time-to-live in milliseconds.\n * @returns A promise that resolves to the retrieved or newly created value.\n */\n async getOrSet<T = unknown>(key: string, factory: () => T | Promise<T>, options?: { ttlMs?: number }): Promise<T> {\n let value = await this.get<T>(key);\n\n if (value === null) {\n value = await factory();\n await this.set(key, value, options);\n }\n\n return value;\n },\n};\n\n/**\n * Resets all module-level singleton state used by the storage module.\n * Intended for use in tests to ensure isolation between test cases.\n * Do NOT call this in production code.\n * @since 3.13.0\n */\nexport function resetStorageForTesting(): void {\n initPromise = null;\n activeDriver = fsDriver;\n nodeFsDriverState.baseDir = '';\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"upload.util.js","names":[],"sources":["../../../src/node/upload/upload.util.ts"],"sourcesContent":["import { Buffer } from 'node:buffer';\nimport nodePath from 'node:path';\nimport { Transform } from 'node:stream';\nimport { ReadableStream } from 'node:stream/web';\n\nimport type { I_Return } from '#typescript/index.js';\n\nimport { RESPONSE_STATUS } from '#constant/index.js';\n\nimport type { I_UploadConfig, I_UploadFile, I_UploadFileData, I_UploadOptions, I_UploadTypeConfig, I_UploadValidationConfig } from './upload.type.js';\n\nimport { createWriteStream, mkdirSync, pathExistsSync } from '../fs/index.js';\nimport { log } from '../log/index.js';\nimport { dirname } from '../path/index.js';\nimport { BYTES_PER_MB, DEFAULT_UPLOAD_CONFIG } from './upload.constant.js';\nimport { E_UploadType } from './upload.type.js';\n\n/**\n * Calculates the size of a file from a readable stream.\n * This function reads through the entire stream to determine the total byte size\n * by accumulating the length of each data chunk.\n *\n * @param stream - The readable stream to calculate the size for.\n * @returns A promise that resolves to the total size of the stream in bytes.\n */\nexport async function getFileSizeFromStream(stream: NodeJS.ReadableStream): Promise<number> {\n return new Promise((resolve, reject) => {\n let size = 0;\n stream.on('data', (chunk) => {\n size += chunk.length;\n });\n stream.on('end', () => resolve(size));\n stream.on('error', reject);\n });\n}\n\n/**\n * Extracts and validates file data from an upload file.\n * This function processes upload files by:\n * - Extracting file metadata and creating a readable stream\n * - Calculating the file size from the stream\n * - Validating file size and extension against upload configuration\n * - Returning a standardized response with success status and error codes\n * - Providing validated file data for further processing\n *\n * @param type - The type of upload being processed (IMAGE, VIDEO, DOCUMENT, OTHER).\n * @param file - The upload file object containing file metadata and stream creation method.\n * @param config - Optional upload configuration. If not provided, uses default configuration.\n * @returns A promise that resolves to a standardized response containing validated file data or error information.\n */\nexport async function getAndValidateFile(type: E_UploadType, file: I_UploadFile, config?: I_UploadConfig): Promise<I_Return<I_UploadFileData>> {\n const fileData = await (await file).file;\n const stream = fileData.createReadStream();\n // Stream is consumed here for validation; callers use createReadStream() again for the actual write.\n // This is intentional — createReadStream() is a factory that yields a new stream per call.\n const fileSize = await getFileSizeFromStream(stream);\n const uploadConfig = config ?? createUploadConfig();\n\n const validationResult = validateUpload(\n { filename: fileData.filename, fileSize, mimetype: fileData.mimetype },\n uploadConfig,\n type,\n );\n\n if (!validationResult.isValid) {\n return {\n success: false,\n message: validationResult.error || 'File validation failed',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n\n return {\n success: true,\n result: fileData,\n message: 'File validated successfully',\n };\n}\n\n/**\n * Creates a validated web-readable stream from an upload file with size validation.\n * This function processes file uploads for web environments by:\n * - Validating file data using getAndValidateFile function\n * - Creating a size validation transform stream to monitor upload progress\n * - Returning a web-compatible ReadableStream with real-time validation\n * - Providing standardized error responses for validation failures\n * - Wrapping the stream in a standardized response format\n *\n * @param type - The type of upload being processed (IMAGE, VIDEO, DOCUMENT, OTHER).\n * @param file - The upload file object containing file metadata and stream creation method.\n * @param config - Optional upload configuration. If not provided, uses default configuration.\n * @returns A promise that resolves to a standardized response containing either a web ReadableStream or error information.\n */\nexport async function getFileWebStream(type: E_UploadType, file: I_UploadFile, config?: I_UploadConfig): Promise<I_Return<ReadableStream<Uint8Array>>> {\n const uploadConfig = config ?? createUploadConfig();\n const typeConfig = uploadConfig[type];\n\n const fileData = await getAndValidateFile(type, file, config);\n\n if (!fileData.success) {\n return fileData;\n }\n\n const { createReadStream } = fileData.result;\n\n let remainingBytes = typeConfig.sizeLimit;\n\n const sizeValidationStream = new Transform({\n transform(chunk: Buffer, _enc: BufferEncoding, cb) {\n remainingBytes -= chunk.length;\n\n if (remainingBytes < 0) {\n cb(new Error(`File size exceeds limit of ${typeConfig.sizeLimit / BYTES_PER_MB}MB`));\n }\n else {\n cb(null, chunk);\n }\n },\n });\n const originalStream = createReadStream();\n const validatedStream = originalStream.pipe(sizeValidationStream);\n\n return {\n success: true,\n result: new ReadableStream<Uint8Array>({\n start(controller) {\n validatedStream.on('data', (chunk: Buffer | string) => {\n controller.enqueue(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);\n });\n validatedStream.on('end', () => controller.close());\n validatedStream.on('error', (err: unknown) => controller.error(err));\n },\n }),\n };\n}\n\n/**\n * Validates if a file has an allowed extension.\n * This function extracts the file extension from the filename and checks if it's\n * included in the list of allowed extensions (case-insensitive comparison).\n *\n * @param filename - The filename to check for valid extension.\n * @param allowedExtensions - An array of allowed file extensions (without dots).\n * @returns True if the file extension is allowed, false otherwise.\n */\nexport function validateFileExtension(filename: string, allowedExtensions: string[]): boolean {\n const lastDotIndex = filename.lastIndexOf('.');\n\n if (lastDotIndex === -1) {\n return false;\n }\n\n const extension = filename.substring(lastDotIndex + 1).toLowerCase();\n\n return allowedExtensions.includes(extension);\n}\n\n/**\n * Validates an upload against the specified configuration.\n * This function performs comprehensive validation including:\n * - File extension validation against allowed extensions\n * - File size validation against size limits\n * - Returns detailed error messages for validation failures\n *\n * @param config - The validation configuration including filename and optional file size.\n * @param uploadConfig - The upload configuration containing allowed extensions and size limits.\n * @param uploadType - The type of upload being validated.\n * @returns An object indicating validation success and optional error message.\n */\nexport function validateUpload(\n config: I_UploadValidationConfig,\n uploadConfig: I_UploadConfig,\n uploadType: E_UploadType,\n): { isValid: boolean; error?: string } {\n const { filename, fileSize, mimetype } = config;\n const typeConfig: I_UploadTypeConfig = uploadConfig[uploadType];\n\n const { allowedExtensions, sizeLimit } = typeConfig;\n\n if (!validateFileExtension(filename, allowedExtensions)) {\n return {\n isValid: false,\n error: `File extension not allowed for ${uploadType.toLowerCase()} files. Allowed extensions: ${allowedExtensions.join(', ')}`,\n };\n }\n\n if (fileSize !== undefined && fileSize > sizeLimit) {\n const maxSizeMB = Math.round(sizeLimit / (1024 * 1024));\n\n return {\n isValid: false,\n error: `File size exceeds limit for ${uploadType.toLowerCase()} files. Maximum size: ${maxSizeMB}MB`,\n };\n }\n\n if (mimetype) {\n let expectedPrefix = '';\n\n if (uploadType === E_UploadType.IMAGE) {\n expectedPrefix = 'image/';\n }\n else if (uploadType === E_UploadType.VIDEO) {\n expectedPrefix = 'video/';\n }\n else if (uploadType === E_UploadType.AUDIO) {\n expectedPrefix = 'audio/';\n }\n\n if (expectedPrefix && !mimetype.startsWith(expectedPrefix)) {\n // Advisory MIME validation - log warning but DO NOT reject\n log.warn(`Advisory Mimetype Warning: File '${filename}' (type: ${uploadType}) has unexpected mimetype '${mimetype}'. Expected prefix: '${expectedPrefix}'`);\n }\n }\n\n return { isValid: true };\n}\n\n/**\n * Creates a default upload configuration with predefined settings for different file types.\n * This function provides sensible defaults for image, video, document, and other file types,\n * including allowed extensions and size limits. The configuration can be customized with overrides.\n *\n * @param overrides - Optional configuration overrides to merge with the default configuration.\n * @returns A complete upload configuration object with defaults and any provided overrides.\n * @since 3.13.0\n */\nexport function createUploadConfig(overrides?: Partial<I_UploadConfig>): I_UploadConfig {\n return { ...DEFAULT_UPLOAD_CONFIG, ...overrides };\n}\n\n/**\n * Uploads a file with comprehensive validation and error handling.\n * This function processes file uploads with the following features:\n * - Input validation for path and file parameters\n * - Configuration validation for all upload types\n * - File validation using getAndValidateFile function\n * - Automatic directory creation\n * - Stream-based file writing\n * - Comprehensive error handling with standardized response codes\n *\n * @param options - Upload configuration including file, path, type, and optional validation config.\n * @returns A promise that resolves to a standardized response with success status, message, file path, and response codes.\n */\nexport async function upload(options: I_UploadOptions): Promise<I_Return<string>> {\n const { path, file, config, type, baseDir } = options;\n\n if (!path || typeof path !== 'string') {\n return {\n success: false,\n message: 'Invalid path provided',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n\n // Security: Validate path is within allowed base directory to prevent path traversal.\n // When baseDir is provided, the resolved path must start with it.\n // When baseDir is not provided, reject any path containing \"..\" segments.\n const resolvedPath = nodePath.resolve(path);\n\n if (baseDir) {\n const resolvedBase = nodePath.resolve(baseDir) + nodePath.sep;\n if (!resolvedPath.startsWith(resolvedBase) && resolvedPath !== nodePath.resolve(baseDir)) {\n return {\n success: false,\n message: 'Path traversal detected: path resolves outside the allowed base directory',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n }\n else if (path.includes('..')) {\n return {\n success: false,\n message: 'Path traversal detected: \"..\" segments are not allowed',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n\n if (!file || typeof file !== 'object') {\n return {\n success: false,\n message: 'Invalid file provided',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n\n if (config) {\n const requiredTypes = [E_UploadType.IMAGE, E_UploadType.VIDEO, E_UploadType.DOCUMENT, E_UploadType.OTHER];\n\n for (const requiredType of requiredTypes) {\n if (!config[requiredType] || !Array.isArray(config[requiredType].allowedExtensions) || config[requiredType].allowedExtensions.length === 0) {\n return {\n success: false,\n message: `Invalid config for ${requiredType.toLowerCase()} files`,\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n if (typeof config[requiredType].sizeLimit !== 'number' || config[requiredType].sizeLimit <= 0) {\n return {\n success: false,\n message: `Invalid size limit for ${requiredType.toLowerCase()} files`,\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n }\n }\n\n try {\n const fileData = await getAndValidateFile(type, await file, config);\n\n if (!fileData.success) {\n return fileData;\n }\n\n const { createReadStream } = fileData.result;\n\n const dir = dirname(path);\n\n if (!pathExistsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const readStream = createReadStream();\n const out = createWriteStream(path);\n readStream.pipe(out);\n\n await new Promise<void>((resolve, reject) => {\n out.on('finish', () => resolve());\n out.on('error', (err) => {\n // Destroy the read stream to release resources if write fails\n if ('destroy' in readStream && typeof readStream.destroy === 'function') {\n readStream.destroy();\n }\n reject(err);\n });\n readStream.on('error', (err) => {\n out.destroy();\n reject(err);\n });\n });\n\n return {\n success: true,\n result: path,\n message: 'File uploaded successfully',\n code: RESPONSE_STATUS.OK.CODE,\n };\n }\n catch (error) {\n return {\n success: false,\n message: error instanceof Error ? error.message : 'File upload failed',\n code: RESPONSE_STATUS.INTERNAL_SERVER_ERROR.CODE,\n };\n }\n}\n"],"mappings":";;;;;;;;;;;AAyBA,eAAsB,EAAsB,GAAgD;AACxF,QAAO,IAAI,SAAS,GAAS,MAAW;EACpC,IAAI,IAAO;AAKX,EAJA,EAAO,GAAG,SAAS,MAAU;AACzB,QAAQ,EAAM;IAChB,EACF,EAAO,GAAG,aAAa,EAAQ,EAAK,CAAC,EACrC,EAAO,GAAG,SAAS,EAAO;GAC5B;;AAiBN,eAAsB,EAAmB,GAAoB,GAAoB,GAA8D;CAC3I,IAAM,IAAW,OAAO,MAAM,GAAM,MAI9B,IAAW,MAAM,EAHR,EAAS,kBAAkB,CAGU,EAC9C,IAAe,KAAU,GAAoB,EAE7C,IAAmB,EACrB;EAAE,UAAU,EAAS;EAAU;EAAU,UAAU,EAAS;EAAU,EACtE,GACA,EACH;AAUD,QARK,EAAiB,UAQf;EACH,SAAS;EACT,QAAQ;EACR,SAAS;EACZ,GAXU;EACH,SAAS;EACT,SAAS,EAAiB,SAAS;EACnC,MAAM,EAAgB,YAAY;EACrC;;AAwBT,eAAsB,EAAiB,GAAoB,GAAoB,GAAwE;CAEnJ,IAAM,KADe,KAAU,GAAoB,EACnB,IAE1B,IAAW,MAAM,EAAmB,GAAM,GAAM,EAAO;AAE7D,KAAI,CAAC,EAAS,QACV,QAAO;CAGX,IAAM,EAAE,wBAAqB,EAAS,QAElC,IAAiB,EAAW,WAE1B,IAAuB,IAAI,EAAU,EACvC,UAAU,GAAe,GAAsB,GAAI;AAG/C,EAFA,KAAkB,EAAM,QAEpB,IAAiB,IACjB,EAAG,gBAAI,MAAM,8BAA8B,EAAW,YAAY,EAAa,IAAI,CAAC,GAGpF,EAAG,MAAM,EAAM;IAG1B,CAAC,EAEI,IADiB,GAAkB,CACF,KAAK,EAAqB;AAEjE,QAAO;EACH,SAAS;EACT,QAAQ,IAAI,EAA2B,EACnC,MAAM,GAAY;AAKd,GAJA,EAAgB,GAAG,SAAS,MAA2B;AACnD,MAAW,QAAQ,OAAO,KAAU,WAAW,EAAO,KAAK,EAAM,GAAG,EAAM;KAC5E,EACF,EAAgB,GAAG,aAAa,EAAW,OAAO,CAAC,EACnD,EAAgB,GAAG,UAAU,MAAiB,EAAW,MAAM,EAAI,CAAC;KAE3E,CAAC;EACL;;AAYL,SAAgB,EAAsB,GAAkB,GAAsC;CAC1F,IAAM,IAAe,EAAS,YAAY,IAAI;AAE9C,KAAI,MAAiB,GACjB,QAAO;CAGX,IAAM,IAAY,EAAS,UAAU,IAAe,EAAE,CAAC,aAAa;AAEpE,QAAO,EAAkB,SAAS,EAAU;;AAehD,SAAgB,EACZ,GACA,GACA,GACoC;CACpC,IAAM,EAAE,aAAU,aAAU,gBAAa,GAGnC,EAAE,sBAAmB,iBAFY,EAAa;AAIpD,KAAI,CAAC,EAAsB,GAAU,EAAkB,CACnD,QAAO;EACH,SAAS;EACT,OAAO,kCAAkC,EAAW,aAAa,CAAC,8BAA8B,EAAkB,KAAK,KAAK;EAC/H;AAGL,KAAI,MAAa,KAAA,KAAa,IAAW,GAAW;EAChD,IAAM,IAAY,KAAK,MAAM,KAAa,OAAO,MAAM;AAEvD,SAAO;GACH,SAAS;GACT,OAAO,+BAA+B,EAAW,aAAa,CAAC,wBAAwB,EAAU;GACpG;;AAGL,KAAI,GAAU;EACV,IAAI,IAAiB;AAYrB,EAVI,MAAe,EAAa,QAC5B,IAAiB,WAEZ,MAAe,EAAa,QACjC,IAAiB,WAEZ,MAAe,EAAa,UACjC,IAAiB,WAGjB,KAAkB,CAAC,EAAS,WAAW,EAAe,IAEtD,EAAI,KAAK,oCAAoC,EAAS,WAAW,EAAW,6BAA6B,EAAS,uBAAuB,EAAe,GAAG;;AAInK,QAAO,EAAE,SAAS,IAAM;;AAY5B,SAAgB,EAAmB,GAAqD;AACpF,QAAO;EAAE,GAAG;EAAuB,GAAG;EAAW;;AAgBrD,eAAsB,EAAO,GAAqD;CAC9E,IAAM,EAAE,SAAM,SAAM,WAAQ,SAAM,eAAY;AAE9C,KAAI,CAAC,KAAQ,OAAO,KAAS,SACzB,QAAO;EACH,SAAS;EACT,SAAS;EACT,MAAM,EAAgB,YAAY;EACrC;CAML,IAAM,IAAe,EAAS,QAAQ,EAAK;AAE3C,KAAI,GAAS;EACT,IAAM,IAAe,EAAS,QAAQ,EAAQ,GAAG,EAAS;AAC1D,MAAI,CAAC,EAAa,WAAW,EAAa,IAAI,MAAiB,EAAS,QAAQ,EAAQ,CACpF,QAAO;GACH,SAAS;GACT,SAAS;GACT,MAAM,EAAgB,YAAY;GACrC;YAGA,EAAK,SAAS,KAAK,CACxB,QAAO;EACH,SAAS;EACT,SAAS;EACT,MAAM,EAAgB,YAAY;EACrC;AAGL,KAAI,CAAC,KAAQ,OAAO,KAAS,SACzB,QAAO;EACH,SAAS;EACT,SAAS;EACT,MAAM,EAAgB,YAAY;EACrC;AAGL,KAAI,GAAQ;EACR,IAAM,IAAgB;GAAC,EAAa;GAAO,EAAa;GAAO,EAAa;GAAU,EAAa;GAAM;AAEzG,OAAK,IAAM,KAAgB,GAAe;AACtC,OAAI,CAAC,EAAO,MAAiB,CAAC,MAAM,QAAQ,EAAO,GAAc,kBAAkB,IAAI,EAAO,GAAc,kBAAkB,WAAW,EACrI,QAAO;IACH,SAAS;IACT,SAAS,sBAAsB,EAAa,aAAa,CAAC;IAC1D,MAAM,EAAgB,YAAY;IACrC;AAEL,OAAI,OAAO,EAAO,GAAc,aAAc,YAAY,EAAO,GAAc,aAAa,EACxF,QAAO;IACH,SAAS;IACT,SAAS,0BAA0B,EAAa,aAAa,CAAC;IAC9D,MAAM,EAAgB,YAAY;IACrC;;;AAKb,KAAI;EACA,IAAM,IAAW,MAAM,EAAmB,GAAM,MAAM,GAAM,EAAO;AAEnE,MAAI,CAAC,EAAS,QACV,QAAO;EAGX,IAAM,EAAE,wBAAqB,EAAS,QAEhC,IAAM,EAAQ,EAAK;AAEzB,EAAK,EAAe,EAAI,IACpB,EAAU,GAAK,EAAE,WAAW,IAAM,CAAC;EAGvC,IAAM,IAAa,GAAkB,EAC/B,IAAM,EAAkB,EAAK;AAkBnC,SAjBA,EAAW,KAAK,EAAI,EAEpB,MAAM,IAAI,SAAe,GAAS,MAAW;AASzC,GARA,EAAI,GAAG,gBAAgB,GAAS,CAAC,EACjC,EAAI,GAAG,UAAU,MAAQ;AAKrB,IAHI,aAAa,KAAc,OAAO,EAAW,WAAY,cACzD,EAAW,SAAS,EAExB,EAAO,EAAI;KACb,EACF,EAAW,GAAG,UAAU,MAAQ;AAE5B,IADA,EAAI,SAAS,EACb,EAAO,EAAI;KACb;IACJ,EAEK;GACH,SAAS;GACT,QAAQ;GACR,SAAS;GACT,MAAM,EAAgB,GAAG;GAC5B;UAEE,GAAO;AACV,SAAO;GACH,SAAS;GACT,SAAS,aAAiB,QAAQ,EAAM,UAAU;GAClD,MAAM,EAAgB,sBAAsB;GAC/C"}
1
+ {"version":3,"file":"upload.util.js","names":[],"sources":["../../../src/node/upload/upload.util.ts"],"sourcesContent":["import { Buffer } from 'node:buffer';\nimport nodePath from 'node:path';\nimport { Transform } from 'node:stream';\nimport { ReadableStream } from 'node:stream/web';\n\nimport type { I_Return } from '#typescript/index.js';\n\nimport { RESPONSE_STATUS } from '#constant/index.js';\n\nimport type { I_UploadConfig, I_UploadFile, I_UploadFileData, I_UploadOptions, I_UploadTypeConfig, I_UploadValidationConfig } from './upload.type.js';\n\nimport { createWriteStream, mkdirSync, pathExistsSync } from '../fs/index.js';\nimport { log } from '../log/index.js';\nimport { dirname } from '../path/index.js';\nimport { BYTES_PER_MB, DEFAULT_UPLOAD_CONFIG } from './upload.constant.js';\nimport { E_UploadType } from './upload.type.js';\n\n/**\n * Calculates the size of a file from a readable stream.\n * This function reads through the entire stream to determine the total byte size\n * by accumulating the length of each data chunk.\n *\n * @param stream - The readable stream to calculate the size for.\n * @returns A promise that resolves to the total size of the stream in bytes.\n */\nexport async function getFileSizeFromStream(stream: NodeJS.ReadableStream): Promise<number> {\n return new Promise((resolve, reject) => {\n let size = 0;\n stream.on('data', (chunk) => {\n size += chunk.length;\n });\n stream.on('end', () => resolve(size));\n stream.on('error', reject);\n });\n}\n\n/**\n * Extracts and validates file data from an upload file.\n * This function processes upload files by:\n * - Extracting file metadata and creating a readable stream\n * - Calculating the file size from the stream\n * - Validating file size and extension against upload configuration\n * - Returning a standardized response with success status and error codes\n * - Providing validated file data for further processing\n *\n * @param type - The type of upload being processed (IMAGE, VIDEO, DOCUMENT, OTHER).\n * @param file - The upload file object containing file metadata and stream creation method.\n * @param config - Optional upload configuration. If not provided, uses default configuration.\n * @returns A promise that resolves to a standardized response containing validated file data or error information.\n */\nexport async function getAndValidateFile(type: E_UploadType, file: I_UploadFile, config?: I_UploadConfig): Promise<I_Return<I_UploadFileData>> {\n const fileData = await (await file).file;\n const stream = fileData.createReadStream();\n // Stream is consumed here for validation; callers use createReadStream() again for the actual write.\n // This is intentional — createReadStream() is a factory that yields a new stream per call.\n const fileSize = await getFileSizeFromStream(stream);\n const uploadConfig = config ?? createUploadConfig();\n\n const validationResult = validateUpload(\n { filename: fileData.filename, fileSize, mimetype: fileData.mimetype },\n uploadConfig,\n type,\n );\n\n if (!validationResult.isValid) {\n return {\n success: false,\n message: validationResult.error || 'File validation failed',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n\n return {\n success: true,\n result: fileData,\n message: 'File validated successfully',\n };\n}\n\n/**\n * Creates a validated web-readable stream from an upload file with size validation.\n * This function processes file uploads for web environments by:\n * - Validating file data using getAndValidateFile function\n * - Creating a size validation transform stream to monitor upload progress\n * - Returning a web-compatible ReadableStream with real-time validation\n * - Providing standardized error responses for validation failures\n * - Wrapping the stream in a standardized response format\n *\n * @param type - The type of upload being processed (IMAGE, VIDEO, DOCUMENT, OTHER).\n * @param file - The upload file object containing file metadata and stream creation method.\n * @param config - Optional upload configuration. If not provided, uses default configuration.\n * @returns A promise that resolves to a standardized response containing either a web ReadableStream or error information.\n */\nexport async function getFileWebStream(type: E_UploadType, file: I_UploadFile, config?: I_UploadConfig): Promise<I_Return<ReadableStream<Uint8Array>>> {\n const uploadConfig = config ?? createUploadConfig();\n const typeConfig = uploadConfig[type];\n\n const fileData = await getAndValidateFile(type, file, config);\n\n if (!fileData.success) {\n return fileData;\n }\n\n const { createReadStream } = fileData.result;\n\n let remainingBytes = typeConfig.sizeLimit;\n\n const sizeValidationStream = new Transform({\n transform(chunk: Buffer, _enc: BufferEncoding, cb) {\n remainingBytes -= chunk.length;\n\n if (remainingBytes < 0) {\n cb(new Error(`File size exceeds limit of ${typeConfig.sizeLimit / BYTES_PER_MB}MB`));\n }\n else {\n cb(null, chunk);\n }\n },\n });\n const originalStream = createReadStream();\n const validatedStream = originalStream.pipe(sizeValidationStream);\n\n return {\n success: true,\n result: new ReadableStream<Uint8Array>({\n start(controller) {\n validatedStream.on('data', (chunk: Buffer | string) => {\n controller.enqueue(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);\n });\n validatedStream.on('end', () => controller.close());\n validatedStream.on('error', (err: unknown) => controller.error(err));\n },\n }),\n };\n}\n\n/**\n * Validates if a file has an allowed extension.\n * This function extracts the file extension from the filename and checks if it's\n * included in the list of allowed extensions (case-insensitive comparison).\n *\n * @param filename - The filename to check for valid extension.\n * @param allowedExtensions - An array of allowed file extensions (without dots).\n * @returns True if the file extension is allowed, false otherwise.\n */\nexport function validateFileExtension(filename: string, allowedExtensions: string[]): boolean {\n const lastDotIndex = filename.lastIndexOf('.');\n\n if (lastDotIndex === -1) {\n return false;\n }\n\n const extension = filename.substring(lastDotIndex + 1).toLowerCase();\n\n return allowedExtensions.includes(extension);\n}\n\n/**\n * Validates an upload against the specified configuration.\n * This function performs comprehensive validation including:\n * - File extension validation against allowed extensions\n * - File size validation against size limits\n * - Returns detailed error messages for validation failures\n *\n * @param config - The validation configuration including filename and optional file size.\n * @param uploadConfig - The upload configuration containing allowed extensions and size limits.\n * @param uploadType - The type of upload being validated.\n * @returns An object indicating validation success and optional error message.\n */\nexport function validateUpload(\n config: I_UploadValidationConfig,\n uploadConfig: I_UploadConfig,\n uploadType: E_UploadType,\n): { isValid: boolean; error?: string } {\n const { filename, fileSize, mimetype } = config;\n const typeConfig: I_UploadTypeConfig = uploadConfig[uploadType];\n\n const { allowedExtensions, sizeLimit } = typeConfig;\n\n if (!validateFileExtension(filename, allowedExtensions)) {\n return {\n isValid: false,\n error: `File extension not allowed for ${uploadType.toLowerCase()} files. Allowed extensions: ${allowedExtensions.join(', ')}`,\n };\n }\n\n if (fileSize !== undefined && fileSize > sizeLimit) {\n const maxSizeMB = Math.round(sizeLimit / (1024 * 1024));\n\n return {\n isValid: false,\n error: `File size exceeds limit for ${uploadType.toLowerCase()} files. Maximum size: ${maxSizeMB}MB`,\n };\n }\n\n if (mimetype) {\n let expectedPrefix = '';\n\n if (uploadType === E_UploadType.IMAGE) {\n expectedPrefix = 'image/';\n }\n else if (uploadType === E_UploadType.VIDEO) {\n expectedPrefix = 'video/';\n }\n else if (uploadType === E_UploadType.AUDIO) {\n expectedPrefix = 'audio/';\n }\n\n if (expectedPrefix && !mimetype.startsWith(expectedPrefix)) {\n // Advisory MIME validation - log warning but DO NOT reject\n log.warn(`Advisory Mimetype Warning: File '${filename}' (type: ${uploadType}) has unexpected mimetype '${mimetype}'. Expected prefix: '${expectedPrefix}'`);\n }\n }\n\n return { isValid: true };\n}\n\n/**\n * Creates a default upload configuration with predefined settings for different file types.\n * This function provides sensible defaults for image, video, document, and other file types,\n * including allowed extensions and size limits. The configuration can be customized with overrides.\n *\n * @param overrides - Optional configuration overrides to merge with the default configuration.\n * @returns A complete upload configuration object with defaults and any provided overrides.\n * @since 3.13.0\n */\nexport function createUploadConfig(overrides?: Partial<I_UploadConfig>): I_UploadConfig {\n return { ...DEFAULT_UPLOAD_CONFIG, ...overrides };\n}\n\n/**\n * Uploads a file with comprehensive validation and error handling.\n * This function processes file uploads with the following features:\n * - Input validation for path and file parameters\n * - Configuration validation for all upload types\n * - File validation using getAndValidateFile function\n * - Automatic directory creation\n * - Stream-based file writing\n * - Comprehensive error handling with standardized response codes\n *\n * @param options - Upload configuration including file, path, type, and optional validation config.\n * @returns A promise that resolves to a standardized response with success status, message, file path, and response codes.\n */\nexport async function upload(options: I_UploadOptions): Promise<I_Return<string>> {\n const { path, file, config, type, baseDir } = options;\n\n if (!path || typeof path !== 'string') {\n return {\n success: false,\n message: 'Invalid path provided',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n\n // Security: Validate path is within allowed base directory to prevent path traversal.\n // When baseDir is provided, the resolved path must start with it.\n // When baseDir is not provided, reject any path containing \"..\" segments.\n const resolvedPath = nodePath.resolve(path);\n\n if (baseDir) {\n const resolvedBase = nodePath.resolve(baseDir) + nodePath.sep;\n if (!resolvedPath.startsWith(resolvedBase) && resolvedPath !== nodePath.resolve(baseDir)) {\n return {\n success: false,\n message: 'Path traversal detected: path resolves outside the allowed base directory',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n }\n else if (path.includes('..')) {\n return {\n success: false,\n message: 'Path traversal detected: \"..\" segments are not allowed',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n\n if (!file || typeof file !== 'object') {\n return {\n success: false,\n message: 'Invalid file provided',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n\n if (config) {\n const requiredTypes = [E_UploadType.IMAGE, E_UploadType.VIDEO, E_UploadType.DOCUMENT, E_UploadType.OTHER];\n\n for (const requiredType of requiredTypes) {\n if (!config[requiredType] || !Array.isArray(config[requiredType].allowedExtensions) || config[requiredType].allowedExtensions.length === 0) {\n return {\n success: false,\n message: `Invalid config for ${requiredType.toLowerCase()} files`,\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n if (typeof config[requiredType].sizeLimit !== 'number' || config[requiredType].sizeLimit <= 0) {\n return {\n success: false,\n message: `Invalid size limit for ${requiredType.toLowerCase()} files`,\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n }\n }\n\n try {\n const fileData = await getAndValidateFile(type, await file, config);\n\n if (!fileData.success) {\n return fileData;\n }\n\n const { createReadStream } = fileData.result;\n\n const dir = dirname(path);\n\n if (!pathExistsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const readStream = createReadStream();\n const out = createWriteStream(path);\n readStream.pipe(out);\n\n await new Promise<void>((resolve, reject) => {\n out.on('finish', () => resolve());\n out.on('error', (err) => {\n // Destroy the read stream to release resources if write fails\n if ('destroy' in readStream && typeof readStream.destroy === 'function') {\n readStream.destroy();\n }\n reject(err);\n });\n readStream.on('error', (err) => {\n out.destroy();\n reject(err);\n });\n });\n\n return {\n success: true,\n result: path,\n message: 'File uploaded successfully',\n code: RESPONSE_STATUS.OK.CODE,\n };\n }\n catch (error: unknown) {\n return {\n success: false,\n message: error instanceof Error ? error.message : 'File upload failed',\n code: RESPONSE_STATUS.INTERNAL_SERVER_ERROR.CODE,\n };\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"storage.hook.js","names":[],"sources":["../../../src/react/storage/storage.hook.tsx"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\n\nimport type { I_Serializer } from '#util/serializer/index.js';\n\nimport { serializer as defaultSerializer } from '#util/serializer/index.js';\n\nimport { catchError } from '../log/index.js';\nimport { storage } from './storage.util.js';\n\n/**\n * React hook that provides persistent storage functionality with automatic serialization.\n * This hook manages state that persists across browser sessions using localStorage,\n * with automatic serialization/deserialization of complex data types. It provides\n * a React-friendly interface for storage operations with proper error handling.\n *\n * Features:\n * - Automatic data serialization and deserialization\n * - Persistent storage across browser sessions\n * - Initial value handling and fallback\n * - Error handling with graceful degradation\n * - Automatic storage synchronization\n * - Support for complex data types via custom serializers\n *\n * @param key - The unique storage key for the data.\n * @param initialValue - Optional initial value to use if no stored value exists.\n * @param serializer - Optional custom serializer for complex data types (defaults to JSON serializer).\n * @returns An object containing the current value, set function, and remove function.\n */\nexport function useStorage<T>(\n key: string,\n initialValue?: T,\n serializer: I_Serializer<T> = defaultSerializer as I_Serializer<T>,\n) {\n const [value, setValue] = useState<T | undefined>(initialValue);\n const [isLoaded, setIsLoaded] = useState(false);\n\n useEffect(() => {\n let isMounted = true;\n\n const loadValue = async () => {\n try {\n const valueFound = await storage.get<string>(key);\n\n if (isMounted) {\n if (valueFound !== null) {\n const parsedValue = serializer.deserialize(valueFound);\n setValue(parsedValue);\n }\n else if (initialValue !== undefined) {\n const serialized = serializer.serialize(initialValue);\n await storage.set(key, serialized);\n setValue(initialValue);\n }\n else {\n setValue(undefined);\n }\n }\n }\n catch (error) {\n catchError(error);\n\n if (isMounted) {\n setValue(initialValue);\n }\n }\n finally {\n if (isMounted)\n setIsLoaded(true);\n }\n };\n\n loadValue();\n\n return () => {\n isMounted = false;\n setIsLoaded(false);\n };\n }, [key, initialValue, serializer]);\n\n useEffect(() => {\n if (!isLoaded)\n return;\n\n const saveValue = async () => {\n try {\n if (value !== undefined) {\n const serialized = serializer.serialize(value);\n await storage.set(key, serialized);\n }\n }\n catch (error) {\n catchError(error);\n }\n };\n\n saveValue();\n }, [value, key, serializer, isLoaded]);\n\n const set = useCallback(\n (newValue: T | ((val: T | undefined) => T)) => {\n setValue((prev) => {\n if (typeof newValue === 'function') {\n return (newValue as (val: T | undefined) => T)(prev);\n }\n return newValue;\n });\n },\n [],\n );\n\n const remove = useCallback(async () => {\n try {\n await storage.remove(key);\n setValue(undefined);\n }\n catch (error) {\n catchError(error);\n }\n }, [key]);\n\n return { value, set, remove };\n}\n"],"mappings":";;;;;AA4BA,SAAgB,EACZ,GACA,GACA,IAA8B,GAChC;CACE,IAAM,CAAC,GAAO,KAAY,EAAwB,EAAa,EACzD,CAAC,GAAU,KAAe,EAAS,GAAM;AAsF/C,QApFA,QAAgB;EACZ,IAAI,IAAY;AAoChB,UAlCkB,YAAY;AAC1B,OAAI;IACA,IAAM,IAAa,MAAM,EAAQ,IAAY,EAAI;AAEjD,QAAI,EACA,KAAI,MAAe,KAEf,GADoB,EAAW,YAAY,EAAW,CACjC;aAEhB,MAAiB,KAAA,GAAW;KACjC,IAAM,IAAa,EAAW,UAAU,EAAa;AAErD,KADA,MAAM,EAAQ,IAAI,GAAK,EAAW,EAClC,EAAS,EAAa;UAGtB,GAAS,KAAA,EAAU;YAIxB,GAAO;AAGV,IAFA,EAAW,EAAM,EAEb,KACA,EAAS,EAAa;aAGtB;AACJ,IAAI,KACA,EAAY,GAAK;;MAIlB,QAEE;AAET,GADA,IAAY,IACZ,EAAY,GAAM;;IAEvB;EAAC;EAAK;EAAc;EAAW,CAAC,EAEnC,QAAgB;AACP,QAGa,YAAY;AAC1B,OAAI;AACA,QAAI,MAAU,KAAA,GAAW;KACrB,IAAM,IAAa,EAAW,UAAU,EAAM;AAC9C,WAAM,EAAQ,IAAI,GAAK,EAAW;;YAGnC,GAAO;AACV,MAAW,EAAM;;MAId;IACZ;EAAC;EAAO;EAAK;EAAY;EAAS,CAAC,EAwB/B;EAAE;EAAO,KAtBJ,GACP,MAA8C;AAC3C,MAAU,MACF,OAAO,KAAa,aACZ,EAAuC,EAAK,GAEjD,EACT;KAEN,EAAE,CACL;EAYoB,QAVN,EAAY,YAAY;AACnC,OAAI;AAEA,IADA,MAAM,EAAQ,OAAO,EAAI,EACzB,EAAS,KAAA,EAAU;YAEhB,GAAO;AACV,MAAW,EAAM;;KAEtB,CAAC,EAAI,CAAC;EAEoB"}
1
+ {"version":3,"file":"storage.hook.js","names":[],"sources":["../../../src/react/storage/storage.hook.tsx"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\n\nimport type { I_Serializer } from '#util/serializer/index.js';\n\nimport { serializer as defaultSerializer } from '#util/serializer/index.js';\n\nimport { catchError } from '../log/index.js';\nimport { storage } from './storage.util.js';\n\n/**\n * React hook that provides persistent storage functionality with automatic serialization.\n * This hook manages state that persists across browser sessions using localStorage,\n * with automatic serialization/deserialization of complex data types. It provides\n * a React-friendly interface for storage operations with proper error handling.\n *\n * Features:\n * - Automatic data serialization and deserialization\n * - Persistent storage across browser sessions\n * - Initial value handling and fallback\n * - Error handling with graceful degradation\n * - Automatic storage synchronization\n * - Support for complex data types via custom serializers\n *\n * @param key - The unique storage key for the data.\n * @param initialValue - Optional initial value to use if no stored value exists.\n * @param serializer - Optional custom serializer for complex data types (defaults to JSON serializer).\n * @returns An object containing the current value, set function, and remove function.\n */\nexport function useStorage<T>(\n key: string,\n initialValue?: T,\n serializer: I_Serializer<T> = defaultSerializer as I_Serializer<T>,\n) {\n const [value, setValue] = useState<T | undefined>(initialValue);\n const [isLoaded, setIsLoaded] = useState(false);\n\n useEffect(() => {\n let isMounted = true;\n\n const loadValue = async () => {\n try {\n const valueFound = await storage.get<string>(key);\n\n if (isMounted) {\n if (valueFound !== null) {\n const parsedValue = serializer.deserialize(valueFound);\n setValue(parsedValue);\n }\n else if (initialValue !== undefined) {\n const serialized = serializer.serialize(initialValue);\n await storage.set(key, serialized);\n setValue(initialValue);\n }\n else {\n setValue(undefined);\n }\n }\n }\n catch (error: unknown) {\n catchError(error);\n\n if (isMounted) {\n setValue(initialValue);\n }\n }\n finally {\n if (isMounted)\n setIsLoaded(true);\n }\n };\n\n loadValue();\n\n return () => {\n isMounted = false;\n setIsLoaded(false);\n };\n }, [key, initialValue, serializer]);\n\n useEffect(() => {\n if (!isLoaded)\n return;\n\n const saveValue = async () => {\n try {\n if (value !== undefined) {\n const serialized = serializer.serialize(value);\n await storage.set(key, serialized);\n }\n }\n catch (error: unknown) {\n catchError(error);\n }\n };\n\n saveValue();\n }, [value, key, serializer, isLoaded]);\n\n const set = useCallback(\n (newValue: T | ((val: T | undefined) => T)) => {\n setValue((prev) => {\n if (typeof newValue === 'function') {\n return (newValue as (val: T | undefined) => T)(prev);\n }\n return newValue;\n });\n },\n [],\n );\n\n const remove = useCallback(async () => {\n try {\n await storage.remove(key);\n setValue(undefined);\n }\n catch (error: unknown) {\n catchError(error);\n }\n }, [key]);\n\n return { value, set, remove };\n}\n"],"mappings":";;;;;AA4BA,SAAgB,EACZ,GACA,GACA,IAA8B,GAChC;CACE,IAAM,CAAC,GAAO,KAAY,EAAwB,EAAa,EACzD,CAAC,GAAU,KAAe,EAAS,GAAM;AAsF/C,QApFA,QAAgB;EACZ,IAAI,IAAY;AAoChB,UAlCkB,YAAY;AAC1B,OAAI;IACA,IAAM,IAAa,MAAM,EAAQ,IAAY,EAAI;AAEjD,QAAI,EACA,KAAI,MAAe,KAEf,GADoB,EAAW,YAAY,EAAW,CACjC;aAEhB,MAAiB,KAAA,GAAW;KACjC,IAAM,IAAa,EAAW,UAAU,EAAa;AAErD,KADA,MAAM,EAAQ,IAAI,GAAK,EAAW,EAClC,EAAS,EAAa;UAGtB,GAAS,KAAA,EAAU;YAIxB,GAAgB;AAGnB,IAFA,EAAW,EAAM,EAEb,KACA,EAAS,EAAa;aAGtB;AACJ,IAAI,KACA,EAAY,GAAK;;MAIlB,QAEE;AAET,GADA,IAAY,IACZ,EAAY,GAAM;;IAEvB;EAAC;EAAK;EAAc;EAAW,CAAC,EAEnC,QAAgB;AACP,QAGa,YAAY;AAC1B,OAAI;AACA,QAAI,MAAU,KAAA,GAAW;KACrB,IAAM,IAAa,EAAW,UAAU,EAAM;AAC9C,WAAM,EAAQ,IAAI,GAAK,EAAW;;YAGnC,GAAgB;AACnB,MAAW,EAAM;;MAId;IACZ;EAAC;EAAO;EAAK;EAAY;EAAS,CAAC,EAwB/B;EAAE;EAAO,KAtBJ,GACP,MAA8C;AAC3C,MAAU,MACF,OAAO,KAAa,aACZ,EAAuC,EAAK,GAEjD,EACT;KAEN,EAAE,CACL;EAYoB,QAVN,EAAY,YAAY;AACnC,OAAI;AAEA,IADA,MAAM,EAAQ,OAAO,EAAI,EACzB,EAAS,KAAA,EAAU;YAEhB,GAAgB;AACnB,MAAW,EAAM;;KAEtB,CAAC,EAAI,CAAC;EAEoB"}
@@ -1 +1 @@
1
- {"version":3,"file":"storage.util.js","names":[],"sources":["../../../src/react/storage/storage.util.ts"],"sourcesContent":["import { createTtlEnvelope, isExpiredEnvelope, isTtlEnvelope } from '../../util/storage/storage-envelope.js';\nimport { catchError } from '../log/index.js';\n\n/**\n * Browser storage utility object using native localStorage.\n * This object provides a unified interface for browser storage operations\n * with comprehensive error handling and type safety.\n * Values are stored as JSON strings for consistent serialization.\n */\nexport const storage = {\n /**\n * Retrieves a value from browser storage by key.\n * This method fetches data that was previously stored using the set method.\n * Returns null if the key doesn't exist or if an error occurs during retrieval.\n *\n * @param key - The unique identifier for the stored value.\n * @returns A promise that resolves to the stored value or null if not found.\n */\n async get<T = unknown>(key: string): Promise<T | null> {\n try {\n const raw = localStorage.getItem(key);\n\n if (raw === null) {\n return null;\n }\n\n const obj = JSON.parse(raw);\n\n if (isTtlEnvelope<T>(obj)) {\n if (isExpiredEnvelope(obj)) {\n localStorage.removeItem(key);\n\n return null;\n }\n\n return obj.value;\n }\n\n return obj as T;\n }\n catch (error) {\n return catchError(error, { returnValue: null });\n }\n },\n /**\n * Stores a value in browser storage with a unique key.\n * This method saves data that can be retrieved later using the get method.\n * The data is automatically serialized to JSON.\n *\n * @param key - The unique identifier for the value to store.\n * @param value - The data to store (will be automatically serialized to JSON).\n * @param options - Optional settings.\n * @param options.ttlMs - The time-to-live in milliseconds.\n * @returns A promise that resolves when the storage operation is complete.\n */\n async set<T = unknown>(key: string, value: T, options?: { ttlMs?: number }): Promise<void> {\n try {\n let payloadToStore: unknown = value;\n\n if (options?.ttlMs) {\n payloadToStore = createTtlEnvelope(value, options.ttlMs);\n }\n\n localStorage.setItem(key, JSON.stringify(payloadToStore));\n }\n catch (error) {\n catchError(error);\n }\n },\n /**\n * Removes a value from browser storage by key.\n * This method permanently deletes the stored data associated with the specified key.\n * If the key doesn't exist, the operation completes successfully without error.\n *\n * @param key - The unique identifier of the value to remove.\n * @returns A promise that resolves when the removal operation is complete.\n */\n async remove(key: string): Promise<void> {\n try {\n localStorage.removeItem(key);\n }\n catch (error) {\n catchError(error);\n }\n },\n /**\n * Retrieves all storage keys.\n * This method returns an array of all keys that currently have stored values.\n * Returns an empty array if no keys exist or if an error occurs during retrieval.\n *\n * @returns A promise that resolves to an array of storage keys.\n */\n async keys(): Promise<string[]> {\n try {\n const keys: string[] = [];\n\n for (let i = 0; i < localStorage.length; i++) {\n const key = localStorage.key(i);\n\n if (key !== null) {\n keys.push(key);\n }\n }\n\n return keys;\n }\n catch (error) {\n return catchError(error, { returnValue: [] });\n }\n },\n /**\n * Checks if a key exists in browser storage.\n * This method efficiently checks for key existence without deserializing the value.\n * It also respects TTL — returns false if the key exists but has expired.\n *\n * @param key - The unique identifier to check.\n * @returns A promise that resolves to true if the key exists and has not expired.\n */\n async has(key: string): Promise<boolean> {\n try {\n const raw = localStorage.getItem(key);\n\n if (raw === null) {\n return false;\n }\n\n const obj = JSON.parse(raw);\n\n if (isTtlEnvelope<unknown>(obj)) {\n if (isExpiredEnvelope(obj)) {\n localStorage.removeItem(key);\n\n return false;\n }\n }\n\n return true;\n }\n catch (error) {\n return catchError(error, { returnValue: false });\n }\n },\n /**\n * Clears all values from browser storage.\n * This method permanently removes all stored data.\n *\n * @returns A promise that resolves when the clear operation is complete.\n */\n async clear(): Promise<void> {\n try {\n localStorage.clear();\n }\n catch (error) {\n catchError(error);\n }\n },\n /**\n * Retrieves a value from browser storage, or creates and stores it if it doesn't exist.\n * This method combines check, creation, and storage into a single convenient operation.\n *\n * @param key - The unique identifier for the value.\n * @param factory - A function (sync or async) that generates the value if it's missing or expired.\n * @param options - Optional storage options.\n * @param options.ttlMs - The time-to-live in milliseconds.\n * @returns A promise that resolves to the retrieved or newly created value.\n */\n async getOrSet<T = unknown>(key: string, factory: () => T | Promise<T>, options?: { ttlMs?: number }): Promise<T> {\n let value = await this.get<T>(key);\n\n if (value === null) {\n value = await factory();\n await this.set(key, value, options);\n }\n\n return value;\n },\n};\n"],"mappings":";;;AASA,IAAa,IAAU;CASnB,MAAM,IAAiB,GAAgC;AACnD,MAAI;GACA,IAAM,IAAM,aAAa,QAAQ,EAAI;AAErC,OAAI,MAAQ,KACR,QAAO;GAGX,IAAM,IAAM,KAAK,MAAM,EAAI;AAY3B,UAVI,EAAiB,EAAI,GACjB,EAAkB,EAAI,IACtB,aAAa,WAAW,EAAI,EAErB,QAGJ,EAAI,QAGR;WAEJ,GAAO;AACV,UAAO,EAAW,GAAO,EAAE,aAAa,MAAM,CAAC;;;CAcvD,MAAM,IAAiB,GAAa,GAAU,GAA6C;AACvF,MAAI;GACA,IAAI,IAA0B;AAM9B,GAJI,GAAS,UACT,IAAiB,EAAkB,GAAO,EAAQ,MAAM,GAG5D,aAAa,QAAQ,GAAK,KAAK,UAAU,EAAe,CAAC;WAEtD,GAAO;AACV,KAAW,EAAM;;;CAWzB,MAAM,OAAO,GAA4B;AACrC,MAAI;AACA,gBAAa,WAAW,EAAI;WAEzB,GAAO;AACV,KAAW,EAAM;;;CAUzB,MAAM,OAA0B;AAC5B,MAAI;GACA,IAAM,IAAiB,EAAE;AAEzB,QAAK,IAAI,IAAI,GAAG,IAAI,aAAa,QAAQ,KAAK;IAC1C,IAAM,IAAM,aAAa,IAAI,EAAE;AAE/B,IAAI,MAAQ,QACR,EAAK,KAAK,EAAI;;AAItB,UAAO;WAEJ,GAAO;AACV,UAAO,EAAW,GAAO,EAAE,aAAa,EAAE,EAAE,CAAC;;;CAWrD,MAAM,IAAI,GAA+B;AACrC,MAAI;GACA,IAAM,IAAM,aAAa,QAAQ,EAAI;AAErC,OAAI,MAAQ,KACR,QAAO;GAGX,IAAM,IAAM,KAAK,MAAM,EAAI;AAU3B,UARI,EAAuB,EAAI,IACvB,EAAkB,EAAI,IACtB,aAAa,WAAW,EAAI,EAErB,MAIR;WAEJ,GAAO;AACV,UAAO,EAAW,GAAO,EAAE,aAAa,IAAO,CAAC;;;CASxD,MAAM,QAAuB;AACzB,MAAI;AACA,gBAAa,OAAO;WAEjB,GAAO;AACV,KAAW,EAAM;;;CAazB,MAAM,SAAsB,GAAa,GAA+B,GAA0C;EAC9G,IAAI,IAAQ,MAAM,KAAK,IAAO,EAAI;AAOlC,SALI,MAAU,SACV,IAAQ,MAAM,GAAS,EACvB,MAAM,KAAK,IAAI,GAAK,GAAO,EAAQ,GAGhC;;CAEd"}
1
+ {"version":3,"file":"storage.util.js","names":[],"sources":["../../../src/react/storage/storage.util.ts"],"sourcesContent":["import { createTtlEnvelope, isExpiredEnvelope, isTtlEnvelope } from '../../util/storage/storage-envelope.js';\nimport { catchError } from '../log/index.js';\n\n/**\n * Browser storage utility object using native localStorage.\n * This object provides a unified interface for browser storage operations\n * with comprehensive error handling and type safety.\n * Values are stored as JSON strings for consistent serialization.\n */\nexport const storage = {\n /**\n * Retrieves a value from browser storage by key.\n * This method fetches data that was previously stored using the set method.\n * Returns null if the key doesn't exist or if an error occurs during retrieval.\n *\n * @param key - The unique identifier for the stored value.\n * @returns A promise that resolves to the stored value or null if not found.\n */\n async get<T = unknown>(key: string): Promise<T | null> {\n try {\n const raw = localStorage.getItem(key);\n\n if (raw === null) {\n return null;\n }\n\n const obj = JSON.parse(raw);\n\n if (isTtlEnvelope<T>(obj)) {\n if (isExpiredEnvelope(obj)) {\n localStorage.removeItem(key);\n\n return null;\n }\n\n return obj.value;\n }\n\n return obj as T;\n }\n catch (error: unknown) {\n return catchError(error, { returnValue: null });\n }\n },\n /**\n * Stores a value in browser storage with a unique key.\n * This method saves data that can be retrieved later using the get method.\n * The data is automatically serialized to JSON.\n *\n * @param key - The unique identifier for the value to store.\n * @param value - The data to store (will be automatically serialized to JSON).\n * @param options - Optional settings.\n * @param options.ttlMs - The time-to-live in milliseconds.\n * @returns A promise that resolves when the storage operation is complete.\n */\n async set<T = unknown>(key: string, value: T, options?: { ttlMs?: number }): Promise<void> {\n try {\n let payloadToStore: unknown = value;\n\n if (options?.ttlMs) {\n payloadToStore = createTtlEnvelope(value, options.ttlMs);\n }\n\n localStorage.setItem(key, JSON.stringify(payloadToStore));\n }\n catch (error: unknown) {\n catchError(error);\n }\n },\n /**\n * Removes a value from browser storage by key.\n * This method permanently deletes the stored data associated with the specified key.\n * If the key doesn't exist, the operation completes successfully without error.\n *\n * @param key - The unique identifier of the value to remove.\n * @returns A promise that resolves when the removal operation is complete.\n */\n async remove(key: string): Promise<void> {\n try {\n localStorage.removeItem(key);\n }\n catch (error: unknown) {\n catchError(error);\n }\n },\n /**\n * Retrieves all storage keys.\n * This method returns an array of all keys that currently have stored values.\n * Returns an empty array if no keys exist or if an error occurs during retrieval.\n *\n * @returns A promise that resolves to an array of storage keys.\n */\n async keys(): Promise<string[]> {\n try {\n const keys: string[] = [];\n\n for (let i = 0; i < localStorage.length; i++) {\n const key = localStorage.key(i);\n\n if (key !== null) {\n keys.push(key);\n }\n }\n\n return keys;\n }\n catch (error: unknown) {\n return catchError(error, { returnValue: [] });\n }\n },\n /**\n * Checks if a key exists in browser storage.\n * This method efficiently checks for key existence without deserializing the value.\n * It also respects TTL — returns false if the key exists but has expired.\n *\n * @param key - The unique identifier to check.\n * @returns A promise that resolves to true if the key exists and has not expired.\n */\n async has(key: string): Promise<boolean> {\n try {\n const raw = localStorage.getItem(key);\n\n if (raw === null) {\n return false;\n }\n\n const obj = JSON.parse(raw);\n\n if (isTtlEnvelope<unknown>(obj)) {\n if (isExpiredEnvelope(obj)) {\n localStorage.removeItem(key);\n\n return false;\n }\n }\n\n return true;\n }\n catch (error: unknown) {\n return catchError(error, { returnValue: false });\n }\n },\n /**\n * Clears all values from browser storage.\n * This method permanently removes all stored data.\n *\n * @returns A promise that resolves when the clear operation is complete.\n */\n async clear(): Promise<void> {\n try {\n localStorage.clear();\n }\n catch (error: unknown) {\n catchError(error);\n }\n },\n /**\n * Retrieves a value from browser storage, or creates and stores it if it doesn't exist.\n * This method combines check, creation, and storage into a single convenient operation.\n *\n * @param key - The unique identifier for the value.\n * @param factory - A function (sync or async) that generates the value if it's missing or expired.\n * @param options - Optional storage options.\n * @param options.ttlMs - The time-to-live in milliseconds.\n * @returns A promise that resolves to the retrieved or newly created value.\n */\n async getOrSet<T = unknown>(key: string, factory: () => T | Promise<T>, options?: { ttlMs?: number }): Promise<T> {\n let value = await this.get<T>(key);\n\n if (value === null) {\n value = await factory();\n await this.set(key, value, options);\n }\n\n return value;\n },\n};\n"],"mappings":";;;AASA,IAAa,IAAU;CASnB,MAAM,IAAiB,GAAgC;AACnD,MAAI;GACA,IAAM,IAAM,aAAa,QAAQ,EAAI;AAErC,OAAI,MAAQ,KACR,QAAO;GAGX,IAAM,IAAM,KAAK,MAAM,EAAI;AAY3B,UAVI,EAAiB,EAAI,GACjB,EAAkB,EAAI,IACtB,aAAa,WAAW,EAAI,EAErB,QAGJ,EAAI,QAGR;WAEJ,GAAgB;AACnB,UAAO,EAAW,GAAO,EAAE,aAAa,MAAM,CAAC;;;CAcvD,MAAM,IAAiB,GAAa,GAAU,GAA6C;AACvF,MAAI;GACA,IAAI,IAA0B;AAM9B,GAJI,GAAS,UACT,IAAiB,EAAkB,GAAO,EAAQ,MAAM,GAG5D,aAAa,QAAQ,GAAK,KAAK,UAAU,EAAe,CAAC;WAEtD,GAAgB;AACnB,KAAW,EAAM;;;CAWzB,MAAM,OAAO,GAA4B;AACrC,MAAI;AACA,gBAAa,WAAW,EAAI;WAEzB,GAAgB;AACnB,KAAW,EAAM;;;CAUzB,MAAM,OAA0B;AAC5B,MAAI;GACA,IAAM,IAAiB,EAAE;AAEzB,QAAK,IAAI,IAAI,GAAG,IAAI,aAAa,QAAQ,KAAK;IAC1C,IAAM,IAAM,aAAa,IAAI,EAAE;AAE/B,IAAI,MAAQ,QACR,EAAK,KAAK,EAAI;;AAItB,UAAO;WAEJ,GAAgB;AACnB,UAAO,EAAW,GAAO,EAAE,aAAa,EAAE,EAAE,CAAC;;;CAWrD,MAAM,IAAI,GAA+B;AACrC,MAAI;GACA,IAAM,IAAM,aAAa,QAAQ,EAAI;AAErC,OAAI,MAAQ,KACR,QAAO;GAGX,IAAM,IAAM,KAAK,MAAM,EAAI;AAU3B,UARI,EAAuB,EAAI,IACvB,EAAkB,EAAI,IACtB,aAAa,WAAW,EAAI,EAErB,MAIR;WAEJ,GAAgB;AACnB,UAAO,EAAW,GAAO,EAAE,aAAa,IAAO,CAAC;;;CASxD,MAAM,QAAuB;AACzB,MAAI;AACA,gBAAa,OAAO;WAEjB,GAAgB;AACnB,KAAW,EAAM;;;CAazB,MAAM,SAAsB,GAAa,GAA+B,GAA0C;EAC9G,IAAI,IAAQ,MAAM,KAAK,IAAO,EAAI;AAOlC,SALI,MAAU,SACV,IAAQ,MAAM,GAAS,EACvB,MAAM,KAAK,IAAI,GAAK,GAAO,EAAQ,GAGhC;;CAEd"}
@@ -34,39 +34,31 @@ function p(e, t) {
34
34
  }
35
35
  function m(e = 8) {
36
36
  if (!Number.isSafeInteger(e) || e < 0) throw RangeError("length must be a non-negative safe integer");
37
- let t = "abcdefghijklmnopqrstuvwxyz", n = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", r = "0123456789", i = "!@#$%^&*()_+[]{}|;:,.<>?", a = p(e, t + n + r + i);
37
+ let t = "abcdefghijklmnopqrstuvwxyz", n = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", r = "0123456789", i = "!@#$%^&*()_+[]{}|;:,.<>?", a = t + n + r + i, u = p(e, a);
38
38
  if (e >= 4) {
39
- let u = o.test(a), d = s.test(a), f = c.test(a), m = l.test(a);
40
- if (u && d && f && m) return a;
41
- let h = [...a], g = [
42
- {
43
- test: u,
44
- pool: t
45
- },
46
- {
47
- test: d,
48
- pool: n
49
- },
50
- {
51
- test: f,
52
- pool: r
53
- },
54
- {
55
- test: m,
56
- pool: i
57
- }
58
- ], _ = Array.from({ length: e }, (e, t) => t);
59
- for (let e = _.length - 1; e > 0; e--) {
60
- let t = new Uint32Array(1);
61
- crypto.getRandomValues(t);
62
- let n = t[0] % (e + 1);
63
- [_[e], _[n]] = [_[n], _[e]];
39
+ let d = o.test(u), f = s.test(u), m = c.test(u), h = l.test(u);
40
+ if (d && f && m && h) return u;
41
+ let g = [
42
+ p(1, t),
43
+ p(1, n),
44
+ p(1, r),
45
+ p(1, i)
46
+ ];
47
+ if (e > 4) {
48
+ let t = p(e - 4, a);
49
+ for (let e of t) g.push(e);
64
50
  }
65
- let v = 0;
66
- for (let { test: e, pool: t } of g) e || (h[_[v]] = p(1, t), v++);
67
- return h.join("");
51
+ for (let e = g.length - 1; e > 0; e--) {
52
+ let t = e + 1, n = Math.floor(4294967296 / t) * t, r = new Uint32Array(1), i;
53
+ do
54
+ crypto.getRandomValues(r), i = r[0];
55
+ while (i >= n);
56
+ let a = i % t;
57
+ [g[e], g[a]] = [g[a], g[e]];
58
+ }
59
+ return g.join("");
68
60
  }
69
- return a;
61
+ return u;
70
62
  }
71
63
  function h(e = 8, t = "abcdefghijklmnopqrstuvwxyz0123456789") {
72
64
  if (!Number.isSafeInteger(e) || e < 0) throw RangeError("length must be a non-negative safe integer");