integration stuff

This commit is contained in:
2021-12-22 15:41:11 +01:00
parent 790791d919
commit 8de62e30f6
33 changed files with 587 additions and 135 deletions
@@ -1,31 +1,172 @@
using Microsoft.AspNetCore.Mvc;
using System.Collections;
using zero.Mapper;
namespace zero.Api.Endpoints.Integrations;
public class IntegrationsController : ZeroApiController
{
readonly IIntegrationTypeService IntegrationTypes;
readonly IIntegrationStore Store;
public IntegrationsController(IIntegrationTypeService integrationTypes)
public IntegrationsController(IIntegrationTypeService integrationTypes, IIntegrationStore store)
{
IntegrationTypes = integrationTypes;
Store = store;
}
[HttpGet("types")]
//[ZeroAuthorize(SpacePermissions.Read)]
public virtual ActionResult<IEnumerable> GetTypes()
public virtual async Task<ActionResult<IEnumerable>> GetTypes()
{
IEnumerable<IntegrationTypeDisplay> result = Mapper.Map<IntegrationType, IntegrationTypeDisplay>(IntegrationTypes.GetAll());
foreach (IntegrationTypeDisplay display in result)
{
Integration model = await Store.Load(display.Alias);
if (model != null)
{
display.IsConfigured = true;
display.IsActivated = model.IsActive;
display.ModelId = model.Id;
}
}
return Ok(result);
}
[HttpGet("types/{alias}")]
//[ZeroAuthorize(SpacePermissions.Read)]
public virtual ActionResult<SpaceType> GetTypes(string alias)
public virtual async Task<ActionResult<IntegrationTypeDisplay>> GetType(string alias)
{
IntegrationTypeDisplay result = Mapper.Map<IntegrationType, IntegrationTypeDisplay>(IntegrationTypes.GetByAlias(alias));
if (result == null)
{
return NotFound();
}
Integration model = await Store.Load(result.Alias);
if (model != null)
{
result.IsConfigured = true;
result.IsActivated = model.IsActive;
result.ModelId = model.Id;
}
return Ok(result);
}
[HttpGet("empty/{alias}")]
//[ZeroAuthorize(CountryPermissions.Create)]
public virtual async Task<ActionResult<Integration>> Empty(string alias)
{
Integration model = await Store.Empty(alias);
if (model == null)
{
return NotFound();
}
return model;
}
[HttpGet("{alias}")]
//[ZeroAuthorize(CountryPermissions.Read)]
public virtual async Task<ActionResult<Integration>> Get(string alias)
{
Integration model = await Store.Load(alias);
if (model == null)
{
return NotFound();
}
HttpContext.Items[ApiConstants.ChangeToken] = Store.GetChangeToken(model);
return model;
}
[HttpPost("")]
//[ZeroAuthorize(CountryPermissions.Create)]
public virtual async Task<ActionResult<Result>> Create(Integration saveModel)
{
Result<Integration> result = await Store.Create(saveModel);
bool minimalResponse = Hints.ResponsePreference == ApiResponsePreference.Minimal;
if (result.IsSuccess)
{
return Created("/", minimalResponse ? null : saveModel);
}
return result.WithoutModel();
}
[HttpPut("{alias}")]
//[ZeroAuthorize(CountryPermissions.Update)]
public virtual async Task<ActionResult<Result>> Update(string alias, Integration updateModel, [FromQuery] string changeToken = null)
{
if (alias != updateModel.TypeAlias)
{
return BadRequest(Result.Fail(nameof(alias), "@integration.errors.noaliasmatch"));
}
Result<Integration> result = await Store.Update(updateModel);
if (Hints.ResponsePreference == ApiResponsePreference.Minimal)
{
return result.WithoutModel();
}
return result;
}
[HttpPut("{alias}/activate")]
//[ZeroAuthorize(CountryPermissions.Update)]
public virtual async Task<ActionResult<Result>> Activate(string alias)
{
Result<Integration> result = await Store.Activate(alias);
if (Hints.ResponsePreference == ApiResponsePreference.Minimal)
{
return result.WithoutModel();
}
return result;
}
[HttpPut("{alias}/deactivate")]
//[ZeroAuthorize(CountryPermissions.Update)]
public virtual async Task<ActionResult<Result>> Deactivate(string alias)
{
Result<Integration> result = await Store.Deactivate(alias);
if (Hints.ResponsePreference == ApiResponsePreference.Minimal)
{
return result.WithoutModel();
}
return result;
}
[HttpDelete("{alias}")]
//[ZeroAuthorize(CountryPermissions.Delete)]
public virtual async Task<ActionResult<Result>> Delete(string alias)
{
Result<Integration> result = await Store.Delete(alias);
return result.WithoutModel();
}
}
@@ -1,43 +0,0 @@
//using Microsoft.AspNetCore.Mvc;
//using System.Collections.Generic;
//using System.Threading.Tasks;
//using zero.Core.Collections;
//using zero.Core.Entities;
//using zero.Core.Integrations;
//using zero.Web.Models;
//namespace zero.Web.Controllers
//{
// //[ZeroAuthorize(Permissions.Sections.PREFIX + "commerce", PermissionsValue.Read)]
// public class IntegrationsController : BackofficeController
// {
// IIntegrationsCollection Collection;
// public IntegrationsController(IIntegrationsCollection collection)
// {
// Collection = collection;
// }
// public EditModel<Integration> GetEmpty([FromQuery] string alias) => Edit(Collection.GetEmpty(alias));
// public async Task<EditModel<Integration>> GetByAlias([FromQuery] string alias) => Edit(await Collection.GetByAlias(alias));
// public async Task<Paged<Integration>> Load([FromQuery] ListQuery<Integration> query) => await Collection.Load(query);
// public async Task<IList<IntegrationTypeWithStatus>> GetTypes() => await Collection.GetTypesWithStatus();
// [HttpPost]
// public async Task<Result<Integration>> Save([FromBody] Integration model) => await Collection.Save(model);
// [HttpPost]
// public async Task<Result<Integration>> SaveActiveState([FromBody] Integration model) => model.IsActive ? await Collection.Activate(model.Alias) : await Collection.Deactivate(model.Alias);
// [HttpDelete]
// public async Task<Result<Integration>> Delete([FromQuery] string alias) => await Collection.DeleteByAlias(alias);
// }
//}
+1 -9
View File
@@ -28,10 +28,6 @@ import uiDatagrid from './ui-datagrid.vue';
import uiProgress from './ui-progress.vue';
import uiDate from './ui-date.vue';
import uiEditor from './editor/ui-editor.vue';
import uiEditorInfos from './editor/ui-editor-infos.vue';
import uiEditorHeader from './editor/ui-editor-header.vue';
export {
uiIcon,
uiLoading,
@@ -61,9 +57,5 @@ export {
uiPick,
uiDatagrid,
uiProgress,
uiDate,
uiEditor,
uiEditorInfos,
uiEditorHeader
uiDate
};
+2 -2
View File
@@ -7,7 +7,7 @@ import { createRouter, RouteRecordRaw, RouterOptions } from 'vue-router';
import registerDirectives from '../directives/register';
import registerComponents from '../components/register';
import registerFormComponents from '../forms/register';
//import registerEditorComponents from '../editor/register';
import registerEditorComponents from '../editor/register';
import { getRouterConfig, appendRouterGuards } from './router/routerConfig';
import
{
@@ -73,7 +73,7 @@ export class ZeroRuntime implements Zero
registerDirectives(app);
registerComponents(app);
registerFormComponents(app);
//registerEditorComponents(app);
registerEditorComponents(app);
}
@@ -28,7 +28,7 @@
<script>
import * as overlays from '../../../services/overlay';
import * as overlays from '../../services/overlay';
export default {
name: 'uiEditorBlueprintProperty',
@@ -25,7 +25,7 @@
<script>
import { arrayRemove } from '../../../utils/arrays';
import { arrayRemove } from '../../utils/arrays';
//import Localization from 'zero/helpers/localization.js';
//import BlueprintApi from 'zero/api/blueprint.js';
+6 -1
View File
@@ -126,8 +126,13 @@ export function compileField(zero: Zero, editor: ZeroEditor, field: ZeroEditorFi
}
export function compileEditor(zero: Zero, editor: ZeroEditor): ZeroCompiledEditor
export function compileEditor(zero: Zero, editor: ZeroEditor): ZeroCompiledEditor | null
{
if (!editor)
{
return null;
}
let model = {
alias: editor.alias,
blueprint: null,
@@ -291,7 +291,7 @@
.ui-icon
{
color: var(--color-text);
color: var(--color-accent);
margin-top: -2px;
}
}
+12
View File
@@ -1,11 +1,23 @@
import { ZeroPlugin, ZeroPluginOptions } from '../core';
import { ZeroEditor } from './editor';
import createFields from './fields/createFields';
const editor = new ZeroEditor('integration.analytics.fathom');
editor.field('siteId').text({ maxLength: 30 });
editor.field('customDomain', { optional: true }).text({ maxLength: 150 });
const editor2 = new ZeroEditor('integration.analytics.google');
editor2.field('trackingId').text({ maxLength: 30 });
editor2.field('siteVerificationId').text({ maxLength: 150 });
export default {
name: "zero.editor",
install(app: ZeroPluginOptions)
{
createFields(app);
app.schema('integration.analytics.fathom', editor);
app.schema('integration.analytics.google', editor2);
}
} as ZeroPlugin;
@@ -0,0 +1,9 @@
import { App, defineAsyncComponent } from 'vue';
export default function (app: App)
{
app.component('ui-editor', defineAsyncComponent(() => import('./ui-editor.vue')));
app.component('ui-editor-infos', defineAsyncComponent(() => import('./ui-editor-infos.vue')));
app.component('ui-editor-header', defineAsyncComponent(() => import('./ui-editor-header.vue')));
};
@@ -23,8 +23,8 @@
<script>
import { selectorToArray, setObjectValue } from '../../utils';
import { localize } from '../../services/localization';
import { selectorToArray, setObjectValue } from '../utils';
import { localize } from '../services/localization';
import { defineComponent } from 'vue';
//import Overlay from 'zero/helpers/overlay.js';
@@ -1,5 +1,5 @@
<template>
<div class="editor-outer" v-if="loaded">
<div class="editor-outer" v-if="loaded && editorConfig">
<header class="editor-above" v-if="aboveDefined">
<!--<slot name="above" v-bind:config="editorConfig"></slot>-->
</header>
@@ -38,6 +38,15 @@
</aside>
</div>
</div>
<div class="editor-outer" v-if="loaded && !editorConfig">
<div class="page page-error" style="min-height: 400px;">
<ui-icon symbol="fth-cloud-snow" class="page-error-icon" :size="82" />
<p class="page-error-text">
<strong class="page-error-headline">Could not find editor</strong><br>
<span v-if="typeof config === 'string'">Please register an editor schema with the alias:<br />[{{config}}]</span>
</p>
</div>
</div>
</template>
@@ -49,7 +58,7 @@
import EditorComponent from './ui-editor-component.vue';
import BlueprintProperty from './blueprint/property.vue';
import { defineComponent } from 'vue';
import { compileEditor } from '../../editor/compile';
import { compileEditor } from './compile';
export default defineComponent({
name: 'uiEditor',
@@ -118,8 +127,7 @@
{
this.system = this.$route.query['zero.scope'] == 'system';
const schema = typeof this.config === 'string' ? await this.zero.getSchema(this.config) : this.config;
const editor = compileEditor(this.zero, schema);
this.editorConfig = editor;
this.editorConfig = compileEditor(this.zero, schema);
this.onConfigure(this);
@@ -6,11 +6,17 @@ export default {
getType: (alias: string, config?: ApiRequestConfig) => get('integrations/types/' + alias, { ...config }),
//getEmpty: async alias => await get(base + 'getEmpty', { params: { alias } }),
//getTypes: async () => await get(base + 'getTypes'),
//getByAlias: async (alias, config) => await get(base + 'getByAlias', { ...config, params: { alias } }),
//getByQuery: async (query, config) => await get(base + 'getByQuery', { ...config, params: { query } }),
//save: async (model, config) => await post(base + 'save', model, { ...config }),
//saveActiveState: async (model, config) => await post(base + 'saveActiveState', model, { ...config }),
//delete: async alias => await del(base + 'delete', { params: { alias } })
getEmpty: (alias: string, config?: ApiRequestConfig) => get('integrations/empty/' + alias, { ...config }),
getByAlias: (alias: string, config?: ApiRequestConfig) => get('integrations/' + alias, { ...config }),
create: (model: any, config?: ApiRequestConfig) => post('integrations', model, config),
update: (model: any, config?: ApiRequestConfig) => put('integrations/' + model.typeAlias, model, config),
activate: (alias: string, config?: ApiRequestConfig) => put('integrations/' + alias + '/activate', null, config),
deactivate: (alias: string, config?: ApiRequestConfig) => put('integrations/' + alias + '/deactivate', null, config),
delete: (alias: string, config?: ApiRequestConfig) => del('integrations/' + alias, null, config),
};
@@ -1,48 +1,112 @@
<template>
<ui-form ref="form" class="translation" v-slot="form" @submit="onSubmit" @load="onLoad" :route="route">
<ui-form-header v-model:value="model" prefix="@translation.list" title="@translation.name" :disabled="disabled" :is-create="!id" :state="form.state" :can-delete="meta.canDelete" @delete="onDelete" />
<ui-editor config="translations:edit" v-model="model" :meta="meta" :disabled="disabled" :scope="true">
<template v-slot:below>
<ui-editor-infos v-model="model" :disabled="disabled" />
<ui-form ref="form" v-slot="form" @submit="onSubmit" @load="onLoad">
<ui-trinity class="ui-editor-overlay integration">
<template v-slot:header>
<ui-header-bar :title="integration.name" prefix="@integration.list" :back-button="false" :close-button="true" />
</template>
</ui-editor>
<template v-slot:footer>
<ui-button type="light onbg" label="@ui.close" @click="config.close"></ui-button>
<template v-if="integration.isConfigured">
<ui-button type="light onbg" label="@ui.remove" @click="onDelete"></ui-button>
<ui-button v-if="!disabled" type="primary" :submit="true" label="@ui.save" :state="form.state" :disabled="loading"></ui-button>
</template>
<template v-if="!integration.isConfigured && !disabled">
<ui-button type="light onbg" :submit="true" label="@ui.save" :state="form.state" :disabled="loading"></ui-button>
<ui-button type="accent" @click="saveAndActivate" label="Save and activate" :state="form.state" :disabled="loading"></ui-button>
</template>
</template>
<ui-loading v-if="loading" :is-big="true" />
<div v-if="!loading" class="ui-editor-overlay-editor">
<ui-editor :config="editor" v-model="model" :meta="meta" :is-page="false" infos="none" :disabled="disabled" />
</div>
</ui-trinity>
</ui-form>
</template>
<script>
import * as overlays from '../../services/overlay';
//import Notification from 'zero/helpers/notification.js';
import api from './api';
export default {
props: ['id'],
props: {
config: Object
},
data: () => ({
integration: {},
disabled: false,
loading: true,
state: 'default',
editor: null,
meta: {},
model: { },
route: 'translations-edit',
disabled: false
model: {}
}),
created()
{
this.integration = { ...this.config.model };
//this.$el.style.setProperty('--color-primary', this.config.model.color);
//this.$el.style.setProperty('--color-button', this.config.model.color);
},
methods: {
async onLoad(form)
{
const response = await form.load(() => this.id ? api.getById(this.id, undefined, { system: this.$route.query.scope == 'system' }) : api.getEmpty(this.$route.query['zero.flavor']));
const alias = this.integration.alias;
this.editor = this.integration.editorAlias || 'integration.' + alias;
const response = await form.load(() => this.integration.isConfigured ? api.getByAlias(alias) : api.getEmpty(alias));
this.model = response;
this.loading = false;
},
saveAndActivate(e)
{
this.model.isActive = true;
this.onSubmit(this.$refs.form);
},
async onSubmit(form)
{
const response = this.id ? await api.update(this.model) : await api.create(this.model);
const response = this.integration.isConfigured ? await api.update(this.model) : await api.create(this.model);
await form.handle(response);
if (response.success)
{
this.config.confirm(response.data);
}
},
onDelete(item, opts)
async onDelete()
{
opts.hide();
this.$refs.form.onDelete(api.delete.bind(this, this.id));
const result = await overlays.confirmDelete();
if (result.eventType === 'confirm')
{
result.state('loading');
const response = await api.delete(this.integration.alias);
if (response.success)
{
result.state('success');
result.close();
//Notification.success('@deleteoverlay.success', '@deleteoverlay.success_text');
this.config.confirm(response);
}
}
}
}
}
@@ -4,9 +4,7 @@
<span class="integrations-item-icon" :style="{'background-color': hasColor ? model.color : null }">
<ui-icon :symbol="model.icon || 'fth-box'" :size="26" />
</span>
<!--<button type="button" v-if="!model.isConfigured" @click="open" class="ui-button type-primary type-block">
<span class="ui-button-text" v-localize="'Setup'" />
</button>
<!--
<button type="button" v-else @click="open" class="ui-button type-primary type-block">
<span class="ui-button-text" v-localize="'Edit'" />
</button>-->
@@ -20,9 +18,10 @@
<span v-localize="model.description"></span>
</template>
</p>
<div class="integrations-item-tags">
<!--<div class="integrations-item-tags">
<span class="ui-tag" v-for="tag in model.tags">{{tag}}</span>
</div>
</div>-->
<ui-button class="integrations-item-button" type="action small" v-if="!model.isConfigured" v-localize="'Setup'" @click="open" />
<ui-toggle class="integrations-item-toggle" v-if="model.isConfigured" v-model="model.isActivacted" @input="$emit('onActiveChange', model)" on-content="Active" off-content="Active" />
</main>
</div>
@@ -30,7 +29,7 @@
<script>
//import Overlay from 'zero/helpers/overlay.js';
import * as overlays from '../../services/overlay';
export default {
props: {
@@ -52,20 +51,19 @@
},
methods: {
open()
async open()
{
// open editing overlay
//return Overlay.open({
// component: () => import('./integration.vue'),
// display: 'editor',
// model: this.model.type,
// isCreate: !this.model.isConfigured,
// alias: this.model.type.alias,
// width: 960
//}).then(value =>
//{
// this.$emit('change', value);
//});
const result = await overlays.open({
component: () => import('./integration.vue'),
display: 'editor',
model: this.model,
width: 960
});
if (result.eventType === 'confirm')
{
this.$emit('change', value);
}
}
}
}
@@ -80,6 +78,11 @@
grid-template-columns: auto 1fr;
gap: var(--padding);
align-items: flex-start;
> aside
{
align-self: stretch;
}
}
.integrations-item .ui-button.type-block
@@ -95,9 +98,14 @@
padding-top: var(--padding-m);
}
.integrations-item-button
{
margin-top: var(--padding-s);
}
.integrations-item-tags
{
margin-top: var(--padding-s);
margin-top: var(--padding-s);
}
.integrations-item-icon
@@ -106,7 +114,8 @@
justify-content: center;
align-items: center;
width: 120px;
height: 90px;
min-height: 90px;
height: 100%;
font-size: 22px;
text-align: center;
background: var(--color-box-nested);
@@ -123,7 +132,7 @@
{
line-height: 1.3;
color: var(--color-text-dim);
margin: 0.5em 0 0;
margin: 0;
max-width: 820px;
}
@@ -544,8 +544,10 @@
"errors": {
"typenotfound": "The integration type does not exist",
"multiplenotallowed": "Can not create multiple integrations per type",
"alreadycreated": "You have already created an integration for this type",
"notfound": "Could not find a matching integration",
"couldnotupdatestate": "Not updated"
"couldnotupdatestate": "Not updated",
"noaliasmatch": "The alias as part of the URL does not match the alias of the model"
},
"fields": {
}
@@ -20,6 +20,7 @@ public class ConfigurationModule : ZeroModule
services.AddTransient<IZeroOptions, ZeroOptions>(factory => factory.GetService<IOptions<ZeroOptions>>().Value);
services.AddScoped<IIntegrationTypeService, IntegrationTypeService>();
services.AddScoped<IIntegrationStore, IntegrationStore>();
services.AddOptions<FeatureOptions>().Bind(configuration.GetSection("Zero:Features"));
services.AddOptions<IntegrationOptions>();
@@ -1,11 +1,13 @@
namespace zero.Configuration;
using System.Text.Json.Serialization;
namespace zero.Configuration;
/// <summary>
/// An integration is an application part which has a public configuration per app.
/// It's up to the user to provide functionality.
/// </summary>
[RavenCollection("Integrations")]
public class IntegrationModel : ZeroEntity
public class Integration : ZeroEntity
{
/// <inheritdoc />
public string TypeAlias { get; set; }
@@ -4,7 +4,7 @@ namespace zero.Configuration;
public class IntegrationOptions : List<IntegrationType>
{
public void Add<T>(string alias, string name, string description, string editorAlias = null, List<string> tags = default, string imagePath = null, IValidator validator = null) where T : IntegrationModel, new()
public void Add<T>(string alias, string name, string description, string editorAlias = null, List<string> tags = default, string imagePath = null, IValidator validator = null) where T : Integration , new()
{
Add(new IntegrationType(typeof(T))
{
@@ -22,6 +22,11 @@ public class IntegrationOptions : List<IntegrationType>
public void Add(Type type, string alias, string name, string description, string editorAlias = null, List<string> tags = default, string imagePath = null, IValidator validator = null)
{
if (!typeof(Integration).IsAssignableFrom(type))
{
throw new ArgumentException("Type has to inherit the Integration base model", nameof(type));
}
Add(new IntegrationType(type)
{
Alias = alias,
@@ -30,7 +35,7 @@ public class IntegrationOptions : List<IntegrationType>
ImagePath = imagePath,
Tags = tags,
Validator = validator,
Construct = cfg => Activator.CreateInstance(type),
Construct = cfg => Activator.CreateInstance(type) as Integration,
EditorAlias = editorAlias
});
}
@@ -1,14 +0,0 @@
//using Microsoft.Extensions.Logging;
//namespace zero.Configuration;
//public class IntegrationService : IntegrationsCollection, IIntegrationService
//{
// public IntegrationService(IStoreContext<Integration> context, ILogger<IntegrationsCollection> logger) : base(context, logger)
// {
// Options = new(false);
// }
//}
//public interface IIntegrationService : IIntegrationsCollection { }
@@ -0,0 +1,170 @@
using FluentValidation.Results;
namespace zero.Configuration;
public class IntegrationStore : IIntegrationStore
{
protected IStoreOperations Operations { get; private set; }
protected IIntegrationTypeService IntegrationTypes { get; private set; }
public IntegrationStore(IStoreContext context, IIntegrationTypeService integrationTypes)
{
IntegrationTypes = integrationTypes;
Operations = new StoreOperations(context.Context, context.Interceptors, context.Options, new StoreConfig() { IncludeInactive = true });
}
/// <inheritdoc />
public virtual Task<Integration> Empty(string alias)
{
IntegrationType type = IntegrationTypes.GetByAlias(alias);
Integration integration = type?.Construct(type);
if (integration != null)
{
integration.TypeAlias = alias;
}
return Task.FromResult(integration);
}
/// <inheritdoc />
public virtual async Task<Integration> Load(string alias)
{
Paged<Integration> result = await Operations.Load<Integration>(1, 1, q => q.Where(x => x.TypeAlias == alias));
return result.Items.FirstOrDefault();
}
/// <inheritdoc />
public virtual string GetChangeToken(Integration model) => Operations.GetChangeToken(model);
/// <inheritdoc />
public virtual async Task<Result<Integration>> Create(Integration model) => await Save(model, true);
/// <inheritdoc />
public virtual async Task<Result<Integration>> Update(Integration model) => await Save(model, false);
/// <inheritdoc />
public virtual async Task<Result<Integration>> Activate(string alias)
{
Integration model = await Load(alias);
if (model != null)
{
model.IsActive = true;
}
return await Update(model);
}
/// <inheritdoc />
public virtual async Task<Result<Integration>> Deactivate(string alias)
{
Integration model = await Load(alias);
if (model != null)
{
model.IsActive = false;
}
return await Update(model);
}
/// <inheritdoc />
public async Task<Result<Integration>> Delete(string alias)
{
Integration integration = await Load(alias);
return await Operations.Delete(integration);
}
// <inheritdoc />
protected virtual async Task<ValidationResult> Validate(Integration model)
{
await Task.Delay(0);
return new ValidationResult();
//ZeroValidator<T> validator = new();
//ValidationRules(validator);
//return await validator.ValidateAsync(model);
//return base.Validate(model);
}
/// <inheritdoc />
protected virtual async Task<Result<Integration>> Save(Integration model, bool create = false)
{
if (model == null)
{
return Result<Integration>.Fail("@integration.errors.notfound");
}
IntegrationType type = IntegrationTypes.GetByAlias(model.TypeAlias);
if (type == null)
{
return Result<Integration>.Fail("@integration.errors.typenotfound");
}
if (create && await Operations.Any<Integration>(q => q.Where(x => x.TypeAlias == model.TypeAlias)))
{
return Result<Integration>.Fail("@integration.errors.multiplenotallowed");
}
if (!create && await Operations.Any<Integration>(q => q.Where(x => x.TypeAlias == model.TypeAlias && x.Id != model.Id)))
{
return Result<Integration>.Fail("@integration.errors.alreadycreated");
}
return await Operations.Create(model, async x => await Validate(x));
}
}
public interface IIntegrationStore
{
/// <summary>
/// Get new instance of an integration by integration alias
/// </summary>
Task<Integration> Empty(string alias);
/// <summary>
/// Get an integration by integration alias
/// </summary>
Task<Integration> Load(string alias);
/// <summary>
/// Get the change vector for a model (Proxy to IAsyncDocumentSession.GetChangeVectorFor<>)
/// </summary>
string GetChangeToken(Integration model);
/// <summary>
/// Creates a new integration
/// </summary>
Task<Result<Integration>> Create(Integration model);
/// <summary>
/// Updates an integration
/// </summary>
Task<Result<Integration>> Update(Integration model);
/// <summary>
/// Activates a configured integration
/// </summary>
Task<Result<Integration>> Activate(string alias);
/// <summary>
/// Disables a configured integration
/// </summary>
Task<Result<Integration>> Deactivate(string alias);
/// <summary>
/// Deletes configuration of an integration and disables it
/// </summary>
Task<Result<Integration>> Delete(string alias);
}
@@ -52,7 +52,7 @@ public class IntegrationType
public IValidator Validator { get; set; }
[JsonIgnore]
public Func<FlavorConfig, object> Construct { get; set; }
public Func<IntegrationType, Integration> Construct { get; set; }
public IntegrationType(Type type)
{
@@ -32,7 +32,7 @@ public class IntegrationTypeService : IIntegrationTypeService
/// <inheritdoc />
public IntegrationType GetByType<T>() where T : IntegrationModel
public IntegrationType GetByType<T>() where T : Integration
{
Type type = typeof(T);
return Types.FirstOrDefault(x => x.ModelType == type);
@@ -55,5 +55,5 @@ public interface IIntegrationTypeService
/// <summary>
/// Get a specific integration type by model type.
/// </summary>
IntegrationType GetByType<T>() where T : IntegrationModel;
IntegrationType GetByType<T>() where T : Integration;
}
+8
View File
@@ -41,6 +41,14 @@ public partial class StoreOperations : IStoreOperations
}
/// <inheritdoc />
public virtual async Task<bool> Any<T>(Func<IRavenQueryable<T>, IQueryable<T>> querySelector = default) where T : ZeroIdEntity, new()
{
querySelector ??= x => x;
return await querySelector(Session.Query<T>()).AnyAsync();
}
/// <inheritdoc />
public virtual async Task<Paged<T>> Load<T>(int pageNumber, int pageSize, Func<IRavenQueryable<T>, IQueryable<T>> querySelector = default) where T : ZeroIdEntity, new()
{
+5
View File
@@ -141,6 +141,11 @@ public interface IStoreOperations
/// </summary>
T PrepareForSave<T>(T model) where T : ZeroIdEntity;
/// <summary>
/// Check if any items exist in this collection (with optional query)
/// </summary>
Task<bool> Any<T>(Func<IRavenQueryable<T>, IQueryable<T>> querySelector = default) where T : ZeroIdEntity, new();
/// <summary>
/// Get an entity by Id
/// </summary>
@@ -0,0 +1,54 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace zero.Utils;
public class JsonFlavorVariantConverter : JsonConverter<ISupportsFlavors>
{
public JsonFlavorVariantConverter() : base()
{
}
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return typeof(ISupportsFlavors).IsAssignableFrom(typeToConvert);
}
public override ISupportsFlavors Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
Utf8JsonReader variantReader = reader;
string flavor = null;
while (variantReader.Read())
{
if (variantReader.TokenType == JsonTokenType.PropertyName)
{
string property = variantReader.GetString();
if (property == "Flavor")
{
variantReader.Read();
flavor = variantReader.GetString();
break;
}
}
}
while (reader.Read()) { }
return new ZeroEntity() { Flavor = flavor };
}
public override void Write(Utf8JsonWriter writer, ISupportsFlavors value, JsonSerializerOptions options)
{
JsonSerializerOptions newOptions = new(options);
JsonConverter toRemove = newOptions.GetConverter(typeof(JsonFlavorVariantConverter));
newOptions.Converters.Remove(toRemove);
JsonSerializer.Serialize(writer, value, value.GetType(), newOptions);
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
namespace zero.Demo
{
public class FathomAnalyticsIntegration : IntegrationModel
public class FathomAnalyticsIntegration : Integration
{
/// <summary>
/// ID of the site in Fathom
+1 -1
View File
@@ -2,7 +2,7 @@
namespace zero.Demo
{
public class GoogleAnalyticsIntegration : IntegrationModel
public class GoogleAnalyticsIntegration : Integration
{
/// <summary>
/// Provided tracking ID from Google
+16
View File
@@ -1,8 +1,11 @@
using Microsoft.AspNetCore.Mvc;
using Raven.Client.Documents.Indexes;
using System.Text.Json;
using zero.Configuration;
using zero.Context;
using zero.Persistence;
using zero.Routing;
using zero.Utils;
namespace zero.Demo.Controllers;
@@ -40,6 +43,19 @@ public class SetupController : Controller
//}
[HttpGet("/api/setup/json")]
public async Task<IActionResult> TestJson([FromServices] IIntegrationStore store)
{
Integration integration = await store.Load("analytics.fathom");
JsonFlavorVariantConverter converter = new();
JsonSerializerOptions opts = new();
opts.Converters.Add(converter);
return Content(JsonSerializer.Serialize(integration, opts), "application/json");
}
[HttpGet("/api/setup/indexes")]
public async Task<IActionResult> Indexes([FromServices] IZeroStore store, [FromServices] IZeroContext context)
{