Skip to content

Commit 525660e

Browse files
committed
Allow defining scopes and collections on buckets
1 parent 79d5bcb commit 525660e

File tree

10 files changed

+186
-11
lines changed

10 files changed

+186
-11
lines changed

src/Couchbase.Aspire.Hosting/Api/Bucket.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,24 @@ internal sealed class SampleBucketTask
3030
[JsonPropertyName("taskId")]
3131
public string? TaskId { get; set; }
3232
}
33+
34+
internal sealed class ScopesResponse
35+
{
36+
[JsonPropertyName("scopes")]
37+
public List<Scope> Scopes { get; set; } = [];
38+
}
39+
40+
internal sealed class Scope
41+
{
42+
[JsonPropertyName("name")]
43+
public string Name { get; set; } = "";
44+
45+
[JsonPropertyName("collections")]
46+
public List<Collection> Collections { get; set; } = [];
47+
}
48+
49+
internal sealed class Collection
50+
{
51+
[JsonPropertyName("name")]
52+
public string Name { get; set; } = "";
53+
}

src/Couchbase.Aspire.Hosting/Api/CouchbaseApi.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,53 @@ public async Task FlushBucketAsync(CouchbaseServerResource server, string bucket
350350
await ThrowOnFailureAsync(response, cancellationToken).ConfigureAwait(false);
351351
}
352352

