Initial project setup with a declarative API for models definition and their dispatchers.

This commit is contained in:
Madeorsk 2024-12-08 01:21:35 +01:00
commit bb11117a6d
Signed by: Madeorsk
GPG key ID: 677E51CA765BB79F
20 changed files with 1147 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
# IDEA
*.iml
# Zig
zig-out/
.zig-cache/

29
README.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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();

View 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
View file

@ -0,0 +1,3 @@
comptime {
_ = @import("example.zig");
}