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