Skip to content

feat(surrealdb): ensures ns/db are created in strict mode #790

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,34 @@

var builder = DistributedApplication.CreateBuilder(args);

var db = builder.AddSurrealServer("surreal")
bool strictMode = true;

var db = builder.AddSurrealServer("surreal", strictMode: strictMode)
.WithSurrealist()
.AddNamespace("ns")
.AddDatabase("db");

if (strictMode)
{
db.WithCreationScript(
$"""
DEFINE DATABASE IF NOT EXISTS {nameof(db)};
USE DATABASE {nameof(db)};
DEFINE TABLE todo;
DEFINE FIELD title ON todo TYPE string;
DEFINE FIELD dueBy ON todo TYPE datetime;
DEFINE FIELD isComplete ON todo TYPE bool;
DEFINE TABLE weatherForecast;
DEFINE FIELD date ON weatherForecast TYPE datetime;
DEFINE FIELD country ON weatherForecast TYPE string;
DEFINE FIELD temperatureC ON weatherForecast TYPE number;
DEFINE FIELD summary ON weatherForecast TYPE string;
"""
);
}

builder.AddProject<CommunityToolkit_Aspire_Hosting_SurrealDb_ApiService>("apiservice")
.WithReference(db)
.WaitFor(db);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using CommunityToolkit.Aspire.SurrealDb;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using SurrealDb.Net;
using System.Text;
using System.Text.Json;
Expand Down Expand Up @@ -80,6 +81,7 @@ public static IResourceBuilder<SurrealDbServerResource> AddSurrealServer(
: SurrealDbContainerImageTags.Tag;

var surrealServer = new SurrealDbServerResource(name, userName?.Resource, passwordParameter);

return builder.AddResource(surrealServer)
.WithEndpoint(port: port, targetPort: SurrealDbPort, name: SurrealDbServerResource.PrimaryEndpointName)
.WithImage(SurrealDbContainerImageTags.Image, imageTag)
Expand All @@ -90,7 +92,47 @@ public static IResourceBuilder<SurrealDbServerResource> AddSurrealServer(
context.EnvironmentVariables[PasswordEnvVarName] = surrealServer.PasswordParameter;
})
.WithEntrypoint("/surreal")
.WithArgs([.. args]);
.WithArgs([.. args])
.OnResourceReady(async (_, @event, ct) =>
{
if (!strictMode)
{
return;
}

var connectionString = await surrealServer.GetConnectionStringAsync(ct).ConfigureAwait(false);
if (connectionString is null)
{
throw new DistributedApplicationException($"ResourceReadyEvent was published for the '{surrealServer.Name}' resource but the connection string was null.");
}

var options = new SurrealDbOptionsBuilder().FromConnectionString(connectionString).Build();
await using var surrealClient = new SurrealDbClient(options);

foreach (var nsResourceName in surrealServer.Namespaces.Keys)
{
if (builder.Resources.FirstOrDefault(n =>
string.Equals(n.Name, nsResourceName, StringComparison.OrdinalIgnoreCase)) is
SurrealDbNamespaceResource surrealDbNamespace)
{
await CreateNamespaceAsync(surrealClient, surrealDbNamespace, @event.Services, ct)
.ConfigureAwait(false);

await surrealClient.Use(surrealDbNamespace.NamespaceName, null!, ct).ConfigureAwait(false);

foreach (var dbResourceName in surrealDbNamespace.Databases.Keys)
{
if (builder.Resources.FirstOrDefault(n =>
string.Equals(n.Name, dbResourceName, StringComparison.OrdinalIgnoreCase)) is
SurrealDbDatabaseResource surrealDbDatabase)
{
await CreateDatabaseAsync(surrealClient, surrealDbDatabase, @event.Services, ct)
.ConfigureAwait(false);
}
}
}
}
});
}

/// <summary>
Expand Down Expand Up @@ -132,6 +174,25 @@ public static IResourceBuilder<SurrealDbNamespaceResource> AddNamespace(
var surrealServerNamespace = new SurrealDbNamespaceResource(name, namespaceName, builder.Resource);
return builder.ApplicationBuilder.AddResource(surrealServerNamespace);
}

/// <summary>
/// Defines the SQL script used to create the namespace.
/// </summary>
/// <param name="builder">The builder for the <see cref="SurrealDbNamespaceResource"/>.</param>
/// <param name="script">The SQL script used to create the namespace.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
/// <remarks>
/// <value>Default script is <code>DEFINE NAMESPACE IF NOT EXISTS `QUOTED_NAMESPACE_NAME`;</code></value>
/// </remarks>
public static IResourceBuilder<SurrealDbNamespaceResource> WithCreationScript(this IResourceBuilder<SurrealDbNamespaceResource> builder, string script)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(script);

builder.WithAnnotation(new SurrealDbCreateNamespaceScriptAnnotation(script));

return builder;
}

/// <summary>
/// Adds a SurrealDB database to the application model. This is a child resource of a <see cref="SurrealDbNamespaceResource"/>.
Expand Down Expand Up @@ -202,6 +263,25 @@ public static IResourceBuilder<SurrealDbDatabaseResource> AddDatabase(
return builder.ApplicationBuilder.AddResource(surrealServerDatabase)
.WithHealthCheck(healthCheckKey);
}

/// <summary>
/// Defines the SQL script used to create the database.
/// </summary>
/// <param name="builder">The builder for the <see cref="SurrealDbDatabaseResource"/>.</param>
/// <param name="script">The SQL script used to create the database.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
/// <remarks>
/// <value>Default script is <code>DEFINE DATABASE IF NOT EXISTS `QUOTED_DATABASE_NAME`;</code></value>
/// </remarks>
public static IResourceBuilder<SurrealDbDatabaseResource> WithCreationScript(this IResourceBuilder<SurrealDbDatabaseResource> builder, string script)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(script);

