first commit

This commit is contained in:
Nimer Farahty 2025-05-31 18:38:18 +03:00
commit f729f0c48f
48 changed files with 10744 additions and 0 deletions

4
.dockerignore Normal file
View File

@ -0,0 +1,4 @@
tmp
go-mongo
bin
.env

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
tmp
go-mongo
bin
.env

1
.vscode/configurationCache.log vendored Normal file
View File

@ -0,0 +1 @@
{"buildTargets":["build","clean","dev","format","hello","run","simplify","start"],"launchTargets":[],"customConfigurationProvider":{"workspaceBrowse":{"browsePath":[],"compilerArgs":[]},"fileIndex":[]}}

6
.vscode/dryrun.log vendored Normal file
View File

@ -0,0 +1,6 @@
make --dry-run --always-make --keep-going --print-directory
make: Entering directory `/Users/nimer/Projects/go-mongo'
echo 'bin/app'
echo -ldflags "-X=main.Version=1.0.0 -X=main.Build=`git rev-parse HEAD`"
make: Leaving directory `/Users/nimer/Projects/go-mongo'

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"nuxt.isNuxtApp": false
}

382
.vscode/targets.log vendored Normal file
View File

@ -0,0 +1,382 @@
make all --print-data-base --no-builtin-variables --no-builtin-rules --question
# GNU Make 3.81
# Copyright (C) 2006 Free Software Foundation, Inc.
# This is free software; see the source for copying conditions.
# There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE.
# This program built for i386-apple-darwin11.3.0
make: *** No rule to make target `all'. Stop.
# Make data base, printed on Fri Aug 5 15:34:35 2022
# Variables
# automatic
<D = $(patsubst %/,%,$(dir $<))
# automatic
?F = $(notdir $?)
# environment
ELECTRON_NO_ATTACH_CONSOLE = 1
# makefile (from `makefile', line 15)
SRC = $(shell find . -type f -name '*.go' -not -path "./vendor/*")
# environment
LC_CTYPE = UTF-8
# makefile (from `makefile', line 4)
TARGET := 'bin/app'
# automatic
?D = $(patsubst %/,%,$(dir $?))
# environment
DIRENV_WATCHES = eJxszr1OBCEQAOB3mXq94Xc4trc0ucbKWAA7e4dhIQE8TYzvbm_uBb58bz9wCfMGK-Dr4D6w5oM7Xnr74DQHXtvT0eq14YnrHRZ4advMB8MqyUpvzo7cAs_fecwB6-yf_Ls8Fk-lpVBw3EJn3HLnesdQSvvCSOS0jzZFHU0ySSiObtuN1sqqKISPRImUUYG8S9Jp8tIKK8TOQqk9PFidPf1bvf8FAAD__2AjSHk=
# automatic
@D = $(patsubst %/,%,$(dir $@))
# automatic
@F = $(notdir $@)
# makefile
CURDIR := /Users/nimer/Projects/go-mongo
# makefile (from `makefile', line 1)
SHELL := /bin/bash
# environment
VSCODE_NLS_CONFIG = {"locale":"en-us","availableLanguages":{},"_languagePackSupport":true}
# environment
_ = /usr/bin/make
# environment
DIRENV_FILE = /Users/nimer/Projects/go-mongo/.env
# environment
MONGO_URI = mongodb+srv://tutulala:20210810@maincluster.lok3f.mongodb.net/go-playground?retryWrites=true&w=majority
# makefile (from `makefile', line 1)
MAKEFILE_LIST := makefile
# environment
VSCODE_AMD_ENTRYPOINT = vs/workbench/api/node/extensionHostProcess
# environment
ACCESS_SECRET = fwerfwefwegrhrgreg9879rw7eg9w9reg9hw9reug9nweg
# makefile (from `makefile', line 8)
VERSION := 1.0.0
# environment
__CFBundleIdentifier = com.microsoft.VSCode
# environment
INFOPATH = /opt/homebrew/share/info:
# environment
P9K_SSH = 0
# environment
DIRENV_DIFF = eJx8kl9zmlAQxb_LvhblqiQCM5mWfwZiFAVNNNMZRmDBP3DRy4VbzeS7d-y0sX3p086ePWcfzvze4Qj6-4cEFPR3MCzLCcPIWc28YA06DEgJ0h81dKzAWYAOmUCWCcwE5mzLcoa5pg41JoaYa0K7rtvraHKNCsxBAtsLnOlLZHsB6NCRlzWyWqa7Epk8Y9UeE17LedUpK5pXN_vIe3ZAh__b5S7S9pZ5NRaW64SgAz79qC-s55uWMzd8c1CuTpqyShTOEtIk8fjVMIaopFtv7qiL9aWNy6gx4zs1vmjCyiaP447NFPte3FWT4YqyoWLP3RVvFwRPPWVNmRu8KZu0nZjq5HReHttLNUyMOsowEeZ955xFz7U6OnSK44spBs4THbjPFOt0HratFfoh2V_eRu6SnMNw58e8mfbq02nshbPAK5fL9aMaak9HlXvjsbrw56eDNhvt0lnWi9tMHRmGHUV9Yx-6hweQwJm-gA4ptlhUxxIpBwkm_vTRj2wTdMirzrHYnHNWNTT9PC0DD3T41WEaf6lZq8syb3hTbIqN3if9HlF75Fu52dGkaGqOrFtUh0HW_Z3oUuTyP5-_MuTs_Mp2HOsHzhr83hDSvxcP5WZfsR0_gwQzP7gCpBBCQILAGQVO6N5wU7Z_qZ-4YYbiihsRmUo0oWYEVSKyEuHj42cAAAD__2TO5Tg=
# environment
VSCODE_CWD = /Users/nimer/Projects/go-mongo
# environment
GOPROXY = https://proxy.golang.org,direct
# environment
PATH = /opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Applications/VMware Fusion Tech Preview.app/Contents/Public:/usr/local/share/dotnet:~/.dotnet/tools:/Library/Apple/usr/bin:/Library/Frameworks/Mono.framework/Versions/Current/Commands:/Users/nimer/.cargo/bin:/Users/nimer/.fig/bin:/Users/nimer/.local/bin:/Users/nimer/Library/Application Support/JetBrains/Toolbox/scripts:/Users/nimer/Library/Android/sdk/emulator:/Users/nimer/Library/Android/sdk/platform-tools:/Users/nimer/.local/bin:/Users/nimer/go/bin
# environment
LSCOLORS = Gxfxcxdxbxegedabagacad
# environment
GOPATH = /Users/nimer/go
# environment
LaunchInstanceID = 51D3B68F-6A57-4DF5-A264-9514A08AEE59
# environment
ELECTRON_RUN_AS_NODE = 1
# makefile (from `makefile', line 12)
LDFLAGS = -ldflags "-X=main.Version=$(VERSION) -X=main.Build=$(BUILD)"
# default
.FEATURES := target-specific order-only second-expansion else-if archives jobserver check-symlink
# environment
SSH_AUTH_SOCK = /private/tmp/com.apple.launchd.sO1jiABdEa/Listeners
# automatic
%F = $(notdir $%)
# environment
TTY = not a tty
# environment
FIG_PID = 1717
# environment
PWD = /Users/nimer/Projects/go-mongo
# environment
HOMEBREW_CELLAR = /opt/homebrew/Cellar
# environment
ORIGINAL_XDG_CURRENT_DESKTOP = undefined
# environment
MANPATH = /opt/homebrew/share/man::
# environment
GOMODCACHE = /Users/nimer/go/pkg/mod
# environment
HOME = /Users/nimer
# default
MAKEFILEPATH := /Applications/Xcode.app/Contents/Developer/Makefiles
# environment
VSCODE_CLI = 1
# environment
VSCODE_CODE_CACHE_PATH = /Users/nimer/Library/Application Support/Code/CachedData/da76f93349a72022ca4670c1b84860304616aaa2
# environment
LOGNAME = nimer
# environment
APPLICATION_INSIGHTS_NO_DIAGNOSTIC_CHANNEL = 1
# environment
ZSH = /Users/nimer/.local/share/fig/plugins/ohmyzsh
# environment
VSCODE_HANDLES_UNCAUGHT_ERRORS = true
# automatic
^D = $(patsubst %/,%,$(dir $^))
# environment
XPC_FLAGS = 0x0
# environment
COLORTERM = truecolor
# default
MAKE = $(MAKE_COMMAND)
# environment
MONGO_DB = go-playground
# environment
LC_TERMINAL = iTerm2
# environment
DIRENV_DIR = -/Users/nimer/Projects/go-mongo
# environment
SHLVL = 3
# environment
FIG_TERM = 1
# default
MAKE_VERSION := 3.81
# environment
USER = nimer
# environment
FIG_TERM_VERSION = 4.4.2
# default
MAKECMDGOALS := all
# environment
TERM_SESSION_ID = w0t0p0:7D9739A3-C132-4705-9E22-CC1AF54494E5
# environment
LESS = -R
# automatic
%D = $(patsubst %/,%,$(dir $%))
# environment
FIG_INTEGRATION_VERSION = 8
# environment
PORT = 4000
# environment
REFRESH_EXPIRY = 4h
# environment
TERM_PROGRAM = iTerm.app
# default
.VARIABLES :=
# environment
TMPDIR = /var/folders/d5/2z76mds52290hxy7vp0d40480000gn/T/
# automatic
*F = $(notdir $*)
# environment
VSCODE_IPC_HOOK = /Users/nimer/Library/Application Support/Code/1.70.0-main.sock
# environment
DB_PASSWORD = 20210810
# environment
MallocNanoZone = 0
# makefile
MAKEFLAGS = Rrqp
# environment
MFLAGS = -Rrqp
# automatic
*D = $(patsubst %/,%,$(dir $*))
# environment
TERM_PROGRAM_VERSION = 3.4.16
# environment
XPC_SERVICE_NAME = application.com.microsoft.VSCode.23980001.23980007.AC55BB20-6465-4F2A-8E09-A1D51E0B4219
# environment
LC_TERMINAL_VERSION = 3.4.16
# environment
HOMEBREW_PREFIX = /opt/homebrew
# automatic
+D = $(patsubst %/,%,$(dir $+))
# automatic
+F = $(notdir $+)
# environment
ITERM_SESSION_ID = w0t0p0:7D9739A3-C132-4705-9E22-CC1AF54494E5
# environment
HOMEBREW_REPOSITORY = /opt/homebrew
# environment
COLORFGBG = 15;0
# default
MAKE_COMMAND := /Applications/Xcode.app/Contents/Developer/usr/bin/make
# environment
__CF_USER_TEXT_ENCODING = 0x1F5:0x0:0x0
# environment
COMMAND_MODE = unix2003
# default
MAKEFILES :=
# makefile (from `makefile', line 9)
BUILD := `git rev-parse HEAD`
# automatic
<F = $(notdir $<)
# environment
ITERM_PROFILE = Default
# environment
PAGER = less
# environment
ANDROID_SDK_ROOT = /Users/nimer/Library/Android/sdk
# environment
LC_ALL = C
# environment
REFRESH_SECRET = efewfwef0wf809w8f0e80wfme
# environment
SECURITYSESSIONID = 186a5
# environment
P9K_TTY = old
# automatic
^F = $(notdir $^)
# default
SUFFIXES :=
# environment
MAKELEVEL := 0
# makefile
.DEFAULT_GOAL := hello
# environment
ENV = development
# environment
LANG = C
# environment
TERM = xterm-256color
# environment
_P9K_TTY = not a tty
# environment
VSCODE_PID = 2509
# environment
ACCESS_EXPIRY = 30m
# variable set hash-table stats:
# Load=111/1024=11%, Rehash=0, Collisions=8/136=6%
# Pattern-specific Variable Values
# No pattern-specific variable values.
# Directories
# . (device 16777229, inode 6481395): 25 files, no impossibilities.
# 25 files, no impossibilities in 1 directories.
# Implicit Rules
# No implicit rules.
# Files
# Not a target:
all:
# Command-line target.
# Implicit rule search has been done.
# File does not exist.
# File has not been updated.
# variable set hash-table stats:
# Load=0/32=0%, Rehash=0, Collisions=0/0=0%
# Not a target:
.SUFFIXES:
# Implicit rule search has not been done.
# Modification time never checked.
# File has not been updated.
format:
# Implicit rule search has not been done.
# Modification time never checked.
# File has not been updated.
# commands to execute (from `makefile', line 40):
@gofmt -l -w $(SRC)
simplify:
# Implicit rule search has not been done.
# Modification time never checked.
# File has not been updated.
# commands to execute (from `makefile', line 43):
@gofmt -s -l -w $(SRC)
start: build
# Implicit rule search has not been done.
# Modification time never checked.
# File has not been updated.
# commands to execute (from `makefile', line 33):
@./bin/app
# Not a target:
makefile:
# Implicit rule search has been done.
# Last modified 2022-08-05 15:32:50
# File has been updated.
# Successfully updated.
# variable set hash-table stats:
# Load=0/32=0%, Rehash=0, Collisions=0/0=0%
build:
# Implicit rule search has not been done.
# Modification time never checked.
# File has not been updated.
# commands to execute (from `makefile', line 24):
@go build $(LDFLAGS) -o bin/app .
hello:
# Implicit rule search has not been done.
# Modification time never checked.
# File has not been updated.
# commands to execute (from `makefile', line 19):
@echo $(TARGET)
@echo $(LDFLAGS)
# Not a target:
'bin/app':
# Implicit rule search has not been done.
# Modification time never checked.
# File has not been updated.
dev:
# Implicit rule search has not been done.
# Modification time never checked.
# File has not been updated.
# commands to execute (from `makefile', line 30):
@air
# Not a target:
.DEFAULT:
# Implicit rule search has not been done.
# Modification time never checked.
# File has not been updated.
.DEFAULT_GOAL: 'bin/app'
# Implicit rule search has not been done.
# Modification time never checked.
# File has not been updated.
run:
# Implicit rule search has not been done.
# Modification time never checked.
# File has not been updated.
# commands to execute (from `makefile', line 27):
@go run .
clean:
# Implicit rule search has not been done.
# Modification time never checked.
# File has not been updated.
# commands to execute (from `makefile', line 36):
@rm -rf bin
@rm -rf tmp
# files hash-table stats:
# Load=14/1024=1%, Rehash=0, Collisions=0/28=0%
# VPATH Search Paths
# No `vpath' search paths.
# No general (`VPATH' variable) search path.
# # of strings in strcache: 1
# # of strcache buffers: 1
# strcache size: total = 4096 / max = 4096 / min = 4096 / avg = 4096
# strcache free: total = 4087 / max = 4087 / min = 4087 / avg = 4087
# Finished Make data base on Fri Aug 5 15:34:35 2022

