method + test + implementation

This commit is contained in:
Niels Lyngsø
2024-03-14 15:50:57 +01:00
parent e7ab5e0859
commit 21907f4bf6
8 changed files with 184 additions and 50 deletions

View File

@@ -1,14 +1,30 @@
import type { UmbApi } from '../models/api.interface.js';
import { createExtensionApi } from './create-extension-api.function.js';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
/**
*
* @param {UmbControllerHost} host - The host to parse on as the host to the extension api.
* @param {string} alias - The alias of the extension api to create.
* @param {Array<unknown>} constructorArgs - The constructor arguments to pass to the extension api, host will always be appended as the first argument, meaning these arguments will be second and so forth.
* @returns {ApiType} a class instance of the api provided via the manifest that matches the given alias. You have to type this via the generic `ApiType` parameter.
*/
export async function createExtensionApiByAlias<ApiType extends UmbApi = UmbApi>(
host: UmbControllerHost,
alias: string,
constructorArgs?: Array<unknown>,
): Promise<ApiType | undefined> {
// Get Manifest:
const manifest = {};
): Promise<ApiType> {
// Get manifest:
const manifest = umbExtensionsRegistry.getByAlias(alias);
if (!manifest) {
throw new Error(`Failed to get manifest by alias: ${alias}`);
}
const api = await createExtensionApi<ApiType>(host, manifest, constructorArgs);
if (!api) {
throw new Error(`Failed to create extension api from alias: ${alias}`);
}
// Create extension:
return createExtensionApi(host, manifest, constructorArgs);
return api;
}

View File

@@ -0,0 +1,127 @@
import { expect, fixture } from '@open-wc/testing';
import type { ManifestApi } from '../types/index.js';
import type { UmbApi } from '../models/api.interface.js';
import { createExtensionApiByAlias } from './create-extension-api-by-alias.function.js';
import { customElement, html } from '@umbraco-cms/backoffice/external/lit';
import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
@customElement('umb-test-controller-host')
class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {}
interface UmbExtensionApiBoolTestClass extends UmbApi {
isValidClassInstance(): boolean;
}
class UmbExtensionApiTrueTestClass implements UmbExtensionApiBoolTestClass {
isValidClassInstance() {
return true;
}
destroy() {}
}
class UmbExtensionApiFalseTestClass implements UmbExtensionApiBoolTestClass {
isValidClassInstance() {
return false;
}
destroy() {}
}
const jsModuleWithDefaultExport = {
default: UmbExtensionApiTrueTestClass,
};
const jsModuleWithApiExport = {
api: UmbExtensionApiTrueTestClass,
};
const jsModuleWithDefaultAndApiExport = {
default: UmbExtensionApiFalseTestClass,
api: UmbExtensionApiTrueTestClass,
};
describe('Create Extension Api By Alias Method', () => {
let hostElement: UmbTestControllerHostElement;
beforeEach(async () => {
hostElement = await fixture(html`<umb-test-controller-host></umb-test-controller-host>`);
});
it('Returns `undefined` when manifest does not have any correct properties', (done) => {
const manifest: ManifestApi = {
type: 'my-test-type',
alias: 'Umb.Test.createManifestApi',
name: 'pretty name',
};
umbExtensionsRegistry.register(manifest);
createExtensionApiByAlias<UmbExtensionApiBoolTestClass>(hostElement, manifest.alias, []).then(() => {
umbExtensionsRegistry.unregister(manifest.alias);
done(new Error('Should not resolve'));
});
setTimeout(() => {
umbExtensionsRegistry.unregister(manifest.alias);
done();
}, 10);
});
it('Handles when `api` property contains a class constructor', async () => {
const manifest: ManifestApi<UmbExtensionApiTrueTestClass> = {
type: 'my-test-type',
alias: 'Umb.Test.createManifestApi',
name: 'pretty name',
api: UmbExtensionApiTrueTestClass,
};
umbExtensionsRegistry.register(manifest);
const api = await createExtensionApiByAlias<UmbExtensionApiBoolTestClass>(hostElement, manifest.alias, []);
expect(api.isValidClassInstance()).to.be.true;
umbExtensionsRegistry.unregister(manifest.alias);
});
it('Handles when `loader` has a default export', async () => {
const manifest: ManifestApi<UmbExtensionApiTrueTestClass> = {
type: 'my-test-type',
alias: 'Umb.Test.createManifestApi',
name: 'pretty name',
js: () => Promise.resolve(jsModuleWithDefaultExport),
};
umbExtensionsRegistry.register(manifest);
const api = await createExtensionApiByAlias<UmbExtensionApiBoolTestClass>(hostElement, manifest.alias, []);
expect(api.isValidClassInstance()).to.be.true;
umbExtensionsRegistry.unregister(manifest.alias);
});
it('Handles when `loader` has a api export', async () => {
const manifest: ManifestApi<UmbExtensionApiTrueTestClass> = {
type: 'my-test-type',
alias: 'Umb.Test.createManifestApi',
name: 'pretty name',
js: () => Promise.resolve(jsModuleWithApiExport),
};
umbExtensionsRegistry.register(manifest);
const api = await createExtensionApiByAlias<UmbExtensionApiBoolTestClass>(hostElement, manifest.alias, []);
expect(api.isValidClassInstance()).to.be.true;
umbExtensionsRegistry.unregister(manifest.alias);
});
it('Prioritizes api export from loader property', async () => {
const manifest: ManifestApi<UmbExtensionApiTrueTestClass> = {
type: 'my-test-type',
alias: 'Umb.Test.createManifestApi',
name: 'pretty name',
js: () => Promise.resolve(jsModuleWithDefaultAndApiExport),
};
umbExtensionsRegistry.register(manifest);
const api = await createExtensionApiByAlias<UmbExtensionApiBoolTestClass>(hostElement, manifest.alias, []);
expect(api.isValidClassInstance()).to.be.true;
umbExtensionsRegistry.unregister(manifest.alias);
});
});