353+
public async Task<ScopesResponse> GetScopesAsync(CouchbaseServerResource server, string bucketName, CancellationToken cancellationToken = default)
354+
{
355+
ArgumentNullException.ThrowIfNull(server);
356+
ArgumentException.ThrowIfNullOrEmpty(bucketName);
357+
358+
var response = await SendRequestAsync(server.GetManagementEndpoint(),
359+
HttpMethod.Get,
360+
$"/pools/default/buckets/{bucketName}/scopes",
361+
cancellationToken: cancellationToken).ConfigureAwait(false);
362+
363+
await ThrowOnFailureAsync(response, cancellationToken).ConfigureAwait(false);
364+
365+
return (await response.Content.ReadFromJsonAsync<ScopesResponse>(cancellationToken).ConfigureAwait(false))!;
366+
}
367+
368+
public async Task CreateScopeAsync(CouchbaseServerResource server, string bucketName, string scopeName, CancellationToken cancellationToken = default)
369+
{
370+
ArgumentNullException.ThrowIfNull(server);
371+
ArgumentException.ThrowIfNullOrEmpty(bucketName);
372+
ArgumentException.ThrowIfNullOrEmpty(scopeName);
373+
374+
var response = await SendRequestAsync(server.GetManagementEndpoint(),
375+
HttpMethod.Post,
376+
$"/pools/default/buckets/{bucketName}/scopes",
377+
content: new FormUrlEncodedContent([new("name", scopeName)]),
378+
cancellationToken: cancellationToken).ConfigureAwait(false);
379+
380+
await ThrowOnFailureAsync(response, cancellationToken).ConfigureAwait(false);
381+
}
382+
383+
public async Task CreateCollectionAsync(CouchbaseServerResource server, string bucketName, string scopeName, string collectionName,
384+
CancellationToken cancellationToken = default)
385+
{
386+
ArgumentNullException.ThrowIfNull(server);
387+
ArgumentException.ThrowIfNullOrEmpty(bucketName);
388+
ArgumentException.ThrowIfNullOrEmpty(scopeName);
389+
ArgumentException.ThrowIfNullOrEmpty(collectionName);
390+
391+
var response = await SendRequestAsync(server.GetManagementEndpoint(),
392+
HttpMethod.Post,
393+
$"/pools/default/buckets/{bucketName}/scopes/{scopeName}/collections",
394+
content: new FormUrlEncodedContent([new("name", collectionName)]),
395+
cancellationToken: cancellationToken).ConfigureAwait(false);
396+
397+
await ThrowOnFailureAsync(response, cancellationToken).ConfigureAwait(false);
398+
}
399+
353400
public async Task<List<ClusterTask>> GetClusterTasksAsync(CouchbaseServerResource server, CancellationToken cancellationToken)
354401
{
355402
ArgumentNullException.ThrowIfNull(server);

src/Couchbase.Aspire.Hosting/Api/ICouchbaseApi.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ internal interface ICouchbaseApi
77
Task CreateBucketAsync(CouchbaseServerResource server, string bucketName, CouchbaseBucketSettings settings, CancellationToken cancellationToken = default);
88
Task<SampleBucketResponse> CreateSampleBucketAsync(CouchbaseServerResource server, string bucketName, CancellationToken cancellationToken);
99
Task<Bucket?> GetBucketAsync(CouchbaseServerResource server, string bucketName, CancellationToken cancellationToken);
10+
Task<ScopesResponse> GetScopesAsync(CouchbaseServerResource server, string bucketName, CancellationToken cancellationToken);
11+
Task CreateScopeAsync(CouchbaseServerResource server, string bucketName, string scopeName, CancellationToken cancellationToken);
12+
Task CreateCollectionAsync(CouchbaseServerResource server, string bucketName, string scopeName, string collectionName, CancellationToken cancellationToken);
1013
Task FlushBucketAsync(CouchbaseServerResource server, string bucketName, CancellationToken cancellationToken);
1114
Task<Pool> GetClusterNodesAsync(CouchbaseServerResource server, CancellationToken cancellationToken = default);
1215
Task<NodeServices> GetNodeServicesAsync(CouchbaseServerResource server, CancellationToken cancellationToken = default);

src/Couchbase.Aspire.Hosting/CouchbaseBucketBaseResource.cs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using System.Diagnostics.CodeAnalysis;
2-
using System.Runtime.CompilerServices;
31
using Aspire.Hosting;
42
using Aspire.Hosting.ApplicationModel;
53

@@ -28,7 +26,7 @@ public abstract class CouchbaseBucketBaseResource(string name, string bucketName
2826
/// <summary>
2927
/// Gets the database name.
3028
/// </summary>
31-
public string BucketName { get; } = ThrowIfNullOrEmpty(bucketName);
29+
public string BucketName { get; } = ThrowHelpers.ThrowIfNullOrEmpty(bucketName);
3230

3331
/// <summary>
3432
/// Gets the bucket name expression for the Couchbase bucket.
@@ -47,10 +45,4 @@ IEnumerable<KeyValuePair<string, ReferenceExpression>> IResourceWithConnectionSt
4745
Cluster.CombineProperties([
4846
new("BucketName", BucketNameExpression)
4947
]);
50-
51-
private static string ThrowIfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
52-
{
53-
ArgumentException.ThrowIfNullOrEmpty(argument, paramName);
54-
return argument;
55-
}
5648
}

src/Couchbase.Aspire.Hosting/CouchbaseBucketBuilderExtensions.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,4 +389,48 @@ public static IResourceBuilder<CouchbaseBucketResource> WithMaximumTimeToLive(th
389389
return Task.CompletedTask;
390390
});
391391
}
392+
393+
/// <summary>
394+
/// Adds a scope to the Couchbase bucket.
395+
/// </summary>
396+
/// <param name="builder">The bucket builder.</param>
397+
/// <param name="scopeName">Name of the scope.</param>
398+
/// <param name="collections">List of collection names to be created within the scope.</param>
399+
/// <returns>The <paramref name="builder"/>.</returns>
400+
public static IResourceBuilder<CouchbaseBucketResource> WithScope(this IResourceBuilder<CouchbaseBucketResource> builder, string scopeName,
401+
IEnumerable<string>? collections = null)
402+
{
403+
ArgumentNullException.ThrowIfNull(builder);
404+
ArgumentException.ThrowIfNullOrEmpty(scopeName);
405+
406+
CouchbaseScopeAnnotation? annotation = null;
407+
if (builder.Resource.TryGetAnnotationsOfType<CouchbaseScopeAnnotation>(out var annotations))
408+
{
409+
annotation = annotations.FirstOrDefault(p => p.ScopeName == scopeName);
410+
}
411+
412+
if (annotation is null)
413+
{
414+
annotation = new CouchbaseScopeAnnotation(scopeName);
415+
416+
if (collections is not null)
417+
{
418+
annotation.CollectionNames = [.. collections];
419+
}
420+
421+
builder.WithAnnotation(annotation);
422+
}
423+
else if (collections is not null)
424+
{
425+
foreach (var collectionName in collections)
426+
{
427+
if (!annotation.CollectionNames.Contains(collectionName))
428+
{
429+
annotation.CollectionNames.Add(collectionName);
430+
}
431+
}
432+
}
433+
434+
return builder;
435+
}
392436
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using Aspire.Hosting.ApplicationModel;
2+
3+
namespace Couchbase.Aspire.Hosting;
4+
5+
/// <summary>
6+
/// Indicates a scope to be created for a Couchbase bucket.
7+
/// </summary>
8+
/// <param name="scopeName">Name of the scope.</param>
9+
public class CouchbaseScopeAnnotation(string scopeName) : IResourceAnnotation
10+
{
11+
/// <summary>
12+
/// Name of the scope.
13+
/// </summary>
14+
public string ScopeName { get; } = ThrowHelpers.ThrowIfNullOrEmpty(scopeName);
15+
16+
/// <summary>
17+
/// List of collection names to be created within the scope.
18+
/// </summary>
19+
public List<string> CollectionNames
20+
{
21+
get => field ??= [];
22+
set
23+
{
24+
ArgumentNullException.ThrowIfNull(value);
25+
26+
field = value;
27+
}
28+
}
29+
}

