jetzig/src/commands/database.zig
Bob Farrell 92dce21244 Database CLI improvements
Eradication of `data` arg to requests. We no longer need to pass this
value around as we have a) type inference, b) nested object insertion
via `put` and `append`.

Fix `joinPath` numeric type coercion

Detect empty string params and treat them as blank with expectParams()

Fix error logging/stack trace printing.

Update Zmpl - includes debug tokens + error tracing to source template
in debug builds.
2024-11-19 21:39:01 +00:00

153 lines
5.8 KiB
Zig

const std = @import("std");
const build_options = @import("build_options");
const jetquery = @import("jetquery");
const jetzig = @import("jetzig");
const Migrate = @import("jetquery_migrate").Migrate;
const MigrateSchema = @import("jetquery_migrate").MigrateSchema;
const Schema = @import("Schema");
const util = @import("util.zig");
const confirm_drop_env = "JETZIG_DROP_PRODUCTION_DATABASE";
const production_drop_failure_message = "To drop a production database, " ++
"set `" ++ confirm_drop_env ++ "={s}`. Exiting.";
const environment = jetzig.build_options.environment;
const config = @field(jetquery.config.database, @tagName(environment));
const Action = enum { migrate, rollback, create, drop, reflect };
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer std.debug.assert(gpa.deinit() == .ok);
if (comptime !@hasField(@TypeOf(config), "adapter") or config.adapter == .null) {
try util.print(
.failure,
"Database is currently not configured. Update `config/database.zig` before running database commands.",
.{},
);
std.process.exit(1);
}
const gpa_allocator = gpa.allocator();
var arena = std.heap.ArenaAllocator.init(gpa_allocator);
defer arena.deinit();
const allocator = arena.allocator();
const args = try std.process.argsAlloc(allocator);
if (args.len < 2) return error.JetzigMissingArgument;
const map = std.StaticStringMap(Action).initComptime(.{
.{ "migrate", .migrate },
.{ "rollback", .rollback },
.{ "create", .create },
.{ "drop", .drop },
.{ "reflect", .reflect },
});
const action = map.get(args[1]) orelse return error.JetzigUnrecognizedArgument;
const env = try jetzig.Environment.init(allocator, .{ .silent = true });
const repo_env = try jetzig.database.repoEnv(env);
const maybe_database = repo_env.database orelse
if (comptime @hasField(@TypeOf(config), "database")) @as(?[]const u8, config.database) else null;
const database = maybe_database orelse {
std.debug.print("Missing `database` option in `config/database.zig` " ++
"for current environment or `JETQUERY_DATABASE` environment variable.\n", .{});
std.process.exit(1);
return;
};
switch (action) {
.migrate => {
var repo = try migrationsRepo(action, allocator, repo_env);
defer repo.deinit();
try Migrate(config.adapter).init(&repo).migrate();
},
.rollback => {
var repo = try migrationsRepo(action, allocator, repo_env);
defer repo.deinit();
try Migrate(config.adapter).init(&repo).rollback();
},
.create => {
var repo = try migrationsRepo(action, allocator, repo_env);
defer repo.deinit();
try repo.createDatabase(database, .{});
},
.drop => {
if (environment == .production) {
const confirm = std.process.getEnvVarOwned(allocator, confirm_drop_env) catch |err| {
switch (err) {
error.EnvironmentVariableNotFound => {
std.log.err(production_drop_failure_message, .{database});
std.process.exit(1);
},
else => return err,
}
};
if (std.mem.eql(u8, confirm, database)) {
var repo = try migrationsRepo(action, allocator, repo_env);
defer repo.deinit();
try repo.dropDatabase(database, .{});
} else {
std.log.err(production_drop_failure_message, .{database});
std.process.exit(1);
}
} else {
var repo = try migrationsRepo(action, allocator, repo_env);
defer repo.deinit();
try repo.dropDatabase(database, .{});
}
},
.reflect => {
var cwd = try jetzig.util.detectJetzigProjectDir();
defer cwd.close();
const Repo = jetquery.Repo(config.adapter, Schema);
var repo = try Repo.loadConfig(
allocator,
std.enums.nameCast(jetquery.Environment, environment),
.{ .context = .migration, .env = repo_env },
);
const reflect = @import("jetquery_reflect").Reflect(config.adapter, Schema).init(
allocator,
&repo,
.{
.import_jetquery =
\\@import("jetzig").jetquery
,
},
);
const schema = try reflect.generateSchema();
const project_dir = try jetzig.util.detectJetzigProjectDir();
const project_dir_realpath = try project_dir.realpathAlloc(allocator, ".");
const path = try std.fs.path.join(
allocator,
&.{ project_dir_realpath, "src", "app", "database", "Schema.zig" },
);
try jetzig.util.createFile(path, schema);
std.log.info("Database schema written to `{s}`.", .{path});
},
}
}
const MigrationsRepo = jetquery.Repo(config.adapter, MigrateSchema);
fn migrationsRepo(action: Action, allocator: std.mem.Allocator, repo_env: anytype) !MigrationsRepo {
return try MigrationsRepo.loadConfig(
allocator,
std.enums.nameCast(jetquery.Environment, environment),
.{
.admin = switch (action) {
.migrate, .rollback => false,
.create, .drop => true,
.reflect => unreachable, // We use a separate repo for schema reflection.
},
.context = .migration,
.env = repo_env,
},
);
}