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.
This commit is contained in:
Bob Farrell 2024-11-17 19:38:32 +00:00
parent d054e58fa0
commit 92dce21244
36 changed files with 611 additions and 146 deletions

View File

@ -7,8 +7,8 @@
.hash = "1220d0e8734628fd910a73146e804d10a3269e3e7d065de6bb0e3e88d5ba234eb163",
},
.zmpl = .{
.url = "https://github.com/jetzig-framework/zmpl/archive/7b7452bc7fdb0bd2f8a6a9c4e9312900b486aeba.tar.gz",
.hash = "1220ed127f38fa51df53a85b3cc2030a7555e34058db7fd374ebaef817abb43d35f7",
.url = "https://github.com/jetzig-framework/zmpl/archive/25b91d030b992631d319adde1cf01baecd9f3934.tar.gz",
.hash = "12208dd5a4bf0c6c7efc4e9f37a5d8ed80d6004d5680176d1fc2114bfa593e927baf",
},
.jetkv = .{
.url = "https://github.com/jetzig-framework/jetkv/archive/2b1130a48979ea2871c8cf6ca89c38b1e7062839.tar.gz",

208
cli/colors.zig Normal file
View File

@ -0,0 +1,208 @@
const std = @import("std");
// Must be consistent with `std.io.tty.Color` for Windows compatibility.
pub const codes = .{
.escape = "\x1b[",
.black = "30m",
.red = "31m",
.green = "32m",
.yellow = "33m",
.blue = "34m",
.magenta = "35m",
.cyan = "36m",
.white = "37m",
.bright_black = "90m",
.bright_red = "91m",
.bright_green = "92m",
.bright_yellow = "93m",
.bright_blue = "94m",
.bright_magenta = "95m",
.bright_cyan = "96m",
.bright_white = "97m",
.bold = "1m",
.dim = "2m",
.reset = "0m",
};
/// Map color codes generated by `std.io.tty.Config.setColor` back to `std.io.tty.Color`. Used by
/// `jetzig.util.writeAnsi` to parse escape codes so they can be passed to
/// `std.io.tty.Config.setColor` (using Windows API to set console color mode).
const ansi_colors = .{
.{ "30", .black },
.{ "31", .red },
.{ "32", .green },
.{ "33", .yellow },
.{ "34", .blue },
.{ "35", .magenta },
.{ "36", .cyan },
.{ "37", .white },
.{ "90", .bright_black },
.{ "91", .bright_red },
.{ "92", .bright_green },
.{ "93", .bright_yellow },
.{ "94", .bright_blue },
.{ "95", .bright_magenta },
.{ "96", .bright_cyan },
.{ "97", .bright_white },
.{ "1", .bold },
.{ "2", .dim },
.{ "0", .reset },
};
pub const codes_map = if (@hasDecl(std, "ComptimeStringMap"))
std.ComptimeStringMap(std.io.tty.Color, ansi_colors)
else if (@hasDecl(std, "StaticStringMap"))
std.StaticStringMap(std.io.tty.Color).initComptime(ansi_colors)
else
unreachable;
// Map basic ANSI color codes to Windows TextAttribute colors
// used by std.os.windows.SetConsoleTextAttribute()
const windows_colors = .{
.{ "30", 0 },
.{ "31", 4 },
.{ "32", 2 },
.{ "33", 6 },
.{ "34", 1 },
.{ "35", 5 },
.{ "36", 3 },
.{ "37", 7 },
.{ "90", 8 },
.{ "91", 12 },
.{ "92", 10 },
.{ "93", 14 },
.{ "94", 9 },
.{ "95", 13 },
.{ "96", 11 },
.{ "97", 15 },
.{ "1", 7 },
.{ "2", 7 },
.{ "0", 7 },
};
pub const windows_map = if (@hasDecl(std, "ComptimeStringMap"))
std.ComptimeStringMap(u16, windows_colors)
else if (@hasDecl(std, "StaticStringMap"))
std.StaticStringMap(u16).initComptime(windows_colors)
else
unreachable;
/// Colorize a log message. Note that we force `.escape_codes` when we are a TTY even on Windows.
/// `jetzig.loggers.LogQueue` parses the ANSI codes and uses `std.io.tty.Config.setColor` to
/// invoke the appropriate Windows API call to set the terminal color before writing each token.
/// We must do it this way because Windows colors are set by API calls at the time of write, not
/// encoded into the message string.
pub fn colorize(color: std.io.tty.Color, buf: []u8, input: []const u8, is_colorized: bool) ![]const u8 {
if (!is_colorized) return input;
const config: std.io.tty.Config = .escape_codes;
var stream = std.io.fixedBufferStream(buf);
const writer = stream.writer();
try config.setColor(writer, color);
try writer.writeAll(input);
try config.setColor(writer, .reset);
return stream.getWritten();
}
fn wrap(comptime attribute: []const u8, comptime message: []const u8) []const u8 {
return codes.escape ++ attribute ++ message ++ codes.escape ++ codes.reset;
}
fn runtimeWrap(allocator: std.mem.Allocator, attribute: []const u8, message: []const u8) ![]const u8 {
return try std.mem.join(
allocator,
"",
&[_][]const u8{ codes.escape, attribute, message, codes.escape, codes.reset },
);
}
pub fn bold(comptime message: []const u8) []const u8 {
return codes.escape ++ codes.bold ++ message ++ codes.escape ++ codes.reset;
}
pub fn black(comptime message: []const u8) []const u8 {
return wrap(codes.black, message);
}
pub fn runtimeBlack(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
return try runtimeWrap(allocator, codes.black, message);
}
pub fn red(comptime message: []const u8) []const u8 {
return wrap(codes.red, message);
}
pub fn runtimeRed(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
return try runtimeWrap(allocator, codes.red, message);
}
pub fn green(comptime message: []const u8) []const u8 {
return wrap(codes.green, message);
}
pub fn runtimeGreen(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
return try runtimeWrap(allocator, codes.green, message);
}
pub fn yellow(comptime message: []const u8) []const u8 {
return wrap(codes.yellow, message);
}
pub fn runtimeYellow(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
return try runtimeWrap(allocator, codes.yellow, message);
}
pub fn blue(comptime message: []const u8) []const u8 {
return wrap(codes.blue, message);
}
pub fn runtimeBlue(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
return try runtimeWrap(allocator, codes.blue, message);
}
pub fn magenta(comptime message: []const u8) []const u8 {
return wrap(codes.magenta, message);
}
pub fn runtimeMagenta(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
return try runtimeWrap(allocator, codes.magenta, message);
}
pub fn cyan(comptime message: []const u8) []const u8 {
return wrap(codes.cyan, message);
}
pub fn runtimeCyan(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
return try runtimeWrap(allocator, codes.cyan, message);
}
pub fn white(comptime message: []const u8) []const u8 {
return wrap(codes.white, message);
}
pub fn runtimeWhite(allocator: std.mem.Allocator, message: []const u8) ![]const u8 {
return try runtimeWrap(allocator, codes.white, message);
}
pub fn duration(buf: *[256]u8, delta: i64, is_colorized: bool) ![]const u8 {
if (!is_colorized) {
return try std.fmt.bufPrint(
buf,
"{}",
.{std.fmt.fmtDurationSigned(delta)},
);
}
const color: std.io.tty.Color = if (delta < 1000000)
.green
else if (delta < 5000000)
.yellow
else
.red;
var duration_buf: [256]u8 = undefined;
const formatted_duration = try std.fmt.bufPrint(
&duration_buf,
"{}",
.{std.fmt.fmtDurationSigned(delta)},
);
return try colorize(color, buf, formatted_duration, true);
}

View File

@ -5,17 +5,16 @@ const util = @import("../util.zig");
/// Command line options for the `update` command.
pub const Options = struct {
pub const meta = .{
.usage_summary = "[password]",
.usage_summary = "[init|create]",
.full_text =
\\Generates a password
\\Manage user authentication. Initialize with `init` to generate a users table migration.
\\
\\Example:
\\
\\ jetzig update
\\ jetzig update web
\\ jetzig auth init
\\ jetzig auth create bob@jetzig.dev
,
.option_docs = .{
.path = "Set the output path relative to the current directory (default: current directory)",
},
.option_docs = .{},
};
};
@ -32,9 +31,10 @@ pub fn run(
defer arena.deinit();
const allocator = arena.allocator();
const Action = enum { user_create };
const Action = enum { init, create };
const map = std.StaticStringMap(Action).initComptime(.{
.{ "user:create", .user_create },
.{ "init", .init },
.{ "create", .create },
});
const action = if (main_options.positionals.len > 0)
@ -55,7 +55,20 @@ pub fn run(
break :blk error.JetzigCommandError;
} else if (action) |capture|
switch (capture) {
.user_create => blk: {
.init => {
const argv = [_][]const u8{
"jetzig",
"generate",
"migration",
"create_users",
"table:users",
"column:email:string:index:unique",
"column:password_hash:string",
};
try util.runCommand(allocator, &argv);
try util.print(.success, "Migration created. Run `jetzig database update` to run migration and reflect database.", .{});
},
.create => blk: {
if (sub_args.len < 1) {
std.debug.print("Missing argument. Expected an email/username parameter.\n", .{});
break :blk error.JetzigCommandError;

View File

@ -149,7 +149,7 @@ pub fn run(
const tmpdir_real_path = try tmpdir.realpathAlloc(allocator, ".");
defer allocator.free(tmpdir_real_path);
try util.runCommand(allocator, tmpdir_real_path, tar_argv.items);
try util.runCommandInDir(allocator, tar_argv.items, .{ .path = tmpdir_real_path });
switch (builtin.os.tag) {
.windows => {},
@ -215,7 +215,7 @@ fn zig_build_install(allocator: std.mem.Allocator, path: []const u8, options: Op
defer project_dir.close();
project_dir.makePath(".bundle") catch {};
try util.runCommand(allocator, path, install_argv.items);
try util.runCommandInDir(allocator, install_argv.items, .{ .path = path });
const install_bin_path = try std.fs.path.join(allocator, &[_][]const u8{ ".bundle", "bin" });
defer allocator.free(install_bin_path);

View File

@ -9,12 +9,15 @@ const rollback = @import("database/rollback.zig");
const create = @import("database/create.zig");
const drop = @import("database/drop.zig");
const reflect = @import("database/reflect.zig");
const update = @import("database/update.zig");
const setup = @import("database/setup.zig");
pub const confirm_drop_env = "JETZIG_DROP_PRODUCTION_DATABASE";
/// Command line options for the `database` command.
pub const Options = struct {
pub const meta = .{
.usage_summary = "[migrate|rollback|create|drop|reflect]",
.usage_summary = "[setup|create|drop|migrate|rollback|reflect|update]",
.full_text =
\\Manage the application's database.
\\
@ -38,13 +41,15 @@ pub fn run(
defer arena.deinit();
const alloc = arena.allocator();
const Action = enum { migrate, rollback, create, drop, reflect };
const Action = enum { migrate, rollback, create, drop, reflect, update, setup };
const map = std.StaticStringMap(Action).initComptime(.{
.{ "migrate", .migrate },
.{ "rollback", .rollback },
.{ "create", .create },
.{ "drop", .drop },
.{ "reflect", .reflect },
.{ "update", .update },
.{ "setup", .setup },
});
const action = if (main_options.positionals.len > 0)
@ -73,6 +78,8 @@ pub fn run(
.create => create.run(alloc, cwd, sub_args, options, T, main_options),
.drop => drop.run(alloc, cwd, sub_args, options, T, main_options),
.reflect => reflect.run(alloc, cwd, sub_args, options, T, main_options),
.update => update.run(alloc, cwd, sub_args, options, T, main_options),
.setup => setup.run(alloc, cwd, sub_args, options, T, main_options),
};
};
}

View File

@ -0,0 +1,54 @@
const std = @import("std");
const cli = @import("../../cli.zig");
const util = @import("../../util.zig");
pub fn run(
allocator: std.mem.Allocator,
cwd: std.fs.Dir,
args: []const []const u8,
options: cli.database.Options,
T: type,
main_options: T,
) !void {
_ = cwd;
_ = options;
if (main_options.options.help or args.len != 0) {
std.debug.print(
\\Set up a database: create a database, run migrations, reflect schema.
\\
\\Convenience wrapper for:
\\
\\* jetzig database create
\\* jetzig database update
\\
\\Example:
\\
\\ jetzig database setup
\\ jetzig --environment=testing setup
\\
, .{});
return if (main_options.options.help) {} else error.JetzigCommandError;
}
const env = main_options.options.environment;
try runCommand(allocator, env, "create");
try runCommand(allocator, env, "migrate");
try runCommand(allocator, env, "reflect");
try util.print(
.success,
"Database created, migrations applied, and Schema generated successfully.",
.{},
);
}
fn runCommand(allocator: std.mem.Allocator, environment: anytype, comptime action: []const u8) !void {
try util.runCommand(allocator, &.{
"zig",
"build",
util.environmentBuildOption(environment),
"jetzig:database:" ++ action,
});
}

View File

@ -0,0 +1,45 @@
const std = @import("std");
const cli = @import("../../cli.zig");
const util = @import("../../util.zig");
pub fn run(
allocator: std.mem.Allocator,
cwd: std.fs.Dir,
args: []const []const u8,
options: cli.database.Options,
T: type,
main_options: T,
) !void {
_ = cwd;
_ = options;
if (main_options.options.help or args.len != 0) {
std.debug.print(
\\Update a database: run migrations and reflect schema.
\\
\\Convenience wrapper for `jetzig database migrate` and `jetzig database reflect`.
\\
\\Example:
\\
\\ jetzig database update
\\ jetzig --environment=testing update
\\
, .{});
return if (main_options.options.help) {} else error.JetzigCommandError;
}
try util.runCommand(allocator, &.{
"zig",
"build",
util.environmentBuildOption(main_options.options.environment),
"jetzig:database:migrate",
});
try util.runCommand(allocator, &.{
"zig",
"build",
util.environmentBuildOption(main_options.options.environment),
"jetzig:database:reflect",
});
}

View File

@ -47,7 +47,7 @@ pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, he
\\//
\\// Arguments:
\\// * allocator: Arena allocator for use during the job execution process.
\\// * params: Params assigned to a job (from a request, any values added to `data`).
\\// * params: Params assigned to a job (from a request, values added to response data).
\\// * env: Provides the following fields:
\\// - logger: Logger attached to the same stream as the Jetzig server.
\\// - environment: Enum of `{ production, development }`.

View File

@ -194,12 +194,16 @@ pub fn run(
null,
);
try util.runCommand(allocator, realpath, &[_][]const u8{
try util.runCommandInDir(
allocator,
&[_][]const u8{
"zig",
"fetch",
"--save",
github_url,
});
},
.{ .dir = install_dir },
);
// TODO: Use arg or interactive prompt to do Git setup in net project, default to no.
// const git_setup = false;
@ -322,22 +326,34 @@ fn promptInput(
// Initialize a new Git repository when setting up a new project (optional).
fn gitSetup(allocator: std.mem.Allocator, install_dir: *std.fs.Dir) !void {
try util.runCommand(allocator, install_dir, &[_][]const u8{
try util.runCommandInDir(
allocator,
&[_][]const u8{
"git",
"init",
".",
});
},
.{ .path = install_dir },
);
try util.runCommand(allocator, install_dir, &[_][]const u8{
try util.runCommandInDir(
allocator,
&[_][]const u8{
"git",
"add",
".",
});
},
.{ .path = install_dir },
);
try util.runCommand(allocator, install_dir, &[_][]const u8{
try util.runCommandInDir(
allocator,
&[_][]const u8{
"git",
"commit",
"-m",
"Initialize Jetzig project",
});
},
.{ .path = install_dir },
);
}

View File

@ -63,9 +63,8 @@ pub fn run(
);
while (true) {
util.runCommand(
util.runCommandInDir(
allocator,
realpath,
&.{
"zig",
"build",
@ -75,6 +74,7 @@ pub fn run(
"--color",
"on",
},
.{ .path = realpath },
) catch {
std.debug.print("Build failed, waiting for file change...\n", .{});
try awaitFileChange(allocator, cwd, &mtime);

View File

@ -55,18 +55,15 @@ pub fn run(
const save_arg = try std.mem.concat(allocator, u8, &[_][]const u8{ "--save=", name });
defer allocator.free(save_arg);
var cwd = try util.detectJetzigProjectDir();
defer cwd.close();
const realpath = try std.fs.realpathAlloc(allocator, ".");
defer allocator.free(realpath);
try util.runCommand(allocator, realpath, &[_][]const u8{
try util.runCommand(
allocator,
&[_][]const u8{
"zig",
"fetch",
save_arg,
github_url,
});
},
);
std.debug.print(
\\Update complete.

View File

@ -2,6 +2,7 @@ const std = @import("std");
const builtin = @import("builtin");
const cli = @import("cli.zig");
const colors = @import("colors.zig");
/// Decode a base64 string, used for parsing out build artifacts generated by the CLI program's
/// build.zig which are stored in the executable as a module.
@ -16,14 +17,35 @@ pub fn base64Decode(allocator: std.mem.Allocator, input: []const u8) ![]const u8
return ptr;
}
const icons = .{
.check = "",
.cross = "",
};
/// Print a success confirmation.
pub fn printSuccess() void {
std.debug.print("\n", .{});
std.debug.print(" " ++ icons.check ++ "\n", .{});
}
/// Print a failure confirmation.
pub fn printFailure() void {
std.debug.print("\n", .{});
std.debug.print(" " ++ icons.cross ++ "\n", .{});
}
const PrintContext = enum { success, failure };
/// Print some output in with a given context to stderr.
pub fn print(comptime context: PrintContext, comptime message: []const u8, args: anytype) !void {
const writer = std.io.getStdErr().writer();
switch (context) {
.success => try writer.print(
std.fmt.comptimePrint("{s} {s}\n", .{ icons.check, colors.green(message) }),
args,
),
.failure => try writer.print(
std.fmt.comptimePrint("{s} {s}\n", .{ icons.cross, colors.red(message) }),
args,
),
}
}
/// Detects a Jetzig project directory either in the current directory or one of its parent
@ -126,9 +148,32 @@ pub fn runCommandStreaming(allocator: std.mem.Allocator, install_path: []const u
_ = try child.wait();
}
/// Runs a command as a child process and verifies successful exit code.
pub fn runCommand(allocator: std.mem.Allocator, install_path: []const u8, argv: []const []const u8) !void {
const result = try std.process.Child.run(.{ .allocator = allocator, .argv = argv, .cwd = install_path });
/// Runs a command as a child process in Jetzig project directory and verifies successful exit
/// code.
pub fn runCommand(allocator: std.mem.Allocator, argv: []const []const u8) !void {
var dir = try detectJetzigProjectDir();
defer dir.close();
try runCommandInDir(allocator, argv, .{ .dir = dir });
}
const Dir = union(enum) {
path: []const u8,
dir: std.fs.Dir,
};
/// Runs a command as a child process in the given directory and verifies successful exit code.
pub fn runCommandInDir(allocator: std.mem.Allocator, argv: []const []const u8, dir: Dir) !void {
const cwd_path = switch (dir) {
.path => |capture| capture,
.dir => |capture| try capture.realpathAlloc(allocator, "."),
};
defer if (dir == .dir) allocator.free(cwd_path);
const result = try std.process.Child.run(.{
.allocator = allocator,
.argv = argv,
.cwd = cwd_path,
});
defer allocator.free(result.stdout);
defer allocator.free(result.stderr);

View File

@ -28,12 +28,11 @@ pub const defaults: jetzig.mail.DefaultMailParams = .{
pub fn deliver(
allocator: std.mem.Allocator,
mail: *jetzig.mail.MailParams,
data: *jetzig.data.Data,
params: *jetzig.data.Value,
env: jetzig.jobs.JobEnv,
) !void {
_ = allocator;
try params.put("email_message", data.string("Custom email message"));
try params.put("email_message", "Custom email message");
try env.logger.INFO("Delivering email with subject: '{?s}'", .{mail.get(.subject)});
}

View File

@ -1,7 +1,7 @@
const jetzig = @import("jetzig");
pub fn bar(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
var root = try data.object();
try root.put("id", data.string(id));
pub fn bar(id: []const u8, request: *jetzig.Request) !jetzig.View {
var root = try request.data(.object);
try root.put("id", id);
return request.render(.ok);
}

View File

@ -1,13 +1,12 @@
const std = @import("std");
const jetzig = @import("jetzig");
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
_ = data;
pub fn index(request: *jetzig.Request) !jetzig.View {
return request.render(.ok);
}
pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
var root = try data.root(.object);
pub fn post(request: *jetzig.Request) !jetzig.View {
var root = try request.data(.object);
const params = try request.params();

View File

@ -2,8 +2,8 @@ const std = @import("std");
const jetzig = @import("jetzig");
/// This example demonstrates usage of Jetzig's KV store.
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
var root = try data.object();
pub fn index(request: *jetzig.Request) !jetzig.View {
var root = try request.data(.object);
// Fetch a string from the KV store. If it exists, store it in the root data object,
// otherwise store a string value to be picked up by the next request.
@ -11,7 +11,7 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
try root.put("stored_string", capture);
} else {
try root.put("stored_string", null);
try request.store.put("example-key", data.string("example-value"));
try request.store.put("example-key", "example-value");
}
// Left-pop an item from an array and store it in the root data object. This will empty the
@ -21,9 +21,9 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
try root.put("popped", value);
} else {
// Store some values in an array in the KV store.
try request.store.append("example-array", data.string("hello"));
try request.store.append("example-array", data.string("goodbye"));
try request.store.append("example-array", data.string("hello again"));
try request.store.append("example-array", "hello");
try request.store.append("example-array", "goodbye");
try request.store.append("example-array", "hello again");
try root.put("popped", null);
}

View File

@ -1,8 +1,7 @@
const std = @import("std");
const jetzig = @import("jetzig");
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
_ = data;
pub fn index(request: *jetzig.Request) !jetzig.View {
return request.render(.ok);
}
@ -13,9 +12,9 @@ pub const static_params = .{
},
};
pub fn get(id: []const u8, request: *jetzig.StaticRequest, data: *jetzig.Data) !jetzig.View {
var object = try data.object();
try object.put("id", data.string(id));
pub fn get(id: []const u8, request: *jetzig.StaticRequest) !jetzig.View {
var object = try request.data(.object);
try object.put("id", id);
const params = try request.params();
if (params.get("foo")) |value| try object.put("foo", value);
return request.render(.ok);

View File

@ -42,6 +42,14 @@ test "post query params" {
},
});
try response2.expectStatus(.unprocessable_entity);
const response3 = try app.request(.POST, "/params", .{
.params = .{
.name = "", // empty param
.favorite_animal = "raccoon",
},
});
try response3.expectStatus(.unprocessable_entity);
}
test "post json" {

View File

@ -1,14 +1,13 @@
const std = @import("std");
const jetzig = @import("jetzig");
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
_ = data;
pub fn index(request: *jetzig.Request) !jetzig.View {
const params = try request.params();
if (params.get("redirect")) |location| {
switch (location.*) {
// Value is `.Null` when param is empty, e.g.:
// Value is `.null` when param is empty, e.g.:
// `http://localhost:8080/redirect?redirect`
.Null => return request.redirect("http://www.example.com/", .moved_permanently),
.null => return request.redirect("http://www.example.com/", .moved_permanently),
// Value is `.string` when param is present, e.g.:
// `http://localhost:8080/redirect?redirect=https://jetzig.dev/`
.string => |string| return request.redirect(string.value, .moved_permanently),

View File

@ -7,6 +7,7 @@ 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, " ++
@ -20,6 +21,15 @@ 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();

33
src/commands/util.zig Normal file
View File

@ -0,0 +1,33 @@
const std = @import("std");
const colors = @import("jetzig").colors;
const icons = .{
.check = "",
.cross = "",
};
/// Print a success confirmation.
pub fn printSuccess() void {
std.debug.print(" " ++ icons.check ++ "\n", .{});
}
/// Print a failure confirmation.
pub fn printFailure() void {
std.debug.print(" " ++ icons.cross ++ "\n", .{});
}
const PrintContext = enum { success, failure };
/// Print some output in with a given context to stderr.
pub fn print(comptime context: PrintContext, comptime message: []const u8, args: anytype) !void {
const writer = std.io.getStdErr().writer();
switch (context) {
.success => try writer.print(
std.fmt.comptimePrint("{s} {s}\n", .{ icons.check, colors.green(message) }),
args,
),
.failure => try writer.print(
std.fmt.comptimePrint("{s} {s}\n", .{ icons.cross, colors.red(message) }),
args,
),
}
}

View File

@ -186,13 +186,13 @@ test "query string with param without value" {
const foo = data.get("foo").?;
try switch (foo.*) {
.Null => {},
.null => {},
else => std.testing.expect(false),
};
const bar = data.get("bar").?;
try switch (bar.*) {
.Null => {},
.null => {},
else => std.testing.expect(false),
};
}

View File

@ -60,8 +60,9 @@ pub const RequestStore = struct {
}
/// Get a String from the store.
pub fn put(self: RequestStore, key: []const u8, value: *jetzig.data.Value) !void {
try self.store.put(key, value);
pub fn put(self: RequestStore, key: []const u8, value: anytype) !void {
const alloc = (try self.data()).allocator();
try self.store.put(key, try jetzig.Data.zmplValue(value, alloc));
}
/// Remove a String to from the key-value store and return it if found.
@ -75,13 +76,15 @@ pub const RequestStore = struct {
}
/// Append a Value to the end of an Array in the key-value store.
pub fn append(self: RequestStore, key: []const u8, value: *jetzig.data.Value) !void {
try self.store.append(key, value);
pub fn append(self: RequestStore, key: []const u8, value: anytype) !void {
const alloc = (try self.data()).allocator();
try self.store.append(key, try jetzig.Data.zmplValue(value, alloc));
}
/// Prepend a Value to the start of an Array in the key-value store.
pub fn prepend(self: RequestStore, key: []const u8, value: *jetzig.data.Value) !void {
try self.store.prepend(key, value);
pub fn prepend(self: RequestStore, key: []const u8, value: anytype) !void {
const alloc = (try self.data()).allocator();
try self.store.prepend(key, try jetzig.Data.zmplValue(value, alloc));
}
/// Pop a String from an Array in the key-value store.
@ -660,7 +663,7 @@ pub fn joinPath(self: *const Request, args: anytype) ![]const u8 {
@compileError("Cannot coerce type `" ++ @typeName(field.type) ++ "` to string."),
else => args[index], // Assume []const u8, let Zig do the work.
},
.int, .float => try std.fmt.allocPrint(self.allocator, "{d}", args[index]),
.int, .float => try std.fmt.allocPrint(self.allocator, "{d}", .{args[index]}),
else => @compileError("Cannot coerce type `" ++ @typeName(field.type) ++ "` to string."),
};
}

View File

@ -166,7 +166,7 @@ fn renderResponse(self: *Server, request: *jetzig.http.Request) !void {
const static_resource = self.matchStaticResource(request) catch |err| {
if (isUnhandledError(err)) return err;
const rendered = try self.renderInternalServerError(request, err);
const rendered = try self.renderInternalServerError(request, @errorReturnTrace(), err);
request.setResponse(rendered, .{});
return;
};
@ -220,7 +220,11 @@ fn renderHTML(
if (zmpl.findPrefixed("views", matched_route.template)) |template| {
const rendered = self.renderView(matched_route, request, template) catch |err| {
if (isUnhandledError(err)) return err;
const rendered_error = try self.renderInternalServerError(request, err);
const rendered_error = try self.renderInternalServerError(
request,
@errorReturnTrace(),
err,
);
return request.setResponse(rendered_error, .{});
};
return request.setResponse(rendered, .{});
@ -229,7 +233,7 @@ fn renderHTML(
// assigned in a view.
const rendered = self.renderView(matched_route, request, null) catch |err| {
if (isUnhandledError(err)) return err;
const rendered_error = try self.renderInternalServerError(request, err);
const rendered_error = try self.renderInternalServerError(request, @errorReturnTrace(), err);
return request.setResponse(rendered_error, .{});
};
@ -296,10 +300,9 @@ fn renderView(
// `return request.render(.ok)`, but the actual rendered view is stored in
// `request.rendered_view`.
_ = route.render(route, request) catch |err| {
try self.logger.ERROR("Encountered error: {s}", .{@errorName(err)});
if (isUnhandledError(err)) return err;
if (isBadRequest(err)) return try self.renderBadRequest(request);
return try self.renderInternalServerError(request, err);
return try self.renderInternalServerError(request, @errorReturnTrace(), err);
};
if (request.failed) {
@ -414,10 +417,15 @@ fn isBadHttpError(err: anyerror) bool {
};
}
fn renderInternalServerError(self: *Server, request: *jetzig.http.Request, err: anyerror) !RenderedView {
fn renderInternalServerError(
self: *Server,
request: *jetzig.http.Request,
stack_trace: ?*std.builtin.StackTrace,
err: anyerror,
) !RenderedView {
request.response_data.reset();
try self.logger.logError(err);
try self.logger.logError(stack_trace, err);
const status = .internal_server_error;
return try self.renderError(request, status);
@ -460,13 +468,11 @@ fn renderErrorView(
_ = route.render(route.*, request) catch |err| {
if (isUnhandledError(err)) return err;
try self.logger.logError(err);
try self.logger.logError(@errorReturnTrace(), err);
try self.logger.ERROR(
"Unexepected error occurred while rendering error page: {s}",
.{@errorName(err)},
);
const stack = @errorReturnTrace();
if (stack) |capture| try self.logStackTrace(capture, request.allocator);
return try renderDefaultError(request, status_code);
};

View File

@ -1,12 +1,12 @@
const std = @import("std");
const Self = @This();
const StaticRequest = @This();
const jetzig = @import("../../jetzig.zig");
response_data: *jetzig.data.Data,
allocator: std.mem.Allocator,
json: []const u8,
pub fn init(allocator: std.mem.Allocator, json: []const u8) !Self {
pub fn init(allocator: std.mem.Allocator, json: []const u8) !StaticRequest {
return .{
.allocator = allocator,
.response_data = try allocator.create(jetzig.data.Data),
@ -14,31 +14,35 @@ pub fn init(allocator: std.mem.Allocator, json: []const u8) !Self {
};
}
pub fn deinit(self: *Self) void {
pub fn deinit(self: *StaticRequest) void {
_ = self;
}
pub fn render(self: *Self, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View {
pub fn render(self: *StaticRequest, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View {
return .{ .data = self.response_data, .status_code = status_code };
}
pub fn resourceId(self: *Self) ![]const u8 {
var data = try self.allocator.create(jetzig.data.Data);
data.* = jetzig.data.Data.init(self.allocator);
defer self.allocator.destroy(data);
defer data.deinit();
pub fn data(self: *StaticRequest, comptime root: @TypeOf(.enum_literal)) !*jetzig.Data.Value {
return try self.response_data.root(root);
}
try data.fromJson(self.json);
pub fn resourceId(self: *StaticRequest) ![]const u8 {
var params_data = try self.allocator.create(jetzig.data.Data);
params_data.* = jetzig.data.Data.init(self.allocator);
defer self.allocator.destroy(params_data);
defer params_data.deinit();
try params_data.fromJson(self.json);
// Routes generator rejects missing `.id` option so this should always be present.
// Note that static requests are never rendered at runtime so we can be unsafe here and risk
// failing a build (which would not be coherent if we allowed it to complete).
return try self.allocator.dupe(u8, data.value.?.get("id").?.string.value);
return try self.allocator.dupe(u8, params_data.value.?.get("id").?.string.value);
}
/// Returns the static params defined by `pub const static_params` in the relevant view.
pub fn params(self: *Self) !*jetzig.data.Value {
var data = try self.allocator.create(jetzig.data.Data);
data.* = jetzig.data.Data.init(self.allocator);
try data.fromJson(self.json);
return data.value.?.get("params") orelse data.object();
pub fn params(self: *StaticRequest) !*jetzig.data.Value {
var params_data = try self.allocator.create(jetzig.data.Data);
params_data.* = jetzig.data.Data.init(self.allocator);
try params_data.fromJson(self.json);
return params_data.value.?.get("params") orelse params_data.object();
}

View File

@ -13,7 +13,13 @@ pub fn expectParams(request: *jetzig.http.Request, T: type) !?T {
var failed = false;
inline for (fields, 0..) |field, index| {
if (actual_params.get(field.name)) |value| {
var maybe_value = actual_params.get(field.name);
if (isBlank(maybe_value)) {
maybe_value = null;
}
if (maybe_value) |value| {
switch (@typeInfo(field.type)) {
.optional => |info| if (value.coerce(info.child)) |coerced| {
@field(t, field.name) = coerced;
@ -66,6 +72,12 @@ pub fn expectParams(request: *jetzig.http.Request, T: type) !?T {
return t;
}
fn isBlank(maybe_value: ?*const jetzig.Data.Value) bool {
if (maybe_value) |value| {
return value.* == .string and jetzig.util.strip(value.string.value).len == 0;
} else return true;
}
/// See `Request.paramsInfo`.
pub const ParamsInfo = struct {
params: std.BoundedArray(ParamInfo, 1024),

View File

@ -44,7 +44,7 @@ pub fn deinit(self: *Store) void {
self.store.deinit();
}
/// Put a Value or into the key-value store.
/// Put a or into the key-value store.
pub fn put(self: *Store, key: []const u8, value: *jetzig.data.Value) !void {
try self.store.put(key, try value.toJson());
}

View File

@ -83,9 +83,13 @@ pub const Logger = union(enum) {
}
}
pub fn logError(self: *const Logger, err: anyerror) !void {
pub fn logError(
self: *const Logger,
stack_trace: ?*std.builtin.StackTrace,
err: anyerror,
) !void {
switch (self.*) {
inline else => |*logger| try logger.logError(err),
inline else => |*logger| try logger.logError(stack_trace, err),
}
}

View File

@ -238,17 +238,13 @@ fn printSql(self: *const DevelopmentLogger, sql: []const u8) !void {
try self.print(.INFO, "{s}", .{stream.getWritten()});
}
pub fn logError(self: *const DevelopmentLogger, err: anyerror) !void {
if (@errorReturnTrace()) |stack| {
try self.log(.ERROR, "\nStack Trace:\n{}", .{stack});
var buf = std.ArrayList(u8).init(self.allocator);
defer buf.deinit();
const writer = buf.writer();
try stack.format("", .{}, writer);
try self.logger.ERROR("{s}\n", .{buf.items});
}
pub fn logError(self: *const DevelopmentLogger, stack_trace: ?*std.builtin.StackTrace, err: anyerror) !void {
if (stack_trace) |stack| {
try self.log(.ERROR, "Encountered Error: {s}", .{@errorName(err)});
try self.log(.ERROR, "Stack trace:\n{}", .{stack});
} else {
try self.log(.ERROR, "Encountered Error: {s}", .{@errorName(err)});
}
}
fn logFile(self: DevelopmentLogger, comptime level: jetzig.loggers.LogLevel) std.fs.File {

View File

@ -112,7 +112,13 @@ pub fn logSql(self: *const JsonLogger, event: jetzig.jetquery.events.Event) !voi
try self.log_queue.print("{s}\n", .{stream.getWritten()}, .stdout);
}
pub fn logError(self: *const JsonLogger, err: anyerror) !void {
pub fn logError(
self: *const JsonLogger,
stack_trace: ?*std.builtin.StackTrace,
err: anyerror,
) !void {
// TODO: Format this as JSON and include line number/column if available.
_ = stack_trace;
try self.log(.ERROR, "Encountered error: {s}", .{@errorName(err)});
}

View File

@ -19,7 +19,8 @@ pub inline fn logRequest(self: @This(), request: *const jetzig.http.Request) !vo
_ = request;
}
pub inline fn logError(self: @This(), err: anyerror) !void {
pub inline fn logError(self: @This(), stack_trace: ?*std.builtin.StackTrace, err: anyerror) !void {
_ = self;
_ = stack_trace;
std.debug.print("Error: {s}\n", .{@errorName(err)});
}

View File

@ -141,6 +141,8 @@ const sql_tokens = .{
"VALUES",
};
pub fn logError(self: *const ProductionLogger, err: anyerror) !void {
pub fn logError(self: *const ProductionLogger, stack_trace: ?*std.builtin.StackTrace, err: anyerror) !void {
// TODO: Include line number/column if available.
_ = stack_trace;
try self.log(.ERROR, "Encountered Error: {s}", .{@errorName(err)});
}

View File

@ -46,7 +46,9 @@ pub fn logSql(self: TestLogger, event: jetzig.jetquery.events.Event) !void {
try self.log(.INFO, "[database] {?s}", .{event.sql});
}
pub fn logError(self: TestLogger, err: anyerror) !void {
pub fn logError(self: TestLogger, stack_trace: ?*std.builtin.StackTrace, err: anyerror) !void {
// TODO: Output useful debug info from stack trace
_ = stack_trace;
try self.log(.ERROR, "Encountered error: {s}", .{@errorName(err)});
}

View File

@ -4,7 +4,7 @@ const jetzig = @import("../../jetzig.zig");
/// Default Mail Job. Send an email with the given params in the background.
pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig.jobs.JobEnv) !void {
const mailer_name = if (params.get("mailer_name")) |param| switch (param.*) {
.Null => null,
.null => null,
.string => |string| string.value,
else => null,
} else null;
@ -34,9 +34,7 @@ pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig
.defaults = mailer.defaults,
};
var data = jetzig.data.Data.init(allocator);
try mailer.deliverFn(allocator, &mail_params, &data, params.get("params").?, env);
try mailer.deliverFn(allocator, &mail_params, params.get("params").?, env);
const mail = jetzig.mail.Mail.init(allocator, env, .{
.subject = mail_params.get(.subject) orelse "(No subject)",
@ -62,7 +60,7 @@ pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig
fn resolveSubject(subject: ?*const jetzig.data.Value) ?[]const u8 {
if (subject) |capture| {
return switch (capture.*) {
.Null => null,
.null => null,
.string => |string| string.value,
else => unreachable,
};
@ -73,7 +71,7 @@ fn resolveSubject(subject: ?*const jetzig.data.Value) ?[]const u8 {
fn resolveFrom(from: ?*const jetzig.data.Value) ?[]const u8 {
return if (from) |capture| switch (capture.*) {
.Null => null,
.null => null,
.string => |string| string.value,
else => unreachable,
} else null;
@ -97,7 +95,7 @@ fn resolveText(
) !?[]const u8 {
if (text) |capture| {
return switch (capture.*) {
.Null => try defaultText(allocator, mailer, params),
.null => try defaultText(allocator, mailer, params),
.string => |string| string.value,
else => unreachable,
};
@ -114,7 +112,7 @@ fn resolveHtml(
) !?[]const u8 {
if (text) |capture| {
return switch (capture.*) {
.Null => try defaultHtml(allocator, mailer, params),
.null => try defaultHtml(allocator, mailer, params),
.string => |string| string.value,
else => unreachable,
};

View File

@ -4,7 +4,6 @@ const jetzig = @import("../../jetzig.zig");
pub const DeliverFn = *const fn (
std.mem.Allocator,
*jetzig.mail.MailParams,
*jetzig.data.Data,
*jetzig.data.Value,
jetzig.jobs.JobEnv,
) anyerror!void;

View File

@ -205,7 +205,7 @@ pub fn expectJson(expected_path: []const u8, expected_value: anytype, response:
},
else => {},
},
.Null => switch (@typeInfo(@TypeOf(expected_value))) {
.null => switch (@typeInfo(@TypeOf(expected_value))) {
.optional => {
if (expected_value == null) return;
},
@ -264,7 +264,7 @@ pub fn expectJson(expected_path: []const u8, expected_value: anytype, response:
else => unreachable,
}
},
.Null => {
.null => {
logFailure(
"Expected value in " ++ jetzig.colors.cyan("{s}") ++ ", found " ++ jetzig.colors.green("null") ++ "\nJSON:" ++ json_banner,
.{ expected_path, try jsonPretty(response) },