37
Dockerfile Normal file
View File

@ -0,0 +1,37 @@
# ---------- Build Stage ----------
FROM golang:1.24-alpine AS builder
# Set working directory
WORKDIR /app
# Install dependencies
RUN apk add --no-cache git
# Cache and install Go modules
COPY go.mod go.sum ./
RUN go mod download
# Copy the source code
COPY . .
# Build the Go binary
RUN go build -o server .
# ---------- Final Stage ----------
FROM alpine:latest
# Install timezone data (optional)
RUN apk add --no-cache tzdata ca-certificates
# Set working directory
WORKDIR /root/
# Copy binary from builder
COPY --from=builder /app/server .
# Copy binary from builder
COPY --from=builder /app/model.conf .
# Run the binary
CMD ["./server"]

54
app/app-context.go Normal file
View File

@ -0,0 +1,54 @@
package app
import (
"context"
"fmt"
"github.com/farahty/go-mongo/models"
)
type contextKey struct {
name string
}
var (
UserKey = &contextKey{"user"}
StatusKey = &contextKey{"status"}
ExpiryKey = &contextKey{"expiry"}
LoadersKey = &contextKey{"dataloaders"}
)
// Retrieves the current user from the context
func CurrentUser(ctx context.Context) (*models.UserJWT, error) {
user, _ := ctx.Value(UserKey).(*models.UserJWT)
status, _ := ctx.Value(StatusKey).(string)
if status != "ok" {
return nil, fmt.Errorf("%s", status)
}
return user, nil
}
// Check if the token was marked as expired
func IsTokenExpired(ctx context.Context) bool {
if expired, ok := ctx.Value(ExpiryKey).(bool); ok {
return expired
}
return false
}
// Sets the status in context (e.g., "ok" or error message)
func SetStatus(ctx context.Context, data any) context.Context {
return context.WithValue(ctx, StatusKey, data)
}
// Sets the authenticated user into the context
func SetCurrentUser(ctx context.Context, user *models.UserJWT) context.Context {
return context.WithValue(ctx, UserKey, user)
}
// Marks the token as expired in the context
func SetTokenExpired(ctx context.Context, expiry bool) context.Context {
return context.WithValue(ctx, ExpiryKey, expiry)
}

134
app/auth.go Normal file
View File

