From 198754eef247b3553690d4a6dac2c85ea4e3aadf Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Sat, 7 Sep 2024 16:48:41 +0100 Subject: [PATCH] WIP --- src/commands/migrate.zig | 3 +- src/jetzig.zig | 148 +---------------------------- src/jetzig/App.zig | 8 +- src/jetzig/config.zig | 183 ++++++++++++++++++++++++++++++++++++ src/jetzig/database.zig | 38 ++++++++ src/jetzig/http/Request.zig | 12 +++ src/jetzig/http/Server.zig | 4 + 7 files changed, 248 insertions(+), 148 deletions(-) create mode 100644 src/jetzig/config.zig create mode 100644 src/jetzig/database.zig diff --git a/src/commands/migrate.zig b/src/commands/migrate.zig index 7ef07fd..2d828ba 100644 --- a/src/commands/migrate.zig +++ b/src/commands/migrate.zig @@ -14,12 +14,13 @@ pub fn main() !void { const allocator = arena.allocator(); + // FIXME: Load config from app var repo = try jetquery.Repo.init( allocator, .{ .adapter = .{ .postgresql = .{ - .database = "postgres", + .database = "jetzig_website", .username = "postgres", .hostname = "127.0.0.1", .password = "password", diff --git a/src/jetzig.zig b/src/jetzig.zig index 8d340d0..ba55346 100644 --- a/src/jetzig.zig +++ b/src/jetzig.zig @@ -17,7 +17,9 @@ pub const markdown = @import("jetzig/markdown.zig"); pub const jobs = @import("jetzig/jobs.zig"); pub const mail = @import("jetzig/mail.zig"); pub const kv = @import("jetzig/kv.zig"); +pub const database = @import("jetzig/database.zig"); pub const testing = @import("jetzig/testing.zig"); +pub const config = @import("jetzig/config.zig"); /// The primary interface for a Jetzig application. Create an `App` in your application's /// `src/main.zig` and call `start` to launch the application. @@ -69,152 +71,6 @@ pub const root = @import("root"); pub const Global = if (@hasDecl(root, "Global")) root.Global else DefaultGlobal; pub const DefaultGlobal = struct { __jetzig_default: bool }; -/// Global configuration. Override these values by defining in `src/main.zig` with: -/// ```zig -/// pub const jetzig_options = struct { -/// // ... -/// } -/// ``` -/// All constants defined below can be overridden. -pub const config = struct { - /// Maximum bytes to allow in request body. - pub const max_bytes_request_body: usize = std.math.pow(usize, 2, 16); - - /// Maximum filesize for `public/` content. - pub const max_bytes_public_content: usize = std.math.pow(usize, 2, 20); - - /// Maximum filesize for `static/` content (applies only to apps using `jetzig.http.StaticRequest`). - pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 18); - - /// Maximum length of a header name. There is no limit imposed by the HTTP specification but - /// AWS load balancers reference 40 as a limit so we use that as a baseline: - /// https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_HttpHeaderConditionConfig.html - /// This can be increased if needed. - pub const max_bytes_header_name: u16 = 40; - - /// Maximum number of `multipart/form-data`-encoded fields to accept per request. - pub const max_multipart_form_fields: usize = 20; - - /// Log message buffer size. Log messages exceeding this size spill to heap with degraded - /// performance. Log messages should aim to fit in the message buffer. - pub const log_message_buffer_len: usize = 4096; - - /// Maximum log pool size. When a log buffer is no longer required it is returned to a pool - /// for recycling. When logging i/o is slow, a high volume of requests will result in this - /// pool growing. When the pool size reaches the maximum value defined here, log events are - /// freed instead of recycled. - pub const max_log_pool_len: usize = 256; - - /// Number of request threads. Defaults to number of detected CPUs. - pub const thread_count: ?u16 = null; - - /// Per-thread stack memory to use before spilling into request arena (possibly with allocations). - pub const buffer_size: usize = 64 * 1024; - - /// The pre-heated size of each item in the available memory pool used by requests for - /// rendering. Total retained allocation: `worker_count * max_connections`. Requests - /// requiring more memory will allocate per-request, leaving `arena_size` bytes pre-allocated - /// for the next request. - pub const arena_size: usize = 1024 * 1024; - - /// Number of response worker threads. - pub const worker_count: u16 = 4; - - /// Total number of connections managed by worker threads. - pub const max_connections: u16 = 512; - - /// Path relative to cwd() to serve public content from. Symlinks are not followed. - pub const public_content_path = "public"; - - /// Middleware chain. Add any custom middleware here, or use middleware provided in - /// `jetzig.middleware` (e.g. `jetzig.middleware.HtmxMiddleware`). - pub const middleware = &.{}; - - /// HTTP buffer. Must be large enough to store all headers. This should typically not be - /// modified. - pub const http_buffer_size: usize = std.math.pow(usize, 2, 16); - - /// A struct of fragments to use when rendering Markdown templates. - pub const markdown_fragments = zmd.html.DefaultFragments; - - /// The number of worker threads to spawn on startup for processing Jobs (NOT the number of - /// HTTP server worker threads). - pub const job_worker_threads: usize = 1; - - /// Duration before looking for more Jobs when the queue is found to be empty, in - /// milliseconds. - pub const job_worker_sleep_interval_ms: usize = 10; - - /// Key-value store options. Set backend to `.file` to use a file-based store. - /// When using `.file` backend, you must also set `.file_options`. - /// The key-value store is exposed as `request.store` in views and is also available in as - /// `env.store` in all jobs/mailers. - pub const store: kv.Store.KVOptions = .{ - .backend = .memory, - // .backend = .file, - // .file_options = .{ - // .path = "/path/to/jetkv-store.db", - // .truncate = false, // Set to `true` to clear the store on each server launch. - // .address_space_size = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096), - // }, - }; - - /// Job queue options. Identical to `store` options, but allows using different - /// backends (e.g. `.memory` for key-value store, `.file` for jobs queue. - /// The job queue is managed internally by Jetzig. - pub const job_queue: kv.Store.KVOptions = .{ - .backend = .memory, - // .backend = .file, - // .file_options = .{ - // .path = "/path/to/jetkv-queue.db", - // .truncate = false, // Set to `true` to clear the store on each server launch. - // .address_space_size = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096), - // }, - }; - - /// Cache. Identical to `store` options, but allows using different - /// backends (e.g. `.memory` for key-value store, `.file` for cache. - pub const cache: kv.Store.KVOptions = .{ - .backend = .memory, - // .backend = .file, - // .file_options = .{ - // .path = "/path/to/jetkv-cache.db", - // .truncate = false, // Set to `true` to clear the store on each server launch. - // .address_space_size = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096), - // }, - }; - - /// SMTP configuration for Jetzig Mail. - pub const smtp: mail.SMTPConfig = .{ - .port = 25, - .encryption = .none, // .insecure, .none, .tls, .start_tls - .host = "localhost", - .username = null, - .password = null, - }; - - /// HTTP cookie configuration - pub const cookie_options: http.Cookies.CookieOptions = .{ - .domain = "localhost", - .path = "/", - }; - - /// Force email delivery in development mode (instead of printing email body to logger). - pub const force_development_email_delivery = false; - - /// Reconciles a configuration value from user-defined values and defaults provided by Jetzig. - pub fn get(T: type, comptime key: []const u8) T { - const self = @This(); - if (!@hasDecl(self, key)) @panic("Unknown config option: " ++ key); - - if (@hasDecl(root, "jetzig_options") and @hasDecl(root.jetzig_options, key)) { - return @field(root.jetzig_options, key); - } else { - return @field(self, key); - } - } -}; - pub const initHook: ?*const fn (*App) anyerror!void = if (@hasDecl(root, "init")) root.init else null; /// Initialize a new Jetzig app. Call this from `src/main.zig` and then call diff --git a/src/jetzig/App.zig b/src/jetzig/App.zig index 403ea2e..73d426c 100644 --- a/src/jetzig/App.zig +++ b/src/jetzig/App.zig @@ -41,7 +41,6 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void { }; defer for (self.custom_routes.items) |custom_route| { - self.allocator.free(custom_route.view_name); self.allocator.free(custom_route.template); }; @@ -70,6 +69,12 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void { ); defer log_thread.join(); + var repo = try jetzig.database.repo( + self.allocator, + jetzig.config.get(?jetzig.database.DatabaseOptions, "database"), + ); + defer repo.deinit(); + if (self.env.detach) { const argv = try std.process.argsAlloc(self.allocator); defer std.process.argsFree(self.allocator, argv); @@ -96,6 +101,7 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void { &store, &job_queue, &cache, + &repo, options.global, ); diff --git a/src/jetzig/config.zig b/src/jetzig/config.zig new file mode 100644 index 0000000..f84e27e --- /dev/null +++ b/src/jetzig/config.zig @@ -0,0 +1,183 @@ +const std = @import("std"); + +pub const zmpl = @import("zmpl").zmpl; +pub const zmd = @import("zmd").zmd; +pub const jetkv = @import("jetkv").jetkv; +pub const jetquery = @import("jetquery"); + +pub const http = @import("http.zig"); +pub const loggers = @import("loggers.zig"); +pub const data = @import("data.zig"); +pub const views = @import("views.zig"); +pub const colors = @import("colors.zig"); +pub const util = @import("util.zig"); +pub const types = @import("types.zig"); +pub const markdown = @import("markdown.zig"); +pub const jobs = @import("jobs.zig"); +pub const mail = @import("mail.zig"); +pub const kv = @import("kv.zig"); +pub const db = @import("database.zig"); + +const root = @import("root"); + +/// Global configuration. Override these values by defining in `src/main.zig` with: +/// ```zig +/// pub const jetzig_options = struct { +/// // ... +/// } +/// ``` +/// All constants defined below can be overridden. +const config = @This(); + +/// Maximum bytes to allow in request body. +pub const max_bytes_request_body: usize = std.math.pow(usize, 2, 16); + +/// Maximum filesize for `public/` content. +pub const max_bytes_public_content: usize = std.math.pow(usize, 2, 20); + +/// Maximum filesize for `static/` content (applies only to apps using `jetzig.http.StaticRequest`). +pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 18); + +/// Maximum length of a header name. There is no limit imposed by the HTTP specification but +/// AWS load balancers reference 40 as a limit so we use that as a baseline: +/// https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_HttpHeaderConditionConfig.html +/// This can be increased if needed. +pub const max_bytes_header_name: u16 = 40; + +/// Maximum number of `multipart/form-data`-encoded fields to accept per request. +pub const max_multipart_form_fields: usize = 20; + +/// Log message buffer size. Log messages exceeding this size spill to heap with degraded +/// performance. Log messages should aim to fit in the message buffer. +pub const log_message_buffer_len: usize = 4096; + +/// Maximum log pool size. When a log buffer is no longer required it is returned to a pool +/// for recycling. When logging i/o is slow, a high volume of requests will result in this +/// pool growing. When the pool size reaches the maximum value defined here, log events are +/// freed instead of recycled. +pub const max_log_pool_len: usize = 256; + +/// Number of request threads. Defaults to number of detected CPUs. +pub const thread_count: ?u16 = null; + +/// Per-thread stack memory to use before spilling into request arena (possibly with allocations). +pub const buffer_size: usize = 64 * 1024; + +/// The pre-heated size of each item in the available memory pool used by requests for +/// rendering. Total retained allocation: `worker_count * max_connections`. Requests +/// requiring more memory will allocate per-request, leaving `arena_size` bytes pre-allocated +/// for the next request. +pub const arena_size: usize = 1024 * 1024; + +/// Number of response worker threads. +pub const worker_count: u16 = 4; + +/// Total number of connections managed by worker threads. +pub const max_connections: u16 = 512; + +/// Path relative to cwd() to serve public content from. Symlinks are not followed. +pub const public_content_path = "public"; + +/// Middleware chain. Add any custom middleware here, or use middleware provided in +/// `jetzig.middleware` (e.g. `jetzig.middleware.HtmxMiddleware`). +pub const middleware = &.{}; + +/// HTTP buffer. Must be large enough to store all headers. This should typically not be +/// modified. +pub const http_buffer_size: usize = std.math.pow(usize, 2, 16); + +/// A struct of fragments to use when rendering Markdown templates. +pub const markdown_fragments = zmd.html.DefaultFragments; + +/// The number of worker threads to spawn on startup for processing Jobs (NOT the number of +/// HTTP server worker threads). +pub const job_worker_threads: usize = 1; + +/// Duration before looking for more Jobs when the queue is found to be empty, in +/// milliseconds. +pub const job_worker_sleep_interval_ms: usize = 10; + +/// Database configuration. +pub const database: ?db.DatabaseOptions = null; + +/// Database Schema. +pub const Schema: type = struct { + pub const _null = struct {}; // https://github.com/ziglang/zig/pull/21331 + pub const Blogs = struct { + pub const table_name = "blogs"; + pub const Definition = struct { + id: []const u8, + title: []const u8, + content: []const u8, + }; + }; +}; + +/// Key-value store options. Set backend to `.file` to use a file-based store. +/// When using `.file` backend, you must also set `.file_options`. +/// The key-value store is exposed as `request.store` in views and is also available in as +/// `env.store` in all jobs/mailers. +pub const store: kv.Store.KVOptions = .{ + .backend = .memory, + // .backend = .file, + // .file_options = .{ + // .path = "/path/to/jetkv-store.db", + // .truncate = false, // Set to `true` to clear the store on each server launch. + // .address_space_size = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096), + // }, +}; + +/// Job queue options. Identical to `store` options, but allows using different +/// backends (e.g. `.memory` for key-value store, `.file` for jobs queue. +/// The job queue is managed internally by Jetzig. +pub const job_queue: kv.Store.KVOptions = .{ + .backend = .memory, + // .backend = .file, + // .file_options = .{ + // .path = "/path/to/jetkv-queue.db", + // .truncate = false, // Set to `true` to clear the store on each server launch. + // .address_space_size = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096), + // }, +}; + +/// Cache. Identical to `store` options, but allows using different +/// backends (e.g. `.memory` for key-value store, `.file` for cache. +pub const cache: kv.Store.KVOptions = .{ + .backend = .memory, + // .backend = .file, + // .file_options = .{ + // .path = "/path/to/jetkv-cache.db", + // .truncate = false, // Set to `true` to clear the store on each server launch. + // .address_space_size = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096), + // }, +}; + +/// SMTP configuration for Jetzig Mail. +pub const smtp: mail.SMTPConfig = .{ + .port = 25, + .encryption = .none, // .insecure, .none, .tls, .start_tls + .host = "localhost", + .username = null, + .password = null, +}; + +/// HTTP cookie configuration +pub const cookie_options: http.Cookies.CookieOptions = .{ + .domain = "localhost", + .path = "/", +}; + +/// Force email delivery in development mode (instead of printing email body to logger). +pub const force_development_email_delivery = false; + +/// Reconciles a configuration value from user-defined values and defaults provided by Jetzig. +pub fn get(T: type, comptime key: []const u8) T { + const self = @This(); + if (!@hasDecl(self, key)) @panic("Unknown config option: " ++ key); + + if (@hasDecl(root, "jetzig_options") and @hasDecl(root.jetzig_options, key)) { + return @field(root.jetzig_options, key); + } else { + return @field(self, key); + } +} diff --git a/src/jetzig/database.zig b/src/jetzig/database.zig new file mode 100644 index 0000000..edb5cfa --- /dev/null +++ b/src/jetzig/database.zig @@ -0,0 +1,38 @@ +const std = @import("std"); + +const jetzig = @import("../jetzig.zig"); + +pub const DatabaseOptions = struct { + adapter: enum { postgresql }, + hostname: []const u8, + port: u16, + username: []const u8, + password: []const u8, + database: []const u8, +}; + +pub const Schema = jetzig.get(type, "Schema"); + +pub fn repo(allocator: std.mem.Allocator, maybe_options: ?DatabaseOptions) !jetzig.jetquery.Repo { + const options = maybe_options orelse return try jetzig.jetquery.Repo.init( + allocator, + .{ .adapter = .null }, + ); + + return switch (options.adapter) { + .postgresql => try jetzig.jetquery.Repo.init( + allocator, + .{ + .adapter = .{ + .postgresql = .{ + .hostname = options.hostname, + .port = options.port, + .username = options.username, + .password = options.password, + .database = options.database, + }, + }, + }, + ), + }; +} diff --git a/src/jetzig/http/Request.zig b/src/jetzig/http/Request.zig index 5a3bafb..a0498a6 100644 --- a/src/jetzig/http/Request.zig +++ b/src/jetzig/http/Request.zig @@ -43,6 +43,7 @@ rendered_view: ?jetzig.views.View = null, start_time: i128, store: RequestStore, cache: RequestStore, +repo: *jetzig.jetquery.Repo, global: *jetzig.Global, /// Wrapper for KV store that uses the request's arena allocator for fetching values. @@ -104,6 +105,7 @@ pub fn init( httpz_request: *httpz.Request, httpz_response: *httpz.Response, response: *jetzig.http.Response, + repo: *jetzig.jetquery.Repo, ) !Request { const method = switch (httpz_request.method) { .DELETE => Method.DELETE, @@ -131,6 +133,7 @@ pub fn init( .start_time = start_time, .store = .{ .store = server.store, .allocator = allocator }, .cache = .{ .store = server.cache, .allocator = allocator }, + .repo = repo, .global = if (@hasField(jetzig.Global, "__jetzig_default")) undefined else @@ -517,6 +520,15 @@ pub fn mail(self: *Request, name: []const u8, mail_params: jetzig.mail.MailParam }; } +pub fn query( + self: *const Request, + comptime table: std.meta.DeclEnum(jetzig.config.get(type, "Schema")), +) jetzig.jetquery.Query(@field(jetzig.config.get(type, "Schema"), @tagName(table))) { + return jetzig.jetquery.Query( + @field(jetzig.config.get(type, "Schema"), @tagName(table)), + ).init(self.allocator); +} + fn extensionFormat(self: *const Request) ?jetzig.http.Request.Format { const extension = self.path.extension orelse return null; if (std.mem.eql(u8, extension, ".html")) { diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index 04b9e9a..fe99958 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -19,6 +19,7 @@ initialized: bool = false, store: *jetzig.kv.Store, job_queue: *jetzig.kv.Store, cache: *jetzig.kv.Store, +repo: *jetzig.jetquery.Repo, global: *anyopaque, decoded_static_route_params: []*jetzig.data.Value = &.{}, @@ -35,6 +36,7 @@ pub fn init( store: *jetzig.kv.Store, job_queue: *jetzig.kv.Store, cache: *jetzig.kv.Store, + repo: *jetzig.jetquery.Repo, global: *anyopaque, ) Server { return .{ @@ -49,6 +51,7 @@ pub fn init( .store = store, .job_queue = job_queue, .cache = cache, + .repo = repo, .global = global, }; } @@ -130,6 +133,7 @@ pub fn processNextRequest( httpz_request, httpz_response, &response, + self.repo, ); try request.process();