     1	/**
     2	 * @file Contains all the built-in type and validator handlers for the schema library.
     3	 */
     4	
     5	import * as flatted from 'flatted'
     6	
     7	/**
     8	 * @typedef {import('./Schema.js').ValidationContext} ValidationContext
     9	 */
    10	
    11	/**
    12	 * The CorePlugin provides the default set of types and validators.
    13	 */
    14	const CorePlugin = {
    15	  /**
    16	   * Installs the core types and validators.
    17	   * @param {{addType: Function, addValidator: Function}} api - An object containing registration functions.
    18	   */
    19	  install (api) {
    20	    const { addType, addValidator } = api // Destructure the API functions
    21	
    22	    // --- Type Handlers ---
    23	
    24	    addType('none', context => context.value)
    25	
    26	    addType('file', context => {
    27	      const val = context.value
    28	      if (val === undefined || val === null) context.throwTypeError()
    29	
    30	      // Only attempt to convert primitives. Fail on complex objects/arrays.
    31	      // A file is expected to be just a file handle
    32	      const valType = typeof val
    33	      if (valType === 'string' || valType === 'number' || valType === 'boolean') {
    34	        const s = val.toString()
    35	        return context.definition.noTrim ? s : s.trim()
    36	      }
    37	
    38	      // If it's not a primitive that can be safely converted, it's a type error.
    39	      context.throwTypeError()
    40	    })
    41	
    42	    addType('string', context => {
    43	      const val = context.value
    44	      if (val === undefined || val === null) return ''
    45	
    46	      // Only attempt to convert primitives. Fail on complex objects/arrays.
    47	      const valType = typeof val
    48	      if (valType === 'string' || valType === 'number' || valType === 'boolean') {
    49	        const s = val.toString()
    50	        return context.definition.noTrim ? s : s.trim()
    51	      }
    52	
    53	      // If it's not a primitive that can be safely converted, it's a type error.
    54	      context.throwTypeError()
    55	    })
    56	
    57	    addType('blob', context => context.value)
    58	    addType('number', context => {
    59	      if (context.value === undefined || context.value === null || context.value === '') return 0
    60	      const r = Number(context.value)
    61	      if (isNaN(r)) context.throwTypeError()
    62	      return r
    63	    })
    64	    addType('timestamp', context => {
    65	      const r = Number(context.value)
    66	      if (isNaN(r)) context.throwTypeError()
    67	      if (!r && context.computedOptions.nullable) return null
    68	      return r
    69	    })
    70	    addType('dateTime', context => {
    71	      if (!context.value || context.value === '') return null
    72	
    73	      // If already a Date object, return it
    74	      if (context.value instanceof Date) {
    75	        return isNaN(context.value.getTime()) ? null : context.value
    76	      }
    77	
    78	      // Handle string values
    79	      if (typeof context.value === 'string') {
    80	        // Detect MySQL datetime format: 'YYYY-MM-DD HH:MM:SS'
    81	        const isMySQLFormat = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/.test(context.value) &&
    82	                             !context.value.includes('T') &&
    83	                             !context.value.includes('Z')
    84	
    85	        if (isMySQLFormat) {
    86	          // Convert to ISO format and force UTC interpretation
    87	          const d = new Date(context.value.replace(' ', 'T') + 'Z')
    88	          if (isNaN(d.getTime())) {
    89	            context.throwTypeError()
    90	          }
    91	          return d
    92	        }
    93	      }
    94	
    95	      // Try to parse the value normally
    96	      const d = new Date(context.value)
    97	      if (isNaN(d.getTime())) {
    98	        return null
    99	      }
   100	
   101	      // Return the Date object directly - let Knex handle the database formatting
   102	      return d
   103	    })
   104	    addType('date', context => {
   105	      if (!context.value || context.value === '') return null
   106	
   107	      // Parse the input value to a Date object
   108	      let d
   109	      if (context.value instanceof Date) {
   110	        d = context.value
   111	      } else {
   112	        let dateStr = String(context.value)
   113	
   114	        // If it's just a date (YYYY-MM-DD), add time at UTC midnight
   115	        if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
   116	          dateStr += 'T00:00:00Z'
   117	        }
   118	
   119	        d = new Date(dateStr)
   120	      }
   121	
   122	      if (isNaN(d.getTime())) {
   123	        context.throwTypeError()
   124	      }
   125	
   126	      // Normalize to midnight UTC
   127	      const normalized = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()))
   128	      return normalized
   129	    })
   130	    addType('time', context => {
   131	      if (!context.value || context.value === '') return null
   132	
   133	      // Try to parse as time string (HH:MM:SS or HH:MM)
   134	      if (typeof context.value === 'string') {
   135	        // Match HH:MM:SS or HH:MM format
   136	        const timeMatch = context.value.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/)
   137	        if (timeMatch) {
   138	          const hours = parseInt(timeMatch[1], 10)
   139	          const minutes = parseInt(timeMatch[2], 10)
   140	          const seconds = timeMatch[3] ? parseInt(timeMatch[3], 10) : 0
   141	
   142	          // Validate ranges
   143	          if (hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59 && seconds >= 0 && seconds <= 59) {
   144	            // Return normalized HH:MM:SS format as string
   145	            // Note: We return string because most databases don't have a true time-only type
   146	            return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
   147	          }
   148	        }
   149	      }
   150	
   151	      // Try to extract time from a Date object or datetime string
   152	      try {
   153	        const d = new Date(context.value)
   154	        if (!isNaN(d.getTime())) {
   155	          // Extract time portion in HH:MM:SS format
   156	          return d.toISOString().slice(11, 19)
   157	        }
   158	      } catch (e) {
   159	        // Invalid date
   160	      }
   161	
   162	      context.throwTypeError()
   163	    })
   164	    addType('array', context => {
   165	      if (context.definition.type === 'array' && !Array.isArray(context.value)) {
   166	        return [context.value]
   167	      }
   168	      return context.value
   169	    })
   170	    addType('object', context => context.value)
   171	    addType('serialize', context => {
   172	      try {
   173	        // First try regular JSON.stringify for non-circular objects
   174	        return JSON.stringify(context.value)
   175	      } catch (e) {
   176	        // If that fails (likely circular reference), use flatted
   177	        try {
   178	          return flatted.stringify(context.value)
   179	        } catch (e2) {
   180	          context.throwTypeError()
   181	        }
   182	      }
   183	    })
   184	    addType('boolean', context => {
   185	      if (typeof context.value === 'string') {
   186	        const falseVal = context.definition.stringFalseWhen || 'false'
   187	        const trueVal = context.definition.stringTrueWhen || 'true'
   188	        const lowerValue = context.value.toLowerCase()
   189	        if (lowerValue === falseVal) return false
   190	        if ([trueVal, 'on'].includes(lowerValue)) return true
   191	        return false
   192	      }
   193	      return !!context.value
   194	    })
   195	    addType('id', context => {
   196	      const n = parseInt(context.value, 10)
   197	      if (isNaN(n)) context.throwTypeError()
   198	      return n
   199	    })
   200	
   201	    // --- Validator Handlers ---
   202	    addValidator('minLength', context => {
   203	      if (context.value === undefined) return
   204	      if (context.definition.type === 'string' && context.value.toString && context.value.toString().length < context.parameterValue) {
   205	        context.throwParamError('MIN_LENGTH', `Length must be at least ${context.parameterValue} characters.`, { min: context.parameterValue, actual: context.value.toString().length })
   206	      }
   207	    })
   208	
   209	    addValidator('min', context => {
   210	      if (context.value === undefined) return
   211	      if (context.definition.type === 'number' && typeof context.value === 'number' && context.value < context.parameterValue) {
   212	        context.throwParamError('MIN_VALUE', `Value must be at least ${context.parameterValue}.`, { min: context.parameterValue, actual: context.value })
   213	      }
   214	    })
   215	    addValidator('maxLength', context => {
   216	      if (context.value === undefined) return
   217	      if (context.definition.type === 'string' && context.value.toString && context.value.toString().length > context.parameterValue) {
   218	        context.throwParamError('MAX_LENGTH', `Length must be no more than ${context.parameterValue} characters.`, { max: context.parameterValue, actual: context.value.toString().length })
   219	      }
   220	    })
   221	
   222	    addValidator('max', context => {
   223	      if (context.value === undefined) return
   224	      if (context.definition.type === 'number' && typeof context.value === 'number' && context.value > context.parameterValue) {
   225	        context.throwParamError('MAX_VALUE', `Value must be no more than ${context.parameterValue}.`, { max: context.parameterValue, actual: context.value })
   226	      }
   227	    })
   228	    addValidator('validator', async context => {
   229	      if (typeof context.parameterValue !== 'function') {
   230	        throw new Error(`Validator for ${context.fieldName} must be a function.`)
   231	      }
   232	      const r = await context.parameterValue(context.value, context.object, context)
   233	      if (typeof r === 'string') {
   234	        context.throwParamError('CUSTOM_VALIDATOR_FAILED', r)
   235	      }
   236	    })
   237	    addValidator('uppercase', context => {
   238	      if (typeof context.value === 'string') return context.value.toUpperCase()
   239	    })
   240	    addValidator('lowercase', context => {
   241	      if (typeof context.value === 'string') return context.value.toLowerCase()
   242	    })
   243	    addValidator('length', context => {
   244	      if (typeof context.value === 'string') {
   245	        return context.value.substr(0, context.parameterValue)
   246	      } else if (Number.isInteger(Number(context.valueBeforeCast)) && String(context.valueBeforeCast).length > context.parameterValue) {
   247	        context.throwParamError('RANGE_EXCEEDED', 'Numeric value is out of the allowed character range.', { max: context.parameterValue, actual: String(context.valueBeforeCast).length })
   248	      }
   249	    })
   250	    addValidator('notEmpty', context => {
   251	      const bc = context.valueBeforeCast
   252	      const bcs = (bc !== undefined && bc !== null && bc.toString) ? bc.toString() : ''
   253	      if (context.parameterValue && !Array.isArray(context.value) && bc !== undefined && bcs === '') {
   254	        context.throwParamError('NOT_EMPTY', 'Field cannot be empty.')
   255	      }
   256	    })
   257	    addValidator('required', () => {}) // Stays as a no-op, logic is handled in _validateField
   258	
   259	    // Add new validators for database alignment
   260	    addValidator('unsigned', () => {}) // No-op, used for schema metadata
   261	    addValidator('precision', () => {}) // No-op, used for schema metadata
   262	    addValidator('scale', () => {}) // No-op, used for schema metadata
   263	    addValidator('nullable', () => {}) // No-op, handled in Schema.js
   264	    addValidator('nullOnEmpty', () => {}) // No-op, handled in Schema.js
   265	    addValidator('defaultTo', () => {}) // No-op, handled in Schema.js
   266	  }
   267	}
   268	
   269	export default CorePlugin
