Skip to content

Commit ce844a4

Browse files
committed
feat(surrealdb): ensures ns/db are created in strict mode
1 parent 8410e56 commit ce844a4

File tree

5 files changed

+266
-2
lines changed

5 files changed

+266
-2
lines changed

examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.AppHost/Program.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,34 @@
22

33
var builder = DistributedApplication.CreateBuilder(args);
44

5-
var db = builder.AddSurrealServer("surreal")
5+
bool strictMode = true;
6+
7+
var db = builder.AddSurrealServer("surreal", strictMode: strictMode)
68
.WithSurrealist()
79
.AddNamespace("ns")
810
.AddDatabase("db");
911

12+
if (strictMode)
13+
{
14+
db.WithCreationScript(
15+
$"""
16+
DEFINE DATABASE IF NOT EXISTS {nameof(db)};
17+
USE DATABASE {nameof(db)};
18+
19+
DEFINE TABLE todo;
20+
DEFINE FIELD title ON todo TYPE string;
21+
DEFINE FIELD dueBy ON todo TYPE datetime;
22+
DEFINE FIELD isComplete ON todo TYPE bool;
23+
24+
DEFINE TABLE weatherForecast;
25+
DEFINE FIELD date ON weatherForecast TYPE datetime;
26+
DEFINE FIELD country ON weatherForecast TYPE string;
27+
DEFINE FIELD temperatureC ON weatherForecast TYPE number;
28+
DEFINE FIELD summary ON weatherForecast TYPE string;
29+
"""
30+
);
31+
}
32+
1033
builder.AddProject<CommunityToolkit_Aspire_Hosting_SurrealDb_ApiService>("apiservice")
1134
.WithReference(db)
1235
.WaitFor(db);

src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealDbBuilderExtensions.cs

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using CommunityToolkit.Aspire.SurrealDb;
66
using Microsoft.Extensions.DependencyInjection;
77
using Microsoft.Extensions.Diagnostics.HealthChecks;
8+
using Microsoft.Extensions.Logging;
89
using SurrealDb.Net;
910
using System.Text;
1011
using System.Text.Json;
@@ -80,6 +81,7 @@ public static IResourceBuilder<SurrealDbServerResource> AddSurrealServer(
8081
: SurrealDbContainerImageTags.Tag;
8182

8283
var surrealServer = new SurrealDbServerResource(name, userName?.Resource, passwordParameter);
84+
8385
return builder.AddResource(surrealServer)
8486
.WithEndpoint(port: port, targetPort: SurrealDbPort, name: SurrealDbServerResource.PrimaryEndpointName)
8587
.WithImage(SurrealDbContainerImageTags.Image, imageTag)
@@ -90,7 +92,47 @@ public static IResourceBuilder<SurrealDbServerResource> AddSurrealServer(
9092
context.EnvironmentVariables[PasswordEnvVarName] = surrealServer.PasswordParameter;
9193
})
9294
.WithEntrypoint("/surreal")
93-
.WithArgs([.. args]);
95+
.WithArgs([.. args])
96+
.OnResourceReady(async (_, @event, ct) =>
97+
{
98+
if (!strictMode)
99+
{
100+
return;
101+
}
102+
103+
var connectionString = await surrealServer.GetConnectionStringAsync(ct).ConfigureAwait(false);
104+
if (connectionString is null)
105+
{
106+
throw new DistributedApplicationException($"ResourceReadyEvent was published for the '{surrealServer.Name}' resource but the connection string was null.");
107+
}
108+
109+
var options = new SurrealDbOptionsBuilder().FromConnectionString(connectionString).Build();
110+
await using var surrealClient = new SurrealDbClient(options);
111+
112+
foreach (var nsResourceName in surrealServer.Namespaces.Keys)
113+
{
114+
if (builder.Resources.FirstOrDefault(n =>
115+
string.Equals(n.Name, nsResourceName, StringComparison.OrdinalIgnoreCase)) is
116+
SurrealDbNamespaceResource surrealDbNamespace)
117+
{
118+
await CreateNamespaceAsync(surrealClient, surrealDbNamespace, @event.Services, ct)
119+
.ConfigureAwait(false);
120+
121+
await surrealClient.Use(surrealDbNamespace.NamespaceName, null!, ct).ConfigureAwait(false);
122+
123+
foreach (var dbResourceName in surrealDbNamespace.Databases.Keys)
124+
{
125+
if (builder.Resources.FirstOrDefault(n =>
126+
string.Equals(n.Name, dbResourceName, StringComparison.OrdinalIgnoreCase)) is
127+
SurrealDbDatabaseResource surrealDbDatabase)
128+
{
129+
await CreateDatabaseAsync(surrealClient, surrealDbDatabase, @event.Services, ct)
130+
.ConfigureAwait(false);
131+
}
132+
}
133+
}
134+
}
135+
});
94136
}
95137