@ -0,0 +1,134 @@
package app
import (
"context"
"errors"
"fmt"
"log"
"os"
"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/casbin/casbin/v2"
mongodbadapter "github.com/casbin/mongodb-adapter/v3"
"github.com/farahty/go-mongo/models"
"github.com/golang-jwt/jwt/v4"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
var (
Authorizer *casbin.Enforcer
)
func LoadAuthorizer(ctx context.Context) error {
a, err := mongodbadapter.NewAdapterWithClientOption(options.Client().ApplyURI(os.Getenv("MONGO_URI")), os.Getenv("MONGO_DB"))
if err != nil {
return err
}
if enforcer, err := casbin.NewEnforcer("./model.conf", a); err != nil {
return err
} else {
Authorizer = enforcer
}
a.AddPolicy("mongodb", "p", []string{"*", "login", "mutation"})
a.AddPolicy("mongodb", "p", []string{"todos-admin", "createTodo", "mutation"})
a.AddPolicy("mongodb", "p", []string{"todos-admin", "todos", "query"})
a.AddPolicy("mongodb", "p", []string{"todos-admin", "todo", "query"})
a.AddPolicy("mongodb", "p", []string{"category-admin", "createCategory", "mutation"})
a.AddPolicy("mongodb", "p", []string{"category-admin", "categories", "query"})
a.AddPolicy("mongodb", "p", []string{"category-admin", "category", "query"})
a.AddPolicy("mongodb", "p", []string{"users-admin", "users", "query"})
a.AddPolicy("mongodb", "p", []string{"users-admin", "createUser", "mutation"})
Authorizer.AddGroupingPolicy("admin", "users-admin")
Authorizer.AddGroupingPolicy("admin", "todos-admin")
Authorizer.AddGroupingPolicy("admin", "category-admin")
email := os.Getenv("ADMIN_EMAIL")
password, _ := models.MakeHash(os.Getenv("ADMIN_PASSWORD"))
if _, err := FindOne[models.User](ctx, "users", bson.M{"email": email}); err != nil {
log.Println("Creating admin user")
admin, err := InsertOne[models.User](ctx, "users", models.User{
Password: &password,
Email: &email,
})
if err != nil {
return fmt.Errorf("error creating admin user: %w", err)
}
Authorizer.AddRoleForUser(admin.ID.Hex(), "admin")
}
return nil
}
func AuthorizeWebSocket(ctx context.Context, initPayload transport.InitPayload) (context.Context, *transport.InitPayload, error) {
var token string
var ok bool
if token, ok = initPayload["token"].(string); !ok {
return ctx, &initPayload, nil
}
user, err := getUserFromToken(token)
if err != nil {
ctx = SetStatus(ctx, err.Error())
if errors.Is(err, jwt.ErrTokenExpired) {
ctx = SetTokenExpired(ctx, true)
}
return ctx, &initPayload, nil
}
ctx = SetCurrentUser(ctx, user)
ctx = SetStatus(ctx, "ok")
return ctx, &initPayload, nil
}
func RootFieldsAuthorizer(ctx context.Context, next graphql.RootResolver) graphql.Marshaler {
if err := AuthorizeOperation(ctx); err != nil {
graphql.AddError(ctx, err)
return graphql.Null
}
return next(ctx)
}
func AuthorizeOperation(ctx context.Context) error {
user := "Anonymous"
object := graphql.GetRootFieldContext(ctx).Field.Name
action := string(graphql.GetOperationContext(ctx).Operation.Operation)
if object == "__type" {
return nil
}
if obj, err := CurrentUser(ctx); err == nil {
user = string(obj.ID)
}
if allowed, err := Authorizer.Enforce(user, object, action); err != nil {
return fmt.Errorf("error while enforcing user roles \n%s", err.Error())
} else if !allowed {
return fmt.Errorf("user %s is not allowed to access %s %s", user, action, object)
}
return nil
}

183
app/database.go Normal file
View File

@ -0,0 +1,183 @@
package app
import (
"context"
"fmt"
"os"
"time"
"github.com/fatih/color"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
var (
Mongo *mongo.Client
)
// Connect to MongoDB and return a cancel function
func Connect() (context.CancelFunc, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
var err error
Mongo, err = mongo.Connect(ctx, options.Client().ApplyURI(os.Getenv("MONGO_URI")))
if err != nil {
color.Red("❌ Database connection failed\n" + err.Error())
return cancel, err
}
if err = Mongo.Ping(ctx, nil); err != nil {
color.Red("❌ Connection Failed to Database")
color.Red(err.Error())
return cancel, err
}
color.Green("✅ Connected to MongoDB")
return cancel, nil
}
// Disconnect cleanly from MongoDB
func Disconnect(ctx context.Context) error {
return Mongo.Disconnect(ctx)
}
// Collection returns a MongoDB collection
func Collection(name string) *mongo.Collection {
return Mongo.Database(os.Getenv("MONGO_DB")).Collection(name)
}
// Find returns all matching documents from a collection
func Find[T any](ctx context.Context, coll string, filter any) ([]*T, error) {
var results []*T
cursor, err := Collection(coll).Find(ctx, filter)
if err != nil {
return nil, fmt.Errorf("error while finding documents in %s: %w", coll, err)
}
if err := cursor.All(ctx, &results); err != nil {
return nil, fmt.Errorf("error while parsing documents in %s: %w", coll, err)
}
return results, nil
}
// FindOne finds a single document from a collection
func FindOne[T any](ctx context.Context, coll string, filter any) (*T, error) {
var result T
err := Collection(coll).FindOne(ctx, filter).Decode(&result)
if err == mongo.ErrNoDocuments {
return nil, fmt.Errorf("no data found")
}
if err != nil {
return nil, fmt.Errorf("findOne error: %w", err)
}
return &result, nil
}
// FindById finds a document by its ObjectID
func FindById[T any](ctx context.Context, coll string, id primitive.ObjectID) (*T, error) {
var result T
err := Collection(coll).FindOne(ctx, bson.M{"_id": id}).Decode(&result)
if err == mongo.ErrNoDocuments {
return nil, fmt.Errorf("no data found")
}
if err != nil {
return nil, fmt.Errorf("findById error: %w", err)
}
return &result, nil
}
// InsertOne inserts a new document and returns it
func InsertOne[T any](ctx context.Context, coll string, input any) (*T, error) {
now := time.Now()
// Marshal + Unmarshal to apply bson tags
data, err := bson.Marshal(input)
if err != nil {
return nil, fmt.Errorf("marshal error: %w", err)
}
var doc bson.M
if err := bson.Unmarshal(data, &doc); err != nil {
return nil, fmt.Errorf("unmarshal error: %w", err)
}
doc["createdAt"] = now
doc["updatedAt"] = now
if user, err := CurrentUser(ctx); err == nil {
if id, err := primitive.ObjectIDFromHex(user.ID); err == nil {
doc["createdById"] = id
doc["updatedById"] = id
doc["ownerId"] = id
}
}
res, err := Collection(coll).InsertOne(ctx, doc)
if err != nil {
return nil, fmt.Errorf("insert error: %w", err)
}
var inserted T
err = Collection(coll).FindOne(ctx, bson.M{"_id": res.InsertedID}).Decode(&inserted)
if err != nil {
return nil, fmt.Errorf("fetch inserted document error: %w", err)
}
return &inserted, nil
}
// UpdateByID updates a document and returns the new version
func UpdateByID[T any](ctx context.Context, coll string, id string, input any) (*T, error) {
docID, err := primitive.ObjectIDFromHex(id)
if err != nil {
return nil, fmt.Errorf("invalid ID format: %w", err)
}
now := time.Now()
data, err := bson.Marshal(input)
if err != nil {
return nil, fmt.Errorf("marshal error: %w", err)
}
var updateData bson.M
if err := bson.Unmarshal(data, &updateData); err != nil {
return nil, fmt.Errorf("unmarshal error: %w", err)
}
updateData["updatedAt"] = now
if user, err := CurrentUser(ctx); err == nil {
if userID, err := primitive.ObjectIDFromHex(user.ID); err == nil {
updateData["updatedById"] = userID
}
}
res, err := Collection(coll).UpdateByID(ctx, docID, bson.M{"$set": updateData})
if err != nil {
return nil, fmt.Errorf("update error: %w", err)
}
if res.MatchedCount == 0 {
return nil, fmt.Errorf("no document found with ID %s", id)
}
if res.ModifiedCount == 0 {
color.Yellow("⚠️ Document with ID %s was found but not modified", id)
}
var updated T
err = Collection(coll).FindOne(ctx, bson.M{"_id": docID}).Decode(&updated)
if err != nil {
return nil, fmt.Errorf("error fetching updated document: %w", err)
}
return &updated, nil
}

53
app/helpers.go Normal file
View File

@ -0,0 +1,53 @@
package app
import (
"fmt"
"os"
"strings"
"github.com/farahty/go-mongo/models"
"github.com/golang-jwt/jwt/v4"
"github.com/mitchellh/mapstructure"
)
func getTokenFromHeader(authHeader string) (string, error) {
if authHeader == "" {
return "", fmt.Errorf("there is no authorization header provided")
}
authSlice := strings.Split(authHeader, " ")
if len(authSlice) != 2 {
return "", fmt.Errorf("wrong access token or header format")
}
return strings.TrimSpace(authSlice[1]), nil
}
func getUserFromToken(tokenString string) (*models.UserJWT, error) {
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("wrong token format ")
}
return []byte(os.Getenv("ACCESS_SECRET")), nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, fmt.Errorf("token is not valid")
}
var user *models.UserJWT
claims := token.Claims.(jwt.MapClaims)
if err := mapstructure.Decode(claims["data"], &user); err != nil {
return nil, fmt.Errorf("error while decoding payload claim")
}
return user, nil
}

