Setup and write documentation website, logo update.
This commit is contained in:
parent
b897ce70ab
commit
bc654b35ec
21 changed files with 3782 additions and 35 deletions
8
docs/.editorconfig
Normal file
8
docs/.editorconfig
Normal file
|
@ -0,0 +1,8 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
indent_style = tab
|
||||
indent_size = 2
|
||||
charset = utf-8
|
||||
insert_final_newline = true
|
4
docs/.gitattributes
vendored
Normal file
4
docs/.gitattributes
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
/.yarn/** linguist-vendored
|
||||
/.yarn/releases/* binary
|
||||
/.yarn/plugins/**/* binary
|
||||
/.pnp.* binary linguist-generated
|
6
docs/.gitignore
vendored
Normal file
6
docs/.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
# Yarn
|
||||
.yarn/*
|
||||
.pnp.*
|
||||
|
||||
# Vitepress
|
||||
.vitepress/cache
|
49
docs/.vitepress/config.mts
Normal file
49
docs/.vitepress/config.mts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import {defineConfig} from 'vitepress';
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
title: "ZRM",
|
||||
description: "Website of the ZRM library.",
|
||||
themeConfig: {
|
||||
logo: {
|
||||
light: "/logo.svg",
|
||||
dark: "/logo-dark.svg",
|
||||
},
|
||||
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
nav: [
|
||||
{text: "Home", link: "/"},
|
||||
{text: "Documentation", link: "/docs"},
|
||||
],
|
||||
|
||||
search: {
|
||||
provider: "local",
|
||||
},
|
||||
|
||||
sidebar: [
|
||||
{
|
||||
text: "Documentation",
|
||||
link: "/docs",
|
||||
items: [
|
||||
{text: "Installation", link: "/docs/install"},
|
||||
{text: "Database", link: "/docs/database"},
|
||||
{text: "Repositories", link: "/docs/repositories"},
|
||||
{text: "Queries", link: "/docs/queries"},
|
||||
{text: "Insert & update", link: "/docs/insert-update"},
|
||||
{text: "Relationships", link: "/docs/relationships"},
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
socialLinks: [
|
||||
{icon: "git", link: "https://code.zeptotech.net/zedd/zrm"},
|
||||
{icon: {
|
||||
svg: '<?xml version="1.0" encoding="UTF-8"?><svg version="1.1" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="m375.47 87.498c-77.422 4e-3 -154.85-4e-3 -232.27 0.0248-7.7478 0.26053-14.842 6.6567-15.698 14.398-0.42683 3.6963-0.0855 7.4215-0.20025 11.131 0.0438 5.8956-0.15111 11.802 0.19405 17.689 1.0025 7.6834 8.0618 14.125 15.846 14.215 4.2666 0.16804 8.5375-4e-3 12.806 0.0652 47.273 4e-3 94.547 8e-3 141.82 0.0124-59.064 78.8-118.13 157.6-177.17 236.42-4.1696 5.8777-5.9165 13.246-5.5202 20.391 7e-4 4.3283-0.39389 8.8988 1.5211 12.928 2.6294 5.7046 8.6562 9.7403 14.978 9.7021 3.7094 0.0962 7.4213-0.0206 11.132 0.0372 79.299-8e-5 158.6-4.9e-4 237.9-0.0248 7.7662-0.24442 14.896-6.6555 15.736-14.42 0.4355-3.6719 0.0838-7.374 0.20189-11.059-0.0475-5.9531 0.16144-11.918-0.19405-17.862-1.0754-7.9984-8.7536-14.488-16.828-14.158-56.76-6.6e-4 -113.52-9.9e-4 -170.28-2e-3 59.168-78.755 118.35-157.5 177.49-236.27 4.1886-5.8581 6.0254-13.209 5.6328-20.359-0.033-4.3117 0.43852-8.8577-1.403-12.897-2.671-6.0034-9.0893-10.229-15.705-9.9512z" stroke-width="1.3882"/><path d="m412.39 47.672c-4.8102 0.08112-9.8691 1.9242-12.803 5.8954-3.1585 4.1727-3.3865 10.069-1.2002 14.73 1.5724 3.5822 3.9639 6.716 5.7997 10.158 9.2795 15.896 17.67 32.32 24.949 49.228 3.9724 9.2879 7.6063 18.957 10.803 28.551 3.92 11.803 7.3391 23.791 10.043 35.968 2.7053 12.009 4.5884 24.371 5.742 36.692 0.55268 5.8068 0.90486 11.631 1.0857 17.461 0.0329 1.6505 0.0973 3.5193 0.10888 5.2554 0.082 5.6578 3e-3 11.344-0.19389 16.944-0.10313 2.9514-0.28719 6.1037-0.51345 9.1456-0.59634 8.0708-1.5579 16.346-2.8077 24.349-1.5636 10.089-3.8089 20.181-6.4161 30.111-2.6012 9.8786-5.6052 19.65-8.9883 29.289-1.3489 3.8284-2.8061 7.7936-4.2981 11.637-4.4468 11.564-9.6421 22.97-15.217 34.094-5.476 11.002-11.561 21.865-17.891 32.467-1.4743 2.3235-2.817 4.7811-3.3944 7.4976-1.0035 4.1928-0.25168 8.936 2.6191 12.254 2.9126 3.4016 7.4828 5.0572 11.898 4.927 5.9702-0.0534 11.822-1.8401 17.148-4.4584 4.2796-2.1304 8.2468-4.9643 11.372-8.6053 2.8245-3.2611 5.4571-6.6826 8.1435-10.057 19.783-25.454 35.502-54.065 46.284-84.449 9.7389-27.315 15.5-56.057 16.96-85.021 1.0691-21.939-0.089-44.005-3.7557-65.669-6.031-35.934-18.69-70.751-37.215-102.13-9.2406-15.755-19.847-30.719-31.751-44.574-6.0592-6.1653-14.206-10.202-22.74-11.48-1.2512-0.14827-2.5111-0.23355-3.7715-0.20898z" stroke-width="1.3461"/><path d="m99.614 464.33c4.8102-0.0811 9.8691-1.9242 12.803-5.8954 3.1585-4.1727 3.3865-10.069 1.2002-14.73-1.5724-3.5822-3.9639-6.716-5.7997-10.158-9.2795-15.896-17.67-32.32-24.949-49.228-3.9724-9.2879-7.6063-18.957-10.803-28.551-3.92-11.803-7.3391-23.791-10.043-35.968-2.7053-12.009-4.5884-24.371-5.742-36.692-0.55268-5.8068-0.90486-11.631-1.0857-17.461-0.0329-1.6505-0.0973-3.5193-0.10888-5.2554-0.082-5.6578-3e-3 -11.344 0.19389-16.944 0.10313-2.9514 0.28719-6.1037 0.51345-9.1456 0.59634-8.0708 1.5579-16.346 2.8077-24.349 1.5636-10.089 3.8089-20.181 6.4161-30.111 2.6012-9.8786 5.6052-19.65 8.9883-29.289 1.3489-3.8284 2.8061-7.7936 4.2981-11.637 4.4468-11.564 9.6421-22.97 15.217-34.094 5.476-11.002 11.561-21.865 17.891-32.467 1.4743-2.3235 2.817-4.7811 3.3944-7.4976 1.0035-4.1928 0.25168-8.936-2.6191-12.254-2.9126-3.4016-7.4828-5.0572-11.898-4.927-5.9702 0.0534-11.822 1.8401-17.148 4.4584-4.2796 2.1304-8.2468 4.9643-11.372 8.6053-2.8245 3.2611-5.4571 6.6826-8.1435 10.057-19.783 25.454-35.502 54.065-46.284 84.449-9.7389 27.315-15.5 56.057-16.96 85.021-1.0691 21.939 0.089 44.005 3.7557 65.669 6.031 35.934 18.69 70.751 37.215 102.13 9.2406 15.755 19.847 30.719 31.751 44.574 6.0592 6.1653 14.206 10.202 22.74 11.48 1.2512 0.14827 2.5111 0.23355 3.7715 0.20898z" stroke-width="1.3461"/></svg>',
|
||||
}, link: "https://code.zeptotech.net/zedd"},
|
||||
],
|
||||
|
||||
footer: {
|
||||
message: "<a class='zeptotech' href='https://zeptotech.fr' target='_blank' ><img src='/zeptotech.svg' alt='Zeptotech' /></a> Powered by Zeptotech"
|
||||
}
|
||||
},
|
||||
});
|
17
docs/.vitepress/theme/index.ts
Normal file
17
docs/.vitepress/theme/index.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
// https://vitepress.dev/guide/custom-theme
|
||||
import {h} from 'vue';
|
||||
import type {Theme} from 'vitepress';
|
||||
import DefaultTheme from 'vitepress/theme';
|
||||
import './style.css';
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
Layout: () => {
|
||||
return h(DefaultTheme.Layout, null, {
|
||||
// https://vitepress.dev/guide/extending-default-theme#layout-slots
|
||||
})
|
||||
},
|
||||
enhanceApp({app, router, siteData})
|
||||
{
|
||||
}
|
||||
} satisfies Theme;
|
170
docs/.vitepress/theme/style.css
Normal file
170
docs/.vitepress/theme/style.css
Normal file
|
@ -0,0 +1,170 @@
|
|||
/**
|
||||
* Customize default theme styling by overriding CSS variables:
|
||||
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
|
||||
*/
|
||||
|
||||
/**
|
||||
* Colors
|
||||
*
|
||||
* Each colors have exact same color scale system with 3 levels of solid
|
||||
* colors with different brightness, and 1 soft color.
|
||||
*
|
||||
* - `XXX-1`: The most solid color used mainly for colored text. It must
|
||||
* satisfy the contrast ratio against when used on top of `XXX-soft`.
|
||||
*
|
||||
* - `XXX-2`: The color used mainly for hover state of the button.
|
||||
*
|
||||
* - `XXX-3`: The color for solid background, such as bg color of the button.
|
||||
* It must satisfy the contrast ratio with pure white (#ffffff) text on
|
||||
* top of it.
|
||||
*
|
||||
* - `XXX-soft`: The color used for subtle background such as custom container
|
||||
* or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
|
||||
* on top of it.
|
||||
*
|
||||
* The soft color must be semi transparent alpha channel. This is crucial
|
||||
* because it allows adding multiple "soft" colors on top of each other
|
||||
* to create a accent, such as when having inline code block inside
|
||||
* custom containers.
|
||||
*
|
||||
* - `default`: The color used purely for subtle indication without any
|
||||
* special meanings attached to it such as bg color for menu hover state.
|
||||
*
|
||||
* - `brand`: Used for primary brand colors, such as link text, button with
|
||||
* brand theme, etc.
|
||||
*
|
||||
* - `tip`: Used to indicate useful information. The default theme uses the
|
||||
* brand color for this by default.
|
||||
*
|
||||
* - `warning`: Used to indicate warning to the users. Used in custom
|
||||
* container, badges, etc.
|
||||
*
|
||||
* - `danger`: Used to show error, or dangerous message to the users. Used
|
||||
* in custom container, badges, etc.
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root
|
||||
{
|
||||
--vp-c-default-1: var(--vp-c-gray-1);
|
||||
--vp-c-default-2: var(--vp-c-gray-2);
|
||||
--vp-c-default-3: var(--vp-c-gray-3);
|
||||
--vp-c-default-soft: var(--vp-c-gray-soft);
|
||||
|
||||
--vp-c-brand-1: var(--vp-c-indigo-1);
|
||||
--vp-c-brand-2: var(--vp-c-indigo-2);
|
||||
--vp-c-brand-3: var(--vp-c-indigo-3);
|
||||
--vp-c-brand-soft: var(--vp-c-indigo-soft);
|
||||
|
||||
--vp-c-tip-1: var(--vp-c-brand-1);
|
||||
--vp-c-tip-2: var(--vp-c-brand-2);
|
||||
--vp-c-tip-3: var(--vp-c-brand-3);
|
||||
--vp-c-tip-soft: var(--vp-c-brand-soft);
|
||||
|
||||
--vp-c-warning-1: var(--vp-c-yellow-1);
|
||||
--vp-c-warning-2: var(--vp-c-yellow-2);
|
||||
--vp-c-warning-3: var(--vp-c-yellow-3);
|
||||
--vp-c-warning-soft: var(--vp-c-yellow-soft);
|
||||
|
||||
--vp-c-danger-1: var(--vp-c-red-1);
|
||||
--vp-c-danger-2: var(--vp-c-red-2);
|
||||
--vp-c-danger-3: var(--vp-c-red-3);
|
||||
--vp-c-danger-soft: var(--vp-c-red-soft);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Button
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root
|
||||
{
|
||||
--vp-button-brand-border: transparent;
|
||||
--vp-button-brand-text: var(--vp-c-white);
|
||||
--vp-button-brand-bg: var(--vp-c-brand-3);
|
||||
--vp-button-brand-hover-border: transparent;
|
||||
--vp-button-brand-hover-text: var(--vp-c-white);
|
||||
--vp-button-brand-hover-bg: var(--vp-c-brand-2);
|
||||
--vp-button-brand-active-border: transparent;
|
||||
--vp-button-brand-active-text: var(--vp-c-white);
|
||||
--vp-button-brand-active-bg: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Home
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root
|
||||
{
|
||||
--vp-home-hero-name-color: transparent;
|
||||
--vp-home-hero-name-background: -webkit-linear-gradient(
|
||||
120deg,
|
||||
#22aeff 30%,
|
||||
#00f1cb
|
||||
);
|
||||
|
||||
--vp-home-hero-image-background-image: linear-gradient(
|
||||
-45deg,
|
||||
#22aeff 50%,
|
||||
#00f1cb 50%
|
||||
);
|
||||
--vp-home-hero-image-filter: blur(44px);
|
||||
}
|
||||
|
||||
@media (min-width: 640px)
|
||||
{
|
||||
:root
|
||||
{
|
||||
--vp-home-hero-image-filter: blur(56px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px)
|
||||
{
|
||||
:root
|
||||
{
|
||||
--vp-home-hero-image-filter: blur(68px);
|
||||
}
|
||||
}
|
||||
|
||||
.VPFeatures .icon svg
|
||||
{
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Custom Block
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root
|
||||
{
|
||||
--vp-custom-block-tip-border: transparent;
|
||||
--vp-custom-block-tip-text: var(--vp-c-text-1);
|
||||
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
|
||||
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Algolia
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
.DocSearch
|
||||
{
|
||||
--docsearch-primary-color: var(--vp-c-brand-1) !important;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Component: Footer
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
footer a.zeptotech
|
||||
{
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
footer img
|
||||
{
|
||||
display: block;
|
||||
margin: auto;
|
||||
width: 3em;
|
||||
max-width: 95%;
|
||||
}
|
3
docs/README.md
Normal file
3
docs/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# ZRM :: Documentation
|
||||
|
||||
This is the documentation website of the ZRM library.
|
70
docs/docs/database.md
Normal file
70
docs/docs/database.md
Normal file
|
@ -0,0 +1,70 @@
|
|||
# Database
|
||||
|
||||
As a database-to-zig mapper, ZRM obviously needs to connect to a database to operate. All interactions with ZRM features will first need to define a database connection.
|
||||
|
||||
::: info
|
||||
ZRM currently only supports PostgreSQL through [pg.zig](https://github.com/karlseguin/pg.zig). More DBMS's support is [planned](https://code.zeptotech.net/zedd/zrm/issues/8).
|
||||
:::
|
||||
|
||||
## Connection
|
||||
|
||||
As ZRM is currently using [pg.zig](https://github.com/karlseguin/pg.zig) to connect to PostgreSQL databases, you can find a full documentation and example on the [pg.zig documentation](https://github.com/karlseguin/pg.zig#example).
|
||||
|
||||
```zig
|
||||
const database = try pg.Pool.init(allocator, .{
|
||||
.connect = .{
|
||||
.host = "localhost",
|
||||
.port = 5432,
|
||||
},
|
||||
.auth = .{
|
||||
.username = "zrm",
|
||||
.password = "zrm",
|
||||
.database = "zrm",
|
||||
},
|
||||
.size = 5,
|
||||
});
|
||||
```
|
||||
|
||||
## Connector
|
||||
|
||||
ZRM does not use opened connections directly. All features use a generic interface called a `Connector`. A connector manages how connections are opened and released for a group of operations. There are currently two types of connectors in ZRM.
|
||||
|
||||
### Pool connector
|
||||
|
||||
The pool connector simply use a `pg.Pool` to get connections when needed. The only requirement is an opened database pool from pg.zig.
|
||||
|
||||
```zig
|
||||
var poolConnector = zrm.database.PoolConnector{
|
||||
.pool = database,
|
||||
};
|
||||
```
|
||||
|
||||
### Session connector
|
||||
|
||||
A session connector use a single connection while it is initialized, which is very useful when you want to perform a group of operations in a transaction. The deinitialization releases the connection.
|
||||
|
||||
```zig
|
||||
// Start a new session.
|
||||
var session = try zrm.Session.init(database);
|
||||
defer session.deinit();
|
||||
```
|
||||
|
||||
Using sessions, you can start transactions and use savepoints.
|
||||
|
||||
```zig
|
||||
try session.beginTransaction();
|
||||
|
||||
// Do something.
|
||||
|
||||
try session.savepoint("my_savepoint");
|
||||
|
||||
// Do something else.
|
||||
|
||||
try session.rollbackTo("my_savepoint");
|
||||
|
||||
// Do a third thing.
|
||||
|
||||
try session.commitTransaction();
|
||||
// or
|
||||
try session.rollbackTransaction();
|
||||
```
|
110
docs/docs/index.md
Normal file
110
docs/docs/index.md
Normal file
|
@ -0,0 +1,110 @@
|
|||
# Documentation
|
||||
|
||||
Welcome to the ZRM documentation!
|
||||
|
||||
ZRM is a try to make a fast and efficient zig-native [ORM](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping) (Object Relational Mapper). With ZRM, you can define your zig models structures and easily link them to your database tables.
|
||||
|
||||
ZRM is using [compile-time features](https://ziglang.org/documentation/0.13.0/#toc-comptime) of the zig language to do a lot of generic work you clearly don't want to do.
|
||||
|
||||
## How does it look like?
|
||||
|
||||
```zig
|
||||
/// User model.
|
||||
pub const User = struct {
|
||||
pub const Table = struct {
|
||||
id: i32,
|
||||
name: []const u8,
|
||||
|
||||
pub const Insert = struct {
|
||||
name: []const u8,
|
||||
};
|
||||
};
|
||||
|
||||
id: i32,
|
||||
name: []const u8,
|
||||
info: ?UserInfo = null,
|
||||
};
|
||||
/// Repository of User model.
|
||||
pub const UserRepository = zrm.Repository(User, User.Table, .{
|
||||
.table = "example_users",
|
||||
.insertShape = User.Table.Insert,
|
||||
|
||||
.key = &[_][]const u8{"id"},
|
||||
|
||||
.fromSql = zrm.helpers.TableModel(User, User.Table).copyTableToModel,
|
||||
.toSql = zrm.helpers.TableModel(User, User.Table).copyModelToTable,
|
||||
});
|
||||
/// Relationships of User model.
|
||||
pub const UserRelationships = UserRepository.relationships.define(.{
|
||||
.info = UserRepository.relationships.one(UserInfoRepository, .{
|
||||
.reverse = .{},
|
||||
}),
|
||||
});
|
||||
|
||||
/// User info model.
|
||||
pub const UserInfo = struct {
|
||||
pub const Table = struct {
|
||||
user_id: i32,
|
||||
birthdate: i64,
|
||||
|
||||
pub const Insert = struct {
|
||||
user_id: i32,
|
||||
birthdate: i64,
|
||||
};
|
||||
};
|
||||
|
||||
user_id: i32,
|
||||
birthdate: i64,
|
||||
|
||||
user: ?*User = null,
|
||||
};
|
||||
/// Repository of UserInfo model.
|
||||
pub const UserInfoRepository = zrm.Repository(UserInfo, UserInfo.Table, .{
|
||||
.table = "example_users_info",
|
||||
.insertShape = UserInfo.Table.Insert,
|
||||
|
||||
.key = &[_][]const u8{"user_id"},
|
||||
|
||||
.fromSql = zrm.helpers.TableModel(UserInfo, UserInfo.Table).copyTableToModel,
|
||||
.toSql = zrm.helpers.TableModel(UserInfo, UserInfo.Table).copyModelToTable,
|
||||
});
|
||||
/// Relationships of UserInfo model.
|
||||
pub const UserInfoRelationships = UserInfoRepository.relationships.define(.{
|
||||
.user = UserInfoRepository.relationships.one(UserRepository, .{
|
||||
.direct = .{
|
||||
.foreignKey = "user_id",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Initialize a query to get users.
|
||||
var firstQuery = UserRepository.QueryWith(
|
||||
// Retrieve info of users.
|
||||
&[_]zrm.relationships.Relationship{UserRelationships.info}
|
||||
).init(std.testing.allocator, poolConnector.connector(), .{});
|
||||
// We want to get the user with ID 2.
|
||||
try firstQuery.whereKey(2);
|
||||
defer firstQuery.deinit();
|
||||
|
||||
// Executing the query and getting its result.
|
||||
var firstResult = try firstQuery.get(std.testing.allocator);
|
||||
defer firstResult.deinit();
|
||||
|
||||
if (firstResult.first()) |myUser| {
|
||||
// A user has been found.
|
||||
std.debug.print("birthdate timestamp: {d}\n", .{user.info.birthdate});
|
||||
// Changing user name.
|
||||
myUser.name = "zrm lover";
|
||||
// Saving the altered user.
|
||||
const saveResult = UserRepository.save(allocator, poolConnector.connector(), myUser);
|
||||
defer saveResult.deinit();
|
||||
} else {
|
||||
std.debug.print("no user with id 2 :-(\n", .{});
|
||||
}
|
||||
```
|
||||
|
||||
## Discover
|
||||
|
||||
This documentation will help you to discover all the features provided by ZRM and how you can use them in your project. We'll cover [installation](/docs/install), [database connection](/docs/database), [repository declaration](/docs/repositories), [queries](/docs/queries), [insertions and updates](/docs/insert-update), and [relationships](/docs/relationships). Most examples are based on a test file, [`tests/example.zig`](https://code.zeptotech.net/zedd/zrm/src/branch/main/tests/example.zig), which demonstrates and tests models, repositories, relationships and queries.
|
147
docs/docs/insert-update.md
Normal file
147
docs/docs/insert-update.md
Normal file
|
@ -0,0 +1,147 @@
|
|||
# Insert & update
|
||||
|
||||
To define and use inserts and updates, you must have a fully defined [repository](/docs/repositories). In this tutorial, we'll be assuming that we have a defined repository for a user model, as it's defined in [this section](/docs/repositories.html#define-a-repository).
|
||||
|
||||
Executing inserts and updates also require to [set up a connection to your database](/docs/databases). We'll also be assuming that we have a working database connector set up, as it's defined in [this section](/docs/database#pool-connector).
|
||||
|
||||
## Insert
|
||||
|
||||
Just like queries, we can insert models using the type `Insert` published on repositories. With this type, the inserted shape is the default one of the repository. You can customize this structure by using `InsertCustom` function.
|
||||
|
||||
```zig
|
||||
const insertQuery = UserRepository.Insert.init(allocator, poolConnector.connector());
|
||||
// or
|
||||
const insertQuery = UserRepository.InsertCustom(struct {
|
||||
id: i32,
|
||||
name: []const u8,
|
||||
}).init(allocator, poolConnector.connector());
|
||||
```
|
||||
|
||||
If you just need to insert a model without any other parameters, repositories provide a `create` function which do just that. The given model **will be altered** with inserted row data.
|
||||
|
||||
```zig
|
||||
const results = UserRepository.create(allocator, poolConnector.connector(), model);
|
||||
defer results.deinit();
|
||||
```
|
||||
|
||||
### Values
|
||||
|
||||
With an insert query, we can pass our values to insert with the `values` function. This looks like [`set` function of update queries](#values-1).
|
||||
|
||||
```zig
|
||||
// Insert a single model.
|
||||
try insertQuery.values(model);
|
||||
// Insert an array of models.
|
||||
try insertQuery.values(&[_]Model{firstModel, secondModel});
|
||||
|
||||
// Insert a table-shaped structure.
|
||||
try insertQuery.values(table);
|
||||
// Insert an array of table-shaped structures.
|
||||
try insertQuery.values(&[_]Model.Table{firstTable, secondTable});
|
||||
|
||||
// Insert a structure matching InsertShape.
|
||||
try insertQuery.values(insertShapeStructure);
|
||||
// Insert an array of structures matching InsertShape.
|
||||
try insertQuery.values(&[_]Model.Table.Insert{firstInsertShape, secondInsertShape});
|
||||
```
|
||||
|
||||
### Returning
|
||||
|
||||
It's often useful to retrieve inserted data after the query. One use case would for example to get the inserted auto-increment IDs of the models. We can do this using the `returningX` functions of the insert query builder.
|
||||
|
||||
::: danger
|
||||
Never put user-sent values as selected columns. This could lead to severe security issues (like [SQL injections](https://en.wikipedia.org/wiki/SQL_injection)).
|
||||
:::
|
||||
|
||||
#### Returning all
|
||||
|
||||
This will return all the columns of the inserted rows.
|
||||
|
||||
```zig
|
||||
try insertQuery.values(...);
|
||||
insertQuery.returningAll();
|
||||
```
|
||||
|
||||
#### Returning columns
|
||||
|
||||
This will return all the provided columns of the inserted rows.
|
||||
|
||||
```zig
|
||||
try insertQuery.values(...);
|
||||
insertQuery.returningColumns(&[_][]const u8{"id", "name"});
|
||||
```
|
||||
|
||||
#### Raw returning
|
||||
|
||||
We can also directly provide raw `RETURNING` clause content.
|
||||
|
||||
```zig
|
||||
try insertQuery.values(...);
|
||||
insertQuery.returning("id, label AS name");
|
||||
```
|
||||
|
||||
### Results
|
||||
|
||||
We can perform the insertion by running `insert` on the insert query.
|
||||
|
||||
```zig
|
||||
const results = try insertQuery.insert(allocator);
|
||||
defer results.deinit();
|
||||
```
|
||||
|
||||
The results of an insert query are the same as normal queries. You can find the documentation about it in [its dedicated section](/docs/queries#results).
|
||||
|
||||
## Update
|
||||
|
||||
To make an update query, we must provide the structure of the updated columns (called update shape).
|
||||
|
||||
```zig
|
||||
const updateQuery = UserRepository.Update(struct { name: []const u8 }).init(allocator, poolConnector.connector());
|
||||
```
|
||||
|
||||
If you just need to update a model without any other parameters, repositories provide a `save` function which do just that. The given model **will be altered** with updated row data.
|
||||
|
||||
```zig
|
||||
const results = UserRepository.save(allocator, poolConnector.connector(), model);
|
||||
defer results.deinit();
|
||||
```
|
||||
|
||||
### Values
|
||||
|
||||
With an update query, we can set our updated values with the `set` function. This looks like [`values` function of insert queries](#values).
|
||||
|
||||
```zig
|
||||
// Set data of a single model.
|
||||
try updateQuery.set(model);
|
||||
// Set data of an array of models.
|
||||
try updateQuery.values(&[_]Model{firstModel, secondModel});
|
||||
|
||||
// Set data of a table-shaped structure.
|
||||
try updateQuery.values(table);
|
||||
// Set data of an array of table-shaped structures.
|
||||
try updateQuery.values(&[_]Model.Table{firstTable, secondTable});
|
||||
|
||||
// Set data of a structure matching UpdateShape.
|
||||
try updateQuery.values(myUpdate);
|
||||
// Set data of an array of structures matching UpdateShape.
|
||||
try updateQuery.values(&[_]UpdateStruct{myFirstUpdate, mySecondUpdate});
|
||||
```
|
||||
|
||||
### Conditions
|
||||
|
||||
The conditions building API is the same as normal queries. You can find the documentation about it in [its dedicated section](/docs/queries#conditions).
|
||||
|
||||
### Returning
|
||||
|
||||
The returning columns API is the same as insert queries. You can find the documentation about it in [its dedicated section](#returning).
|
||||
|
||||
### Results
|
||||
|
||||
We can perform the update by running `update` on the update query.
|
||||
|
||||
```zig
|
||||
const results = try updateQuery.update(allocator);
|
||||
defer results.deinit();
|
||||
```
|
||||
|
||||
The results of an update query are the same as normal queries. You can find the documentation about it in [its dedicated section](/docs/queries#results).
|
41
docs/docs/install.md
Normal file
41
docs/docs/install.md
Normal file
|
@ -0,0 +1,41 @@
|
|||
# Installation
|
||||
|
||||
You can easily install ZRM using the `zig fetch` command:
|
||||
|
||||
```shell
|
||||
$ zig fetch --save https://code.zeptotech.net/zedd/zrm/archive/v0.3.0.tar.gz
|
||||
```
|
||||
|
||||
::: info
|
||||
You can tweak the version if you want to use something else than the latest stable one, you can find all available versions in [the tags page](https://code.zeptotech.net/zedd/zrm/tags) on the repository.
|
||||
:::
|
||||
|
||||
This should add something like the following in `build.zig.zon` dependencies:
|
||||
|
||||
```zon
|
||||
.{
|
||||
// ...
|
||||
.dependencies = .{
|
||||
// ...
|
||||
.zrm = .{
|
||||
.url = "https://code.zeptotech.net/zedd/zrm/archive/v0.3.0.tar.gz",
|
||||
.hash = "12200fe147879d72381633e6f44d76db2c8a603cda1969b4e474c15c31052dbb24b7",
|
||||
},
|
||||
// ...
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Then, you can add the `zrm` module to your project build in `build.zig`.
|
||||
|
||||
```zig
|
||||
// Add zrm dependency.
|
||||
const zrm = b.dependency("zrm", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
exe.root_module.addImport("zrm", zrm.module("zrm"));
|
||||
```
|
||||
|
||||
You can now start to use ZRM! Why not trying to define [your first repository](/docs/repositories)?
|
209
docs/docs/queries.md
Normal file
209
docs/docs/queries.md
Normal file
|
@ -0,0 +1,209 @@
|
|||
# Queries
|
||||
|
||||
To define and use queries, you must have a fully defined [repository](/docs/repositories). In this tutorial, we'll be assuming that we have a defined repository for a user model, as it's defined in [this section](/docs/repositories.html#define-a-repository).
|
||||
|
||||
Executing queries also require to [set up a connection to your database](/docs/databases). We'll also be assuming that we have a working database connector set up, as it's defined in [this section](/docs/database#pool-connector).
|
||||
|
||||
## Query building
|
||||
|
||||
ZRM repositories provide a model query builder by default. We can access it from the published type `Query`.
|
||||
|
||||
### Basics
|
||||
|
||||
Let's start with the most simple query we can make.
|
||||
|
||||
```zig
|
||||
var myFirstQuery = UserRepository.Query.init(
|
||||
allocator, poolConnector.connector(), .{}
|
||||
);
|
||||
defer myFirstQuery.deinit();
|
||||
```
|
||||
|
||||
As you can see, we're creating a new query instance with our [previously defined connector](/docs/database#pool-connector). The last argument has no mandatory fields, but here are all the available options:
|
||||
|
||||
- `select`: a raw query part where we can set all columns that we want to select. By default, all columns of the table are retrieved (in the form of `"table_name".*`).
|
||||
- `join`: a raw query part where we can set all joined tables. By default, nothing is set (~ empty string) and there will be no `JOIN` clause.
|
||||
- `where`: a raw query part where we can set all the conditions to apply to the query. By default, nothing is set and there will be no `WHERE` clause.
|
||||
|
||||
::: warning
|
||||
It is currently **NOT recommended** to use these variables to build your queries. This configuration object is experimental and might be used later to define comptime-known parts of the query, and may also be entirely removed.
|
||||
:::
|
||||
|
||||
Based on the current configuration, we can execute this query as is, and ZRM will try to get **all** models in the defined table.
|
||||
|
||||
### Conditions
|
||||
|
||||
::: warning
|
||||
Calling any of the `whereX` functions on a query overrides anything that has been previously set. If you call `whereValue` two times, only the secondly defined condition will be kept. If you need to have multiple conditions at once, you should use the [conditions builder](/docs/queries#conditions-builder).
|
||||
:::
|
||||
|
||||
#### Simple value
|
||||
|
||||
Add a condition between a column and a runtime value. The type of the value must be provided as a first argument. Any valid SQL operator is accepted.
|
||||
|
||||
```zig
|
||||
try query.whereValue(usize, "id", "!=", 1);
|
||||
try query.whereValue(f32, "\"products\".\"amount\"", "<", 35.25);
|
||||
```
|
||||
|
||||
#### Primary keys
|
||||
|
||||
Add a condition on the primary keys. If the primary key is composite, a structure with all the keys values is expected.
|
||||
|
||||
```zig
|
||||
// Find the model with ID 1.
|
||||
try query.whereKey(1);
|
||||
// Find the model with primary key ('foo', 'bar').
|
||||
try compositeQuery.whereKey(.{ .identifier = "foo", .name = "bar" });
|
||||
```
|
||||
|
||||
The provided argument can also be an array of keys.
|
||||
|
||||
```zig
|
||||
// Find models with ID 1 or 3.
|
||||
try query.whereKey(&[_]usize{1, 3});
|
||||
// Find models with primary key ('foo', 'bar') or ('baz', 'test').
|
||||
try compositeQuery.whereKey(&[_]struct{identifier: []const u8, name: []const u8}{
|
||||
.{ .identifier = "foo", .name = "bar" },
|
||||
.{ .identifier = "baz", .name = "test" }
|
||||
});
|
||||
```
|
||||
|
||||
If you just need to get models from their ID without any other condition, repositories provide a `find` function which do just that.
|
||||
|
||||
```zig
|
||||
const models = UserRepository.find(allocator, poolConnector.connector(), 1);
|
||||
```
|
||||
|
||||
#### Array
|
||||
|
||||
Add a `WHERE column IN` condition.
|
||||
|
||||
```zig
|
||||
try query.whereIn(usize, "id", &[_]usize{1, 2});
|
||||
```
|
||||
|
||||
#### Column
|
||||
|
||||
Add a condition between two columns in the query. The columns name all must be comptime-known.
|
||||
|
||||
```zig
|
||||
try query.whereColumn("products.amount", "<", "clients.available_amount");
|
||||
```
|
||||
|
||||
#### Conditions builder
|
||||
|
||||
Sometimes, we need to build complex conditions. For this purpose, we can use the conditions builder.
|
||||
|
||||
The recommended way to initialize a conditions builder is to use the query, as the built conditions will be freed when the query is deinitialized.
|
||||
|
||||
```zig
|
||||
try query.newCondition()
|
||||
```
|
||||
|
||||
We can also directly use the conditions builder with our own allocator.
|
||||
|
||||
```zig
|
||||
try zrm.conditions.Builder.init(allocator);
|
||||
```
|
||||
|
||||
With the conditions builder, we can build complex conditions with AND / OR and different types of tests.
|
||||
|
||||
```zig
|
||||
query.where(
|
||||
try query.newCondition().@"or"(&[_]zrm.RawQuery{
|
||||
try query.newCondition().value(usize, "id", "=", 1),
|
||||
try query.newCondition().@"and"(&[_]zrm.RawQuery{
|
||||
try query.newCondition().in(usize, "id", &[_]usize{100000, 200000, 300000}),
|
||||
try query.newCondition().@"or"(&[_]zrm.RawQuery{
|
||||
try query.newCondition().value(f64, "amount", ">", 12.13),
|
||||
try query.newCondition().value([]const u8, "name", "=", "test"),
|
||||
})
|
||||
}),
|
||||
})
|
||||
);
|
||||
// will produce the following WHERE clause:
|
||||
// WHERE (id = ? OR (id IN (?,?,?) AND (amount > ? OR name = ?)))
|
||||
```
|
||||
|
||||
#### Raw where
|
||||
|
||||
To set a raw `WHERE` clause content, we can use the `where` function.
|
||||
|
||||
```zig
|
||||
query.where(zrm.RawQuery{
|
||||
.sql = "id = ?",
|
||||
.params = &[_]zrm.RawQueryParameter{.{.integer = 1}}
|
||||
});
|
||||
```
|
||||
|
||||
### Joins
|
||||
|
||||
::: warning
|
||||
ZRM currently only supports **raw joins** definitions. Real join definition functions are expected to come in next releases.
|
||||
:::
|
||||
|
||||
To set a raw `JOIN` clause, we can use the `join` function.
|
||||
|
||||
```zig
|
||||
query.join(zrm.RawQuery{
|
||||
.sql = "INNER JOIN foo ON user.id = foo.user_id",
|
||||
.params = &[0]zrm.RawQueryParameter{}
|
||||
});
|
||||
// or
|
||||
query.join(zrm.RawQuery{
|
||||
.sql = "LEFT JOIN foo ON foo.id = ?",
|
||||
.params = &[_]zrm.RawQueryParameter{.{.integer = 1}}
|
||||
});
|
||||
```
|
||||
|
||||
### Selects
|
||||
|
||||
::: danger
|
||||
**Never** put user-sent values as selected columns. This could lead to severe security issues (like [SQL injections](https://en.wikipedia.org/wiki/SQL_injection)).
|
||||
:::
|
||||
|
||||
#### Columns
|
||||
|
||||
We can select specific columns in a query with `selectColumns`. At least one selected column is required.
|
||||
|
||||
```zig
|
||||
try query.selectColumns(&[_][]const u8{"id", "label AS name", "amount"});
|
||||
```
|
||||
|
||||
#### Raw select
|
||||
|
||||
To set a raw `SELECT` clause content, we can use the `select` function.
|
||||
|
||||
```zig
|
||||
query.where(zrm.RawQuery{
|
||||
.sql = "id, label AS name, amount",
|
||||
.params = &[0]zrm.RawQueryParameter{}
|
||||
});
|
||||
```
|
||||
|
||||
## Results
|
||||
|
||||
When our query is fully configured, we can finally call `get` to retrieve the results. We must provide an allocator to hold all the allocated models and their values. The results don't require the query to be kept, so we **can** run `query.deinit()` after getting the results without losing what has been retrieved.
|
||||
|
||||
```zig
|
||||
var result = try query.get(allocator);
|
||||
defer result.deinit();
|
||||
```
|
||||
|
||||
The result structure allows to access the models list or the first model directly, if we just want a single one (or made sure that only one has been retrieved).
|
||||
|
||||
```zig{4,8}
|
||||
var result = try query.get(allocator);
|
||||
defer result.deinit();
|
||||
|
||||
if (result.first()) |model| {
|
||||
// Do something with the first model.
|
||||
}
|
||||
|
||||
for (result.models) |model| {
|
||||
// Do something with all models.
|
||||
}
|
||||
```
|
||||
|
||||
The query builder allows you to get zig models from the database, but you may also need to [store them in database](/docs/insert-update) after creating or altering them.
|
174
docs/docs/relationships.md
Normal file
174
docs/docs/relationships.md
Normal file
|
@ -0,0 +1,174 @@
|
|||
# Relationships
|
||||
|
||||
To define and use relationships, you must have a fully defined [repository](/docs/repositories). In this tutorial, we'll be assuming that we have a defined repository for a user model, as it's defined in [this section](/docs/repositories.html#define-a-repository).
|
||||
|
||||
Executing queries also require to [set up a connection to your database](/docs/databases). We'll also be assuming that we have a working database connector set up, as it's defined in [this section](/docs/database#pool-connector).
|
||||
|
||||
## What is a relationship?
|
||||
|
||||
Before starting to define our relationships, let's try to define what they are. A relationship is a logical connection between models. In real-world applications, models are often connected between each other, and that's why we even made _relational_ databases.
|
||||
|
||||
If we are trying to create an easy model of a chat room, we could have two main entities:
|
||||
|
||||
- the chatters, that we will then call "users".
|
||||
- the messages.
|
||||
|
||||
There is a relationship between a message and a user, because a message is always written by _someone_. In relational databases, we use foreign keys to represent this relationship (we would store a `user_id` in the `messages` table). But in programming languages, we manipulate structures and objects directly, so it can be a pain to perform operations with indexed maps or arrays.
|
||||
|
||||
That's why ORM are now so common in object-oriented languages. They greatly simplify the use of database-stored models, even sometimes completely hiding this fact. Zig structures are sometimes quite similar to objects of object-oriented languages, so simplifying interactions between zig structures and database tables is important.
|
||||
|
||||
## Define relationships
|
||||
|
||||
In ZRM, we define relationships on a repository. The defined relationships are stored in a comptime-known structure, reusable when building a model query.
|
||||
|
||||
```zig
|
||||
const UserRelationships = UserRepository.relationships.define(.{
|
||||
// Here, we can define the relationships of the User model.
|
||||
});
|
||||
```
|
||||
|
||||
The field where the related models will be stored after retrieval is the one with the same name in the relationships structure.
|
||||
|
||||
```zig
|
||||
const UserRelationships = UserRepository.relationships.define(.{
|
||||
// Will put the related model in `relatedModel` field of User structure:
|
||||
.relatedModel = UserRepository.relationships.one(.{...}),
|
||||
// Will put the related models in `relatedModels` field of User structure:
|
||||
.relatedModels = UserRepository.relationships.many(.{...}),
|
||||
});
|
||||
```
|
||||
|
||||
## `one` relationships
|
||||
|
||||
This type of relationship is used when only a single model is related. In our chat example, the relationship type between a message and a user is "one", as there's only one message author.
|
||||
|
||||
### Direct
|
||||
|
||||
The direct one relationship uses a local foreign key to get the related model. In other libraries, this type of relationship can be referred as "belongs to". It has two parameters:
|
||||
|
||||
- **mandatory** `foreignKey`: name of the field / column where the related model key is stored.
|
||||
- _optional_ `modelKey`: name of the key of the related model. When none is provided, the default related model key name is used (it's usually the right choice).
|
||||
|
||||
```zig
|
||||
const MessageRelationships = MessageRepository.relationships.define(.{
|
||||
.user = MessageRepository.relationships.one(UserRepository, .{
|
||||
.direct => .{
|
||||
.foreignKey = "user_id",
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
### Reverse
|
||||
|
||||
The reverse one relationship uses a distant foreign key to get the related model. It can be used to get related models when they hold a foreign key to the origin model. In other libraries, this type of relationship can be referred as "has one". It has two parameters:
|
||||
|
||||
- _optional_ `foreignKey`: name of the field / column where the related model key is stored. When none is provided, the default related model key name is used.
|
||||
- _optional_ `modelKey`: name of the key of the origin model. When none is provided, the default origin model key name is used (it's usually the right choice).
|
||||
|
||||
```zig
|
||||
const UserRelationships = UserRepository.relationships.define(.{
|
||||
.info = UserRepository.relationships.one(UserInfoRepository, .{
|
||||
.reverse = .{
|
||||
.foreignKey = "user_id", // this is optional if "user_id" is the defined primary key of UserInfoRepository.
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
### Through
|
||||
|
||||
The through one relationship uses a pivot table to get the related model. It can be used to get related models when the foreign key is hold by an intermediate table. In other libraries, this type of relationship can be referred as "has one through". It has five parameters:
|
||||
|
||||
- **mandatory** `table`: name of the pivot / intermediate / join table.
|
||||
- _optional_ `foreignKey`: name of the foreign key in the origin table. When none is provided, the default origin model key name is used (it's usually the right choice).
|
||||
- **mandatory** `joinForeignKey`: name of the foreign key in the intermediate table. Its value will match the one in `foreignKey`.
|
||||
- **mandatory** `joinModelKey`: name of the related model key name in the intermediate table. Its value will match the one in `modelKey`.
|
||||
- _optional_ `modelKey`: name of the model key in the related table. When none is provided, the default related model key name is used (it's usually the right choice).
|
||||
|
||||
```zig
|
||||
const MessageRelationships = MessageRepository.relationships.define(.{
|
||||
.user = MessageRepository.relationships.one(UserRepository, .{
|
||||
.direct = .{
|
||||
.foreignKey = "user_id",
|
||||
}
|
||||
}),
|
||||
|
||||
.user_picture = MessageRepository.relationships.one(MediaRepository, .{
|
||||
.through = .{
|
||||
.table = "example_users",
|
||||
.foreignKey = "user_id",
|
||||
.joinForeignKey = "id",
|
||||
.joinModelKey = "picture_id",
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
## `many` relationships
|
||||
|
||||
This type of relationship is used when only a many models are related. In our chat example, the relationship type between a user and messages is "many", as an author can write multiple messages.
|
||||
|
||||
### Direct
|
||||
|
||||
The direct many relationship uses a distant foreign key to get related models. It's often used at the opposite side of a direct one relationship. In other libraries, this type of relationship can be referred as "has many". It has two parameters:
|
||||
|
||||
- **mandatory** `foreignKey`: name of the field / column where the origin model key is stored.
|
||||
- _optional_ `modelKey`: name of the key of the origin model. When none is provided, the default origin model key name is used (it's usually the right choice).
|
||||
|
||||
```zig
|
||||
const UserRelationships = UserRepository.relationships.define(.{
|
||||
.messages = UserRepository.relationships.many(MessageRepository, .{
|
||||
.direct = .{
|
||||
.foreignKey = "user_id",
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
### Through
|
||||
|
||||
The through many relationship uses a pivot table to get the related models. It can be used to get related models when the foreign key is hold by an intermediate table. In other libraries, this type of relationship can be referred as "belongs to many". It has five parameters:
|
||||
|
||||
- **mandatory** `table`: name of the pivot / intermediate / join table.
|
||||
- _optional_ `foreignKey`: name of the foreign key in the origin table. When none is provided, the default origin model key name is used (it's usually the right choice).
|
||||
- **mandatory** `joinForeignKey`: name of the foreign key in the intermediate table. Its value will match the one in `foreignKey`.
|
||||
- **mandatory** `joinModelKey`: name of the related model key name in the intermediate table. Its value will match the one in `modelKey`.
|
||||
- _optional_ `modelKey`: name of the model key in the related table. When none is provided, the default related model key name is used (it's usually the right choice).
|
||||
|
||||
```zig
|
||||
const MessageRelationships = MessageRepository.relationships.define(.{
|
||||
.medias = MessageRepository.relationships.many(MediaRepository, .{
|
||||
.through = .{
|
||||
.table = "example_messages_medias",
|
||||
.joinModelKey = "message_id",
|
||||
.joinForeignKey = "media_id",
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
## Query related models
|
||||
|
||||
Now that our relationships are defined, we can query our models with their relationships directly with the `QueryWith` function of the repository. `QueryWith` takes an array of `Relationship` structures, which are created by `Repository.relationships.define`. To get relationships along with the models, you just need to fill this array with the requested relationships.
|
||||
|
||||
```zig
|
||||
// Initialize a user query, with their messages.
|
||||
var userQuery = UserRepository.QueryWith(
|
||||
// Get messages of retrieved users.
|
||||
&[_]zrm.relationships.Relationship{UserRelationships.messages}
|
||||
).init(std.testing.allocator, poolConnector.connector(), .{});
|
||||
try userQuery.whereKey(1);
|
||||
defer userQuery.deinit();
|
||||
|
||||
// Get the queried user with their messages.
|
||||
var userResult = try userQuery.get(std.testing.allocator);
|
||||
defer userResult.deinit();
|
||||
|
||||
if (userResult.first()) |user| {
|
||||
// The user has been found, showing their messages.
|
||||
for (user.messages.?) |message| {
|
||||
std.debug.print("{s}: {s}", .{user.name, message.text});
|
||||
}
|
||||
}
|
||||
```
|
114
docs/docs/repositories.md
Normal file
114
docs/docs/repositories.md
Normal file
|
@ -0,0 +1,114 @@
|
|||
# Repositories
|
||||
|
||||
The first concept that ZRM introduces is a pretty common one: **repositories**. Repositories are the main interface for you to access what is stored in database, or to store anything in it. It is the _bridge_ between your model (a normal zig structure) and the table in database.
|
||||
|
||||
## Define a model
|
||||
|
||||
There's nothing special to do to define a model to use with ZRM. Let's start with a simple user model.
|
||||
|
||||
```zig
|
||||
const User = struct {
|
||||
id: i32,
|
||||
name: []const u8,
|
||||
};
|
||||
```
|
||||
|
||||
It's a quite simple structure, but you'll quickly add more things when working with it, so let's define another structure that will hold the structure of the user in database.
|
||||
|
||||
```zig
|
||||
const User = struct {
|
||||
pub const Table = struct {
|
||||
id: i32,
|
||||
name: []const u8,
|
||||
};
|
||||
|
||||
id: i32,
|
||||
name: []const u8,
|
||||
};
|
||||
```
|
||||
|
||||
For now, `User` and `User.Table` are the same, but this will change as we add more features to our user.
|
||||
|
||||
## Define a repository
|
||||
|
||||
Now, let's define a repository for our `User` model.
|
||||
|
||||
```zig
|
||||
const UserRepository = zrm.Repository(User, User.Table, .{
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
A repository is mainly based on 2 structures: the model and the table. These are the first two arguments. Next, it's a configuration object, with the following mandatory values:
|
||||
|
||||
- `table`: the table in which the models are stored.
|
||||
- `insertShape`: the inserted columns by default. See [Insert & update](/docs/insert-update#insert) for more info.
|
||||
- `key`: array of fields / columns to use as primary keys.
|
||||
- `fromSql` / `toSql`: functions to convert tables to models and models to tables, which are used when getting and storing data.
|
||||
|
||||
Let's define all these fields:
|
||||
|
||||
```zig
|
||||
const User = struct {
|
||||
pub const Table = struct {
|
||||
id: i32,
|
||||
name: []const u8,
|
||||
|
||||
pub const Insert = struct {
|
||||
name: []const u8,
|
||||
};
|
||||
};
|
||||
|
||||
id: i32,
|
||||
name: []const u8,
|
||||
};
|
||||
|
||||
const UserRepository = zrm.Repository(User, User.Table, .{
|
||||
.table = "example_users",
|
||||
.insertShape = User.Table.Insert,
|
||||
|
||||
.key = &[_][]const u8{"id"},
|
||||
|
||||
.fromSql = userFromSql,
|
||||
.toSql = userToSql,
|
||||
});
|
||||
|
||||
fn userFromSql(table: User.Table) User {
|
||||
return .{
|
||||
.id = table.id,
|
||||
.name = table.name,
|
||||
};
|
||||
}
|
||||
|
||||
fn userToSql(user: User) User.Table {
|
||||
return .{
|
||||
.id = user.id,
|
||||
.name = user.name,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
We created a new structure for `insertShape`: it's the same as `User.Table`, but without the ID, as it will be automatically filled by the database when inserting (assuming that its column is defined as _auto-incrementing on insert_).
|
||||
|
||||
You may see that current implementation of `userFromSql` and `userToSql` is a bit useless. Luckily, ZRM provides a helper function to automatically generate them.
|
||||
|
||||
```zig{7,8}
|
||||
const UserRepository = zrm.Repository(User, User.Table, .{
|
||||
.table = "example_users",
|
||||
.insertShape = User.Table.Insert,
|
||||
|
||||
.key = &[_][]const u8{"id"},
|
||||
|
||||
.fromSql = zrm.helpers.TableModel(User, User.Table).copyTableToModel,
|
||||
.toSql = zrm.helpers.TableModel(User, User.Table).copyModelToTable,
|
||||
});
|
||||
```
|
||||
|
||||
It's finally done! Our repository is fully defined. As you can see we defined the following:
|
||||
|
||||
- where to store the models in database (which table).
|
||||
- what will be inserted.
|
||||
- what are the primary keys of the model.
|
||||
- how to format stored data and how to get them from their stored form.
|
||||
|
||||
These are all the info required by ZRM to know how to deal with your models. We can now have a look to [how to retrieve models from database](/docs/queries).
|
32
docs/index.md
Normal file
32
docs/index.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
# https://vitepress.dev/reference/default-theme-home-page
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: "ZRM"
|
||||
text: "Zig Relational Mapper"
|
||||
tagline: Easy interactions with your database
|
||||
image:
|
||||
light: "/logo.svg"
|
||||
dark: "/logo-dark.svg"
|
||||
alt: "ZRM logo"
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Documentation
|
||||
link: /docs
|
||||
- theme: alt
|
||||
text: API
|
||||
link: https://zedd.zeptotech.net/zrm/api
|
||||
|
||||
features:
|
||||
- icon: '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M216 80c0 26.51-39.4 48-88 48s-88-21.49-88-48s39.4-48 88-48s88 21.49 88 48" opacity="0.2"/><path d="M128 24c-53.83 0-96 24.6-96 56v96c0 31.4 42.17 56 96 56s96-24.6 96-56V80c0-31.4-42.17-56-96-56m80 104c0 9.62-7.88 19.43-21.61 26.92C170.93 163.35 150.19 168 128 168s-42.93-4.65-58.39-13.08C55.88 147.43 48 137.62 48 128v-16.64c17.06 15 46.23 24.64 80 24.64s62.94-9.68 80-24.64ZM69.61 53.08C85.07 44.65 105.81 40 128 40s42.93 4.65 58.39 13.08C200.12 60.57 208 70.38 208 80s-7.88 19.43-21.61 26.92C170.93 115.35 150.19 120 128 120s-42.93-4.65-58.39-13.08C55.88 99.43 48 89.62 48 80s7.88-19.43 21.61-26.92m116.78 149.84C170.93 211.35 150.19 216 128 216s-42.93-4.65-58.39-13.08C55.88 195.43 48 185.62 48 176v-16.64c17.06 15 46.23 24.64 80 24.64s62.94-9.68 80-24.64V176c0 9.62-7.88 19.43-21.61 26.92"/></g></svg>'
|
||||
title: Database models mapper
|
||||
details: Connect zig models to your database.
|
||||
- icon: '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M64 112v32a8 8 0 0 1-8 8H24a8 8 0 0 1-8-8v-32a8 8 0 0 1 8-8h32a8 8 0 0 1 8 8m144-72h-48a8 8 0 0 0-8 8v48a8 8 0 0 0 8 8h48a8 8 0 0 0 8-8V48a8 8 0 0 0-8-8m0 112h-48a8 8 0 0 0-8 8v48a8 8 0 0 0 8 8h48a8 8 0 0 0 8-8v-48a8 8 0 0 0-8-8" opacity="0.2"/><path d="M160 112h48a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16h-48a16 16 0 0 0-16 16v16h-16a24 24 0 0 0-24 24v32H72v-8a16 16 0 0 0-16-16H24a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h32a16 16 0 0 0 16-16v-8h32v32a24 24 0 0 0 24 24h16v16a16 16 0 0 0 16 16h48a16 16 0 0 0 16-16v-48a16 16 0 0 0-16-16h-48a16 16 0 0 0-16 16v16h-16a8 8 0 0 1-8-8V88a8 8 0 0 1 8-8h16v16a16 16 0 0 0 16 16M56 144H24v-32h32zm104 16h48v48h-48Zm0-112h48v48h-48Z"/></g></svg>'
|
||||
title: Models relationships definition
|
||||
details: Get related models in a single well-defined structure.
|
||||
- icon: '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="m96 240l16-80l-64-24L160 16l-16 80l64 24Z" opacity="0.2"/><path d="M215.79 118.17a8 8 0 0 0-5-5.66L153.18 90.9l14.66-73.33a8 8 0 0 0-13.69-7l-112 120a8 8 0 0 0 3 13l57.63 21.61l-14.62 73.25a8 8 0 0 0 13.69 7l112-120a8 8 0 0 0 1.94-7.26M109.37 214l10.47-52.38a8 8 0 0 0-5-9.06L62 132.71l84.62-90.66l-10.46 52.38a8 8 0 0 0 5 9.06l52.8 19.8Z"/></g></svg>'
|
||||
title: Fast and easy queries
|
||||
details: Easily get and store model data to your database from zig code.
|
||||
---
|
||||
|
1
docs/logo-dark.svg
Normal file
1
docs/logo-dark.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="512" height="512" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" fill="white"><g style="display:inline;fill-opacity:1"><path d="M20 18c0 2.2091-3.5817 4-8 4-4.41828 0-8-1.7909-8-4v-4.026c.50221.6166 1.21495 1.1289 2.00774 1.5252C7.58004 16.2854 9.69967 16.75 12 16.75c2.3003 0 4.42-.4646 5.9923-1.2508.7928-.3963 1.5055-.9086 2.0077-1.5252Z" style="fill-opacity:1" transform="translate(-22.35483335 -22.47245115) scale(23.20604)"/><path d="M12 10.75c2.3003 0 4.42-.4646 5.9923-1.25075.7928-.3964 1.5055-.90866 2.0077-1.52528V12c0 .5-1.7857 1.5911-2.6786 2.1576C15.9983 14.8192 14.118 15.25 12 15.25c-2.11795 0-3.99832-.4308-5.32144-1.0924C5.5 13.5683 4 12.5 4 12V7.97397c.50221.61662 1.21495 1.12888 2.00774 1.52528C7.58004 10.2854 9.69967 10.75 12 10.75Z" style="fill-opacity:1" transform="translate(-22.35483335 -22.47245115) scale(23.20604)"/><path d="M17.3214 8.15761C15.9983 8.81917 14.118 9.25 12 9.25c-2.11795 0-3.99832-.43083-5.32144-1.09239-.51472-.20165-1.67219-.84269-2.47706-1.87826-.13696-.17622-.19574-.40082-.16162-.62137.02295-.14829.05492-.30103.0959-.39572C4.82815 3.40554 8.0858 2 12 2c3.9142 0 7.1718 1.40554 7.8642 3.26226.041.09469.073.24743.0959.39572.0341.22055-.0246.44515-.1616.62137-.8049 1.03557-1.9623 1.67661-2.4771 1.87826z" style="fill-opacity:1" transform="translate(-22.35483335 -22.47245115) scale(23.20604)"/></g><path style="display:inline;fill-opacity:1;stroke-width:23.5293" d="M276.04706 168.09412-.000001 474.65883l150.164711-43.03528 30.1891-25.96241L512 37.341172 362.30588 80.258809Z"/></svg>
|
After Width: | Height: | Size: 1.5 KiB |
1
docs/logo.svg
Normal file
1
docs/logo.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="512" height="512" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g style="display:inline;fill-opacity:1"><path d="M20 18c0 2.2091-3.5817 4-8 4-4.41828 0-8-1.7909-8-4v-4.026c.50221.6166 1.21495 1.1289 2.00774 1.5252C7.58004 16.2854 9.69967 16.75 12 16.75c2.3003 0 4.42-.4646 5.9923-1.2508.7928-.3963 1.5055-.9086 2.0077-1.5252Z" style="fill-opacity:1" transform="translate(-22.35483335 -22.47245115) scale(23.20604)"/><path d="M12 10.75c2.3003 0 4.42-.4646 5.9923-1.25075.7928-.3964 1.5055-.90866 2.0077-1.52528V12c0 .5-1.7857 1.5911-2.6786 2.1576C15.9983 14.8192 14.118 15.25 12 15.25c-2.11795 0-3.99832-.4308-5.32144-1.0924C5.5 13.5683 4 12.5 4 12V7.97397c.50221.61662 1.21495 1.12888 2.00774 1.52528C7.58004 10.2854 9.69967 10.75 12 10.75Z" style="fill-opacity:1" transform="translate(-22.35483335 -22.47245115) scale(23.20604)"/><path d="M17.3214 8.15761C15.9983 8.81917 14.118 9.25 12 9.25c-2.11795 0-3.99832-.43083-5.32144-1.09239-.51472-.20165-1.67219-.84269-2.47706-1.87826-.13696-.17622-.19574-.40082-.16162-.62137.02295-.14829.05492-.30103.0959-.39572C4.82815 3.40554 8.0858 2 12 2c3.9142 0 7.1718 1.40554 7.8642 3.26226.041.09469.073.24743.0959.39572.0341.22055-.0246.44515-.1616.62137-.8049 1.03557-1.9623 1.67661-2.4771 1.87826z" style="fill-opacity:1" transform="translate(-22.35483335 -22.47245115) scale(23.20604)"/></g><path style="display:inline;fill-opacity:1;stroke-width:23.5293" d="M276.04706 168.09412-.000001 474.65883l150.164711-43.03528 30.1891-25.96241L512 37.341172 362.30588 80.258809Z"/></svg>
|
After Width: | Height: | Size: 1.5 KiB |
12
docs/package.json
Normal file
12
docs/package.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "zrm-docs",
|
||||
"packageManager": "yarn@4.5.2",
|
||||
"scripts": {
|
||||
"docs:dev": "vitepress dev",
|
||||
"docs:build": "vitepress build",
|
||||
"docs:preview": "vitepress preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitepress": "^1.5.0"
|
||||
}
|
||||
}
|
2544
docs/yarn.lock
Normal file
2544
docs/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
69
docs/zeptotech.svg
Normal file
69
docs/zeptotech.svg
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
|
||||
<svg
|
||||
width="720.14001"
|
||||
height="720.14001"
|
||||
viewBox="0 0 190.53705 190.53705"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1">
|
||||
<linearGradient
|
||||
id="linearGradient3">
|
||||
<stop
|
||||
style="stop-color:#22aeff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop1" />
|
||||
<stop
|
||||
style="stop-color:#00f1cb;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop2" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
xlink:href="#linearGradient3"
|
||||
id="linearGradient2"
|
||||
x1="25.052734"
|
||||
y1="160.9082"
|
||||
x2="167.30664"
|
||||
y2="12.158203"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
xlink:href="#linearGradient3"
|
||||
id="linearGradient6"
|
||||
x1="215.58984"
|
||||
y1="163.26172"
|
||||
x2="42.798828"
|
||||
y2="63.660156"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
xlink:href="#linearGradient3"
|
||||
id="linearGradient12"
|
||||
x1="91.082031"
|
||||
y1="73.657227"
|
||||
x2="215.58984"
|
||||
y2="73.657227"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<g
|
||||
id="layer1"
|
||||
transform="translate(-25.052762,8.7353212)">
|
||||
<g
|
||||
id="g1">
|
||||
<path
|
||||
id="path3-3"
|
||||
style="display:inline;fill:url(#linearGradient12);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 176.85742,117.05273 H 103.875 L 91.082031,137.50977 H 215.58984 L 135.76758,9.8046875 h -25.93946 z" />
|
||||
<path
|
||||
id="path4"
|
||||
style="display:inline;fill:url(#linearGradient6);stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 42.798828,163.26172 H 202.79688 l 12.79296,-20.45899 H 81.53125 L 118.02148,84.412109 105.05273,63.660156 Z" />
|
||||
<path
|
||||
id="path3"
|
||||
style="display:inline;fill:url(#linearGradient2);stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 25.052734,140.15625 38.023438,160.9082 105.05273,53.669922 141.36719,111.75977 h 25.93945 L 105.05273,12.158203 Z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
36
logo.svg
36
logo.svg
|
@ -1,35 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="512"
|
||||
height="512"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><g
|
||||
id="layer1"><g
|
||||
style="display:inline;fill:#000000;fill-opacity:1"
|
||||
id="g19"
|
||||
transform="matrix(2.0189327,0,0,2.0189327,249.8064,248.00901)"><path
|
||||
d="m 20,18 c 0,2.2091 -3.5817,4 -8,4 -4.41828,0 -8,-1.7909 -8,-4 v -4.026 c 0.50221,0.6166 1.21495,1.1289 2.00774,1.5252 C 7.58004,16.2854 9.69967,16.75 12,16.75 c 2.3003,0 4.42,-0.4646 5.9923,-1.2508 C 18.7851,15.1029 19.4978,14.5906 20,13.974 Z"
|
||||
fill="#1c274c"
|
||||
id="path17"
|
||||
style="fill:#000000;fill-opacity:1"
|
||||
transform="matrix(11.494213,0,0,11.494213,-134.80451,-133.9725)" /><path
|
||||
d="m 12,10.75 c 2.3003,0 4.42,-0.4646 5.9923,-1.25075 C 18.7851,9.10285 19.4978,8.59059 20,7.97397 V 12 c 0,0.5 -1.7857,1.5911 -2.6786,2.1576 C 15.9983,14.8192 14.118,15.25 12,15.25 9.88205,15.25 8.00168,14.8192 6.67856,14.1576 5.5,13.5683 4,12.5 4,12 V 7.97397 C 4.50221,8.59059 5.21495,9.10285 6.00774,9.49925 7.58004,10.2854 9.69967,10.75 12,10.75 Z"
|
||||
fill="#1c274c"
|
||||
id="path18"
|
||||
style="fill:#000000;fill-opacity:1"
|
||||
transform="matrix(11.494213,0,0,11.494213,-134.80451,-133.9725)" /><path
|
||||
d="M 17.3214,8.15761 C 15.9983,8.81917 14.118,9.25 12,9.25 9.88205,9.25 8.00168,8.81917 6.67856,8.15761 6.16384,7.95596 5.00637,7.31492 4.2015,6.27935 4.06454,6.10313 4.00576,5.87853 4.03988,5.65798 4.06283,5.50969 4.0948,5.35695 4.13578,5.26226 4.82815,3.40554 8.0858,2 12,2 c 3.9142,0 7.1718,1.40554 7.8642,3.26226 0.041,0.09469 0.073,0.24743 0.0959,0.39572 0.0341,0.22055 -0.0246,0.44515 -0.1616,0.62137 -0.8049,1.03557 -1.9623,1.67661 -2.4771,1.87826 z"
|
||||
fill="#1c274c"
|
||||
id="path19"
|
||||
style="fill:#000000;fill-opacity:1"
|
||||
transform="matrix(11.494213,0,0,11.494213,-134.80451,-133.9725)" /></g><path
|
||||
id="polygon3"
|
||||
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:23.5293"
|
||||
d="M 276.04706,168.09412 -10e-7,474.65883 150.16471,431.62355 180.35381,405.66114 512,37.341172 362.30588,80.258809 Z" /></g></svg>
|
||||
<svg width="512" height="512" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g style="display:inline;fill-opacity:1"><path d="M20 18c0 2.2091-3.5817 4-8 4-4.41828 0-8-1.7909-8-4v-4.026c.50221.6166 1.21495 1.1289 2.00774 1.5252C7.58004 16.2854 9.69967 16.75 12 16.75c2.3003 0 4.42-.4646 5.9923-1.2508.7928-.3963 1.5055-.9086 2.0077-1.5252Z" style="fill-opacity:1" transform="translate(-22.35483335 -22.47245115) scale(23.20604)"/><path d="M12 10.75c2.3003 0 4.42-.4646 5.9923-1.25075.7928-.3964 1.5055-.90866 2.0077-1.52528V12c0 .5-1.7857 1.5911-2.6786 2.1576C15.9983 14.8192 14.118 15.25 12 15.25c-2.11795 0-3.99832-.4308-5.32144-1.0924C5.5 13.5683 4 12.5 4 12V7.97397c.50221.61662 1.21495 1.12888 2.00774 1.52528C7.58004 10.2854 9.69967 10.75 12 10.75Z" style="fill-opacity:1" transform="translate(-22.35483335 -22.47245115) scale(23.20604)"/><path d="M17.3214 8.15761C15.9983 8.81917 14.118 9.25 12 9.25c-2.11795 0-3.99832-.43083-5.32144-1.09239-.51472-.20165-1.67219-.84269-2.47706-1.87826-.13696-.17622-.19574-.40082-.16162-.62137.02295-.14829.05492-.30103.0959-.39572C4.82815 3.40554 8.0858 2 12 2c3.9142 0 7.1718 1.40554 7.8642 3.26226.041.09469.073.24743.0959.39572.0341.22055-.0246.44515-.1616.62137-.8049 1.03557-1.9623 1.67661-2.4771 1.87826z" style="fill-opacity:1" transform="translate(-22.35483335 -22.47245115) scale(23.20604)"/></g><path style="display:inline;fill-opacity:1;stroke-width:23.5293" d="M276.04706 168.09412-.000001 474.65883l150.164711-43.03528 30.1891-25.96241L512 37.341172 362.30588 80.258809Z"/></svg>
|
||||
|
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.5 KiB |
Loading…
Reference in a new issue