96138
/// <summary>
@@ -132,6 +174,25 @@ public static IResourceBuilder<SurrealDbNamespaceResource> AddNamespace(
132174
var surrealServerNamespace = new SurrealDbNamespaceResource(name, namespaceName, builder.Resource);
133175
return builder.ApplicationBuilder.AddResource(surrealServerNamespace);
134176
}
177+
178+
/// <summary>
179+
/// Defines the SQL script used to create the namespace.
180+
/// </summary>
181+
/// <param name="builder">The builder for the <see cref="SurrealDbNamespaceResource"/>.</param>
182+
/// <param name="script">The SQL script used to create the namespace.</param>
183+
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
184+
/// <remarks>
185+
/// <value>Default script is <code>DEFINE NAMESPACE IF NOT EXISTS `QUOTED_NAMESPACE_NAME`;</code></value>
186+
/// </remarks>
187+
public static IResourceBuilder<SurrealDbNamespaceResource> WithCreationScript(this IResourceBuilder<SurrealDbNamespaceResource> builder, string script)
188+
{
189+
ArgumentNullException.ThrowIfNull(builder);
190+
ArgumentNullException.ThrowIfNull(script);
191+
192+
builder.WithAnnotation(new SurrealDbCreateNamespaceScriptAnnotation(script));
193+
194+
return builder;
195+
}
135196

136197
/// <summary>
137198
/// Adds a SurrealDB database to the application model. This is a child resource of a <see cref="SurrealDbNamespaceResource"/>.
@@ -202,6 +263,25 @@ public static IResourceBuilder<SurrealDbDatabaseResource> AddDatabase(
202263
return builder.ApplicationBuilder.AddResource(surrealServerDatabase)
203264
.WithHealthCheck(healthCheckKey);
204265
}
266+
267+
/// <summary>
268+
/// Defines the SQL script used to create the database.
269+
/// </summary>
270+
/// <param name="builder">The builder for the <see cref="SurrealDbDatabaseResource"/>.</param>
271+
/// <param name="script">The SQL script used to create the database.</param>
272+
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
273+
/// <remarks>
274+
/// <value>Default script is <code>DEFINE DATABASE IF NOT EXISTS `QUOTED_DATABASE_NAME`;</code></value>
275+
/// </remarks>
276+
public static IResourceBuilder<SurrealDbDatabaseResource> WithCreationScript(this IResourceBuilder<SurrealDbDatabaseResource> builder, string script)
277+
{
278+
ArgumentNullException.ThrowIfNull(builder);
279+
ArgumentNullException.ThrowIfNull(script);
280+
281+
builder.WithAnnotation(new SurrealDbCreateDatabaseScriptAnnotation(script));
282+
283+
return builder;
284+
}
205285

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