View File

@@ -1,3 +1,4 @@
export * from './create-extension-api-by-alias.function.js';
export * from './create-extension-api.function.js';
export * from './create-extension-element-with-api.function.js';
export * from './create-extension-element.function.js';

View File

@@ -248,8 +248,8 @@ export class UmbExtensionRegistry<
}
/**
* Get an observable that provides extensions matching the given alias.
* @param alias {string} - The alias of the extensions to get.
* Get an observable that provides an extension matching the given alias.
* @param alias {string} - The alias of the extension to get.
* @returns {Observable<T | undefined>} - An observable of the extension that matches the alias.
*/
byAlias<T extends ManifestBase = ManifestBase>(alias: string): Observable<T | undefined> {
@@ -267,6 +267,19 @@ export class UmbExtensionRegistry<
) as Observable<T | undefined>;
}
/**
* Get an extension that matches the given alias, this will not return an observable, it is a one of retrieval if it exists at the given point in time.
* @param alias {string} - The alias of the extension to get.
* @returns {<T | undefined>} - The extension manifest that matches the alias.
*/
getByAlias<T extends ManifestBase = ManifestBase>(alias: string): T | undefined {
const ext = this._extensions.getValue().find((ext) => ext.alias === alias) as T | undefined;
if (ext?.kind) {
return this.#mergeExtensionWithKinds([ext, this._kinds.getValue()]);
}
return ext;
}
/**
* Get an observable that provides extensions matching the given type and alias.
* @param type {string} - The type of the extensions to get.

View File

@@ -13,8 +13,8 @@ export class UmbObserverController<T = unknown> extends UmbObserver<T> implement
constructor(
host: UmbControllerHost,
source: Observable<T>,
callback: ObserverCallback<T>,
alias: UmbControllerAlias,
callback?: ObserverCallback<T>,
alias?: UmbControllerAlias,
) {
super(source, callback);
this.#host = host;

View File

@@ -15,9 +15,11 @@ export class UmbObserver<T> {
#callback!: ObserverCallback<T>;
#subscription!: Subscription;
constructor(source: Observable<T>, callback: ObserverCallback<T>) {
constructor(source: Observable<T>, callback?: ObserverCallback<T>) {
this.#source = source;
this.#subscription = source.subscribe(callback);
if (callback) {
this.#subscription = source.subscribe(callback);
}
}
/**
@@ -63,9 +65,9 @@ export class UmbObserver<T> {
destroy(): void {
if (this.#subscription) {
this.#subscription.unsubscribe();
(this.#source as any) = undefined;
(this.#callback as any) = undefined;
(this.#subscription as any) = undefined;
}
(this.#source as any) = undefined;
}
}

View File

@@ -1,53 +1,23 @@
import { UmbEntityActionBase } from '../../entity-action-base.js';
import type { UmbEntityActionArgs } from '../../types.js';
import type { MetaEntityActionDeleteKind } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-api';
import type { UmbDetailRepository, UmbItemRepository } from '@umbraco-cms/backoffice/repository';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/event';
export class UmbDeleteEntityAction extends UmbEntityActionBase<MetaEntityActionDeleteKind> {
// TODO: make base type for item and detail models
#itemRepository?: UmbItemRepository<any>;
#detailRepository?: UmbDetailRepository<any>;
#init: Promise<unknown>;
constructor(host: UmbControllerHost, args: UmbEntityActionArgs<MetaEntityActionDeleteKind>) {
super(host, args);
// TODO: We should properly look into how we can simplify the one time usage of a extension api, as its a bit of overkill to take conditions/overwrites and observation of extensions into play here: [NL]
// But since this happens when we execute an action, it does most likely not hurt any users, but it is a bit of a overkill to do this for every action: [NL]
this.#init = Promise.all([
new UmbExtensionApiInitializer(
this._host,
umbExtensionsRegistry,
this.args.meta.itemRepositoryAlias,
[this._host],
(permitted, ctrl) => {
this.#itemRepository = permitted ? (ctrl.api as UmbItemRepository<any>) : undefined;
},
).asPromise(),
new UmbExtensionApiInitializer(
this._host,
umbExtensionsRegistry,
this.args.meta.detailRepositoryAlias,
[this._host],
(permitted, ctrl) => {
this.#detailRepository = permitted ? (ctrl.api as UmbDetailRepository<any>) : undefined;
},
).asPromise(),
]);
}
async execute() {
if (!this.args.unique) throw new Error('Cannot delete an item without a unique identifier.');
await this.#init;
const { data } = await this.#itemRepository!.requestItems([this.args.unique]);
const itemRepository = await createExtensionApiByAlias<UmbItemRepository<any>>(
this,
this.args.meta.itemRepositoryAlias,
);
const { data } = await itemRepository.requestItems([this.args.unique]);
const item = data?.[0];
if (!item) throw new Error('Item not found.');
@@ -59,7 +29,11 @@ export class UmbDeleteEntityAction extends UmbEntityActionBase<MetaEntityActionD
confirmLabel: 'Delete',
});
await this.#detailRepository!.delete(this.args.unique);
const detailRepository = await createExtensionApiByAlias<UmbDetailRepository<any>>(
this,
this.args.meta.detailRepositoryAlias,
);
detailRepository.delete(this.args.unique);
const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const event = new UmbRequestReloadStructureForEntityEvent({

View File

@@ -3,9 +3,10 @@ import type {
UmbRepositoryResponse,
UmbRepositoryResponseWithAsObservable,
} from '../types.js';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
export interface UmbDetailRepository<DetailModelType> {
export interface UmbDetailRepository<DetailModelType> extends UmbApi {
createScaffold(preset?: Partial<DetailModelType>): Promise<UmbRepositoryResponse<DetailModelType>>;
requestByUnique(unique: string): Promise<UmbRepositoryResponseWithAsObservable<DetailModelType>>;
byUnique(unique: string): Promise<Observable<DetailModelType | undefined>>;