@databricks-solutions/lakebase-scm-utils 0.2.51 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -4
- package/dist/scripts/index.cjs +2813 -2244
- package/dist/scripts/index.d.cts +1 -1
- package/dist/scripts/index.d.ts +1 -1
- package/dist/scripts/index.js +857 -298
- package/dist/scripts/lakebase/ci-resolve-branch.cli.cjs +2460 -2030
- package/dist/scripts/lakebase/ci-resolve-branch.cli.js +508 -81
- package/dist/scripts/lakebase/collapse-heads.cli.cjs +2429 -2007
- package/dist/scripts/lakebase/collapse-heads.cli.js +477 -58
- package/dist/scripts/lakebase/detect-language.cli.cjs +2441 -2016
- package/dist/scripts/lakebase/detect-language.cli.js +489 -67
- package/dist/scripts/lakebase/doctor.cli.cjs +2484 -2040
- package/dist/scripts/lakebase/doctor.cli.js +532 -91
- package/dist/scripts/lakebase/get-connection.cli.cjs +2 -2
- package/dist/scripts/lakebase/get-connection.cli.js +2 -2
- package/dist/scripts/lakebase/index.cjs +2810 -2205
- package/dist/scripts/lakebase/index.d.cts +59 -10
- package/dist/scripts/lakebase/index.d.ts +59 -10
- package/dist/scripts/lakebase/index.js +851 -256
- package/dist/scripts/lakebase/infra-runner.cli.cjs +2435 -2013
- package/dist/scripts/lakebase/infra-runner.cli.js +483 -64
- package/dist/scripts/lakebase/new-migration.cli.cjs +2429 -2007
- package/dist/scripts/lakebase/new-migration.cli.js +477 -58
- package/dist/scripts/lakebase/schema-diff.cli.cjs +2 -2
- package/dist/scripts/lakebase/schema-diff.cli.js +2 -2
- package/dist/scripts/lakebase/schema-migrate.cli.cjs +2461 -2029
- package/dist/scripts/lakebase/schema-migrate.cli.js +509 -80
- package/dist/scripts/lakebase/scm-claim-feature.cli.cjs +2460 -2030
- package/dist/scripts/lakebase/scm-claim-feature.cli.js +508 -81
- package/dist/scripts/lakebase/scm-doctor.cli.cjs +2486 -2056
- package/dist/scripts/lakebase/scm-doctor.cli.js +537 -107
- package/dist/scripts/lakebase/scm-merge.cli.cjs +2443 -2021
- package/dist/scripts/lakebase/scm-merge.cli.js +494 -72
- package/dist/scripts/lakebase/scm-reconcile-tier.cli.cjs +2464 -2034
- package/dist/scripts/lakebase/scm-reconcile-tier.cli.js +511 -84
- package/dist/scripts/util/index.cjs +2 -2
- package/dist/scripts/util/index.js +2 -2
- package/package.json +1 -1
- package/scripts/lakebase/adapters/efcore-adapter.ts +158 -0
- package/scripts/lakebase/adapters/flyway-adapter.ts +8 -5
- package/scripts/lakebase/client-lockfile.ts +102 -0
- package/scripts/lakebase/collapse-heads.cli.ts +1 -1
- package/scripts/lakebase/create-project.ts +25 -2
- package/scripts/lakebase/detect-language.cli.ts +2 -2
- package/scripts/lakebase/doctor.ts +29 -0
- package/scripts/lakebase/dotnet-project.ts +112 -0
- package/scripts/lakebase/index.ts +1 -0
- package/scripts/lakebase/migration-layout.ts +43 -2
- package/scripts/lakebase/new-migration.cli.ts +1 -1
- package/scripts/lakebase/scaffold-language.ts +23 -2
- package/scripts/lakebase/scaffold.ts +4 -1
- package/scripts/lakebase/schema-migrate-runners/efcore.ts +399 -0
- package/scripts/lakebase/schema-migrate-runners/flyway.ts +6 -4
- package/scripts/lakebase/schema-migrate.cli.ts +3 -1
- package/scripts/lakebase/schema-migrate.ts +24 -10
- package/templates/project/common/.github/workflows/merge.yml +13 -1
- package/templates/project/common/.github/workflows/pr.yml +25 -5
- package/templates/project/common/scripts/flyway-migrate.sh +54 -1
- package/templates/project/common/scripts/prepare-schema-diff.sh +5 -0
- package/templates/project/common/scripts/run-dev.sh +10 -1
- package/templates/project/common/scripts/run-tests.sh +6 -1
- package/templates/project/dotnet/.gitignore.extra +8 -0
- package/templates/project/dotnet/App.sln +56 -0
- package/templates/project/dotnet/Makefile +13 -0
- package/templates/project/dotnet/global.json +6 -0
- package/templates/project/dotnet/src/App/Data/AppDbContext.cs +12 -0
- package/templates/project/dotnet/src/App/Lakebase/DotEnv.cs +48 -0
- package/templates/project/dotnet/src/App/Lakebase/LakebaseConnection.cs +225 -0
- package/templates/project/dotnet/src/App/Program.cs +34 -0
- package/templates/project/dotnet/src/App/appsettings.json +9 -0
- package/templates/project/dotnet/tests/App.Tests/App.Tests.csproj +25 -0
- package/templates/project/dotnet/tests/App.Tests/HealthTests.cs +18 -0
- package/templates/project/dotnet/tests/App.Tests/OpenApiTests.cs +23 -0
- package/templates/project/dotnet-efcore/.config/dotnet-tools.json +13 -0
- package/templates/project/dotnet-efcore/src/App/App.csproj +22 -0
- package/templates/project/dotnet-efcore/src/App/Data/DesignTimeDbContextFactory.cs +24 -0
- package/templates/project/dotnet-efcore/src/App/Migrations/README.md +23 -0
- package/templates/project/dotnet-flyway/db/migrations/README.md +23 -0
- package/templates/project/dotnet-flyway/src/App/App.csproj +17 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
using System.Diagnostics;
|
|
2
|
+
using System.Text.Json;
|
|
3
|
+
using Microsoft.EntityFrameworkCore;
|
|
4
|
+
using Microsoft.Extensions.DependencyInjection.Extensions;
|
|
5
|
+
using Npgsql;
|
|
6
|
+
|
|
7
|
+
namespace App.Lakebase;
|
|
8
|
+
|
|
9
|
+
/// <summary>
|
|
10
|
+
/// Builds the app's Postgres connection. Credentials are minted at RUNTIME, never
|
|
11
|
+
/// stored in .env: the data source is built from connection METADATA
|
|
12
|
+
/// (LAKEBASE_HOST, LAKEBASE_PROJECT_ID, LAKEBASE_BRANCH_ID, DB_USERNAME) and a
|
|
13
|
+
/// periodic password provider injects a short-lived Lakebase credential minted by
|
|
14
|
+
/// the databricks CLI (the same credential source as the Python and Node templates).
|
|
15
|
+
///
|
|
16
|
+
/// An explicit DATABASE_URL (CI secret, Docker, the kit's ephemeral-verify DSN)
|
|
17
|
+
/// OVERRIDES this and is used verbatim. postgresql:// URLs and Npgsql
|
|
18
|
+
/// "Host=...;" strings are both accepted.
|
|
19
|
+
/// </summary>
|
|
20
|
+
public static class LakebaseConnection
|
|
21
|
+
{
|
|
22
|
+
// Lakebase credentials live ~1h; re-mint after 40 min so a new physical
|
|
23
|
+
// connection never presents an expired token.
|
|
24
|
+
private static readonly TimeSpan MintTtl = TimeSpan.FromMinutes(40);
|
|
25
|
+
private static readonly SemaphoreSlim MintLock = new(1, 1);
|
|
26
|
+
private static string? _cachedToken;
|
|
27
|
+
private static DateTime _mintedAt = DateTime.MinValue;
|
|
28
|
+
|
|
29
|
+
/// <summary>
|
|
30
|
+
/// Registers one process-wide NpgsqlDataSource and a DbContext that uses it.
|
|
31
|
+
/// Never build a data source per DbContext: that creates a new pool and mints a
|
|
32
|
+
/// new credential on every request.
|
|
33
|
+
/// </summary>
|
|
34
|
+
public static IServiceCollection AddLakebaseDbContext<TContext>(this IServiceCollection services)
|
|
35
|
+
where TContext : DbContext
|
|
36
|
+
{
|
|
37
|
+
services.TryAddSingleton(_ => CreateDataSource());
|
|
38
|
+
services.AddDbContext<TContext>((sp, options) =>
|
|
39
|
+
options.UseNpgsql(sp.GetRequiredService<NpgsqlDataSource>()));
|
|
40
|
+
return services;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// <summary>Data source for the app (and the EF Core design-time factory).</summary>
|
|
44
|
+
public static NpgsqlDataSource CreateDataSource()
|
|
45
|
+
{
|
|
46
|
+
var explicitUrl = Environment.GetEnvironmentVariable("DATABASE_URL");
|
|
47
|
+
if (!string.IsNullOrWhiteSpace(explicitUrl))
|
|
48
|
+
return NpgsqlDataSource.Create(Normalize(new NpgsqlConnectionStringBuilder(ToNpgsql(explicitUrl))).ConnectionString);
|
|
49
|
+
|
|
50
|
+
var host = Env("LAKEBASE_HOST") ?? Env("DB_HOST");
|
|
51
|
+
var endpoint = EndpointPathFromEnv();
|
|
52
|
+
var builder = new NpgsqlConnectionStringBuilder
|
|
53
|
+
{
|
|
54
|
+
Host = host ?? "localhost",
|
|
55
|
+
Port = 5432,
|
|
56
|
+
Database = Env("DB_NAME") ?? "databricks_postgres",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (host is null || endpoint is null)
|
|
60
|
+
return NpgsqlDataSource.Create(Normalize(builder).ConnectionString); // pure-local fallback
|
|
61
|
+
|
|
62
|
+
// Resolve the user without failing here: design-time commands such as
|
|
63
|
+
// `dotnet ef migrations add` build a data source but never connect, so
|
|
64
|
+
// an offline or expired CLI session must only fail when a connection
|
|
65
|
+
// is actually opened.
|
|
66
|
+
Exception? userError = null;
|
|
67
|
+
var user = Env("DB_USERNAME");
|
|
68
|
+
if (user is null)
|
|
69
|
+
{
|
|
70
|
+
try { user = CurrentUser(); }
|
|
71
|
+
catch (Exception ex) { userError = ex; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
builder.Username = user ?? "unresolved-lakebase-user";
|
|
75
|
+
var dataSourceBuilder = new NpgsqlDataSourceBuilder(Normalize(builder).ConnectionString);
|
|
76
|
+
dataSourceBuilder.UsePeriodicPasswordProvider(
|
|
77
|
+
async (_, ct) => userError is null
|
|
78
|
+
? await MintTokenAsync(endpoint, ct)
|
|
79
|
+
: throw new InvalidOperationException(
|
|
80
|
+
"Could not resolve the Lakebase user. Set DB_USERNAME in .env or run 'databricks auth login'.", userError),
|
|
81
|
+
successRefreshInterval: TimeSpan.FromMinutes(5),
|
|
82
|
+
failureRefreshInterval: TimeSpan.FromSeconds(10));
|
|
83
|
+
return dataSourceBuilder.Build();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// <summary>projects/<id>/branches/<branch>/endpoints/<endpoint>, or null without metadata.</summary>
|
|
87
|
+
public static string? EndpointPathFromEnv()
|
|
88
|
+
{
|
|
89
|
+
var project = Env("LAKEBASE_PROJECT_ID");
|
|
90
|
+
var branch = Env("LAKEBASE_BRANCH_ID");
|
|
91
|
+
if (project is null || branch is null)
|
|
92
|
+
return null;
|
|
93
|
+
return $"projects/{project}/branches/{branch}/endpoints/{Env("LAKEBASE_ENDPOINT") ?? "primary"}";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// <summary>Convert a postgresql:// URL to an Npgsql connection string; pass Npgsql strings through.</summary>
|
|
97
|
+
public static string ToNpgsql(string databaseUrl)
|
|
98
|
+
{
|
|
99
|
+
var raw = databaseUrl.Trim();
|
|
100
|
+
if (!raw.StartsWith("postgres://", StringComparison.OrdinalIgnoreCase)
|
|
101
|
+
&& !raw.StartsWith("postgresql://", StringComparison.OrdinalIgnoreCase))
|
|
102
|
+
return raw;
|
|
103
|
+
|
|
104
|
+
var uri = new Uri(raw);
|
|
105
|
+
var builder = new NpgsqlConnectionStringBuilder
|
|
106
|
+
{
|
|
107
|
+
Host = uri.Host,
|
|
108
|
+
Port = uri.Port > 0 ? uri.Port : 5432,
|
|
109
|
+
Database = Uri.UnescapeDataString(uri.AbsolutePath.TrimStart('/')),
|
|
110
|
+
};
|
|
111
|
+
var userInfo = uri.UserInfo.Split(':', 2);
|
|
112
|
+
if (userInfo[0].Length > 0)
|
|
113
|
+
builder.Username = Uri.UnescapeDataString(userInfo[0]);
|
|
114
|
+
if (userInfo.Length > 1)
|
|
115
|
+
builder.Password = Uri.UnescapeDataString(userInfo[1]);
|
|
116
|
+
|
|
117
|
+
foreach (var pair in uri.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries))
|
|
118
|
+
{
|
|
119
|
+
var kv = pair.Split('=', 2);
|
|
120
|
+
var value = kv.Length > 1 ? Uri.UnescapeDataString(kv[1]) : "";
|
|
121
|
+
switch (kv[0].ToLowerInvariant())
|
|
122
|
+
{
|
|
123
|
+
case "sslmode":
|
|
124
|
+
builder.SslMode = value.ToLowerInvariant() switch
|
|
125
|
+
{
|
|
126
|
+
"disable" => SslMode.Disable,
|
|
127
|
+
"allow" => SslMode.Allow,
|
|
128
|
+
"prefer" => SslMode.Prefer,
|
|
129
|
+
"verify-ca" => SslMode.VerifyCA,
|
|
130
|
+
"verify-full" => SslMode.VerifyFull,
|
|
131
|
+
_ => SslMode.Require,
|
|
132
|
+
};
|
|
133
|
+
break;
|
|
134
|
+
case "application_name":
|
|
135
|
+
builder.ApplicationName = value;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return builder.ConnectionString;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// TLS on and a non-empty application_name, so every connection is
|
|
144
|
+
// identifiable in pg_stat_activity.
|
|
145
|
+
private static NpgsqlConnectionStringBuilder Normalize(NpgsqlConnectionStringBuilder builder)
|
|
146
|
+
{
|
|
147
|
+
var weakTls = builder.SslMode is SslMode.Disable or SslMode.Allow or SslMode.Prefer;
|
|
148
|
+
if (weakTls && builder.Host != "localhost")
|
|
149
|
+
builder.SslMode = SslMode.Require;
|
|
150
|
+
if (string.IsNullOrEmpty(builder.ApplicationName))
|
|
151
|
+
{
|
|
152
|
+
var consort = Env("CONSORT_VERSION");
|
|
153
|
+
builder.ApplicationName = Env("PGAPPNAME")
|
|
154
|
+
?? (consort is not null ? $"consort/{consort}" : "scm-utils/{{LAKEBASE_SCM_UTILS_VERSION}}");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return builder;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private static async Task<string> MintTokenAsync(string endpoint, CancellationToken ct)
|
|
161
|
+
{
|
|
162
|
+
await MintLock.WaitAsync(ct);
|
|
163
|
+
try
|
|
164
|
+
{
|
|
165
|
+
if (_cachedToken is not null && DateTime.UtcNow - _mintedAt < MintTtl)
|
|
166
|
+
return _cachedToken;
|
|
167
|
+
|
|
168
|
+
var json = await RunDatabricksAsync(["postgres", "generate-database-credential", endpoint, "-o", "json"], ct);
|
|
169
|
+
using var doc = JsonDocument.Parse(json);
|
|
170
|
+
_cachedToken = doc.RootElement.GetProperty("token").GetString()
|
|
171
|
+
?? throw new InvalidOperationException($"generate-database-credential returned no token for {endpoint}");
|
|
172
|
+
_mintedAt = DateTime.UtcNow;
|
|
173
|
+
return _cachedToken;
|
|
174
|
+
}
|
|
175
|
+
finally
|
|
176
|
+
{
|
|
177
|
+
MintLock.Release();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private static string CurrentUser()
|
|
182
|
+
{
|
|
183
|
+
var json = RunDatabricksAsync(["current-user", "me", "-o", "json"], CancellationToken.None).GetAwaiter().GetResult();
|
|
184
|
+
using var doc = JsonDocument.Parse(json);
|
|
185
|
+
return doc.RootElement.GetProperty("userName").GetString()
|
|
186
|
+
?? throw new InvalidOperationException("databricks current-user me returned no userName; set DB_USERNAME in .env");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private static async Task<string> RunDatabricksAsync(IEnumerable<string> args, CancellationToken ct)
|
|
190
|
+
{
|
|
191
|
+
var psi = new ProcessStartInfo("databricks")
|
|
192
|
+
{
|
|
193
|
+
RedirectStandardOutput = true,
|
|
194
|
+
RedirectStandardError = true,
|
|
195
|
+
UseShellExecute = false,
|
|
196
|
+
};
|
|
197
|
+
foreach (var arg in args)
|
|
198
|
+
psi.ArgumentList.Add(arg);
|
|
199
|
+
if (Env("DATABRICKS_CONFIG_PROFILE") is { } profile)
|
|
200
|
+
{
|
|
201
|
+
psi.ArgumentList.Add("--profile");
|
|
202
|
+
psi.ArgumentList.Add(profile);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
using var process = Process.Start(psi)
|
|
206
|
+
?? throw new InvalidOperationException("Could not start the databricks CLI. Is it installed and on PATH?");
|
|
207
|
+
var stdout = process.StandardOutput.ReadToEndAsync(ct);
|
|
208
|
+
var stderr = process.StandardError.ReadToEndAsync(ct);
|
|
209
|
+
await process.WaitForExitAsync(ct);
|
|
210
|
+
if (process.ExitCode != 0)
|
|
211
|
+
{
|
|
212
|
+
var err = (await stderr).Trim();
|
|
213
|
+
var hint = err.Contains("auth login", StringComparison.OrdinalIgnoreCase)
|
|
214
|
+
|| err.Contains("refresh token", StringComparison.OrdinalIgnoreCase)
|
|
215
|
+
? " Run 'databricks auth login' to re-authenticate."
|
|
216
|
+
: "";
|
|
217
|
+
throw new InvalidOperationException($"databricks {string.Join(' ', psi.ArgumentList)} failed: {err}{hint}");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return await stdout;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private static string? Env(string name)
|
|
224
|
+
=> Environment.GetEnvironmentVariable(name) is { Length: > 0 } value ? value : null;
|
|
225
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
using App.Data;
|
|
2
|
+
using App.Lakebase;
|
|
3
|
+
|
|
4
|
+
// Load .env from the repo root so the Lakebase connection metadata written by
|
|
5
|
+
// the post-checkout hook is available however the app is started. Variables
|
|
6
|
+
// already set in the environment (CI secrets, Docker --env) always win.
|
|
7
|
+
DotEnv.LoadFromRepoRoot();
|
|
8
|
+
|
|
9
|
+
var builder = WebApplication.CreateBuilder(args);
|
|
10
|
+
|
|
11
|
+
// One process-wide data source: a single connection pool whose password is a
|
|
12
|
+
// short-lived Lakebase credential refreshed in the background.
|
|
13
|
+
builder.Services.AddLakebaseDbContext<AppDbContext>();
|
|
14
|
+
|
|
15
|
+
// OpenAPI description of the endpoints, browsable at /docs (like the Node template).
|
|
16
|
+
builder.Services.AddOpenApi();
|
|
17
|
+
|
|
18
|
+
var app = builder.Build();
|
|
19
|
+
|
|
20
|
+
app.MapOpenApi("/openapi.json");
|
|
21
|
+
app.UseSwaggerUI(options =>
|
|
22
|
+
{
|
|
23
|
+
options.RoutePrefix = "docs";
|
|
24
|
+
options.SwaggerEndpoint("/openapi.json", "API");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
app.MapGet("/health", () => Results.Ok(new { status = "ok" }));
|
|
28
|
+
|
|
29
|
+
// Register API endpoints above this line.
|
|
30
|
+
|
|
31
|
+
app.Run();
|
|
32
|
+
|
|
33
|
+
// Exposed for WebApplicationFactory<Program> in tests.
|
|
34
|
+
public partial class Program;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<Project Sdk="Microsoft.NET.Sdk">
|
|
2
|
+
|
|
3
|
+
<PropertyGroup>
|
|
4
|
+
<TargetFramework>net10.0</TargetFramework>
|
|
5
|
+
<ImplicitUsings>enable</ImplicitUsings>
|
|
6
|
+
<Nullable>enable</Nullable>
|
|
7
|
+
<IsPackable>false</IsPackable>
|
|
8
|
+
</PropertyGroup>
|
|
9
|
+
|
|
10
|
+
<ItemGroup>
|
|
11
|
+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.12" />
|
|
12
|
+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
|
13
|
+
<PackageReference Include="xunit" Version="2.9.3" />
|
|
14
|
+
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
|
15
|
+
</ItemGroup>
|
|
16
|
+
|
|
17
|
+
<ItemGroup>
|
|
18
|
+
<Using Include="Xunit" />
|
|
19
|
+
</ItemGroup>
|
|
20
|
+
|
|
21
|
+
<ItemGroup>
|
|
22
|
+
<ProjectReference Include="..\..\src\App\App.csproj" />
|
|
23
|
+
</ItemGroup>
|
|
24
|
+
|
|
25
|
+
</Project>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
using System.Net;
|
|
2
|
+
using System.Net.Http.Json;
|
|
3
|
+
using Microsoft.AspNetCore.Mvc.Testing;
|
|
4
|
+
|
|
5
|
+
namespace App.Tests;
|
|
6
|
+
|
|
7
|
+
public class HealthTests(WebApplicationFactory<Program> factory) : IClassFixture<WebApplicationFactory<Program>>
|
|
8
|
+
{
|
|
9
|
+
[Fact]
|
|
10
|
+
public async Task Health_ReturnsOk()
|
|
11
|
+
{
|
|
12
|
+
var response = await factory.CreateClient().GetAsync("/health");
|
|
13
|
+
|
|
14
|
+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
15
|
+
var body = await response.Content.ReadFromJsonAsync<Dictionary<string, string>>();
|
|
16
|
+
Assert.Equal("ok", body!["status"]);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
using System.Net;
|
|
2
|
+
using Microsoft.AspNetCore.Mvc.Testing;
|
|
3
|
+
|
|
4
|
+
namespace App.Tests;
|
|
5
|
+
|
|
6
|
+
public class OpenApiTests(WebApplicationFactory<Program> factory) : IClassFixture<WebApplicationFactory<Program>>
|
|
7
|
+
{
|
|
8
|
+
[Fact]
|
|
9
|
+
public async Task OpenApiDocument_ListsEndpoints()
|
|
10
|
+
{
|
|
11
|
+
var body = await factory.CreateClient().GetStringAsync("/openapi.json");
|
|
12
|
+
|
|
13
|
+
Assert.Contains("\"/health\"", body);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
[Fact]
|
|
17
|
+
public async Task Docs_ServesSwaggerUi()
|
|
18
|
+
{
|
|
19
|
+
var response = await factory.CreateClient().GetAsync("/docs/index.html");
|
|
20
|
+
|
|
21
|
+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
2
|
+
|
|
3
|
+
<PropertyGroup>
|
|
4
|
+
<TargetFramework>net10.0</TargetFramework>
|
|
5
|
+
<Nullable>enable</Nullable>
|
|
6
|
+
<ImplicitUsings>enable</ImplicitUsings>
|
|
7
|
+
<RootNamespace>App</RootNamespace>
|
|
8
|
+
<!-- {{PROJECT_NAME}} -->
|
|
9
|
+
</PropertyGroup>
|
|
10
|
+
|
|
11
|
+
<ItemGroup>
|
|
12
|
+
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
|
|
13
|
+
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
|
|
14
|
+
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.3.0" />
|
|
15
|
+
<!-- Marks this project as the EF Core migrations owner for lakebase-schema-migrate. -->
|
|
16
|
+
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.4">
|
|
17
|
+
<PrivateAssets>all</PrivateAssets>
|
|
18
|
+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
19
|
+
</PackageReference>
|
|
20
|
+
</ItemGroup>
|
|
21
|
+
|
|
22
|
+
</Project>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
using App.Lakebase;
|
|
2
|
+
using Microsoft.EntityFrameworkCore;
|
|
3
|
+
using Microsoft.EntityFrameworkCore.Design;
|
|
4
|
+
|
|
5
|
+
namespace App.Data;
|
|
6
|
+
|
|
7
|
+
/// <summary>
|
|
8
|
+
/// Used by `dotnet ef` (and the kit's lakebase-schema-migrate) to build the
|
|
9
|
+
/// DbContext without starting the web host. It reads the same connection
|
|
10
|
+
/// settings as the app: DATABASE_URL when set (the kit passes the target
|
|
11
|
+
/// branch DSN this way, so no password appears on a command line), otherwise
|
|
12
|
+
/// the Lakebase metadata in .env with a runtime-minted credential.
|
|
13
|
+
/// </summary>
|
|
14
|
+
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>
|
|
15
|
+
{
|
|
16
|
+
public AppDbContext CreateDbContext(string[] args)
|
|
17
|
+
{
|
|
18
|
+
DotEnv.LoadFromRepoRoot();
|
|
19
|
+
var options = new DbContextOptionsBuilder<AppDbContext>()
|
|
20
|
+
.UseNpgsql(LakebaseConnection.CreateDataSource())
|
|
21
|
+
.Options;
|
|
22
|
+
return new AppDbContext(options);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# EF Core migrations
|
|
2
|
+
|
|
3
|
+
Create a migration with the kit's tool-agnostic command (run in the project
|
|
4
|
+
root):
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
./scripts/lk lakebase-new-migration --name "<description>"
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
This runs `dotnet ef migrations add`, which generates the migration from the
|
|
11
|
+
difference between your `AppDbContext` model and the last snapshot. Review the
|
|
12
|
+
generated `Up()` / `Down()` before committing. You can also run
|
|
13
|
+
`dotnet ef migrations add <Name> --project src/App` directly.
|
|
14
|
+
|
|
15
|
+
EF Core names migrations `<UTC timestamp>_<Name>.cs`, so sibling feature
|
|
16
|
+
branches never collide on an id. Commit the migration, its `.Designer.cs`, and
|
|
17
|
+
the updated `AppDbContextModelSnapshot.cs` together.
|
|
18
|
+
|
|
19
|
+
You do not run migrations by hand: the kit applies them to each paired Lakebase
|
|
20
|
+
branch through CI (`pr.yml` on the feature branch, `merge.yml` on staging /
|
|
21
|
+
production) via `lakebase-schema-migrate`, which passes the branch connection to
|
|
22
|
+
`dotnet ef` in `DATABASE_URL` (read by `Data/DesignTimeDbContextFactory.cs`).
|
|
23
|
+
To migrate your paired branch locally, run `./scripts/flyway-migrate.sh`.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Flyway migrations
|
|
2
|
+
|
|
3
|
+
Create a migration with the kit's tool-agnostic command (run in the project
|
|
4
|
+
root):
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
./scripts/lk lakebase-new-migration --name "<description>"
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
This writes a timestamp-versioned skeleton (`V<timestamp>__<slug>.sql`, a UTC
|
|
11
|
+
`YYYYMMDDHHMMSS` version); then author the DDL/DML. Timestamps are globally
|
|
12
|
+
unique and sort chronologically, so sibling feature branches never collide on a
|
|
13
|
+
version. Flyway runs with `-baselineOnMigrate=true -baselineVersion=0`, so every
|
|
14
|
+
`V<n>` file is applied as a pending migration.
|
|
15
|
+
|
|
16
|
+
Flyway owns the schema; EF Core is used only as the runtime ORM. Keep your
|
|
17
|
+
`AppDbContext` model in step with the SQL here, and do not add EF Core
|
|
18
|
+
migrations to this project.
|
|
19
|
+
|
|
20
|
+
You do not run migrations by hand: the kit applies them to each paired Lakebase
|
|
21
|
+
branch through CI (`pr.yml` on the feature branch, `merge.yml` on staging /
|
|
22
|
+
production) via `lakebase-schema-migrate`. To migrate your paired branch
|
|
23
|
+
locally, run `./scripts/flyway-migrate.sh`.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
2
|
+
|
|
3
|
+
<PropertyGroup>
|
|
4
|
+
<TargetFramework>net10.0</TargetFramework>
|
|
5
|
+
<Nullable>enable</Nullable>
|
|
6
|
+
<ImplicitUsings>enable</ImplicitUsings>
|
|
7
|
+
<RootNamespace>App</RootNamespace>
|
|
8
|
+
<!-- {{PROJECT_NAME}} -->
|
|
9
|
+
</PropertyGroup>
|
|
10
|
+
|
|
11
|
+
<ItemGroup>
|
|
12
|
+
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
|
|
13
|
+
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
|
|
14
|
+
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.3.0" />
|
|
15
|
+
</ItemGroup>
|
|
16
|
+
|
|
17
|
+
</Project>
|