Initial commit
This commit is contained in:
91
Dockerfile
Normal file
91
Dockerfile
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# ================================
|
||||||
|
# Build image
|
||||||
|
# ================================
|
||||||
|
FROM swift:6.1-noble AS build
|
||||||
|
|
||||||
|
# Install OS updates
|
||||||
|
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
|
||||||
|
&& apt-get -q update \
|
||||||
|
&& apt-get -q dist-upgrade -y \
|
||||||
|
&& apt-get install -y libjemalloc-dev
|
||||||
|
|
||||||
|
# Set up a build area
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# First just resolve dependencies.
|
||||||
|
# This creates a cached layer that can be reused
|
||||||
|
# as long as your Package.swift/Package.resolved
|
||||||
|
# files do not change.
|
||||||
|
COPY ./Package.* ./
|
||||||
|
RUN swift package resolve \
|
||||||
|
$([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true)
|
||||||
|
|
||||||
|
# Copy entire repo into container
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN mkdir /staging
|
||||||
|
|
||||||
|
# Build the application, with optimizations, with static linking, and using jemalloc
|
||||||
|
# N.B.: The static version of jemalloc is incompatible with the static Swift runtime.
|
||||||
|
RUN --mount=type=cache,target=/build/.build \
|
||||||
|
swift build -c release \
|
||||||
|
--product ExodaiAcademy \
|
||||||
|
--static-swift-stdlib \
|
||||||
|
-Xlinker -ljemalloc && \
|
||||||
|
# Copy main executable to staging area
|
||||||
|
cp "$(swift build -c release --show-bin-path)/ExodaiAcademy" /staging && \
|
||||||
|
# Copy resources bundled by SPM to staging area
|
||||||
|
find -L "$(swift build -c release --show-bin-path)" -regex '.*\.resources$' -exec cp -Ra {} /staging \;
|
||||||
|
|
||||||
|
|
||||||
|
# Switch to the staging area
|
||||||
|
WORKDIR /staging
|
||||||
|
|
||||||
|
# Copy static swift backtracer binary to staging area
|
||||||
|
RUN cp "/usr/libexec/swift/linux/swift-backtrace-static" ./
|
||||||
|
|
||||||
|
# Copy any resources from the public directory and views directory if the directories exist
|
||||||
|
# Ensure that by default, neither the directory nor any of its contents are writable.
|
||||||
|
RUN [ -d /build/Public ] && { mv /build/Public ./Public && chmod -R a-w ./Public; } || true
|
||||||
|
RUN [ -d /build/Resources ] && { mv /build/Resources ./Resources && chmod -R a-w ./Resources; } || true
|
||||||
|
|
||||||
|
# ================================
|
||||||
|
# Run image
|
||||||
|
# ================================
|
||||||
|
FROM ubuntu:noble
|
||||||
|
|
||||||
|
# Make sure all system packages are up to date, and install only essential packages.
|
||||||
|
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
|
||||||
|
&& apt-get -q update \
|
||||||
|
&& apt-get -q dist-upgrade -y \
|
||||||
|
&& apt-get -q install -y \
|
||||||
|
libjemalloc2 \
|
||||||
|
ca-certificates \
|
||||||
|
tzdata \
|
||||||
|
# If your app or its dependencies import FoundationNetworking, also install `libcurl4`.
|
||||||
|
# libcurl4 \
|
||||||
|
# If your app or its dependencies import FoundationXML, also install `libxml2`.
|
||||||
|
# libxml2 \
|
||||||
|
&& rm -r /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Create a vapor user and group with /app as its home directory
|
||||||
|
RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app vapor
|
||||||
|
|
||||||
|
# Switch to the new home directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy built executable and any staged resources from builder
|
||||||
|
COPY --from=build --chown=vapor:vapor /staging /app
|
||||||
|
|
||||||
|
# Provide configuration needed by the built-in crash reporter and some sensible default behaviors.
|
||||||
|
ENV SWIFT_BACKTRACE=enable=yes,sanitize=yes,threads=all,images=all,interactive=no,swift-backtrace=./swift-backtrace-static
|
||||||
|
|
||||||
|
# Ensure all further commands run as the vapor user
|
||||||
|
USER vapor:vapor
|
||||||
|
|
||||||
|
# Let Docker bind to port 8080
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Start the Vapor service when the image is run, default to listening on 8080 in production environment
|
||||||
|
ENTRYPOINT ["./ExodaiAcademy"]
|
||||||
|
CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]
|
||||||
312
Package.resolved
Normal file
312
Package.resolved
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
{
|
||||||
|
"originHash" : "613dcdfdad088d6787a90d1011bb49c2f514c20f9c4e6f0a6eb7454e985f996d",
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "async-http-client",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/swift-server/async-http-client.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "4b99975677236d13f0754339864e5360142ff5a1",
|
||||||
|
"version" : "1.30.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "async-kit",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/vapor/async-kit.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "6f3615ccf2ac3c2ae0c8087d527546e9544a43dd",
|
||||||
|
"version" : "1.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "console-kit",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/vapor/console-kit.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "742f624a998cba2a9e653d9b1e91ad3f3a5dff6b",
|
||||||
|
"version" : "4.15.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "fluent",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/vapor/fluent.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "2fe9e36daf4bdb5edcf193e0d0806ba2074d2864",
|
||||||
|
"version" : "4.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "fluent-kit",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/vapor/fluent-kit.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "06dadea2c17b3fa4e671f4a3cdc0840e970939b3",
|
||||||
|
"version" : "1.54.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "fluent-postgres-driver",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/vapor/fluent-postgres-driver.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "59bff45a41d1ece1950bb8a6e0006d88c1fb6e69",
|
||||||
|
"version" : "2.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "multipart-kit",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/vapor/multipart-kit.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "3498e60218e6003894ff95192d756e238c01f44e",
|
||||||
|
"version" : "4.7.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "postgres-kit",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/vapor/postgres-kit.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "7c079553e9cda74811e627775bf22e40a9405ad9",
|
||||||
|
"version" : "2.15.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "postgres-nio",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/vapor/postgres-nio.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "d578b86fb2c8321b114d97cd70831d1a3e9531a6",
|
||||||
|
"version" : "1.30.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "routing-kit",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/vapor/routing-kit.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "1a10ccea61e4248effd23b6e814999ce7bdf0ee0",
|
||||||
|
"version" : "4.9.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sql-kit",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/vapor/sql-kit.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "c0ea243ffeb8b5ff9e20a281e44003c6abb8896f",
|
||||||
|
"version" : "3.34.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-algorithms",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-algorithms.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "87e50f483c54e6efd60e885f7f5aa946cee68023",
|
||||||
|
"version" : "1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-asn1",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-asn1.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "810496cf121e525d660cd0ea89a758740476b85f",
|
||||||
|
"version" : "1.5.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-async-algorithms",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-async-algorithms.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "6c050d5ef8e1aa6342528460db614e9770d7f804",
|
||||||
|
"version" : "1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-atomics",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-atomics.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7",
|
||||||
|
"version" : "1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-certificates",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-certificates.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "7d5f6124c91a2d06fb63a811695a3400d15a100e",
|
||||||
|
"version" : "1.17.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-collections",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-collections.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e",
|
||||||
|
"version" : "1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-crypto",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-crypto.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "6f70fa9eab24c1fd982af18c281c4525d05e3095",
|
||||||
|
"version" : "4.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-distributed-tracing",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-distributed-tracing.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "baa932c1336f7894145cbaafcd34ce2dd0b77c97",
|
||||||
|
"version" : "1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-http-structured-headers",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-http-structured-headers.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "76d7627bd88b47bf5a0f8497dd244885960dde0b",
|
||||||
|
"version" : "1.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-http-types",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-http-types.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "45eb0224913ea070ec4fba17291b9e7ecf4749ca",
|
||||||
|
"version" : "1.5.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-log",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-log.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "2778fd4e5a12a8aaa30a3ee8285f4ce54c5f3181",
|
||||||
|
"version" : "1.9.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-metrics",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-metrics.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "0743a9364382629da3bf5677b46a2c4b1ce5d2a6",
|
||||||
|
"version" : "2.7.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-nio",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-nio.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "4a9a97111099376854a7f8f0f9f88b9d61f52eff",
|
||||||
|
"version" : "2.92.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-nio-extras",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-nio-extras.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "f4ea23b6c1c545a6656dbcac8c82a2864075766e",
|
||||||
|
"version" : "1.31.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-nio-http2",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-nio-http2.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "c2ba4cfbb83f307c66f5a6df6bb43e3c88dfbf80",
|
||||||
|
"version" : "1.39.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-nio-ssl",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-nio-ssl.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "173cc69a058623525a58ae6710e2f5727c663793",
|
||||||
|
"version" : "2.36.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-nio-transport-services",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-nio-transport-services.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "60c3e187154421171721c1a38e800b390680fb5d",
|
||||||
|
"version" : "1.26.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-numerics",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-numerics.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "0c0290ff6b24942dadb83a929ffaaa1481df04a2",
|
||||||
|
"version" : "1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-service-context",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-service-context.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "1983448fefc717a2bc2ebde5490fe99873c5b8a6",
|
||||||
|
"version" : "1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-service-lifecycle",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/swift-server/swift-service-lifecycle.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "1de37290c0ab3c5a96028e0f02911b672fd42348",
|
||||||
|
"version" : "2.9.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-system",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-system.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "7c6ad0fc39d0763e0b699210e4124afd5041c5df",
|
||||||
|
"version" : "1.6.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "vapor",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/vapor/vapor.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "6f3db7122ccffb28e11e121c3797a176fcb88796",
|
||||||
|
"version" : "4.121.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "websocket-kit",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/vapor/websocket-kit.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "8666c92dbbb3c8eefc8008c9c8dcf50bfd302167",
|
||||||
|
"version" : "2.16.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 3
|
||||||
|
}
|
||||||
44
Package.swift
Normal file
44
Package.swift
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// swift-tools-version:6.0
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "ExodaiAcademy",
|
||||||
|
platforms: [
|
||||||
|
.macOS(.v13)
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
// 💧 A server-side Swift web framework.
|
||||||
|
.package(url: "https://github.com/vapor/vapor.git", from: "4.115.0"),
|
||||||
|
// 🗄 An ORM for SQL and NoSQL databases.
|
||||||
|
.package(url: "https://github.com/vapor/fluent.git", from: "4.9.0"),
|
||||||
|
// 🐘 Fluent driver for Postgres.
|
||||||
|
.package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.8.0"),
|
||||||
|
// 🔵 Non-blocking, event-driven networking for Swift. Used for custom executors
|
||||||
|
.package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
.executableTarget(
|
||||||
|
name: "ExodaiAcademy",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "Fluent", package: "fluent"),
|
||||||
|
.product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
|
||||||
|
.product(name: "Vapor", package: "vapor"),
|
||||||
|
.product(name: "NIOCore", package: "swift-nio"),
|
||||||
|
.product(name: "NIOPosix", package: "swift-nio"),
|
||||||
|
],
|
||||||
|
swiftSettings: swiftSettings
|
||||||
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "ExodaiAcademyTests",
|
||||||
|
dependencies: [
|
||||||
|
.target(name: "ExodaiAcademy"),
|
||||||
|
.product(name: "VaporTesting", package: "vapor"),
|
||||||
|
],
|
||||||
|
swiftSettings: swiftSettings
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
var swiftSettings: [SwiftSetting] { [
|
||||||
|
.enableUpcomingFeature("ExistentialAny"),
|
||||||
|
] }
|
||||||
0
Public/.gitkeep
Normal file
0
Public/.gitkeep
Normal file
27
README.md
Normal file
27
README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# ExodaiAcademy
|
||||||
|
|
||||||
|
💧 A project built with the Vapor web framework.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
To build the project using the Swift Package Manager, run the following command in the terminal from the root of the project:
|
||||||
|
```bash
|
||||||
|
swift build
|
||||||
|
```
|
||||||
|
|
||||||
|
To run the project and start the server, use the following command:
|
||||||
|
```bash
|
||||||
|
swift run
|
||||||
|
```
|
||||||
|
|
||||||
|
To execute tests, use the following command:
|
||||||
|
```bash
|
||||||
|
swift test
|
||||||
|
```
|
||||||
|
|
||||||
|
### See more
|
||||||
|
|
||||||
|
- [Vapor Website](https://vapor.codes)
|
||||||
|
- [Vapor Documentation](https://docs.vapor.codes)
|
||||||
|
- [Vapor GitHub](https://github.com/vapor)
|
||||||
|
- [Vapor Community](https://github.com/vapor-community)
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
//
|
||||||
|
// Countries.swift
|
||||||
|
// ExodaiAcademy
|
||||||
|
//
|
||||||
|
// Created by Exodai on 22/01/2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Vapor
|
||||||
|
|
||||||
|
enum CountryCode: String, Codable, CaseIterable {
|
||||||
|
case af = "AF" // Afghanistan
|
||||||
|
case ax = "AX" // Åland Islands
|
||||||
|
case al = "AL" // Albania
|
||||||
|
case dz = "DZ" // Algeria
|
||||||
|
case aS = "AS" // American Samoa
|
||||||
|
case ad = "AD" // Andorra
|
||||||
|
case ao = "AO" // Angola
|
||||||
|
case ai = "AI" // Anguilla
|
||||||
|
case aq = "AQ" // Antarctica
|
||||||
|
case ag = "AG" // Antigua and Barbuda
|
||||||
|
case ar = "AR" // Argentina
|
||||||
|
case am = "AM" // Armenia
|
||||||
|
case aw = "AW" // Aruba
|
||||||
|
case au = "AU" // Australia
|
||||||
|
case at = "AT" // Austria
|
||||||
|
case az = "AZ" // Azerbaijan
|
||||||
|
case bs = "BS" // Bahamas
|
||||||
|
case bh = "BH" // Bahrain
|
||||||
|
case bd = "BD" // Bangladesh
|
||||||
|
case bb = "BB" // Barbados
|
||||||
|
case by = "BY" // Belarus
|
||||||
|
case be = "BE" // Belgium
|
||||||
|
case bz = "BZ" // Belize
|
||||||
|
case bj = "BJ" // Benin
|
||||||
|
case bm = "BM" // Bermuda
|
||||||
|
case bt = "BT" // Bhutan
|
||||||
|
case bo = "BO" // Bolivia
|
||||||
|
case bq = "BQ" // Bonaire, Sint Eustatius and Saba
|
||||||
|
case ba = "BA" // Bosnia and Herzegovina
|
||||||
|
case bw = "BW" // Botswana
|
||||||
|
case bv = "BV" // Bouvet Island
|
||||||
|
case br = "BR" // Brazil
|
||||||
|
case io = "IO" // British Indian Ocean Territory
|
||||||
|
case bn = "BN" // Brunei
|
||||||
|
case bg = "BG" // Bulgaria
|
||||||
|
case bf = "BF" // Burkina Faso
|
||||||
|
case bi = "BI" // Burundi
|
||||||
|
case kh = "KH" // Cambodia
|
||||||
|
case cm = "CM" // Cameroon
|
||||||
|
case ca = "CA" // Canada
|
||||||
|
case cv = "CV" // Cape Verde
|
||||||
|
case ky = "KY" // Cayman Islands
|
||||||
|
case cf = "CF" // Central African Republic
|
||||||
|
case td = "TD" // Chad
|
||||||
|
case cl = "CL" // Chile
|
||||||
|
case cn = "CN" // China
|
||||||
|
case cx = "CX" // Christmas Island
|
||||||
|
case cc = "CC" // Cocos (Keeling) Islands
|
||||||
|
case co = "CO" // Colombia
|
||||||
|
case km = "KM" // Comoros
|
||||||
|
case cg = "CG" // Congo
|
||||||
|
case cd = "CD" // Congo (DRC)
|
||||||
|
case ck = "CK" // Cook Islands
|
||||||
|
case cr = "CR" // Costa Rica
|
||||||
|
case ci = "CI" // Côte d’Ivoire
|
||||||
|
case hr = "HR" // Croatia
|
||||||
|
case cu = "CU" // Cuba
|
||||||
|
case cw = "CW" // Curaçao
|
||||||
|
case cy = "CY" // Cyprus
|
||||||
|
case cz = "CZ" // Czech Republic
|
||||||
|
case dk = "DK" // Denmark
|
||||||
|
case dj = "DJ" // Djibouti
|
||||||
|
case dm = "DM" // Dominica
|
||||||
|
case dO = "DO" // Dominican Republic
|
||||||
|
case ec = "EC" // Ecuador
|
||||||
|
case eg = "EG" // Egypt
|
||||||
|
case sv = "SV" // El Salvador
|
||||||
|
case gq = "GQ" // Equatorial Guinea
|
||||||
|
case er = "ER" // Eritrea
|
||||||
|
case ee = "EE" // Estonia
|
||||||
|
case sz = "SZ" // Eswatini
|
||||||
|
case et = "ET" // Ethiopia
|
||||||
|
case fk = "FK" // Falkland Islands
|
||||||
|
case fo = "FO" // Faroe Islands
|
||||||
|
case fj = "FJ" // Fiji
|
||||||
|
case fi = "FI" // Finland
|
||||||
|
case fr = "FR" // France
|
||||||
|
case gf = "GF" // French Guiana
|
||||||
|
case pf = "PF" // French Polynesia
|
||||||
|
case tf = "TF" // French Southern Territories
|
||||||
|
case ga = "GA" // Gabon
|
||||||
|
case gm = "GM" // Gambia
|
||||||
|
case ge = "GE" // Georgia
|
||||||
|
case de = "DE" // Germany
|
||||||
|
case gh = "GH" // Ghana
|
||||||
|
case gi = "GI" // Gibraltar
|
||||||
|
case gr = "GR" // Greece
|
||||||
|
case gl = "GL" // Greenland
|
||||||
|
case gd = "GD" // Grenada
|
||||||
|
case gp = "GP" // Guadeloupe
|
||||||
|
case gu = "GU" // Guam
|
||||||
|
case gt = "GT" // Guatemala
|
||||||
|
case gg = "GG" // Guernsey
|
||||||
|
case gn = "GN" // Guinea
|
||||||
|
case gw = "GW" // Guinea-Bissau
|
||||||
|
case gy = "GY" // Guyana
|
||||||
|
case ht = "HT" // Haiti
|
||||||
|
case hm = "HM" // Heard Island and McDonald Islands
|
||||||
|
case va = "VA" // Vatican City
|
||||||
|
case hn = "HN" // Honduras
|
||||||
|
case hk = "HK" // Hong Kong
|
||||||
|
case hu = "HU" // Hungary
|
||||||
|
case iS = "IS" // Iceland
|
||||||
|
case iN = "IN" // India
|
||||||
|
case id = "ID" // Indonesia
|
||||||
|
case ir = "IR" // Iran
|
||||||
|
case iq = "IQ" // Iraq
|
||||||
|
case ie = "IE" // Ireland
|
||||||
|
case im = "IM" // Isle of Man
|
||||||
|
case il = "IL" // Israel
|
||||||
|
case it = "IT" // Italy
|
||||||
|
case jm = "JM" // Jamaica
|
||||||
|
case jp = "JP" // Japan
|
||||||
|
case je = "JE" // Jersey
|
||||||
|
case jo = "JO" // Jordan
|
||||||
|
case kz = "KZ" // Kazakhstan
|
||||||
|
case ke = "KE" // Kenya
|
||||||
|
case ki = "KI" // Kiribati
|
||||||
|
case kp = "KP" // North Korea
|
||||||
|
case kr = "KR" // South Korea
|
||||||
|
case kw = "KW" // Kuwait
|
||||||
|
case kg = "KG" // Kyrgyzstan
|
||||||
|
case la = "LA" // Laos
|
||||||
|
case lv = "LV" // Latvia
|
||||||
|
case lb = "LB" // Lebanon
|
||||||
|
case ls = "LS" // Lesotho
|
||||||
|
case lr = "LR" // Liberia
|
||||||
|
case ly = "LY" // Libya
|
||||||
|
case li = "LI" // Liechtenstein
|
||||||
|
case lt = "LT" // Lithuania
|
||||||
|
case lu = "LU" // Luxembourg
|
||||||
|
case mo = "MO" // Macao
|
||||||
|
case mg = "MG" // Madagascar
|
||||||
|
case mw = "MW" // Malawi
|
||||||
|
case my = "MY" // Malaysia
|
||||||
|
case mv = "MV" // Maldives
|
||||||
|
case ml = "ML" // Mali
|
||||||
|
case mt = "MT" // Malta
|
||||||
|
case mh = "MH" // Marshall Islands
|
||||||
|
case mq = "MQ" // Martinique
|
||||||
|
case mr = "MR" // Mauritania
|
||||||
|
case mu = "MU" // Mauritius
|
||||||
|
case yt = "YT" // Mayotte
|
||||||
|
case mx = "MX" // Mexico
|
||||||
|
case fm = "FM" // Micronesia
|
||||||
|
case md = "MD" // Moldova
|
||||||
|
case mc = "MC" // Monaco
|
||||||
|
case mn = "MN" // Mongolia
|
||||||
|
case me = "ME" // Montenegro
|
||||||
|
case ms = "MS" // Montserrat
|
||||||
|
case ma = "MA" // Morocco
|
||||||
|
case mz = "MZ" // Mozambique
|
||||||
|
case mm = "MM" // Myanmar
|
||||||
|
case na = "NA" // Namibia
|
||||||
|
case nr = "NR" // Nauru
|
||||||
|
case np = "NP" // Nepal
|
||||||
|
case nl = "NL" // Netherlands
|
||||||
|
case nc = "NC" // New Caledonia
|
||||||
|
case nz = "NZ" // New Zealand
|
||||||
|
case ni = "NI" // Nicaragua
|
||||||
|
case ne = "NE" // Niger
|
||||||
|
case ng = "NG" // Nigeria
|
||||||
|
case nu = "NU" // Niue
|
||||||
|
case nf = "NF" // Norfolk Island
|
||||||
|
case mk = "MK" // North Macedonia
|
||||||
|
case mp = "MP" // Northern Mariana Islands
|
||||||
|
case no = "NO" // Norway
|
||||||
|
case om = "OM" // Oman
|
||||||
|
case pk = "PK" // Pakistan
|
||||||
|
case pw = "PW" // Palau
|
||||||
|
case ps = "PS" // Palestine
|
||||||
|
case pa = "PA" // Panama
|
||||||
|
case pg = "PG" // Papua New Guinea
|
||||||
|
case py = "PY" // Paraguay
|
||||||
|
case pe = "PE" // Peru
|
||||||
|
case ph = "PH" // Philippines
|
||||||
|
case pn = "PN" // Pitcairn
|
||||||
|
case pl = "PL" // Poland
|
||||||
|
case pt = "PT" // Portugal
|
||||||
|
case pr = "PR" // Puerto Rico
|
||||||
|
case qa = "QA" // Qatar
|
||||||
|
case re = "RE" // Réunion
|
||||||
|
case ro = "RO" // Romania
|
||||||
|
case ru = "RU" // Russia
|
||||||
|
case rw = "RW" // Rwanda
|
||||||
|
case bl = "BL" // Saint Barthélemy
|
||||||
|
case sh = "SH" // Saint Helena
|
||||||
|
case kn = "KN" // Saint Kitts and Nevis
|
||||||
|
case lc = "LC" // Saint Lucia
|
||||||
|
case mf = "MF" // Saint Martin
|
||||||
|
case pm = "PM" // Saint Pierre and Miquelon
|
||||||
|
case vc = "VC" // Saint Vincent and the Grenadines
|
||||||
|
case ws = "WS" // Samoa
|
||||||
|
case sm = "SM" // San Marino
|
||||||
|
case st = "ST" // São Tomé and Príncipe
|
||||||
|
case sa = "SA" // Saudi Arabia
|
||||||
|
case sn = "SN" // Senegal
|
||||||
|
case rs = "RS" // Serbia
|
||||||
|
case sc = "SC" // Seychelles
|
||||||
|
case sl = "SL" // Sierra Leone
|
||||||
|
case sg = "SG" // Singapore
|
||||||
|
case sx = "SX" // Sint Maarten
|
||||||
|
case sk = "SK" // Slovakia
|
||||||
|
case si = "SI" // Slovenia
|
||||||
|
case sb = "SB" // Solomon Islands
|
||||||
|
case so = "SO" // Somalia
|
||||||
|
case za = "ZA" // South Africa
|
||||||
|
case gs = "GS" // South Georgia
|
||||||
|
case ss = "SS" // South Sudan
|
||||||
|
case es = "ES" // Spain
|
||||||
|
case lk = "LK" // Sri Lanka
|
||||||
|
case sd = "SD" // Sudan
|
||||||
|
case sr = "SR" // Suriname
|
||||||
|
case sj = "SJ" // Svalbard and Jan Mayen
|
||||||
|
case se = "SE" // Sweden
|
||||||
|
case ch = "CH" // Switzerland
|
||||||
|
case sy = "SY" // Syria
|
||||||
|
case tw = "TW" // Taiwan
|
||||||
|
case tj = "TJ" // Tajikistan
|
||||||
|
case tz = "TZ" // Tanzania
|
||||||
|
case th = "TH" // Thailand
|
||||||
|
case tl = "TL" // Timor-Leste
|
||||||
|
case tg = "TG" // Togo
|
||||||
|
case tk = "TK" // Tokelau
|
||||||
|
case to = "TO" // Tonga
|
||||||
|
case tt = "TT" // Trinidad and Tobago
|
||||||
|
case tn = "TN" // Tunisia
|
||||||
|
case tr = "TR" // Turkey
|
||||||
|
case tm = "TM" // Turkmenistan
|
||||||
|
case tc = "TC" // Turks and Caicos Islands
|
||||||
|
case tv = "TV" // Tuvalu
|
||||||
|
case ug = "UG" // Uganda
|
||||||
|
case ua = "UA" // Ukraine
|
||||||
|
case ae = "AE" // United Arab Emirates
|
||||||
|
case gb = "GB" // United Kingdom
|
||||||
|
case us = "US" // United States
|
||||||
|
case um = "UM" // U.S. Minor Outlying Islands
|
||||||
|
case uy = "UY" // Uruguay
|
||||||
|
case uz = "UZ" // Uzbekistan
|
||||||
|
case vu = "VU" // Vanuatu
|
||||||
|
case ve = "VE" // Venezuela
|
||||||
|
case vn = "VN" // Vietnam
|
||||||
|
case vg = "VG" // British Virgin Islands
|
||||||
|
case vi = "VI" // U.S. Virgin Islands
|
||||||
|
case wf = "WF" // Wallis and Futuna
|
||||||
|
case eh = "EH" // Western Sahara
|
||||||
|
case ye = "YE" // Yemen
|
||||||
|
case zm = "ZM" // Zambia
|
||||||
|
case zw = "ZW" // Zimbabwe
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CountryCode {
|
||||||
|
/// Returns the localized full country name (e.g. "Germany", "Deutschland")
|
||||||
|
func fullName(locale: Locale = .current) -> String {
|
||||||
|
locale.localizedString(forRegionCode: self.rawValue) ?? self.rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
//
|
||||||
|
// File.swift
|
||||||
|
// ExodaiAcademy
|
||||||
|
//
|
||||||
|
// Created by Exodai on 22/01/2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum Database: String, Codable {
|
||||||
|
case users
|
||||||
|
case campuses
|
||||||
|
case courses
|
||||||
|
case sessions
|
||||||
|
case articles
|
||||||
|
case categories
|
||||||
|
case tags
|
||||||
|
case logs
|
||||||
|
case purchases
|
||||||
|
case products
|
||||||
|
case confirmations
|
||||||
|
case tokens
|
||||||
|
case folders
|
||||||
|
case files
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// Role.swift
|
||||||
|
// ExodaiAcademy
|
||||||
|
//
|
||||||
|
// Created by Exodai on 22/01/2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
enum Role: String, Codable {
|
||||||
|
case admin
|
||||||
|
case editor
|
||||||
|
case leadEditor
|
||||||
|
case writer
|
||||||
|
case reviewer
|
||||||
|
case moderator
|
||||||
|
case user
|
||||||
|
case blocked
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
//
|
||||||
|
// Status.swift
|
||||||
|
// ExodaiAcademy
|
||||||
|
//
|
||||||
|
// Created by Exodai on 23/01/2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
enum Status: String, Codable {
|
||||||
|
case published
|
||||||
|
case draft
|
||||||
|
case planned
|
||||||
|
case inReview
|
||||||
|
case archived
|
||||||
|
case trash
|
||||||
|
|
||||||
|
}
|
||||||
108
Sources/ExodaiAcademy/Infrastructure/Database/ArticleModel.swift
Normal file
108
Sources/ExodaiAcademy/Infrastructure/Database/ArticleModel.swift
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
//
|
||||||
|
// ArticleModel.swift
|
||||||
|
// ExodaiAcademy
|
||||||
|
//
|
||||||
|
// Created by Exodai on 23/01/2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Fluent
|
||||||
|
|
||||||
|
final class ArticleModel: Model, @unchecked Sendable {
|
||||||
|
static let schema: String = Database.articles.rawValue
|
||||||
|
|
||||||
|
// MARK: - ID
|
||||||
|
|
||||||
|
@ID(key: .id)
|
||||||
|
var id: UUID?
|
||||||
|
|
||||||
|
// MARK: - CMS Fields (all optional)
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.title)
|
||||||
|
var title: String?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.description)
|
||||||
|
var description: String?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.slug)
|
||||||
|
var slug: String?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.excerpt)
|
||||||
|
var excerpt: String?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.image)
|
||||||
|
var image: String?
|
||||||
|
|
||||||
|
@OptionalEnum(key: FieldKeys.status)
|
||||||
|
var status: Status?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.authorID)
|
||||||
|
var authorID: UserModel.IDValue?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.categories)
|
||||||
|
var categories: [CategoryModel.IDValue]?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.tags)
|
||||||
|
var tags: [TagModel.IDValue]?
|
||||||
|
|
||||||
|
// MARK: - Timestamps
|
||||||
|
|
||||||
|
@Timestamp(key: FieldKeys.createdAt, on: .create)
|
||||||
|
var createdAt: Date?
|
||||||
|
|
||||||
|
@Timestamp(key: FieldKeys.updatedAt, on: .update)
|
||||||
|
var updatedAt: Date?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.publishDate)
|
||||||
|
var publishDate: Date?
|
||||||
|
|
||||||
|
// MARK: - Initializer
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension ArticleModel {
|
||||||
|
struct FieldKeys {
|
||||||
|
static var title: FieldKey { "title" }
|
||||||
|
static var description: FieldKey { "description" }
|
||||||
|
static var slug: FieldKey { "slug" }
|
||||||
|
static var excerpt: FieldKey { "excerpt" }
|
||||||
|
static var image: FieldKey { "image" }
|
||||||
|
static var status: FieldKey { "status" }
|
||||||
|
static var authorID: FieldKey { "authorID" }
|
||||||
|
static var categories: FieldKey { "categories" }
|
||||||
|
static var tags: FieldKey { "tags" }
|
||||||
|
static var createdAt: FieldKey { "createdAt" }
|
||||||
|
static var updatedAt: FieldKey { "updatedAt" }
|
||||||
|
static var publishDate: FieldKey { "publishDate" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import Fluent
|
||||||
|
|
||||||
|
extension ArticleModel {
|
||||||
|
struct Migration: AsyncMigration {
|
||||||
|
|
||||||
|
func prepare(on database: any FluentKit.Database) async throws {
|
||||||
|
try await database.schema(ArticleModel.schema)
|
||||||
|
.id()
|
||||||
|
.field(FieldKeys.title, .string)
|
||||||
|
.field(FieldKeys.description, .string)
|
||||||
|
.field(FieldKeys.slug, .string)
|
||||||
|
.field(FieldKeys.excerpt, .string)
|
||||||
|
.field(FieldKeys.image, .string)
|
||||||
|
.field(FieldKeys.status, .string)
|
||||||
|
.field(FieldKeys.authorID, .uuid)
|
||||||
|
.field(FieldKeys.categories, .array(of: .uuid))
|
||||||
|
.field(FieldKeys.tags, .array(of: .uuid))
|
||||||
|
.field(FieldKeys.createdAt, .datetime)
|
||||||
|
.field(FieldKeys.updatedAt, .datetime)
|
||||||
|
.field(FieldKeys.publishDate, .datetime)
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
func revert(on database: any FluentKit.Database) async throws {
|
||||||
|
try await database.schema(ArticleModel.schema).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
107
Sources/ExodaiAcademy/Infrastructure/Database/CampusModel.swift
Normal file
107
Sources/ExodaiAcademy/Infrastructure/Database/CampusModel.swift
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
//
|
||||||
|
// CampusModel.swift
|
||||||
|
// ExodaiAcademy
|
||||||
|
//
|
||||||
|
// Created by Exodai on 23/01/2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Fluent
|
||||||
|
|
||||||
|
final class CampusModel: Model, @unchecked Sendable {
|
||||||
|
static let schema: String = Database.campuses.rawValue
|
||||||
|
|
||||||
|
// MARK: - ID
|
||||||
|
|
||||||
|
@ID(key: .id)
|
||||||
|
var id: UUID?
|
||||||
|
|
||||||
|
// MARK: - Data
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.title)
|
||||||
|
var title: String?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.description)
|
||||||
|
var description: String?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.slug)
|
||||||
|
var slug: String?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.content)
|
||||||
|
var content: String?
|
||||||
|
|
||||||
|
@Enum(key: FieldKeys.status)
|
||||||
|
var status: Status
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.instructorID)
|
||||||
|
var instructorID: UserModel.IDValue
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.price)
|
||||||
|
var price: Double?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.tags)
|
||||||
|
var tags: [TagModel.IDValue]?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.categories)
|
||||||
|
var categories: [CategoryModel.IDValue]?
|
||||||
|
|
||||||
|
// MARK: - Timestamps
|
||||||
|
|
||||||
|
@Timestamp(key: FieldKeys.createdAt, on: .create)
|
||||||
|
var createdAt: Date?
|
||||||
|
|
||||||
|
@Timestamp(key: FieldKeys.updatedAt, on: .update)
|
||||||
|
var updatedAt: Date?
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.publishDate)
|
||||||
|
var publishDate: Date?
|
||||||
|
|
||||||
|
// MARK: - Initializers
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CampusModel {
|
||||||
|
struct FieldKeys {
|
||||||
|
static var title: FieldKey { "title" }
|
||||||
|
static var description: FieldKey { "description" }
|
||||||
|
static var slug: FieldKey {"slug"}
|
||||||
|
static var content: FieldKey { "content" }
|
||||||
|
static var status: FieldKey { "status" }
|
||||||
|
static var instructorID: FieldKey { "instructorID" }
|
||||||
|
static var price: FieldKey { "price" }
|
||||||
|
static var tags: FieldKey { "tags" }
|
||||||
|
static var categories: FieldKey { "categories" }
|
||||||
|
static var createdAt: FieldKey { "createdAt" }
|
||||||
|
static var updatedAt: FieldKey { "updatedAt" }
|
||||||
|
static var publishDate: FieldKey { "publishDate" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import Fluent
|
||||||
|
|
||||||
|
extension CampusModel {
|
||||||
|
struct Migration: AsyncMigration {
|
||||||
|
|
||||||
|
func prepare(on database: any FluentKit.Database) async throws {
|
||||||
|
try await database.schema(CampusModel.schema)
|
||||||
|
.id()
|
||||||
|
.field(FieldKeys.title, .string)
|
||||||
|
.field(FieldKeys.description, .string)
|
||||||
|
.field(FieldKeys.slug, .string)
|
||||||
|
.field(FieldKeys.content, .string)
|
||||||
|
.field(FieldKeys.status, .string, .required)
|
||||||
|
.field(FieldKeys.instructorID, .uuid, .required)
|
||||||
|
.field(FieldKeys.price, .double)
|
||||||
|
.field(FieldKeys.tags, .array(of: .uuid))
|
||||||
|
.field(FieldKeys.categories, .array(of: .uuid))
|
||||||
|
.field(FieldKeys.createdAt, .datetime)
|
||||||
|
.field(FieldKeys.updatedAt, .datetime)
|
||||||
|
.field(FieldKeys.publishDate, .datetime)
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
func revert(on database: any FluentKit.Database) async throws {
|
||||||
|
try await database.schema(CampusModel.schema).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
//
|
||||||
|
// CategoryModel.swift
|
||||||
|
// ExodaiAcademy
|
||||||
|
//
|
||||||
|
// Created by Exodai on 23/01/2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Fluent
|
||||||
|
|
||||||
|
final class CategoryModel: Model, @unchecked Sendable {
|
||||||
|
static let schema: String = Database.categories.rawValue
|
||||||
|
|
||||||
|
// MARK: - ID
|
||||||
|
|
||||||
|
@ID(key: .id)
|
||||||
|
var id: UUID?
|
||||||
|
|
||||||
|
// MARK: - Data
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.name)
|
||||||
|
var name: String
|
||||||
|
|
||||||
|
// MARK: - Initializers
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
|
||||||
|
init(
|
||||||
|
id: UUID? = nil,
|
||||||
|
name: String
|
||||||
|
) {
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CategoryModel {
|
||||||
|
struct FieldKeys {
|
||||||
|
static var name: FieldKey { "name" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CategoryModel {
|
||||||
|
struct Migration: AsyncMigration {
|
||||||
|
|
||||||
|
func prepare(on database: any FluentKit.Database) async throws {
|
||||||
|
try await database.schema(CategoryModel.schema)
|
||||||
|
.id()
|
||||||
|
.field(FieldKeys.name, .string, .required)
|
||||||
|
.unique(on: FieldKeys.name)
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
func revert(on database: any FluentKit.Database) async throws {
|
||||||
|
try await database.schema(CategoryModel.schema).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
//
|
||||||
|
// ConfirmationModel.swift
|
||||||
|
// ExodaiAcademy
|
||||||
|
//
|
||||||
|
// Created by Exodai on 23/01/2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Fluent
|
||||||
|
import Vapor
|
||||||
|
|
||||||
|
import Fluent
|
||||||
|
|
||||||
|
final class ConfirmationModel: Model, @unchecked Sendable {
|
||||||
|
static let schema: String = Database.confirmations.rawValue
|
||||||
|
|
||||||
|
@ID(key: .id)
|
||||||
|
var id: UUID?
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.userID)
|
||||||
|
var userID: UserModel.IDValue
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.confirmationCode)
|
||||||
|
var confirmationCode: String
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.email)
|
||||||
|
var email: String
|
||||||
|
|
||||||
|
@Timestamp(key: FieldKeys.createdAt, on: .create)
|
||||||
|
var createdAt: Date?
|
||||||
|
|
||||||
|
@Timestamp(key: FieldKeys.validTill, on: .none)
|
||||||
|
var validTill: Date?
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.isConfirmed)
|
||||||
|
var isConfirmed: Bool
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
|
||||||
|
init(id: UUID? = nil, userID: UserModel.IDValue, email: String?, confirmationCode: String, validTill: Date, isConfirmed: Bool = false) {
|
||||||
|
self.id = id
|
||||||
|
self.userID = userID
|
||||||
|
self.confirmationCode = confirmationCode
|
||||||
|
self.validTill = validTill
|
||||||
|
self.isConfirmed = isConfirmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ConfirmationModel {
|
||||||
|
struct FieldKeys {
|
||||||
|
static var userID: FieldKey { "userID" }
|
||||||
|
static var confirmationCode: FieldKey { "confirmationCode" }
|
||||||
|
static var email: FieldKey { "email" }
|
||||||
|
static var createdAt: FieldKey { "createdAt" }
|
||||||
|
static var validTill: FieldKey { "validTill" }
|
||||||
|
static var isConfirmed: FieldKey { "isConfirmed" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ConfirmationModel {
|
||||||
|
struct Migration: AsyncMigration {
|
||||||
|
|
||||||
|
func prepare(on database: any FluentKit.Database) async throws {
|
||||||
|
try await database.schema(ConfirmationModel.schema)
|
||||||
|
.id()
|
||||||
|
.field(FieldKeys.userID, .uuid, .required)
|
||||||
|
.field(FieldKeys.confirmationCode, .string, .required)
|
||||||
|
.field(FieldKeys.email, .string, .required)
|
||||||
|
.field(FieldKeys.createdAt, .datetime)
|
||||||
|
.field(FieldKeys.validTill, .datetime)
|
||||||
|
.field(FieldKeys.isConfirmed, .bool, .required)
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
func revert(on database: any FluentKit.Database) async throws {
|
||||||
|
try await database.schema(ConfirmationModel.schema).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
103
Sources/ExodaiAcademy/Infrastructure/Database/CourseModel.swift
Normal file
103
Sources/ExodaiAcademy/Infrastructure/Database/CourseModel.swift
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
//
|
||||||
|
// CourseModel.swift
|
||||||
|
// ExodaiAcademy
|
||||||
|
//
|
||||||
|
// Created by Exodai on 23/01/2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Fluent
|
||||||
|
|
||||||
|
final class CourseModel: Model, @unchecked Sendable {
|
||||||
|
static let schema: String = Database.courses.rawValue
|
||||||
|
|
||||||
|
// MARK: - ID
|
||||||
|
|
||||||
|
@ID(key: .id)
|
||||||
|
var id: UUID?
|
||||||
|
|
||||||
|
// MARK: - Data
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.title)
|
||||||
|
var title: String?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.description)
|
||||||
|
var description: String?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.slug)
|
||||||
|
var slug: String?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.excerpt)
|
||||||
|
var excerpt: String?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.content)
|
||||||
|
var content: String?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.campusID)
|
||||||
|
var campusID: CampusModel.IDValue?
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.authorID)
|
||||||
|
var authorID: UserModel.IDValue
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.image)
|
||||||
|
var image: String?
|
||||||
|
|
||||||
|
@Enum(key: FieldKeys.status)
|
||||||
|
var status: Status
|
||||||
|
|
||||||
|
@Timestamp(key: FieldKeys.createdAt, on: .create)
|
||||||
|
var createdAt: Date?
|
||||||
|
|
||||||
|
@Timestamp(key: FieldKeys.updatedAt, on: .update)
|
||||||
|
var updatedAt: Date?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.publishDate)
|
||||||
|
var publishDate: Date?
|
||||||
|
|
||||||
|
// MARK: - Initializers
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CourseModel {
|
||||||
|
struct FieldKeys {
|
||||||
|
static var title: FieldKey { "title" }
|
||||||
|
static var description: FieldKey { "description" }
|
||||||
|
static var slug: FieldKey { "slug" }
|
||||||
|
static var excerpt: FieldKey { "excerpt" }
|
||||||
|
static var content: FieldKey { "content" }
|
||||||
|
static var campusID: FieldKey { "campusID" }
|
||||||
|
static var authorID: FieldKey { "authorID" }
|
||||||
|
static var image: FieldKey { "image" }
|
||||||
|
static var status: FieldKey { "status" }
|
||||||
|
static var createdAt: FieldKey { "createdAt" }
|
||||||
|
static var updatedAt: FieldKey { "updatedAt" }
|
||||||
|
static var publishDate: FieldKey { "publishDate" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CourseModel {
|
||||||
|
struct Migration: AsyncMigration {
|
||||||
|
|
||||||
|
func prepare(on database: any FluentKit.Database) async throws {
|
||||||
|
try await database.schema(CourseModel.schema)
|
||||||
|
.id()
|
||||||
|
.field(FieldKeys.title, .string)
|
||||||
|
.field(FieldKeys.description, .string)
|
||||||
|
.field(FieldKeys.slug, .string)
|
||||||
|
.field(FieldKeys.excerpt, .string)
|
||||||
|
.field(FieldKeys.content, .string)
|
||||||
|
.field(FieldKeys.campusID, .uuid)
|
||||||
|
.field(FieldKeys.authorID, .uuid, .required)
|
||||||
|
.field(FieldKeys.image, .string)
|
||||||
|
.field(FieldKeys.status, .string, .required)
|
||||||
|
.field(FieldKeys.createdAt, .datetime)
|
||||||
|
.field(FieldKeys.updatedAt, .datetime)
|
||||||
|
.field(FieldKeys.publishDate, .datetime)
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
func revert(on database: any FluentKit.Database) async throws {
|
||||||
|
try await database.schema(CourseModel.schema).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
//
|
||||||
|
// FileModel.swift
|
||||||
|
// ExodaiAcademy
|
||||||
|
//
|
||||||
|
// Created by Exodai on 24/01/2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Fluent
|
||||||
|
|
||||||
|
final class FileModel: Model, @unchecked Sendable {
|
||||||
|
static let schema: String = Database.files.rawValue
|
||||||
|
|
||||||
|
@ID(key: .id)
|
||||||
|
var id: UUID?
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.folderID)
|
||||||
|
var folderID: FolderModel.IDValue
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.name)
|
||||||
|
var name: String
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.type)
|
||||||
|
var type: String
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.mimeType)
|
||||||
|
var mimeType: String
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.storageKey)
|
||||||
|
var storageKey: String
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.size)
|
||||||
|
var size: Int64
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.metadata)
|
||||||
|
var metadata: String?
|
||||||
|
|
||||||
|
@Timestamp(key: FieldKeys.createdAt, on: .create)
|
||||||
|
var createdAt: Date?
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FileModel {
|
||||||
|
struct FieldKeys {
|
||||||
|
static var folderID: FieldKey { "folderID" }
|
||||||
|
static var name: FieldKey { "name" }
|
||||||
|
static var type: FieldKey { "type" }
|
||||||
|
static var mimeType: FieldKey { "mimeType" }
|
||||||
|
static var storageKey: FieldKey { "storageKey" }
|
||||||
|
static var size: FieldKey { "size" }
|
||||||
|
static var metadata: FieldKey { "metadata" }
|
||||||
|
static var createdAt: FieldKey { "createdAt" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import Fluent
|
||||||
|
|
||||||
|
extension FileModel {
|
||||||
|
struct Migration: AsyncMigration {
|
||||||
|
|
||||||
|
func prepare(on database: any FluentKit.Database) async throws {
|
||||||
|
try await database.schema(FileModel.schema)
|
||||||
|
.id()
|
||||||
|
.field(FieldKeys.folderID, .uuid, .required)
|
||||||
|
.field(FieldKeys.name, .string, .required)
|
||||||
|
.field(FieldKeys.type, .string, .required)
|
||||||
|
.field(FieldKeys.mimeType, .string, .required)
|
||||||
|
.field(FieldKeys.storageKey, .string, .required)
|
||||||
|
.field(FieldKeys.size, .int64, .required)
|
||||||
|
.field(FieldKeys.metadata, .string)
|
||||||
|
.field(FieldKeys.createdAt, .datetime)
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
func revert(on database: any FluentKit.Database) async throws {
|
||||||
|
try await database.schema(FileModel.schema).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
//
|
||||||
|
// FolderModel.swift
|
||||||
|
// ExodaiAcademy
|
||||||
|
//
|
||||||
|
// Created by Exodai on 24/01/2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Fluent
|
||||||
|
|
||||||
|
final class FolderModel: Model, @unchecked Sendable {
|
||||||
|
static let schema: String = Database.folders.rawValue
|
||||||
|
|
||||||
|
@ID(key: .id)
|
||||||
|
var id: UUID?
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.name)
|
||||||
|
var name: String
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.parentFolderID)
|
||||||
|
var parentFolderID: FolderModel.IDValue?
|
||||||
|
|
||||||
|
@Timestamp(key: FieldKeys.createdAt, on: .create)
|
||||||
|
var createdAt: Date?
|
||||||
|
|
||||||
|
@Timestamp(key: FieldKeys.updatedAt, on: .update)
|
||||||
|
var updatedAt: Date?
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FolderModel {
|
||||||
|
struct FieldKeys {
|
||||||
|
static var name: FieldKey { "name" }
|
||||||
|
static var parentFolderID: FieldKey { "parentFolderID" }
|
||||||
|
static var createdAt: FieldKey { "createdAt" }
|
||||||
|
static var updatedAt: FieldKey { "updatedAt" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import Fluent
|
||||||
|
|
||||||
|
extension FolderModel {
|
||||||
|
struct Migration: AsyncMigration {
|
||||||
|
|
||||||
|
func prepare(on database: any FluentKit.Database) async throws {
|
||||||
|
try await database.schema(FolderModel.schema)
|
||||||
|
.id()
|
||||||
|
.field(FieldKeys.name, .string, .required)
|
||||||
|
.field(FieldKeys.parentFolderID, .uuid)
|
||||||
|
.field(FieldKeys.createdAt, .datetime)
|
||||||
|
.field(FieldKeys.updatedAt, .datetime)
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
func revert(on database: any FluentKit.Database) async throws {
|
||||||
|
try await database.schema(FolderModel.schema).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
72
Sources/ExodaiAcademy/Infrastructure/Database/LogModel.swift
Normal file
72
Sources/ExodaiAcademy/Infrastructure/Database/LogModel.swift
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
//
|
||||||
|
// LogModel.swift
|
||||||
|
// ExodaiAcademy
|
||||||
|
//
|
||||||
|
// Created by Exodai on 24/01/2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Fluent
|
||||||
|
|
||||||
|
final class LogModel: Model, @unchecked Sendable {
|
||||||
|
static let schema: String = Database.logs.rawValue
|
||||||
|
|
||||||
|
@ID(key: .id)
|
||||||
|
var id: UUID?
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.event)
|
||||||
|
var event: String
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.actorID)
|
||||||
|
var actorID: UserModel.IDValue?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.targetType)
|
||||||
|
var targetType: String?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.targetID)
|
||||||
|
var targetID: UUID?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.context)
|
||||||
|
var context: String?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.ipAddress)
|
||||||
|
var ipAddress: String?
|
||||||
|
|
||||||
|
@Timestamp(key: FieldKeys.createdAt, on: .create)
|
||||||
|
var createdAt: Date?
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LogModel {
|
||||||
|
struct FieldKeys {
|
||||||
|
static var event: FieldKey { "event" }
|
||||||
|
static var actorID: FieldKey { "actorID" }
|
||||||
|
static var ipAddress: FieldKey { "ipAddress" }
|
||||||
|
static var targetType: FieldKey { "targetType" }
|
||||||
|
static var targetID: FieldKey { "targetID" }
|
||||||
|
static var context: FieldKey { "context" }
|
||||||
|
static var createdAt: FieldKey { "createdAt" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LogModel {
|
||||||
|
struct Migration: AsyncMigration {
|
||||||
|
|
||||||
|
func prepare(on database: any FluentKit.Database) async throws {
|
||||||
|
try await database.schema(LogModel.schema)
|
||||||
|
.id()
|
||||||
|
.field(FieldKeys.event, .string, .required)
|
||||||
|
.field(FieldKeys.actorID, .uuid)
|
||||||
|
.field(FieldKeys.ipAddress, .string)
|
||||||
|
.field(FieldKeys.targetType, .string)
|
||||||
|
.field(FieldKeys.targetID, .uuid)
|
||||||
|
.field(FieldKeys.context, .string)
|
||||||
|
.field(FieldKeys.createdAt, .datetime)
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
func revert(on database: any FluentKit.Database) async throws {
|
||||||
|
try await database.schema(LogModel.schema).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
//
|
||||||
|
// PurchaseModel.swift
|
||||||
|
// ExodaiAcademy
|
||||||
|
//
|
||||||
|
// Created by Exodai on 24/01/2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Fluent
|
||||||
|
|
||||||
|
final class PurchaseModel: Model, @unchecked Sendable {
|
||||||
|
static let schema: String = Database.purchases.rawValue
|
||||||
|
|
||||||
|
@ID(key: .id)
|
||||||
|
var id: UUID?
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.userID)
|
||||||
|
var userID: UserModel.IDValue
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.courseID)
|
||||||
|
var courseID: CourseModel.IDValue
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.pricePaid)
|
||||||
|
var pricePaid: Double
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.currency)
|
||||||
|
var currency: String
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.paymentProvider)
|
||||||
|
var paymentProvider: String
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.paymentReference)
|
||||||
|
var paymentReference: String
|
||||||
|
|
||||||
|
@Timestamp(key: FieldKeys.purchasedAt, on: .create)
|
||||||
|
var purchasedAt: Date?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.refundedAt)
|
||||||
|
var refundedAt: Date?
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.grantedByAdmin)
|
||||||
|
var grantedByAdmin: Bool
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PurchaseModel {
|
||||||
|
struct FieldKeys {
|
||||||
|
static var userID: FieldKey { "userID" }
|
||||||
|
static var courseID: FieldKey { "courseID" }
|
||||||
|
static var pricePaid: FieldKey { "pricePaid" }
|
||||||
|
static var currency: FieldKey { "currency" }
|
||||||
|
static var paymentProvider: FieldKey { "paymentProvider" }
|
||||||
|
static var paymentReference: FieldKey { "paymentReference" }
|
||||||
|
static var purchasedAt: FieldKey { "purchasedAt" }
|
||||||
|
static var refundedAt: FieldKey { "refundedAt" }
|
||||||
|
static var grantedByAdmin: FieldKey { "grantedByAdmin" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PurchaseModel {
|
||||||
|
struct Migration: AsyncMigration {
|
||||||
|
|
||||||
|
func prepare(on database: any FluentKit.Database) async throws {
|
||||||
|
try await database.schema(PurchaseModel.schema)
|
||||||
|
.id()
|
||||||
|
.field(FieldKeys.userID, .uuid, .required)
|
||||||
|
.field(FieldKeys.courseID, .uuid, .required)
|
||||||
|
.field(FieldKeys.pricePaid, .double, .required)
|
||||||
|
.field(FieldKeys.currency, .string, .required)
|
||||||
|
.field(FieldKeys.paymentProvider, .string, .required)
|
||||||
|
.field(FieldKeys.paymentReference, .string, .required)
|
||||||
|
.field(FieldKeys.purchasedAt, .datetime)
|
||||||
|
.field(FieldKeys.refundedAt, .datetime)
|
||||||
|
.field(FieldKeys.grantedByAdmin, .bool, .required)
|
||||||
|
.unique(on: FieldKeys.userID, FieldKeys.courseID)
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
func revert(on database: any FluentKit.Database) async throws {
|
||||||
|
try await database.schema(PurchaseModel.schema).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
100
Sources/ExodaiAcademy/Infrastructure/Database/SessionModel.swift
Normal file
100
Sources/ExodaiAcademy/Infrastructure/Database/SessionModel.swift
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
//
|
||||||
|
// SessionModel.swift
|
||||||
|
// ExodaiAcademy
|
||||||
|
//
|
||||||
|
// Created by Exodai on 23/01/2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Fluent
|
||||||
|
|
||||||
|
final class SessionModel: Model, @unchecked Sendable {
|
||||||
|
static let schema: String = Database.sessions.rawValue
|
||||||
|
|
||||||
|
// MARK: - ID
|
||||||
|
|
||||||
|
@ID(key: .id)
|
||||||
|
var id: UUID?
|
||||||
|
|
||||||
|
// MARK: - CMS Fields (ALL optional)
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.title)
|
||||||
|
var title: String?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.description)
|
||||||
|
var description: String?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.slug)
|
||||||
|
var slug: String?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.mp4URL)
|
||||||
|
var mp4URL: String?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.hlsURL)
|
||||||
|
var hlsURL: String?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.content)
|
||||||
|
var content: String?
|
||||||
|
|
||||||
|
@OptionalEnum(key: FieldKeys.status)
|
||||||
|
var status: Status?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.courseID)
|
||||||
|
var courseID: CourseModel.IDValue?
|
||||||
|
|
||||||
|
// MARK: - Timestamps
|
||||||
|
|
||||||
|
@Timestamp(key: FieldKeys.createdAt, on: .create)
|
||||||
|
var createdAt: Date?
|
||||||
|
|
||||||
|
@Timestamp(key: FieldKeys.updatedAt, on: .update)
|
||||||
|
var updatedAt: Date?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.publishDate)
|
||||||
|
var publishDate: Date?
|
||||||
|
|
||||||
|
// MARK: - Initializer
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SessionModel {
|
||||||
|
struct FieldKeys {
|
||||||
|
static var title: FieldKey { "title" }
|
||||||
|
static var description: FieldKey { "description" }
|
||||||
|
static var slug: FieldKey { "slug" }
|
||||||
|
static var mp4URL: FieldKey { "mp4URL" }
|
||||||
|
static var hlsURL: FieldKey { "hlsURL" }
|
||||||
|
static var content: FieldKey { "content" }
|
||||||
|
static var status: FieldKey { "status" }
|
||||||
|
static var courseID: FieldKey { "courseID" }
|
||||||
|
static var createdAt: FieldKey { "createdAt" }
|
||||||
|
static var updatedAt: FieldKey { "updatedAt" }
|
||||||
|
static var publishDate: FieldKey { "publishDate" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SessionModel {
|
||||||
|
struct Migration: AsyncMigration {
|
||||||
|
|
||||||
|
func prepare(on database: any FluentKit.Database) async throws {
|
||||||
|
try await database.schema(SessionModel.schema)
|
||||||
|
.id()
|
||||||
|
.field(FieldKeys.title, .string)
|
||||||
|
.field(FieldKeys.description, .string)
|
||||||
|
.field(FieldKeys.slug, .string)
|
||||||
|
.field(FieldKeys.mp4URL, .string)
|
||||||
|
.field(FieldKeys.hlsURL, .string)
|
||||||
|
.field(FieldKeys.content, .string)
|
||||||
|
.field(FieldKeys.status, .string)
|
||||||
|
.field(FieldKeys.courseID, .uuid)
|
||||||
|
.field(FieldKeys.createdAt, .datetime)
|
||||||
|
.field(FieldKeys.updatedAt, .datetime)
|
||||||
|
.field(FieldKeys.publishDate, .datetime)
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
func revert(on database: any FluentKit.Database) async throws {
|
||||||
|
try await database.schema(SessionModel.schema).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
Sources/ExodaiAcademy/Infrastructure/Database/TagModel.swift
Normal file
31
Sources/ExodaiAcademy/Infrastructure/Database/TagModel.swift
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
//
|
||||||
|
// TagModel.swift
|
||||||
|
// ExodaiAcademy
|
||||||
|
//
|
||||||
|
// Created by Exodai on 23/01/2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Fluent
|
||||||
|
|
||||||
|
final class TagModel: Model, @unchecked Sendable {
|
||||||
|
static let schema: String = Database.tags.rawValue
|
||||||
|
|
||||||
|
@ID(key: .id)
|
||||||
|
var id: UUID?
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.name)
|
||||||
|
var name: String
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
|
||||||
|
init(id: UUID? = nil, name: String) {
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TagModel {
|
||||||
|
struct FieldKeys {
|
||||||
|
static var name: FieldKey { "name" }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
//
|
||||||
|
// TokenModel.swift
|
||||||
|
// ExodaiAcademy
|
||||||
|
//
|
||||||
|
// Created by Exodai on 23/01/2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Fluent
|
||||||
|
|
||||||
|
final class TokenModel: Model, @unchecked Sendable {
|
||||||
|
static let schema: String = Database.tokens.rawValue
|
||||||
|
|
||||||
|
@ID(key: .id)
|
||||||
|
var id: UUID?
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.userID)
|
||||||
|
var userID: UserModel.IDValue
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.value)
|
||||||
|
var value: String
|
||||||
|
|
||||||
|
@Timestamp(key: FieldKeys.createdAt, on: .create)
|
||||||
|
var createdAt: Date?
|
||||||
|
|
||||||
|
@Timestamp(key: FieldKeys.validTill, on: .none)
|
||||||
|
var validTill: Date?
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
|
||||||
|
init(id: UUID? = nil, userID: UserModel.IDValue, value: String, validTill: Date?) {
|
||||||
|
self.id = id
|
||||||
|
self.userID = userID
|
||||||
|
self.value = value
|
||||||
|
self.validTill = validTill
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TokenModel {
|
||||||
|
struct FieldKeys {
|
||||||
|
static var userID: FieldKey { "userID" }
|
||||||
|
static var value: FieldKey { "value" }
|
||||||
|
static var createdAt: FieldKey { "createdAt" }
|
||||||
|
static var validTill: FieldKey { "validTill" }
|
||||||
|
}
|
||||||
|
}
|
||||||
177
Sources/ExodaiAcademy/Infrastructure/Database/UserModel.swift
Normal file
177
Sources/ExodaiAcademy/Infrastructure/Database/UserModel.swift
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
//
|
||||||
|
// UserModel.swift
|
||||||
|
// ExodaiAcademy
|
||||||
|
//
|
||||||
|
// Created by Exodai on 22/01/2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Vapor
|
||||||
|
import Fluent
|
||||||
|
import FluentKit
|
||||||
|
|
||||||
|
final class UserModel: Model, @unchecked Sendable {
|
||||||
|
static let schema: String = Database.users.rawValue
|
||||||
|
|
||||||
|
@ID(key: .id)
|
||||||
|
var id: UUID?
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.username)
|
||||||
|
var username: String
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.email)
|
||||||
|
var email: String
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.password)
|
||||||
|
var password: String
|
||||||
|
|
||||||
|
@Field(key: FieldKeys.role)
|
||||||
|
var role: Role
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.name)
|
||||||
|
var name: String?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.lastname)
|
||||||
|
var lastname: String?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.address)
|
||||||
|
var address: String?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.zipCode)
|
||||||
|
var zipCode: String?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.city)
|
||||||
|
var city: String?
|
||||||
|
|
||||||
|
@OptionalEnum(key: FieldKeys.country)
|
||||||
|
var Country: CountryCode?
|
||||||
|
|
||||||
|
@OptionalField(key: FieldKeys.stripeID)
|
||||||
|
var stripeID: String?
|
||||||
|
|
||||||
|
@Timestamp(key: FieldKeys.createdAt, on: .create)
|
||||||
|
var createdAt: Date?
|
||||||
|
|
||||||
|
@Timestamp(key: FieldKeys.updatedAt, on: .update)
|
||||||
|
var updatedAt: Date?
|
||||||
|
|
||||||
|
@Timestamp(key: FieldKeys.deletedAt, on: .delete)
|
||||||
|
var deletedAt: Date?
|
||||||
|
|
||||||
|
@Timestamp(key: FieldKeys.lastLogin, on: .none)
|
||||||
|
var lastLogin: Date?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
init(id: UUID? = nil, username: String, email: String, password: String, role: Role) {
|
||||||
|
self.id = id
|
||||||
|
self.username = username
|
||||||
|
self.email = email
|
||||||
|
self.password = password
|
||||||
|
self.role = role
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UserModel {
|
||||||
|
struct FieldKeys {
|
||||||
|
static var username: FieldKey {"username"}
|
||||||
|
static var email: FieldKey {"email"}
|
||||||
|
static var password: FieldKey {"password"}
|
||||||
|
static var role: FieldKey {"role"}
|
||||||
|
static var name: FieldKey {"name"}
|
||||||
|
static var lastname: FieldKey {"lastname"}
|
||||||
|
static var address: FieldKey {"address"}
|
||||||
|
static var zipCode: FieldKey {"zipCode"}
|
||||||
|
static var city: FieldKey {"city"}
|
||||||
|
static var country: FieldKey {"country"}
|
||||||
|
static var stripeID: FieldKey {"stripeID"}
|
||||||
|
static var createdAt: FieldKey {"createdAt"}
|
||||||
|
static var updatedAt: FieldKey {"updatedAt"}
|
||||||
|
static var deletedAt: FieldKey {"deletedAt"}
|
||||||
|
static var lastLogin: FieldKey {"lastLogin"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import Fluent
|
||||||
|
|
||||||
|
extension UserModel {
|
||||||
|
struct Migration: AsyncMigration {
|
||||||
|
|
||||||
|
func prepare(on database: any FluentKit.Database) async throws {
|
||||||
|
try await database.schema(UserModel.schema)
|
||||||
|
.id()
|
||||||
|
.field(FieldKeys.username, .string, .required)
|
||||||
|
.field(FieldKeys.email, .string, .required)
|
||||||
|
.field(FieldKeys.password, .string, .required)
|
||||||
|
.field(FieldKeys.role, .string, .required)
|
||||||
|
.field(FieldKeys.name, .string)
|
||||||
|
.field(FieldKeys.lastname, .string)
|
||||||
|
.field(FieldKeys.address, .string)
|
||||||
|
.field(FieldKeys.zipCode, .string)
|
||||||
|
.field(FieldKeys.city, .string)
|
||||||
|
.field(FieldKeys.country, .string)
|
||||||
|
.field(FieldKeys.stripeID, .string)
|
||||||
|
.field(FieldKeys.createdAt, .datetime)
|
||||||
|
.field(FieldKeys.updatedAt, .datetime)
|
||||||
|
.field(FieldKeys.deletedAt, .datetime)
|
||||||
|
.field(FieldKeys.lastLogin, .datetime)
|
||||||
|
.unique(on: FieldKeys.username)
|
||||||
|
.unique(on: FieldKeys.email)
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
func revert(on database: any FluentKit.Database) async throws {
|
||||||
|
try await database.schema(UserModel.schema).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UserModel {
|
||||||
|
|
||||||
|
struct Public: Content {
|
||||||
|
let id: UUID?
|
||||||
|
let username: String
|
||||||
|
let email: String
|
||||||
|
let role: Role
|
||||||
|
let name: String?
|
||||||
|
let lastname: String?
|
||||||
|
let address: String?
|
||||||
|
let zipCode: String?
|
||||||
|
let city: String?
|
||||||
|
let country: CountryCode?
|
||||||
|
let stripeID: String?
|
||||||
|
let createdAt: Date?
|
||||||
|
let updatedAt: Date?
|
||||||
|
let lastLogin: Date?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UserModel {
|
||||||
|
|
||||||
|
func convertToPublic() -> Public {
|
||||||
|
.init(
|
||||||
|
id: self.id,
|
||||||
|
username: self.username,
|
||||||
|
email: self.email,
|
||||||
|
role: self.role,
|
||||||
|
name: self.name,
|
||||||
|
lastname: self.lastname,
|
||||||
|
address: self.address,
|
||||||
|
zipCode: self.zipCode,
|
||||||
|
city: self.city,
|
||||||
|
country: self.Country,
|
||||||
|
stripeID: self.stripeID,
|
||||||
|
createdAt: self.createdAt,
|
||||||
|
updatedAt: self.updatedAt,
|
||||||
|
lastLogin: self.lastLogin
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Collection where Element == UserModel {
|
||||||
|
|
||||||
|
func convertToPublic() -> [UserModel.Public] {
|
||||||
|
self.map { $0.convertToPublic() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
//
|
||||||
|
// UserAuthRepository.swift
|
||||||
|
// ExodaiAcademy
|
||||||
|
//
|
||||||
|
// Created by Exodai on 25/01/2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol UserAuthRepository {
|
||||||
|
func findByEmail(_ email: String) async throws -> UserModel?
|
||||||
|
func findByUsername(_ username: String) async throws -> UserModel?
|
||||||
|
func create(
|
||||||
|
username: String,
|
||||||
|
email: String,
|
||||||
|
passwordHash: String,
|
||||||
|
role: Role
|
||||||
|
) async throws -> UserModel
|
||||||
|
func updatePassword(userID: UUID, passwordHash: String) async throws
|
||||||
|
func delete(userID: UUID) async throws
|
||||||
|
}
|
||||||
|
|
||||||
|
import Fluent
|
||||||
|
import Vapor
|
||||||
|
|
||||||
|
struct FluentUserAuthRepository: UserAuthRepository {
|
||||||
|
let db: any FluentKit.Database
|
||||||
|
|
||||||
|
func findByEmail(_ email: String) async throws -> UserModel? {
|
||||||
|
try await UserModel.query(on: db)
|
||||||
|
.filter(\.$email == email)
|
||||||
|
.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
func findByUsername(_ username: String) async throws -> UserModel? {
|
||||||
|
try await UserModel.query(on: db)
|
||||||
|
.filter(\.$username == username)
|
||||||
|
.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
func create(username: String, email: String, passwordHash: String, role: Role) async throws -> UserModel {
|
||||||
|
let user = UserModel(
|
||||||
|
username: username,
|
||||||
|
email: email,
|
||||||
|
password: passwordHash,
|
||||||
|
role: role
|
||||||
|
)
|
||||||
|
|
||||||
|
try await user.create(on: db)
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
func updatePassword(userID: UUID, passwordHash: String) async throws {
|
||||||
|
guard let user = try await UserModel.find(userID, on: db) else {
|
||||||
|
throw Abort(.notFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
user.password = passwordHash
|
||||||
|
try await user.save(on: db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func delete(userID: UUID) async throws {
|
||||||
|
try await UserModel.find(userID, on: db)?
|
||||||
|
.delete(on: db)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
//
|
||||||
|
// UserRepository.swift
|
||||||
|
// ExodaiAcademy
|
||||||
|
//
|
||||||
|
// Created by Exodai on 25/01/2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol UserRepository {
|
||||||
|
func find(id: UUID) async throws -> UserModel.Public?
|
||||||
|
func findByEmail(_ email: String) async throws -> UserModel.Public?
|
||||||
|
func findByUsername(_ username: String) async throws -> UserModel.Public?
|
||||||
|
func all() async throws -> [UserModel.Public]
|
||||||
|
}
|
||||||
|
|
||||||
|
import Fluent
|
||||||
|
|
||||||
|
struct FluentUserRepository: UserRepository {
|
||||||
|
let db: any FluentKit.Database
|
||||||
|
|
||||||
|
func find(id: UUID) async throws -> UserModel.Public? {
|
||||||
|
try await UserModel.find(id, on: db)?
|
||||||
|
.convertToPublic()
|
||||||
|
}
|
||||||
|
|
||||||
|
func findByEmail(_ email: String) async throws -> UserModel.Public? {
|
||||||
|
try await UserModel.query(on: db)
|
||||||
|
.filter(\.$email == email)
|
||||||
|
.first()?
|
||||||
|
.convertToPublic()
|
||||||
|
}
|
||||||
|
|
||||||
|
func findByUsername(_ username: String) async throws -> UserModel.Public? {
|
||||||
|
try await UserModel.query(on: db)
|
||||||
|
.filter(\.$username == username)
|
||||||
|
.first()?
|
||||||
|
.convertToPublic()
|
||||||
|
}
|
||||||
|
|
||||||
|
func all() async throws -> [UserModel.Public] {
|
||||||
|
try await UserModel.query(on: db)
|
||||||
|
.all()
|
||||||
|
.convertToPublic()
|
||||||
|
}
|
||||||
|
}
|
||||||
6
Sources/ExodaiAcademy/Router/routes.swift
Normal file
6
Sources/ExodaiAcademy/Router/routes.swift
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import Fluent
|
||||||
|
import Vapor
|
||||||
|
|
||||||
|
func routes(_ app: Application) throws {
|
||||||
|
|
||||||
|
}
|
||||||
23
Sources/ExodaiAcademy/configure.swift
Normal file
23
Sources/ExodaiAcademy/configure.swift
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import NIOSSL
|
||||||
|
import Fluent
|
||||||
|
import FluentPostgresDriver
|
||||||
|
import Vapor
|
||||||
|
|
||||||
|
// configures your application
|
||||||
|
public func configure(_ app: Application) async throws {
|
||||||
|
// uncomment to serve files from /Public folder
|
||||||
|
// app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
|
||||||
|
|
||||||
|
app.databases.use(DatabaseConfigurationFactory.postgres(configuration: .init(
|
||||||
|
hostname: Environment.get("DATABASE_HOST") ?? "localhost",
|
||||||
|
port: Environment.get("DATABASE_PORT").flatMap(Int.init(_:)) ?? SQLPostgresConfiguration.ianaPortNumber,
|
||||||
|
username: Environment.get("DATABASE_USERNAME") ?? "vapor_username",
|
||||||
|
password: Environment.get("DATABASE_PASSWORD") ?? "vapor_password",
|
||||||
|
database: Environment.get("DATABASE_NAME") ?? "vapor_database",
|
||||||
|
tls: .prefer(try .init(configuration: .clientDefault)))
|
||||||
|
), as: .psql)
|
||||||
|
|
||||||
|
|
||||||
|
// register routes
|
||||||
|
try routes(app)
|
||||||
|
}
|
||||||
31
Sources/ExodaiAcademy/entrypoint.swift
Normal file
31
Sources/ExodaiAcademy/entrypoint.swift
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import Vapor
|
||||||
|
import Logging
|
||||||
|
import NIOCore
|
||||||
|
import NIOPosix
|
||||||
|
|
||||||
|
@main
|
||||||
|
enum Entrypoint {
|
||||||
|
static func main() async throws {
|
||||||
|
var env = try Environment.detect()
|
||||||
|
try LoggingSystem.bootstrap(from: &env)
|
||||||
|
|
||||||
|
let app = try await Application.make(env)
|
||||||
|
|
||||||
|
// This attempts to install NIO as the Swift Concurrency global executor.
|
||||||
|
// You can enable it if you'd like to reduce the amount of context switching between NIO and Swift Concurrency.
|
||||||
|
// Note: this has caused issues with some libraries that use `.wait()` and cleanly shutting down.
|
||||||
|
// If enabled, you should be careful about calling async functions before this point as it can cause assertion failures.
|
||||||
|
// let executorTakeoverSuccess = NIOSingletons.unsafeTryInstallSingletonPosixEventLoopGroupAsConcurrencyGlobalExecutor()
|
||||||
|
// app.logger.debug("Tried to install SwiftNIO's EventLoopGroup as Swift's global concurrency executor", metadata: ["success": .stringConvertible(executorTakeoverSuccess)])
|
||||||
|
|
||||||
|
do {
|
||||||
|
try await configure(app)
|
||||||
|
try await app.execute()
|
||||||
|
} catch {
|
||||||
|
app.logger.report(error: error)
|
||||||
|
try? await app.asyncShutdown()
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
try await app.asyncShutdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
23
Sources/Extensions/RandomConfirmationCode.swift
Normal file
23
Sources/Extensions/RandomConfirmationCode.swift
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// RandomConfirmationCode.swift
|
||||||
|
// ExodaiAcademy
|
||||||
|
//
|
||||||
|
// Created by Exodai on 23/01/2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
static func randomConfirmationCode(length: Int = 8) -> String {
|
||||||
|
let characters = Array("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||||
|
var result = ""
|
||||||
|
result.reserveCapacity(length)
|
||||||
|
|
||||||
|
for _ in 0..<length {
|
||||||
|
let index = Int.random(in: 0..<characters.count)
|
||||||
|
result.append(characters[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
84
Tests/ExodaiAcademyTests/ExodaiAcademyTests.swift
Normal file
84
Tests/ExodaiAcademyTests/ExodaiAcademyTests.swift
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
@testable import ExodaiAcademy
|
||||||
|
import VaporTesting
|
||||||
|
import Testing
|
||||||
|
import Fluent
|
||||||
|
|
||||||
|
@Suite("App Tests with DB", .serialized)
|
||||||
|
struct ExodaiAcademyTests {
|
||||||
|
private func withApp(_ test: (Application) async throws -> ()) async throws {
|
||||||
|
let app = try await Application.make(.testing)
|
||||||
|
do {
|
||||||
|
try await configure(app)
|
||||||
|
try await app.autoMigrate()
|
||||||
|
try await test(app)
|
||||||
|
try await app.autoRevert()
|
||||||
|
} catch {
|
||||||
|
try? await app.autoRevert()
|
||||||
|
try await app.asyncShutdown()
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
try await app.asyncShutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Test Hello World Route")
|
||||||
|
func helloWorld() async throws {
|
||||||
|
try await withApp { app in
|
||||||
|
try await app.testing().test(.GET, "hello", afterResponse: { res async in
|
||||||
|
#expect(res.status == .ok)
|
||||||
|
#expect(res.body.string == "Hello, world!")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Getting all the Todos")
|
||||||
|
func getAllTodos() async throws {
|
||||||
|
try await withApp { app in
|
||||||
|
let sampleTodos = [Todo(title: "sample1"), Todo(title: "sample2")]
|
||||||
|
try await sampleTodos.create(on: app.db)
|
||||||
|
|
||||||
|
try await app.testing().test(.GET, "todos", afterResponse: { res async throws in
|
||||||
|
#expect(res.status == .ok)
|
||||||
|
#expect(try
|
||||||
|
res.content.decode([TodoDTO].self).sorted(by: { ($0.title ?? "") < ($1.title ?? "") }) ==
|
||||||
|
sampleTodos.map { $0.toDTO() }.sorted(by: { ($0.title ?? "") < ($1.title ?? "") })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Creating a Todo")
|
||||||
|
func createTodo() async throws {
|
||||||
|
let newDTO = TodoDTO(id: nil, title: "test")
|
||||||
|
|
||||||
|
try await withApp { app in
|
||||||
|
try await app.testing().test(.POST, "todos", beforeRequest: { req in
|
||||||
|
try req.content.encode(newDTO)
|
||||||
|
}, afterResponse: { res async throws in
|
||||||
|
#expect(res.status == .ok)
|
||||||
|
let models = try await Todo.query(on: app.db).all()
|
||||||
|
#expect(models.map({ $0.toDTO().title }) == [newDTO.title])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Deleting a Todo")
|
||||||
|
func deleteTodo() async throws {
|
||||||
|
let testTodos = [Todo(title: "test1"), Todo(title: "test2")]
|
||||||
|
|
||||||
|
try await withApp { app in
|
||||||
|
try await testTodos.create(on: app.db)
|
||||||
|
|
||||||
|
try await app.testing().test(.DELETE, "todos/\(testTodos[0].requireID())", afterResponse: { res async throws in
|
||||||
|
#expect(res.status == .noContent)
|
||||||
|
let model = try await Todo.find(testTodos[0].id, on: app.db)
|
||||||
|
#expect(model == nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TodoDTO: Equatable {
|
||||||
|
public static func == (lhs: Self, rhs: Self) -> Bool {
|
||||||
|
lhs.id == rhs.id && lhs.title == rhs.title
|
||||||
|
}
|
||||||
|
}
|
||||||
72
docker-compose.yml
Normal file
72
docker-compose.yml
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# Docker Compose file for Vapor
|
||||||
|
#
|
||||||
|
# Install Docker on your system to run and test
|
||||||
|
# your Vapor app in a production-like environment.
|
||||||
|
#
|
||||||
|
# Note: This file is intended for testing and does not
|
||||||
|
# implement best practices for a production deployment.
|
||||||
|
#
|
||||||
|
# Learn more: https://docs.docker.com/compose/reference/
|
||||||
|
#
|
||||||
|
# Build images: docker compose build
|
||||||
|
# Start app: docker compose up app
|
||||||
|
# Start database: docker compose up db
|
||||||
|
# Run migrations: docker compose run migrate
|
||||||
|
# Stop all: docker compose down (add -v to wipe db)
|
||||||
|
#
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db_data:
|
||||||
|
|
||||||
|
x-shared_environment: &shared_environment
|
||||||
|
LOG_LEVEL: ${LOG_LEVEL:-debug}
|
||||||
|
DATABASE_HOST: db
|
||||||
|
DATABASE_NAME: &db_name vapor_database
|
||||||
|
DATABASE_USERNAME: &db_username vapor_username
|
||||||
|
DATABASE_PASSWORD: &db_password vapor_password
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: exodai-academy:latest
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
environment:
|
||||||
|
<<: *shared_environment
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
ports:
|
||||||
|
- '8080:8080'
|
||||||
|
# user: '0' # uncomment to run as root for testing purposes even though Dockerfile defines 'vapor' user.
|
||||||
|
command: ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]
|
||||||
|
migrate:
|
||||||
|
image: exodai-academy:latest
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
environment:
|
||||||
|
<<: *shared_environment
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
command: ["migrate", "--yes"]
|
||||||
|
deploy:
|
||||||
|
replicas: 0
|
||||||
|
revert:
|
||||||
|
image: exodai-academy:latest
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
environment:
|
||||||
|
<<: *shared_environment
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
command: ["migrate", "--revert", "--yes"]
|
||||||
|
deploy:
|
||||||
|
replicas: 0
|
||||||
|
db:
|
||||||
|
image: postgres:18-alpine
|
||||||
|
volumes:
|
||||||
|
- db_data:/var/lib/postgresql
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: *db_username
|
||||||
|
POSTGRES_PASSWORD: *db_password
|
||||||
|
POSTGRES_DB: *db_name
|
||||||
|
ports:
|
||||||
|
- '5432:5432'
|
||||||
Reference in New Issue
Block a user