448528
return Encoding.UTF8.GetString(stream.ToArray());
449529
}
530+
531+
private static async Task CreateNamespaceAsync(
532+
SurrealDbClient surrealClient,
533+
SurrealDbNamespaceResource namespaceResource,
534+
IServiceProvider serviceProvider,
535+
CancellationToken cancellationToken
536+
)
537+
{
538+
var scriptAnnotation = namespaceResource.Annotations.OfType<SurrealDbCreateNamespaceScriptAnnotation>().LastOrDefault();
539+
540+
var logger = serviceProvider.GetRequiredService<ResourceLoggerService>().GetLogger(namespaceResource.Parent);
541+
logger.LogDebug("Creating namespace '{NamespaceName}'", namespaceResource.NamespaceName);
542+
543+
try
544+
{
545+
var response = await surrealClient.RawQuery(
546+
scriptAnnotation?.Script ?? $"DEFINE NAMESPACE IF NOT EXISTS `{namespaceResource.NamespaceName}`;",
547+
cancellationToken: cancellationToken
548+
).ConfigureAwait(false);
549+
550+
response.EnsureAllOks();
551+
552+
logger.LogDebug("Namespace '{NamespaceName}' created successfully", namespaceResource.NamespaceName);
553+
}
554+
catch (Exception e)
555+
{
556+
logger.LogError(e, "Failed to create namespace '{NamespaceName}'", namespaceResource.NamespaceName);
557+
}
558+
}
559+
560+
private static async Task CreateDatabaseAsync(
561+
SurrealDbClient surrealClient,
562+
SurrealDbDatabaseResource databaseResource,
563+
IServiceProvider serviceProvider,
564+
CancellationToken cancellationToken
565+
)
566+
{
567+
var scriptAnnotation = databaseResource.Annotations.OfType<SurrealDbCreateDatabaseScriptAnnotation>().LastOrDefault();
568+
569+
var logger = serviceProvider.GetRequiredService<ResourceLoggerService>().GetLogger(databaseResource.Parent.Parent);
570+
logger.LogDebug("Creating database '{DatabaseName}'", databaseResource.DatabaseName);
571+
572+
try
573+
{
574+
var response = await surrealClient.RawQuery(
575+
scriptAnnotation?.Script ?? $"DEFINE DATABASE IF NOT EXISTS `{databaseResource.DatabaseName}`;",
576+
cancellationToken: cancellationToken
577+
).ConfigureAwait(false);
578+
579+
response.EnsureAllOks();
580+
581+
logger.LogDebug("Database '{DatabaseName}' created successfully", databaseResource.DatabaseName);
582+
}
583+
catch (Exception e)
584+
{
585+
logger.LogError(e, "Failed to create database '{DatabaseName}'", databaseResource.DatabaseName);
586+
}
587+
}
450588
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Hosting.ApplicationModel;
5+
6+
namespace Aspire.Hosting;
7+
8+
/// <summary>
9+
/// Represents an annotation for defining a script to create a database in SurrealDB.
10+
/// </summary>
11+
internal sealed class SurrealDbCreateDatabaseScriptAnnotation : IResourceAnnotation
12+
{
13+
/// <summary>
14+
/// Initializes a new instance of the <see cref="SurrealDbCreateDatabaseScriptAnnotation"/> class.
15+
/// </summary>
16+
/// <param name="script">The script used to create the database.</param>
17+
public SurrealDbCreateDatabaseScriptAnnotation(string script)
18+
{
19+
ArgumentNullException.ThrowIfNull(script);
20+
Script = script;
21+
}
22+
23+
/// <summary>
24+
/// Gets the script used to create the database.
25+
/// </summary>
26+
public string Script { get; }
27+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Hosting.ApplicationModel;
5+
6+
namespace Aspire.Hosting;
7+
8+
/// <summary>
9+
/// Represents an annotation for defining a script to create a namespace in SurrealDB.
10+
/// </summary>
11+
internal sealed class SurrealDbCreateNamespaceScriptAnnotation : IResourceAnnotation
12+
{
13+
/// <summary>
14+
/// Initializes a new instance of the <see cref="SurrealDbCreateNamespaceScriptAnnotation"/> class.
15+
/// </summary>
16+
/// <param name="script">The script used to create the namespace.</param>
17+
public SurrealDbCreateNamespaceScriptAnnotation(string script)
18+
{
19+
ArgumentNullException.ThrowIfNull(script);
20+
Script = script;
21+
}
22+
23+
/// <summary>
24+
/// Gets the script used to create the namespace.
25+
/// </summary>
26+
public string Script { get; }
27+
}

tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/SurrealDbFunctionalTests.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,55 @@ await pipeline.ExecuteAsync(async token =>
334334
}
335335
}
336336
}
337+
338+
[Fact]
339+
public async Task AddDatabaseCreatesDatabaseWithCustomScript()
340+
{
341+
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
342+
343+
using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
344+
345+
var surrealNsName = "ns1";
346+
var surrealDbName = "db1";
347+
348+
var surreal = builder.AddSurrealServer("surreal", strictMode: true);
349+
350+
var db = surreal
351+
.AddNamespace(surrealNsName)
352+
.AddDatabase(surrealDbName)
353+
.WithCreationScript(
354+
$"""
355+
DEFINE DATABASE IF NOT EXISTS {surrealDbName};
356+
USE DATABASE {surrealDbName};
357+
358+
DEFINE TABLE todo;
359+
DEFINE FIELD Title ON todo TYPE string;
360+
DEFINE FIELD DueBy ON todo TYPE datetime;
361+
DEFINE FIELD IsComplete ON todo TYPE bool;
362+
"""
363+
);
364+
365+
using var app = builder.Build();
366+
367+
await app.StartAsync(cts.Token);
368+
369+
var hb = Host.CreateApplicationBuilder();
370+
371+
hb.Configuration[$"ConnectionStrings:{db.Resource.Name}"] = await db.Resource.ConnectionStringExpression.GetValueAsync(default);
372+
373+
hb.AddSurrealClient(db.Resource.Name);
374+
375+
using var host = hb.Build();
376+
377+
await host.StartAsync();
378+
379+
await app.ResourceNotifications.WaitForResourceHealthyAsync(db.Resource.Name, cts.Token);
380+
381+
await using var client = host.Services.GetRequiredService<SurrealDbClient>();
382+
383+
await CreateTestData(client, cts.Token);
384+
await AssertTestData(client, cts.Token);
385+
}
337386

338387
private static async Task CreateTestData(SurrealDbClient surrealDbClient, CancellationToken ct)
339388
{

0 commit comments

Comments
 (0)