103
app/loaders.go Normal file
View File

@ -0,0 +1,103 @@
package app
import (
"context"
"fmt"
"net/http"
"reflect"
"github.com/farahty/go-mongo/models"
"github.com/graph-gophers/dataloader"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// Loaders holds all the dataloaders used in the application
type Loaders struct {
TodosLoader *dataloader.Loader
UsersLoader *dataloader.Loader
CategoryLoader *dataloader.Loader
}
// NewLoaders initializes all batch loaders
func NewLoaders() *Loaders {
return &Loaders{
TodosLoader: dataloader.NewBatchedLoader(CreateBatch[models.Todo]("todos")),
UsersLoader: dataloader.NewBatchedLoader(CreateBatch[models.User]("users")),
CategoryLoader: dataloader.NewBatchedLoader(CreateBatch[models.Category]("categories")),
}
}
// Middleware injects dataloaders into context for each HTTP request
func LoaderMiddleware(loaders *Loaders, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctxWithLoaders := context.WithValue(r.Context(), LoadersKey, loaders)
next.ServeHTTP(w, r.WithContext(ctxWithLoaders))
})
}
// LoaderFor retrieves the dataloaders from context
func LoaderFor(ctx context.Context) *Loaders {
if loaders, ok := ctx.Value(LoadersKey).(*Loaders); ok {
return loaders
}
panic("dataloader not found in context")
}
// CreateBatch creates a generic batched loader for a MongoDB collection
func CreateBatch[T any](coll string) dataloader.BatchFunc {
return func(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
// Convert all keys to MongoDB ObjectIDs
var objectIDs []primitive.ObjectID
keyOrder := make(map[primitive.ObjectID]int) // Track original key order
for i, key := range keys {
id, err := primitive.ObjectIDFromHex(key.String())
if err != nil {
continue
}
objectIDs = append(objectIDs, id)
keyOrder[id] = i
}
// Query the collection for the documents
filter := bson.M{"_id": bson.M{"$in": objectIDs}}
data, err := Find[T](ctx, coll, filter)
if err != nil {
// If the DB fails, return error for all keys
results := make([]*dataloader.Result, len(keys))
for i := range keys {
results[i] = &dataloader.Result{Data: nil, Error: err}
}
return results
}
// Build map of result keyed by ID
objByID := make(map[primitive.ObjectID]T)
for _, item := range data {
val := reflect.ValueOf(item).Elem()
idValue := reflect.Indirect(val).FieldByName("ID").Interface()
if id, ok := idValue.(primitive.ObjectID); ok {
objByID[id] = *item
}
}
// Assemble results in original key order
results := make([]*dataloader.Result, len(keys))
for i, key := range keys {
id, err := primitive.ObjectIDFromHex(key.String())
if err != nil {
results[i] = &dataloader.Result{Data: nil, Error: fmt.Errorf("invalid object ID: %s", key.String())}
continue
}
if val, ok := objByID[id]; ok {
results[i] = &dataloader.Result{Data: &val, Error: nil}
} else {
results[i] = &dataloader.Result{Data: nil, Error: fmt.Errorf("object not found: %s", key.String())}
}
}
return results
}
}

47
app/middlewares.go Normal file
View File

@ -0,0 +1,47 @@
package app
import (
"context"
"errors"
"net/http"
"github.com/99designs/gqlgen/graphql"
"github.com/golang-jwt/jwt/v4"
)
// ExpiryMiddleware checks for expired tokens in GraphQL resolvers
func ExpiryMiddleware(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
if IsTokenExpired(ctx) {
return graphql.ErrorResponse(ctx, "token expired")
}
return next(ctx)
}
// AuthMiddleware parses JWT token and injects user context for HTTP requests
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
tokenStr, err := getTokenFromHeader(authHeader)
if err != nil {
ctx := SetStatus(r.Context(), err.Error())
next.ServeHTTP(rw, r.WithContext(ctx))
return
}
user, err := getUserFromToken(tokenStr)
ctx := r.Context()
if err != nil {
ctx = SetStatus(ctx, err.Error())
if errors.Is(err, jwt.ErrTokenExpired) {
ctx = SetTokenExpired(ctx, true)
}
next.ServeHTTP(rw, r.WithContext(ctx))
return
}
ctx = SetCurrentUser(ctx, user)
ctx = SetStatus(ctx, "ok")
next.ServeHTTP(rw, r.WithContext(ctx))
})
}

29
app/scalers.go Normal file
View File

@ -0,0 +1,29 @@
package app
import (
"errors"
"io"
"strconv"
"github.com/99designs/gqlgen/graphql"
"go.mongodb.org/mongo-driver/bson/primitive"
)
func MarshalObjectID(value primitive.ObjectID) graphql.Marshaler {
return graphql.WriterFunc(func(writer io.Writer) {
io.WriteString(writer, strconv.Quote(value.Hex()))
})
}
func UnmarshalObjectID(value interface{}) (primitive.ObjectID, error) {
if str, ok := value.(string); ok {
return primitive.ObjectIDFromHex(str)
}
return primitive.NilObjectID, errors.New("invalid Object ID string")
}

28
controllers/user.go Normal file
View File

@ -0,0 +1,28 @@
package controllers
import (
"encoding/json"
"net/http"
userService "github.com/farahty/go-mongo/services/user"
"github.com/go-chi/chi/v5"
)
func UserRouter() chi.Router {
router := chi.NewRouter()
router.Get("/", getUsers)
return router
}
func getUsers(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("content-type", "application/json")
users, err := userService.Find(r.Context())
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
rw.Write([]byte(`{"message": ` + err.Error() + `}`))
return
}
json.NewEncoder(rw).Encode(users)
}

19
directives/auth.go Normal file
View File

@ -0,0 +1,19 @@
package directives
import (
"context"
"fmt"
"github.com/99designs/gqlgen/graphql"
"github.com/farahty/go-mongo/app"
)
func Auth(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {
if _, err := app.CurrentUser(ctx); err != nil {
return nil, fmt.Errorf("access denied, %s", err.Error())
}
return next(ctx)
}

63
generate.go Normal file
View File

@ -0,0 +1,63 @@
//go:build tools
// +build tools
package main
import (
"fmt"
"os"
"strings"
"github.com/99designs/gqlgen/api"
"github.com/99designs/gqlgen/codegen/config"
"github.com/99designs/gqlgen/plugin/modelgen"
)
func mutateHook(b *modelgen.ModelBuild) *modelgen.ModelBuild {
for _, model := range b.Models {
for _, field := range model.Fields {
if field.Name == "id" {
field.Tag += ` bson:"_id,omitempty"`
} else {
if strings.HasPrefix(field.Description, "#bson:") {
command := strings.TrimPrefix(field.Description, "#bson:")
switch command {
case "ignore":
field.Tag = strings.TrimSuffix(field.Tag, `"`) + `,omitempty" bson:"-"`
}
} else {
field.Tag = strings.TrimSuffix(field.Tag, `"`) + `" bson:"` + field.Name + `,omitempty"`
}
}
}
}
return b
}
func main() {
cfg, err := config.LoadConfigFromDefaultLocations()
if err != nil {
fmt.Fprintln(os.Stderr, "❌ failed to load config", err.Error())
os.Exit(2)
}
p := modelgen.Plugin{
MutateHook: mutateHook,
}
err = api.Generate(cfg, api.ReplacePlugin(&p))
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(3)
}
fmt.Println("✅ generator finished successfully")
os.Exit(0)
}

8093
generated/generated.go Normal file

File diff suppressed because it is too large Load Diff

58
go.mod Normal file
View File

