first commit
This commit is contained in:
commit
f729f0c48f
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
tmp
|
||||||
|
go-mongo
|
||||||
|
bin
|
||||||
|
.env
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
tmp
|
||||||
|
go-mongo
|
||||||
|
bin
|
||||||
|
.env
|
||||||
1
.vscode/configurationCache.log
vendored
Normal file
1
.vscode/configurationCache.log
vendored
Normal 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
6
.vscode/dryrun.log
vendored
Normal 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
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"nuxt.isNuxtApp": false
|
||||||
|
}
|
||||||
382
.vscode/targets.log
vendored
Normal file
382
.vscode/targets.log
vendored
Normal 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
37
Dockerfile
Normal 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
54
app/app-context.go
Normal 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
134
app/auth.go
Normal 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
183
app/database.go
Normal 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
53
app/helpers.go
Normal 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
103
app/loaders.go
Normal 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
47
app/middlewares.go
Normal 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
29
app/scalers.go
Normal 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
28
controllers/user.go
Normal 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
19
directives/auth.go
Normal 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
63
generate.go
Normal 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
8093
generated/generated.go
Normal file
File diff suppressed because it is too large
Load Diff
58
go.mod
Normal file
58
go.mod
Normal 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
160
go.sum
Normal 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
16
gql/auth.gql
Normal 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
40
gql/base.gql
Normal 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
40
gql/category.gql
Normal 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
37
gql/todo.gql
Normal 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
25
gql/user.gql
Normal 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
36
gqlgen.yml
Normal 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
47
helpers/encrypt.go
Normal 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
97
main.go
Normal 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
50
makefile
Normal 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
15
model.conf
Normal 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
11
models/base.go
Normal 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
200
models/models_gen.go
Normal 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
23
models/todo.go
Normal 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
31
models/user.go
Normal 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
13
readme.md
Normal 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
|
||||||
|
```
|
||||||
23
resolvers/auth.resolvers.go
Normal file
23
resolvers/auth.resolvers.go
Normal 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 }
|
||||||
62
resolvers/category.resolvers.go
Normal file
62
resolvers/category.resolvers.go
Normal 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
54
resolvers/resolver.go
Normal 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
|
||||||
|
|
||||||
|
}
|
||||||
70
resolvers/todo.resolvers.go
Normal file
70
resolvers/todo.resolvers.go
Normal 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 }
|
||||||
35
resolvers/user.resolvers.go
Normal file
35
resolvers/user.resolvers.go
Normal 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
100
router.go
Normal 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
36
services/auth/auth.go
Normal 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)
|
||||||
|
}
|
||||||
29
services/auth/create_token.go
Normal file
29
services/auth/create_token.go
Normal 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
|
||||||
|
}
|
||||||
70
services/auth/sucess_login.go
Normal file
70
services/auth/sucess_login.go
Normal 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
|
||||||
|
|
||||||
|
}
|
||||||
37
services/category/category.go
Normal file
37
services/category/category.go
Normal 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
37
services/todo/todo.go
Normal 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
40
services/user/user.go
Normal 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
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user