builder.WithAnnotation(new SurrealDbCreateDatabaseScriptAnnotation(script));

return builder;
}

/// <summary>
/// Adds a named volume for the data folder to a SurrealDB resource.
Expand Down Expand Up @@ -447,4 +527,62 @@ CancellationToken cancellationToken

return Encoding.UTF8.GetString(stream.ToArray());
}

private static async Task CreateNamespaceAsync(
SurrealDbClient surrealClient,
SurrealDbNamespaceResource namespaceResource,
IServiceProvider serviceProvider,
CancellationToken cancellationToken
)
{
var scriptAnnotation = namespaceResource.Annotations.OfType<SurrealDbCreateNamespaceScriptAnnotation>().LastOrDefault();

var logger = serviceProvider.GetRequiredService<ResourceLoggerService>().GetLogger(namespaceResource.Parent);
logger.LogDebug("Creating namespace '{NamespaceName}'", namespaceResource.NamespaceName);

try
{
var response = await surrealClient.RawQuery(
scriptAnnotation?.Script ?? $"DEFINE NAMESPACE IF NOT EXISTS `{namespaceResource.NamespaceName}`;",
cancellationToken: cancellationToken
).ConfigureAwait(false);

response.EnsureAllOks();

logger.LogDebug("Namespace '{NamespaceName}' created successfully", namespaceResource.NamespaceName);
}
catch (Exception e)
{
logger.LogError(e, "Failed to create namespace '{NamespaceName}'", namespaceResource.NamespaceName);
}
}

private static async Task CreateDatabaseAsync(
SurrealDbClient surrealClient,
SurrealDbDatabaseResource databaseResource,
IServiceProvider serviceProvider,
CancellationToken cancellationToken
)
{
var scriptAnnotation = databaseResource.Annotations.OfType<SurrealDbCreateDatabaseScriptAnnotation>().LastOrDefault();

var logger = serviceProvider.GetRequiredService<ResourceLoggerService>().GetLogger(databaseResource.Parent.Parent);
logger.LogDebug("Creating database '{DatabaseName}'", databaseResource.DatabaseName);

try
{
var response = await surrealClient.RawQuery(
scriptAnnotation?.Script ?? $"DEFINE DATABASE IF NOT EXISTS `{databaseResource.DatabaseName}`;",
cancellationToken: cancellationToken
).ConfigureAwait(false);

response.EnsureAllOks();

logger.LogDebug("Database '{DatabaseName}' created successfully", databaseResource.DatabaseName);
}
catch (Exception e)
{
logger.LogError(e, "Failed to create database '{DatabaseName}'", databaseResource.DatabaseName);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.ApplicationModel;

namespace Aspire.Hosting;

/// <summary>
/// Represents an annotation for defining a script to create a database in SurrealDB.
/// </summary>
internal sealed class SurrealDbCreateDatabaseScriptAnnotation : IResourceAnnotation
{
/// <summary>
/// Initializes a new instance of the <see cref="SurrealDbCreateDatabaseScriptAnnotation"/> class.
/// </summary>
/// <param name="script">The script used to create the database.</param>
public SurrealDbCreateDatabaseScriptAnnotation(string script)
{
ArgumentNullException.ThrowIfNull(script);
Script = script;
}

/// <summary>
/// Gets the script used to create the database.
/// </summary>
public string Script { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.ApplicationModel;

namespace Aspire.Hosting;

/// <summary>
/// Represents an annotation for defining a script to create a namespace in SurrealDB.
/// </summary>
internal sealed class SurrealDbCreateNamespaceScriptAnnotation : IResourceAnnotation
{
/// <summary>
/// Initializes a new instance of the <see cref="SurrealDbCreateNamespaceScriptAnnotation"/> class.
/// </summary>
/// <param name="script">The script used to create the namespace.</param>
public SurrealDbCreateNamespaceScriptAnnotation(string script)
{
ArgumentNullException.ThrowIfNull(script);
Script = script;
}

/// <summary>
/// Gets the script used to create the namespace.
/// </summary>
public string Script { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,55 @@ await pipeline.ExecuteAsync(async token =>
}
}
}

[Fact]
public async Task AddDatabaseCreatesDatabaseWithCustomScript()
{
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));

using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);

var surrealNsName = "ns1";
var surrealDbName = "db1";

var surreal = builder.AddSurrealServer("surreal", strictMode: true);

var db = surreal
.AddNamespace(surrealNsName)
.AddDatabase(surrealDbName)
.WithCreationScript(
$"""
DEFINE DATABASE IF NOT EXISTS {surrealDbName};
USE DATABASE {surrealDbName};
DEFINE TABLE todo;
DEFINE FIELD Title ON todo TYPE string;
DEFINE FIELD DueBy ON todo TYPE datetime;
DEFINE FIELD IsComplete ON todo TYPE bool;
"""
);

using var app = builder.Build();

await app.StartAsync(cts.Token);

var hb = Host.CreateApplicationBuilder();

hb.Configuration[$"ConnectionStrings:{db.Resource.Name}"] = await db.Resource.ConnectionStringExpression.GetValueAsync(default);

hb.AddSurrealClient(db.Resource.Name);

using var host = hb.Build();

await host.StartAsync();

await app.ResourceNotifications.WaitForResourceHealthyAsync(db.Resource.Name, cts.Token);

await using var client = host.Services.GetRequiredService<SurrealDbClient>();

await CreateTestData(client, cts.Token);
await AssertTestData(client, cts.Token);
}

private static async Task CreateTestData(SurrealDbClient surrealDbClient, CancellationToken ct)
{
Expand Down
Loading