     1	/**
     2	 * @file Creates Knex table definitions from json-rest-schema definitions
     3	 */
     4	
     5	/**
     6	 * Maps json-rest-schema types to Knex column types
     7	 * @param {object} table - The Knex table builder instance
     8	 * @param {string} columnName - The name of the column
     9	 * @param {object} definition - The schema definition for the field
    10	 * @returns {object} The Knex column builder instance
    11	 */
    12	function mapTypeToKnex (table, columnName, definition) {
    13	  const { type, precision, scale, maxLength, unsigned } = definition
    14	
    15	  switch (type) {
    16	    case 'string':
    17	      return maxLength ? table.string(columnName, maxLength) : table.string(columnName)
    18	
    19	    case 'number':
    20	      if (precision !== undefined && scale !== undefined) {
    21	        return table.decimal(columnName, precision, scale)
    22	      }
    23	      return table.float(columnName)
    24	
    25	    case 'id':
    26	      const col = table.integer(columnName)
    27	      return unsigned !== false ? col.unsigned() : col
    28	
    29	    case 'boolean':
    30	      return table.boolean(columnName)
    31	
    32	    case 'date':
    33	      return table.date(columnName)
    34	
    35	    case 'dateTime':
    36	      return table.datetime(columnName)
    37	
    38	    case 'time':
    39	      return table.time(columnName)
    40	
    41	    case 'timestamp':
    42	      return table.integer(columnName)
    43	
    44	    case 'array':
    45	    case 'object':
    46	    case 'serialize':
    47	      return table.json(columnName)
    48	
    49	    case 'blob':
    50	    case 'file':
    51	      return table.binary(columnName)
    52	
    53	    case 'none':
    54	    default:
    55	      return table.string(columnName)
    56	  }
    57	}
    58	
    59	/**
    60	 * Applies schema constraints to a Knex column
    61	 * @param {object} column - The Knex column builder instance
    62	 * @param {object} definition - The schema definition for the field
    63	 */
    64	function applyConstraints (column, definition) {
    65	  // Nullability
    66	  if (definition.nullable === true) {
    67	    column.nullable()
    68	  } else if (definition.required === true || definition.nullable === false) {
    69	    column.notNullable()
    70	  }
    71	
    72	  // Default value
    73	  if (definition.defaultTo !== undefined) {
    74	    column.defaultTo(definition.defaultTo)
    75	  }
    76	
    77	  // Database-specific constraints
    78	  if (definition.unique === true) {
    79	    column.unique()
    80	  }
    81	
    82	  if (definition.primary === true) {
    83	    column.primary()
    84	  }
    85	
    86	  if (definition.index === true) {
    87	    column.index()
    88	  }
    89	
    90	  if (definition.references) {
    91	    column.references(definition.references.column || 'id')
    92	      .inTable(definition.references.table)
    93	
    94	    if (definition.references.onDelete) {
    95	      column.onDelete(definition.references.onDelete)
    96	    }
    97	
    98	    if (definition.references.onUpdate) {
    99	      column.onUpdate(definition.references.onUpdate)
   100	    }
   101	  }
   102	
   103	  if (definition.comment) {
   104	    column.comment(definition.comment)
   105	  }
   106	}
   107	
   108	/**
   109	 * Creates a Knex table from a json-rest-schema definition
   110	 * @param {object} knex - The Knex instance
   111	 * @param {string} tableName - The name of the table to create
   112	 * @param {object} schema - The json-rest-schema instance
   113	 * @param {string} [idProperty='id'] - The name of the ID column
   114	 * @param {object} [options={}] - Additional options
   115	 * @param {boolean} [options.autoIncrement=true] - Whether to use auto-incrementing IDs
   116	 * @param {boolean} [options.timestamps=false] - Whether to add created_at/updated_at columns
   117	 * @returns {Promise} A promise that resolves when the table is created
   118	 */
   119	export async function createKnexTable (knex, schemaInfo, tableSchemaInstance, options = {}) {
   120	  const { autoIncrement = true, timestamps = false } = options
   121	
   122	  const tableName = schemaInfo.tableName
   123	  const idProperty = schemaInfo.idProperty
   124	  const tableSchemaStructure = tableSchemaInstance.structure
   125	
   126	  return knex.schema.createTable(tableName, (table) => {
   127	    // Check if schema has the idProperty field with primary key
   128	    const hasIdField = tableSchemaStructure[idProperty] && tableSchemaStructure[idProperty].primary === true
   129	
   130	    // Add auto-incrementing ID if no primary key is defined and autoIncrement is true
   131	    if (!hasIdField && autoIncrement) {
   132	      table.increments(idProperty).primary()
   133	    }
   134	
   135	    // Process each field in the schema
   136	    for (const [fieldName, definition] of Object.entries(tableSchemaStructure)) {
   137	      // Skip if this is the ID field and we already handled it
   138	      if (fieldName === idProperty && !hasIdField && autoIncrement) {
   139	        continue
   140	      }
   141	
   142	      // Create the column with the appropriate type
   143	      const column = mapTypeToKnex(table, fieldName, definition)
   144	
   145	      // Apply constraints
   146	      applyConstraints(column, definition)
   147	    }
   148	
   149	    // Add timestamps if requested
   150	    if (timestamps) {
   151	      table.timestamps(true, true)
   152	    }
   153	  })
   154	}
   155	
   156	export async function addKnexFields (knex, tableName, schema) {
   157	  return knex.schema.alterTable(tableName, (table) => {
   158	    for (const [fieldName, definition] of Object.entries(schema.structure)) {
   159	      const column = mapTypeToKnex(table, fieldName, definition)
   160	      applyConstraints(column, definition)
   161	    }
   162	  })
   163	}
   164	
   165	// Helper function to alter multiple fields in an existing table
   166	export async function alterKnexFields (knex, tableName, fields, options = {}) {
   167	  return knex.schema.alterTable(tableName, (table) => {
   168	    for (const [fieldName, definition] of Object.entries(fields)) {
   169	      // Create the column with alter flag
   170	      const column = mapTypeToKnex(table, fieldName, definition)
   171	
   172	      // Mark this as an alteration
   173	      column.alter()
   174	
   175	      // Apply constraints (these will be reapplied with the alter)
   176	      applyConstraints(column, definition)
   177	    }
   178	  })
   179	}
   180	
   181	/**
   182	 * Generates a Knex migration string from a json-rest-schema definition
   183	 * @param {string} tableName - The name of the table
   184	 * @param {object} schema - The json-rest-schema instance
   185	 * @param {object} [options={}] - Additional options
   186	 * @returns {string} The migration code as a string
   187	 */
   188	export function generateKnexMigration (tableName, schema, options = {}) {
   189	  const { autoIncrement = true, timestamps = false } = options
   190	
   191	  let migration = `exports.up = function(knex) {
   192	  return knex.schema.createTable('${tableName}', (table) => {\n`
   193	
   194	  // Check if schema has an 'id' field with primary key
   195	  const hasIdField = schema.structure.id && schema.structure.id.primary === true
   196	
   197	  // Add auto-incrementing ID if needed
   198	  if (!hasIdField && autoIncrement) {
   199	    migration += '    table.increments(\'id\').primary();\n'
   200	  }
   201	
   202	  // Process each field
   203	  for (const [fieldName, definition] of Object.entries(schema.structure)) {
   204	    if (fieldName === 'id' && !hasIdField && autoIncrement) {
   205	      continue
   206	    }
   207	
   208	    let line = '    '
   209	
   210	    // Map type
   211	    switch (definition.type) {
   212	      case 'string':
   213	        line += definition.maxLength
   214	          ? `table.string('${fieldName}', ${definition.maxLength})`
   215	          : `table.string('${fieldName}')`
   216	        break
   217	      case 'number':
   218	        if (definition.precision !== undefined && definition.scale !== undefined) {
   219	          line += `table.decimal('${fieldName}', ${definition.precision}, ${definition.scale})`
   220	        } else {
   221	          line += `table.float('${fieldName}')`
   222	        }
   223	        break
   224	      case 'id':
   225	        line += `table.integer('${fieldName}')`
   226	        if (definition.unsigned !== false) line += '.unsigned()'
   227	        break
   228	      case 'boolean':
   229	        line += `table.boolean('${fieldName}')`
   230	        break
   231	      case 'date':
   232	        line += `table.date('${fieldName}')`
   233	        break
   234	      case 'dateTime':
   235	        line += `table.datetime('${fieldName}')`
   236	        break
   237	      case 'time':
   238	        line += `table.time('${fieldName}')`
   239	        break
   240	      case 'timestamp':
   241	        line += `table.integer('${fieldName}')`
   242	        break
   243	      case 'array':
   244	      case 'object':
   245	      case 'serialize':
   246	        line += `table.json('${fieldName}')`
   247	        break
   248	      case 'blob':
   249	      case 'file':
   250	        line += `table.binary('${fieldName}')`
   251	        break
   252	      default:
   253	        line += `table.string('${fieldName}')`
   254	    }
   255	
   256	    // Add constraints
   257	    if (definition.nullable === true) {
   258	      line += '.nullable()'
   259	    } else if (definition.required === true || definition.nullable === false) {
   260	      line += '.notNullable()'
   261	    }
   262	
   263	    if (definition.defaultTo !== undefined) {
   264	      const defaultValue = typeof definition.defaultTo === 'string'
   265	        ? `'${definition.defaultTo}'`
   266	        : definition.defaultTo
   267	      line += `.defaultTo(${defaultValue})`
   268	    }
   269	
   270	    if (definition.unique === true) line += '.unique()'
   271	    if (definition.primary === true) line += '.primary()'
   272	    if (definition.index === true) line += '.index()'
   273	
   274	    if (definition.references) {
   275	      const refCol = definition.references.column || 'id'
   276	      line += `.references('${refCol}').inTable('${definition.references.table}')`
   277	      if (definition.references.onDelete) {
   278	        line += `.onDelete('${definition.references.onDelete}')`
   279	      }
   280	      if (definition.references.onUpdate) {
   281	        line += `.onUpdate('${definition.references.onUpdate}')`
   282	      }
   283	    }
   284	
   285	    if (definition.comment) {
   286	      line += `.comment('${definition.comment}')`
   287	    }
   288	
   289	    migration += line + ';\n'
   290	  }
   291	
   292	  if (timestamps) {
   293	    migration += '    table.timestamps(true, true);\n'
   294	  }
   295	
   296	  migration += `  });
   297	};
   298	
   299	exports.down = function(knex) {
   300	  return knex.schema.dropTable('${tableName}');
   301	};`
   302	
   303	  return migration
   304	}
