mirror of https://github.com/Squidex/squidex.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
539 lines
17 KiB
539 lines
17 KiB
// ==========================================================================
|
|
// Squidex Headless CMS
|
|
// ==========================================================================
|
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
// All rights reserved. Licensed under the MIT license.
|
|
// ==========================================================================
|
|
|
|
using System.Collections;
|
|
using System.Globalization;
|
|
using System.Reflection;
|
|
using System.Security.Claims;
|
|
using System.Text.RegularExpressions;
|
|
using NodaTime;
|
|
using Squidex.Domain.Apps.Core.Assets;
|
|
using Squidex.Domain.Apps.Core.Contents;
|
|
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
|
|
using Squidex.Infrastructure;
|
|
using Squidex.Infrastructure.Collections;
|
|
using Squidex.Infrastructure.Queries;
|
|
using Squidex.Infrastructure.Reflection.Internal;
|
|
using Squidex.Shared.Users;
|
|
using Squidex.Text;
|
|
|
|
namespace Squidex.Domain.Apps.Core.Scripting;
|
|
|
|
public sealed partial class ScriptingCompleter
|
|
{
|
|
private readonly IEnumerable<IScriptDescriptor> descriptors;
|
|
private static readonly FilterSchema DynamicData = new FilterSchema(FilterSchemaType.Object)
|
|
{
|
|
Fields = ReadonlyList.Create(
|
|
new FilterField(new FilterSchema(FilterSchemaType.Object), "my-field"),
|
|
new FilterField(FilterSchema.String, "my-field.iv"))
|
|
};
|
|
|
|
public ScriptingCompleter(IEnumerable<IScriptDescriptor> descriptors)
|
|
{
|
|
this.descriptors = descriptors;
|
|
}
|
|
|
|
public IReadOnlyList<ScriptingValue> Trigger(string type)
|
|
{
|
|
Guard.NotNull(type);
|
|
|
|
switch (type)
|
|
{
|
|
case "AssetChanged":
|
|
return AssetTrigger();
|
|
case "Comment":
|
|
return CommentTrigger();
|
|
case "ContentChanged":
|
|
return ContentTrigger(DynamicData);
|
|
case "SchemaChanged":
|
|
return SchemaTrigger();
|
|
case "Usage":
|
|
return UsageTrigger();
|
|
default:
|
|
return new List<ScriptingValue>();
|
|
}
|
|
}
|
|
|
|
public IReadOnlyList<ScriptingValue> ContentScript(FilterSchema dataSchema)
|
|
{
|
|
Guard.NotNull(dataSchema);
|
|
|
|
return new Process(descriptors, dataSchema.Flatten()).ContentScript();
|
|
}
|
|
|
|
public IReadOnlyList<ScriptingValue> ContentTrigger(FilterSchema dataSchema)
|
|
{
|
|
Guard.NotNull(dataSchema);
|
|
|
|
return new Process(descriptors, dataSchema.Flatten()).ContentTrigger();
|
|
}
|
|
|
|
public IReadOnlyList<ScriptingValue> FieldRule(FilterSchema dataSchema)
|
|
{
|
|
Guard.NotNull(dataSchema);
|
|
|
|
return new Process(descriptors, dataSchema.Flatten()).FieldRule();
|
|
}
|
|
|
|
public IReadOnlyList<ScriptingValue> PreviewUrl(FilterSchema dataSchema)
|
|
{
|
|
Guard.NotNull(dataSchema);
|
|
|
|
return new Process(descriptors, dataSchema.Flatten()).PreviewUrl();
|
|
}
|
|
|
|
public IReadOnlyList<ScriptingValue> AssetScript()
|
|
{
|
|
return new Process(descriptors).AssetScript();
|
|
}
|
|
|
|
public IReadOnlyList<ScriptingValue> AssetTrigger()
|
|
{
|
|
return new Process(descriptors).AssetTrigger();
|
|
}
|
|
|
|
public IReadOnlyList<ScriptingValue> CommentTrigger()
|
|
{
|
|
return new Process(descriptors).CommentTrigger();
|
|
}
|
|
|
|
public IReadOnlyList<ScriptingValue> SchemaTrigger()
|
|
{
|
|
return new Process(descriptors).SchemaTrigger();
|
|
}
|
|
|
|
public IReadOnlyList<ScriptingValue> UsageTrigger()
|
|
{
|
|
return new Process(descriptors).UsageTrigger();
|
|
}
|
|
|
|
private sealed partial class Process
|
|
{
|
|
private static readonly Regex RegexProperty = BuildPropertyRegex();
|
|
private readonly Stack<string> prefixes = new Stack<string>();
|
|
private readonly Dictionary<string, ScriptingValue> result = new Dictionary<string, ScriptingValue>();
|
|
private readonly IEnumerable<IScriptDescriptor> descriptors;
|
|
private readonly FilterSchema? dataSchema;
|
|
|
|
public Process(IEnumerable<IScriptDescriptor> descriptors, FilterSchema? dataSchema = null)
|
|
{
|
|
this.descriptors = descriptors;
|
|
this.dataSchema = dataSchema;
|
|
}
|
|
|
|
public IReadOnlyList<ScriptingValue> SchemaTrigger()
|
|
{
|
|
AddHelpers(ScriptScope.SchemaTrigger | ScriptScope.Async);
|
|
|
|
AddObject("event", FieldDescriptions.Event, () =>
|
|
{
|
|
AddType(typeof(EnrichedSchemaEvent));
|
|
});
|
|
|
|
return Build();
|
|
}
|
|
|
|
public IReadOnlyList<ScriptingValue> CommentTrigger()
|
|
{
|
|
AddHelpers(ScriptScope.CommentTrigger | ScriptScope.Async);
|
|
|
|
AddObject("event", FieldDescriptions.Event, () =>
|
|
{
|
|
AddType(typeof(EnrichedCommentEvent));
|
|
});
|
|
|
|
return Build();
|
|
}
|
|
|
|
public IReadOnlyList<ScriptingValue> UsageTrigger()
|
|
{
|
|
AddHelpers(ScriptScope.UsageTrigger | ScriptScope.Async);
|
|
|
|
AddObject("event", FieldDescriptions.Event, () =>
|
|
{
|
|
AddType(typeof(EnrichedUsageExceededEvent));
|
|
});
|
|
|
|
return Build();
|
|
}
|
|
|
|
public IReadOnlyList<ScriptingValue> ContentScript()
|
|
{
|
|
AddHelpers(ScriptScope.ContentScript | ScriptScope.Transform | ScriptScope.Async);
|
|
|
|
AddObject("ctx", FieldDescriptions.Context, () =>
|
|
{
|
|
AddType(typeof(ContentScriptVars));
|
|
});
|
|
|
|
return Build();
|
|
}
|
|
|
|
public IReadOnlyList<ScriptingValue> ContentTrigger()
|
|
{
|
|
AddHelpers(ScriptScope.ContentTrigger | ScriptScope.Async);
|
|
|
|
AddObject("event", FieldDescriptions.Event, () =>
|
|
{
|
|
AddType(typeof(EnrichedContentEvent));
|
|
});
|
|
|
|
return Build();
|
|
}
|
|
|
|
public IReadOnlyList<ScriptingValue> AssetTrigger()
|
|
{
|
|
AddHelpers(ScriptScope.AssetTrigger | ScriptScope.Async);
|
|
|
|
AddObject("event", FieldDescriptions.Event, () =>
|
|
{
|
|
AddType(typeof(EnrichedAssetEvent));
|
|
});
|
|
|
|
return Build();
|
|
}
|
|
|
|
public IReadOnlyList<ScriptingValue> AssetScript()
|
|
{
|
|
AddHelpers(ScriptScope.AssetScript | ScriptScope.Async);
|
|
|
|
AddObject("ctx", FieldDescriptions.Event, () =>
|
|
{
|
|
AddType(typeof(AssetScriptVars));
|
|
});
|
|
|
|
return Build();
|
|
}
|
|
|
|
public IReadOnlyList<ScriptingValue> PreviewUrl()
|
|
{
|
|
AddString("id",
|
|
FieldDescriptions.EntityId);
|
|
|
|
AddNumber("version",
|
|
FieldDescriptions.EntityVersion);
|
|
|
|
AddString("accessToken",
|
|
FieldDescriptions.AccessToken);
|
|
|
|
AddObject("data", FieldDescriptions.ContentData, () =>
|
|
{
|
|
AddData();
|
|
});
|
|
|
|
return Build();
|
|
}
|
|
|
|
public IReadOnlyList<ScriptingValue> FieldRule()
|
|
{
|
|
AddObject("user", FieldDescriptions.User, () =>
|
|
{
|
|
AddString("id",
|
|
FieldDescriptions.UserId);
|
|
|
|
AddString("email",
|
|
FieldDescriptions.UserEmail);
|
|
|
|
AddString("displayName",
|
|
FieldDescriptions.UserDisplayName);
|
|
|
|
AddString("role",
|
|
FieldDescriptions.UserAppRole);
|
|
});
|
|
|
|
AddObject("data", FieldDescriptions.ContentData, () =>
|
|
{
|
|
AddData();
|
|
});
|
|
|
|
AddObject("itemData", FieldDescriptions.ItemData, () =>
|
|
{
|
|
AddAny("my-field", FieldDescriptions.ItemDataField);
|
|
});
|
|
|
|
return Build();
|
|
}
|
|
|
|
private IReadOnlyList<ScriptingValue> Build()
|
|
{
|
|
return result.Values.OrderBy(x => x.Path).ToList();
|
|
}
|
|
|
|
private void AddHelpers(ScriptScope scope)
|
|
{
|
|
foreach (var descriptor in descriptors)
|
|
{
|
|
descriptor.Describe(Add, scope);
|
|
}
|
|
}
|
|
|
|
private void AddType(Type type)
|
|
{
|
|
foreach (var (name, description, propertyTypeOrNullable) in GetFields(type))
|
|
{
|
|
var propertyType = Nullable.GetUnderlyingType(propertyTypeOrNullable) ?? propertyTypeOrNullable;
|
|
|
|
if (propertyType.IsEnum)
|
|
{
|
|
var allowedValues = Enum.GetValues(propertyType).OfType<object>().Select(x => x.ToString()!).Order().ToArray();
|
|
|
|
Add(JsonType.String, name, description, allowedValues);
|
|
}
|
|
else if (
|
|
propertyType == typeof(string) ||
|
|
propertyType == typeof(DomainId) ||
|
|
propertyType == typeof(Instant) ||
|
|
propertyType == typeof(Status))
|
|
{
|
|
AddString(name, description);
|
|
}
|
|
else if (propertyType == typeof(int) || propertyType == typeof(long))
|
|
{
|
|
AddNumber(name, description);
|
|
}
|
|
else if (propertyType == typeof(bool))
|
|
{
|
|
AddBoolean(name, description);
|
|
}
|
|
else if (typeof(MulticastDelegate).IsAssignableFrom(propertyType.BaseType))
|
|
{
|
|
AddFunction(name, description);
|
|
}
|
|
else if (propertyType == typeof(AssetMetadata))
|
|
{
|
|
AddObject(name, description, () =>
|
|
{
|
|
AddString("my-name",
|
|
FieldDescriptions.AssetMetadataValue);
|
|
});
|
|
}
|
|
else if (propertyType == typeof(NamedId<DomainId>))
|
|
{
|
|
AddObject(name, description, () =>
|
|
{
|
|
AddString("id",
|
|
FieldDescriptions.NamedId);
|
|
|
|
AddString("name",
|
|
FieldDescriptions.NamedName);
|
|
});
|
|
}
|
|
else if (propertyType == typeof(RefToken))
|
|
{
|
|
AddObject(name, description, () =>
|
|
{
|
|
AddString("identifier",
|
|
FieldDescriptions.ActorIdentifier);
|
|
|
|
AddString("type",
|
|
FieldDescriptions.ActorType);
|
|
});
|
|
}
|
|
else if (propertyType == typeof(IUser) || propertyType == typeof(ClaimsPrincipal))
|
|
{
|
|
AddObject(name, description, () =>
|
|
{
|
|
AddString("id",
|
|
FieldDescriptions.UserId);
|
|
|
|
AddString("email",
|
|
FieldDescriptions.UserEmail);
|
|
|
|
AddBoolean("isClient",
|
|
FieldDescriptions.UserIsClient);
|
|
|
|
AddBoolean("isUser",
|
|
FieldDescriptions.UserIsUser);
|
|
|
|
AddObject("claims", FieldDescriptions.UserClaims, () =>
|
|
{
|
|
AddArray("name",
|
|
FieldDescriptions.UsersClaimsValue);
|
|
});
|
|
});
|
|
}
|
|
else if (propertyType == typeof(ContentData) && dataSchema != null)
|
|
{
|
|
AddObject(name, description, () =>
|
|
{
|
|
AddData();
|
|
});
|
|
}
|
|
else if (GetFields(propertyType).Any())
|
|
{
|
|
AddObject(name, description, () =>
|
|
{
|
|
AddType(propertyType);
|
|
});
|
|
}
|
|
else if (propertyType.GetInterfaces().Contains(typeof(IEnumerable)))
|
|
{
|
|
AddArray(name, description);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<(string Name, string Description, Type Type)> GetFields(Type type)
|
|
{
|
|
foreach (var property in type.GetPublicProperties())
|
|
{
|
|
var descriptionKey = property.GetCustomAttribute<FieldDescriptionAttribute>()?.Name;
|
|
|
|
if (descriptionKey == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var description = FieldDescriptions.ResourceManager.GetString(descriptionKey, CultureInfo.InvariantCulture)!;
|
|
|
|
yield return (property.Name.ToCamelCase(), description, property.PropertyType);
|
|
}
|
|
}
|
|
|
|
private void AddData()
|
|
{
|
|
if (dataSchema?.Fields == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var field in dataSchema.Fields)
|
|
{
|
|
JsonType type = ConvertType(field);
|
|
|
|
Add(type, field.Path, field.Description, field.Schema.AllowedValues);
|
|
}
|
|
|
|
static JsonType ConvertType(FilterField field)
|
|
{
|
|
switch (field.Schema.Type)
|
|
{
|
|
case FilterSchemaType.Any:
|
|
return JsonType.Any;
|
|
case FilterSchemaType.Boolean:
|
|
return JsonType.Boolean;
|
|
case FilterSchemaType.DateTime:
|
|
return JsonType.String;
|
|
case FilterSchemaType.GeoObject:
|
|
return JsonType.Object;
|
|
case FilterSchemaType.Guid:
|
|
return JsonType.String;
|
|
case FilterSchemaType.Number:
|
|
return JsonType.Number;
|
|
case FilterSchemaType.Object:
|
|
return JsonType.Object;
|
|
case FilterSchemaType.ObjectArray:
|
|
return JsonType.Array;
|
|
case FilterSchemaType.String:
|
|
return JsonType.String;
|
|
case FilterSchemaType.StringArray:
|
|
return JsonType.Array;
|
|
default:
|
|
return JsonType.String;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void AddAny(string? name, string? description)
|
|
{
|
|
Add(JsonType.Any, name, description);
|
|
}
|
|
|
|
private void AddArray(string? name, string? description)
|
|
{
|
|
Add(JsonType.Array, name, description);
|
|
}
|
|
|
|
private void AddBoolean(string? name, string? description)
|
|
{
|
|
Add(JsonType.Boolean, name, description);
|
|
}
|
|
|
|
private void AddNumber(string? name, string? description)
|
|
{
|
|
Add(JsonType.Number, name, description);
|
|
}
|
|
|
|
private void AddFunction(string? name, string? description)
|
|
{
|
|
Add(JsonType.Function, name, description);
|
|
}
|
|
|
|
private void AddString(string? name, string? description)
|
|
{
|
|
Add(JsonType.String, name, description);
|
|
}
|
|
|
|
private void Add(JsonType type, string? name, string? description, string[]? allowedValues = null, string? decprecationReason = null)
|
|
{
|
|
var parts = name?.Split('.') ?? Array.Empty<string>();
|
|
|
|
foreach (var part in parts)
|
|
{
|
|
PushPrefix(part);
|
|
}
|
|
|
|
if (prefixes.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var path = string.Concat(prefixes.Reverse());
|
|
|
|
result[path] = new ScriptingValue(path, type)
|
|
{
|
|
AllowedValues = allowedValues,
|
|
Description = description,
|
|
DeprecationReason = decprecationReason
|
|
};
|
|
|
|
for (int i = 0; i < parts.Length; i++)
|
|
{
|
|
prefixes.Pop();
|
|
}
|
|
}
|
|
|
|
private void AddObject(string name, string description, Action inner)
|
|
{
|
|
Add(JsonType.Object, name, description);
|
|
|
|
var parts = name.Split('.');
|
|
|
|
foreach (var part in parts)
|
|
{
|
|
PushPrefix(part);
|
|
}
|
|
|
|
inner();
|
|
|
|
for (int i = 0; i < parts.Length; i++)
|
|
{
|
|
prefixes.Pop();
|
|
}
|
|
}
|
|
|
|
private void PushPrefix(string name)
|
|
{
|
|
if (prefixes.Count == 0)
|
|
{
|
|
prefixes.Push(name);
|
|
}
|
|
else if (RegexProperty.IsMatch(name))
|
|
{
|
|
prefixes.Push($".{name}");
|
|
}
|
|
else
|
|
{
|
|
prefixes.Push($"['{name}']");
|
|
}
|
|
}
|
|
|
|
[GeneratedRegex("^(?!\\d)[\\w$]+$", RegexOptions.Compiled)]
|
|
private static partial Regex BuildPropertyRegex();
|
|
}
|
|
}
|
|
|