feat(api): add account workspaces and signed content events
This commit is contained in:
1 parent
937b774a93
commit
7b8d61d1ba
63 files changed
+5407
-370
No files matched your search
@@ -0,0 +1,92 @@
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Nodes;
|
||||
using WebDrop.Api;
|
||||
using WebDrop.Core;
|
||||
|
||||
// Build-time reflection only; production serialization remains source-generated and AOT-safe.
|
||||
var root = Path.GetFullPath(args.FirstOrDefault(a => a != "--check") ?? ".");
|
||||
string[] sources = ["server/src/WebDrop.Api/ApiJson.cs", "server/src/WebDrop.Core/Dtos.cs", "server/src/WebDrop.Core/Settings.cs", "server/src/WebDrop.Core/Enums.cs", "server/src/WebDrop.Core/Workspace.cs", "server/src/WebDrop.Core/AccountProfile.cs", "server/src/WebDrop.Api/PasskeyDtos.cs", "server/src/WebDrop.Core/WorkspaceCrypto.cs", "server/src/WebDrop.Api/Endpoints.WorkspaceCrypto.cs"];
|
||||
var digest = Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(string.Join("", sources.Select(p => p + "\n" + File.ReadAllText(Path.Combine(root, p)).Replace("\r\n", "\n") + "\n")))));
|
||||
var pending = new SortedDictionary<string, Type>();
|
||||
var done = new HashSet<Type>();
|
||||
foreach (var entry in typeof(ApiJson).GetCustomAttributesData().Where(a => a.AttributeType == typeof(JsonSerializableAttribute))) Add((Type)entry.ConstructorArguments[0].Value!);
|
||||
var output = new StringBuilder($"// Generated from backend DTOs; do not edit.\n// backend-contract-sha256: {digest}\n\n");
|
||||
var nullable = new NullabilityInfoContext();
|
||||
var definitions = new JsonObject();
|
||||
while (pending.Count > 0)
|
||||
{
|
||||
var type = pending.First().Value;
|
||||
pending.Remove(type.Name);
|
||||
if (!done.Add(type)) continue;
|
||||
if (type.IsEnum)
|
||||
{
|
||||
var values = type.GetFields(BindingFlags.Public | BindingFlags.Static).Select(f => "'" + (f.GetCustomAttribute<JsonStringEnumMemberNameAttribute>()?.Name ?? f.Name) + "'");
|
||||
output.AppendLine($"export type {type.Name} = {string.Join(" | ", values)};\n");
|
||||
definitions[type.Name] = new JsonObject { ["type"] = "string", ["enum"] = new JsonArray(type.GetFields(BindingFlags.Public | BindingFlags.Static).Select(f => JsonValue.Create(f.GetCustomAttribute<JsonStringEnumMemberNameAttribute>()?.Name ?? f.Name) as JsonNode).ToArray()) };
|
||||
continue;
|
||||
}
|
||||
output.AppendLine($"export interface {type.Name} {{");
|
||||
var properties = new JsonObject();
|
||||
var required = new JsonArray();
|
||||
foreach (var prop in type.GetProperties().OrderBy(p => p.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name ?? p.Name, StringComparer.Ordinal))
|
||||
{
|
||||
var ignore = prop.GetCustomAttribute<JsonIgnoreAttribute>();
|
||||
if (ignore?.Condition == JsonIgnoreCondition.Always) continue;
|
||||
var jsonName = prop.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name;
|
||||
if (jsonName is null) throw new InvalidOperationException($"Wire property needs an explicit JSON name: {type.Name}.{prop.Name}");
|
||||
var isNullable = Nullable.GetUnderlyingType(prop.PropertyType) is not null || nullable.Create(prop).ReadState == NullabilityState.Nullable;
|
||||
var optional = isNullable && ignore?.Condition != JsonIgnoreCondition.Never;
|
||||
output.AppendLine($" {jsonName}{(optional ? "?" : "")}: {Ts(prop.PropertyType)}{(isNullable ? " | null" : "")};");
|
||||
properties[jsonName] = isNullable ? new JsonObject { ["anyOf"] = new JsonArray(Schema(prop.PropertyType), new JsonObject { ["type"] = "null" }) } : Schema(prop.PropertyType);
|
||||
if (!optional) required.Add(jsonName);
|
||||
}
|
||||
output.AppendLine("}");
|
||||
definitions[type.Name] = new JsonObject { ["type"] = "object", ["properties"] = properties, ["required"] = required };
|
||||
}
|
||||
var path = Path.Combine(root, "web/src/core-bridge/generated/api.ts");
|
||||
var text = output.ToString().Replace("\r\n", "\n");
|
||||
if (args.Contains("--check"))
|
||||
{
|
||||
if (!File.Exists(path) || File.ReadAllText(path).Replace("\r\n", "\n") != text) throw new InvalidOperationException("Generated API types are stale. Run ContractGenerator.");
|
||||
}
|
||||
else { Directory.CreateDirectory(Path.GetDirectoryName(path)!); File.WriteAllText(path, text, new UTF8Encoding(false)); }
|
||||
var schemaPath = Path.Combine(root, "contracts/api.schemas.json");
|
||||
var schemaText = new JsonObject { ["$schema"] = "https://json-schema.org/draft/2020-12/schema", ["x-backend-contract-sha256"] = digest, ["$defs"] = definitions }.ToJsonString(new() { WriteIndented = true }).Replace("\r\n", "\n") + "\n";
|
||||
if (args.Contains("--check"))
|
||||
{
|
||||
if (!File.Exists(schemaPath) || File.ReadAllText(schemaPath).Replace("\r\n", "\n") != schemaText) throw new InvalidOperationException("Generated API schemas are stale.");
|
||||
}
|
||||
else File.WriteAllText(schemaPath, schemaText, new UTF8Encoding(false));
|
||||
Console.WriteLine($"Backend API types {(args.Contains("--check") ? "verified" : "generated")}.");
|
||||
|
||||
void Add(Type type)
|
||||
{
|
||||
type = Nullable.GetUnderlyingType(type) ?? type;
|
||||
if (type.IsArray) { Add(type.GetElementType()!); return; }
|
||||
if (type.IsGenericType) { foreach (var arg in type.GetGenericArguments()) Add(arg); return; }
|
||||
if ((type.Namespace is "WebDrop.Api" or "WebDrop.Core") && !done.Contains(type)) pending[type.Name] = type;
|
||||
}
|
||||
string Ts(Type type)
|
||||
{
|
||||
type = Nullable.GetUnderlyingType(type) ?? type;
|
||||
if (type == typeof(string)) return "string";
|
||||
if (type == typeof(bool)) return "boolean";
|
||||
if (type.IsPrimitive || type == typeof(decimal)) return "number";
|
||||
if (type.IsArray) { Add(type.GetElementType()!); return Ts(type.GetElementType()!) + "[]"; }
|
||||
if (type.IsGenericType && type.GetGenericArguments().Length == 1) { var item = type.GetGenericArguments()[0]; Add(item); return Ts(item) + "[]"; }
|
||||
Add(type);
|
||||
return type.Name;
|
||||
}
|
||||
JsonObject Schema(Type type)
|
||||
{
|
||||
type = Nullable.GetUnderlyingType(type) ?? type;
|
||||
if (type == typeof(string)) return new() { ["type"] = "string" };
|
||||
if (type == typeof(bool)) return new() { ["type"] = "boolean" };
|
||||
if (type.IsPrimitive || type == typeof(decimal)) return new() { ["type"] = type == typeof(float) || type == typeof(double) || type == typeof(decimal) ? "number" : "integer" };
|
||||
if (type.IsArray) return new() { ["type"] = "array", ["items"] = Schema(type.GetElementType()!) };
|
||||
if (type.IsGenericType && type.GetGenericArguments().Length == 1) return new() { ["type"] = "array", ["items"] = Schema(type.GetGenericArguments()[0]) };
|
||||
return new() { ["$ref"] = "#/$defs/" + type.Name };
|
||||
}
|
||||
Reference in new issue
Block a user