Repository: directus/directus
File: api/src/database/migrations/20240204A-marketplace.ts (TypeScript)

Code before:
		if (isScopedModuleBundleParent) continue;

		const isScopedModuleBundleChild = name.startsWith('@') && splittedName.length > 2;

		const bundleParentName =
			isScopedModuleBundleParent || isScopedModuleBundleChild ? splittedName.slice(0, 2).join('/') : splittedName[0];

		const bundleParent = idMap.get(bundleParentName);

		if (!bundleParent) continue;

		await knex('directus_extensions')
			.update({
				bundle: bundleParent.id,
				folder: name.substring(bundleParentName.length + 1),
				source: bundleParent.source,
			})
			.where({ folder: name });
	}

	await knex.schema.alterTable('directus_extensions', (table) => {
		table.uuid('id').alter().notNullable();
	});

	await knex.transaction(async (trx) => {
		const helpers = getHelpers(trx);

		await helpers.schema.changePrimaryKey('directus_extensions', ['id']);

		await trx.schema.alterTable('directus_extensions', (table) => {
			table.dropColumn('name');
			table.string('source').alter().notNullable();
			table.string('folder').alter().notNullable();
		});
	});
}

/*
 * Note: For local extensions having a different package & folder name,
 * we aren't able to revert to the exact same state as before.
 * But we still need to do the name convertion, in order for the migration to succeed.
 */
export async function down(knex: Knex): Promise<void> {
	await knex.schema.alterTable('directus_extensions', (table) => {
		table.string('name');
	});

	const installedExtensions = await knex.select(['id', 'folder', 'bundle', 'source']).from('directus_extensions');

	const idMap = new Map<string, string>(installedExtensions.map((extension) => [extension.id, extension.folder]));


Review comment (refactor):
I assume when using `changePrimaryKey` we will always need to wrap it in a transaction? 

Would be great to handle this internally in the helper

This way callers don't need to remember to wrap it. Thoughts?

Code after:
		if (isScopedModuleBundleParent) continue;

		const isScopedModuleBundleChild = name.startsWith('@') && splittedName.length > 2;

		const bundleParentName =
			isScopedModuleBundleParent || isScopedModuleBundleChild ? splittedName.slice(0, 2).join('/') : splittedName[0];

		const bundleParent = idMap.get(bundleParentName);

		if (!bundleParent) continue;

		await knex('directus_extensions')
			.update({
				bundle: bundleParent.id,
				folder: name.substring(bundleParentName.length + 1),
				source: bundleParent.source,
			})
			.where({ folder: name });
	}

	await knex.schema.alterTable('directus_extensions', (table) => {
		table.uuid('id').alter().notNullable();
	});

	// knex does not bundle the drop + add PK, the transaction ensures they are apart of the same commit
	await knex.transaction(async (trx) => {
		await getHelpers(trx).schema.changePrimaryKey('directus_extensions', ['id']);
	});

	await knex.schema.alterTable('directus_extensions', (table) => {
		table.dropColumn('name');
		table.string('source').alter().notNullable();
		table.string('folder').alter().notNullable();
	});
}

/*
 * Note: For local extensions having a different package & folder name,
 * we aren't able to revert to the exact same state as before.
 * But we still need to do the name convertion, in order for the migration to succeed.
 */
export async function down(knex: Knex): Promise<void> {
	await knex.schema.alterTable('directus_extensions', (table) => {
		table.string('name');
	});

	const installedExtensions = await knex.select(['id', 'folder', 'bundle', 'source']).from('directus_extensions');

	const idMap = new Map<string, string>(installedExtensions.map((extension) => [extension.id, extension.folder]));

	for (const { id, folder, bundle, source } of installedExtensions) {