src/Couchbase.Aspire.Hosting/Orchestration/CouchbaseClusterOrchestrator.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,31 @@ private async Task InitializeBucketAsync(CouchbaseBucketResource bucket, ILogger
632632
await api.CreateBucketAsync(node, bucket.BucketName, settings, cancellationToken).ConfigureAwait(false);
633633
}
634634

635+
// Create scopes and collections that don't already exist
636+
if (bucket.TryGetAnnotationsOfType<CouchbaseScopeAnnotation>(out var scopes))
637+
{
638+
var currentScopes = await api.GetScopesAsync(node, bucket.BucketName, cancellationToken).ConfigureAwait(false);
639+
640+
foreach (var scope in scopes)
641+
{
642+
var currentScope = currentScopes.Scopes.FirstOrDefault(p => p.Name == scope.ScopeName);
643+
if (currentScope is null)
644+
{
645+
resourceLogger.LogInformation("Creating scope '{ScopeName}'...", scope.ScopeName);
646+
await api.CreateScopeAsync(node, bucket.BucketName, scope.ScopeName, cancellationToken).ConfigureAwait(false);
647+
}
648+
649+
foreach (var collectionName in scope.CollectionNames)
650+
{
651+
if (currentScope?.Collections.FirstOrDefault(p => p.Name == collectionName) is null)
652+
{
653+
resourceLogger.LogInformation("Creating collection '{ScopeName}.{CollectionName}'...", scope.ScopeName, collectionName);
654+
await api.CreateCollectionAsync(node, bucket.BucketName, scope.ScopeName, collectionName, cancellationToken).ConfigureAwait(false);
655+
}
656+
}
657+
}
658+
}
659+
635660
// Wait for bucket to be healthy
636661
resourceLogger.LogInformation("Waiting for bucket '{BucketName}' to be healthy...", bucket.BucketName);
637662
while (!cancellationToken.IsCancellationRequested)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Runtime.CompilerServices;
3+
4+
namespace Couchbase.Aspire.Hosting;
5+
6+
internal static class ThrowHelpers
7+
{
8+
public static string ThrowIfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
9+
{
10+
ArgumentException.ThrowIfNullOrEmpty(argument, paramName);
11+
return argument;
12+
}
13+
}

tests/Aspire.Test.AppHost/AppHost.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
.WithReplicas(2);
2121

2222
var testBucket = couchbase.AddBucket("test-bucket", bucketName: "test")
23+
.WithScope("test-scope", ["counter"])
2324
.WithMemoryQuota(200) // Optional memory quota, default is 100MB
2425
.WithConflictResolutionType(ConflictResolutionType.Timestamp)
2526
.WithMinimumDurabilityLevel(DurabilityLevel.MajorityAndPersistToActive)

tests/Aspire.Test.WebApp/Components/Pages/Counter.razor

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
try
2929
{
3030
var bucket = await BucketProvider.GetBucketAsync();
31-
var collection = bucket.DefaultCollection();
31+
var collection = bucket.Scope("test-scope").Collection("counter");
3232

3333
var result = await collection.GetAsync("counter", new Couchbase.KeyValue.GetOptions()
3434
.Transcoder(new Couchbase.Core.IO.Transcoders.LegacyTranscoder()));
@@ -50,7 +50,7 @@
5050
try
5151
{
5252
var bucket = await BucketProvider.GetBucketAsync();
53-
var collection = bucket.DefaultCollection();
53+
var collection = bucket.Scope("test-scope").Collection("counter");
5454

5555
try
5656
{

0 commit comments

Comments
 (0)