     1	/**
     2	 * Normalizes a date value to appropriate format based on field type
     3	 *
     4	 * @param {*} value - The date value from the database
     5	 * @param {string} type - The field type ('date', 'dateTime', or 'time')
     6	 * @returns {Date|string|null} Date object for date/dateTime, string for time, or null
     7	 *
     8	 * @example
     9	 * // Input: Already a Date object
    10	 * normalizeDateValue(new Date('2024-01-15T10:30:00Z'), 'dateTime');
    11	 * // Output: Date object (unchanged)
    12	 *
    13	 * @example
    14	 * // Input: MySQL datetime string without timezone
    15	 * normalizeDateValue('2024-01-15 10:30:00', 'dateTime');
    16	 * // Output: Date object parsed as UTC (2024-01-15T10:30:00Z)
    17	 *
    18	 * @example
    19	 * // Input: Date-only string
    20	 * normalizeDateValue('2024-01-15', 'date');
    21	 * // Output: Date object at UTC midnight (2024-01-15T00:00:00Z)
    22	 *
    23	 * @example
    24	 * // Input: Time field (always returns string)
    25	 * normalizeDateValue('14:30:45', 'time');
    26	 * // Output: "14:30:45" (string)
    27	 *
    28	 * @example
    29	 * // Input: Date object for time field
    30	 * normalizeDateValue(new Date('2024-01-15T14:30:45Z'), 'time');
    31	 * // Output: "14:30:45" (extracts time portion)
    32	 *
    33	 * @description
    34	 * Used by:
    35	 * - normalizeAttributes to process individual field values
    36	 * - Applied to all date/time fields fetched from database
    37	 *
    38	 * Purpose:
    39	 * - Handles database-specific date formats (especially MySQL)
    40	 * - Prevents timezone shifts for date-only fields
    41	 * - Ensures consistent Date objects across databases
    42	 * - Keeps time fields as HH:MM:SS strings
    43	 *
    44	 * Data flow:
    45	 * 1. Checks for null/undefined (returns null)
    46	 * 2. For time fields: ensures HH:MM:SS string format
    47	 * 3. For date/dateTime: converts to Date objects
    48	 * 4. Handles MySQL datetime format (no T separator)
    49	 * 5. Forces UTC interpretation to prevent timezone issues
    50	 */
    51	function normalizeDateValue (value, type) {
    52	  // Handle null/undefined
    53	  if (value === null || value === undefined) {
    54	    return null
    55	  }
    56	
    57	  // Handle time type specially - always return as string
    58	  if (type === 'time') {
    59	    // If it's already a properly formatted time string, return as-is
    60	    if (typeof value === 'string' && /^\d{2}:\d{2}:\d{2}/.test(value)) {
    61	      return value
    62	    }
    63	    // If we got a Date object for a time field, extract the time portion
    64	    if (value instanceof Date) {
    65	      return value.toISOString().slice(11, 19) // Extract HH:MM:SS
    66	    }
    67	    // Try to parse and extract time
    68	    if (typeof value === 'string' || typeof value === 'number') {
    69	      const d = new Date(value)
    70	      if (!isNaN(d.getTime())) {
    71	        return d.toISOString().slice(11, 19)
    72	      }
    73	    }
    74	    return null
    75	  }
    76	
    77	  // For date and dateTime: Already a Date object? Return as-is
    78	  if (value instanceof Date) {
    79	    return value
    80	  }
    81	
    82	  // Handle string values
    83	  if (typeof value === 'string') {
    84	    // Detect MySQL datetime format: 'YYYY-MM-DD HH:MM:SS'
    85	    // These have no T separator and no timezone indicator
    86	    const isMySQLDateTime = type === 'dateTime' &&
    87	                           /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/.test(value) &&
    88	                           !value.includes('T') &&
    89	                           !value.includes('Z') &&
    90	                           !value.includes('+') &&
    91	                           !value.includes('-', 10) // Don't match date separators
    92	
    93	    if (isMySQLDateTime) {
    94	      // Convert to ISO format and force UTC interpretation
    95	      // '2024-01-15 10:30:00' becomes '2024-01-15T10:30:00Z'
    96	      return new Date(value.replace(' ', 'T') + 'Z')
    97	    }
    98	
    99	    // For date-only fields, ensure parsing at UTC midnight
   100	    // This prevents timezone shifts when parsing dates
   101	    if (type === 'date' && /^\d{4}-\d{2}-\d{2}$/.test(value)) {
   102	      return new Date(value + 'T00:00:00Z')
   103	    }
   104	  }
   105	
   106	  // Handle numeric values (Unix timestamps)
   107	  if (typeof value === 'number') {
   108	    return new Date(value)
   109	  }
   110	
   111	  // Try to parse any other format
   112	  const dateObj = new Date(value)
   113	
   114	  // Check if date is valid
   115	  if (isNaN(dateObj.getTime())) {
   116	    console.warn(`Invalid date value for field type '${type}': ${value}`)
   117	    return null // Return null for invalid dates
   118	  }
   119	
   120	  return dateObj
   121	}
   122	
   123	/**
   124	 * Normalizes database values in an attributes object
   125	 *
   126	 * @param {Object} attributes - The attributes object to normalize
   127	 * @param {Object} schemaStructure - The schema structure defining field types
   128	 * @returns {Object} The normalized attributes object
   129	 *
   130	 * @example
   131	 * // Input: Raw database values
   132	 * const attributes = {
   133	 *   id: 1,
   134	 *   is_active: 1,              // Boolean as 0/1
   135	 *   created_at: '2024-01-15 10:30:00',  // MySQL datetime
   136	 *   birth_date: '1990-05-20',           // Date string
   137	 *   shift_time: '09:00:00'              // Time string
   138	 * };
   139	 *
   140	 * const schema = {
   141	 *   is_active: { type: 'boolean' },
   142	 *   created_at: { type: 'dateTime' },
   143	 *   birth_date: { type: 'date' },
   144	 *   shift_time: { type: 'time' }
   145	 * };
   146	 *
   147	 * const normalized = normalizeAttributes(attributes, schema);
   148	 *
   149	 * // Output: Properly typed values
   150	 * // {
   151	 * //   id: 1,
   152	 * //   is_active: true,                    // 1 → true
   153	 * //   created_at: Date('2024-01-15T10:30:00Z'),  // Date object
   154	 * //   birth_date: Date('1990-05-20T00:00:00Z'),  // Date at midnight UTC
   155	 * //   shift_time: '09:00:00'                     // String unchanged
   156	 * // }
   157	 *
   158	 * @example
   159	 * // Input: Boolean edge cases
   160	 * const attributes = {
   161	 *   flag1: 0,      // Number 0
   162	 *   flag2: '1',    // String '1'
   163	 *   flag3: true,   // Already boolean
   164	 *   flag4: null    // Null value
   165	 * };
   166	 *
   167	 * const normalized = normalizeAttributes(attributes, {
   168	 *   flag1: { type: 'boolean' },
   169	 *   flag2: { type: 'boolean' },
   170	 *   flag3: { type: 'boolean' },
   171	 *   flag4: { type: 'boolean' }
   172	 * });
   173	 *
   174	 * // Output:
   175	 * // {
   176	 * //   flag1: false,  // 0 → false
   177	 * //   flag2: true,   // '1' → true
   178	 * //   flag3: true,   // unchanged
   179	 * //   flag4: null    // null preserved
   180	 * // }
   181	 *
   182	 * @description
   183	 * Used by:
   184	 * - normalizeRecordAttributes for each record's attributes
   185	 * - Applied after fetching data from database
   186	 *
   187	 * Purpose:
   188	 * - Handles databases without native boolean support (SQLite, older MySQL)
   189	 * - Normalizes date formats from different databases
   190	 * - Ensures consistent data types in API responses
   191	 * - Preserves null values appropriately
   192	 *
   193	 * Data flow:
   194	 * 1. Creates copy of attributes to avoid mutation
   195	 * 2. Iterates through each field with schema definition
   196	 * 3. Normalizes booleans: 1/0 or '1'/'0' to true/false
   197	 * 4. Normalizes dates using normalizeDateValue
   198	 * 5. Returns new object with normalized values
   199	 */
   200	export function normalizeAttributes (attributes, schemaStructure) {
   201	  if (!attributes || !schemaStructure) {
   202	    return attributes
   203	  }
   204	
   205	  const normalized = { ...attributes }
   206	
   207	  // Iterate through each attribute
   208	  for (const [fieldName, value] of Object.entries(attributes)) {
   209	    const fieldDef = schemaStructure[fieldName]
   210	
   211	    // Skip if no field definition found
   212	    if (!fieldDef) continue
   213	
   214	    // Normalize boolean values
   215	    if (fieldDef.type === 'boolean') {
   216	      if (value === 1 || value === '1') {
   217	        normalized[fieldName] = true
   218	      } else if (value === 0 || value === '0') {
   219	        normalized[fieldName] = false
   220	      }
   221	      // null/undefined remain as-is
   222	    }
   223	
   224	    // Normalize date/dateTime/time values
   225	    if (fieldDef.type === 'date' || fieldDef.type === 'dateTime' || fieldDef.type === 'time') {
   226	      normalized[fieldName] = normalizeDateValue(value, fieldDef.type)
   227	    }
   228	  }
   229	
   230	  return normalized
   231	}
   232	
   233	/**
   234	 * Normalizes all records in a JSON:API response
   235	 *
   236	 * @param {Object} record - The JSON:API response object
   237	 * @param {Object} scopes - All available scopes for schema lookup
   238	 * @returns {Object} The response with normalized values
   239	 *
   240	 * @example
   241	 * // Input: JSON:API response with various data types
   242	 * const response = {
   243	 *   data: {
   244	 *     type: 'articles',
   245	 *     id: '1',
   246	 *     attributes: {
   247	 *       title: 'My Article',
   248	 *       is_published: 1,                    // Boolean as number
   249	 *       published_at: '2024-01-15 10:00:00' // MySQL datetime
   250	 *     }
   251	 *   },
   252	 *   included: [{
   253	 *     type: 'users',
   254	 *     id: '10',
   255	 *     attributes: {
   256	 *       name: 'John',
   257	 *       is_admin: 0,         // Boolean as number
   258	 *       last_login: '2024-01-14 15:30:00'
   259	 *     }
   260	 *   }]
   261	 * };
   262	 *
   263	 * const normalized = normalizeRecordAttributes(response, scopes);
   264	 *
   265	 * // Output: All booleans and dates normalized
   266	 * // {
   267	 * //   data: {
   268	 * //     type: 'articles',
   269	 * //     id: '1',
   270	 * //     attributes: {
   271	 * //       title: 'My Article',
   272	 * //       is_published: true,                        // 1 → true
   273	 * //       published_at: Date('2024-01-15T10:00:00Z') // Date object
   274	 * //     }
   275	 * //   },
   276	 * //   included: [{
   277	 * //     type: 'users',
   278	 * //     id: '10',
   279	 * //     attributes: {
   280	 * //       name: 'John',
   281	 * //       is_admin: false,                          // 0 → false
   282	 * //       last_login: Date('2024-01-14T15:30:00Z')  // Date object
   283	 * //     }
   284	 * //   }]
   285	 * // }
   286	 *
   287	 * @example
   288	 * // Input: Collection response (array of records)
   289	 * const response = {
   290	 *   data: [
   291	 *     {
   292	 *       type: 'comments',
   293	 *       id: '1',
   294	 *       attributes: { approved: 1, created_at: '2024-01-01 09:00:00' }
   295	 *     },
   296	 *     {
   297	 *       type: 'comments',
   298	 *       id: '2',
   299	 *       attributes: { approved: 0, created_at: '2024-01-02 10:00:00' }
   300	 *     }
   301	 *   ]
   302	 * };
   303	 *
   304	 * // Each record in the array is normalized
   305	 *
   306	 * @description
   307	 * Used by:
   308	 * - rest-api-knex-plugin after fetching data, before sending response
   309	 * - Applied to both GET (single) and QUERY (collection) responses
   310	 *
   311	 * Purpose:
   312	 * - Ensures consistent data types in API responses
   313	 * - Handles both primary data and included resources
   314	 * - Works with single records and collections
   315	 * - Uses schema definitions to determine field types
   316	 *
   317	 * Data flow:
   318	 * 1. Checks if data is array (collection) or object (single)
   319	 * 2. For each record, looks up its schema by type
   320	 * 3. Calls normalizeAttributes with schema structure
   321	 * 4. Processes included array the same way
   322	 * 5. Returns complete response with normalized values
   323	 */
   324	export function normalizeRecordAttributes (record, scopes) {
   325	  if (!record || !scopes) {
   326	    return record
   327	  }
   328	
   329	  // Normalize main data records
   330	  if (record.data) {
   331	    if (Array.isArray(record.data)) {
   332	      // Handle array of records (query result)
   333	      for (const entry of record.data) {
   334	        const scope = scopes[entry.type]
   335	        if (scope?.vars?.schemaInfo?.schemaStructure && entry.attributes) {
   336	          entry.attributes = normalizeAttributes(
   337	            entry.attributes,
   338	            scope.vars.schemaInfo.schemaStructure
   339	          )
   340	        }
   341	      }
   342	    } else {
   343	      // Handle single record (get result)
   344	      const entry = record.data
   345	      const scope = scopes[entry.type]
   346	      if (scope?.vars?.schemaInfo?.schemaStructure && entry.attributes) {
   347	        entry.attributes = normalizeAttributes(
   348	          entry.attributes,
   349	          scope.vars.schemaInfo.schemaStructure
   350	        )
   351	      }
   352	    }
   353	  }
   354	
   355	  // Normalize included records
   356	  if (record.included && Array.isArray(record.included)) {
   357	    for (const entry of record.included) {
   358	      const scope = scopes[entry.type]
   359	      if (scope?.vars?.schemaInfo?.schemaStructure && entry.attributes) {
   360	        entry.attributes = normalizeAttributes(
   361	          entry.attributes,
   362	          scope.vars.schemaInfo.schemaStructure
   363	        )
   364	      }
   365	    }
   366	  }
   367	
   368	  return record
   369	}
