diff --git a/build.zig b/build.zig index 5337a24..0ed382b 100644 --- a/build.zig +++ b/build.zig @@ -62,6 +62,7 @@ pub fn build(b: *std.Build) !void { .target = target, .optimize = optimize, .jetquery_migrations_path = @as([]const u8, "src/app/database/migrations"), + .jetquery_seeders_path = @as([]const u8, "src/app/database/seeders"), .jetquery_config_path = @as([]const u8, "config/database.zig"), }); const jetcommon_dep = b.dependency("jetcommon", .{ .target = target, .optimize = optimize }); @@ -76,6 +77,7 @@ pub fn build(b: *std.Build) !void { b.modules.put("jetquery", jetquery_dep.module("jetquery")) catch @panic("Out of memory"); b.modules.put("jetcommon", jetcommon_dep.module("jetcommon")) catch @panic("Out of memory"); b.modules.put("jetquery_migrate", jetquery_dep.module("jetquery_migrate")) catch @panic("Out of memory"); + b.modules.put("jetquery_seeder", jetquery_dep.module("jetquery_seeder")) catch @panic("Out of memory"); const smtp_client_dep = b.dependency("smtp_client", .{ .target = target, @@ -174,6 +176,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn .target = target, .optimize = optimize, .jetquery_migrations_path = @as([]const u8, "src/app/database/migrations"), + .jetquery_seeders_path = @as([]const u8, "src/app/database/seeders"), .jetquery_config_path = @as([]const u8, "config/database.zig"), }); @@ -183,6 +186,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn const jetquery_module = jetzig_dep.module("jetquery"); const jetcommon_module = jetzig_dep.module("jetcommon"); const jetquery_migrate_module = jetzig_dep.module("jetquery_migrate"); + const jetquery_seeder_module = jetzig_dep.module("jetquery_seeder"); const jetquery_reflect_module = jetquery_dep.module("jetquery_reflect"); const build_options = b.addOptions(); @@ -403,6 +407,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn exe_database.root_module.addImport("jetzig", jetzig_module); exe_database.root_module.addImport("jetcommon", jetcommon_module); exe_database.root_module.addImport("jetquery_migrate", jetquery_migrate_module); + exe_database.root_module.addImport("jetquery_seeder", jetquery_seeder_module); exe_database.root_module.addImport("jetquery_reflect", jetquery_reflect_module); exe_database.root_module.addImport("Schema", schema_module); @@ -421,6 +426,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn fn registerDatabaseSteps(b: *std.Build, exe_database: *std.Build.Step.Compile) void { const commands = .{ .{ "migrate", "Migrate your Jetzig app's database." }, + .{ "seed", "Run seeds and setup initial data." }, .{ "rollback", "Roll back a migration in your Jetzig app's database." }, .{ "create", "Create a database for your Jetzig app." }, .{ "drop", "Drop your Jetzig app's database." }, diff --git a/build.zig.zon b/build.zig.zon index 0061b20..f4b1535 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -33,8 +33,7 @@ .hash = "httpz-0.0.0-PNVzrEK4BgBpHQGA2m0RPqPGEjnTdDXHodBwzjYDrmps", }, .jetquery = .{ - .url = "https://github.com/jetzig-framework/jetquery/archive/e1f969f2e3e0e1ad9cc30d56fde9739aa692fdc3.tar.gz", - .hash = "jetquery-0.0.0-TNf3zo2ABgBgcsIAvJ1Ud2B2zDzrBy9GQ31kKmTYZ7Ya", + .path = "../jetquery", }, }, diff --git a/cli/build.zig b/cli/build.zig index a0fc17f..7bc84ca 100644 --- a/cli/build.zig +++ b/cli/build.zig @@ -18,6 +18,7 @@ pub fn build(b: *std.Build) !void { .target = target, .optimize = optimize, .jetquery_migrations_path = @as([]const u8, "src/app/database/migrations"), + .jetquery_seeders_path = @as([]const u8, "src/app/database/seeders"), }); exe.root_module.addImport("jetquery", jetquery_dep.module("jetquery")); exe.root_module.addImport("args", zig_args_dep.module("args")); diff --git a/cli/build.zig.zon b/cli/build.zig.zon index bb04aa6..cf536fc 100644 --- a/cli/build.zig.zon +++ b/cli/build.zig.zon @@ -10,8 +10,7 @@ .hash = "zig_args-0.0.0-jqtN6P_NAAC97fGpk9hS2K681jkiqPsWP6w3ucb_ctGH", }, .jetquery = .{ - .url = "https://github.com/jetzig-framework/jetquery/archive/e1f969f2e3e0e1ad9cc30d56fde9739aa692fdc3.tar.gz", - .hash = "jetquery-0.0.0-TNf3zo2ABgBgcsIAvJ1Ud2B2zDzrBy9GQ31kKmTYZ7Ya", + .path = "../../jetquery", }, }, .paths = .{ diff --git a/cli/commands/database.zig b/cli/commands/database.zig index 256019d..9df480a 100644 --- a/cli/commands/database.zig +++ b/cli/commands/database.zig @@ -5,6 +5,7 @@ const args = @import("args"); const util = @import("../util.zig"); const cli = @import("../cli.zig"); const migrate = @import("database/migrate.zig"); +const seed = @import("database/seed.zig"); const rollback = @import("database/rollback.zig"); const create = @import("database/create.zig"); const drop = @import("database/drop.zig"); @@ -41,9 +42,19 @@ pub fn run( defer arena.deinit(); const alloc = arena.allocator(); - const Action = enum { migrate, rollback, create, drop, reflect, update, setup }; + const Action = enum { + migrate, + seed, + rollback, + create, + drop, + reflect, + update, + setup, + }; const map = std.StaticStringMap(Action).initComptime(.{ .{ "migrate", .migrate }, + .{ "seed", .seed }, .{ "rollback", .rollback }, .{ "create", .create }, .{ "drop", .drop }, @@ -74,6 +85,7 @@ pub fn run( break :blk switch (capture) { .migrate => migrate.run(alloc, cwd, sub_args, options, T, main_options), + .seed => seed.run(alloc, cwd, sub_args, options, T, main_options), .rollback => rollback.run(alloc, cwd, sub_args, options, T, main_options), .create => create.run(alloc, cwd, sub_args, options, T, main_options), .drop => drop.run(alloc, cwd, sub_args, options, T, main_options), diff --git a/cli/commands/database/seed.zig b/cli/commands/database/seed.zig new file mode 100644 index 0000000..a6cb041 --- /dev/null +++ b/cli/commands/database/seed.zig @@ -0,0 +1,37 @@ +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( + \\Run database seeders. + \\ + \\Example: + \\ + \\ jetzig database seed + \\ jetzig database seed ./database/seeders/iguana.zig + \\ jetzig --environment=testing database seed + \\ + , .{}); + + return if (main_options.options.help) {} else error.JetzigCommandError; + } + + try util.execCommand(allocator, &.{ + "zig", + "build", + util.environmentBuildOption(main_options.options.environment), + "jetzig:database:seed", + }); +} diff --git a/cli/commands/generate.zig b/cli/commands/generate.zig index bb6b3b9..b3c3ad7 100644 --- a/cli/commands/generate.zig +++ b/cli/commands/generate.zig @@ -10,11 +10,12 @@ const middleware = @import("generate/middleware.zig"); const job = @import("generate/job.zig"); const mailer = @import("generate/mailer.zig"); const migration = @import("generate/migration.zig"); +const seeder = @import("generate/seeder.zig"); /// Command line options for the `generate` command. pub const Options = struct { pub const meta = .{ - .usage_summary = "[view|partial|layout|mailer|middleware|job|secret|migration] [options]", + .usage_summary = "[view|partial|layout|mailer|middleware|job|secret|migration|seeder] [options]", .full_text = \\Generate scaffolding for views, middleware, and other objects. \\ @@ -39,7 +40,17 @@ pub fn run( _ = options; - const Generator = enum { view, partial, layout, mailer, middleware, job, secret, migration }; + const Generator = enum { + view, + partial, + layout, + mailer, + middleware, + job, + secret, + migration, + seeder, + }; var sub_args = std.ArrayList([]const u8).init(allocator); defer sub_args.deinit(); @@ -55,6 +66,7 @@ pub fn run( .{ "middleware", .middleware }, .{ "secret", .secret }, .{ "migration", .migration }, + .{ "seeder", .seeder }, }); for (map.keys()) |key| try available_buf.append(key); @@ -92,6 +104,7 @@ pub fn run( .middleware => middleware.run(arena, cwd, sub_args.items, main_options.options.help), .secret => secret.run(arena, cwd, sub_args.items, main_options.options.help), .migration => migration.run(arena, cwd, sub_args.items, main_options.options.help), + .seeder => seeder.run(arena, cwd, sub_args.items, main_options.options.help), }; } } diff --git a/cli/commands/generate/seeder.zig b/cli/commands/generate/seeder.zig new file mode 100644 index 0000000..c680a51 --- /dev/null +++ b/cli/commands/generate/seeder.zig @@ -0,0 +1,45 @@ +const std = @import("std"); + +const jetquery = @import("jetquery"); + +/// Run the seeder generator. Create a seed in `src/app/database/seeders/` +pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, help: bool) !void { + if (help or args.len < 1) { + std.debug.print( + \\Generate a new Seeder. Seeders is a way to set up some inital data for your application. + \\ + \\Example: + \\ + \\ jetzig generate seeder iguana + \\ + \\ More information: https://www.jetzig.dev/documentation/sections/database/command_line_tools + \\ + , .{}); + + if (help) return; + + return error.JetzigCommandError; + } + + const name = args[0]; + const command = if (args.len > 1) + try std.mem.join(allocator, " ", args[1..]) + else + null; + + const seeders_dir = try cwd.makeOpenPath( + try std.fs.path.join(allocator, &.{ "src", "app", "database", "seeders" }), + .{}, + ); + const seed = jetquery.Seeder.init( + allocator, + name, + .{ + .seeders_path = try seeders_dir.realpathAlloc(allocator, "."), + .command = command, + }, + ); + const path = try seed.save(); + + std.log.info("Saved seed: {s}", .{path}); +} diff --git a/demo/config/database.zig b/demo/config/database.zig index 79d7960..ee03400 100644 --- a/demo/config/database.zig +++ b/demo/config/database.zig @@ -1,11 +1,11 @@ pub const database = .{ .development = .{ .adapter = .postgresql, - .username = "postgres", - .password = "postgres", + .username = "root", + .password = "root", .hostname = "localhost", .database = "jetzig_demo_dev", - .port = 14173, // See `compose.yml` + .port = 5432, // See `compose.yml` }, // This configuration is used for CI // in GitHub diff --git a/demo/src/app/database/seeders/2025-03-10_01-36-58_create_users.zig b/demo/src/app/database/seeders/2025-03-10_01-36-58_create_users.zig new file mode 100644 index 0000000..1c08acf --- /dev/null +++ b/demo/src/app/database/seeders/2025-03-10_01-36-58_create_users.zig @@ -0,0 +1,20 @@ +const std = @import("std"); +const jetquery = @import("jetquery"); + +pub fn run(repo: anytype) !void { + try repo.insert( + .User, + .{ + .email = "iguana@jetzig.dev", + .password_hash = "not_secure", + }, + ); + + try repo.insert( + .User, + .{ + .email = "admin@jetzig.dev", + .password_hash = "do_not_use", + }, + ); +} diff --git a/src/commands/database.zig b/src/commands/database.zig index e3a67d0..2db0615 100644 --- a/src/commands/database.zig +++ b/src/commands/database.zig @@ -6,6 +6,8 @@ const jetquery = @import("jetquery"); const jetzig = @import("jetzig"); const Migrate = @import("jetquery_migrate").Migrate; const MigrateSchema = @import("jetquery_migrate").MigrateSchema; +const Seeder = @import("jetquery_seeder").Seed; +const SeederSchema = @import("jetquery_seeder").SeederSchema; const Schema = @import("Schema"); const util = @import("util.zig"); @@ -15,7 +17,7 @@ const production_drop_failure_message = "To drop a production database, " ++ const environment = jetzig.build_options.environment; const config = @field(jetquery.config.database, @tagName(environment)); -const Action = enum { migrate, rollback, create, drop, reflect, setup, update }; +const Action = enum { migrate, rollback, create, drop, reflect, setup, update, seed }; pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; @@ -42,6 +44,7 @@ pub fn main() !void { const map = std.StaticStringMap(Action).initComptime(.{ .{ "migrate", .migrate }, + .{ "seed", .seed }, .{ "rollback", .rollback }, .{ "create", .create }, .{ "drop", .drop }, @@ -69,6 +72,11 @@ pub fn main() !void { defer repo.deinit(); try Migrate(config.adapter).init(&repo).migrate(); }, + .seed => { + var repo = try seedersRepo(action, allocator, repo_env); + defer repo.deinit(); + try Seeder(config.adapter, Schema).init(&repo).seed(); + }, .rollback => { var repo = try migrationsRepo(action, allocator, repo_env); defer repo.deinit(); @@ -136,7 +144,7 @@ fn migrationsRepo(action: Action, allocator: std.mem.Allocator, repo_env: anytyp @field(jetquery.Environment, @tagName(environment)), .{ .admin = switch (action) { - .migrate, .rollback, .update => false, + .migrate, .seed, .rollback, .update => false, .create, .drop => true, .reflect => unreachable, // We use a separate repo for schema reflection. .setup => unreachable, // Setup uses `create` and then `update` @@ -147,6 +155,24 @@ fn migrationsRepo(action: Action, allocator: std.mem.Allocator, repo_env: anytyp ); } +const SeedersRepo = jetquery.Repo(config.adapter, Schema); +fn seedersRepo(action: Action, allocator: std.mem.Allocator, repo_env: anytype) !SeedersRepo { + return try SeedersRepo.loadConfig( + allocator, + @field(jetquery.Environment, @tagName(environment)), + .{ + .admin = switch (action) { + .migrate, .seed, .rollback, .update => false, + .create, .drop => false, + .reflect => unreachable, // We use a separate repo for schema reflection. + .setup => unreachable, // Setup uses `create` and then `update` + }, + .context = .seed, + .env = repo_env, + }, + ); +} + fn reflectSchema(allocator: std.mem.Allocator, repo_env: anytype) !void { var cwd = try jetzig.util.detectJetzigProjectDir(); defer cwd.close();