@ -0,0 +1,58 @@
module github.com/farahty/go-mongo
go 1.23.0
toolchain go1.24.3
require (
github.com/casbin/casbin/v2 v2.80.0
github.com/casbin/mongodb-adapter/v3 v3.5.0
github.com/go-chi/chi/v5 v5.0.11
github.com/gorilla/websocket v1.5.1
github.com/graph-gophers/dataloader v5.0.0+incompatible
github.com/joho/godotenv v1.5.1
github.com/mitchellh/mapstructure v1.5.0
github.com/redis/go-redis/v9 v9.3.1
github.com/vektah/gqlparser/v2 v2.5.26
go.mongodb.org/mongo-driver v1.13.1
)
require (
github.com/agnivade/levenshtein v1.2.1 // indirect
github.com/casbin/govaluate v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sosodev/duration v1.3.1 // indirect
github.com/urfave/cli/v2 v2.27.6 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/tools v0.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require (
github.com/99designs/gqlgen v0.17.73
github.com/fatih/color v1.16.0
github.com/go-chi/httprate v0.8.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.6.0
github.com/klauspost/compress v1.17.4 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
golang.org/x/crypto v0.37.0
golang.org/x/sync v0.13.0 // indirect
golang.org/x/text v0.24.0 // indirect
)

160
go.sum Normal file
View File

@ -0,0 +1,160 @@
github.com/99designs/gqlgen v0.17.73 h1:A3Ki+rHWqKbAOlg5fxiZBnz6OjW3nwupDHEG15gEsrg=
github.com/99designs/gqlgen v0.17.73/go.mod h1:2RyGWjy2k7W9jxrs8MOQthXGkD3L3oGr0jXW3Pu8lGg=
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/casbin/casbin/v2 v2.80.0 h1:khGQBLnC+4XuAoGH/KW1JvyY0/nfFG8AhgzDrQKCH/g=
github.com/casbin/casbin/v2 v2.80.0/go.mod h1:jX8uoN4veP85O/n2674r2qtfSXI6myvxW85f6TH50fw=
github.com/casbin/govaluate v1.1.0 h1:6xdCWIpE9CwHdZhlVQW+froUrCsjb6/ZYNcXODfLT+E=
github.com/casbin/govaluate v1.1.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/mongodb-adapter/v3 v3.5.0 h1:WacrRWP0PfKgwo/+m5a81tsyDG7LODaLcecZr5zFHuc=
github.com/casbin/mongodb-adapter/v3 v3.5.0/go.mod h1:R5491PozS7Nx4dnHRSTu9CzRsJZ62IZrzAaC7PFych8=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/httprate v0.8.0 h1:CyKng28yhGnlGXH9EDGC/Qizj29afJQSNW15W/yj34o=
github.com/go-chi/httprate v0.8.0/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/graph-gophers/dataloader v5.0.0+incompatible h1:R+yjsbrNq1Mo3aPG+Z/EKYrXrXXUNJHOgbRt+U6jOug=
github.com/graph-gophers/dataloader v5.0.0+incompatible/go.mod h1:jk4jk0c5ZISbKaMe8WsVopGB5/15GvGHMdMdPtwlRp4=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.3.1 h1:KqdY8U+3X6z+iACvumCNxnoluToB+9Me+TvyFa21Mds=
github.com/redis/go-redis/v9 v9.3.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/vektah/gqlparser/v2 v2.5.26 h1:REqqFkO8+SOEgZHR/eHScjjVjGS8Nk3RMO/juiTobN4=
github.com/vektah/gqlparser/v2 v2.5.26/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk=
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

16
gql/auth.gql Normal file
View File

@ -0,0 +1,16 @@
directive @auth on FIELD_DEFINITION
type LoginResponse {
user: User!
accessToken: String!
refreshToken: String!
}
input LoginInput {
identity: String!
password: String!
}
extend type Mutation {
login(input: LoginInput!): LoginResponse!
}

40
gql/base.gql Normal file
View File

@ -0,0 +1,40 @@
directive @goField(
forceResolver: Boolean
name: String
) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION
directive @goTag(
key: String!
value: String
) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION
scalar Time
interface Base {
id: ID!
createdAt: Time!
updatedAt: Time!
createdBy: User
updatedBy: User
owner: User
}
enum Status {
Active
Deactivated
Blocked
Draft
Pending
Closed
}
type Translated {
value: String!
isPrimary: Boolean!
language: String!
}
input TranslatedInput {
value: String!
isPrimary: Boolean!
language: String!
}

40
gql/category.gql Normal file
View File

@ -0,0 +1,40 @@
type Category implements Base {
id: ID!
title: [Translated!]!
body: [Translated!]
"#bson:ignore"
parent: Category @goField(forceResolver: true)
parentId: ID
createdAt: Time!
updatedAt: Time!
"#bson:ignore"
createdBy: User! @goField(forceResolver: true)
createdById: ID!
"#bson:ignore"
updatedBy: User! @goField(forceResolver: true)
updatedById: ID!
"#bson:ignore"
owner: User @goField(forceResolver: true)
ownerId: ID!
}
input CreateCategoryInput {
title: [TranslatedInput!]!
body: [TranslatedInput]
parentId: ID
}
extend type Mutation {
createCategory(input: CreateCategoryInput!): Category!
}
extend type Query {
categories: [Category]!
category(id: ID!): Category!
}

37
gql/todo.gql Normal file
View File

@ -0,0 +1,37 @@
type Todo implements Base {
id: ID!
title: String
completed: Boolean
createdAt: Time!
updatedAt: Time!
"#bson:ignore"
createdBy: User! @goField(forceResolver: true)
createdById: ID!
"#bson:ignore"
updatedBy: User! @goField(forceResolver: true)
updatedById: ID!
"#bson:ignore"
owner: User @goField(forceResolver: true)
ownerId: ID!
}
input CreateTodoInput {
title: String!
completed: Boolean = false
}
extend type Mutation {
createTodo(input: CreateTodoInput!): Todo
}
extend type Query {
todos: [Todo]
todo(id: ID!): Todo
}
extend type Subscription {
onTodo: Todo
}

25
gql/user.gql Normal file
View File

@ -0,0 +1,25 @@
type User {
id: ID!
phone: String
email: String
type: String
status: String
verified: Boolean @goField(forceResolver: true)
password: String
token: String
}
input CreateUserInput {
email: String
phone: String
status: Status
password: String!
}
extend type Query {
users: [User]
}
extend type Mutation {
createUser(input: CreateUserInput!): User
}

36
gqlgen.yml Normal file
View File

@ -0,0 +1,36 @@
schema:
- gql/*.gql
exec:
filename: generated/generated.go
package: generated
model:
filename: models/models_gen.go
package: models
resolver:
layout: follow-schema
dir: resolvers
package: resolvers
autobind:
- "github.com/farahty/go-mongo/models"
# This section declares type mapping between the GraphQL and go type systems
#
# The first line in each type will be used as defaults for resolver arguments and
# modelgen, the others will be allowed when binding to fields. Configure them to
# your liking
models:
ID:
model:
- github.com/farahty/go-mongo/app.ObjectID
- github.com/99designs/gqlgen/graphql.ID
Int:
model:
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64
- github.com/99designs/gqlgen/graphql.Int32

47
helpers/encrypt.go Normal file
View File

@ -0,0 +1,47 @@
package helpers
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
)
var bytes = []byte{35, 46, 57, 24, 85, 35, 24, 74, 87, 35, 88, 98, 66, 32, 14, 05}
func Encode(b []byte) string {
return base64.StdEncoding.EncodeToString(b)
}
func Decode(s string) []byte {
data, err := base64.StdEncoding.DecodeString(s)
if err != nil {
panic(err)
}
return data
}
// Encrypt method is to encrypt or hide any classified text
func Encrypt(text, MySecret string) (string, error) {
block, err := aes.NewCipher([]byte(MySecret))
if err != nil {
return "", err
}
plainText := []byte(text)
cfb := cipher.NewCFBEncrypter(block, bytes)
cipherText := make([]byte, len(plainText))
cfb.XORKeyStream(cipherText, plainText)
return Encode(cipherText), nil
}
// Decrypt method is to extract back the encrypted text
func Decrypt(text, MySecret string) (string, error) {
block, err := aes.NewCipher([]byte(MySecret))
if err != nil {
return "", err
}
cipherText := Decode(text)
cfb := cipher.NewCFBDecrypter(block, bytes)
plainText := make([]byte, len(cipherText))
cfb.XORKeyStream(plainText, cipherText)
return string(plainText), nil
}

97
main.go Normal file
View File

@ -0,0 +1,97 @@
package main
import (
"context"
"os"
"os/signal"
"time"
"log"
"net/http"
"github.com/farahty/go-mongo/app"
"github.com/fatih/color"
"github.com/joho/godotenv"
"github.com/redis/go-redis/v9"
)
func main() {
color.Yellow("Starting server ...\n")
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file\n")
}
port := os.Getenv("PORT")
color.Green("✅ .env loaded\n")
if cancel, err := app.Connect(); err != nil {
cancel()
log.Fatal(err)
} else {
defer func() {
color.Red("❌ Database Connection Closed\n")
cancel()
}()
}
defer func() {
if err := app.Mongo.Disconnect(context.Background()); err != nil {
log.Fatal("MogoDB Errors" + err.Error())
}
}()
color.Green("✅ Connected to Database successfully\n")
if err := app.LoadAuthorizer(context.Background()); err != nil {
log.Fatal("Authorizer Errors : " + err.Error())
}
color.Green("✅ Authorization policies loaded successfully\n")
redisClient := redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_HOST") + ":" + os.Getenv("REDIS_PORT"),
Password: os.Getenv("REDIS_PASSWORD"), // no password set
})
if _, err := redisClient.Ping(context.Background()).Result(); err != nil {
log.Fatal("Redis Error : " + err.Error())
}
defer redisClient.Close()
color.Green("✅ Connected to Redis cache successfully\n")
graphqlServer := createGraphqlServer(redisClient)
color.Green("🚀 Server Started at http://localhost:" + port + "\n")
//http.ListenAndServe(":"+port, createRouter(graphqlServer))
server := &http.Server{
Addr: ":" + port,
WriteTimeout: time.Second * 30,
ReadTimeout: time.Second * 30,
IdleTimeout: time.Second * 30,
Handler: createRouter(graphqlServer),
}
go server.ListenAndServe()
// Wait for interrupt signal to gracefully shut down the server with
// a timeout of 15 seconds.
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit
color.Yellow(" 🎬 Start Shutdown Signal ... ")
ctx, cancelShutdown := context.WithTimeout(context.Background(), 15*time.Second)
defer cancelShutdown()
if err := server.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown:", err)
}
color.Red("❌ Server Exiting")
}

50
makefile Normal file
View File

@ -0,0 +1,50 @@
SHELL := /bin/bash
.DEFAULT_GOAL := build
TARGET := bin/app
VERSION := 1.0.0
BUILD := $(shell git rev-parse HEAD 2>/dev/null || echo "dev")
LDFLAGS := "-X=main.Version=$(VERSION) -X=main.Build=$(BUILD)"
GOFLAGS := -ldflags="$(LDFLAGS)"
SRC := $(shell find . -type f -name '*.go' -not -path "./vendor/*")
hello:
@echo "Target: $(TARGET)"
@echo "Go Flags: $(GOFLAGS)"
build: | bin
@go build $(GOFLAGS) -o $(TARGET) .
bin:
@mkdir -p bin
run: build
@./$(TARGET)
dev:
@CompileDaemon \
-directory="." \
-exclude-dir="vendor" \
-exclude="\.tmp$$" \
-exclude="\.log$$" \
-exclude="\.pid$$" \
-build='go build -o $(TARGET)' \
-command='./$(TARGET)'
start: build
@./$(TARGET)
clean:
@rm -rf bin tmp
format:
@gofmt -l -w $(SRC)
simplify:
@gofmt -s -l -w $(SRC)
test:
@go test ./...

15
model.conf Normal file
View File

@ -0,0 +1,15 @@
[request_definition]
r = user, object, action
[policy_definition]
p = user, object, action
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = (g(r.user, p.user) || p.user == "*" ) && r.object == p.object && r.action == p.action

11
models/base.go Normal file
View File

@ -0,0 +1,11 @@
package models
import "go.mongodb.org/mongo-driver/bson/primitive"
type Identifiable interface {
getId() primitive.ObjectID
}
type Validate interface {
validate() []error
}

200
models/models_gen.go Normal file
View File

@ -0,0 +1,200 @@
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
package models
import (
"bytes"
"fmt"
"io"
"strconv"
"time"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type Base interface {
IsBase()
GetID() primitive.ObjectID
GetCreatedAt() time.Time
GetUpdatedAt() time.Time
GetCreatedBy() *User
GetUpdatedBy() *User
GetOwner() *User
}
type Category struct {
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"`
Title []*Translated `json:"title" bson:"title,omitempty"`
Body []*Translated `json:"body,omitempty" bson:"body,omitempty"`
// #bson:ignore
Parent *Category `json:"parent,omitempty,omitempty" bson:"-"`
ParentID *primitive.ObjectID `json:"parentId,omitempty" bson:"parentId,omitempty"`
CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"`
UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"`
// #bson:ignore
CreatedBy *User `json:"createdBy,omitempty" bson:"-"`
CreatedByID primitive.ObjectID `json:"createdById" bson:"createdById,omitempty"`
// #bson:ignore
UpdatedBy *User `json:"updatedBy,omitempty" bson:"-"`
UpdatedByID primitive.ObjectID `json:"updatedById" bson:"updatedById,omitempty"`
// #bson:ignore
Owner *User `json:"owner,omitempty,omitempty" bson:"-"`
OwnerID primitive.ObjectID `json:"ownerId" bson:"ownerId,omitempty"`
}
func (Category) IsBase() {}
func (this Category) GetID() primitive.ObjectID { return this.ID }
func (this Category) GetCreatedAt() time.Time { return this.CreatedAt }
func (this Category) GetUpdatedAt() time.Time { return this.UpdatedAt }
func (this Category) GetCreatedBy() *User { return this.CreatedBy }
func (this Category) GetUpdatedBy() *User { return this.UpdatedBy }
func (this Category) GetOwner() *User { return this.Owner }
type CreateCategoryInput struct {
Title []*TranslatedInput `json:"title" bson:"title,omitempty"`
Body []*TranslatedInput `json:"body,omitempty" bson:"body,omitempty"`
ParentID *primitive.ObjectID `json:"parentId,omitempty" bson:"parentId,omitempty"`
}
type CreateTodoInput struct {
Title string `json:"title" bson:"title,omitempty"`
Completed *bool `json:"completed,omitempty" bson:"completed,omitempty"`
}
type CreateUserInput struct {
Email *string `json:"email,omitempty" bson:"email,omitempty"`
Phone *string `json:"phone,omitempty" bson:"phone,omitempty"`
Status *Status `json:"status,omitempty" bson:"status,omitempty"`
Password string `json:"password" bson:"password,omitempty"`
}
type LoginInput struct {
Identity string `json:"identity" bson:"identity,omitempty"`
Password string `json:"password" bson:"password,omitempty"`
}
type LoginResponse struct {
User *User `json:"user" bson:"user,omitempty"`
AccessToken string `json:"accessToken" bson:"accessToken,omitempty"`
RefreshToken string `json:"refreshToken" bson:"refreshToken,omitempty"`
}
type Mutation struct {
}
type Query struct {
}
type Subscription struct {
}
type Todo struct {
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"`
Title *string `json:"title,omitempty" bson:"title,omitempty"`
Completed *bool `json:"completed,omitempty" bson:"completed,omitempty"`
CreatedAt time.Time `json:"createdAt" bson:"createdAt,omitempty"`
UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt,omitempty"`
// #bson:ignore
CreatedBy *User `json:"createdBy,omitempty" bson:"-"`
CreatedByID primitive.ObjectID `json:"createdById" bson:"createdById,omitempty"`
// #bson:ignore
UpdatedBy *User `json:"updatedBy,omitempty" bson:"-"`
UpdatedByID primitive.ObjectID `json:"updatedById" bson:"updatedById,omitempty"`
// #bson:ignore
Owner *User `json:"owner,omitempty,omitempty" bson:"-"`
OwnerID primitive.ObjectID `json:"ownerId" bson:"ownerId,omitempty"`
}
func (Todo) IsBase() {}
func (this Todo) GetID() primitive.ObjectID { return this.ID }
func (this Todo) GetCreatedAt() time.Time { return this.CreatedAt }
func (this Todo) GetUpdatedAt() time.Time { return this.UpdatedAt }
func (this Todo) GetCreatedBy() *User { return this.CreatedBy }
func (this Todo) GetUpdatedBy() *User { return this.UpdatedBy }
func (this Todo) GetOwner() *User { return this.Owner }
type Translated struct {
Value string `json:"value" bson:"value,omitempty"`
IsPrimary bool `json:"isPrimary" bson:"isPrimary,omitempty"`
Language string `json:"language" bson:"language,omitempty"`
}
type TranslatedInput struct {
Value string `json:"value" bson:"value,omitempty"`
IsPrimary bool `json:"isPrimary" bson:"isPrimary,omitempty"`
Language string `json:"language" bson:"language,omitempty"`
}
type User struct {
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"`
Phone *string `json:"phone,omitempty" bson:"phone,omitempty"`
Email *string `json:"email,omitempty" bson:"email,omitempty"`
Type *string `json:"type,omitempty" bson:"type,omitempty"`
Status *string `json:"status,omitempty" bson:"status,omitempty"`
Verified *bool `json:"verified,omitempty" bson:"verified,omitempty"`
Password *string `json:"password,omitempty" bson:"password,omitempty"`
Token *string `json:"token,omitempty" bson:"token,omitempty"`
}
type Status string
const (
StatusActive Status = "Active"
StatusDeactivated Status = "Deactivated"
StatusBlocked Status = "Blocked"
StatusDraft Status = "Draft"
StatusPending Status = "Pending"
StatusClosed Status = "Closed"
)
var AllStatus = []Status{
StatusActive,
StatusDeactivated,
StatusBlocked,
StatusDraft,
StatusPending,
StatusClosed,
}
func (e Status) IsValid() bool {
switch e {
case StatusActive, StatusDeactivated, StatusBlocked, StatusDraft, StatusPending, StatusClosed:
return true
}
return false
}
func (e Status) String() string {
return string(e)
}
func (e *Status) UnmarshalGQL(v any) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = Status(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid Status", str)
}
return nil
}
func (e Status) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
func (e *Status) UnmarshalJSON(b []byte) error {
s, err := strconv.Unquote(string(b))
if err != nil {
return err
}
return e.UnmarshalGQL(s)
}
func (e Status) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
e.MarshalGQL(&buf)
return buf.Bytes(), nil
}

23
models/todo.go Normal file
View File

@ -0,0 +1,23 @@
package models
import "go.mongodb.org/mongo-driver/bson/primitive"
func (m Todo) Validate() []error {
errors := []error{}
if len(errors) == 0 {
return nil
}
return errors
}
func (m Todo) getId() primitive.ObjectID {
return m.ID
}
func (t *Todo) validate() []error {
return nil
}

31
models/user.go Normal file
View File

@ -0,0 +1,31 @@
package models
import (
"go.mongodb.org/mongo-driver/bson/primitive"
"golang.org/x/crypto/bcrypt"
)
func MakeHash(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
func (u *User) CheckPassword(password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(*u.Password), []byte(password))
return err == nil
}
func (u User) getId() primitive.ObjectID {
return u.ID
}
func (u User) validate() []error {
return nil
}
type UserJWT struct {
ID string `json:"id"`
Identity string `json:"identity"`
}

13
readme.md Normal file
View File

@ -0,0 +1,13 @@
# Go MongoDB Playground
just a playground to test mongodb driver with go lan
#### How to start ?
```go
go run .
```
### make sure to run below code before running go run generate.go
```go
go get github.com/99designs/gqlgen
```

View File

@ -0,0 +1,23 @@
package resolvers
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.73
import (
"context"
"github.com/farahty/go-mongo/generated"
"github.com/farahty/go-mongo/models"
authService "github.com/farahty/go-mongo/services/auth"
)
// Login is the resolver for the login field.
func (r *mutationResolver) Login(ctx context.Context, input models.LoginInput) (*models.LoginResponse, error) {
return authService.Login(ctx, &input)
}
// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
type mutationResolver struct{ *Resolver }

View File

@ -0,0 +1,62 @@
package resolvers
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.73
import (
"context"
"github.com/farahty/go-mongo/generated"
"github.com/farahty/go-mongo/models"
categoryService "github.com/farahty/go-mongo/services/category"
userService "github.com/farahty/go-mongo/services/user"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// Parent is the resolver for the parent field.
func (r *categoryResolver) Parent(ctx context.Context, obj *models.Category) (*models.Category, error) {
if obj.ParentID == nil {
return nil, nil
}
return categoryService.FindByID(ctx, *obj.ParentID)
}
// CreatedBy is the resolver for the createdBy field.
func (r *categoryResolver) CreatedBy(ctx context.Context, obj *models.Category) (*models.User, error) {
return userService.FindById(ctx, obj.CreatedByID)
}
// UpdatedBy is the resolver for the updatedBy field.
func (r *categoryResolver) UpdatedBy(ctx context.Context, obj *models.Category) (*models.User, error) {
return userService.FindById(ctx, obj.UpdatedByID)
}
// Owner is the resolver for the owner field.
func (r *categoryResolver) Owner(ctx context.Context, obj *models.Category) (*models.User, error) {
return userService.FindById(ctx, obj.OwnerID)
}
// CreateCategory is the resolver for the createCategory field.
func (r *mutationResolver) CreateCategory(ctx context.Context, input models.CreateCategoryInput) (*models.Category, error) {
return categoryService.Create(ctx, input)
}
// Categories is the resolver for the categories field.
func (r *queryResolver) Categories(ctx context.Context) ([]*models.Category, error) {
return categoryService.Find(ctx)
}
// Category is the resolver for the category field.
func (r *queryResolver) Category(ctx context.Context, id primitive.ObjectID) (*models.Category, error) {
return categoryService.FindByID(ctx, id)
}
// Category returns generated.CategoryResolver implementation.
func (r *Resolver) Category() generated.CategoryResolver { return &categoryResolver{r} }
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type categoryResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }

54
resolvers/resolver.go Normal file
View File

@ -0,0 +1,54 @@
package resolvers
import (
"context"
"encoding/json"
"log"
"github.com/redis/go-redis/v9"
)
// This file will not be regenerated automatically.
//
// It serves as dependency injection for your app, add any dependencies you require here.
type Resolver struct {
Redis *redis.Client
}
func Subscribe[T any](ctx context.Context, redis *redis.Client, event string) (<-chan *T, error) {
clientChannel := make(chan *T, 1)
go func() {
sub := redis.Subscribe(ctx, event)
if _, err := sub.Receive(ctx); err != nil {
return
}
serverChannel := sub.Channel()
for {
select {
case message := <-serverChannel:
var obj *T
if err := json.Unmarshal([]byte(message.Payload), &obj); err != nil {
log.Print(err)
return
}
clientChannel <- obj
case <-ctx.Done():
sub.Close()
return
}
}
}()
return clientChannel, nil
}

View File

@ -0,0 +1,70 @@
package resolvers
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.73
import (
"context"
"encoding/json"
"github.com/farahty/go-mongo/generated"
"github.com/farahty/go-mongo/models"
todoService "github.com/farahty/go-mongo/services/todo"
userService "github.com/farahty/go-mongo/services/user"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// CreateTodo is the resolver for the createTodo field.
func (r *mutationResolver) CreateTodo(ctx context.Context, input models.CreateTodoInput) (*models.Todo, error) {
obj, err := todoService.Create(ctx, input)
if err != nil {
return nil, err
}
if objJson, err := json.Marshal(obj); err == nil {
r.Redis.Publish(ctx, "NEW_TODO_EVENT", objJson)
}
return obj, nil
}
// Todos is the resolver for the todos field.
func (r *queryResolver) Todos(ctx context.Context) ([]*models.Todo, error) {
return todoService.Find(ctx)
}
// Todo is the resolver for the todo field.
func (r *queryResolver) Todo(ctx context.Context, id primitive.ObjectID) (*models.Todo, error) {
return todoService.FindByID(ctx, id)
}
// OnTodo is the resolver for the onTodo field.
func (r *subscriptionResolver) OnTodo(ctx context.Context) (<-chan *models.Todo, error) {
return Subscribe[models.Todo](ctx, r.Redis, "NEW_TODO_EVENT")
}
// CreatedBy is the resolver for the createdBy field.
func (r *todoResolver) CreatedBy(ctx context.Context, obj *models.Todo) (*models.User, error) {
return userService.FindById(ctx, obj.CreatedByID)
}
// UpdatedBy is the resolver for the updatedBy field.
func (r *todoResolver) UpdatedBy(ctx context.Context, obj *models.Todo) (*models.User, error) {
return userService.FindById(ctx, obj.UpdatedByID)
}
// Owner is the resolver for the owner field.
func (r *todoResolver) Owner(ctx context.Context, obj *models.Todo) (*models.User, error) {
return userService.FindById(ctx, obj.OwnerID)
}
// Subscription returns generated.SubscriptionResolver implementation.
func (r *Resolver) Subscription() generated.SubscriptionResolver { return &subscriptionResolver{r} }
// Todo returns generated.TodoResolver implementation.
func (r *Resolver) Todo() generated.TodoResolver { return &todoResolver{r} }
type subscriptionResolver struct{ *Resolver }
type todoResolver struct{ *Resolver }

View File

@ -0,0 +1,35 @@
package resolvers
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.73
import (
"context"
"github.com/farahty/go-mongo/generated"
"github.com/farahty/go-mongo/models"
userService "github.com/farahty/go-mongo/services/user"
)
// CreateUser is the resolver for the createUser field.
func (r *mutationResolver) CreateUser(ctx context.Context, input models.CreateUserInput) (*models.User, error) {
return userService.Create(ctx, input)
}
// Users is the resolver for the users field.
func (r *queryResolver) Users(ctx context.Context) ([]*models.User, error) {
return userService.Find(ctx)
}
// Verified is the resolver for the verified field.
func (r *userResolver) Verified(ctx context.Context, obj *models.User) (*bool, error) {
ver := obj != nil
return &ver, nil
}
// User returns generated.UserResolver implementation.
func (r *Resolver) User() generated.UserResolver { return &userResolver{r} }
type userResolver struct{ *Resolver }

100
router.go Normal file
View File

@ -0,0 +1,100 @@
package main
import (
"net/http"
"os"
"time"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/handler/extension"
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/farahty/go-mongo/app"
"github.com/farahty/go-mongo/controllers"
"github.com/farahty/go-mongo/directives"
"github.com/farahty/go-mongo/generated"
"github.com/farahty/go-mongo/resolvers"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/httprate"
"github.com/gorilla/websocket"
"github.com/redis/go-redis/v9"
)
func createRouter(graphqlServer http.Handler) chi.Router {
router := chi.NewRouter()
// Apply middleware
if os.Getenv("ENV") == "production" {
router.Use(httprate.LimitByIP(100, 1*time.Minute)) // 100 requests/minute/IP
}
router.Use(middleware.Logger)
router.Use(middleware.StripSlashes)
router.Use(middleware.RealIP)
router.Use(middleware.Recoverer)
// Custom middleware for Auth
router.Use(app.AuthMiddleware)
// REST routes
router.Mount("/users", controllers.UserRouter())
// GraphQL endpoints
router.Handle("/", playground.Handler("GraphQL Playground", "/graphql"))
router.Handle("/graphql", graphqlServer)
return router
}
func createGraphqlServer(redisClient *redis.Client) http.Handler {
// Setup gqlgen with resolvers and Redis client
schema := generated.Config{
Resolvers: &resolvers.Resolver{
Redis: redisClient,
},
}
// Map directives (e.g., @auth)
mapDirectives(&schema)
// Initialize GraphQL handler
srv := handler.New(generated.NewExecutableSchema(schema))
// Enable transports (WebSocket, GET, POST, etc.)
srv.AddTransport(transport.Websocket{
KeepAlivePingInterval: 10 * time.Second,
Upgrader: websocket.Upgrader{
HandshakeTimeout: time.Minute,
EnableCompression: true,
CheckOrigin: func(r *http.Request) bool { return true },
},
InitFunc: app.AuthorizeWebSocket,
})
srv.AddTransport(transport.Options{})
srv.AddTransport(transport.GET{})
srv.AddTransport(transport.POST{})
srv.AddTransport(transport.MultipartForm{})
// Optional: Enable persisted queries or caching
// srv.Use(extension.AutomaticPersistedQuery{
// Cache: lru.New(100),
// })
// srv.SetQueryCache(lru.New(1000))
// Enable introspection for Playground
srv.Use(extension.Introspection{})
// Apply global middleware
srv.AroundRootFields(app.RootFieldsAuthorizer) // Check for @auth at root fields
srv.AroundResponses(app.ExpiryMiddleware) // Token expiry validation
// Inject DataLoaders into request context
return app.LoaderMiddleware(app.NewLoaders(), srv)
}
func mapDirectives(config *generated.Config) {
config.Directives.Auth = directives.Auth
}

36
services/auth/auth.go Normal file
View File

@ -0,0 +1,36 @@
package authService
import (
"context"
"fmt"
"github.com/farahty/go-mongo/app"
"github.com/farahty/go-mongo/models"
"go.mongodb.org/mongo-driver/bson"
)
func Login(ctx context.Context, loginInput *models.LoginInput) (*models.LoginResponse, error) {
// todo : fix the security threats here
filter := bson.D{
{
Key: "$or",
Value: bson.A{
bson.D{{Key: "phone", Value: loginInput.Identity}},
bson.D{{Key: "email", Value: loginInput.Identity}},
},
},
}
user, err := app.FindOne[models.User](ctx, "users", filter)
if err != nil {
return nil, err
}
if !user.CheckPassword(loginInput.Password) {
return nil, fmt.Errorf("incorrect password")
}
return successLogin(ctx, user)
}

View File

@ -0,0 +1,29 @@
package authService
import (
"time"
"github.com/golang-jwt/jwt/v4"
)
func createToken(sub, secret, expiry string, payload interface{}) (*string, error) {
duration, err := time.ParseDuration(expiry)
if err != nil {
return nil, err
}
token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims)
claims["sub"] = sub
claims["exp"] = time.Now().Add(duration).Unix()
claims["data"] = payload
signedToken, err := token.SignedString([]byte(secret))
if err != nil {
return nil, err
}
return &signedToken, nil
}

View File

@ -0,0 +1,70 @@
package authService
import (
"context"
"encoding/hex"
"os"
"github.com/farahty/go-mongo/app"
"github.com/farahty/go-mongo/models"
"github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson"
)
func successLogin(ctx context.Context, user *models.User) (*models.LoginResponse, error) {
refresh_secret := os.Getenv("REFRESH_SECRET")
refresh_expiry := os.Getenv("REFRESH_EXPIRY")
access_secret := os.Getenv("ACCESS_SECRET")
access_expiry := os.Getenv("ACCESS_EXPIRY")
identity := user.Email
if identity == nil {
identity = user.Phone
}
refreshHandle := hex.EncodeToString([]byte(uuid.NewString()))
refreshToken, err := createToken(
refreshHandle,
refresh_secret,
refresh_expiry,
models.UserJWT{
ID: user.ID.Hex(),
Identity: *identity,
},
)
if err != nil {
return nil, err
}
accessToken, err := createToken(
user.ID.Hex(),
access_secret,
access_expiry,
models.UserJWT{
ID: user.ID.Hex(),
Identity: *identity,
},
)
if err != nil {
return nil, err
}
user.Token = &refreshHandle
_, err = app.Collection("users").UpdateByID(ctx, user.ID, bson.D{{Key: "$set", Value: user}})
if err != nil {
return nil, err
}
return &models.LoginResponse{
AccessToken: *accessToken,
RefreshToken: *refreshToken,
User: user,
}, nil
}

View File

@ -0,0 +1,37 @@
package categoryService
import (
"context"
"github.com/farahty/go-mongo/app"
"github.com/farahty/go-mongo/models"
"github.com/graph-gophers/dataloader"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
var coll = "categories"
func Find(ctx context.Context) ([]*models.Category, error) {
return app.Find[models.Category](ctx, coll, bson.D{})
}
func Create(ctx context.Context, input models.CreateCategoryInput) (*models.Category, error) {
return app.InsertOne[models.Category](ctx, coll, input)
}
func FindByID(ctx context.Context, id primitive.ObjectID) (*models.Category, error) {
loaders := app.LoaderFor(ctx)
thunk := loaders.CategoryLoader.Load(ctx, dataloader.StringKey(id.Hex()))
result, err := thunk()
if err != nil {
return nil, err
}
return result.(*models.Category), nil
}

37
services/todo/todo.go Normal file
View File

@ -0,0 +1,37 @@
package todoService
import (
"context"
"github.com/farahty/go-mongo/app"
"github.com/farahty/go-mongo/models"
"github.com/graph-gophers/dataloader"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
var coll = "todos"
func Find(ctx context.Context) ([]*models.Todo, error) {
return app.Find[models.Todo](ctx, coll, bson.D{})
}
func FindByID(ctx context.Context, id primitive.ObjectID) (*models.Todo, error) {
loaders := app.LoaderFor(ctx)
thunk := loaders.TodosLoader.Load(ctx, dataloader.StringKey(id.Hex()))
result, err := thunk()
if err != nil {
return nil, err
}
return result.(*models.Todo), nil
}
func Create(ctx context.Context, input models.CreateTodoInput) (*models.Todo, error) {
return app.InsertOne[models.Todo](ctx, coll, input)
}

40
services/user/user.go Normal file
View File

@ -0,0 +1,40 @@
package userService
import (
"context"
"github.com/farahty/go-mongo/app"
"github.com/farahty/go-mongo/models"
"github.com/graph-gophers/dataloader"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
var coll = "users"
func Create(ctx context.Context, input models.CreateUserInput) (*models.User, error) {
input.Password, _ = models.MakeHash(input.Password)
return app.InsertOne[models.User](ctx, coll, input)
}
func Find(ctx context.Context) ([]*models.User, error) {
return app.Find[models.User](ctx, coll, bson.D{})
}
func FindById(ctx context.Context, id primitive.ObjectID) (*models.User, error) {
loader := app.LoaderFor(ctx).UsersLoader
thunk := loader.Load(ctx, dataloader.StringKey(id.Hex()))
result, err := thunk()
if err != nil {
return nil, err
}
return result.(*models.User), nil
}

9
tools.go Normal file
View File

@ -0,0 +1,9 @@
//go:build tools
// +build tools
package tools
import (
_ "github.com/99designs/gqlgen"
_ "github.com/99designs/gqlgen/graphql/introspection"
)