Initial project setup with a declarative API for models definition and their dispatchers.
This commit is contained in:
commit
bb11117a6d
20 changed files with 1147 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
# IDEA
|
||||
*.iml
|
||||
|
||||
# Zig
|
||||
zig-out/
|
||||
.zig-cache/
|
29
README.md
Normal file
29
README.md
Normal file
|
@ -0,0 +1,29 @@
|
|||
# PGMQL
|
||||
|
||||
PGMQL (or _PostgreMQL_ or _Pro Gamer Master Query Language_) is a library designed to ease the creation of models APIs for backend and frontend uses.
|
||||
|
||||
PGMQL features **model definition with relationships**, **CRUD operations**, **custom dispatchers definition**, **authentication** and **authorization**. All these features are based on a PostgreSQL database, which means that you're never stuck with PGMQL: you can interact with your data in other ways if you feel limited with PGMQL (its goal is **not** to replace _everything_).
|
||||
|
||||
PGMQL uses Zig to provide very high performances and compile-time definitions of models, APIs and authorizations.
|
||||
|
||||
## Features
|
||||
|
||||
### Model definition
|
||||
|
||||
With PGMQL, you can define your models in a _structure-oriented_ way. A model is a structure which can be related to other structures, through _relationships_. You can then ask PGMQL to retrieve the models with the required relationships.
|
||||
|
||||
### CRUD operations
|
||||
|
||||
PGMQL natively provide a full-featured REST API to perform CRUD operations on your models. For each defined model, you can easily create them with `POST` requests, read them with `GET` requests, update them with `PATCH` requests and delete them with `DELETE` requests.
|
||||
|
||||
### Custom dispatchers
|
||||
|
||||
In real-world applications, models often need to allow more operations than simple CRUD: with PGMQL, you can define custom dispatchers to perform multiple complex operations in a single transaction.
|
||||
|
||||
### Authentication
|
||||
|
||||
Your models can be protected to be accessible / editable by authenticated users only. Authenticated accounts can also bear some useful metadata for CRUD operations or dispatchers.
|
||||
|
||||
### Authorization
|
||||
|
||||
PGMQL allows you to define policies for every model: you can easily define access control for CRUD operations, but also custom dispatchers.
|
81
build.zig
Normal file
81
build.zig
Normal file
|
@ -0,0 +1,81 @@
|
|||
const std = @import("std");
|
||||
|
||||
// Although this function looks imperative, note that its job is to
|
||||
// declaratively construct a build graph that will be executed by an external
|
||||
// runner.
|
||||
pub fn build(b: *std.Build) void {
|
||||
// Standard target options allows the person running `zig build` to choose
|
||||
// what target to build for. Here we do not override the defaults, which
|
||||
// means any target is allowed, and the default is native. Other options
|
||||
// for restricting supported target set are available.
|
||||
const target = b.standardTargetOptions(.{});
|
||||
|
||||
// Standard optimization options allow the person running `zig build` to select
|
||||
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
|
||||
// set a preferred release mode, allowing the user to decide how to optimize.
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
// Create PGMQL module.
|
||||
const pgmql = b.addModule("pgmql", .{
|
||||
.root_source_file = b.path("lib/root.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// Add tardy dependency.
|
||||
const tardy = b.dependency("tardy", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
pgmql.addImport("tardy", tardy.module("tardy"));
|
||||
|
||||
// Add zzz dependency.
|
||||
const zzz = b.dependency("zzz", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
pgmql.addImport("zzz", zzz.module("zzz"));
|
||||
|
||||
// Create MQL executable.
|
||||
const mql = b.addExecutable(.{
|
||||
.name = "mql",
|
||||
.root_source_file = b.path("src/mql.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
mql.root_module.addImport("pgmql", pgmql);
|
||||
b.installArtifact(mql);
|
||||
|
||||
// Create example executable.
|
||||
const example = b.addExecutable(.{
|
||||
.name = "example",
|
||||
.root_source_file = b.path("tests/example.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
example.root_module.addImport("pgmql", pgmql);
|
||||
example.root_module.addImport("tardy", tardy.module("tardy"));
|
||||
example.root_module.addImport("zzz", zzz.module("zzz"));
|
||||
b.installArtifact(example);
|
||||
|
||||
const lib_unit_tests = b.addTest(.{
|
||||
.root_source_file = b.path("lib/root.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
|
||||
|
||||
const lib_example_tests = b.addTest(.{
|
||||
.root_source_file = b.path("tests/root.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
// Add pgmql dependency.
|
||||
lib_example_tests.root_module.addImport("pgmql", pgmql);
|
||||
const run_lib_example_tests = b.addRunArtifact(lib_example_tests);
|
||||
run_lib_example_tests.has_side_effects = true;
|
||||
|
||||
const test_step = b.step("test", "Run unit tests.");
|
||||
test_step.dependOn(&run_lib_example_tests.step);
|
||||
test_step.dependOn(&run_lib_unit_tests.step);
|
||||
}
|
22
build.zig.zon
Normal file
22
build.zig.zon
Normal file
|
@ -0,0 +1,22 @@
|
|||
.{
|
||||
.name = "pgmql",
|
||||
.version = "0.1.0",
|
||||
|
||||
.dependencies = .{
|
||||
.zzz = .{
|
||||
.url = "git+https://github.com/mookums/zzz?ref=main#e7cc636a32a43b25d5e6ebde4f34e9e0a873fcb8",
|
||||
.hash = "12200fd1e8c229eb5800fe3d2937ddf8f3daa02a65b9b894e227526b97b709513cba",
|
||||
},
|
||||
.tardy = .{
|
||||
.url = "git+https://github.com/mookums/tardy?ref=v0.1.0#ae0970d6b3fa5b03625b14e142c664efe1fd7789",
|
||||
.hash = "12207f5afee3b8933c1c32737e8feedc80a2e4feebe058739509094c812e4a8d2cc8",
|
||||
},
|
||||
},
|
||||
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
"README.md",
|
||||
},
|
||||
}
|
100
lib/api.zig
Normal file
100
lib/api.zig
Normal file
|
@ -0,0 +1,100 @@
|
|||
const std = @import("std");
|
||||
const _registry = @import("registry.zig");
|
||||
const _model = @import("model.zig");
|
||||
const _dispatchers = @import("dispatchers.zig");
|
||||
|
||||
|
||||
/// Type of dispatchers map.
|
||||
pub const DispatchersMap = std.StaticStringMap(_dispatchers.Dispatcher);
|
||||
|
||||
/// Type of models dispatchers map.
|
||||
pub const ModelsDispatchersMap = std.StaticStringMap(DispatchersMap);
|
||||
|
||||
/// Build dispatchers maps of all models from the dispatchers definition structure.
|
||||
fn BuildModelsDispatchersMaps(comptime dispatchersDefinition: type) ModelsDispatchersMap {
|
||||
return ModelsDispatchersMap.initComptime(comptime dispatchersKv: {
|
||||
// Get all models dispatchers definition structures.
|
||||
const modelsDispatchersDecls = std.meta.declarations(dispatchersDefinition);
|
||||
|
||||
// Initialize a new key-values array for all models.
|
||||
var dispatchersKv: [modelsDispatchersDecls.len]struct{[]const u8, DispatchersMap} = undefined;
|
||||
|
||||
// Build all dispatchers maps and add them in the key-values array.
|
||||
for (&dispatchersKv, modelsDispatchersDecls) |*dispatcherKv, modelDispatchersDecl| {
|
||||
dispatcherKv.* = .{ modelDispatchersDecl.name, BuildDispatchersMap(modelDispatchersDecl.name, @field(dispatchersDefinition, modelDispatchersDecl.name)) };
|
||||
}
|
||||
|
||||
// Return the new dispatchers maps key-values.
|
||||
break :dispatchersKv dispatchersKv;
|
||||
});
|
||||
}
|
||||
|
||||
/// Build a dispatchers map for a model from its dispatchers definition structure.
|
||||
fn BuildDispatchersMap(comptime modelName: []const u8, comptime dispatchersBuilders: type) DispatchersMap {
|
||||
// Return the new dispatchers map.
|
||||
return DispatchersMap.initComptime(comptime dispatchersKv: {
|
||||
// Get all dispatchers declarations.
|
||||
const dispatchersDecls = std.meta.declarations(dispatchersBuilders);
|
||||
|
||||
// Initialize a new key-values array for all dispatchers.
|
||||
var dispatchersKv: [dispatchersDecls.len]struct{[]const u8, _dispatchers.Dispatcher} = undefined;
|
||||
|
||||
// Build all dispatchers and add them in the key-values array.
|
||||
for (dispatchersKv[0..dispatchersDecls.len], dispatchersDecls) |*dispatcherKv, dispatcherDecl| {
|
||||
dispatcherKv.* = .{ dispatcherDecl.name, @field(dispatchersBuilders, dispatcherDecl.name).build(modelName, dispatcherDecl.name) };
|
||||
}
|
||||
|
||||
// Return the new dispatchers key-values.
|
||||
break :dispatchersKv dispatchersKv;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/// MQL API structure.
|
||||
pub fn Api(
|
||||
comptime registry: type,
|
||||
comptime dispatchersDefinition: type,
|
||||
) type {
|
||||
// Build models dispatchers map.
|
||||
const dispatchersMap = BuildModelsDispatchersMaps(dispatchersDefinition);
|
||||
|
||||
// Compute dispatchers array size.
|
||||
const dispatchersArraySize = comptime dispatchersArraySize: {
|
||||
var size = 0;
|
||||
|
||||
for (dispatchersMap.values()) |dispatchers| {
|
||||
size += dispatchers.values().len;
|
||||
}
|
||||
|
||||
break :dispatchersArraySize size;
|
||||
};
|
||||
// Build dispatchers array.
|
||||
const dispatchersArray = comptime dispatchersArray: {
|
||||
var dispatchersArray: [dispatchersArraySize]_dispatchers.Dispatcher = undefined;
|
||||
var dispatchersArrayIndex = 0;
|
||||
|
||||
for (dispatchersMap.values()) |dispatchers| {
|
||||
for (dispatchers.values()) |dispatcher| {
|
||||
dispatchersArray[dispatchersArrayIndex] = dispatcher;
|
||||
dispatchersArrayIndex += 1;
|
||||
}
|
||||
}
|
||||
|
||||
break :dispatchersArray dispatchersArray;
|
||||
};
|
||||
|
||||
return struct {
|
||||
pub const Registry = registry;
|
||||
pub const dispatchers = dispatchersMap;
|
||||
|
||||
/// Get model name from its provided instance.
|
||||
pub fn getModelName(comptime model: _model.Model) []const u8 {
|
||||
return Registry.getModelName(model);
|
||||
}
|
||||
|
||||
/// Get a list of all declared dispatchers.
|
||||
pub fn getDispatchers() []const _dispatchers.Dispatcher {
|
||||
return dispatchersArray[0..dispatchersArraySize];
|
||||
}
|
||||
};
|
||||
}
|
77
lib/crud.zig
Normal file
77
lib/crud.zig
Normal file
|
@ -0,0 +1,77 @@
|
|||
const std = @import("std");
|
||||
const zzz = @import("zzz");
|
||||
|
||||
/// CRUD dispatchers definitions for a given model dispatcher definition.
|
||||
pub fn Crud(ModelDispatcherDefinition: type) type {
|
||||
return struct {
|
||||
/// Make a definition structure to define all CRUD operations with the provided policy.
|
||||
pub fn All(policy: ModelDispatcherDefinition.Policy) type {
|
||||
return struct {
|
||||
pub usingnamespace Create(policy);
|
||||
pub usingnamespace Read(policy);
|
||||
pub usingnamespace Update(policy);
|
||||
pub usingnamespace Delete(policy);
|
||||
};
|
||||
}
|
||||
|
||||
/// Make a definition structure to define model creation operation with the provided policy.
|
||||
pub fn Create(policy: ModelDispatcherDefinition.Policy) type {
|
||||
return struct {
|
||||
pub const create = (ModelDispatcherDefinition{
|
||||
.policy = policy,
|
||||
.run = struct {
|
||||
fn f(context: ModelDispatcherDefinition.Context) void {
|
||||
_ = context; //TODO!
|
||||
}
|
||||
}.f,
|
||||
.httpMethod = zzz.HTTP.Method.POST,
|
||||
}).builder();
|
||||
};
|
||||
}
|
||||
|
||||
/// Make a definition structure to define model retrieval operation with the provided policy.
|
||||
pub fn Read(policy: ModelDispatcherDefinition.Policy) type {
|
||||
return struct {
|
||||
pub const read = (ModelDispatcherDefinition{
|
||||
.policy = policy,
|
||||
.run = struct {
|
||||
fn f(context: ModelDispatcherDefinition.Context) void {
|
||||
_ = context; //TODO!
|
||||
}
|
||||
}.f,
|
||||
.httpMethod = zzz.HTTP.Method.GET,
|
||||
}).builder();
|
||||
};
|
||||
}
|
||||
|
||||
/// Make a definition structure to define model update operation with the provided policy.
|
||||
pub fn Update(policy: ModelDispatcherDefinition.Policy) type {
|
||||
return struct {
|
||||
pub const update = (ModelDispatcherDefinition{
|
||||
.policy = policy,
|
||||
.run = struct {
|
||||
fn f(context: ModelDispatcherDefinition.Context) void {
|
||||
_ = context; //TODO!
|
||||
}
|
||||
}.f,
|
||||
.httpMethod = zzz.HTTP.Method.PATCH,
|
||||
}).builder();
|
||||
};
|
||||
}
|
||||
|
||||
/// Make a definition structure to define model deletion operation with the provided policy.
|
||||
pub fn Delete(policy: ModelDispatcherDefinition.Policy) type {
|
||||
return struct {
|
||||
pub const delete = (ModelDispatcherDefinition{
|
||||
.policy = policy,
|
||||
.run = struct {
|
||||
fn f(context: ModelDispatcherDefinition.Context) void {
|
||||
_ = context; //TODO!
|
||||
}
|
||||
}.f,
|
||||
.httpMethod = zzz.HTTP.Method.DELETE,
|
||||
}).builder();
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
127
lib/dispatchers.zig
Normal file
127
lib/dispatchers.zig
Normal file
|
@ -0,0 +1,127 @@
|
|||
const std = @import("std");
|
||||
const zzz = @import("zzz");
|
||||
const _comptime = @import("utils/comptime.zig");
|
||||
|
||||
/// Type of a dispatcher run function.
|
||||
pub fn DispatchFunction(AuthenticationType: type, PayloadType: type) type {
|
||||
return *const fn(context: DispatcherContext(AuthenticationType, PayloadType)) void;
|
||||
}
|
||||
|
||||
/// Model dispatcher runtime context structure.
|
||||
pub fn DispatcherContext(AuthenticationType: type, PayloadType: type) type {
|
||||
return struct {
|
||||
/// Payload of the running dispatch.
|
||||
payload: *const PayloadType,
|
||||
|
||||
/// The authenticated account, if there is one. NULL = no authenticated account.
|
||||
account: ?*AuthenticationType,
|
||||
};
|
||||
}
|
||||
|
||||
/// Dispatcher builder interface.
|
||||
pub const DispatcherBuilder = struct {
|
||||
build: *const fn (comptime modelName: []const u8, comptime dispatchName: []const u8) Dispatcher,
|
||||
};
|
||||
|
||||
/// Compile-time model dispatcher definition structure.
|
||||
pub fn DispatcherDefinition(AuthenticationType: type, PayloadType: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
pub const Context = DispatcherContext(AuthenticationType, PayloadType);
|
||||
pub const Policy = DispatcherPolicy(AuthenticationType, PayloadType);
|
||||
pub const PolicyContext = DispatcherPolicyContext(AuthenticationType, PayloadType);
|
||||
pub const Dispatch = DispatchFunction(AuthenticationType, PayloadType);
|
||||
|
||||
/// Dispatcher call policy.
|
||||
policy: Policy,
|
||||
/// Run function of the dispatcher.
|
||||
run: Dispatch,
|
||||
|
||||
/// HTTP method to use in HTTP API for this dispatcher.
|
||||
httpMethod: zzz.HTTP.Method = zzz.HTTP.Method.GET,
|
||||
|
||||
/// Initialize a dispatcher builder for the defined dispatcher.
|
||||
pub fn builder(comptime self: *const Self) DispatcherBuilder {
|
||||
return DispatcherBuilder{
|
||||
.build = struct {
|
||||
fn f(comptime modelName: []const u8, comptime dispatchName: []const u8) Dispatcher {
|
||||
// Build the model identifier from the model name.
|
||||
const modelIdentifier = comptime _comptime.pascalToKebab(modelName);
|
||||
|
||||
return Dispatcher{
|
||||
._interface = .{
|
||||
.instance = self,
|
||||
|
||||
.getModelIdentifier = struct { pub fn f(_: *const anyopaque) []const u8 {
|
||||
return modelIdentifier;
|
||||
} }.f,
|
||||
|
||||
.getDispatchName = struct { pub fn f(_: *const anyopaque) []const u8 {
|
||||
return dispatchName;
|
||||
} }.f,
|
||||
|
||||
.getHttpMethod = struct { pub fn f(opaqueSelf: *const anyopaque) zzz.HTTP.Method {
|
||||
const impl: *const Self = @ptrCast(@alignCast(opaqueSelf));
|
||||
return impl.httpMethod;
|
||||
} }.f,
|
||||
},
|
||||
};
|
||||
}
|
||||
}.f,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Compile-time model dispatcher policy definition structure.
|
||||
pub fn DispatcherPolicy(AuthenticationType: type, PayloadType: type) type {
|
||||
return union(enum) {
|
||||
/// Allow / disallow any call to the dispatcher.
|
||||
any: bool,
|
||||
/// Allow calls to the dispatcher with any authenticated account.
|
||||
anyAuthenticated: bool,
|
||||
/// Allow calls to the dispatcher if the defined function returns true.
|
||||
func: *const fn(context: DispatcherPolicyContext(AuthenticationType, PayloadType)) bool, //TODO define rules instead of func?
|
||||
};
|
||||
}
|
||||
|
||||
/// Model dispatcher policy runtime context structure.
|
||||
pub fn DispatcherPolicyContext(AuthenticationType: type, PayloadType: type) type {
|
||||
return struct {
|
||||
/// Payload of the running dispatch.
|
||||
payload: *const PayloadType,
|
||||
|
||||
/// The authenticated account, if there is one. NULL = no authenticated account.
|
||||
account: ?*AuthenticationType,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// Dispatcher interface.
|
||||
pub const Dispatcher = struct {
|
||||
const Self = @This();
|
||||
|
||||
_interface: struct {
|
||||
instance: *const anyopaque,
|
||||
|
||||
getModelIdentifier: *const fn(instance: *const anyopaque) []const u8,
|
||||
getDispatchName: *const fn(instance: *const anyopaque) []const u8,
|
||||
getHttpMethod: *const fn(instance: *const anyopaque) zzz.HTTP.Method,
|
||||
},
|
||||
|
||||
/// Get model identifier.
|
||||
pub fn getModelIdentifier(self: *const Self) []const u8 {
|
||||
return self._interface.getModelIdentifier(self._interface.instance);
|
||||
}
|
||||
|
||||
/// Get dispatch name.
|
||||
pub fn getDispatchName(self: *const Self) []const u8 {
|
||||
return self._interface.getDispatchName(self._interface.instance);
|
||||
}
|
||||
|
||||
/// Get HTTP API method.
|
||||
pub fn getHttpMethod(self: *const Self) zzz.HTTP.Method {
|
||||
return self._interface.getHttpMethod(self._interface.instance);
|
||||
}
|
||||
};
|
100
lib/http.zig
Normal file
100
lib/http.zig
Normal file
|
@ -0,0 +1,100 @@
|
|||
const std = @import("std");
|
||||
const zzz = @import("zzz");
|
||||
const tardy = @import("tardy");
|
||||
const _dispatchers = @import("dispatchers.zig");
|
||||
const log = std.log.scoped(.@"pgmql/http");
|
||||
|
||||
/// HTTP API server structure.
|
||||
pub const Server = struct {
|
||||
const Self = @This();
|
||||
|
||||
const Tardy = tardy.Tardy(.auto);
|
||||
const Http = zzz.HTTP.Server(.plain);
|
||||
const Router = Http.Router;
|
||||
const Route = Http.Route;
|
||||
const Context = Http.Context;
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
dispatchers: []const _dispatchers.Dispatcher,
|
||||
|
||||
/// Host to use.
|
||||
host: []const u8 = "127.0.0.1",
|
||||
|
||||
/// Port to use.
|
||||
port: u16 = 8122,
|
||||
|
||||
/// Tardy runtime, if initialized.
|
||||
ta: ?Tardy = null,
|
||||
|
||||
/// HTTP router, if initialized.
|
||||
router: ?Router = null,
|
||||
|
||||
/// Initialize a new HTTP API server.
|
||||
pub fn init(allocator: std.mem.Allocator, dispatchers: []const _dispatchers.Dispatcher) Self {
|
||||
return Self{
|
||||
.allocator = allocator,
|
||||
.dispatchers = dispatchers,
|
||||
};
|
||||
}
|
||||
|
||||
/// Deinitialize the HTTP API server.
|
||||
pub fn deinit(self: *Self) void {
|
||||
if (self.ta) |*_ta| {
|
||||
_ta.deinit();
|
||||
}
|
||||
|
||||
if (self.router) |*_router| {
|
||||
_router.deinit();
|
||||
}
|
||||
}
|
||||
|
||||
/// Setup Tardy and ZZZ.
|
||||
fn setup(self: *Self) !void {
|
||||
self.ta = try Tardy.init(.{
|
||||
.allocator = self.allocator,
|
||||
.threading = .auto,
|
||||
});
|
||||
|
||||
self.router = Router.init(self.allocator);
|
||||
|
||||
// Set error JSON response.
|
||||
//TODO Improve zzz to allow this.
|
||||
|
||||
// Set not found JSON response.
|
||||
self.router.?.serve_not_found(Route.init().get({}, struct {
|
||||
fn f(context: *Context, _: void) !void {
|
||||
// Return a "not found" error as JSON.
|
||||
try context.respond(.{
|
||||
.status = zzz.HTTP.Status.@"Not Found",
|
||||
.body = "{\"error\": \"not found\"}",
|
||||
.mime = zzz.HTTP.Mime.JSON,
|
||||
});
|
||||
}
|
||||
}.f));
|
||||
}
|
||||
|
||||
/// Add API endpoints for the given dispatchers.
|
||||
fn addApi(self: *Self) !void {
|
||||
for (self.dispatchers) |dispatcher| {
|
||||
// Add an endpoint of each dispatcher.
|
||||
_ = dispatcher;
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the HTTP API server.
|
||||
pub fn start(self: *Self) !void {
|
||||
try self.setup();
|
||||
|
||||
try self.ta.?.entry(
|
||||
self, struct { fn f(runtime: *tardy.Runtime, server: *const Server) !void {
|
||||
// Start a new HTTP server with the provided configuration.
|
||||
var http = Http.init(runtime.allocator, .{});
|
||||
try http.bind(.{ .ip = .{ .host = server.host, .port = server.port } });
|
||||
try http.serve(&server.router.?, runtime);
|
||||
} }.f,
|
||||
{}, struct { fn f(runtime: *tardy.Runtime, _: void) !void {
|
||||
try Http.clean(runtime);
|
||||
} }.f
|
||||
);
|
||||
}
|
||||
};
|
101
lib/model.zig
Normal file
101
lib/model.zig
Normal file
|
@ -0,0 +1,101 @@
|
|||
const std = @import("std");
|
||||
const _comptime = @import("utils/comptime.zig");
|
||||
const _registry = @import("registry.zig");
|
||||
const _dispatchers = @import("dispatchers.zig");
|
||||
|
||||
/// Compile-time model primary key definition structure.
|
||||
pub const ModelPrimaryKey = union(enum) {
|
||||
const Self = @This();
|
||||
|
||||
/// Simple single-column primary key.
|
||||
single: []const u8,
|
||||
/// Composite (multi-columns) primary key.
|
||||
composite: []const []const u8, //TODO Actually implement composite primary keys.
|
||||
|
||||
/// Get string representation of the model primary key.
|
||||
pub fn toString(self: Self) []const u8 {
|
||||
return switch (self) {
|
||||
.single => |key| key,
|
||||
.composite => |keys| _comptime.join(", ", keys),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Compile-time model definition structure.
|
||||
pub const Model: type = struct {
|
||||
const Self = @This();
|
||||
|
||||
/// Get the structure type of the current model.
|
||||
pub fn structure(self: Self) type {
|
||||
return ModelStructure(self);
|
||||
}
|
||||
|
||||
/// Initialize a new model structure with all values as NULL.
|
||||
pub fn initStruct(self: Self) self.structure() {
|
||||
// Initialize a model instance.
|
||||
var instance: self.structure() = undefined;
|
||||
inline for (@typeInfo(@TypeOf(instance)).Struct.fields) |field| {
|
||||
// Initialize all fields to null.
|
||||
@field(instance, field.name) = null;
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// Table of the model.
|
||||
table: []const u8,
|
||||
/// Primary key of the model.
|
||||
primaryKey: ModelPrimaryKey,
|
||||
|
||||
/// Model fields and relationships definition.
|
||||
definition: type,
|
||||
|
||||
/// Get identifier of the model.
|
||||
pub fn getIdentifier(self: *const Self) []const u8 {
|
||||
return self.table ++ "(" ++ self.primaryKey.toString() ++ ")";
|
||||
}
|
||||
};
|
||||
|
||||
/// Get the type of a model structure from its definition.
|
||||
pub fn ModelStructure(model: Model) type {
|
||||
// Initialize all fields of the structure.
|
||||
var structFields: [std.meta.declarations(model.definition).len]std.builtin.Type.StructField = undefined;
|
||||
|
||||
// For each field in the definition, create its corresponding field in the structure.
|
||||
inline for (std.meta.declarations(model.definition), &structFields) |definitionDelc, *structField| {
|
||||
var fieldType: type = undefined;
|
||||
|
||||
if (@hasField(@TypeOf(@field(model.definition, definitionDelc.name)), "type")) {
|
||||
// It's a simple type, getting the corresponding field type to create the structure field.
|
||||
fieldType = @Type(std.builtin.Type{
|
||||
.Optional = .{ .child = @field(model.definition, definitionDelc.name).type }
|
||||
});
|
||||
} else if (@hasField(@TypeOf(@field(model.definition, definitionDelc.name)), "related")) {
|
||||
// It's a relationship type, getting the corresponding field type.
|
||||
//const modelType = @field(registry.models, @field(model.definition, definitionDelc.name).related).structure(registry);
|
||||
fieldType = @Type(std.builtin.Type{
|
||||
.Optional = .{ .child = *const anyopaque } //TODO Using anyopaque instead of modelType as a workaround of infinite recursion.
|
||||
});
|
||||
} else {
|
||||
// Invalid declaration format.
|
||||
@compileError("invalid declaration \"" ++ definitionDelc.name ++ "\": cannot find its type.");
|
||||
}
|
||||
|
||||
structField.* = std.builtin.Type.StructField{
|
||||
.name = definitionDelc.name,
|
||||
.type = fieldType,
|
||||
.default_value = null,
|
||||
.is_comptime = false,
|
||||
.alignment = @alignOf(fieldType),
|
||||
};
|
||||
}
|
||||
|
||||
// Return the built structure.
|
||||
return @Type(std.builtin.Type{
|
||||
.Struct = .{
|
||||
.layout = std.builtin.Type.ContainerLayout.auto,
|
||||
.fields = &structFields,
|
||||
.decls = &[_]std.builtin.Type.Declaration{},
|
||||
.is_tuple = false,
|
||||
},
|
||||
});
|
||||
}
|
67
lib/registry.zig
Normal file
67
lib/registry.zig
Normal file
|
@ -0,0 +1,67 @@
|
|||
const std = @import("std");
|
||||
const zzz = @import("zzz");
|
||||
const _comptime = @import("utils/comptime.zig");
|
||||
const _api = @import("api.zig");
|
||||
const _model = @import("model.zig");
|
||||
const _dispatchers = @import("dispatchers.zig");
|
||||
const _crud = @import("crud.zig");
|
||||
|
||||
/// Build models map, to associate model name to their identifier name.
|
||||
fn BuildModelsMap(comptime models: type) std.StaticStringMap([]const u8) {
|
||||
// Build key-values array.
|
||||
var mapKeyValues: [std.meta.declarations(models).len](struct{[]const u8, []const u8}) = undefined;
|
||||
|
||||
// For each model, add its identifier name to the key-values array.
|
||||
for (&mapKeyValues, std.meta.declarations(models)) |*keyVal, model| {
|
||||
keyVal.* = .{@field(models, model.name).getIdentifier(), model.name};
|
||||
}
|
||||
|
||||
// Initialize a new static string map and return it.
|
||||
return std.StaticStringMap([]const u8).initComptime(mapKeyValues);
|
||||
}
|
||||
|
||||
/// Compile-time models registry.
|
||||
pub fn Registry(comptime models: type, comptime authenticationModelName: []const u8) type {
|
||||
if (!@hasDecl(models, authenticationModelName))
|
||||
@compileError("\"" ++ authenticationModelName ++ "\" not found in models definition structure.");
|
||||
|
||||
return struct {
|
||||
const SelfRegistry = @This();
|
||||
|
||||
/// Authentication type, induced from the defined authentication model.
|
||||
pub const AuthenticationType = @field(Models, authenticationModelName).structure();
|
||||
|
||||
pub const dispatchers = struct {
|
||||
/// Create a new dispatcher definition for the current registry.
|
||||
pub fn Definition(PayloadType: type) type {
|
||||
return _dispatchers.DispatcherDefinition(AuthenticationType, PayloadType);
|
||||
}
|
||||
|
||||
/// Default CRUD dispatchers definitions for the given model.
|
||||
pub fn Crud(model: _model.Model) type {
|
||||
const ModelDispatcherDefinition = Definition(model.structure());
|
||||
return _crud.Crud(ModelDispatcherDefinition);
|
||||
}
|
||||
};
|
||||
|
||||
/// Create a new API with this registry and the provided dispatchers definition.
|
||||
pub fn Api(comptime dispatchersDefinition: type) type {
|
||||
return _api.Api(SelfRegistry, dispatchersDefinition);
|
||||
}
|
||||
|
||||
/// Defined models.
|
||||
pub const Models = models;
|
||||
|
||||
/// Map models identifiers to models names in the registry.
|
||||
pub const ModelsMap = BuildModelsMap(models);
|
||||
|
||||
/// Get model name from its provided instance.
|
||||
pub fn getModelName(comptime model: _model.Model) []const u8 {
|
||||
if (ModelsMap.get(model.getIdentifier())) |modelName| {
|
||||
return modelName;
|
||||
} else {
|
||||
@compileError("undefined model " ++ model.getIdentifier());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
89
lib/relationships.zig
Normal file
89
lib/relationships.zig
Normal file
|
@ -0,0 +1,89 @@
|
|||
const std = @import("std");
|
||||
|
||||
/// Configure a "one to one" relationship.
|
||||
pub const OneConfiguration = union(enum) {
|
||||
/// Direct one-to-one relationship using a local foreign key.
|
||||
direct: struct {
|
||||
/// The local foreign key name.
|
||||
foreignKey: []const u8,
|
||||
/// Associated model key name.
|
||||
/// Use the default key name of the associated model.
|
||||
modelKey: ?[]const u8 = null,
|
||||
},
|
||||
|
||||
/// Reverse one-to-one relationship using distant foreign key.
|
||||
reverse: struct {
|
||||
/// The distant foreign key name.
|
||||
/// Use the default key name of the related model.
|
||||
foreignKey: ?[]const u8 = null,
|
||||
/// Current model key name.
|
||||
/// Use the default key name of the current model.
|
||||
modelKey: ?[]const u8 = null,
|
||||
},
|
||||
|
||||
/// Used when performing a one-to-one relationship through an association table.
|
||||
through: struct {
|
||||
/// Name of the join table.
|
||||
table: []const u8,
|
||||
/// The local foreign key name.
|
||||
/// Use the default key name of the current model.
|
||||
foreignKey: ?[]const u8 = null,
|
||||
/// The foreign key name in the join table.
|
||||
joinForeignKey: []const u8,
|
||||
/// The model key name in the join table.
|
||||
joinModelKey: []const u8,
|
||||
/// Associated model key name.
|
||||
/// Use the default key name of the associated model.
|
||||
modelKey: ?[]const u8 = null,
|
||||
},
|
||||
};
|
||||
|
||||
/// Type of a related model field.
|
||||
pub const Model = struct {
|
||||
/// Name of the related model definition.
|
||||
related: []const u8,
|
||||
|
||||
/// Configuration of the relationship.
|
||||
relationship: OneConfiguration,
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/// Configure a "one to many" or "many to many" relationship.
|
||||
pub const ManyConfiguration = union(enum) {
|
||||
/// Direct one-to-many relationship using a distant foreign key.
|
||||
direct: struct {
|
||||
/// The distant foreign key name pointing to the current model.
|
||||
foreignKey: []const u8,
|
||||
/// Current model key name.
|
||||
/// Use the default key name of the current model.
|
||||
modelKey: ?[]const u8 = null,
|
||||
},
|
||||
|
||||
/// Used when performing a many-to-many relationship through an association table.
|
||||
through: struct {
|
||||
/// Name of the join table.
|
||||
table: []const u8,
|
||||
/// The local foreign key name.
|
||||
/// Use the default key name of the current model.
|
||||
foreignKey: ?[]const u8 = null,
|
||||
/// The foreign key name in the join table.
|
||||
joinForeignKey: []const u8,
|
||||
/// The model key name in the join table.
|
||||
joinModelKey: []const u8,
|
||||
/// Associated model key name.
|
||||
/// Use the default key name of the associated model.
|
||||
modelKey: ?[]const u8 = null,
|
||||
},
|
||||
};
|
||||
|
||||
/// Type of related models collection field.
|
||||
pub const Models = struct {
|
||||
/// Name of the related model definition.
|
||||
related: []const u8,
|
||||
|
||||
/// Configuration of the relationship.
|
||||
relationship: OneConfiguration,
|
||||
};
|
20
lib/root.zig
Normal file
20
lib/root.zig
Normal file
|
@ -0,0 +1,20 @@
|
|||
const std = @import("std");
|
||||
const _api = @import("api.zig");
|
||||
const _registry = @import("registry.zig");
|
||||
const _model = @import("model.zig");
|
||||
|
||||
pub const Api = _api.Api;
|
||||
pub const Registry = _registry.Registry;
|
||||
|
||||
pub const Model = _model.Model;
|
||||
pub const ModelPrimaryKey = _model.ModelPrimaryKey;
|
||||
|
||||
pub const dispatchers = @import("dispatchers.zig");
|
||||
pub const types = @import("types.zig");
|
||||
pub const relationships = @import("relationships.zig");
|
||||
|
||||
pub const http = @import("http.zig");
|
||||
|
||||
test {
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
55
lib/types.zig
Normal file
55
lib/types.zig
Normal file
|
@ -0,0 +1,55 @@
|
|||
const std = @import("std");
|
||||
|
||||
/// Structure of a field column.
|
||||
pub const FieldColumn = struct {
|
||||
name: ?[]const u8 = null,
|
||||
type: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
/// Type of an integer field.
|
||||
pub const Int = struct {
|
||||
column: FieldColumn = .{},
|
||||
type: type = i64,
|
||||
};
|
||||
|
||||
/// Type of a serial field.
|
||||
pub const Serial = struct {
|
||||
column: FieldColumn = .{},
|
||||
type: type = i64,
|
||||
};
|
||||
|
||||
/// Type of a float field.
|
||||
pub const Float = struct {
|
||||
column: FieldColumn = .{},
|
||||
type: type = f64,
|
||||
};
|
||||
|
||||
/// Type of a decimal (fixed point) field.
|
||||
pub const Decimal = struct {
|
||||
column: FieldColumn = .{},
|
||||
type: type = []const u8,
|
||||
};
|
||||
|
||||
/// Type of a string field.
|
||||
pub const String = struct {
|
||||
column: FieldColumn = .{},
|
||||
type: type = []const u8,
|
||||
};
|
||||
|
||||
/// Type of a datetime field.
|
||||
pub const DateTime = struct {
|
||||
column: FieldColumn = .{},
|
||||
type: type = []const u8, //TODO maybe something else?
|
||||
};
|
||||
|
||||
/// Type of a JSON field.
|
||||
pub const Json = struct {
|
||||
column: FieldColumn = .{},
|
||||
type: type,
|
||||
};
|
||||
|
||||
/// Type of an array field.
|
||||
pub const Array = struct {
|
||||
column: FieldColumn = .{},
|
||||
type: type,
|
||||
};
|
69
lib/utils/comptime.zig
Normal file
69
lib/utils/comptime.zig
Normal file
|
@ -0,0 +1,69 @@
|
|||
const std = @import("std");
|
||||
|
||||
/// Append an element to the given array at comptime.
|
||||
pub fn append(array: anytype, element: anytype) @TypeOf(array ++ .{element}) {
|
||||
return array ++ .{element};
|
||||
}
|
||||
|
||||
/// Join strings into one, with the given separator in between.
|
||||
pub fn join(separator: []const u8, slices: []const[]const u8) []const u8 {
|
||||
if (slices.len == 0) return "";
|
||||
|
||||
// Compute total length of the string to make.
|
||||
const totalLen = total: {
|
||||
// Compute separator length.
|
||||
var total = separator.len * (slices.len - 1);
|
||||
// Add length of all slices.
|
||||
for (slices) |slice| total += slice.len;
|
||||
break :total total;
|
||||
};
|
||||
|
||||
var buffer: [totalLen]u8 = undefined;
|
||||
|
||||
// Based on std.mem.joinMaybeZ implementation.
|
||||
@memcpy(buffer[0..slices[0].len], slices[0]);
|
||||
var buf_index: usize = slices[0].len;
|
||||
for (slices[1..]) |slice| {
|
||||
@memcpy(buffer[buf_index .. buf_index + separator.len], separator);
|
||||
buf_index += separator.len;
|
||||
@memcpy(buffer[buf_index .. buf_index + slice.len], slice);
|
||||
buf_index += slice.len;
|
||||
}
|
||||
|
||||
// Put final buffer in a const variable to allow to use its value at runtime.
|
||||
const finalBuffer = buffer;
|
||||
return &finalBuffer;
|
||||
}
|
||||
|
||||
/// Convert a PascalCased identifier to a kebab-cased identifier.
|
||||
pub fn pascalToKebab(comptime pascalIdentifier: []const u8) []const u8 {
|
||||
// Array of all parts of the identifier (separated by uppercased characters).
|
||||
var parts: []const []const u8 = &[0][]const u8{};
|
||||
|
||||
// The current identifier part.
|
||||
var currentPart: []const u8 = &[0]u8{};
|
||||
|
||||
for (pascalIdentifier) |pascalChar| {
|
||||
// For each character...
|
||||
if (std.ascii.isUpper(pascalChar)) {
|
||||
// ... if it's uppercased, save the current part and initialize a new one.
|
||||
if (currentPart.len > 0) {
|
||||
// If the current part length is bigger than 0, save it.
|
||||
parts = append(parts, currentPart);
|
||||
}
|
||||
// Create a new part, with the lowercased character.
|
||||
currentPart = &[1]u8{std.ascii.toLower(pascalChar)};
|
||||
} else {
|
||||
// ... append the current (not uppercased) character to the current part.
|
||||
currentPart = append(currentPart, pascalChar);
|
||||
}
|
||||
}
|
||||
|
||||
// Append the last current part to parts list.
|
||||
if (currentPart.len > 0) {
|
||||
parts = append(parts, currentPart);
|
||||
}
|
||||
|
||||
// Join all the parts with "-" to create a kebab-cased identifier.
|
||||
return join("-", parts);
|
||||
}
|
6
src/mql.zig
Normal file
6
src/mql.zig
Normal file
|
@ -0,0 +1,6 @@
|
|||
const std = @import("std");
|
||||
const pgmql = @import("pgmql");
|
||||
|
||||
pub fn main() !void {
|
||||
std.debug.print("MQL\n", .{});
|
||||
}
|
94
tests/example.zig
Normal file
94
tests/example.zig
Normal file
|
@ -0,0 +1,94 @@
|
|||
const std = @import("std");
|
||||
const pgmql = @import("pgmql");
|
||||
|
||||
const _account = @import("models/account.zig");
|
||||
const _invoice = @import("models/invoice.zig");
|
||||
const _invoice_product = @import("models/invoice_product.zig");
|
||||
|
||||
pub const registry = pgmql.Registry(struct {
|
||||
pub const Account: pgmql.Model = _account.AccountModel;
|
||||
pub const Invoice: pgmql.Model = _invoice.InvoiceModel;
|
||||
pub const InvoiceProduct: pgmql.Model = _invoice_product.InvoiceProductModel;
|
||||
}, "Account");
|
||||
|
||||
const CreateInvoice = registry.dispatchers.Definition(struct {});
|
||||
|
||||
const api = registry.Api(struct {
|
||||
pub const Account = registry.dispatchers.Crud(registry.Models.Account).All(.{ .anyAuthenticated = true });
|
||||
|
||||
pub const Invoice = struct {
|
||||
pub const create = (CreateInvoice{
|
||||
.policy = .{ .anyAuthenticated = true },
|
||||
.run = struct {
|
||||
fn f(context: CreateInvoice.Context) void {
|
||||
_ = context; //TODO!
|
||||
}
|
||||
}.f,
|
||||
}).builder();
|
||||
|
||||
pub usingnamespace registry.dispatchers.Crud(registry.Models.Invoice).Read(.{.anyAuthenticated = true});
|
||||
pub usingnamespace registry.dispatchers.Crud(registry.Models.Invoice).Update(.{.anyAuthenticated = true});
|
||||
pub usingnamespace registry.dispatchers.Crud(registry.Models.Invoice).Delete(.{.anyAuthenticated = true});
|
||||
};
|
||||
|
||||
pub const InvoiceProduct = registry.dispatchers.Crud(registry.Models.InvoiceProduct).All(.{ .any = false });
|
||||
});
|
||||
|
||||
test "example dispatchers" {
|
||||
const dispatchers = api.getDispatchers();
|
||||
|
||||
try std.testing.expectEqual(12, dispatchers.len);
|
||||
|
||||
try std.testing.expectEqualStrings("account", dispatchers[0].getModelIdentifier());
|
||||
try std.testing.expectEqualStrings("read", dispatchers[0].getDispatchName());
|
||||
|
||||
try std.testing.expectEqualStrings("account", dispatchers[1].getModelIdentifier());
|
||||
try std.testing.expectEqualStrings("create", dispatchers[1].getDispatchName());
|
||||
|
||||
try std.testing.expectEqualStrings("account", dispatchers[2].getModelIdentifier());
|
||||
try std.testing.expectEqualStrings("update", dispatchers[2].getDispatchName());
|
||||
|
||||
try std.testing.expectEqualStrings("account", dispatchers[3].getModelIdentifier());
|
||||
try std.testing.expectEqualStrings("delete", dispatchers[3].getDispatchName());
|
||||
|
||||
|
||||
try std.testing.expectEqualStrings("invoice", dispatchers[4].getModelIdentifier());
|
||||
try std.testing.expectEqualStrings("read", dispatchers[4].getDispatchName());
|
||||
|
||||
try std.testing.expectEqualStrings("invoice", dispatchers[5].getModelIdentifier());
|
||||
try std.testing.expectEqualStrings("create", dispatchers[5].getDispatchName());
|
||||
|
||||
try std.testing.expectEqualStrings("invoice", dispatchers[6].getModelIdentifier());
|
||||
try std.testing.expectEqualStrings("update", dispatchers[6].getDispatchName());
|
||||
|
||||
try std.testing.expectEqualStrings("invoice", dispatchers[7].getModelIdentifier());
|
||||
try std.testing.expectEqualStrings("delete", dispatchers[7].getDispatchName());
|
||||
|
||||
|
||||
try std.testing.expectEqualStrings("invoice-product", dispatchers[8].getModelIdentifier());
|
||||
try std.testing.expectEqualStrings("read", dispatchers[8].getDispatchName());
|
||||
|
||||
try std.testing.expectEqualStrings("invoice-product", dispatchers[9].getModelIdentifier());
|
||||
try std.testing.expectEqualStrings("create", dispatchers[9].getDispatchName());
|
||||
|
||||
try std.testing.expectEqualStrings("invoice-product", dispatchers[10].getModelIdentifier());
|
||||
try std.testing.expectEqualStrings("update", dispatchers[10].getDispatchName());
|
||||
|
||||
try std.testing.expectEqualStrings("invoice-product", dispatchers[11].getModelIdentifier());
|
||||
try std.testing.expectEqualStrings("delete", dispatchers[11].getDispatchName());
|
||||
}
|
||||
|
||||
test "example model HTTP API" {
|
||||
_ = api;
|
||||
}
|
||||
|
||||
/// Example HTTP API executable.
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true }){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var httpSrv = pgmql.http.Server.init(allocator, api.getDispatchers());
|
||||
defer httpSrv.deinit();
|
||||
try httpSrv.start();
|
||||
}
|
20
tests/models/account.zig
Normal file
20
tests/models/account.zig
Normal file
|
@ -0,0 +1,20 @@
|
|||
const std = @import("std");
|
||||
const pgmql = @import("pgmql");
|
||||
const _example = @import("../example.zig");
|
||||
|
||||
pub const AccountModel = pgmql.Model{
|
||||
.table = "accounts",
|
||||
.primaryKey = .{ .single = "id" },
|
||||
|
||||
.definition = struct {
|
||||
pub const id = pgmql.types.Serial{};
|
||||
pub const createdAt = pgmql.types.DateTime{};
|
||||
pub const updatedAt = pgmql.types.DateTime{};
|
||||
pub const name = pgmql.types.String{};
|
||||
pub const role = pgmql.types.String{};
|
||||
},
|
||||
|
||||
//.crudPolicy = _example.builder.policies.crud(.{ .anyAuthenticated = true }),
|
||||
};
|
||||
|
||||
pub const Account = AccountModel.structure();
|
51
tests/models/invoice.zig
Normal file
51
tests/models/invoice.zig
Normal file
|
@ -0,0 +1,51 @@
|
|||
const std = @import("std");
|
||||
const pgmql = @import("pgmql");
|
||||
const _example = @import("../example.zig");
|
||||
|
||||
pub const CreateDispatcher = _example.builder.dispatchers.Dispatcher(struct {});
|
||||
|
||||
pub const InvoiceModel = pgmql.Model{
|
||||
.table = "invoices",
|
||||
.primaryKey = .{ .single = "id" },
|
||||
|
||||
.definition = struct {
|
||||
pub const id = pgmql.types.Serial{};
|
||||
pub const createdAt = pgmql.types.DateTime{};
|
||||
pub const amount = pgmql.types.Decimal{
|
||||
.column = .{
|
||||
.type = "MONEY",
|
||||
},
|
||||
};
|
||||
|
||||
pub const products = pgmql.relationships.Models{
|
||||
.related = "InvoiceProduct",
|
||||
.relationship = .{
|
||||
.direct = .{ .foreignKey = "invoiceId", },
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
// .crudPolicy = _example.builder.policies.crud(.{ .any = false }),
|
||||
//
|
||||
// .dispatchers = struct {
|
||||
// pub const create = CreateDispatcher{
|
||||
// .policy = .{
|
||||
// .func = struct { pub fn f(context: CreateDispatcher.PolicyContext) bool {
|
||||
// if (context.account) |account| {
|
||||
// return std.mem.eql(u8, "billing", account.role) or std.mem.eql(u8, "admin", account.role);
|
||||
// } else {
|
||||
// return false;
|
||||
// }
|
||||
// } }.f
|
||||
// },
|
||||
// .run = struct { pub fn f(context: CreateDispatcher.Context) void {
|
||||
// _ = context;
|
||||
//
|
||||
// const invoice = _example.registry.models.Invoice.initStruct();
|
||||
// _ = invoice;
|
||||
// } }.f,
|
||||
// };
|
||||
// },
|
||||
};
|
||||
|
||||
pub const Invoice = InvoiceModel.structure();
|
30
tests/models/invoice_product.zig
Normal file
30
tests/models/invoice_product.zig
Normal file
|
@ -0,0 +1,30 @@
|
|||
const std = @import("std");
|
||||
const pgmql = @import("pgmql");
|
||||
const _example = @import("../example.zig");
|
||||
|
||||
pub const InvoiceProductModel = pgmql.Model{
|
||||
.table = "invoices_products",
|
||||
.primaryKey = .{ .single = "id" },
|
||||
|
||||
.definition = struct {
|
||||
pub const id = pgmql.types.Serial{};
|
||||
pub const invoiceId = pgmql.types.Int{};
|
||||
pub const details = pgmql.types.String{};
|
||||
pub const amount = pgmql.types.Decimal{
|
||||
.column = .{
|
||||
.type = "MONEY",
|
||||
},
|
||||
};
|
||||
|
||||
pub const invoice = pgmql.relationships.Model{
|
||||
.related = "Invoice",
|
||||
.relationship = .{
|
||||
.direct = .{ .foreignKey = "invoiceId" },
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
// .crudPolicy = _example.builder.policies.crud(.{ .any = false }),
|
||||
};
|
||||
|
||||
pub const InvoiceProduct = InvoiceProductModel.structure();
|
3
tests/root.zig
Normal file
3
tests/root.zig
Normal file
|
@ -0,0 +1,3 @@
|
|||
comptime {
|
||||
_ = @import("example.zig");
|
||||
}
|
Loading…
Add table
Reference in a new issue