diff --git a/api/src/database/migrations/20240204A-marketplace.ts b/api/src/database/migrations/20240204A-marketplace.ts index e03f5e32d9..42c5c60cfe 100644 --- a/api/src/database/migrations/20240204A-marketplace.ts +++ b/api/src/database/migrations/20240204A-marketplace.ts @@ -1,5 +1,10 @@ +import { resolvePackage } from '@directus/utils/node'; import type { Knex } from 'knex'; import { randomUUID } from 'node:crypto'; +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); export async function up(knex: Knex): Promise { await knex.schema.alterTable('directus_extensions', (table) => { @@ -14,22 +19,49 @@ export async function up(knex: Knex): Promise { const idMap = new Map(); for (const { name } of installedExtensions) { - const id = randomUUID(); - await knex('directus_extensions').update({ id, source: 'local' }).where({ name }); - idMap.set(name, id); + // Delete extension meta status that used the legacy `${name}:${type}` name syntax for + // extension-folder scoped extensions + if (name.includes(':')) { + await knex('directus_extensions').delete().where({ name }); + } else { + const id = randomUUID(); + + let source; + + try { + // The NPM package name is the name used in the database. If we can resolve the + // extension as a node module it's safe to assume it's a npm-module source + resolvePackage(name, __dirname); + source = 'module'; + } catch { + source = 'local'; + } + + await knex('directus_extensions').update({ id, source }).where({ name }); + idMap.set(name, id); + } } - // This will also include flat extensions with an NPM org scope, but there's no way to identify - // those - const bundleNames = Array.from(idMap.keys()).filter((name) => name.includes('/')); - for (const { name } of installedExtensions) { - const bundleParent = bundleNames.find((bundleName) => name.startsWith(bundleName + '/')); + if (!name.includes('/')) continue; - if (!bundleParent) continue; + const splittedName = name.split('/'); + + const isScopedModuleBundleParent = name.startsWith('@') && splittedName.length == 2; + + if (isScopedModuleBundleParent) continue; + + const isScopedModuleBundleChild = name.startsWith('@') && splittedName.length > 2; + + const bundleParentName = + isScopedModuleBundleParent || isScopedModuleBundleChild ? splittedName.slice(0, 2).join('/') : splittedName[0]; + + const bundleParentId = idMap.get(bundleParentName); + + if (!bundleParentId) continue; await knex('directus_extensions') - .update({ bundle: idMap.get(bundleParent), name: name.substring(bundleParent.length + 1) }) + .update({ bundle: bundleParentId, name: name.substring(bundleParentName.length + 1) }) .where({ name }); } diff --git a/api/src/extensions/lib/get-extensions-settings.ts b/api/src/extensions/lib/get-extensions-settings.ts index 627cfa96ce..e061bc15ff 100644 --- a/api/src/extensions/lib/get-extensions-settings.ts +++ b/api/src/extensions/lib/get-extensions-settings.ts @@ -67,6 +67,24 @@ export const getExtensionsSettings = async ({ for (const [folder, extension] of local.entries()) { const settingsExist = localSettings.some((settings) => settings.folder === folder); + if (settingsExist) continue; + + const settingsForName = localSettings.find((settings) => settings.folder === extension.name); + + /* + * TODO: Consider removing this in follow-up versions after v10.10.0 + * + * Previously, the package name (from package.json) was used to identify + * local extensions - now it's the folder name. + * If those two are different, we need to check for existing settings + * with the package name, too. On a match and if there's no other local extension + * with such a folder name, these settings can be taken over with the folder updated. + */ + if (settingsForName && !local.has(extension.name)) { + await service.extensionsItemService.updateOne(settingsForName.id, { folder }); + continue; + } + if (!settingsExist) generateSettingsEntry(folder, extension, 'local'); } diff --git a/api/src/services/extensions.ts b/api/src/services/extensions.ts index 73f72ec5ed..144c506cbc 100644 --- a/api/src/services/extensions.ts +++ b/api/src/services/extensions.ts @@ -118,7 +118,9 @@ export class ExtensionsService { if (!parentBundle) continue; - const schema = (parentBundle.schema as BundleExtension).entries.find((entry) => entry.name === meta.folder); + const schema = (parentBundle.schema as BundleExtension | null)?.entries.find( + (entry) => entry.name === meta.folder, + ); if (!schema) continue;