diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 584f4e7..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3cb27f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +/data + + +.env +service_account.json + +/tmp \ No newline at end of file diff --git a/DatabaseCamp b/DatabaseCamp new file mode 100755 index 0000000..24b9e05 Binary files /dev/null and b/DatabaseCamp differ diff --git a/Dockerfile b/Dockerfile index d6c6247..29b43eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,28 @@ -FROM golang:1.16 AS builder - -WORKDIR /src - -COPY . . - -RUN CGO_ENABLED=0 go build -o DatabaseCamp . - -FROM alpine:3.13 - -RUN apk add tzdata -RUN cp /usr/share/zoneinfo/Asia/Bangkok /etc/localtime -RUN echo "Asia/Bangkok" > /etc/timezone - -WORKDIR /usr/src/app - -COPY --from=builder /src/DatabaseCamp /usr/src/app/DatabaseCamp -COPY --from=builder /src/.env /usr/src/app/.env -COPY --from=builder /src/service_account.json /usr/src/app/service_account.json - -RUN apk add dumb-init -ENTRYPOINT ["/usr/bin/dumb-init", "--"] - -EXPOSE 8080 +FROM golang:1.16 AS builder + +WORKDIR /src + +COPY . . + +RUN CGO_ENABLED=0 go build \ + -ldflags "-X 'DatabaseCamp/router.BuildTime=`date "+%Y-%m-%dT%H:%M:%S%Z:00"`'\ + -X 'DatabaseCamp/router.BuildCommit=`git rev-parse --short HEAD`'"\ + -o DatabaseCamp cmd/server/main.go + +FROM alpine:3.13 + +RUN apk add tzdata +RUN cp /usr/share/zoneinfo/Asia/Bangkok /etc/localtime +RUN echo "Asia/Bangkok" > /etc/timezone + +WORKDIR /usr/src/app + +COPY --from=builder /src/DatabaseCamp /usr/src/app/DatabaseCamp +COPY --from=builder /src/.env /usr/src/app/.env +COPY --from=builder /src/service_account.json /usr/src/app/service_account.json +RUN mkdir tmp +RUN apk add dumb-init +ENTRYPOINT ["/usr/bin/dumb-init", "--"] + +EXPOSE 8080 CMD ["./DatabaseCamp"] \ No newline at end of file diff --git a/cloudbuild.yaml b/cloudbuild.yaml index 2daa18d..c03aa45 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -1,25 +1,36 @@ -steps: - - name: "gcr.io/cloud-builders/gsutil" - args: ["cp", "gs://databasecamp-private/backend-infrastructure/service_account.json", "./service_account.json"] - - name: "gcr.io/cloud-builders/gsutil" - args: ["cp", "gs://databasecamp-private/backend-infrastructure/.env.main", "./.env"] - - name: 'gcr.io/cloud-builders/docker' - args: ['build', '-t', 'gcr.io/$PROJECT_ID/$TRIGGER_NAME', '.'] - - name: 'gcr.io/cloud-builders/docker' - args: ['push', 'gcr.io/$PROJECT_ID/$TRIGGER_NAME'] - - name: 'gcr.io/cloud-builders/gcloud' - args: - [ - 'beta', - 'run', - 'deploy', - '$TRIGGER_NAME', - '--image', - 'gcr.io/$PROJECT_ID/$TRIGGER_NAME', - '--region', - 'asia-east1', - '--platform', - 'managed', - '--quiet'] -images: - - gcr.io/$PROJECT_ID/$TRIGGER_NAME \ No newline at end of file +steps: + - name: "gcr.io/cloud-builders/gsutil" + args: + [ + "cp", + "gs://databasecamp-private/backend-infrastructure/service_account.json", + "./service_account.json", + ] + - name: "gcr.io/cloud-builders/gsutil" + args: + [ + "cp", + "gs://databasecamp-private/backend-infrastructure/.env.develop", + "./.env", + ] + - name: "gcr.io/cloud-builders/docker" + args: ["build", "-t", "gcr.io/$PROJECT_ID/$TRIGGER_NAME", "."] + - name: "gcr.io/cloud-builders/docker" + args: ["push", "gcr.io/$PROJECT_ID/$TRIGGER_NAME"] + - name: "gcr.io/cloud-builders/gcloud" + args: + [ + "beta", + "run", + "deploy", + "$TRIGGER_NAME", + "--image", + "gcr.io/$PROJECT_ID/$TRIGGER_NAME", + "--region", + "asia-east1", + "--platform", + "managed", + "--quiet", + ] +images: + - gcr.io/$PROJECT_ID/$TRIGGER_NAME diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..60f3431 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,93 @@ +package main + +import ( + "context" + "database-camp/internal/infrastructure/application" + "database-camp/internal/infrastructure/database" + "database-camp/internal/infrastructure/environment" + "database-camp/internal/logs" + "database-camp/internal/registry" + "database-camp/internal/routes" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" +) + +func setupTimeZone() error { + location, err := time.LoadLocation("Asia/Bangkok") + if err != nil { + return err + } + time.Local = location + return nil +} + +func main() { + + err := environment.New().Load(".env") + if err != nil { + logs.GetInstance().Error(err) + return + } + + err = setupTimeZone() + if err != nil { + logs.GetInstance().Error(err) + return + } + + db := database.GetMySqlDBInstance() + err = db.OpenConnection() + if err != nil { + logs.GetInstance().Error(err) + return + } + defer db.CloseConnection() + + app := application.NewFiberApp() + regis := registry.Regis() + + routes.NewRouter(app, regis) + + liveName := fmt.Sprintf("tmp/live%d", os.Getpid()) + + live, err := os.Create(liveName) + if err != nil { + logs.GetInstance().Error(err) + return + } + + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + go func() { + if err := app.Listen(":" + os.Getenv("PORT")); err != nil && err != http.ErrServerClosed { + log.Fatalf("listen: %s\n", err) + } + }() + + <-ctx.Done() + stop() + + err = live.Close() + if err != nil { + logs.GetInstance().Error(err) + return + } + + err = os.Remove(liveName) + if err != nil { + logs.GetInstance().Error(err) + return + } + + err = app.Shutdown() + if err != nil { + logs.GetInstance().Error(err) + return + } +} diff --git a/config/redis.conf b/config/redis.conf new file mode 100644 index 0000000..6b4fad3 --- /dev/null +++ b/config/redis.conf @@ -0,0 +1,1877 @@ +# Redis configuration file example. +# +# Note that in order to read the configuration file, Redis must be +# started with the file path as first argument: +# +# ./redis-server /path/to/redis.conf + +# Note on units: when memory size is needed, it is possible to specify +# it in the usual form of 1k 5GB 4M and so forth: +# +# 1k => 1000 bytes +# 1kb => 1024 bytes +# 1m => 1000000 bytes +# 1mb => 1024*1024 bytes +# 1g => 1000000000 bytes +# 1gb => 1024*1024*1024 bytes +# +# units are case insensitive so 1GB 1Gb 1gB are all the same. + +################################## INCLUDES ################################### + +# Include one or more other config files here. This is useful if you +# have a standard template that goes to all Redis servers but also need +# to customize a few per-server settings. Include files can include +# other files, so use this wisely. +# +# Note that option "include" won't be rewritten by command "CONFIG REWRITE" +# from admin or Redis Sentinel. Since Redis always uses the last processed +# line as value of a configuration directive, you'd better put includes +# at the beginning of this file to avoid overwriting config change at runtime. +# +# If instead you are interested in using includes to override configuration +# options, it is better to use include as the last line. +# +# include /path/to/local.conf +# include /path/to/other.conf + +################################## MODULES ##################################### + +# Load modules at startup. If the server is not able to load modules +# it will abort. It is possible to use multiple loadmodule directives. +# +# loadmodule /path/to/my_module.so +# loadmodule /path/to/other_module.so + +################################## NETWORK ##################################### + +# By default, if no "bind" configuration directive is specified, Redis listens +# for connections from all available network interfaces on the host machine. +# It is possible to listen to just one or multiple selected interfaces using +# the "bind" configuration directive, followed by one or more IP addresses. +# +# Examples: +# +# bind 192.168.1.100 10.0.0.1 +# bind 127.0.0.1 ::1 +# +# ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the +# internet, binding to all the interfaces is dangerous and will expose the +# instance to everybody on the internet. So by default we uncomment the +# following bind directive, that will force Redis to listen only on the +# IPv4 loopback interface address (this means Redis will only be able to +# accept client connections from the same host that it is running on). +# +# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES +# JUST COMMENT OUT THE FOLLOWING LINE. +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +bind 0.0.0.0 + +# Protected mode is a layer of security protection, in order to avoid that +# Redis instances left open on the internet are accessed and exploited. +# +# When protected mode is on and if: +# +# 1) The server is not binding explicitly to a set of addresses using the +# "bind" directive. +# 2) No password is configured. +# +# The server only accepts connections from clients connecting from the +# IPv4 and IPv6 loopback addresses 127.0.0.1 and ::1, and from Unix domain +# sockets. +# +# By default protected mode is enabled. You should disable it only if +# you are sure you want clients from other hosts to connect to Redis +# even if no authentication is configured, nor a specific set of interfaces +# are explicitly listed using the "bind" directive. +protected-mode yes + +# Accept connections on the specified port, default is 6379 (IANA #815344). +# If port 0 is specified Redis will not listen on a TCP socket. +port 6379 + +# TCP listen() backlog. +# +# In high requests-per-second environments you need a high backlog in order +# to avoid slow clients connection issues. Note that the Linux kernel +# will silently truncate it to the value of /proc/sys/net/core/somaxconn so +# make sure to raise both the value of somaxconn and tcp_max_syn_backlog +# in order to get the desired effect. +tcp-backlog 511 + +# Unix socket. +# +# Specify the path for the Unix socket that will be used to listen for +# incoming connections. There is no default, so Redis will not listen +# on a unix socket when not specified. +# +# unixsocket /tmp/redis.sock +# unixsocketperm 700 + +# Close the connection after a client is idle for N seconds (0 to disable) +timeout 0 + +# TCP keepalive. +# +# If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence +# of communication. This is useful for two reasons: +# +# 1) Detect dead peers. +# 2) Force network equipment in the middle to consider the connection to be +# alive. +# +# On Linux, the specified value (in seconds) is the period used to send ACKs. +# Note that to close the connection the double of the time is needed. +# On other kernels the period depends on the kernel configuration. +# +# A reasonable value for this option is 300 seconds, which is the new +# Redis default starting with Redis 3.2.1. +tcp-keepalive 300 + +################################# TLS/SSL ##################################### + +# By default, TLS/SSL is disabled. To enable it, the "tls-port" configuration +# directive can be used to define TLS-listening ports. To enable TLS on the +# default port, use: +# +# port 0 +# tls-port 6379 + +# Configure a X.509 certificate and private key to use for authenticating the +# server to connected clients, masters or cluster peers. These files should be +# PEM formatted. +# +# tls-cert-file redis.crt +# tls-key-file redis.key + +# Configure a DH parameters file to enable Diffie-Hellman (DH) key exchange: +# +# tls-dh-params-file redis.dh + +# Configure a CA certificate(s) bundle or directory to authenticate TLS/SSL +# clients and peers. Redis requires an explicit configuration of at least one +# of these, and will not implicitly use the system wide configuration. +# +# tls-ca-cert-file ca.crt +# tls-ca-cert-dir /etc/ssl/certs + +# By default, clients (including replica servers) on a TLS port are required +# to authenticate using valid client side certificates. +# +# If "no" is specified, client certificates are not required and not accepted. +# If "optional" is specified, client certificates are accepted and must be +# valid if provided, but are not required. +# +# tls-auth-clients no +# tls-auth-clients optional + +# By default, a Redis replica does not attempt to establish a TLS connection +# with its master. +# +# Use the following directive to enable TLS on replication links. +# +# tls-replication yes + +# By default, the Redis Cluster bus uses a plain TCP connection. To enable +# TLS for the bus protocol, use the following directive: +# +# tls-cluster yes + +# Explicitly specify TLS versions to support. Allowed values are case insensitive +# and include "TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3" (OpenSSL >= 1.1.1) or +# any combination. To enable only TLSv1.2 and TLSv1.3, use: +# +# tls-protocols "TLSv1.2 TLSv1.3" + +# Configure allowed ciphers. See the ciphers(1ssl) manpage for more information +# about the syntax of this string. +# +# Note: this configuration applies only to <= TLSv1.2. +# +# tls-ciphers DEFAULT:!MEDIUM + +# Configure allowed TLSv1.3 ciphersuites. See the ciphers(1ssl) manpage for more +# information about the syntax of this string, and specifically for TLSv1.3 +# ciphersuites. +# +# tls-ciphersuites TLS_CHACHA20_POLY1305_SHA256 + +# When choosing a cipher, use the server's preference instead of the client +# preference. By default, the server follows the client's preference. +# +# tls-prefer-server-ciphers yes + +# By default, TLS session caching is enabled to allow faster and less expensive +# reconnections by clients that support it. Use the following directive to disable +# caching. +# +# tls-session-caching no + +# Change the default number of TLS sessions cached. A zero value sets the cache +# to unlimited size. The default size is 20480. +# +# tls-session-cache-size 5000 + +# Change the default timeout of cached TLS sessions. The default timeout is 300 +# seconds. +# +# tls-session-cache-timeout 60 + +################################# GENERAL ##################################### + +# By default Redis does not run as a daemon. Use 'yes' if you need it. +# Note that Redis will write a pid file in /var/run/redis.pid when daemonized. +daemonize no + +# If you run Redis from upstart or systemd, Redis can interact with your +# supervision tree. Options: +# supervised no - no supervision interaction +# supervised upstart - signal upstart by putting Redis into SIGSTOP mode +# requires "expect stop" in your upstart job config +# supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET +# supervised auto - detect upstart or systemd method based on +# UPSTART_JOB or NOTIFY_SOCKET environment variables +# Note: these supervision methods only signal "process is ready." +# They do not enable continuous pings back to your supervisor. +supervised no + +# If a pid file is specified, Redis writes it where specified at startup +# and removes it at exit. +# +# When the server runs non daemonized, no pid file is created if none is +# specified in the configuration. When the server is daemonized, the pid file +# is used even if not specified, defaulting to "/var/run/redis.pid". +# +# Creating a pid file is best effort: if Redis is not able to create it +# nothing bad happens, the server will start and run normally. +pidfile /var/run/redis_6379.pid + +# Specify the server verbosity level. +# This can be one of: +# debug (a lot of information, useful for development/testing) +# verbose (many rarely useful info, but not a mess like the debug level) +# notice (moderately verbose, what you want in production probably) +# warning (only very important / critical messages are logged) +loglevel notice + +# Specify the log file name. Also the empty string can be used to force +# Redis to log on the standard output. Note that if you use standard +# output for logging but daemonize, logs will be sent to /dev/null +logfile "" + +# To enable logging to the system logger, just set 'syslog-enabled' to yes, +# and optionally update the other syslog parameters to suit your needs. +# syslog-enabled no + +# Specify the syslog identity. +# syslog-ident redis + +# Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7. +# syslog-facility local0 + +# Set the number of databases. The default database is DB 0, you can select +# a different one on a per-connection basis using SELECT where +# dbid is a number between 0 and 'databases'-1 +databases 16 + +# By default Redis shows an ASCII art logo only when started to log to the +# standard output and if the standard output is a TTY. Basically this means +# that normally a logo is displayed only in interactive sessions. +# +# However it is possible to force the pre-4.0 behavior and always show a +# ASCII art logo in startup logs by setting the following option to yes. +always-show-logo yes + +################################ SNAPSHOTTING ################################ +# +# Save the DB on disk: +# +# save +# +# Will save the DB if both the given number of seconds and the given +# number of write operations against the DB occurred. +# +# In the example below the behavior will be to save: +# after 900 sec (15 min) if at least 1 key changed +# after 300 sec (5 min) if at least 10 keys changed +# after 60 sec if at least 10000 keys changed +# +# Note: you can disable saving completely by commenting out all "save" lines. +# +# It is also possible to remove all the previously configured save +# points by adding a save directive with a single empty string argument +# like in the following example: +# +save "" + +# save 900 1 +# save 300 10 +# save 60 10000 + +# By default Redis will stop accepting writes if RDB snapshots are enabled +# (at least one save point) and the latest background save failed. +# This will make the user aware (in a hard way) that data is not persisting +# on disk properly, otherwise chances are that no one will notice and some +# disaster will happen. +# +# If the background saving process will start working again Redis will +# automatically allow writes again. +# +# However if you have setup your proper monitoring of the Redis server +# and persistence, you may want to disable this feature so that Redis will +# continue to work as usual even if there are problems with disk, +# permissions, and so forth. +stop-writes-on-bgsave-error yes + +# Compress string objects using LZF when dump .rdb databases? +# By default compression is enabled as it's almost always a win. +# If you want to save some CPU in the saving child set it to 'no' but +# the dataset will likely be bigger if you have compressible values or keys. +rdbcompression yes + +# Since version 5 of RDB a CRC64 checksum is placed at the end of the file. +# This makes the format more resistant to corruption but there is a performance +# hit to pay (around 10%) when saving and loading RDB files, so you can disable it +# for maximum performances. +# +# RDB files created with checksum disabled have a checksum of zero that will +# tell the loading code to skip the check. +rdbchecksum yes + +# The filename where to dump the DB +dbfilename dump.rdb + +# Remove RDB files used by replication in instances without persistence +# enabled. By default this option is disabled, however there are environments +# where for regulations or other security concerns, RDB files persisted on +# disk by masters in order to feed replicas, or stored on disk by replicas +# in order to load them for the initial synchronization, should be deleted +# ASAP. Note that this option ONLY WORKS in instances that have both AOF +# and RDB persistence disabled, otherwise is completely ignored. +# +# An alternative (and sometimes better) way to obtain the same effect is +# to use diskless replication on both master and replicas instances. However +# in the case of replicas, diskless is not always an option. +rdb-del-sync-files no + +# The working directory. +# +# The DB will be written inside this directory, with the filename specified +# above using the 'dbfilename' configuration directive. +# +# The Append Only File will also be created inside this directory. +# +# Note that you must specify a directory here, not a file name. +dir ./ + +################################# REPLICATION ################################# + +# Master-Replica replication. Use replicaof to make a Redis instance a copy of +# another Redis server. A few things to understand ASAP about Redis replication. +# +# +------------------+ +---------------+ +# | Master | ---> | Replica | +# | (receive writes) | | (exact copy) | +# +------------------+ +---------------+ +# +# 1) Redis replication is asynchronous, but you can configure a master to +# stop accepting writes if it appears to be not connected with at least +# a given number of replicas. +# 2) Redis replicas are able to perform a partial resynchronization with the +# master if the replication link is lost for a relatively small amount of +# time. You may want to configure the replication backlog size (see the next +# sections of this file) with a sensible value depending on your needs. +# 3) Replication is automatic and does not need user intervention. After a +# network partition replicas automatically try to reconnect to masters +# and resynchronize with them. +# +# replicaof + +# If the master is password protected (using the "requirepass" configuration +# directive below) it is possible to tell the replica to authenticate before +# starting the replication synchronization process, otherwise the master will +# refuse the replica request. +# +# masterauth +# +# However this is not enough if you are using Redis ACLs (for Redis version +# 6 or greater), and the default user is not capable of running the PSYNC +# command and/or other commands needed for replication. In this case it's +# better to configure a special user to use with replication, and specify the +# masteruser configuration as such: +# +# masteruser +# +# When masteruser is specified, the replica will authenticate against its +# master using the new AUTH form: AUTH . + +# When a replica loses its connection with the master, or when the replication +# is still in progress, the replica can act in two different ways: +# +# 1) if replica-serve-stale-data is set to 'yes' (the default) the replica will +# still reply to client requests, possibly with out of date data, or the +# data set may just be empty if this is the first synchronization. +# +# 2) If replica-serve-stale-data is set to 'no' the replica will reply with +# an error "SYNC with master in progress" to all commands except: +# INFO, REPLICAOF, AUTH, PING, SHUTDOWN, REPLCONF, ROLE, CONFIG, SUBSCRIBE, +# UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE, PUBLISH, PUBSUB, COMMAND, POST, +# HOST and LATENCY. +# +replica-serve-stale-data yes + +# You can configure a replica instance to accept writes or not. Writing against +# a replica instance may be useful to store some ephemeral data (because data +# written on a replica will be easily deleted after resync with the master) but +# may also cause problems if clients are writing to it because of a +# misconfiguration. +# +# Since Redis 2.6 by default replicas are read-only. +# +# Note: read only replicas are not designed to be exposed to untrusted clients +# on the internet. It's just a protection layer against misuse of the instance. +# Still a read only replica exports by default all the administrative commands +# such as CONFIG, DEBUG, and so forth. To a limited extent you can improve +# security of read only replicas using 'rename-command' to shadow all the +# administrative / dangerous commands. +replica-read-only yes + +# Replication SYNC strategy: disk or socket. +# +# New replicas and reconnecting replicas that are not able to continue the +# replication process just receiving differences, need to do what is called a +# "full synchronization". An RDB file is transmitted from the master to the +# replicas. +# +# The transmission can happen in two different ways: +# +# 1) Disk-backed: The Redis master creates a new process that writes the RDB +# file on disk. Later the file is transferred by the parent +# process to the replicas incrementally. +# 2) Diskless: The Redis master creates a new process that directly writes the +# RDB file to replica sockets, without touching the disk at all. +# +# With disk-backed replication, while the RDB file is generated, more replicas +# can be queued and served with the RDB file as soon as the current child +# producing the RDB file finishes its work. With diskless replication instead +# once the transfer starts, new replicas arriving will be queued and a new +# transfer will start when the current one terminates. +# +# When diskless replication is used, the master waits a configurable amount of +# time (in seconds) before starting the transfer in the hope that multiple +# replicas will arrive and the transfer can be parallelized. +# +# With slow disks and fast (large bandwidth) networks, diskless replication +# works better. +repl-diskless-sync no + +# When diskless replication is enabled, it is possible to configure the delay +# the server waits in order to spawn the child that transfers the RDB via socket +# to the replicas. +# +# This is important since once the transfer starts, it is not possible to serve +# new replicas arriving, that will be queued for the next RDB transfer, so the +# server waits a delay in order to let more replicas arrive. +# +# The delay is specified in seconds, and by default is 5 seconds. To disable +# it entirely just set it to 0 seconds and the transfer will start ASAP. +repl-diskless-sync-delay 5 + +# ----------------------------------------------------------------------------- +# WARNING: RDB diskless load is experimental. Since in this setup the replica +# does not immediately store an RDB on disk, it may cause data loss during +# failovers. RDB diskless load + Redis modules not handling I/O reads may also +# cause Redis to abort in case of I/O errors during the initial synchronization +# stage with the master. Use only if your do what you are doing. +# ----------------------------------------------------------------------------- +# +# Replica can load the RDB it reads from the replication link directly from the +# socket, or store the RDB to a file and read that file after it was completely +# received from the master. +# +# In many cases the disk is slower than the network, and storing and loading +# the RDB file may increase replication time (and even increase the master's +# Copy on Write memory and salve buffers). +# However, parsing the RDB file directly from the socket may mean that we have +# to flush the contents of the current database before the full rdb was +# received. For this reason we have the following options: +# +# "disabled" - Don't use diskless load (store the rdb file to the disk first) +# "on-empty-db" - Use diskless load only when it is completely safe. +# "swapdb" - Keep a copy of the current db contents in RAM while parsing +# the data directly from the socket. note that this requires +# sufficient memory, if you don't have it, you risk an OOM kill. +repl-diskless-load disabled + +# Replicas send PINGs to server in a predefined interval. It's possible to +# change this interval with the repl_ping_replica_period option. The default +# value is 10 seconds. +# +# repl-ping-replica-period 10 + +# The following option sets the replication timeout for: +# +# 1) Bulk transfer I/O during SYNC, from the point of view of replica. +# 2) Master timeout from the point of view of replicas (data, pings). +# 3) Replica timeout from the point of view of masters (REPLCONF ACK pings). +# +# It is important to make sure that this value is greater than the value +# specified for repl-ping-replica-period otherwise a timeout will be detected +# every time there is low traffic between the master and the replica. The default +# value is 60 seconds. +# +# repl-timeout 60 + +# Disable TCP_NODELAY on the replica socket after SYNC? +# +# If you select "yes" Redis will use a smaller number of TCP packets and +# less bandwidth to send data to replicas. But this can add a delay for +# the data to appear on the replica side, up to 40 milliseconds with +# Linux kernels using a default configuration. +# +# If you select "no" the delay for data to appear on the replica side will +# be reduced but more bandwidth will be used for replication. +# +# By default we optimize for low latency, but in very high traffic conditions +# or when the master and replicas are many hops away, turning this to "yes" may +# be a good idea. +repl-disable-tcp-nodelay no + +# Set the replication backlog size. The backlog is a buffer that accumulates +# replica data when replicas are disconnected for some time, so that when a +# replica wants to reconnect again, often a full resync is not needed, but a +# partial resync is enough, just passing the portion of data the replica +# missed while disconnected. +# +# The bigger the replication backlog, the longer the replica can endure the +# disconnect and later be able to perform a partial resynchronization. +# +# The backlog is only allocated if there is at least one replica connected. +# +# repl-backlog-size 1mb + +# After a master has no connected replicas for some time, the backlog will be +# freed. The following option configures the amount of seconds that need to +# elapse, starting from the time the last replica disconnected, for the backlog +# buffer to be freed. +# +# Note that replicas never free the backlog for timeout, since they may be +# promoted to masters later, and should be able to correctly "partially +# resynchronize" with other replicas: hence they should always accumulate backlog. +# +# A value of 0 means to never release the backlog. +# +# repl-backlog-ttl 3600 + +# The replica priority is an integer number published by Redis in the INFO +# output. It is used by Redis Sentinel in order to select a replica to promote +# into a master if the master is no longer working correctly. +# +# A replica with a low priority number is considered better for promotion, so +# for instance if there are three replicas with priority 10, 100, 25 Sentinel +# will pick the one with priority 10, that is the lowest. +# +# However a special priority of 0 marks the replica as not able to perform the +# role of master, so a replica with priority of 0 will never be selected by +# Redis Sentinel for promotion. +# +# By default the priority is 100. +replica-priority 100 + +# It is possible for a master to stop accepting writes if there are less than +# N replicas connected, having a lag less or equal than M seconds. +# +# The N replicas need to be in "online" state. +# +# The lag in seconds, that must be <= the specified value, is calculated from +# the last ping received from the replica, that is usually sent every second. +# +# This option does not GUARANTEE that N replicas will accept the write, but +# will limit the window of exposure for lost writes in case not enough replicas +# are available, to the specified number of seconds. +# +# For example to require at least 3 replicas with a lag <= 10 seconds use: +# +# min-replicas-to-write 3 +# min-replicas-max-lag 10 +# +# Setting one or the other to 0 disables the feature. +# +# By default min-replicas-to-write is set to 0 (feature disabled) and +# min-replicas-max-lag is set to 10. + +# A Redis master is able to list the address and port of the attached +# replicas in different ways. For example the "INFO replication" section +# offers this information, which is used, among other tools, by +# Redis Sentinel in order to discover replica instances. +# Another place where this info is available is in the output of the +# "ROLE" command of a master. +# +# The listed IP address and port normally reported by a replica is +# obtained in the following way: +# +# IP: The address is auto detected by checking the peer address +# of the socket used by the replica to connect with the master. +# +# Port: The port is communicated by the replica during the replication +# handshake, and is normally the port that the replica is using to +# listen for connections. +# +# However when port forwarding or Network Address Translation (NAT) is +# used, the replica may actually be reachable via different IP and port +# pairs. The following two options can be used by a replica in order to +# report to its master a specific set of IP and port, so that both INFO +# and ROLE will report those values. +# +# There is no need to use both the options if you need to override just +# the port or the IP address. +# +# replica-announce-ip 5.5.5.5 +# replica-announce-port 1234 + +############################### KEYS TRACKING ################################# + +# Redis implements server assisted support for client side caching of values. +# This is implemented using an invalidation table that remembers, using +# 16 millions of slots, what clients may have certain subsets of keys. In turn +# this is used in order to send invalidation messages to clients. Please +# check this page to understand more about the feature: +# +# https://redis.io/topics/client-side-caching +# +# When tracking is enabled for a client, all the read only queries are assumed +# to be cached: this will force Redis to store information in the invalidation +# table. When keys are modified, such information is flushed away, and +# invalidation messages are sent to the clients. However if the workload is +# heavily dominated by reads, Redis could use more and more memory in order +# to track the keys fetched by many clients. +# +# For this reason it is possible to configure a maximum fill value for the +# invalidation table. By default it is set to 1M of keys, and once this limit +# is reached, Redis will start to evict keys in the invalidation table +# even if they were not modified, just to reclaim memory: this will in turn +# force the clients to invalidate the cached values. Basically the table +# maximum size is a trade off between the memory you want to spend server +# side to track information about who cached what, and the ability of clients +# to retain cached objects in memory. +# +# If you set the value to 0, it means there are no limits, and Redis will +# retain as many keys as needed in the invalidation table. +# In the "stats" INFO section, you can find information about the number of +# keys in the invalidation table at every given moment. +# +# Note: when key tracking is used in broadcasting mode, no memory is used +# in the server side so this setting is useless. +# +# tracking-table-max-keys 1000000 + +################################## SECURITY ################################### + +# Warning: since Redis is pretty fast, an outside user can try up to +# 1 million passwords per second against a modern box. This means that you +# should use very strong passwords, otherwise they will be very easy to break. +# Note that because the password is really a shared secret between the client +# and the server, and should not be memorized by any human, the password +# can be easily a long string from /dev/urandom or whatever, so by using a +# long and unguessable password no brute force attack will be possible. + +# Redis ACL users are defined in the following format: +# +# user ... acl rules ... +# +# For example: +# +# user worker +@list +@connection ~jobs:* on >ffa9203c493aa99 +# +# The special username "default" is used for new connections. If this user +# has the "nopass" rule, then new connections will be immediately authenticated +# as the "default" user without the need of any password provided via the +# AUTH command. Otherwise if the "default" user is not flagged with "nopass" +# the connections will start in not authenticated state, and will require +# AUTH (or the HELLO command AUTH option) in order to be authenticated and +# start to work. +# +# The ACL rules that describe what a user can do are the following: +# +# on Enable the user: it is possible to authenticate as this user. +# off Disable the user: it's no longer possible to authenticate +# with this user, however the already authenticated connections +# will still work. +# + Allow the execution of that command +# - Disallow the execution of that command +# +@ Allow the execution of all the commands in such category +# with valid categories are like @admin, @set, @sortedset, ... +# and so forth, see the full list in the server.c file where +# the Redis command table is described and defined. +# The special category @all means all the commands, but currently +# present in the server, and that will be loaded in the future +# via modules. +# +|subcommand Allow a specific subcommand of an otherwise +# disabled command. Note that this form is not +# allowed as negative like -DEBUG|SEGFAULT, but +# only additive starting with "+". +# allcommands Alias for +@all. Note that it implies the ability to execute +# all the future commands loaded via the modules system. +# nocommands Alias for -@all. +# ~ Add a pattern of keys that can be mentioned as part of +# commands. For instance ~* allows all the keys. The pattern +# is a glob-style pattern like the one of KEYS. +# It is possible to specify multiple patterns. +# allkeys Alias for ~* +# resetkeys Flush the list of allowed keys patterns. +# > Add this password to the list of valid password for the user. +# For example >mypass will add "mypass" to the list. +# This directive clears the "nopass" flag (see later). +# < Remove this password from the list of valid passwords. +# nopass All the set passwords of the user are removed, and the user +# is flagged as requiring no password: it means that every +# password will work against this user. If this directive is +# used for the default user, every new connection will be +# immediately authenticated with the default user without +# any explicit AUTH command required. Note that the "resetpass" +# directive will clear this condition. +# resetpass Flush the list of allowed passwords. Moreover removes the +# "nopass" status. After "resetpass" the user has no associated +# passwords and there is no way to authenticate without adding +# some password (or setting it as "nopass" later). +# reset Performs the following actions: resetpass, resetkeys, off, +# -@all. The user returns to the same state it has immediately +# after its creation. +# +# ACL rules can be specified in any order: for instance you can start with +# passwords, then flags, or key patterns. However note that the additive +# and subtractive rules will CHANGE MEANING depending on the ordering. +# For instance see the following example: +# +# user alice on +@all -DEBUG ~* >somepassword +# +# This will allow "alice" to use all the commands with the exception of the +# DEBUG command, since +@all added all the commands to the set of the commands +# alice can use, and later DEBUG was removed. However if we invert the order +# of two ACL rules the result will be different: +# +# user alice on -DEBUG +@all ~* >somepassword +# +# Now DEBUG was removed when alice had yet no commands in the set of allowed +# commands, later all the commands are added, so the user will be able to +# execute everything. +# +# Basically ACL rules are processed left-to-right. +# +# For more information about ACL configuration please refer to +# the Redis web site at https://redis.io/topics/acl + +# ACL LOG +# +# The ACL Log tracks failed commands and authentication events associated +# with ACLs. The ACL Log is useful to troubleshoot failed commands blocked +# by ACLs. The ACL Log is stored in memory. You can reclaim memory with +# ACL LOG RESET. Define the maximum entry length of the ACL Log below. +acllog-max-len 128 + +# Using an external ACL file +# +# Instead of configuring users here in this file, it is possible to use +# a stand-alone file just listing users. The two methods cannot be mixed: +# if you configure users here and at the same time you activate the external +# ACL file, the server will refuse to start. +# +# The format of the external ACL user file is exactly the same as the +# format that is used inside redis.conf to describe users. +# +# aclfile /etc/redis/users.acl + +# IMPORTANT NOTE: starting with Redis 6 "requirepass" is just a compatibility +# layer on top of the new ACL system. The option effect will be just setting +# the password for the default user. Clients will still authenticate using +# AUTH as usually, or more explicitly with AUTH default +# if they follow the new protocol: both will work. +# +# requirepass foobared + +# Command renaming (DEPRECATED). +# +# ------------------------------------------------------------------------ +# WARNING: avoid using this option if possible. Instead use ACLs to remove +# commands from the default user, and put them only in some admin user you +# create for administrative purposes. +# ------------------------------------------------------------------------ +# +# It is possible to change the name of dangerous commands in a shared +# environment. For instance the CONFIG command may be renamed into something +# hard to guess so that it will still be available for internal-use tools +# but not available for general clients. +# +# Example: +# +# rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52 +# +# It is also possible to completely kill a command by renaming it into +# an empty string: +# +# rename-command CONFIG "" +# +# Please note that changing the name of commands that are logged into the +# AOF file or transmitted to replicas may cause problems. + +################################### CLIENTS #################################### + +# Set the max number of connected clients at the same time. By default +# this limit is set to 10000 clients, however if the Redis server is not +# able to configure the process file limit to allow for the specified limit +# the max number of allowed clients is set to the current file limit +# minus 32 (as Redis reserves a few file descriptors for internal uses). +# +# Once the limit is reached Redis will close all the new connections sending +# an error 'max number of clients reached'. +# +# IMPORTANT: When Redis Cluster is used, the max number of connections is also +# shared with the cluster bus: every node in the cluster will use two +# connections, one incoming and another outgoing. It is important to size the +# limit accordingly in case of very large clusters. +# +# maxclients 10000 + +############################## MEMORY MANAGEMENT ################################ + +# Set a memory usage limit to the specified amount of bytes. +# When the memory limit is reached Redis will try to remove keys +# according to the eviction policy selected (see maxmemory-policy). +# +# If Redis can't remove keys according to the policy, or if the policy is +# set to 'noeviction', Redis will start to reply with errors to commands +# that would use more memory, like SET, LPUSH, and so on, and will continue +# to reply to read-only commands like GET. +# +# This option is usually useful when using Redis as an LRU or LFU cache, or to +# set a hard memory limit for an instance (using the 'noeviction' policy). +# +# WARNING: If you have replicas attached to an instance with maxmemory on, +# the size of the output buffers needed to feed the replicas are subtracted +# from the used memory count, so that network problems / resyncs will +# not trigger a loop where keys are evicted, and in turn the output +# buffer of replicas is full with DELs of keys evicted triggering the deletion +# of more keys, and so forth until the database is completely emptied. +# +# In short... if you have replicas attached it is suggested that you set a lower +# limit for maxmemory so that there is some free RAM on the system for replica +# output buffers (but this is not needed if the policy is 'noeviction'). +# +# maxmemory + +# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory +# is reached. You can select one from the following behaviors: +# +# volatile-lru -> Evict using approximated LRU, only keys with an expire set. +# allkeys-lru -> Evict any key using approximated LRU. +# volatile-lfu -> Evict using approximated LFU, only keys with an expire set. +# allkeys-lfu -> Evict any key using approximated LFU. +# volatile-random -> Remove a random key having an expire set. +# allkeys-random -> Remove a random key, any key. +# volatile-ttl -> Remove the key with the nearest expire time (minor TTL) +# noeviction -> Don't evict anything, just return an error on write operations. +# +# LRU means Least Recently Used +# LFU means Least Frequently Used +# +# Both LRU, LFU and volatile-ttl are implemented using approximated +# randomized algorithms. +# +# Note: with any of the above policies, Redis will return an error on write +# operations, when there are no suitable keys for eviction. +# +# At the date of writing these commands are: set setnx setex append +# incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd +# sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby +# zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby +# getset mset msetnx exec sort +# +# The default is: +# +# maxmemory-policy noeviction + +# LRU, LFU and minimal TTL algorithms are not precise algorithms but approximated +# algorithms (in order to save memory), so you can tune it for speed or +# accuracy. By default Redis will check five keys and pick the one that was +# used least recently, you can change the sample size using the following +# configuration directive. +# +# The default of 5 produces good enough results. 10 Approximates very closely +# true LRU but costs more CPU. 3 is faster but not very accurate. +# +# maxmemory-samples 5 + +# Starting from Redis 5, by default a replica will ignore its maxmemory setting +# (unless it is promoted to master after a failover or manually). It means +# that the eviction of keys will be just handled by the master, sending the +# DEL commands to the replica as keys evict in the master side. +# +# This behavior ensures that masters and replicas stay consistent, and is usually +# what you want, however if your replica is writable, or you want the replica +# to have a different memory setting, and you are sure all the writes performed +# to the replica are idempotent, then you may change this default (but be sure +# to understand what you are doing). +# +# Note that since the replica by default does not evict, it may end using more +# memory than the one set via maxmemory (there are certain buffers that may +# be larger on the replica, or data structures may sometimes take more memory +# and so forth). So make sure you monitor your replicas and make sure they +# have enough memory to never hit a real out-of-memory condition before the +# master hits the configured maxmemory setting. +# +# replica-ignore-maxmemory yes + +# Redis reclaims expired keys in two ways: upon access when those keys are +# found to be expired, and also in background, in what is called the +# "active expire key". The key space is slowly and interactively scanned +# looking for expired keys to reclaim, so that it is possible to free memory +# of keys that are expired and will never be accessed again in a short time. +# +# The default effort of the expire cycle will try to avoid having more than +# ten percent of expired keys still in memory, and will try to avoid consuming +# more than 25% of total memory and to add latency to the system. However +# it is possible to increase the expire "effort" that is normally set to +# "1", to a greater value, up to the value "10". At its maximum value the +# system will use more CPU, longer cycles (and technically may introduce +# more latency), and will tolerate less already expired keys still present +# in the system. It's a tradeoff between memory, CPU and latency. +# +# active-expire-effort 1 + +############################# LAZY FREEING #################################### + +# Redis has two primitives to delete keys. One is called DEL and is a blocking +# deletion of the object. It means that the server stops processing new commands +# in order to reclaim all the memory associated with an object in a synchronous +# way. If the key deleted is associated with a small object, the time needed +# in order to execute the DEL command is very small and comparable to most other +# O(1) or O(log_N) commands in Redis. However if the key is associated with an +# aggregated value containing millions of elements, the server can block for +# a long time (even seconds) in order to complete the operation. +# +# For the above reasons Redis also offers non blocking deletion primitives +# such as UNLINK (non blocking DEL) and the ASYNC option of FLUSHALL and +# FLUSHDB commands, in order to reclaim memory in background. Those commands +# are executed in constant time. Another thread will incrementally free the +# object in the background as fast as possible. +# +# DEL, UNLINK and ASYNC option of FLUSHALL and FLUSHDB are user-controlled. +# It's up to the design of the application to understand when it is a good +# idea to use one or the other. However the Redis server sometimes has to +# delete keys or flush the whole database as a side effect of other operations. +# Specifically Redis deletes objects independently of a user call in the +# following scenarios: +# +# 1) On eviction, because of the maxmemory and maxmemory policy configurations, +# in order to make room for new data, without going over the specified +# memory limit. +# 2) Because of expire: when a key with an associated time to live (see the +# EXPIRE command) must be deleted from memory. +# 3) Because of a side effect of a command that stores data on a key that may +# already exist. For example the RENAME command may delete the old key +# content when it is replaced with another one. Similarly SUNIONSTORE +# or SORT with STORE option may delete existing keys. The SET command +# itself removes any old content of the specified key in order to replace +# it with the specified string. +# 4) During replication, when a replica performs a full resynchronization with +# its master, the content of the whole database is removed in order to +# load the RDB file just transferred. +# +# In all the above cases the default is to delete objects in a blocking way, +# like if DEL was called. However you can configure each case specifically +# in order to instead release memory in a non-blocking way like if UNLINK +# was called, using the following configuration directives. + +lazyfree-lazy-eviction no +lazyfree-lazy-expire no +lazyfree-lazy-server-del no +replica-lazy-flush no + +# It is also possible, for the case when to replace the user code DEL calls +# with UNLINK calls is not easy, to modify the default behavior of the DEL +# command to act exactly like UNLINK, using the following configuration +# directive: + +lazyfree-lazy-user-del no + +################################ THREADED I/O ################################# + +# Redis is mostly single threaded, however there are certain threaded +# operations such as UNLINK, slow I/O accesses and other things that are +# performed on side threads. +# +# Now it is also possible to handle Redis clients socket reads and writes +# in different I/O threads. Since especially writing is so slow, normally +# Redis users use pipelining in order to speed up the Redis performances per +# core, and spawn multiple instances in order to scale more. Using I/O +# threads it is possible to easily speedup two times Redis without resorting +# to pipelining nor sharding of the instance. +# +# By default threading is disabled, we suggest enabling it only in machines +# that have at least 4 or more cores, leaving at least one spare core. +# Using more than 8 threads is unlikely to help much. We also recommend using +# threaded I/O only if you actually have performance problems, with Redis +# instances being able to use a quite big percentage of CPU time, otherwise +# there is no point in using this feature. +# +# So for instance if you have a four cores boxes, try to use 2 or 3 I/O +# threads, if you have a 8 cores, try to use 6 threads. In order to +# enable I/O threads use the following configuration directive: +# +# io-threads 4 +# +# Setting io-threads to 1 will just use the main thread as usual. +# When I/O threads are enabled, we only use threads for writes, that is +# to thread the write(2) syscall and transfer the client buffers to the +# socket. However it is also possible to enable threading of reads and +# protocol parsing using the following configuration directive, by setting +# it to yes: +# +# io-threads-do-reads no +# +# Usually threading reads doesn't help much. +# +# NOTE 1: This configuration directive cannot be changed at runtime via +# CONFIG SET. Aso this feature currently does not work when SSL is +# enabled. +# +# NOTE 2: If you want to test the Redis speedup using redis-benchmark, make +# sure you also run the benchmark itself in threaded mode, using the +# --threads option to match the number of Redis threads, otherwise you'll not +# be able to notice the improvements. + +############################ KERNEL OOM CONTROL ############################## + +# On Linux, it is possible to hint the kernel OOM killer on what processes +# should be killed first when out of memory. +# +# Enabling this feature makes Redis actively control the oom_score_adj value +# for all its processes, depending on their role. The default scores will +# attempt to have background child processes killed before all others, and +# replicas killed before masters. +# +# Redis supports three options: +# +# no: Don't make changes to oom-score-adj (default). +# yes: Alias to "relative" see below. +# absolute: Values in oom-score-adj-values are written as is to the kernel. +# relative: Values are used relative to the initial value of oom_score_adj when +# the server starts and are then clamped to a range of -1000 to 1000. +# Because typically the initial value is 0, they will often match the +# absolute values. +oom-score-adj no + +# When oom-score-adj is used, this directive controls the specific values used +# for master, replica and background child processes. Values range -2000 to +# 2000 (higher means more likely to be killed). +# +# Unprivileged processes (not root, and without CAP_SYS_RESOURCE capabilities) +# can freely increase their value, but not decrease it below its initial +# settings. This means that setting oom-score-adj to "relative" and setting the +# oom-score-adj-values to positive values will always succeed. +oom-score-adj-values 0 200 800 + +############################## APPEND ONLY MODE ############################### + +# By default Redis asynchronously dumps the dataset on disk. This mode is +# good enough in many applications, but an issue with the Redis process or +# a power outage may result into a few minutes of writes lost (depending on +# the configured save points). +# +# The Append Only File is an alternative persistence mode that provides +# much better durability. For instance using the default data fsync policy +# (see later in the config file) Redis can lose just one second of writes in a +# dramatic event like a server power outage, or a single write if something +# wrong with the Redis process itself happens, but the operating system is +# still running correctly. +# +# AOF and RDB persistence can be enabled at the same time without problems. +# If the AOF is enabled on startup Redis will load the AOF, that is the file +# with the better durability guarantees. +# +# Please check http://redis.io/topics/persistence for more information. + +appendonly yes + +# The name of the append only file (default: "appendonly.aof") + +appendfilename "appendonly.aof" + +# The fsync() call tells the Operating System to actually write data on disk +# instead of waiting for more data in the output buffer. Some OS will really flush +# data on disk, some other OS will just try to do it ASAP. +# +# Redis supports three different modes: +# +# no: don't fsync, just let the OS flush the data when it wants. Faster. +# always: fsync after every write to the append only log. Slow, Safest. +# everysec: fsync only one time every second. Compromise. +# +# The default is "everysec", as that's usually the right compromise between +# speed and data safety. It's up to you to understand if you can relax this to +# "no" that will let the operating system flush the output buffer when +# it wants, for better performances (but if you can live with the idea of +# some data loss consider the default persistence mode that's snapshotting), +# or on the contrary, use "always" that's very slow but a bit safer than +# everysec. +# +# More details please check the following article: +# http://antirez.com/post/redis-persistence-demystified.html +# +# If unsure, use "everysec". + +# appendfsync always +appendfsync everysec +# appendfsync no + +# When the AOF fsync policy is set to always or everysec, and a background +# saving process (a background save or AOF log background rewriting) is +# performing a lot of I/O against the disk, in some Linux configurations +# Redis may block too long on the fsync() call. Note that there is no fix for +# this currently, as even performing fsync in a different thread will block +# our synchronous write(2) call. +# +# In order to mitigate this problem it's possible to use the following option +# that will prevent fsync() from being called in the main process while a +# BGSAVE or BGREWRITEAOF is in progress. +# +# This means that while another child is saving, the durability of Redis is +# the same as "appendfsync none". In practical terms, this means that it is +# possible to lose up to 30 seconds of log in the worst scenario (with the +# default Linux settings). +# +# If you have latency problems turn this to "yes". Otherwise leave it as +# "no" that is the safest pick from the point of view of durability. + +no-appendfsync-on-rewrite no + +# Automatic rewrite of the append only file. +# Redis is able to automatically rewrite the log file implicitly calling +# BGREWRITEAOF when the AOF log size grows by the specified percentage. +# +# This is how it works: Redis remembers the size of the AOF file after the +# latest rewrite (if no rewrite has happened since the restart, the size of +# the AOF at startup is used). +# +# This base size is compared to the current size. If the current size is +# bigger than the specified percentage, the rewrite is triggered. Also +# you need to specify a minimal size for the AOF file to be rewritten, this +# is useful to avoid rewriting the AOF file even if the percentage increase +# is reached but it is still pretty small. +# +# Specify a percentage of zero in order to disable the automatic AOF +# rewrite feature. + +auto-aof-rewrite-percentage 100 +auto-aof-rewrite-min-size 64mb + +# An AOF file may be found to be truncated at the end during the Redis +# startup process, when the AOF data gets loaded back into memory. +# This may happen when the system where Redis is running +# crashes, especially when an ext4 filesystem is mounted without the +# data=ordered option (however this can't happen when Redis itself +# crashes or aborts but the operating system still works correctly). +# +# Redis can either exit with an error when this happens, or load as much +# data as possible (the default now) and start if the AOF file is found +# to be truncated at the end. The following option controls this behavior. +# +# If aof-load-truncated is set to yes, a truncated AOF file is loaded and +# the Redis server starts emitting a log to inform the user of the event. +# Otherwise if the option is set to no, the server aborts with an error +# and refuses to start. When the option is set to no, the user requires +# to fix the AOF file using the "redis-check-aof" utility before to restart +# the server. +# +# Note that if the AOF file will be found to be corrupted in the middle +# the server will still exit with an error. This option only applies when +# Redis will try to read more data from the AOF file but not enough bytes +# will be found. +aof-load-truncated yes + +# When rewriting the AOF file, Redis is able to use an RDB preamble in the +# AOF file for faster rewrites and recoveries. When this option is turned +# on the rewritten AOF file is composed of two different stanzas: +# +# [RDB file][AOF tail] +# +# When loading, Redis recognizes that the AOF file starts with the "REDIS" +# string and loads the prefixed RDB file, then continues loading the AOF +# tail. +aof-use-rdb-preamble yes + +################################ LUA SCRIPTING ############################### + +# Max execution time of a Lua script in milliseconds. +# +# If the maximum execution time is reached Redis will log that a script is +# still in execution after the maximum allowed time and will start to +# reply to queries with an error. +# +# When a long running script exceeds the maximum execution time only the +# SCRIPT KILL and SHUTDOWN NOSAVE commands are available. The first can be +# used to stop a script that did not yet call any write commands. The second +# is the only way to shut down the server in the case a write command was +# already issued by the script but the user doesn't want to wait for the natural +# termination of the script. +# +# Set it to 0 or a negative value for unlimited execution without warnings. +lua-time-limit 5000 + +################################ REDIS CLUSTER ############################### + +# Normal Redis instances can't be part of a Redis Cluster; only nodes that are +# started as cluster nodes can. In order to start a Redis instance as a +# cluster node enable the cluster support uncommenting the following: +# +# cluster-enabled yes + +# Every cluster node has a cluster configuration file. This file is not +# intended to be edited by hand. It is created and updated by Redis nodes. +# Every Redis Cluster node requires a different cluster configuration file. +# Make sure that instances running in the same system do not have +# overlapping cluster configuration file names. +# +# cluster-config-file nodes-6379.conf + +# Cluster node timeout is the amount of milliseconds a node must be unreachable +# for it to be considered in failure state. +# Most other internal time limits are a multiple of the node timeout. +# +# cluster-node-timeout 15000 + +# A replica of a failing master will avoid to start a failover if its data +# looks too old. +# +# There is no simple way for a replica to actually have an exact measure of +# its "data age", so the following two checks are performed: +# +# 1) If there are multiple replicas able to failover, they exchange messages +# in order to try to give an advantage to the replica with the best +# replication offset (more data from the master processed). +# Replicas will try to get their rank by offset, and apply to the start +# of the failover a delay proportional to their rank. +# +# 2) Every single replica computes the time of the last interaction with +# its master. This can be the last ping or command received (if the master +# is still in the "connected" state), or the time that elapsed since the +# disconnection with the master (if the replication link is currently down). +# If the last interaction is too old, the replica will not try to failover +# at all. +# +# The point "2" can be tuned by user. Specifically a replica will not perform +# the failover if, since the last interaction with the master, the time +# elapsed is greater than: +# +# (node-timeout * cluster-replica-validity-factor) + repl-ping-replica-period +# +# So for example if node-timeout is 30 seconds, and the cluster-replica-validity-factor +# is 10, and assuming a default repl-ping-replica-period of 10 seconds, the +# replica will not try to failover if it was not able to talk with the master +# for longer than 310 seconds. +# +# A large cluster-replica-validity-factor may allow replicas with too old data to failover +# a master, while a too small value may prevent the cluster from being able to +# elect a replica at all. +# +# For maximum availability, it is possible to set the cluster-replica-validity-factor +# to a value of 0, which means, that replicas will always try to failover the +# master regardless of the last time they interacted with the master. +# (However they'll always try to apply a delay proportional to their +# offset rank). +# +# Zero is the only value able to guarantee that when all the partitions heal +# the cluster will always be able to continue. +# +# cluster-replica-validity-factor 10 + +# Cluster replicas are able to migrate to orphaned masters, that are masters +# that are left without working replicas. This improves the cluster ability +# to resist to failures as otherwise an orphaned master can't be failed over +# in case of failure if it has no working replicas. +# +# Replicas migrate to orphaned masters only if there are still at least a +# given number of other working replicas for their old master. This number +# is the "migration barrier". A migration barrier of 1 means that a replica +# will migrate only if there is at least 1 other working replica for its master +# and so forth. It usually reflects the number of replicas you want for every +# master in your cluster. +# +# Default is 1 (replicas migrate only if their masters remain with at least +# one replica). To disable migration just set it to a very large value. +# A value of 0 can be set but is useful only for debugging and dangerous +# in production. +# +# cluster-migration-barrier 1 + +# By default Redis Cluster nodes stop accepting queries if they detect there +# is at least a hash slot uncovered (no available node is serving it). +# This way if the cluster is partially down (for example a range of hash slots +# are no longer covered) all the cluster becomes, eventually, unavailable. +# It automatically returns available as soon as all the slots are covered again. +# +# However sometimes you want the subset of the cluster which is working, +# to continue to accept queries for the part of the key space that is still +# covered. In order to do so, just set the cluster-require-full-coverage +# option to no. +# +# cluster-require-full-coverage yes + +# This option, when set to yes, prevents replicas from trying to failover its +# master during master failures. However the master can still perform a +# manual failover, if forced to do so. +# +# This is useful in different scenarios, especially in the case of multiple +# data center operations, where we want one side to never be promoted if not +# in the case of a total DC failure. +# +# cluster-replica-no-failover no + +# This option, when set to yes, allows nodes to serve read traffic while the +# the cluster is in a down state, as long as it believes it owns the slots. +# +# This is useful for two cases. The first case is for when an application +# doesn't require consistency of data during node failures or network partitions. +# One example of this is a cache, where as long as the node has the data it +# should be able to serve it. +# +# The second use case is for configurations that don't meet the recommended +# three shards but want to enable cluster mode and scale later. A +# master outage in a 1 or 2 shard configuration causes a read/write outage to the +# entire cluster without this option set, with it set there is only a write outage. +# Without a quorum of masters, slot ownership will not change automatically. +# +# cluster-allow-reads-when-down no + +# In order to setup your cluster make sure to read the documentation +# available at http://redis.io web site. + +########################## CLUSTER DOCKER/NAT support ######################## + +# In certain deployments, Redis Cluster nodes address discovery fails, because +# addresses are NAT-ted or because ports are forwarded (the typical case is +# Docker and other containers). +# +# In order to make Redis Cluster working in such environments, a static +# configuration where each node knows its public address is needed. The +# following two options are used for this scope, and are: +# +# * cluster-announce-ip +# * cluster-announce-port +# * cluster-announce-bus-port +# +# Each instructs the node about its address, client port, and cluster message +# bus port. The information is then published in the header of the bus packets +# so that other nodes will be able to correctly map the address of the node +# publishing the information. +# +# If the above options are not used, the normal Redis Cluster auto-detection +# will be used instead. +# +# Note that when remapped, the bus port may not be at the fixed offset of +# clients port + 10000, so you can specify any port and bus-port depending +# on how they get remapped. If the bus-port is not set, a fixed offset of +# 10000 will be used as usual. +# +# Example: +# +# cluster-announce-ip 10.1.1.5 +# cluster-announce-port 6379 +# cluster-announce-bus-port 6380 + +################################## SLOW LOG ################################### + +# The Redis Slow Log is a system to log queries that exceeded a specified +# execution time. The execution time does not include the I/O operations +# like talking with the client, sending the reply and so forth, +# but just the time needed to actually execute the command (this is the only +# stage of command execution where the thread is blocked and can not serve +# other requests in the meantime). +# +# You can configure the slow log with two parameters: one tells Redis +# what is the execution time, in microseconds, to exceed in order for the +# command to get logged, and the other parameter is the length of the +# slow log. When a new command is logged the oldest one is removed from the +# queue of logged commands. + +# The following time is expressed in microseconds, so 1000000 is equivalent +# to one second. Note that a negative number disables the slow log, while +# a value of zero forces the logging of every command. +slowlog-log-slower-than 10000 + +# There is no limit to this length. Just be aware that it will consume memory. +# You can reclaim memory used by the slow log with SLOWLOG RESET. +slowlog-max-len 128 + +################################ LATENCY MONITOR ############################## + +# The Redis latency monitoring subsystem samples different operations +# at runtime in order to collect data related to possible sources of +# latency of a Redis instance. +# +# Via the LATENCY command this information is available to the user that can +# print graphs and obtain reports. +# +# The system only logs operations that were performed in a time equal or +# greater than the amount of milliseconds specified via the +# latency-monitor-threshold configuration directive. When its value is set +# to zero, the latency monitor is turned off. +# +# By default latency monitoring is disabled since it is mostly not needed +# if you don't have latency issues, and collecting data has a performance +# impact, that while very small, can be measured under big load. Latency +# monitoring can easily be enabled at runtime using the command +# "CONFIG SET latency-monitor-threshold " if needed. +latency-monitor-threshold 0 + +############################# EVENT NOTIFICATION ############################## + +# Redis can notify Pub/Sub clients about events happening in the key space. +# This feature is documented at http://redis.io/topics/notifications +# +# For instance if keyspace events notification is enabled, and a client +# performs a DEL operation on key "foo" stored in the Database 0, two +# messages will be published via Pub/Sub: +# +# PUBLISH __keyspace@0__:foo del +# PUBLISH __keyevent@0__:del foo +# +# It is possible to select the events that Redis will notify among a set +# of classes. Every class is identified by a single character: +# +# K Keyspace events, published with __keyspace@__ prefix. +# E Keyevent events, published with __keyevent@__ prefix. +# g Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ... +# $ String commands +# l List commands +# s Set commands +# h Hash commands +# z Sorted set commands +# x Expired events (events generated every time a key expires) +# e Evicted events (events generated when a key is evicted for maxmemory) +# t Stream commands +# m Key-miss events (Note: It is not included in the 'A' class) +# A Alias for g$lshzxet, so that the "AKE" string means all the events +# (Except key-miss events which are excluded from 'A' due to their +# unique nature). +# +# The "notify-keyspace-events" takes as argument a string that is composed +# of zero or multiple characters. The empty string means that notifications +# are disabled. +# +# Example: to enable list and generic events, from the point of view of the +# event name, use: +# +# notify-keyspace-events Elg +# +# Example 2: to get the stream of the expired keys subscribing to channel +# name __keyevent@0__:expired use: +# +# notify-keyspace-events Ex +# +# By default all notifications are disabled because most users don't need +# this feature and the feature has some overhead. Note that if you don't +# specify at least one of K or E, no events will be delivered. +notify-keyspace-events "" + +############################### GOPHER SERVER ################################# + +# Redis contains an implementation of the Gopher protocol, as specified in +# the RFC 1436 (https://www.ietf.org/rfc/rfc1436.txt). +# +# The Gopher protocol was very popular in the late '90s. It is an alternative +# to the web, and the implementation both server and client side is so simple +# that the Redis server has just 100 lines of code in order to implement this +# support. +# +# What do you do with Gopher nowadays? Well Gopher never *really* died, and +# lately there is a movement in order for the Gopher more hierarchical content +# composed of just plain text documents to be resurrected. Some want a simpler +# internet, others believe that the mainstream internet became too much +# controlled, and it's cool to create an alternative space for people that +# want a bit of fresh air. +# +# Anyway for the 10nth birthday of the Redis, we gave it the Gopher protocol +# as a gift. +# +# --- HOW IT WORKS? --- +# +# The Redis Gopher support uses the inline protocol of Redis, and specifically +# two kind of inline requests that were anyway illegal: an empty request +# or any request that starts with "/" (there are no Redis commands starting +# with such a slash). Normal RESP2/RESP3 requests are completely out of the +# path of the Gopher protocol implementation and are served as usual as well. +# +# If you open a connection to Redis when Gopher is enabled and send it +# a string like "/foo", if there is a key named "/foo" it is served via the +# Gopher protocol. +# +# In order to create a real Gopher "hole" (the name of a Gopher site in Gopher +# talking), you likely need a script like the following: +# +# https://github.com/antirez/gopher2redis +# +# --- SECURITY WARNING --- +# +# If you plan to put Redis on the internet in a publicly accessible address +# to server Gopher pages MAKE SURE TO SET A PASSWORD to the instance. +# Once a password is set: +# +# 1. The Gopher server (when enabled, not by default) will still serve +# content via Gopher. +# 2. However other commands cannot be called before the client will +# authenticate. +# +# So use the 'requirepass' option to protect your instance. +# +# Note that Gopher is not currently supported when 'io-threads-do-reads' +# is enabled. +# +# To enable Gopher support, uncomment the following line and set the option +# from no (the default) to yes. +# +# gopher-enabled no + +############################### ADVANCED CONFIG ############################### + +# Hashes are encoded using a memory efficient data structure when they have a +# small number of entries, and the biggest entry does not exceed a given +# threshold. These thresholds can be configured using the following directives. +hash-max-ziplist-entries 512 +hash-max-ziplist-value 64 + +# Lists are also encoded in a special way to save a lot of space. +# The number of entries allowed per internal list node can be specified +# as a fixed maximum size or a maximum number of elements. +# For a fixed maximum size, use -5 through -1, meaning: +# -5: max size: 64 Kb <-- not recommended for normal workloads +# -4: max size: 32 Kb <-- not recommended +# -3: max size: 16 Kb <-- probably not recommended +# -2: max size: 8 Kb <-- good +# -1: max size: 4 Kb <-- good +# Positive numbers mean store up to _exactly_ that number of elements +# per list node. +# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size), +# but if your use case is unique, adjust the settings as necessary. +list-max-ziplist-size -2 + +# Lists may also be compressed. +# Compress depth is the number of quicklist ziplist nodes from *each* side of +# the list to *exclude* from compression. The head and tail of the list +# are always uncompressed for fast push/pop operations. Settings are: +# 0: disable all list compression +# 1: depth 1 means "don't start compressing until after 1 node into the list, +# going from either the head or tail" +# So: [head]->node->node->...->node->[tail] +# [head], [tail] will always be uncompressed; inner nodes will compress. +# 2: [head]->[next]->node->node->...->node->[prev]->[tail] +# 2 here means: don't compress head or head->next or tail->prev or tail, +# but compress all nodes between them. +# 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail] +# etc. +list-compress-depth 0 + +# Sets have a special encoding in just one case: when a set is composed +# of just strings that happen to be integers in radix 10 in the range +# of 64 bit signed integers. +# The following configuration setting sets the limit in the size of the +# set in order to use this special memory saving encoding. +set-max-intset-entries 512 + +# Similarly to hashes and lists, sorted sets are also specially encoded in +# order to save a lot of space. This encoding is only used when the length and +# elements of a sorted set are below the following limits: +zset-max-ziplist-entries 128 +zset-max-ziplist-value 64 + +# HyperLogLog sparse representation bytes limit. The limit includes the +# 16 bytes header. When an HyperLogLog using the sparse representation crosses +# this limit, it is converted into the dense representation. +# +# A value greater than 16000 is totally useless, since at that point the +# dense representation is more memory efficient. +# +# The suggested value is ~ 3000 in order to have the benefits of +# the space efficient encoding without slowing down too much PFADD, +# which is O(N) with the sparse encoding. The value can be raised to +# ~ 10000 when CPU is not a concern, but space is, and the data set is +# composed of many HyperLogLogs with cardinality in the 0 - 15000 range. +hll-sparse-max-bytes 3000 + +# Streams macro node max size / items. The stream data structure is a radix +# tree of big nodes that encode multiple items inside. Using this configuration +# it is possible to configure how big a single node can be in bytes, and the +# maximum number of items it may contain before switching to a new node when +# appending new stream entries. If any of the following settings are set to +# zero, the limit is ignored, so for instance it is possible to set just a +# max entires limit by setting max-bytes to 0 and max-entries to the desired +# value. +stream-node-max-bytes 4096 +stream-node-max-entries 100 + +# Active rehashing uses 1 millisecond every 100 milliseconds of CPU time in +# order to help rehashing the main Redis hash table (the one mapping top-level +# keys to values). The hash table implementation Redis uses (see dict.c) +# performs a lazy rehashing: the more operation you run into a hash table +# that is rehashing, the more rehashing "steps" are performed, so if the +# server is idle the rehashing is never complete and some more memory is used +# by the hash table. +# +# The default is to use this millisecond 10 times every second in order to +# actively rehash the main dictionaries, freeing memory when possible. +# +# If unsure: +# use "activerehashing no" if you have hard latency requirements and it is +# not a good thing in your environment that Redis can reply from time to time +# to queries with 2 milliseconds delay. +# +# use "activerehashing yes" if you don't have such hard requirements but +# want to free memory asap when possible. +activerehashing yes + +# The client output buffer limits can be used to force disconnection of clients +# that are not reading data from the server fast enough for some reason (a +# common reason is that a Pub/Sub client can't consume messages as fast as the +# publisher can produce them). +# +# The limit can be set differently for the three different classes of clients: +# +# normal -> normal clients including MONITOR clients +# replica -> replica clients +# pubsub -> clients subscribed to at least one pubsub channel or pattern +# +# The syntax of every client-output-buffer-limit directive is the following: +# +# client-output-buffer-limit +# +# A client is immediately disconnected once the hard limit is reached, or if +# the soft limit is reached and remains reached for the specified number of +# seconds (continuously). +# So for instance if the hard limit is 32 megabytes and the soft limit is +# 16 megabytes / 10 seconds, the client will get disconnected immediately +# if the size of the output buffers reach 32 megabytes, but will also get +# disconnected if the client reaches 16 megabytes and continuously overcomes +# the limit for 10 seconds. +# +# By default normal clients are not limited because they don't receive data +# without asking (in a push way), but just after a request, so only +# asynchronous clients may create a scenario where data is requested faster +# than it can read. +# +# Instead there is a default limit for pubsub and replica clients, since +# subscribers and replicas receive data in a push fashion. +# +# Both the hard or the soft limit can be disabled by setting them to zero. +client-output-buffer-limit normal 0 0 0 +client-output-buffer-limit replica 256mb 64mb 60 +client-output-buffer-limit pubsub 32mb 8mb 60 + +# Client query buffers accumulate new commands. They are limited to a fixed +# amount by default in order to avoid that a protocol desynchronization (for +# instance due to a bug in the client) will lead to unbound memory usage in +# the query buffer. However you can configure it here if you have very special +# needs, such us huge multi/exec requests or alike. +# +# client-query-buffer-limit 1gb + +# In the Redis protocol, bulk requests, that are, elements representing single +# strings, are normally limited to 512 mb. However you can change this limit +# here, but must be 1mb or greater +# +# proto-max-bulk-len 512mb + +# Redis calls an internal function to perform many background tasks, like +# closing connections of clients in timeout, purging expired keys that are +# never requested, and so forth. +# +# Not all tasks are performed with the same frequency, but Redis checks for +# tasks to perform according to the specified "hz" value. +# +# By default "hz" is set to 10. Raising the value will use more CPU when +# Redis is idle, but at the same time will make Redis more responsive when +# there are many keys expiring at the same time, and timeouts may be +# handled with more precision. +# +# The range is between 1 and 500, however a value over 100 is usually not +# a good idea. Most users should use the default of 10 and raise this up to +# 100 only in environments where very low latency is required. +hz 10 + +# Normally it is useful to have an HZ value which is proportional to the +# number of clients connected. This is useful in order, for instance, to +# avoid too many clients are processed for each background task invocation +# in order to avoid latency spikes. +# +# Since the default HZ value by default is conservatively set to 10, Redis +# offers, and enables by default, the ability to use an adaptive HZ value +# which will temporarily raise when there are many connected clients. +# +# When dynamic HZ is enabled, the actual configured HZ will be used +# as a baseline, but multiples of the configured HZ value will be actually +# used as needed once more clients are connected. In this way an idle +# instance will use very little CPU time while a busy instance will be +# more responsive. +dynamic-hz yes + +# When a child rewrites the AOF file, if the following option is enabled +# the file will be fsync-ed every 32 MB of data generated. This is useful +# in order to commit the file to the disk more incrementally and avoid +# big latency spikes. +aof-rewrite-incremental-fsync yes + +# When redis saves RDB file, if the following option is enabled +# the file will be fsync-ed every 32 MB of data generated. This is useful +# in order to commit the file to the disk more incrementally and avoid +# big latency spikes. +rdb-save-incremental-fsync yes + +# Redis LFU eviction (see maxmemory setting) can be tuned. However it is a good +# idea to start with the default settings and only change them after investigating +# how to improve the performances and how the keys LFU change over time, which +# is possible to inspect via the OBJECT FREQ command. +# +# There are two tunable parameters in the Redis LFU implementation: the +# counter logarithm factor and the counter decay time. It is important to +# understand what the two parameters mean before changing them. +# +# The LFU counter is just 8 bits per key, it's maximum value is 255, so Redis +# uses a probabilistic increment with logarithmic behavior. Given the value +# of the old counter, when a key is accessed, the counter is incremented in +# this way: +# +# 1. A random number R between 0 and 1 is extracted. +# 2. A probability P is calculated as 1/(old_value*lfu_log_factor+1). +# 3. The counter is incremented only if R < P. +# +# The default lfu-log-factor is 10. This is a table of how the frequency +# counter changes with a different number of accesses with different +# logarithmic factors: +# +# +--------+------------+------------+------------+------------+------------+ +# | factor | 100 hits | 1000 hits | 100K hits | 1M hits | 10M hits | +# +--------+------------+------------+------------+------------+------------+ +# | 0 | 104 | 255 | 255 | 255 | 255 | +# +--------+------------+------------+------------+------------+------------+ +# | 1 | 18 | 49 | 255 | 255 | 255 | +# +--------+------------+------------+------------+------------+------------+ +# | 10 | 10 | 18 | 142 | 255 | 255 | +# +--------+------------+------------+------------+------------+------------+ +# | 100 | 8 | 11 | 49 | 143 | 255 | +# +--------+------------+------------+------------+------------+------------+ +# +# NOTE: The above table was obtained by running the following commands: +# +# redis-benchmark -n 1000000 incr foo +# redis-cli object freq foo +# +# NOTE 2: The counter initial value is 5 in order to give new objects a chance +# to accumulate hits. +# +# The counter decay time is the time, in minutes, that must elapse in order +# for the key counter to be divided by two (or decremented if it has a value +# less <= 10). +# +# The default value for the lfu-decay-time is 1. A special value of 0 means to +# decay the counter every time it happens to be scanned. +# +# lfu-log-factor 10 +# lfu-decay-time 1 + +########################### ACTIVE DEFRAGMENTATION ####################### +# +# What is active defragmentation? +# ------------------------------- +# +# Active (online) defragmentation allows a Redis server to compact the +# spaces left between small allocations and deallocations of data in memory, +# thus allowing to reclaim back memory. +# +# Fragmentation is a natural process that happens with every allocator (but +# less so with Jemalloc, fortunately) and certain workloads. Normally a server +# restart is needed in order to lower the fragmentation, or at least to flush +# away all the data and create it again. However thanks to this feature +# implemented by Oran Agra for Redis 4.0 this process can happen at runtime +# in a "hot" way, while the server is running. +# +# Basically when the fragmentation is over a certain level (see the +# configuration options below) Redis will start to create new copies of the +# values in contiguous memory regions by exploiting certain specific Jemalloc +# features (in order to understand if an allocation is causing fragmentation +# and to allocate it in a better place), and at the same time, will release the +# old copies of the data. This process, repeated incrementally for all the keys +# will cause the fragmentation to drop back to normal values. +# +# Important things to understand: +# +# 1. This feature is disabled by default, and only works if you compiled Redis +# to use the copy of Jemalloc we ship with the source code of Redis. +# This is the default with Linux builds. +# +# 2. You never need to enable this feature if you don't have fragmentation +# issues. +# +# 3. Once you experience fragmentation, you can enable this feature when +# needed with the command "CONFIG SET activedefrag yes". +# +# The configuration parameters are able to fine tune the behavior of the +# defragmentation process. If you are not sure about what they mean it is +# a good idea to leave the defaults untouched. + +# Enabled active defragmentation +# activedefrag no + +# Minimum amount of fragmentation waste to start active defrag +# active-defrag-ignore-bytes 100mb + +# Minimum percentage of fragmentation to start active defrag +# active-defrag-threshold-lower 10 + +# Maximum percentage of fragmentation at which we use maximum effort +# active-defrag-threshold-upper 100 + +# Minimal effort for defrag in CPU percentage, to be used when the lower +# threshold is reached +# active-defrag-cycle-min 1 + +# Maximal effort for defrag in CPU percentage, to be used when the upper +# threshold is reached +# active-defrag-cycle-max 25 + +# Maximum number of set/hash/zset/list fields that will be processed from +# the main dictionary scan +# active-defrag-max-scan-fields 1000 + +# Jemalloc background thread for purging will be enabled by default +jemalloc-bg-thread yes + +# It is possible to pin different threads and processes of Redis to specific +# CPUs in your system, in order to maximize the performances of the server. +# This is useful both in order to pin different Redis threads in different +# CPUs, but also in order to make sure that multiple Redis instances running +# in the same host will be pinned to different CPUs. +# +# Normally you can do this using the "taskset" command, however it is also +# possible to this via Redis configuration directly, both in Linux and FreeBSD. +# +# You can pin the server/IO threads, bio threads, aof rewrite child process, and +# the bgsave child process. The syntax to specify the cpu list is the same as +# the taskset command: +# +# Set redis server/io threads to cpu affinity 0,2,4,6: +# server_cpulist 0-7:2 +# +# Set bio threads to cpu affinity 1,3: +# bio_cpulist 1,3 +# +# Set aof rewrite child process to cpu affinity 8,9,10,11: +# aof_rewrite_cpulist 8-11 +# +# Set bgsave child process to cpu affinity 1,10,11 +# bgsave_cpulist 1,10-11 + +# In some cases redis will emit warnings and even refuse to start if it detects +# that the system is in bad state, it is possible to suppress these warnings +# by setting the following config which takes a space delimited list of warnings +# to suppress +# +# ignore-warnings ARM64-COW-BUG \ No newline at end of file diff --git a/controllers/controller.exam.go b/controllers/controller.exam.go deleted file mode 100644 index 9678cd5..0000000 --- a/controllers/controller.exam.go +++ /dev/null @@ -1,298 +0,0 @@ -package controllers - -// controller.exam.go -/** - * This file is a part of controllers, used to do business logic of exam - */ - -import ( - "DatabaseCamp/controllers/loaders" - "DatabaseCamp/database" - "DatabaseCamp/errs" - "DatabaseCamp/logs" - "DatabaseCamp/models/entities" - "DatabaseCamp/models/request" - "DatabaseCamp/models/response" - "DatabaseCamp/models/storages" - "DatabaseCamp/repositories" - "DatabaseCamp/utils" -) - -/** - * This class do business logic of exam - */ -type examController struct { - ExamRepo repositories.IExamRepository // repository for load exam data - UserRepo repositories.IUserRepository // repository for load user data -} - -/** - * Constructor creates a new examController instance - * - * @param examRepo Exam Repository for load exam data - * @param userRepo User Repository for load user data - * - * @return instance of examController - */ -func NewExamController(examRepo repositories.IExamRepository, userRepo repositories.IUserRepository) examController { - return examController{ExamRepo: examRepo, UserRepo: userRepo} -} - -/** - * Get the exam to use for the test - * - * @param examID Exam ID for getting activities of the exam - * @param userID User ID for getting detail of user about the exam - * - * @return response of the exam - * @return error of getting the exam - */ -func (c examController) GetExam(examID int, userID int) (*response.ExamResponse, error) { - - // Create exam loader instance - loader := loaders.NewExamLoader(c.ExamRepo, c.UserRepo) - - // Load exam data and check the error - err := loader.Load(userID, examID) - if err != nil || len(loader.GetExamActivitiesDB()) == 0 { - logs.New().Error(err) - return nil, errs.ErrExamNotFound - } - - // Create and prepare exam - exam := entities.Exam{} - exam.Prepare(loader.GetExamActivitiesDB()) - - // User need to have all badges before they can take the final exam - if exam.GetInfo().Type == string(entities.ExamType.Posttest) && !c.canDoFianlExam(loader.GetCorrectedBadgeDB()) { - return nil, errs.ErrFinalExamBadgesNotEnough - } - - // Create exam response - response := response.NewExamResponse(exam) - - return response, nil -} - -/** - * Get overview of the exam - * - * @param userID User ID for getting detail of user about the exam overview - * - * @return response of the exam overview - * @return error of getting exam overview - */ -func (c examController) GetOverview(userID int) (*response.ExamOverviewResponse, error) { - - // Create exam overview loader instance - loader := loaders.NewExamOverviewLoader(c.ExamRepo, c.UserRepo) - - // Load exam overview and check the error - err := loader.Load(userID) - if err != nil { - logs.New().Error(err) - return nil, errs.ErrLoadError - } - - // Get data from loader - examResultDB := loader.GetExamResultsDB() - examDB := loader.GetExamDB() - correctedBadgeDB := loader.GetCorrectedBadgeDB() - - // Check that user can do final exam - canDo := c.canDoFianlExam(correctedBadgeDB) - - // Create exam overview response - response := response.NewExamOverviewResponse(examResultDB, examDB, canDo) - - return response, nil -} - -/** - * Check user can take the final exam - * - * @param correctedBadgesDB all badges that user corrected from the database - * - * @return true if user can take the final exam, false otherwise - */ -func (c examController) canDoFianlExam(correctedBadgesDB []storages.CorrectedBadgeDB) bool { - for _, correctedBadgeDB := range correctedBadgesDB { - if correctedBadgeDB.UserID == nil && correctedBadgeDB.BadgeID != 3 { - return false - } - } - return true -} - -/** - * Check answer of the exam - * - * @param userID User ID for record user exam - * @param request Exam answer request for check answer of the exam - * - * @return response of the exam result overview - * @return error of checking exam - */ -func (c examController) CheckExam(userID int, request request.ExamAnswerRequest) (*response.ExamResultOverviewResponse, error) { - - // Get activities of the exam from the repository - examActivities, err := c.ExamRepo.GetExamActivity(*request.ExamID) - - // Check error and empty exam - if err != nil || len(examActivities) == 0 { - logs.New().Error(err) - return nil, errs.ErrExamNotFound - } - - // Create and prepare exam solution - exam := entities.Exam{} - exam.Prepare(examActivities) - - // The number of activities of the solution and activities of the user's answer should be equal - if len(request.Activities) != len(exam.GetActivities()) { - return nil, errs.ErrActivitiesNumberIncorrect - } - - // Check exam answer - _, err = exam.CheckAnswer(request.Activities) - if err != nil { - return nil, err - } - - // Create user badge - userBadgeDB := storages.UserBadgeDB{ - UserID: userID, - BadgeID: exam.GetInfo().BadgeID, - } - - // Get exam result from exam - examResultDB := exam.ToExamResultDB(userID) - - // Get exam result activities from exam - examResultActivities := exam.ToExamResultActivitiesDB() - - // Save exam result and check the error - err = c.saveExamResult(exam.GetInfo().Type, userBadgeDB, examResultDB, examResultActivities) - if err != nil { - logs.New().Error(err) - return nil, errs.ErrInsertError - } - - // Set exam result ID - exam.SetResultID(examResultDB.ID) - - // Create exam result response - response := response.NewExamResultOverviewResponse(exam) - - return response, nil -} - -/** - * Add exam result ID to exam result activity - * - * @param examResultID Exam result ID to add - * @param examResultActivity Exam result activities for add exam result ID - * - * @return exam result activity that added exam result id - */ -func (c examController) addExamResultID(examResultID int, examResultActivity []storages.ExamResultActivityDB) []storages.ExamResultActivityDB { - newExamResultActivity := make([]storages.ExamResultActivityDB, 0) - - for _, v := range examResultActivity { - newExamResultActivity = append(newExamResultActivity, storages.ExamResultActivityDB{ - ExamResultID: examResultID, - ActivityID: v.ActivityID, - Score: v.Score, - }) - } - - return newExamResultActivity -} - -/** - * Save exam result by database transaction - * - * @param examType Type of the exam - * @param userBadgeDB Badge of the exam to be given to the user - * @param examResultDB Exam result to save into the database - * @param resultActivitiesDB Exam result activities to save into the database - * - * @return the error of saving data - */ -func (c examController) saveExamResult(examType string, userBadgeDB storages.UserBadgeDB, examResultDB *storages.ExamResultDB, resultActivitiesDB []storages.ExamResultActivityDB) error { - - var err error - - // Create database transaction - tx := database.NewTransaction() - - // Begin transaction - tx.Begin() - - // Insert Exam result - *examResultDB, err = c.ExamRepo.InsertExamResultTransaction(tx, *examResultDB) - if err != nil { - tx.Rollback() - return err - } - - // Insert Exam result Activity - _, err = c.ExamRepo.InsertExamResultActivityTransaction(tx, c.addExamResultID(examResultDB.ID, resultActivitiesDB)) - if err != nil { - tx.Rollback() - return err - } - - // Give exam badge if user passed the exam - if (examType == entities.ExamType.MiniExam || examType == entities.ExamType.Posttest) && examResultDB.IsPassed { - _, err = c.UserRepo.InsertUserBadgeTransaction(tx, userBadgeDB) - if err != nil && !utils.NewHelper().IsSqlDuplicateError(err) { - tx.Rollback() - return err - } - } - - // Commit transaction - tx.Commit() - - // Close transaction - tx.Close() - - return nil -} - -/** - * Get exam result of the user - * - * @param userID User ID for getting user data - * @param examResultID Exam result ID for getting exam results of the user - * - * @return response of the exam result overview - * @return error of getting exam result - */ -func (c examController) GetExamResult(userID int, examResultID int) (*response.ExamResultOverviewResponse, error) { - - // Get exam result of the user by exam result id - examResults, err := c.UserRepo.GetExamResultByID(userID, examResultID) - if err != nil || len(examResults) == 0 { - logs.New().Error(err) - return nil, errs.ErrExamNotFound - } - - // Get exam result activities of the exam - examActivities, err := c.ExamRepo.GetExamActivity(examResults[0].ExamID) - if err != nil || len(examActivities) == 0 { - logs.New().Error(err) - return nil, errs.ErrExamNotFound - } - - // Create and prepare exam - exam := entities.Exam{} - exam.Prepare(examActivities) - exam.PrepareResult(examResults[0]) - - // Create exam overview response - response := response.NewExamResultOverviewResponse(exam) - - return response, nil -} diff --git a/controllers/controller.learning.go b/controllers/controller.learning.go deleted file mode 100644 index a5915dd..0000000 --- a/controllers/controller.learning.go +++ /dev/null @@ -1,466 +0,0 @@ -package controllers - -// controller.learning.go -/** - * This file is a part of controllers, used to do business logic of learning - */ - -import ( - "DatabaseCamp/controllers/loaders" - "DatabaseCamp/database" - "DatabaseCamp/errs" - "DatabaseCamp/logs" - "DatabaseCamp/models/entities" - "DatabaseCamp/models/general" - "DatabaseCamp/models/response" - "DatabaseCamp/models/storages" - "DatabaseCamp/repositories" - "DatabaseCamp/utils" - "sync" - "time" -) - -/** - * This class do business logic of learning - */ -type learningController struct { - LearningRepo repositories.ILearningRepository // repository for load learning data - UserRepo repositories.IUserRepository // repository for load user data -} - -/** - * Constructor creates a new learningController instance - * - * @param learningRepo Learning Repository for load learning data - * @param userRepo User Repository for load user data - * - * @return instance of learningController - */ -func NewLearningController( - learningRepo repositories.ILearningRepository, userRepo repositories.IUserRepository) learningController { - return learningController{LearningRepo: learningRepo, UserRepo: userRepo} -} - -/** - * Get video lecture of the content - * - * @param id Content ID for getting video lecture of the content - * - * @return response of the video lecture - * @return error of getting video lecture - */ -func (c learningController) GetVideoLecture(id int) (*response.VideoLectureResponse, error) { - - // Get content from the repository - contentDB, err := c.LearningRepo.GetContent(id) - if err != nil || contentDB == nil { - logs.New().Error(err) - return nil, errs.ErrContentNotFound - } - - // Get video link of the content from the repository - videoLink, err := c.LearningRepo.GetVideoFileLink(contentDB.VideoPath) - if err != nil { - logs.New().Error(err) - return nil, errs.ErrServiceUnavailableError - } - - // Create video lecture response - response := response.NewVideoLectureResponse(contentDB.ID, contentDB.Name, videoLink) - - return response, nil -} - -/** - * Get content overview of thew user - * - * @param userID User ID for getting content overview of the user - * - * @return response of the content overview - * @return error of getting content overview - */ -func (c learningController) GetOverview(userID int) (*response.ContentOverviewResponse, error) { - - // Create learning overview loader - loader := loaders.NewLearningOverviewLoader(c.LearningRepo, c.UserRepo) - - // Load content overview and check error - err := loader.Load(userID) - if err != nil { - logs.New().Error(err) - return nil, errs.ErrLoadError - } - - // Create content overview response - response := response.NewContentOverviewResponse(loader.GetOverviewDB(), loader.GetLearningProgressionDB()) - - return response, nil -} - -/** - * Get activity for user to do - * - * @param userID User ID for getting user hints - * @param activityID Activity ID for getting activity data - * - * @return response of the activity - * @return error of getting activity - */ -func (c learningController) GetActivity(userID int, activityID int) (*response.ActivityResponse, error) { - - // Create activity loader - loader := loaders.NewActivityLoader(c.LearningRepo, c.UserRepo) - - // Load activity data and check error - err := loader.Load(userID, activityID) - if err != nil { - logs.New().Error(err) - return nil, errs.ErrLoadError - } - - // Get activity data from loader - activityDB := loader.GetActivityDB() - - // Get choices of the activity from the database - choiceDB, err := c.getChoices(activityDB.ID, activityDB.TypeID) - if err != nil { - logs.New().Error(err) - return nil, errs.ErrActivitiesNotFound - } - - // Create and set data of the activity - activity := entities.Activity{} - activity.SetActivity(*loader.GetActivityDB()) - activity.SetChoicesByChoiceDB(choiceDB) - activity.SetHint(loader.GetActivityHintsDB(), loader.GetUserHintsDB()) - - // Create activity response - response := response.NewActivityResponse(activity) - - return response, nil -} - -/** - * Get Choices of activity by activity type id - * - * @param activityID Activity ID for indicate activity - * @param typeID Activity type ID for indicate type of the activity - * - * @return choices of the activity - * @return error of getting choices - */ -func (c learningController) getChoices(activityID int, typeID int) (interface{}, error) { - if typeID == 1 { - return c.LearningRepo.GetMatchingChoice(activityID) - } else if typeID == 2 { - return c.LearningRepo.GetMultipleChoice(activityID) - } else if typeID == 3 { - return c.LearningRepo.GetCompletionChoice(activityID) - } else { - return nil, errs.ErrActivityTypeInvalid - } -} - -/** - * Finish activity by database transaction - * - * @param userID User ID for adding activity point - * @param activityID Activity ID for indicate activity - * @param addPoint Point for add to the user - * - * @return error of finishing choices - */ -func (c learningController) finishActivityTrasaction(userID int, activityID int, addPoint int) error { - - // Create database transaction - tx := database.NewTransaction() - - // Begin transaction - tx.Begin() - - // Create progression - progression := storages.LearningProgressionDB{ - UserID: userID, - ActivityID: activityID, - CreatedTimestamp: time.Now().Local(), - } - - // Insert progression - _, err := c.UserRepo.InsertLearningProgressionTransaction(tx, progression) - if err != nil && !utils.NewHelper().IsSqlDuplicateError(err) { - tx.Rollback() - return err - } - - // Give a point to the user if do this activity for the first time - if !utils.NewHelper().IsSqlDuplicateError(err) { - err = c.UserRepo.ChangePointTransaction(tx, userID, addPoint, entities.Mode.Add) - } - - // Check error - if err != nil { - tx.Rollback() - return err - } - - // Commit transaction - tx.Commit() - - // Close transaction - tx.Close() - - return nil -} - -/** - * Use hint of the activity - * - * @param userID User ID for getting user hints - * @param activityID Activity ID for getting all hints of the activity - * - * @return hint of the activity that user can use - * @return error of using hint - */ -func (c learningController) UseHint(userID int, activityID int) (*storages.HintDB, error) { - loader := loaders.NewHintLoader(c.LearningRepo, c.UserRepo) - err := loader.Load(userID, activityID) - if err != nil || len(loader.GetActivityHintsDB()) == 0 { - logs.New().Error(err) - return nil, errs.ErrLoadError - } - - nextLevelHint := c.getNextLevelHint(loader.GetActivityHintsDB(), loader.GetUserHintsDB()) - if nextLevelHint == nil { - return nil, errs.ErrHintAlreadyUsed - } - - if loader.GetUserDB().Point < nextLevelHint.PointReduce { - return nil, errs.ErrHintPointsNotEnough - } - - err = c.useHintTransaction(userID, nextLevelHint.PointReduce, nextLevelHint.ID) - if err != nil { - logs.New().Error(err) - return nil, errs.ErrInsertError - } - - return nextLevelHint, nil -} - -/** - * Check for used hint of the user - * - * @param userHints User hints - * @param hintID Hint ID for check to user hints - * - * @return true if that hint is used, false otherwise - */ -func (c learningController) isUsedHint(userHints []storages.UserHintDB, hintID int) bool { - for _, userHint := range userHints { - if userHint.HintID == hintID { - return true - } - } - return false -} - -/** - * Get the next level of hint that user can use - * - * @param userHints User hints - * @param ActivityHintsDB Hints of the activity - * - * @return next level of hint that user can use - */ -func (c learningController) getNextLevelHint(ActivityHintsDB []storages.HintDB, userHintsDB []storages.UserHintDB) *storages.HintDB { - for _, activityHint := range ActivityHintsDB { - if !c.isUsedHint(userHintsDB, activityHint.ID) { - return &activityHint - } - } - return nil -} - -/** - * Use hint by database transaction - * - * @param userID User ID that used hint - * @param reducePoint Point reduce when used hint - * @param hintID Hint ID that user used - * - * @return error of using hint - */ -func (c learningController) useHintTransaction(userID int, reducePoint int, hintID int) error { - - var wg sync.WaitGroup - var err error - - // Create transaction - tx := database.NewTransaction() - - // Create concurrent - ct := general.ConcurrentTransaction{ - Concurrent: &general.Concurrent{ - Wg: &wg, - Err: &err, - }, - Transaction: tx, - } - - // Add thread for concurrency - wg.Add(2) - - // Doing 2 thread concurrency - - // Update user point - go c.updateUserPointAsyncTrasaction(&ct, userID, reducePoint, entities.Mode.Reduce) - - // Insert user hint - go c.insertUserHintAsyncTransaction(&ct, userID, hintID) - - // Waiting for all thread - wg.Wait() - - // Check error - if err != nil { - tx.Rollback() - return err - } - - // Commit transaction - tx.Commit() - - // Close transaction - tx.Close() - - return nil -} - -/** - * Update concurrency user point by database transaction - * - * @param ct Concurrent transaction model for do concurrent with database transaction - * @param userID User ID for update user point - * @param updatePoint Point to be update - * @param mode mode for update user point - */ -func (c learningController) updateUserPointAsyncTrasaction(ct *general.ConcurrentTransaction, userID int, updatePoint int, mode entities.ChangePointMode) { - defer ct.Concurrent.Wg.Done() - err := c.UserRepo.ChangePointTransaction(ct.Transaction, userID, updatePoint, mode) - if err != nil { - *ct.Concurrent.Err = err - } -} - -/** - * Insert concurrency user hint by database transaction - * - * @param ct Concurrent transaction model for do concurrent with database transaction - * @param userID User ID for insert user hint - * @param hintID Hint ID to insert - */ -func (c learningController) insertUserHintAsyncTransaction(ct *general.ConcurrentTransaction, userID int, hintID int) { - defer ct.Concurrent.Wg.Done() - hint := storages.UserHintDB{ - UserID: userID, - HintID: hintID, - CreatedTimestamp: time.Now().Local(), - } - _, err := c.UserRepo.InsertUserHintTransaction(ct.Transaction, hint) - if err != nil { - *ct.Concurrent.Err = err - } -} - -/** - * Get roadmap of the content - * - * @param userID User ID for getting learning progression of the user - * @param contentID Content ID for getting roadmap of the content - * - * @return response of the content roadmap - * @return error of getting content roadmap - */ -func (c learningController) GetContentRoadmap(userID int, contentID int) (*response.ContentRoadmapResponse, error) { - - // Create content roadmap loader - loader := loaders.NewContentRoadmapLoader(c.LearningRepo, c.UserRepo) - - // Load content roadmap and check error - err := loader.Load(userID, contentID) - if err != nil || loader.GetContentDB() == nil { - logs.New().Error(err) - return nil, errs.ErrContentNotFound - } - - // Get content roadmap from loader - contentDB := loader.GetContentDB() - contentActivity := loader.GetContentActivityDB() - learningProgressionDB := loader.GetLearningProgressionDB() - - // Create content roadmap response - response := response.NewContentRoadmapResponse(*contentDB, contentActivity, learningProgressionDB) - - return response, nil -} - -/** - * Check activity answer - * - * @param userID User ID for record user activity - * @param activityID Activity ID for getting activity solution - * @param typeID Activity ID for indicate type of the activity - * @param answer Answer of the user - * - * @return response of the answer - * @return error of checking activity answer - */ -func (c learningController) CheckAnswer(userID int, activityID int, typeID int, answer interface{}) (*response.AnswerResponse, error) { - - // Create check answer loader - loader := loaders.NewCheckAnswerLoader(c.LearningRepo) - - // Load activity solution data and check error - err := loader.Load(activityID, typeID, c.getChoices) - if err != nil || loader.GetActivityDB() == nil { - logs.New().Error(err) - return nil, errs.ErrLoadError - } - - // type of the activity solution and type of the activity answer should be equal - if loader.GetActivityDB().TypeID != typeID { - return nil, errs.ErrActivityTypeInvalid - } - - // Create activity and set data - activity := entities.Activity{} - activity.SetActivity(*loader.GetActivityDB()) - activity.SetChoicesByChoiceDB(loader.GetChoicesDB()) - - // Check answer - isCorrect, err := activity.IsAnswerCorrect(answer) - if err != nil { - return nil, err - } - - // Finish Activity if answer is correct - if isCorrect { - err = c.finishActivityTrasaction(userID, activityID, activity.GetInfo().Point) - if err != nil && !utils.NewHelper().IsSqlDuplicateError(err) { - logs.New().Error(err) - return nil, errs.ErrInsertError - } - } - - // Get user from the repository - userDB, err := c.UserRepo.GetUserByID(userID) - if err != nil || userDB == nil { - logs.New().Error(err) - return nil, errs.ErrUserNotFound - } - - // Create activity answer response - response := response.NewActivityAnswerResponse(activity, userDB.Point, isCorrect) - - return response, nil -} diff --git a/controllers/controller.user.go b/controllers/controller.user.go deleted file mode 100644 index a1ddf2d..0000000 --- a/controllers/controller.user.go +++ /dev/null @@ -1,192 +0,0 @@ -package controllers - -// controller.user.go -/** - * This file is a part of controllers, used to do business logic of user - */ - -import ( - "DatabaseCamp/errs" - "DatabaseCamp/logs" - "DatabaseCamp/models/entities" - "DatabaseCamp/models/request" - "DatabaseCamp/models/response" - "DatabaseCamp/repositories" - "DatabaseCamp/utils" -) - -/** - * This class do business logic of user - */ -type userController struct { - Repo repositories.IUserRepository -} - -/** - * Constructor creates a new userController instance - * - * @param userRepo User Repository for load user data - * - * @return instance of userController - */ -func NewUserController(repo repositories.IUserRepository) userController { - return userController{Repo: repo} -} - -/** - * Register for learning in platform - * - * @param request User request for create account - * - * @return response of user - * @return error of register - */ -func (c userController) Register(request request.UserRequest) (*response.UserResponse, error) { - - // Create User - user := entities.NewUserByRequest(request) - - // Insert User and Check error - userDB, err := c.Repo.InsertUser(user.ToDB()) - if err != nil { - logs.New().Error(err) - if utils.NewHelper().IsSqlDuplicateError(err) { - return nil, errs.ErrEmailAlreadyExists - } else { - return nil, errs.ErrInsertError - } - } - - // Set user ID - user.SetID(userDB.ID) - - // Create user response - response := response.NewUserReponse(user) - return &response, nil -} - -/** - * Login - * - * @param request User request for login to the platform - * - * @return response of user - * @return error of login - */ -func (c userController) Login(request request.UserRequest) (*response.UserResponse, error) { - - // Get user from the repository - userDB, err := c.Repo.GetUserByEmail(request.Email) - - // Create User - user := entities.NewUserByUserDB(*userDB) - - // Check email and password - if err != nil || !user.IsPasswordCorrect(request.Password) { - logs.New().Error(err) - return nil, errs.ErrEmailOrPasswordNotCorrect - } - - // Create user response - response := response.NewUserReponse(user) - - return &response, nil -} - -/** - * Get user profile - * - * @param userID User ID for getting user profile - * - * @return response of get profile - * @return error of getting profile - */ -func (c userController) GetProfile(id int) (*response.GetProfileResponse, error) { - - // Get user profile from repository - profileDB, err := c.Repo.GetProfile(id) - if err != nil || profileDB == nil { - logs.New().Error(err) - return nil, errs.ErrUserNotFound - } - - // Get all Badge from repository - allBadge, err := c.Repo.GetAllBadge() - if err != nil { - logs.New().Error(err) - return nil, errs.ErrLoadError - } - - // Get user badges from repository - userBadgeGain, err := c.Repo.GetUserBadge(id) - if err != nil { - logs.New().Error(err) - return nil, errs.ErrLoadError - } - - // Create user and set data - user := entities.User{} - user.SetCorrectedBadges(allBadge, userBadgeGain) - - // Create get profile response - response := response.NewGetProfileResponse(*profileDB, user.GetBadges()) - - return &response, nil -} - -/** - * Edit profile data - * - * @param userID User ID for getting user profile - * @param request User request for editing the user profile - * - * @return response of edit profile - * @return error of edit profile - */ -func (c userController) EditProfile(userID int, request request.UserRequest) (*response.EditProfileResponse, error) { - - // Update user data and check error - err := c.Repo.UpdatesByID(userID, map[string]interface{}{"name": request.Name}) - if err != nil { - logs.New().Error(err) - return nil, errs.ErrUpdateError - } - - // Create edit profile response - response := response.EditProfileResponse{UpdatedName: request.Name} - - return &response, nil -} - -/** - * Get others and own ranking - * - * @param id User ID for getting user ranking - * - * @return response of ranking - * @return error of getting ranking - */ -func (c userController) GetRanking(userID int) (*response.RankingResponse, error) { - - // Get user ranking from repository - userRankingDB, err := c.Repo.GetPointRanking(userID) - if err != nil || userRankingDB == nil { - logs.New().Error(err) - return nil, errs.ErrUserNotFound - } - - // Get others ranking from repository - leaderBoardDB, err := c.Repo.GetRankingLeaderBoard() - if err != nil || leaderBoardDB == nil { - logs.New().Error(err) - return nil, errs.ErrLeaderBoardNotFound - } - - // Create ranking response - response := response.RankingResponse{ - UserRanking: *userRankingDB, - LeaderBoard: leaderBoardDB, - } - - return &response, nil -} diff --git a/controllers/interface.go b/controllers/interface.go deleted file mode 100644 index 414a576..0000000 --- a/controllers/interface.go +++ /dev/null @@ -1,190 +0,0 @@ -package controllers - -// interface.go -/** - * This file used to be a interface of controllers - */ - -import ( - "DatabaseCamp/models/request" - "DatabaseCamp/models/response" - "DatabaseCamp/models/storages" -) - -/** - * Interface to show function in exam controller that others can use - */ -type IExamController interface { - - /** - * Get the exam to use for the test - * - * @param examID Exam ID for getting activities of the exam - * @param userID User ID for getting detail of user about the exam - * - * @return response of the exam - * @return error of getting the exam - */ - GetExam(examID int, userID int) (*response.ExamResponse, error) - - /** - * Get overview of the exam - * - * @param userID User ID for getting detail of user about the exam overview - * - * @return response of the exam overview - * @return error of getting exam overview - */ - GetOverview(userID int) (*response.ExamOverviewResponse, error) - - /** - * Check answer of the exam - * - * @param userID User ID for record user exam - * @param request Exam answer request for check answer of the exam - * - * @return response of the exam result overview - * @return error of checking exam - */ - CheckExam(userID int, request request.ExamAnswerRequest) (*response.ExamResultOverviewResponse, error) - - /** - * Get exam result of the user - * - * @param userID User ID for getting user data - * @param examResultID Exam result ID for getting exam results of the user - * - * @return response of the exam result overview - * @return error of getting exam result - */ - GetExamResult(userID int, examResultID int) (*response.ExamResultOverviewResponse, error) -} - -/** - * Interface to show function in learning controller that others can use - */ -type ILearningController interface { - - /** - * Get video lecture of the content - * - * @param id Content ID for getting video lecture of the content - * - * @return response of the video lecture - * @return error of getting video lecture - */ - GetVideoLecture(id int) (*response.VideoLectureResponse, error) - - /** - * Get content overview of thew user - * - * @param userID User ID for getting content overview of the user - * - * @return response of the content overview - * @return error of getting content overview - */ - GetOverview(userID int) (*response.ContentOverviewResponse, error) - - /** - * Get activity for user to do - * - * @param userID User ID for getting user hints - * @param activityID Activity ID for getting activity data - * - * @return response of the activity - * @return error of getting activity - */ - GetActivity(userID int, activityID int) (*response.ActivityResponse, error) - - /** - * Use hint of the activity - * - * @param userID User ID for getting user hints - * @param activityID Activity ID for getting all hints of the activity - * - * @return hint of the activity that user can use - * @return error of using hint - */ - UseHint(userID int, activityID int) (*storages.HintDB, error) - - /** - * Get roadmap of the content - * - * @param userID User ID for getting learning progression of the user - * @param contentID Content ID for getting roadmap of the content - * - * @return response of the content roadmap - * @return error of getting content roadmap - */ - GetContentRoadmap(userID int, contentID int) (*response.ContentRoadmapResponse, error) - - /** - * Check activity answer - * - * @param userID User ID for record user activity - * @param activityID Activity ID for getting activity solution - * @param typeID Activity ID for indicate type of the activity - * @param answer Answer of the user - * - * @return response of the answer - * @return error of checking activity answer - */ - CheckAnswer(userID int, activityID int, typeID int, answer interface{}) (*response.AnswerResponse, error) -} - -/** - * Interface to show function in user controller that others can use - */ -type IUserController interface { - - /** - * Register for learning in platform - * - * @param request User request for create account - * - * @return response of user - * @return error of register - */ - Register(request request.UserRequest) (*response.UserResponse, error) - - /** - * Login - * - * @param request User request for login to the platform - * - * @return response of user - * @return error of login - */ - Login(request request.UserRequest) (*response.UserResponse, error) - - /** - * Get user profile - * - * @param userID User ID for getting user profile - * - * @return response of get profile - * @return error of getting profile - */ - GetProfile(userID int) (*response.GetProfileResponse, error) - - /** - * Edit profile data - * - * @param userID User ID for getting user profile - * @param request User request for editing the user profile - * - * @return response of edit profile - * @return error of edit profile - */ - EditProfile(userID int, request request.UserRequest) (*response.EditProfileResponse, error) - - /** - * Get others and own ranking - * - * @param id User ID for getting user ranking - * - * @return response of ranking - * @return error of getting ranking - */ - GetRanking(id int) (*response.RankingResponse, error) -} diff --git a/controllers/loaders/loader.activity.go b/controllers/loaders/loader.activity.go deleted file mode 100644 index 2d81e2d..0000000 --- a/controllers/loaders/loader.activity.go +++ /dev/null @@ -1,130 +0,0 @@ -package loaders - -// loader.activity.go -/** - * This file is a part of controllers, used to load concurrency activity data - */ - -import ( - "DatabaseCamp/models/general" - "DatabaseCamp/models/storages" - "DatabaseCamp/repositories" - "sync" -) - -/** - * This class load concurrency all activity data - */ -type activityLoader struct { - learningRepo repositories.ILearningRepository // repository for load activity data - userRepo repositories.IUserRepository // repository for load user hints - - activityDB *storages.ActivityDB // activity data from the database - activityHintsDB []storages.HintDB // activity hints from the database - userHintsDB []storages.UserHintDB // user hints from the database -} - -/** - * Constructor creates a new activityLoader instance - * - * @param learningRepo Learning Repository for load learning data - * @param userRepo User Repository for load user hints - * - * @return instance of activityLoader - */ -func NewActivityLoader(learningRepo repositories.ILearningRepository, userRepo repositories.IUserRepository) *activityLoader { - return &activityLoader{learningRepo: learningRepo, userRepo: userRepo} -} - -/** - * Getter for getting activityDB - * - * @return activityDB - */ -func (l *activityLoader) GetActivityDB() *storages.ActivityDB { - return l.activityDB -} - -/** - * Getter for getting activityHintsDB - * - * @return activityHintsDB - */ -func (l *activityLoader) GetActivityHintsDB() []storages.HintDB { - return l.activityHintsDB -} - -/** - * Getter for getting userHintsDB - * - * @return userHintsDB - */ -func (l *activityLoader) GetUserHintsDB() []storages.UserHintDB { - return l.userHintsDB -} - -/** - * Load concurrency all activity data from the database - * - * @param userID User ID for getting user hints information of the activity - * @param activityID Activity ID for getting activity data - * - * @return the error of loading data - */ -func (l *activityLoader) Load(userID int, activityID int) error { - var wg sync.WaitGroup - var err error - concurrent := general.Concurrent{Wg: &wg, Err: &err} - wg.Add(3) - go l.loadActivityAsync(&concurrent, activityID) - go l.loadActivityHints(&concurrent, activityID) - go l.loadUserHintsAsync(&concurrent, userID, activityID) - wg.Wait() - return err -} - -/** - * Load activity data from the database - * - * @param concurrent Concurrent model for doing load concurrency - * @param activityID Activity ID for getting activity data - */ -func (l *activityLoader) loadActivityAsync(concurrent *general.Concurrent, activityID int) { - defer concurrent.Wg.Done() - var err error - l.activityDB, err = l.learningRepo.GetActivity(activityID) - if err != nil { - *concurrent.Err = err - } -} - -/** - * Load activity data from the database - * - * @param concurrent Concurrent model for doing load concurrency - * @param userID User ID for getting user hints information of the activity - * @param activityID Activity ID for getting activity data - */ -func (l *activityLoader) loadUserHintsAsync(concurrent *general.Concurrent, userID int, activityID int) { - defer concurrent.Wg.Done() - result, e := l.userRepo.GetUserHint(userID, activityID) - if e != nil { - *concurrent.Err = e - } - l.userHintsDB = append(l.userHintsDB, result...) -} - -/** - * Load activity data from the database - * - * @param concurrent Concurrent model for doing load concurrency - * @param activityID Activity ID for getting activity data - */ -func (l *activityLoader) loadActivityHints(concurrent *general.Concurrent, activityID int) { - defer concurrent.Wg.Done() - result, e := l.learningRepo.GetActivityHints(activityID) - if e != nil { - *concurrent.Err = e - } - l.activityHintsDB = append(l.activityHintsDB, result...) -} diff --git a/controllers/loaders/loader.check_answer.go b/controllers/loaders/loader.check_answer.go deleted file mode 100644 index de59ac0..0000000 --- a/controllers/loaders/loader.check_answer.go +++ /dev/null @@ -1,109 +0,0 @@ -package loaders - -// loader.check_answer.go -/** - * This file is a part of controllers, used to load concurrency activity answer data - */ - -import ( - "DatabaseCamp/models/general" - "DatabaseCamp/models/storages" - "DatabaseCamp/repositories" - "sync" -) - -/** - * This class load concurrency activity answer data - */ -type checkAnswerLoader struct { - learningRepo repositories.ILearningRepository // repository for load activity data - - choicesDB interface{} // choices of the activity from the database - activityDB *storages.ActivityDB // activity data from the database -} - -/** - * Constructor creates a new checkAnswerLoader instance - * - * @param learningRepo Learning Repository for load learning data - * - * @return instance of checkAnswerLoader - */ -func NewCheckAnswerLoader(learningRepo repositories.ILearningRepository) *checkAnswerLoader { - return &checkAnswerLoader{learningRepo: learningRepo} -} - -/** - * Getter for getting choicesDB - * - * @return choicesDB - */ -func (c *checkAnswerLoader) GetChoicesDB() interface{} { - return c.choicesDB -} - -/** - * Getter for getting activityDB - - * * @return activityDB - */ -func (c *checkAnswerLoader) GetActivityDB() *storages.ActivityDB { - return c.activityDB -} - -/** - * Load concurrency activity answer data - * - * @param activityID Activity ID for getting activity data - * @param activityTypeID Activity Type ID for getting choices of activity by type - * @param getChoicesFunc function for getting choices - * - * @return the error of loading data - */ -func (c *checkAnswerLoader) Load(activityID int, activityTypeID int, getChoicesFunc func(activityID int, activityTypeID int) (interface{}, error)) error { - var wg sync.WaitGroup - var err error - concurrent := general.Concurrent{Wg: &wg, Err: &err} - wg.Add(2) - go c.loadActivityAsync(&concurrent, activityID) - go c.getChioceAsync(&concurrent, activityID, activityTypeID, getChoicesFunc) - wg.Wait() - return err -} - -/** - * Load activity data from the database - * - * @param concurrent Concurrent model for doing load concurrency - * @param activityID Activity ID for getting activity data - */ -func (c *checkAnswerLoader) loadActivityAsync(concurrent *general.Concurrent, activityID int) { - defer concurrent.Wg.Done() - var err error - c.activityDB, err = c.learningRepo.GetActivity(activityID) - if err != nil { - *concurrent.Err = err - } -} - -/** - * call getChoicesFunc for getting activity choices - * - * @param concurrent Concurrent model for doing load concurrency - * @param activityID Activity ID for getting activity data - * @param activityTypeID Activity Type ID for getting choices of activity by type - * @param getChoicesFunc function for getting choices - */ -func (c *checkAnswerLoader) getChioceAsync( - concurrent *general.Concurrent, - activityID int, - activityTypeID int, - getChoicesFunc func(activityID int, activityTypeID int) (interface{}, error), -) { - defer concurrent.Wg.Done() - var err error - c.choicesDB, err = getChoicesFunc(activityID, activityTypeID) - if err != nil { - *concurrent.Err = err - } -} diff --git a/controllers/loaders/loader.content_roadmap.go b/controllers/loaders/loader.content_roadmap.go deleted file mode 100644 index eb36800..0000000 --- a/controllers/loaders/loader.content_roadmap.go +++ /dev/null @@ -1,129 +0,0 @@ -package loaders - -// loader.content_roadmap.go -/** - * This file is a part of controllers, used to load concurrency roadmap of the learning content - */ - -import ( - "DatabaseCamp/models/general" - "DatabaseCamp/models/storages" - "DatabaseCamp/repositories" - "sync" -) - -/** - * This class load concurrency roadmap of the learning content - */ -type contentRoadmapLoader struct { - learningRepo repositories.ILearningRepository // repository for load learning content data - userRepo repositories.IUserRepository // repository for load user learning progression data - - contentDB *storages.ContentDB // learning content data from the database - contentActivityDB []storages.ActivityDB // activities of the content from the database - learningProgressionDB []storages.LearningProgressionDB // learning progression of the user from the database -} - -/** - * Constructor creates a new contentRoadmapLoader instance - * - * @param learningRepo Learning Repository for load learning data - * @param userRepo User Repository for load user data - * - * @return instance of contentRoadmapLoader - */ -func NewContentRoadmapLoader(learningRepo repositories.ILearningRepository, userRepo repositories.IUserRepository) *contentRoadmapLoader { - return &contentRoadmapLoader{learningRepo: learningRepo, userRepo: userRepo} -} - -/** - * Getter for getting contentDB - * - * @return contentDB - */ -func (l *contentRoadmapLoader) GetContentDB() *storages.ContentDB { - return l.contentDB -} - -/** - * Getter for getting contentActivityDB - * - * @return contentActivityDB - */ -func (l *contentRoadmapLoader) GetContentActivityDB() []storages.ActivityDB { - return l.contentActivityDB -} - -/** - * Getter for getting learningProgressionDB - * - * @return learningProgressionDB - */ -func (l *contentRoadmapLoader) GetLearningProgressionDB() []storages.LearningProgressionDB { - return l.learningProgressionDB -} - -/** - * Load concurrency roadmap of the learning content - * - * @param userID User ID for getting user hints information of the activity - * @param contentID Content ID for getting learning content data - * - * @return the error of loading data - */ -func (l *contentRoadmapLoader) Load(userID int, contentID int) error { - var wg sync.WaitGroup - var err error - concurrent := general.Concurrent{Wg: &wg, Err: &err} - wg.Add(3) - go l.loadLearningProgressionAsync(&concurrent, userID) - go l.loadContentActivityAsync(&concurrent, contentID) - go l.loadContentAsync(&concurrent, contentID) - wg.Wait() - return err -} - -/** - * Load learning content data from the database - * - * @param concurrent Concurrent model for doing load concurrency - * @param contentID Content ID for getting learning content data - */ -func (l *contentRoadmapLoader) loadContentAsync(concurrent *general.Concurrent, contentID int) { - defer concurrent.Wg.Done() - result, err := l.learningRepo.GetContent(contentID) - if err != nil { - *concurrent.Err = err - } - l.contentDB = result -} - -/** - * Load activities of the content from the database - * - * @param concurrent Concurrent model for doing load concurrency - * @param contentID Content ID for getting learning content data - */ -func (l *contentRoadmapLoader) loadContentActivityAsync(concurrent *general.Concurrent, contentID int) { - defer concurrent.Wg.Done() - result, err := l.learningRepo.GetContentActivity(contentID) - if err != nil { - *concurrent.Err = err - } - l.contentActivityDB = append(l.contentActivityDB, result...) -} - -/** - * Load learning progression of the user from the database - * - * @param concurrent Concurrent model for doing load concurrency - * @param userID User ID for getting learning progression of the user - */ -func (l *contentRoadmapLoader) loadLearningProgressionAsync(concurrent *general.Concurrent, userID int) { - defer concurrent.Wg.Done() - result, err := l.userRepo.GetLearningProgression(userID) - if err != nil { - *concurrent.Err = err - } - l.learningProgressionDB = append(l.learningProgressionDB, result...) -} diff --git a/controllers/loaders/loader.exam.go b/controllers/loaders/loader.exam.go deleted file mode 100644 index d4645a5..0000000 --- a/controllers/loaders/loader.exam.go +++ /dev/null @@ -1,102 +0,0 @@ -package loaders - -// loader.exam.go -/** - * This file is a part of controllers, used to load exam data - */ - -import ( - "DatabaseCamp/models/general" - "DatabaseCamp/models/storages" - "DatabaseCamp/repositories" - "sync" -) - -/** - * This class load concurrency exam data - */ -type examLoader struct { - examRepo repositories.IExamRepository // repository for load activities of the exam - userRepo repositories.IUserRepository // repository load corrected badges of the user - - correctedBadgeDB []storages.CorrectedBadgeDB // corrected badges of the user from the database - examActivitiesDB []storages.ExamActivityDB // activities of the exam from the database -} - -/** - * Constructor creates a new examLoader instance - * - * @param examRepo Exam Repository for load activities of the exam - * @param userRepo User Repository for load corrected badges of the user - * - * @return instance of examLoader - */ -func NewExamLoader(examRepo repositories.IExamRepository, userRepo repositories.IUserRepository) *examLoader { - return &examLoader{examRepo: examRepo, userRepo: userRepo} -} - -/** - * Getter for getting correctedBadgeDB - * - * @return correctedBadgeDB - */ -func (l *examLoader) GetCorrectedBadgeDB() []storages.CorrectedBadgeDB { - return l.correctedBadgeDB -} - -/** - * Getter for getting examActivitiesDB - * - * @return examActivitiesDB - */ -func (l *examLoader) GetExamActivitiesDB() []storages.ExamActivityDB { - return l.examActivitiesDB -} - -/** - * Load concurrency exam data from the database - * - * @param userID User ID for getting corrected badges of the user - * @param examID Exam ID for getting activities of the exam - * - * @return the error of loading data - */ -func (l *examLoader) Load(userID int, examID int) error { - var wg sync.WaitGroup - var err error - concurrent := general.Concurrent{Wg: &wg, Err: &err} - wg.Add(2) - go l.loadCorrectedBadgeAsync(&concurrent, userID) - go l.loadExamActivityAsync(&concurrent, examID) - wg.Wait() - return err -} - -/** - * Load corrected badges of the user from the database - * - * @param userID User ID for getting corrected badges of the use - */ -func (l *examLoader) loadCorrectedBadgeAsync(concurrent *general.Concurrent, userID int) { - defer concurrent.Wg.Done() - result, err := l.userRepo.GetCollectedBadge(userID) - if err != nil { - *concurrent.Err = err - } - l.correctedBadgeDB = append(l.correctedBadgeDB, result...) -} - -/** - * Load activities of the exam from the database - * - * @param concurrent Concurrent model for doing load concurrency - * @param examID Exam ID for getting activities of the exam - */ -func (l *examLoader) loadExamActivityAsync(concurrent *general.Concurrent, examID int) { - defer concurrent.Wg.Done() - result, err := l.examRepo.GetExamActivity(examID) - if err != nil { - *concurrent.Err = err - } - l.examActivitiesDB = append(l.examActivitiesDB, result...) -} diff --git a/controllers/loaders/loader.exam_overview.go b/controllers/loaders/loader.exam_overview.go deleted file mode 100644 index 66fca96..0000000 --- a/controllers/loaders/loader.exam_overview.go +++ /dev/null @@ -1,129 +0,0 @@ -package loaders - -// loader.exam_overview.go -/** - * This file is a part of controllers, used to load concurrency overview of the exam - */ - -import ( - "DatabaseCamp/models/general" - "DatabaseCamp/models/storages" - "DatabaseCamp/repositories" - "sync" -) - -/** - * This class load concurrency overview of the exam - */ -type examOverviewLoader struct { - examRepo repositories.IExamRepository // repository for load all exam data - userRepo repositories.IUserRepository // repository load corrected badges and exam results of the user - - correctedBadgeDB []storages.CorrectedBadgeDB // corrected badges of the user from the database - examDB []storages.ExamDB // exam data from the database - examResultsDB []storages.ExamResultDB // exam results of the user from the database -} - -/** - * Constructor creates a new examOverviewLoader instance - * - * @param examRepo Exam Repository for load all exam data - * @param userRepo User Repository for load corrected badges and exam results of the user - * - * @return instance of examOverviewLoader - */ -func NewExamOverviewLoader(examRepo repositories.IExamRepository, userRepo repositories.IUserRepository) *examOverviewLoader { - return &examOverviewLoader{examRepo: examRepo, userRepo: userRepo} -} - -/** - * Getter for getting correctedBadgeDB - * - * @return correctedBadgeDB - */ -func (l *examOverviewLoader) GetCorrectedBadgeDB() []storages.CorrectedBadgeDB { - return l.correctedBadgeDB -} - -/** - * Getter for getting examDB - * - * @return examDB - */ -func (l *examOverviewLoader) GetExamDB() []storages.ExamDB { - return l.examDB -} - -/** - * Getter for getting examResultsDB - * - * @return examResultsDB - */ -func (l *examOverviewLoader) GetExamResultsDB() []storages.ExamResultDB { - return l.examResultsDB -} - -/** - * Load concurrency all activity data from the database - * - * @param userID User ID for getting corrected badges and exam results of the user - * - * @return the error of loading data - */ -func (l *examOverviewLoader) Load(userID int) error { - var wg sync.WaitGroup - var err error - concurrent := general.Concurrent{Wg: &wg, Err: &err} - wg.Add(3) - go l.loadCorrectedBadgeAsync(&concurrent, userID) - go l.loadExamAsync(&concurrent) - go l.loadExamResultAsync(&concurrent, userID) - wg.Wait() - return err -} - -/** - * Load exam results of the user from the database - * - * @param concurrent Concurrent model for doing load concurrency - * @param userID User ID for getting exam results of the user - * - * @return the error of loading data - */ -func (l *examOverviewLoader) loadExamResultAsync(concurrent *general.Concurrent, userID int) { - defer concurrent.Wg.Done() - result, err := l.userRepo.GetExamResult(userID) - if err != nil { - *concurrent.Err = err - } - l.examResultsDB = append(l.examResultsDB, result...) -} - -/** - * Load all exam from the database - * - * @param concurrent Concurrent model for doing load concurrency - */ -func (l *examOverviewLoader) loadExamAsync(concurrent *general.Concurrent) { - defer concurrent.Wg.Done() - result, err := l.examRepo.GetExamOverview() - if err != nil { - *concurrent.Err = err - } - l.examDB = append(l.examDB, result...) -} - -/** - * Load corrected badges of the user from the database - * - * @param concurrent Concurrent model for doing load concurrency - * @param userID User ID for getting corrected badges of the user - */ -func (l *examOverviewLoader) loadCorrectedBadgeAsync(concurrent *general.Concurrent, userID int) { - defer concurrent.Wg.Done() - result, err := l.userRepo.GetCollectedBadge(userID) - if err != nil { - *concurrent.Err = err - } - l.correctedBadgeDB = append(l.correctedBadgeDB, result...) -} diff --git a/controllers/loaders/loader.hint.go b/controllers/loaders/loader.hint.go deleted file mode 100644 index ab51028..0000000 --- a/controllers/loaders/loader.hint.go +++ /dev/null @@ -1,130 +0,0 @@ -package loaders - -// loader.hint.go -/** - * This file is a part of controllers, used to load concurrency hints data - */ - -import ( - "DatabaseCamp/models/general" - "DatabaseCamp/models/storages" - "DatabaseCamp/repositories" - "sync" -) - -/** - * This class load concurrency hints data - */ -type hintLoader struct { - learningRepo repositories.ILearningRepository // repository for hints data - userRepo repositories.IUserRepository // repository for load user data - - activityHintsDB []storages.HintDB // hints of the activity from the database - userHintsDB []storages.UserHintDB // user hints from the database - userDB *storages.UserDB // user data from the database -} - -/** - * Constructor creates a new hintLoader instance - * - * @param learningRepo Learning Repository for load hints data - * @param userRepo User Repository for load user data - * - * @return instance of hintLoader - */ -func NewHintLoader(learningRepo repositories.ILearningRepository, userRepo repositories.IUserRepository) *hintLoader { - return &hintLoader{learningRepo: learningRepo, userRepo: userRepo} -} - -/** - * Getter for getting activityHintsDB - * - * @return activityHintsDB - */ -func (l *hintLoader) GetActivityHintsDB() []storages.HintDB { - return l.activityHintsDB -} - -/** - * Getter for getting userHintsDB - * - * @return userHintsDB - */ -func (l *hintLoader) GetUserHintsDB() []storages.UserHintDB { - return l.userHintsDB -} - -/** - * Getter for getting userDB - * - * @return userDB - */ -func (l *hintLoader) GetUserDB() *storages.UserDB { - return l.userDB -} - -/** - * Load concurrency all activity data from the database - - * @param userID User ID for getting user data - * @param activityID Activity ID for getting hints data - * - * @return the error of loading data - */ -func (l *hintLoader) Load(userID int, activityID int) error { - var wg sync.WaitGroup - var err error - concurrent := general.Concurrent{Wg: &wg, Err: &err} - wg.Add(3) - go l.loadActivityHints(&concurrent, activityID) - go l.loadUserHintsAsync(&concurrent, userID, activityID) - go l.loadUser(&concurrent, userID) - wg.Wait() - return err -} - -/** - * Load user data from the database - * - * @param concurrent Concurrent model for doing load concurrency - * @param userID User ID for getting user data - */ -func (l *hintLoader) loadUser(concurrent *general.Concurrent, userID int) { - defer concurrent.Wg.Done() - var err error - l.userDB, err = l.userRepo.GetUserByID(userID) - if err != nil { - *concurrent.Err = err - } -} - -/** - * Load user hints from the database - * - * @param concurrent Concurrent model for doing load concurrency - * @param userID User ID for getting user hints of the activity data - * @param activityID Activity ID for indicate activity - */ -func (l *hintLoader) loadUserHintsAsync(concurrent *general.Concurrent, userID int, activityID int) { - defer concurrent.Wg.Done() - result, e := l.userRepo.GetUserHint(userID, activityID) - if e != nil { - *concurrent.Err = e - } - l.userHintsDB = append(l.userHintsDB, result...) -} - -/** - * Load hints of the activity from the database - * - * @param concurrent Concurrent model for doing load concurrency - * @param activityID Activity ID for getting hints of the activity - */ -func (l *hintLoader) loadActivityHints(concurrent *general.Concurrent, activityID int) { - defer concurrent.Wg.Done() - result, e := l.learningRepo.GetActivityHints(activityID) - if e != nil { - *concurrent.Err = e - } - l.activityHintsDB = append(l.activityHintsDB, result...) -} diff --git a/controllers/loaders/loader.learning_overview.go b/controllers/loaders/loader.learning_overview.go deleted file mode 100644 index 9aaba41..0000000 --- a/controllers/loaders/loader.learning_overview.go +++ /dev/null @@ -1,101 +0,0 @@ -package loaders - -// loader.learning_overview.go -/** - * This file is a part of controllers, used to load concurrency learning overview data - */ - -import ( - "DatabaseCamp/models/general" - "DatabaseCamp/models/storages" - "DatabaseCamp/repositories" - "sync" -) - -/** - * This class load concurrency learning overview data - */ -type learningOverviewLoader struct { - learningRepo repositories.ILearningRepository // repository for load learning overview data - userRepo repositories.IUserRepository // repository for load learning progression of the user data - - overviewDB []storages.OverviewDB // learning overview data from the database - learningProgressionDB []storages.LearningProgressionDB // learning progression of the user from the -} - -/** - * Constructor creates a new activityLoader instance - * - * @param learningRepo Learning Repository for load learning overview data - * @param userRepo User Repository for load learning progression of the user data - * - * @return instance of activityLoader - */ -func NewLearningOverviewLoader(learningRepo repositories.ILearningRepository, userRepo repositories.IUserRepository) *learningOverviewLoader { - return &learningOverviewLoader{learningRepo: learningRepo, userRepo: userRepo} -} - -/** - * Getter for getting overviewDB - * - * @return overviewDB - */ -func (l *learningOverviewLoader) GetOverviewDB() []storages.OverviewDB { - return l.overviewDB -} - -/** - * Getter for getting learningProgressionDB - * - * @return learningProgressionDB - */ -func (l *learningOverviewLoader) GetLearningProgressionDB() []storages.LearningProgressionDB { - return l.learningProgressionDB -} - -/** - * Load concurrency learning overview data from the database - * - * @param userID User ID for getting learning progression of the user - * - * @return the error of loading data - */ -func (l *learningOverviewLoader) Load(userID int) error { - var wg sync.WaitGroup - var err error - concurrent := general.Concurrent{Wg: &wg, Err: &err} - wg.Add(2) - go l.loadOverviewAsync(&concurrent) - go l.loadLearningProgressionAsync(&concurrent, userID) - wg.Wait() - return err -} - -/** - * Load learning overview data from the database - * - * @param concurrent Concurrent model for doing load concurrency - */ -func (l *learningOverviewLoader) loadOverviewAsync(concurrent *general.Concurrent) { - defer concurrent.Wg.Done() - result, err := l.learningRepo.GetOverview() - if err != nil { - *concurrent.Err = err - } - l.overviewDB = append(l.overviewDB, result...) -} - -/** - * Load activity data from the database - * - * @param concurrent Concurrent model for doing load concurrency - * @param userID User ID for getting learning progression of the user - */ -func (l *learningOverviewLoader) loadLearningProgressionAsync(concurrent *general.Concurrent, id int) { - defer concurrent.Wg.Done() - result, err := l.userRepo.GetLearningProgression(id) - if err != nil { - *concurrent.Err = err - } - l.learningProgressionDB = append(l.learningProgressionDB, result...) -} diff --git a/data/redis/appendonly.aof b/data/redis/appendonly.aof new file mode 100644 index 0000000..86ab640 --- /dev/null +++ b/data/redis/appendonly.aof @@ -0,0 +1,7908 @@ +*2 +$6 +SELECT +$1 +0 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644611511449 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644611556575 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644611575087 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644611777530 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644611938180 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644611990595 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644612030453 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644612073058 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644612757054 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644613162562 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644613186678 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644613221436 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644613254821 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644613356232 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644613474947 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*2 +$6 +SELECT +$1 +0 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644654243792 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644654519405 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644654585348 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644654651051 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644654748790 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644654779135 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644654809214 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::12 +$2 +[] +$4 +PXAT +$13 +1644654887509 +*5 +$3 +SET +$35 +learningRepository::GetActivity::12 +$113 +{"activity_id":12,"activity_type_id":7,"content_id":3,"activity_order":96,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644654887523 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::12 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::12 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644654906289 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644654934680 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644655004118 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644655047381 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644655078744 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644655091460 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644655113024 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644655167506 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644655241188 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644655251741 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$113 +{"activity_id":10,"activity_type_id":6,"content_id":5,"activity_order":87,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644655311632 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::12 +$2 +[] +$4 +PXAT +$13 +1644667372229 +*5 +$3 +SET +$35 +learningRepository::GetActivity::12 +$113 +{"activity_id":12,"activity_type_id":7,"content_id":3,"activity_order":96,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644667372256 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::12 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::12 +*5 +$3 +SET +$35 +learningRepository::GetActivity::12 +$113 +{"activity_id":12,"activity_type_id":7,"content_id":3,"activity_order":96,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644667420383 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::12 +$2 +[] +$4 +PXAT +$13 +1644667420403 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::12 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::12 +*5 +$3 +SET +$35 +learningRepository::GetActivity::12 +$113 +{"activity_id":12,"activity_type_id":7,"content_id":3,"activity_order":96,"story":"-","point":300,"question":"-"} +$4 +PXAT +$13 +1644667471456 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::12 +$2 +[] +$4 +PXAT +$13 +1644667471457 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::12 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::12 +*5 +$3 +SET +$26 +examRepository::GetExam::1 +$146 +{"exam_id":0,"exam_type":"","instruction":"","created_timestamp":"0001-01-01T00:00:00Z","content_group_id":0,"content_group_name":"","badge_id":0} +$4 +PXAT +$13 +1644684397023 +*5 +$3 +SET +$36 +examRepository::GetExamActivities::1 +$852 +[{"ActivityID":77,"ActivityTypeID":2},{"ActivityID":78,"ActivityTypeID":2},{"ActivityID":79,"ActivityTypeID":2},{"ActivityID":80,"ActivityTypeID":2},{"ActivityID":81,"ActivityTypeID":2},{"ActivityID":82,"ActivityTypeID":2},{"ActivityID":83,"ActivityTypeID":2},{"ActivityID":84,"ActivityTypeID":2},{"ActivityID":85,"ActivityTypeID":2},{"ActivityID":86,"ActivityTypeID":2},{"ActivityID":87,"ActivityTypeID":2},{"ActivityID":88,"ActivityTypeID":2},{"ActivityID":89,"ActivityTypeID":2},{"ActivityID":90,"ActivityTypeID":2},{"ActivityID":91,"ActivityTypeID":2},{"ActivityID":92,"ActivityTypeID":2},{"ActivityID":93,"ActivityTypeID":2},{"ActivityID":94,"ActivityTypeID":2},{"ActivityID":95,"ActivityTypeID":2},{"ActivityID":96,"ActivityTypeID":2},{"ActivityID":97,"ActivityTypeID":2},{"ActivityID":98,"ActivityTypeID":2},{"ActivityID":99,"ActivityTypeID":2}] +$4 +PXAT +$13 +1644684397028 +*2 +$3 +DEL +$36 +examRepository::GetExamActivities::1 +*2 +$3 +DEL +$26 +examRepository::GetExam::1 +*5 +$3 +SET +$36 +examRepository::GetExamActivities::1 +$852 +[{"ActivityID":77,"ActivityTypeID":2},{"ActivityID":78,"ActivityTypeID":2},{"ActivityID":79,"ActivityTypeID":2},{"ActivityID":80,"ActivityTypeID":2},{"ActivityID":81,"ActivityTypeID":2},{"ActivityID":82,"ActivityTypeID":2},{"ActivityID":83,"ActivityTypeID":2},{"ActivityID":84,"ActivityTypeID":2},{"ActivityID":85,"ActivityTypeID":2},{"ActivityID":86,"ActivityTypeID":2},{"ActivityID":87,"ActivityTypeID":2},{"ActivityID":88,"ActivityTypeID":2},{"ActivityID":89,"ActivityTypeID":2},{"ActivityID":90,"ActivityTypeID":2},{"ActivityID":91,"ActivityTypeID":2},{"ActivityID":92,"ActivityTypeID":2},{"ActivityID":93,"ActivityTypeID":2},{"ActivityID":94,"ActivityTypeID":2},{"ActivityID":95,"ActivityTypeID":2},{"ActivityID":96,"ActivityTypeID":2},{"ActivityID":97,"ActivityTypeID":2},{"ActivityID":98,"ActivityTypeID":2},{"ActivityID":99,"ActivityTypeID":2}] +$4 +PXAT +$13 +1644684455730 +*5 +$3 +SET +$26 +examRepository::GetExam::1 +$146 +{"exam_id":0,"exam_type":"","instruction":"","created_timestamp":"0001-01-01T00:00:00Z","content_group_id":0,"content_group_name":"","badge_id":0} +$4 +PXAT +$13 +1644684455744 +*2 +$3 +DEL +$36 +examRepository::GetExamActivities::1 +*2 +$3 +DEL +$26 +examRepository::GetExam::1 +*5 +$3 +SET +$36 +examRepository::GetExamActivities::1 +$852 +[{"ActivityID":77,"ActivityTypeID":2},{"ActivityID":78,"ActivityTypeID":2},{"ActivityID":79,"ActivityTypeID":2},{"ActivityID":80,"ActivityTypeID":2},{"ActivityID":81,"ActivityTypeID":2},{"ActivityID":82,"ActivityTypeID":2},{"ActivityID":83,"ActivityTypeID":2},{"ActivityID":84,"ActivityTypeID":2},{"ActivityID":85,"ActivityTypeID":2},{"ActivityID":86,"ActivityTypeID":2},{"ActivityID":87,"ActivityTypeID":2},{"ActivityID":88,"ActivityTypeID":2},{"ActivityID":89,"ActivityTypeID":2},{"ActivityID":90,"ActivityTypeID":2},{"ActivityID":91,"ActivityTypeID":2},{"ActivityID":92,"ActivityTypeID":2},{"ActivityID":93,"ActivityTypeID":2},{"ActivityID":94,"ActivityTypeID":2},{"ActivityID":95,"ActivityTypeID":2},{"ActivityID":96,"ActivityTypeID":2},{"ActivityID":97,"ActivityTypeID":2},{"ActivityID":98,"ActivityTypeID":2},{"ActivityID":99,"ActivityTypeID":2}] +$4 +PXAT +$13 +1644684488062 +*5 +$3 +SET +$26 +examRepository::GetExam::1 +$146 +{"exam_id":0,"exam_type":"","instruction":"","created_timestamp":"0001-01-01T00:00:00Z","content_group_id":0,"content_group_name":"","badge_id":0} +$4 +PXAT +$13 +1644684488063 +*2 +$3 +DEL +$36 +examRepository::GetExamActivities::1 +*2 +$3 +DEL +$26 +examRepository::GetExam::1 +*5 +$3 +SET +$36 +examRepository::GetExamActivities::1 +$852 +[{"ActivityID":77,"ActivityTypeID":2},{"ActivityID":78,"ActivityTypeID":2},{"ActivityID":79,"ActivityTypeID":2},{"ActivityID":80,"ActivityTypeID":2},{"ActivityID":81,"ActivityTypeID":2},{"ActivityID":82,"ActivityTypeID":2},{"ActivityID":83,"ActivityTypeID":2},{"ActivityID":84,"ActivityTypeID":2},{"ActivityID":85,"ActivityTypeID":2},{"ActivityID":86,"ActivityTypeID":2},{"ActivityID":87,"ActivityTypeID":2},{"ActivityID":88,"ActivityTypeID":2},{"ActivityID":89,"ActivityTypeID":2},{"ActivityID":90,"ActivityTypeID":2},{"ActivityID":91,"ActivityTypeID":2},{"ActivityID":92,"ActivityTypeID":2},{"ActivityID":93,"ActivityTypeID":2},{"ActivityID":94,"ActivityTypeID":2},{"ActivityID":95,"ActivityTypeID":2},{"ActivityID":96,"ActivityTypeID":2},{"ActivityID":97,"ActivityTypeID":2},{"ActivityID":98,"ActivityTypeID":2},{"ActivityID":99,"ActivityTypeID":2}] +$4 +PXAT +$13 +1644684798267 +*5 +$3 +SET +$26 +examRepository::GetExam::1 +$517 +{"exam_id":1,"exam_type":"PRE","instruction":"ข้อสอบมีทั้งสิ้น 21 ข้อ เพื่อทดสอบความเข้าใจทุกเนื้อหาในการเรียน หลังจากทำข้อสอบเสร็จจะได้รับคำแนะนำว่าควรเรียนกลุ่มเนื้อหาใด","created_timestamp":"2022-01-23T05:30:21+07:00","content_group_id":0,"content_group_name":"","badge_id":0} +$4 +PXAT +$13 +1644684798269 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::99 +$2 +[] +$4 +PXAT +$13 +1644684798345 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::77 +$2 +[] +$4 +PXAT +$13 +1644684798361 +*5 +$3 +SET +$35 +learningRepository::GetActivity::85 +$197 +{"activity_id":85,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่ประเภทของ Database Relational Key"} +$4 +PXAT +$13 +1644684798466 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::84 +$2 +[] +$4 +PXAT +$13 +1644684798466 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::94 +$2 +[] +$4 +PXAT +$13 +1644684798468 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::82 +$2 +[] +$4 +PXAT +$13 +1644684798468 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::81 +$2 +[] +$4 +PXAT +$13 +1644684798470 +*5 +$3 +SET +$35 +learningRepository::GetActivity::80 +$210 +{"activity_id":80,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถูกต้องเกี่ยวกับการออกแบบ Entity"} +$4 +PXAT +$13 +1644684798470 +*5 +$3 +SET +$35 +learningRepository::GetActivity::90 +$271 +{"activity_id":90,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นลำดับขั้นตอนของการออกแบบแบบ Bottom-Up Approach ที่ถูกต้อง"} +$4 +PXAT +$13 +1644684798470 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::95 +$2 +[] +$4 +PXAT +$13 +1644684798471 +*5 +$3 +SET +$35 +learningRepository::GetActivity::92 +$220 +{"activity_id":92,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"้ข้อใดไม่เป็นส่วนประกอบของ Relationship Data Dictionary"} +$4 +PXAT +$13 +1644684798471 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::87 +$2 +[] +$4 +PXAT +$13 +1644684798472 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::93 +$2 +[] +$4 +PXAT +$13 +1644684798473 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::86 +$2 +[] +$4 +PXAT +$13 +1644684798474 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::79 +$2 +[] +$4 +PXAT +$13 +1644684798475 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::92 +$2 +[] +$4 +PXAT +$13 +1644684798475 +*5 +$3 +SET +$35 +learningRepository::GetActivity::83 +$233 +{"activity_id":83,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ในการพิจารณา Relationship ของ Relation ข้อใดกล่าวถูกต้อง"} +$4 +PXAT +$13 +1644684798476 +*5 +$3 +SET +$35 +learningRepository::GetActivity::95 +$192 +{"activity_id":95,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ข้อใดถูกต้องเกี่ยวกับ Data Redundancy"} +$4 +PXAT +$13 +1644684798477 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::97 +$2 +[] +$4 +PXAT +$13 +1644684798477 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::78 +$2 +[] +$4 +PXAT +$13 +1644684798477 +*5 +$3 +SET +$35 +learningRepository::GetActivity::81 +$352 +{"activity_id":81,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ในการออกแบบฐานข้อมูลโรงเรียน ซึ่งประกอบไปด้วย นักเรียน เเละครู จงเลือกข้อที่เป็น Entity"} +$4 +PXAT +$13 +1644684798478 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::83 +$2 +[] +$4 +PXAT +$13 +1644684798480 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::80 +$2 +[] +$4 +PXAT +$13 +1644684798480 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::96 +$2 +[] +$4 +PXAT +$13 +1644684798481 +*5 +$3 +SET +$35 +learningRepository::GetActivity::87 +$214 +{"activity_id":87,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช้จุดประสงค์ของการวาด ER-Diagram"} +$4 +PXAT +$13 +1644684798481 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::98 +$2 +[] +$4 +PXAT +$13 +1644684798481 +*5 +$3 +SET +$35 +learningRepository::GetActivity::82 +$185 +{"activity_id":82,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่Relationship ใน Relational Database"} +$4 +PXAT +$13 +1644684798481 +*5 +$3 +SET +$35 +learningRepository::GetActivity::98 +$380 +{"activity_id":98,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ในการเก็บข้อมูลคุณครู ประกอบด้วย รหัสบุคลากร ชื่อจริง นามสกุล และเบอร์โทร ข้อใดเป็น Full Functional\r\nDependency"} +$4 +PXAT +$13 +1644684798482 +*5 +$3 +SET +$35 +learningRepository::GetActivity::91 +$211 +{"activity_id":91,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่เป็นส่วนประกอบของ Entity Data Dictionary"} +$4 +PXAT +$13 +1644684798483 +*5 +$3 +SET +$35 +learningRepository::GetActivity::96 +$186 +{"activity_id":96,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่ประเภทของ Data Anomaly"} +$4 +PXAT +$13 +1644684798484 +*5 +$3 +SET +$35 +learningRepository::GetActivity::79 +$196 +{"activity_id":79,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถูกต้องเกี่ยวกับ Relational Database"} +$4 +PXAT +$13 +1644684798484 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::90 +$2 +[] +$4 +PXAT +$13 +1644684798485 +*5 +$3 +SET +$35 +learningRepository::GetActivity::86 +$214 +{"activity_id":86,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ข้อใดกล่าวไม่ถูกต้องเกี่ยวกับ Candidate Key"} +$4 +PXAT +$13 +1644684798485 +*5 +$3 +SET +$35 +learningRepository::GetActivity::99 +$149 +{"activity_id":99,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถูกต้อง"} +$4 +PXAT +$13 +1644684798485 +*5 +$3 +SET +$35 +learningRepository::GetActivity::78 +$237 +{"activity_id":78,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ข้อใดไม่เป็นผลดีที่เกิดขึ้นจากการใช้ Database System"} +$4 +PXAT +$13 +1644684798488 +*5 +$3 +SET +$35 +learningRepository::GetActivity::77 +$248 +{"activity_id":77,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถือเป็นข้อจำกัดของการเก็บข้อมูลแบบ File-based System"} +$4 +PXAT +$13 +1644684798488 +*5 +$3 +SET +$35 +learningRepository::GetActivity::89 +$272 +{"activity_id":89,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นลำดับขั้นตอนของการออกแบบแบบ Inside-Out Approach ที่ถูกต้อง"} +$4 +PXAT +$13 +1644684798492 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::89 +$2 +[] +$4 +PXAT +$13 +1644684798493 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::88 +$2 +[] +$4 +PXAT +$13 +1644684798493 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::85 +$2 +[] +$4 +PXAT +$13 +1644684798495 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::91 +$2 +[] +$4 +PXAT +$13 +1644684798497 +*5 +$3 +SET +$35 +learningRepository::GetActivity::93 +$205 +{"activity_id":93,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นส่วนประกอบของ Attribute Data Dictionary"} +$4 +PXAT +$13 +1644684798497 +*5 +$3 +SET +$35 +learningRepository::GetActivity::88 +$270 +{"activity_id":88,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นลำดับขั้นตอนของการออกแบบแบบ Top-Down Approach ที่ถูกต้อง"} +$4 +PXAT +$13 +1644684798497 +*5 +$3 +SET +$35 +learningRepository::GetActivity::84 +$197 +{"activity_id":84,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่ประเภทของ Database Attributes\r\n"} +$4 +PXAT +$13 +1644684798498 +*5 +$3 +SET +$35 +learningRepository::GetActivity::94 +$198 +{"activity_id":94,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"เหตุการณ์ใดที่ทำให้เกิด Data Redundancy"} +$4 +PXAT +$13 +1644684798501 +*5 +$3 +SET +$35 +learningRepository::GetActivity::97 +$386 +{"activity_id":97,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ในการเก็บข้อมูลนักเรียน ประกอบด้วย รหัสประจำตัวนักเรียน ชื่อจริง เเละนามสกุล ข้อใดเป็น Functional\r\nDependency"} +$4 +PXAT +$13 +1644684798507 +*2 +$3 +DEL +$36 +examRepository::GetExamActivities::1 +*2 +$3 +DEL +$26 +examRepository::GetExam::1 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::77 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::92 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::87 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::85 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::80 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::78 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::95 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::96 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::99 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::93 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::93 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::91 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::90 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::90 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::81 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::83 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::95 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::78 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::88 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::86 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::96 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::77 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::86 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::92 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::87 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::97 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::81 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::84 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::85 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::89 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::82 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::94 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::98 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::88 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::91 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::79 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::83 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::89 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::98 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::84 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::80 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::79 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::94 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::99 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::97 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::82 +*5 +$3 +SET +$35 +learningRepository::GetActivity::64 +$1153 +{"activity_id":64,"activity_type_id":5,"content_id":17,"activity_order":1,"story":"นายแกนกำลังออกแบบฐานข้อมูลภายในโรงเรียนเเห่งหนึ่ง โดยประกอบไปด้วยข้อมูลต่าง ๆ ดังนี้\r\n\u003cli\u003eTeacher เก็บข้อมูลเกี่ยวกับครูภายในโรงเรียน โดยข้อมูลประกอบไปด้วย\u003c/li\u003e\r\n\u003cli\u003eteacher_id เป็นรหัสประจำตัวของครู\u003c/li\u003e\r\n\u003cli\u003efirst_name เป็นชื่อจริงของครู\u003c/li\u003e\r\n\u003cli\u003elast_name เป็นนามสกุลของครู\u003c/li\u003e","point":300,"question":"\u003cimg src=\"https://storage.googleapis.com/databasecamp-public/material/dependency1.png\" style=\"width:400px\" /\u003e จงเลือก Attribute ที่เป็น Functional Dependency กันไปวางในช่องว่างให้ถูกต้อง"} +$4 +PXAT +$13 +1644689378624 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::64 +$2 +[] +$4 +PXAT +$13 +1644689378729 +*5 +$3 +SET +$43 +learningRepository::getDependencyChoice::64 +$203 +{"ID":2,"Dependencies":[{"dependent":"teacher_id","determinants":[{"value":"first_name"},{"value":"last_name"}]},{"dependent":"last_name","determinants":[{"value":"teacher_id"},{"value":"first_name"}]}]} +$4 +PXAT +$13 +1644689378793 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::64 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::64 +*2 +$3 +DEL +$43 +learningRepository::getDependencyChoice::64 +*5 +$3 +SET +$35 +learningRepository::GetActivity::64 +$1153 +{"activity_id":64,"activity_type_id":5,"content_id":17,"activity_order":1,"story":"นายแกนกำลังออกแบบฐานข้อมูลภายในโรงเรียนเเห่งหนึ่ง โดยประกอบไปด้วยข้อมูลต่าง ๆ ดังนี้\r\n\u003cli\u003eTeacher เก็บข้อมูลเกี่ยวกับครูภายในโรงเรียน โดยข้อมูลประกอบไปด้วย\u003c/li\u003e\r\n\u003cli\u003eteacher_id เป็นรหัสประจำตัวของครู\u003c/li\u003e\r\n\u003cli\u003efirst_name เป็นชื่อจริงของครู\u003c/li\u003e\r\n\u003cli\u003elast_name เป็นนามสกุลของครู\u003c/li\u003e","point":300,"question":"\u003cimg src=\"https://storage.googleapis.com/databasecamp-public/material/dependency1.png\" style=\"width:400px\" /\u003e จงเลือก Attribute ที่เป็น Functional Dependency กันไปวางในช่องว่างให้ถูกต้อง"} +$4 +PXAT +$13 +1644689389933 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::64 +$2 +[] +$4 +PXAT +$13 +1644689390051 +*5 +$3 +SET +$43 +learningRepository::getDependencyChoice::64 +$203 +{"ID":2,"Dependencies":[{"dependent":"last_name","determinants":[{"value":"teacher_id"},{"value":"first_name"}]},{"dependent":"teacher_id","determinants":[{"value":"first_name"},{"value":"last_name"}]}]} +$4 +PXAT +$13 +1644689390112 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::64 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::64 +*2 +$3 +DEL +$43 +learningRepository::getDependencyChoice::64 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::64 +$2 +[] +$4 +PXAT +$13 +1644689400491 +*5 +$3 +SET +$35 +learningRepository::GetActivity::64 +$1153 +{"activity_id":64,"activity_type_id":5,"content_id":17,"activity_order":1,"story":"นายแกนกำลังออกแบบฐานข้อมูลภายในโรงเรียนเเห่งหนึ่ง โดยประกอบไปด้วยข้อมูลต่าง ๆ ดังนี้\r\n\u003cli\u003eTeacher เก็บข้อมูลเกี่ยวกับครูภายในโรงเรียน โดยข้อมูลประกอบไปด้วย\u003c/li\u003e\r\n\u003cli\u003eteacher_id เป็นรหัสประจำตัวของครู\u003c/li\u003e\r\n\u003cli\u003efirst_name เป็นชื่อจริงของครู\u003c/li\u003e\r\n\u003cli\u003elast_name เป็นนามสกุลของครู\u003c/li\u003e","point":300,"question":"\u003cimg src=\"https://storage.googleapis.com/databasecamp-public/material/dependency1.png\" style=\"width:400px\" /\u003e จงเลือก Attribute ที่เป็น Functional Dependency กันไปวางในช่องว่างให้ถูกต้อง"} +$4 +PXAT +$13 +1644689400603 +*5 +$3 +SET +$43 +learningRepository::getDependencyChoice::64 +$203 +{"ID":2,"Dependencies":[{"dependent":"teacher_id","determinants":[{"value":"first_name"},{"value":"last_name"}]},{"dependent":"last_name","determinants":[{"value":"teacher_id"},{"value":"first_name"}]}]} +$4 +PXAT +$13 +1644689400662 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::64 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::64 +*2 +$3 +DEL +$43 +learningRepository::getDependencyChoice::64 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::64 +$2 +[] +$4 +PXAT +$13 +1644689487080 +*5 +$3 +SET +$35 +learningRepository::GetActivity::64 +$1153 +{"activity_id":64,"activity_type_id":5,"content_id":17,"activity_order":1,"story":"นายแกนกำลังออกแบบฐานข้อมูลภายในโรงเรียนเเห่งหนึ่ง โดยประกอบไปด้วยข้อมูลต่าง ๆ ดังนี้\r\n\u003cli\u003eTeacher เก็บข้อมูลเกี่ยวกับครูภายในโรงเรียน โดยข้อมูลประกอบไปด้วย\u003c/li\u003e\r\n\u003cli\u003eteacher_id เป็นรหัสประจำตัวของครู\u003c/li\u003e\r\n\u003cli\u003efirst_name เป็นชื่อจริงของครู\u003c/li\u003e\r\n\u003cli\u003elast_name เป็นนามสกุลของครู\u003c/li\u003e","point":300,"question":"\u003cimg src=\"https://storage.googleapis.com/databasecamp-public/material/dependency1.png\" style=\"width:400px\" /\u003e จงเลือก Attribute ที่เป็น Functional Dependency กันไปวางในช่องว่างให้ถูกต้อง"} +$4 +PXAT +$13 +1644689487083 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::64 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::64 +*5 +$3 +SET +$35 +learningRepository::GetActivity::64 +$1153 +{"activity_id":64,"activity_type_id":5,"content_id":17,"activity_order":1,"story":"นายแกนกำลังออกแบบฐานข้อมูลภายในโรงเรียนเเห่งหนึ่ง โดยประกอบไปด้วยข้อมูลต่าง ๆ ดังนี้\r\n\u003cli\u003eTeacher เก็บข้อมูลเกี่ยวกับครูภายในโรงเรียน โดยข้อมูลประกอบไปด้วย\u003c/li\u003e\r\n\u003cli\u003eteacher_id เป็นรหัสประจำตัวของครู\u003c/li\u003e\r\n\u003cli\u003efirst_name เป็นชื่อจริงของครู\u003c/li\u003e\r\n\u003cli\u003elast_name เป็นนามสกุลของครู\u003c/li\u003e","point":300,"question":"\u003cimg src=\"https://storage.googleapis.com/databasecamp-public/material/dependency1.png\" style=\"width:400px\" /\u003e จงเลือก Attribute ที่เป็น Functional Dependency กันไปวางในช่องว่างให้ถูกต้อง"} +$4 +PXAT +$13 +1644689501848 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::64 +$2 +[] +$4 +PXAT +$13 +1644689501942 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::64 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::64 +*5 +$3 +SET +$26 +examRepository::GetExam::1 +$517 +{"exam_id":1,"exam_type":"PRE","instruction":"ข้อสอบมีทั้งสิ้น 24 ข้อ เพื่อทดสอบความเข้าใจทุกเนื้อหาในการเรียน หลังจากทำข้อสอบเสร็จจะได้รับคำแนะนำว่าควรเรียนกลุ่มเนื้อหาใด","created_timestamp":"2022-02-12T16:56:43+07:00","content_group_id":0,"content_group_name":"","badge_id":0} +$4 +PXAT +$13 +1644690255535 +*5 +$3 +SET +$36 +examRepository::GetExamActivities::1 +$890 +[{"ActivityID":77,"ActivityTypeID":2},{"ActivityID":78,"ActivityTypeID":2},{"ActivityID":79,"ActivityTypeID":2},{"ActivityID":100,"ActivityTypeID":2},{"ActivityID":80,"ActivityTypeID":2},{"ActivityID":81,"ActivityTypeID":2},{"ActivityID":82,"ActivityTypeID":2},{"ActivityID":83,"ActivityTypeID":2},{"ActivityID":84,"ActivityTypeID":2},{"ActivityID":85,"ActivityTypeID":2},{"ActivityID":86,"ActivityTypeID":2},{"ActivityID":87,"ActivityTypeID":2},{"ActivityID":88,"ActivityTypeID":2},{"ActivityID":89,"ActivityTypeID":2},{"ActivityID":90,"ActivityTypeID":2},{"ActivityID":91,"ActivityTypeID":2},{"ActivityID":92,"ActivityTypeID":2},{"ActivityID":93,"ActivityTypeID":2},{"ActivityID":94,"ActivityTypeID":2},{"ActivityID":95,"ActivityTypeID":2},{"ActivityID":96,"ActivityTypeID":2},{"ActivityID":97,"ActivityTypeID":2},{"ActivityID":98,"ActivityTypeID":2},{"ActivityID":99,"ActivityTypeID":2}] +$4 +PXAT +$13 +1644690255645 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::77 +$642 +[{"multiple_choice_id":20,"content":"รูปแบบไฟล์แตกต่างกัน","is_correct":true},{"multiple_choice_id":21,"content":"ค่าใช้จ่ายสูง","is_correct":false},{"multiple_choice_id":22,"content":"ต้องใช้ผู้เชี่ยวชาญในการจัดการ","is_correct":false},{"multiple_choice_id":23,"content":"ข้อมูลมีความเเม่นยำสูง\r\n","is_correct":false},{"multiple_choice_id":24,"content":"คนไม่เกี่ยวข้องสามารถเข้าถึงได้","is_correct":false}] +$4 +PXAT +$13 +1644690255733 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::99 +$699 +[{"multiple_choice_id":154,"content":"Normalization คือการทำให้ฐานข้อมูลเก็บข้อมูลได้มากขึ้น","is_correct":false},{"multiple_choice_id":155,"content":"ฐานข้อมูลที่ดีควรจะเป็นแบบ First Normal Form\r\n","is_correct":false},{"multiple_choice_id":156,"content":"First Normal Form ไม่เป็น Second Normal Form","is_correct":true},{"multiple_choice_id":157,"content":"Third Normal Form ไม่เป็น First Normal Form\r\n","is_correct":false},{"multiple_choice_id":158,"content":"ไม่มีข้อใดถูกต้อง","is_correct":false}] +$4 +PXAT +$13 +1644690255736 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::78 +$715 +[{"multiple_choice_id":25,"content":"ข้อมูลที่ทุกคนได้รับเป็นข้อมูลที่สอดคล้องกัน\r\n","is_correct":false},{"multiple_choice_id":26,"content":"สามารถเเชร์ข้อมูลระหว่างคอมพิวเตอร์ได","is_correct":false},{"multiple_choice_id":27,"content":"ง่ายต่อการใช้งาน","is_correct":false},{"multiple_choice_id":28,"content":"บุคคลใดก็สามารถเข้าถึงได้\r\n","is_correct":true},{"multiple_choice_id":29,"content":"ทุกข้อไม่เป็นผลดี","is_correct":false}] +$4 +PXAT +$13 +1644690255838 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::83 +$844 +[{"multiple_choice_id":74,"content":"พิจารณาโดยคำนึงถึงความแตกต่างระหว่าง Relation ","is_correct":false},{"multiple_choice_id":75,"content":"พิจารณาจากความสัมพันธ์ระหว่าง Relation ว่าแต่ละ Relation มีความเชื่อมโยงกันอย่างไร","is_correct":true},{"multiple_choice_id":76,"content":"พิจารณาว่าในแต่ละ Relation ต้องมีข้อมูลจากอีก Relation เท่าไร อย่างไร\r\n","is_correct":false},{"multiple_choice_id":77,"content":"ิผิดทุกข้อ\r\n","is_correct":false},{"multiple_choice_id":78,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690255840 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::87 +$864 +[{"multiple_choice_id":94,"content":"เพื่อสื่อสารกับผู้ร่วมงานเกี่ยวกับการออกแบบฐานข้อมูล","is_correct":false},{"multiple_choice_id":95,"content":"เพื่อให้ลูกค้าเห็นถึงความเป็นมืออาชีพในการทำงาน\r\n","is_correct":true},{"multiple_choice_id":96,"content":"เพื่อเป็นบันทึกเอกสารในรายละเอียดการทำงาน","is_correct":false},{"multiple_choice_id":97,"content":"เพื่อทำให้เห็นภาพรวมของการออกแบบฐานข้อมูล","is_correct":false},{"multiple_choice_id":98,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690255840 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::88 +$523 +[{"multiple_choice_id":99,"content":"Entity -\u003e Relationship -\u003e Attribute","is_correct":true},{"multiple_choice_id":100,"content":"Attribute -\u003e Relationship -\u003e Entity","is_correct":false},{"multiple_choice_id":101,"content":"Attribute -\u003e Entity -\u003e Relationship","is_correct":false},{"multiple_choice_id":102,"content":"Relationship -\u003e Entity -\u003e Attribute\r\n","is_correct":false},{"multiple_choice_id":103,"content":"Entity -\u003e Attribute -\u003e Relationship","is_correct":false}] +$4 +PXAT +$13 +1644690255842 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::92 +$578 +[{"multiple_choice_id":119,"content":"ชื่อเรียกความสัมพันธ์ของระหว่าง Entity\r\n","is_correct":false},{"multiple_choice_id":120,"content":"Multiplicity","is_correct":false},{"multiple_choice_id":121,"content":"เหตุการณ์ที่ทำให้เกิดความสัมพันธ์\r\n","is_correct":true},{"multiple_choice_id":122,"content":"ชื่อของสอง Entity","is_correct":false},{"multiple_choice_id":123,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690255843 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::84 +$384 +[{"multiple_choice_id":79,"content":"Composite Attribute","is_correct":false},{"multiple_choice_id":80,"content":"Simple Attribute","is_correct":false},{"multiple_choice_id":81,"content":"Derived Attribute","is_correct":false},{"multiple_choice_id":82,"content":"Group Attribute","is_correct":true},{"multiple_choice_id":83,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690255843 +*5 +$3 +SET +$35 +learningRepository::GetActivity::77 +$248 +{"activity_id":77,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถือเป็นข้อจำกัดของการเก็บข้อมูลแบบ File-based System"} +$4 +PXAT +$13 +1644690255843 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::98 +$728 +[{"multiple_choice_id":149,"content":"{รหัสบุคลากร} -\u003e {ชื่อจริง}","is_correct":true},{"multiple_choice_id":150,"content":"{เบอร์โทร, รหัสบุคลากร} -\u003e {ชื่อจริง}","is_correct":false},{"multiple_choice_id":151,"content":"{ชื่อจริง, นามสกุล} -\u003e {เบอร์โทร}\r\n","is_correct":false},{"multiple_choice_id":152,"content":"{ชื่อจริง, รหัสบุคลากร, เบอร์โทร} -\u003e {ชื่อจริง, เบอร์โทร}\r\n","is_correct":false},{"multiple_choice_id":153,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690255844 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::90 +$524 +[{"multiple_choice_id":109,"content":"Entity -\u003e Relationship -\u003e Attribute\r\n","is_correct":false},{"multiple_choice_id":110,"content":"Attribute -\u003e Relationship -\u003e Entity","is_correct":false},{"multiple_choice_id":111,"content":"Attribute -\u003e Entity -\u003e Relationship","is_correct":true},{"multiple_choice_id":112,"content":"Relationship -\u003e Entity -\u003e Attribute","is_correct":false},{"multiple_choice_id":113,"content":"Entity -\u003e Attribute -\u003e Relationship","is_correct":false}] +$4 +PXAT +$13 +1644690255846 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::86 +$682 +[{"multiple_choice_id":89,"content":"สามารถใช้บ่งบอก Tuple ใน Relation นั้น ๆ ได้","is_correct":false},{"multiple_choice_id":90,"content":"Candidate Key สามารถถูกใช้เป็น Primary Key ได้","is_correct":false},{"multiple_choice_id":91,"content":"Candidate Key ที่ไม่เป็น Primary Key จะเรียกว่า Super Key","is_correct":true},{"multiple_choice_id":92,"content":"Candidate Key สามารถประกอบไปด้วยหลาย Attribute","is_correct":false},{"multiple_choice_id":93,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690255846 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::79 +$868 +[{"multiple_choice_id":54,"content":"สามารถมี Relation ที่เหมือนกันได้หลาย ๆ อัน เพื่อเก็บข้อมูลที่เกิดขึ้นซ้ำ ๆ","is_correct":false},{"multiple_choice_id":55,"content":"ใน Relation สามารถมี Attribute ที่เหมือนกันได้หลาย ๆ อัน เพื่อเก็บข้อมูลเดียวกันหลาย ๆ ข้อมูล","is_correct":false},{"multiple_choice_id":56,"content":"ข้อมูลใน Attribute ต้องอยู่ใน Domain เดียวกัน","is_correct":true},{"multiple_choice_id":57,"content":"ิผิดทุกข้อ","is_correct":false},{"multiple_choice_id":58,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690255846 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::81 +$466 +[{"multiple_choice_id":64,"content":"ชื่อครู\r\n","is_correct":false},{"multiple_choice_id":65,"content":"นามสกุลครู\r\n","is_correct":false},{"multiple_choice_id":66,"content":"เบอร์โทรนักเรียน","is_correct":false},{"multiple_choice_id":67,"content":"นักเรียน","is_correct":true},{"multiple_choice_id":68,"content":"วันเกิดนักเรียน","is_correct":false}] +$4 +PXAT +$13 +1644690255846 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::80 +$917 +[{"multiple_choice_id":59,"content":"Entity เป็นข้อมูลที่เล็กที่สุดใน Relational Database","is_correct":false},{"multiple_choice_id":60,"content":"Entity คือกลุ่มของข้อมูลที่มีคุณสมบัติเหมือนกัน\r\n","is_correct":true},{"multiple_choice_id":61,"content":"ข้อมูลที่อยู่ใน Entity สามารถเป็นข้อมูลใดก็ได้ เพียงเเค่ชื่อเหมือนกับ Entity","is_correct":false},{"multiple_choice_id":62,"content":"การออกแบบ Entity ต้องคำนึงว่ามีสิ่งไหนบ้างที่สามารถจับต้องได","is_correct":false},{"multiple_choice_id":63,"content":"ถูกต้องข้อ\r\n","is_correct":false}] +$4 +PXAT +$13 +1644690255848 +*5 +$3 +SET +$35 +learningRepository::GetActivity::83 +$233 +{"activity_id":83,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ในการพิจารณา Relationship ของ Relation ข้อใดกล่าวถูกต้อง"} +$4 +PXAT +$13 +1644690255848 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::91 +$528 +[{"multiple_choice_id":114,"content":"คำอธิบาย Entity","is_correct":false},{"multiple_choice_id":115,"content":"ความเกี่ยวข้องกับ Entity อื่น","is_correct":true},{"multiple_choice_id":116,"content":"ชื่อเรียก Entity","is_correct":false},{"multiple_choice_id":117,"content":"เหตุการณ์ที่ทำให้เกิด Entity","is_correct":false},{"multiple_choice_id":118,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690255850 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::94 +$753 +[{"multiple_choice_id":129,"content":"มี Relation ที่เหมือนกันหลายอันในฐานข้อมูล","is_correct":false},{"multiple_choice_id":130,"content":"มี Attribute ที่เหมือนกันหลายอันในฐานข้อมูล","is_correct":false},{"multiple_choice_id":131,"content":"มีข้อมูลที่เหมือนกันในหลาย ๆ Tuple","is_correct":true},{"multiple_choice_id":132,"content":"มี Relation ที่มีAttribute มากจนเกินไป\r\n","is_correct":false},{"multiple_choice_id":133,"content":"ทุกเหตุการณ์ทำให้เกิด Data Redundancy","is_correct":false}] +$4 +PXAT +$13 +1644690255850 +*5 +$3 +SET +$35 +learningRepository::GetActivity::90 +$271 +{"activity_id":90,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นลำดับขั้นตอนของการออกแบบแบบ Bottom-Up Approach ที่ถูกต้อง"} +$4 +PXAT +$13 +1644690255852 +*5 +$3 +SET +$35 +learningRepository::GetActivity::78 +$237 +{"activity_id":78,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ข้อใดไม่เป็นผลดีที่เกิดขึ้นจากการใช้ Database System"} +$4 +PXAT +$13 +1644690255853 +*5 +$3 +SET +$35 +learningRepository::GetActivity::98 +$380 +{"activity_id":98,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ในการเก็บข้อมูลคุณครู ประกอบด้วย รหัสบุคลากร ชื่อจริง นามสกุล และเบอร์โทร ข้อใดเป็น Full Functional\r\nDependency"} +$4 +PXAT +$13 +1644690255853 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::93 +$418 +[{"multiple_choice_id":124,"content":"Data Type","is_correct":true},{"multiple_choice_id":125,"content":"Multiplicity","is_correct":false},{"multiple_choice_id":126,"content":"Tuple","is_correct":false},{"multiple_choice_id":127,"content":"เหตุการณ์ที่ทำให้เกิด Entity","is_correct":false},{"multiple_choice_id":128,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690255853 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::97 +$650 +[{"multiple_choice_id":144,"content":"{รหัสนักเรียน} -\u003e {ชื่อจริง, นามสกุล}","is_correct":true},{"multiple_choice_id":145,"content":"{ชื่อจริง} -\u003e {รหัสนักเรียน}","is_correct":false},{"multiple_choice_id":146,"content":"{ชื่อจริง} -\u003e {นามสกุล}","is_correct":false},{"multiple_choice_id":147,"content":"{นามสกุล} -\u003e {ชื่อจริง}","is_correct":false},{"multiple_choice_id":148,"content":"{นามสกุล} -\u003e {รหัสนักเรียน}","is_correct":false}] +$4 +PXAT +$13 +1644690255853 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::82 +$417 +[{"multiple_choice_id":69,"content":"One to One Relationship\r\n","is_correct":false},{"multiple_choice_id":70,"content":"One to Many Relationship","is_correct":false},{"multiple_choice_id":71,"content":"Many to Many Relationship","is_correct":false},{"multiple_choice_id":72,"content":"None to One Relationship","is_correct":true},{"multiple_choice_id":73,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690255855 +*5 +$3 +SET +$35 +learningRepository::GetActivity::89 +$272 +{"activity_id":89,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นลำดับขั้นตอนของการออกแบบแบบ Inside-Out Approach ที่ถูกต้อง"} +$4 +PXAT +$13 +1644690255858 +*5 +$3 +SET +$35 +learningRepository::GetActivity::81 +$352 +{"activity_id":81,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ในการออกแบบฐานข้อมูลโรงเรียน ซึ่งประกอบไปด้วย นักเรียน เเละครู จงเลือกข้อที่เป็น Entity"} +$4 +PXAT +$13 +1644690255858 +*5 +$3 +SET +$35 +learningRepository::GetActivity::92 +$220 +{"activity_id":92,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"้ข้อใดไม่เป็นส่วนประกอบของ Relationship Data Dictionary"} +$4 +PXAT +$13 +1644690255861 +*5 +$3 +SET +$36 +learningRepository::GetActivity::100 +$176 +{"activity_id":100,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่อยู่ใน Relational Database"} +$4 +PXAT +$13 +1644690255862 +*5 +$3 +SET +$35 +learningRepository::GetActivity::82 +$185 +{"activity_id":82,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่Relationship ใน Relational Database"} +$4 +PXAT +$13 +1644690255862 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::89 +$520 +[{"multiple_choice_id":104,"content":"Entity -\u003e Relationship -\u003e Attribute","is_correct":false},{"multiple_choice_id":105,"content":"Attribute -\u003e Relationship -\u003e Entity","is_correct":false},{"multiple_choice_id":106,"content":"Attribute -\u003e Entity -\u003e Relationship","is_correct":false},{"multiple_choice_id":107,"content":"Relationship -\u003e Entity -\u003e Attribute","is_correct":false},{"multiple_choice_id":108,"content":"Entity -\u003e Attribute -\u003e Relationship","is_correct":true}] +$4 +PXAT +$13 +1644690255865 +*5 +$3 +SET +$35 +learningRepository::GetActivity::88 +$270 +{"activity_id":88,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นลำดับขั้นตอนของการออกแบบแบบ Top-Down Approach ที่ถูกต้อง"} +$4 +PXAT +$13 +1644690255866 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::85 +$347 +[{"multiple_choice_id":84,"content":"Super Key","is_correct":false},{"multiple_choice_id":85,"content":"Primary Key","is_correct":false},{"multiple_choice_id":86,"content":"Foreign Key","is_correct":false},{"multiple_choice_id":87,"content":"Candidate Key","is_correct":false},{"multiple_choice_id":88,"content":"President Key","is_correct":true}] +$4 +PXAT +$13 +1644690255866 +*5 +$3 +SET +$35 +learningRepository::GetActivity::79 +$196 +{"activity_id":79,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถูกต้องเกี่ยวกับ Relational Database"} +$4 +PXAT +$13 +1644690255866 +*5 +$3 +SET +$35 +learningRepository::GetActivity::99 +$149 +{"activity_id":99,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถูกต้อง"} +$4 +PXAT +$13 +1644690255868 +*5 +$3 +SET +$42 +learningRepository::getMultipleChoice::100 +$329 +[{"multiple_choice_id":30,"content":"Tuple","is_correct":false},{"multiple_choice_id":31,"content":"Attribute","is_correct":false},{"multiple_choice_id":32,"content":"Degree","is_correct":false},{"multiple_choice_id":33,"content":"Cardinality","is_correct":false},{"multiple_choice_id":34,"content":"Template","is_correct":true}] +$4 +PXAT +$13 +1644690255868 +*5 +$3 +SET +$35 +learningRepository::GetActivity::95 +$192 +{"activity_id":95,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ข้อใดถูกต้องเกี่ยวกับ Data Redundancy"} +$4 +PXAT +$13 +1644690255869 +*5 +$3 +SET +$35 +learningRepository::GetActivity::87 +$214 +{"activity_id":87,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช้จุดประสงค์ของการวาด ER-Diagram"} +$4 +PXAT +$13 +1644690255869 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::95 +$1065 +[{"multiple_choice_id":134,"content":"การเก็บข้อมูลที่ซ้ำกันใน Relation เดียว ทำให้สามารถจัดการได้ง่ายขึ้น\r\n","is_correct":false},{"multiple_choice_id":135,"content":"การเเก้ปัญหา Data Redundancy สามารถทำได้ด้วยการเเยกข้อมูลที่ซ้ำ ไปเก็บใน Relation ใหม","is_correct":true},{"multiple_choice_id":136,"content":"การทำให้เกิด Data Redundancy ทำให้เมื่อต้องการอัพเดทข้อมูล\r\nสามารถทำได้ง่ายเนื่องจากมีข้อมูลที่เหมือนกันอยู่ในที่เดียวกัน","is_correct":false},{"multiple_choice_id":137,"content":"ถูกทุกข้อ\r\n","is_correct":false},{"multiple_choice_id":138,"content":"ผิดทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690255871 +*5 +$3 +SET +$35 +learningRepository::GetActivity::93 +$205 +{"activity_id":93,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นส่วนประกอบของ Attribute Data Dictionary"} +$4 +PXAT +$13 +1644690255873 +*5 +$3 +SET +$35 +learningRepository::GetActivity::94 +$198 +{"activity_id":94,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"เหตุการณ์ใดที่ทำให้เกิด Data Redundancy"} +$4 +PXAT +$13 +1644690255874 +*5 +$3 +SET +$35 +learningRepository::GetActivity::80 +$210 +{"activity_id":80,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถูกต้องเกี่ยวกับการออกแบบ Entity"} +$4 +PXAT +$13 +1644690255877 +*5 +$3 +SET +$35 +learningRepository::GetActivity::96 +$186 +{"activity_id":96,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่ประเภทของ Data Anomaly"} +$4 +PXAT +$13 +1644690255879 +*5 +$3 +SET +$35 +learningRepository::GetActivity::85 +$197 +{"activity_id":85,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่ประเภทของ Database Relational Key"} +$4 +PXAT +$13 +1644690255879 +*5 +$3 +SET +$35 +learningRepository::GetActivity::84 +$197 +{"activity_id":84,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่ประเภทของ Database Attributes\r\n"} +$4 +PXAT +$13 +1644690255879 +*5 +$3 +SET +$35 +learningRepository::GetActivity::86 +$214 +{"activity_id":86,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ข้อใดกล่าวไม่ถูกต้องเกี่ยวกับ Candidate Key"} +$4 +PXAT +$13 +1644690255880 +*5 +$3 +SET +$35 +learningRepository::GetActivity::91 +$211 +{"activity_id":91,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่เป็นส่วนประกอบของ Entity Data Dictionary"} +$4 +PXAT +$13 +1644690255881 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::96 +$388 +[{"multiple_choice_id":139,"content":"Insert Anomaly","is_correct":false},{"multiple_choice_id":140,"content":"Modification Anomaly","is_correct":false},{"multiple_choice_id":141,"content":"Delete Anomaly","is_correct":false},{"multiple_choice_id":142,"content":"Manipulate Anomaly","is_correct":true},{"multiple_choice_id":143,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690255882 +*5 +$3 +SET +$35 +learningRepository::GetActivity::97 +$386 +{"activity_id":97,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ในการเก็บข้อมูลนักเรียน ประกอบด้วย รหัสประจำตัวนักเรียน ชื่อจริง เเละนามสกุล ข้อใดเป็น Functional\r\nDependency"} +$4 +PXAT +$13 +1644690255883 +*2 +$3 +DEL +$36 +examRepository::GetExamActivities::1 +*2 +$3 +DEL +$26 +examRepository::GetExam::1 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::80 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::77 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::92 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::87 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::99 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::90 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::81 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::83 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::78 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::88 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::86 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::77 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::84 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::94 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::98 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::91 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::79 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::83 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::84 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::79 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::94 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::97 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::82 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::99 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::85 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::80 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::78 +*2 +$3 +DEL +$42 +learningRepository::getMultipleChoice::100 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::95 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::96 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::93 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::93 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::91 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::90 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::95 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::86 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::96 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::87 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::92 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::97 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::81 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::89 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::85 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::82 +*2 +$3 +DEL +$36 +learningRepository::GetActivity::100 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::88 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::98 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::89 +*5 +$3 +SET +$36 +examRepository::GetExamActivities::1 +$890 +[{"ActivityID":77,"ActivityTypeID":2},{"ActivityID":78,"ActivityTypeID":2},{"ActivityID":79,"ActivityTypeID":2},{"ActivityID":100,"ActivityTypeID":2},{"ActivityID":80,"ActivityTypeID":2},{"ActivityID":81,"ActivityTypeID":2},{"ActivityID":82,"ActivityTypeID":2},{"ActivityID":83,"ActivityTypeID":2},{"ActivityID":84,"ActivityTypeID":2},{"ActivityID":85,"ActivityTypeID":2},{"ActivityID":86,"ActivityTypeID":2},{"ActivityID":87,"ActivityTypeID":2},{"ActivityID":88,"ActivityTypeID":2},{"ActivityID":89,"ActivityTypeID":2},{"ActivityID":90,"ActivityTypeID":2},{"ActivityID":91,"ActivityTypeID":2},{"ActivityID":92,"ActivityTypeID":2},{"ActivityID":93,"ActivityTypeID":2},{"ActivityID":94,"ActivityTypeID":2},{"ActivityID":95,"ActivityTypeID":2},{"ActivityID":96,"ActivityTypeID":2},{"ActivityID":97,"ActivityTypeID":2},{"ActivityID":98,"ActivityTypeID":2},{"ActivityID":99,"ActivityTypeID":2}] +$4 +PXAT +$13 +1644690304714 +*5 +$3 +SET +$26 +examRepository::GetExam::1 +$517 +{"exam_id":1,"exam_type":"PRE","instruction":"ข้อสอบมีทั้งสิ้น 24 ข้อ เพื่อทดสอบความเข้าใจทุกเนื้อหาในการเรียน หลังจากทำข้อสอบเสร็จจะได้รับคำแนะนำว่าควรเรียนกลุ่มเนื้อหาใด","created_timestamp":"2022-02-12T16:56:43+07:00","content_group_id":0,"content_group_name":"","badge_id":0} +$4 +PXAT +$13 +1644690304835 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::99 +$699 +[{"multiple_choice_id":154,"content":"Normalization คือการทำให้ฐานข้อมูลเก็บข้อมูลได้มากขึ้น","is_correct":false},{"multiple_choice_id":155,"content":"ฐานข้อมูลที่ดีควรจะเป็นแบบ First Normal Form\r\n","is_correct":false},{"multiple_choice_id":156,"content":"First Normal Form ไม่เป็น Second Normal Form","is_correct":true},{"multiple_choice_id":157,"content":"Third Normal Form ไม่เป็น First Normal Form\r\n","is_correct":false},{"multiple_choice_id":158,"content":"ไม่มีข้อใดถูกต้อง","is_correct":false}] +$4 +PXAT +$13 +1644690304929 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::77 +$642 +[{"multiple_choice_id":20,"content":"รูปแบบไฟล์แตกต่างกัน","is_correct":true},{"multiple_choice_id":21,"content":"ค่าใช้จ่ายสูง","is_correct":false},{"multiple_choice_id":22,"content":"ต้องใช้ผู้เชี่ยวชาญในการจัดการ","is_correct":false},{"multiple_choice_id":23,"content":"ข้อมูลมีความเเม่นยำสูง\r\n","is_correct":false},{"multiple_choice_id":24,"content":"คนไม่เกี่ยวข้องสามารถเข้าถึงได้","is_correct":false}] +$4 +PXAT +$13 +1644690304931 +*5 +$3 +SET +$35 +learningRepository::GetActivity::99 +$149 +{"activity_id":99,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถูกต้อง"} +$4 +PXAT +$13 +1644690305048 +*5 +$3 +SET +$35 +learningRepository::GetActivity::80 +$210 +{"activity_id":80,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถูกต้องเกี่ยวกับการออกแบบ Entity"} +$4 +PXAT +$13 +1644690305048 +*5 +$3 +SET +$35 +learningRepository::GetActivity::97 +$386 +{"activity_id":97,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ในการเก็บข้อมูลนักเรียน ประกอบด้วย รหัสประจำตัวนักเรียน ชื่อจริง เเละนามสกุล ข้อใดเป็น Functional\r\nDependency"} +$4 +PXAT +$13 +1644690305051 +*5 +$3 +SET +$42 +learningRepository::getMultipleChoice::100 +$329 +[{"multiple_choice_id":30,"content":"Tuple","is_correct":false},{"multiple_choice_id":31,"content":"Attribute","is_correct":false},{"multiple_choice_id":32,"content":"Degree","is_correct":false},{"multiple_choice_id":33,"content":"Cardinality","is_correct":false},{"multiple_choice_id":34,"content":"Template","is_correct":true}] +$4 +PXAT +$13 +1644690305053 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::83 +$844 +[{"multiple_choice_id":74,"content":"พิจารณาโดยคำนึงถึงความแตกต่างระหว่าง Relation ","is_correct":false},{"multiple_choice_id":75,"content":"พิจารณาจากความสัมพันธ์ระหว่าง Relation ว่าแต่ละ Relation มีความเชื่อมโยงกันอย่างไร","is_correct":true},{"multiple_choice_id":76,"content":"พิจารณาว่าในแต่ละ Relation ต้องมีข้อมูลจากอีก Relation เท่าไร อย่างไร\r\n","is_correct":false},{"multiple_choice_id":77,"content":"ิผิดทุกข้อ\r\n","is_correct":false},{"multiple_choice_id":78,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690305054 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::80 +$917 +[{"multiple_choice_id":59,"content":"Entity เป็นข้อมูลที่เล็กที่สุดใน Relational Database","is_correct":false},{"multiple_choice_id":60,"content":"Entity คือกลุ่มของข้อมูลที่มีคุณสมบัติเหมือนกัน\r\n","is_correct":true},{"multiple_choice_id":61,"content":"ข้อมูลที่อยู่ใน Entity สามารถเป็นข้อมูลใดก็ได้ เพียงเเค่ชื่อเหมือนกับ Entity","is_correct":false},{"multiple_choice_id":62,"content":"การออกแบบ Entity ต้องคำนึงว่ามีสิ่งไหนบ้างที่สามารถจับต้องได","is_correct":false},{"multiple_choice_id":63,"content":"ถูกต้องข้อ\r\n","is_correct":false}] +$4 +PXAT +$13 +1644690305055 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::98 +$728 +[{"multiple_choice_id":149,"content":"{รหัสบุคลากร} -\u003e {ชื่อจริง}","is_correct":true},{"multiple_choice_id":150,"content":"{เบอร์โทร, รหัสบุคลากร} -\u003e {ชื่อจริง}","is_correct":false},{"multiple_choice_id":151,"content":"{ชื่อจริง, นามสกุล} -\u003e {เบอร์โทร}\r\n","is_correct":false},{"multiple_choice_id":152,"content":"{ชื่อจริง, รหัสบุคลากร, เบอร์โทร} -\u003e {ชื่อจริง, เบอร์โทร}\r\n","is_correct":false},{"multiple_choice_id":153,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690305058 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::84 +$384 +[{"multiple_choice_id":79,"content":"Composite Attribute","is_correct":false},{"multiple_choice_id":80,"content":"Simple Attribute","is_correct":false},{"multiple_choice_id":81,"content":"Derived Attribute","is_correct":false},{"multiple_choice_id":82,"content":"Group Attribute","is_correct":true},{"multiple_choice_id":83,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690305059 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::89 +$520 +[{"multiple_choice_id":104,"content":"Entity -\u003e Relationship -\u003e Attribute","is_correct":false},{"multiple_choice_id":105,"content":"Attribute -\u003e Relationship -\u003e Entity","is_correct":false},{"multiple_choice_id":106,"content":"Attribute -\u003e Entity -\u003e Relationship","is_correct":false},{"multiple_choice_id":107,"content":"Relationship -\u003e Entity -\u003e Attribute","is_correct":false},{"multiple_choice_id":108,"content":"Entity -\u003e Attribute -\u003e Relationship","is_correct":true}] +$4 +PXAT +$13 +1644690305060 +*5 +$3 +SET +$35 +learningRepository::GetActivity::82 +$185 +{"activity_id":82,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่Relationship ใน Relational Database"} +$4 +PXAT +$13 +1644690305062 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::81 +$466 +[{"multiple_choice_id":64,"content":"ชื่อครู\r\n","is_correct":false},{"multiple_choice_id":65,"content":"นามสกุลครู\r\n","is_correct":false},{"multiple_choice_id":66,"content":"เบอร์โทรนักเรียน","is_correct":false},{"multiple_choice_id":67,"content":"นักเรียน","is_correct":true},{"multiple_choice_id":68,"content":"วันเกิดนักเรียน","is_correct":false}] +$4 +PXAT +$13 +1644690305062 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::87 +$864 +[{"multiple_choice_id":94,"content":"เพื่อสื่อสารกับผู้ร่วมงานเกี่ยวกับการออกแบบฐานข้อมูล","is_correct":false},{"multiple_choice_id":95,"content":"เพื่อให้ลูกค้าเห็นถึงความเป็นมืออาชีพในการทำงาน\r\n","is_correct":true},{"multiple_choice_id":96,"content":"เพื่อเป็นบันทึกเอกสารในรายละเอียดการทำงาน","is_correct":false},{"multiple_choice_id":97,"content":"เพื่อทำให้เห็นภาพรวมของการออกแบบฐานข้อมูล","is_correct":false},{"multiple_choice_id":98,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690305062 +*5 +$3 +SET +$35 +learningRepository::GetActivity::83 +$233 +{"activity_id":83,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ในการพิจารณา Relationship ของ Relation ข้อใดกล่าวถูกต้อง"} +$4 +PXAT +$13 +1644690305063 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::79 +$868 +[{"multiple_choice_id":54,"content":"สามารถมี Relation ที่เหมือนกันได้หลาย ๆ อัน เพื่อเก็บข้อมูลที่เกิดขึ้นซ้ำ ๆ","is_correct":false},{"multiple_choice_id":55,"content":"ใน Relation สามารถมี Attribute ที่เหมือนกันได้หลาย ๆ อัน เพื่อเก็บข้อมูลเดียวกันหลาย ๆ ข้อมูล","is_correct":false},{"multiple_choice_id":56,"content":"ข้อมูลใน Attribute ต้องอยู่ใน Domain เดียวกัน","is_correct":true},{"multiple_choice_id":57,"content":"ิผิดทุกข้อ","is_correct":false},{"multiple_choice_id":58,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690305064 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::96 +$388 +[{"multiple_choice_id":139,"content":"Insert Anomaly","is_correct":false},{"multiple_choice_id":140,"content":"Modification Anomaly","is_correct":false},{"multiple_choice_id":141,"content":"Delete Anomaly","is_correct":false},{"multiple_choice_id":142,"content":"Manipulate Anomaly","is_correct":true},{"multiple_choice_id":143,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690305065 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::86 +$682 +[{"multiple_choice_id":89,"content":"สามารถใช้บ่งบอก Tuple ใน Relation นั้น ๆ ได้","is_correct":false},{"multiple_choice_id":90,"content":"Candidate Key สามารถถูกใช้เป็น Primary Key ได้","is_correct":false},{"multiple_choice_id":91,"content":"Candidate Key ที่ไม่เป็น Primary Key จะเรียกว่า Super Key","is_correct":true},{"multiple_choice_id":92,"content":"Candidate Key สามารถประกอบไปด้วยหลาย Attribute","is_correct":false},{"multiple_choice_id":93,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690305067 +*5 +$3 +SET +$35 +learningRepository::GetActivity::77 +$248 +{"activity_id":77,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถือเป็นข้อจำกัดของการเก็บข้อมูลแบบ File-based System"} +$4 +PXAT +$13 +1644690305068 +*5 +$3 +SET +$35 +learningRepository::GetActivity::79 +$196 +{"activity_id":79,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถูกต้องเกี่ยวกับ Relational Database"} +$4 +PXAT +$13 +1644690305069 +*5 +$3 +SET +$35 +learningRepository::GetActivity::81 +$352 +{"activity_id":81,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ในการออกแบบฐานข้อมูลโรงเรียน ซึ่งประกอบไปด้วย นักเรียน เเละครู จงเลือกข้อที่เป็น Entity"} +$4 +PXAT +$13 +1644690305070 +*5 +$3 +SET +$35 +learningRepository::GetActivity::86 +$214 +{"activity_id":86,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ข้อใดกล่าวไม่ถูกต้องเกี่ยวกับ Candidate Key"} +$4 +PXAT +$13 +1644690305070 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::82 +$417 +[{"multiple_choice_id":69,"content":"One to One Relationship\r\n","is_correct":false},{"multiple_choice_id":70,"content":"One to Many Relationship","is_correct":false},{"multiple_choice_id":71,"content":"Many to Many Relationship","is_correct":false},{"multiple_choice_id":72,"content":"None to One Relationship","is_correct":true},{"multiple_choice_id":73,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690305070 +*5 +$3 +SET +$35 +learningRepository::GetActivity::84 +$197 +{"activity_id":84,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่ประเภทของ Database Attributes\r\n"} +$4 +PXAT +$13 +1644690305072 +*5 +$3 +SET +$35 +learningRepository::GetActivity::90 +$271 +{"activity_id":90,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นลำดับขั้นตอนของการออกแบบแบบ Bottom-Up Approach ที่ถูกต้อง"} +$4 +PXAT +$13 +1644690305072 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::97 +$650 +[{"multiple_choice_id":144,"content":"{รหัสนักเรียน} -\u003e {ชื่อจริง, นามสกุล}","is_correct":true},{"multiple_choice_id":145,"content":"{ชื่อจริง} -\u003e {รหัสนักเรียน}","is_correct":false},{"multiple_choice_id":146,"content":"{ชื่อจริง} -\u003e {นามสกุล}","is_correct":false},{"multiple_choice_id":147,"content":"{นามสกุล} -\u003e {ชื่อจริง}","is_correct":false},{"multiple_choice_id":148,"content":"{นามสกุล} -\u003e {รหัสนักเรียน}","is_correct":false}] +$4 +PXAT +$13 +1644690305073 +*5 +$3 +SET +$36 +learningRepository::GetActivity::100 +$176 +{"activity_id":100,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่อยู่ใน Relational Database"} +$4 +PXAT +$13 +1644690305073 +*5 +$3 +SET +$35 +learningRepository::GetActivity::89 +$272 +{"activity_id":89,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นลำดับขั้นตอนของการออกแบบแบบ Inside-Out Approach ที่ถูกต้อง"} +$4 +PXAT +$13 +1644690305075 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::94 +$753 +[{"multiple_choice_id":129,"content":"มี Relation ที่เหมือนกันหลายอันในฐานข้อมูล","is_correct":false},{"multiple_choice_id":130,"content":"มี Attribute ที่เหมือนกันหลายอันในฐานข้อมูล","is_correct":false},{"multiple_choice_id":131,"content":"มีข้อมูลที่เหมือนกันในหลาย ๆ Tuple","is_correct":true},{"multiple_choice_id":132,"content":"มี Relation ที่มีAttribute มากจนเกินไป\r\n","is_correct":false},{"multiple_choice_id":133,"content":"ทุกเหตุการณ์ทำให้เกิด Data Redundancy","is_correct":false}] +$4 +PXAT +$13 +1644690305076 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::92 +$578 +[{"multiple_choice_id":119,"content":"ชื่อเรียกความสัมพันธ์ของระหว่าง Entity\r\n","is_correct":false},{"multiple_choice_id":120,"content":"Multiplicity","is_correct":false},{"multiple_choice_id":121,"content":"เหตุการณ์ที่ทำให้เกิดความสัมพันธ์\r\n","is_correct":true},{"multiple_choice_id":122,"content":"ชื่อของสอง Entity","is_correct":false},{"multiple_choice_id":123,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690305076 +*5 +$3 +SET +$35 +learningRepository::GetActivity::87 +$214 +{"activity_id":87,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช้จุดประสงค์ของการวาด ER-Diagram"} +$4 +PXAT +$13 +1644690305076 +*5 +$3 +SET +$35 +learningRepository::GetActivity::78 +$237 +{"activity_id":78,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ข้อใดไม่เป็นผลดีที่เกิดขึ้นจากการใช้ Database System"} +$4 +PXAT +$13 +1644690305077 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::95 +$1065 +[{"multiple_choice_id":134,"content":"การเก็บข้อมูลที่ซ้ำกันใน Relation เดียว ทำให้สามารถจัดการได้ง่ายขึ้น\r\n","is_correct":false},{"multiple_choice_id":135,"content":"การเเก้ปัญหา Data Redundancy สามารถทำได้ด้วยการเเยกข้อมูลที่ซ้ำ ไปเก็บใน Relation ใหม","is_correct":true},{"multiple_choice_id":136,"content":"การทำให้เกิด Data Redundancy ทำให้เมื่อต้องการอัพเดทข้อมูล\r\nสามารถทำได้ง่ายเนื่องจากมีข้อมูลที่เหมือนกันอยู่ในที่เดียวกัน","is_correct":false},{"multiple_choice_id":137,"content":"ถูกทุกข้อ\r\n","is_correct":false},{"multiple_choice_id":138,"content":"ผิดทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690305077 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::88 +$523 +[{"multiple_choice_id":99,"content":"Entity -\u003e Relationship -\u003e Attribute","is_correct":true},{"multiple_choice_id":100,"content":"Attribute -\u003e Relationship -\u003e Entity","is_correct":false},{"multiple_choice_id":101,"content":"Attribute -\u003e Entity -\u003e Relationship","is_correct":false},{"multiple_choice_id":102,"content":"Relationship -\u003e Entity -\u003e Attribute\r\n","is_correct":false},{"multiple_choice_id":103,"content":"Entity -\u003e Attribute -\u003e Relationship","is_correct":false}] +$4 +PXAT +$13 +1644690305078 +*5 +$3 +SET +$35 +learningRepository::GetActivity::88 +$270 +{"activity_id":88,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นลำดับขั้นตอนของการออกแบบแบบ Top-Down Approach ที่ถูกต้อง"} +$4 +PXAT +$13 +1644690305079 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::90 +$524 +[{"multiple_choice_id":109,"content":"Entity -\u003e Relationship -\u003e Attribute\r\n","is_correct":false},{"multiple_choice_id":110,"content":"Attribute -\u003e Relationship -\u003e Entity","is_correct":false},{"multiple_choice_id":111,"content":"Attribute -\u003e Entity -\u003e Relationship","is_correct":true},{"multiple_choice_id":112,"content":"Relationship -\u003e Entity -\u003e Attribute","is_correct":false},{"multiple_choice_id":113,"content":"Entity -\u003e Attribute -\u003e Relationship","is_correct":false}] +$4 +PXAT +$13 +1644690305079 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::78 +$715 +[{"multiple_choice_id":25,"content":"ข้อมูลที่ทุกคนได้รับเป็นข้อมูลที่สอดคล้องกัน\r\n","is_correct":false},{"multiple_choice_id":26,"content":"สามารถเเชร์ข้อมูลระหว่างคอมพิวเตอร์ได","is_correct":false},{"multiple_choice_id":27,"content":"ง่ายต่อการใช้งาน","is_correct":false},{"multiple_choice_id":28,"content":"บุคคลใดก็สามารถเข้าถึงได้\r\n","is_correct":true},{"multiple_choice_id":29,"content":"ทุกข้อไม่เป็นผลดี","is_correct":false}] +$4 +PXAT +$13 +1644690305079 +*5 +$3 +SET +$35 +learningRepository::GetActivity::92 +$220 +{"activity_id":92,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"้ข้อใดไม่เป็นส่วนประกอบของ Relationship Data Dictionary"} +$4 +PXAT +$13 +1644690305080 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::93 +$418 +[{"multiple_choice_id":124,"content":"Data Type","is_correct":true},{"multiple_choice_id":125,"content":"Multiplicity","is_correct":false},{"multiple_choice_id":126,"content":"Tuple","is_correct":false},{"multiple_choice_id":127,"content":"เหตุการณ์ที่ทำให้เกิด Entity","is_correct":false},{"multiple_choice_id":128,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690305081 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::85 +$347 +[{"multiple_choice_id":84,"content":"Super Key","is_correct":false},{"multiple_choice_id":85,"content":"Primary Key","is_correct":false},{"multiple_choice_id":86,"content":"Foreign Key","is_correct":false},{"multiple_choice_id":87,"content":"Candidate Key","is_correct":false},{"multiple_choice_id":88,"content":"President Key","is_correct":true}] +$4 +PXAT +$13 +1644690305082 +*5 +$3 +SET +$35 +learningRepository::GetActivity::96 +$186 +{"activity_id":96,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่ประเภทของ Data Anomaly"} +$4 +PXAT +$13 +1644690305083 +*5 +$3 +SET +$35 +learningRepository::GetActivity::93 +$205 +{"activity_id":93,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นส่วนประกอบของ Attribute Data Dictionary"} +$4 +PXAT +$13 +1644690305084 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::91 +$528 +[{"multiple_choice_id":114,"content":"คำอธิบาย Entity","is_correct":false},{"multiple_choice_id":115,"content":"ความเกี่ยวข้องกับ Entity อื่น","is_correct":true},{"multiple_choice_id":116,"content":"ชื่อเรียก Entity","is_correct":false},{"multiple_choice_id":117,"content":"เหตุการณ์ที่ทำให้เกิด Entity","is_correct":false},{"multiple_choice_id":118,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690305084 +*5 +$3 +SET +$35 +learningRepository::GetActivity::98 +$380 +{"activity_id":98,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ในการเก็บข้อมูลคุณครู ประกอบด้วย รหัสบุคลากร ชื่อจริง นามสกุล และเบอร์โทร ข้อใดเป็น Full Functional\r\nDependency"} +$4 +PXAT +$13 +1644690305085 +*5 +$3 +SET +$35 +learningRepository::GetActivity::85 +$197 +{"activity_id":85,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่ประเภทของ Database Relational Key"} +$4 +PXAT +$13 +1644690305086 +*5 +$3 +SET +$35 +learningRepository::GetActivity::91 +$211 +{"activity_id":91,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่เป็นส่วนประกอบของ Entity Data Dictionary"} +$4 +PXAT +$13 +1644690305087 +*5 +$3 +SET +$35 +learningRepository::GetActivity::95 +$192 +{"activity_id":95,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ข้อใดถูกต้องเกี่ยวกับ Data Redundancy"} +$4 +PXAT +$13 +1644690305088 +*5 +$3 +SET +$35 +learningRepository::GetActivity::94 +$198 +{"activity_id":94,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"เหตุการณ์ใดที่ทำให้เกิด Data Redundancy"} +$4 +PXAT +$13 +1644690305098 +*2 +$3 +DEL +$36 +examRepository::GetExamActivities::1 +*2 +$3 +DEL +$26 +examRepository::GetExam::1 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::77 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::92 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::85 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::87 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::80 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::78 +*2 +$3 +DEL +$42 +learningRepository::getMultipleChoice::100 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::95 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::96 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::99 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::93 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::93 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::91 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::90 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::81 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::90 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::83 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::78 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::95 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::88 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::86 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::96 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::86 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::77 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::92 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::87 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::97 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::81 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::84 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::85 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::89 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::82 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::94 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::98 +*2 +$3 +DEL +$36 +learningRepository::GetActivity::100 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::91 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::88 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::79 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::83 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::98 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::89 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::80 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::79 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::84 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::94 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::82 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::99 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::97 +*5 +$3 +SET +$26 +examRepository::GetExam::1 +$517 +{"exam_id":1,"exam_type":"PRE","instruction":"ข้อสอบมีทั้งสิ้น 24 ข้อ เพื่อทดสอบความเข้าใจทุกเนื้อหาในการเรียน หลังจากทำข้อสอบเสร็จจะได้รับคำแนะนำว่าควรเรียนกลุ่มเนื้อหาใด","created_timestamp":"2022-02-12T16:56:43+07:00","content_group_id":0,"content_group_name":"","badge_id":0} +$4 +PXAT +$13 +1644690345927 +*5 +$3 +SET +$36 +examRepository::GetExamActivities::1 +$890 +[{"ActivityID":77,"ActivityTypeID":2},{"ActivityID":78,"ActivityTypeID":2},{"ActivityID":79,"ActivityTypeID":2},{"ActivityID":100,"ActivityTypeID":2},{"ActivityID":80,"ActivityTypeID":2},{"ActivityID":81,"ActivityTypeID":2},{"ActivityID":82,"ActivityTypeID":2},{"ActivityID":83,"ActivityTypeID":2},{"ActivityID":84,"ActivityTypeID":2},{"ActivityID":85,"ActivityTypeID":2},{"ActivityID":86,"ActivityTypeID":2},{"ActivityID":87,"ActivityTypeID":2},{"ActivityID":88,"ActivityTypeID":2},{"ActivityID":89,"ActivityTypeID":2},{"ActivityID":90,"ActivityTypeID":2},{"ActivityID":91,"ActivityTypeID":2},{"ActivityID":92,"ActivityTypeID":2},{"ActivityID":93,"ActivityTypeID":2},{"ActivityID":94,"ActivityTypeID":2},{"ActivityID":95,"ActivityTypeID":2},{"ActivityID":96,"ActivityTypeID":2},{"ActivityID":97,"ActivityTypeID":2},{"ActivityID":98,"ActivityTypeID":2},{"ActivityID":99,"ActivityTypeID":2}] +$4 +PXAT +$13 +1644690346052 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::99 +$699 +[{"multiple_choice_id":154,"content":"Normalization คือการทำให้ฐานข้อมูลเก็บข้อมูลได้มากขึ้น","is_correct":false},{"multiple_choice_id":155,"content":"ฐานข้อมูลที่ดีควรจะเป็นแบบ First Normal Form\r\n","is_correct":false},{"multiple_choice_id":156,"content":"First Normal Form ไม่เป็น Second Normal Form","is_correct":true},{"multiple_choice_id":157,"content":"Third Normal Form ไม่เป็น First Normal Form\r\n","is_correct":false},{"multiple_choice_id":158,"content":"ไม่มีข้อใดถูกต้อง","is_correct":false}] +$4 +PXAT +$13 +1644690346124 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::77 +$642 +[{"multiple_choice_id":20,"content":"รูปแบบไฟล์แตกต่างกัน","is_correct":true},{"multiple_choice_id":21,"content":"ค่าใช้จ่ายสูง","is_correct":false},{"multiple_choice_id":22,"content":"ต้องใช้ผู้เชี่ยวชาญในการจัดการ","is_correct":false},{"multiple_choice_id":23,"content":"ข้อมูลมีความเเม่นยำสูง\r\n","is_correct":false},{"multiple_choice_id":24,"content":"คนไม่เกี่ยวข้องสามารถเข้าถึงได้","is_correct":false}] +$4 +PXAT +$13 +1644690346128 +*5 +$3 +SET +$35 +learningRepository::GetActivity::85 +$197 +{"activity_id":85,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่ประเภทของ Database Relational Key"} +$4 +PXAT +$13 +1644690346235 +*5 +$3 +SET +$42 +learningRepository::getMultipleChoice::100 +$329 +[{"multiple_choice_id":30,"content":"Tuple","is_correct":false},{"multiple_choice_id":31,"content":"Attribute","is_correct":false},{"multiple_choice_id":32,"content":"Degree","is_correct":false},{"multiple_choice_id":33,"content":"Cardinality","is_correct":false},{"multiple_choice_id":34,"content":"Template","is_correct":true}] +$4 +PXAT +$13 +1644690346237 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::89 +$520 +[{"multiple_choice_id":104,"content":"Entity -\u003e Relationship -\u003e Attribute","is_correct":false},{"multiple_choice_id":105,"content":"Attribute -\u003e Relationship -\u003e Entity","is_correct":false},{"multiple_choice_id":106,"content":"Attribute -\u003e Entity -\u003e Relationship","is_correct":false},{"multiple_choice_id":107,"content":"Relationship -\u003e Entity -\u003e Attribute","is_correct":false},{"multiple_choice_id":108,"content":"Entity -\u003e Attribute -\u003e Relationship","is_correct":true}] +$4 +PXAT +$13 +1644690346242 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::93 +$418 +[{"multiple_choice_id":124,"content":"Data Type","is_correct":true},{"multiple_choice_id":125,"content":"Multiplicity","is_correct":false},{"multiple_choice_id":126,"content":"Tuple","is_correct":false},{"multiple_choice_id":127,"content":"เหตุการณ์ที่ทำให้เกิด Entity","is_correct":false},{"multiple_choice_id":128,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690346242 +*5 +$3 +SET +$35 +learningRepository::GetActivity::97 +$386 +{"activity_id":97,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ในการเก็บข้อมูลนักเรียน ประกอบด้วย รหัสประจำตัวนักเรียน ชื่อจริง เเละนามสกุล ข้อใดเป็น Functional\r\nDependency"} +$4 +PXAT +$13 +1644690346243 +*5 +$3 +SET +$35 +learningRepository::GetActivity::95 +$192 +{"activity_id":95,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ข้อใดถูกต้องเกี่ยวกับ Data Redundancy"} +$4 +PXAT +$13 +1644690346244 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::88 +$523 +[{"multiple_choice_id":99,"content":"Entity -\u003e Relationship -\u003e Attribute","is_correct":true},{"multiple_choice_id":100,"content":"Attribute -\u003e Relationship -\u003e Entity","is_correct":false},{"multiple_choice_id":101,"content":"Attribute -\u003e Entity -\u003e Relationship","is_correct":false},{"multiple_choice_id":102,"content":"Relationship -\u003e Entity -\u003e Attribute\r\n","is_correct":false},{"multiple_choice_id":103,"content":"Entity -\u003e Attribute -\u003e Relationship","is_correct":false}] +$4 +PXAT +$13 +1644690346246 +*5 +$3 +SET +$35 +learningRepository::GetActivity::86 +$214 +{"activity_id":86,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ข้อใดกล่าวไม่ถูกต้องเกี่ยวกับ Candidate Key"} +$4 +PXAT +$13 +1644690346247 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::87 +$864 +[{"multiple_choice_id":94,"content":"เพื่อสื่อสารกับผู้ร่วมงานเกี่ยวกับการออกแบบฐานข้อมูล","is_correct":false},{"multiple_choice_id":95,"content":"เพื่อให้ลูกค้าเห็นถึงความเป็นมืออาชีพในการทำงาน\r\n","is_correct":true},{"multiple_choice_id":96,"content":"เพื่อเป็นบันทึกเอกสารในรายละเอียดการทำงาน","is_correct":false},{"multiple_choice_id":97,"content":"เพื่อทำให้เห็นภาพรวมของการออกแบบฐานข้อมูล","is_correct":false},{"multiple_choice_id":98,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690346247 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::79 +$868 +[{"multiple_choice_id":54,"content":"สามารถมี Relation ที่เหมือนกันได้หลาย ๆ อัน เพื่อเก็บข้อมูลที่เกิดขึ้นซ้ำ ๆ","is_correct":false},{"multiple_choice_id":55,"content":"ใน Relation สามารถมี Attribute ที่เหมือนกันได้หลาย ๆ อัน เพื่อเก็บข้อมูลเดียวกันหลาย ๆ ข้อมูล","is_correct":false},{"multiple_choice_id":56,"content":"ข้อมูลใน Attribute ต้องอยู่ใน Domain เดียวกัน","is_correct":true},{"multiple_choice_id":57,"content":"ิผิดทุกข้อ","is_correct":false},{"multiple_choice_id":58,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690346248 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::91 +$528 +[{"multiple_choice_id":114,"content":"คำอธิบาย Entity","is_correct":false},{"multiple_choice_id":115,"content":"ความเกี่ยวข้องกับ Entity อื่น","is_correct":true},{"multiple_choice_id":116,"content":"ชื่อเรียก Entity","is_correct":false},{"multiple_choice_id":117,"content":"เหตุการณ์ที่ทำให้เกิด Entity","is_correct":false},{"multiple_choice_id":118,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690346249 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::81 +$466 +[{"multiple_choice_id":64,"content":"ชื่อครู\r\n","is_correct":false},{"multiple_choice_id":65,"content":"นามสกุลครู\r\n","is_correct":false},{"multiple_choice_id":66,"content":"เบอร์โทรนักเรียน","is_correct":false},{"multiple_choice_id":67,"content":"นักเรียน","is_correct":true},{"multiple_choice_id":68,"content":"วันเกิดนักเรียน","is_correct":false}] +$4 +PXAT +$13 +1644690346249 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::95 +$1065 +[{"multiple_choice_id":134,"content":"การเก็บข้อมูลที่ซ้ำกันใน Relation เดียว ทำให้สามารถจัดการได้ง่ายขึ้น\r\n","is_correct":false},{"multiple_choice_id":135,"content":"การเเก้ปัญหา Data Redundancy สามารถทำได้ด้วยการเเยกข้อมูลที่ซ้ำ ไปเก็บใน Relation ใหม","is_correct":true},{"multiple_choice_id":136,"content":"การทำให้เกิด Data Redundancy ทำให้เมื่อต้องการอัพเดทข้อมูล\r\nสามารถทำได้ง่ายเนื่องจากมีข้อมูลที่เหมือนกันอยู่ในที่เดียวกัน","is_correct":false},{"multiple_choice_id":137,"content":"ถูกทุกข้อ\r\n","is_correct":false},{"multiple_choice_id":138,"content":"ผิดทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690346250 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::96 +$388 +[{"multiple_choice_id":139,"content":"Insert Anomaly","is_correct":false},{"multiple_choice_id":140,"content":"Modification Anomaly","is_correct":false},{"multiple_choice_id":141,"content":"Delete Anomaly","is_correct":false},{"multiple_choice_id":142,"content":"Manipulate Anomaly","is_correct":true},{"multiple_choice_id":143,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690346250 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::82 +$417 +[{"multiple_choice_id":69,"content":"One to One Relationship\r\n","is_correct":false},{"multiple_choice_id":70,"content":"One to Many Relationship","is_correct":false},{"multiple_choice_id":71,"content":"Many to Many Relationship","is_correct":false},{"multiple_choice_id":72,"content":"None to One Relationship","is_correct":true},{"multiple_choice_id":73,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690346251 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::90 +$524 +[{"multiple_choice_id":109,"content":"Entity -\u003e Relationship -\u003e Attribute\r\n","is_correct":false},{"multiple_choice_id":110,"content":"Attribute -\u003e Relationship -\u003e Entity","is_correct":false},{"multiple_choice_id":111,"content":"Attribute -\u003e Entity -\u003e Relationship","is_correct":true},{"multiple_choice_id":112,"content":"Relationship -\u003e Entity -\u003e Attribute","is_correct":false},{"multiple_choice_id":113,"content":"Entity -\u003e Attribute -\u003e Relationship","is_correct":false}] +$4 +PXAT +$13 +1644690346252 +*5 +$3 +SET +$35 +learningRepository::GetActivity::82 +$185 +{"activity_id":82,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่Relationship ใน Relational Database"} +$4 +PXAT +$13 +1644690346253 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::98 +$728 +[{"multiple_choice_id":149,"content":"{รหัสบุคลากร} -\u003e {ชื่อจริง}","is_correct":true},{"multiple_choice_id":150,"content":"{เบอร์โทร, รหัสบุคลากร} -\u003e {ชื่อจริง}","is_correct":false},{"multiple_choice_id":151,"content":"{ชื่อจริง, นามสกุล} -\u003e {เบอร์โทร}\r\n","is_correct":false},{"multiple_choice_id":152,"content":"{ชื่อจริง, รหัสบุคลากร, เบอร์โทร} -\u003e {ชื่อจริง, เบอร์โทร}\r\n","is_correct":false},{"multiple_choice_id":153,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690346254 +*5 +$3 +SET +$35 +learningRepository::GetActivity::90 +$271 +{"activity_id":90,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นลำดับขั้นตอนของการออกแบบแบบ Bottom-Up Approach ที่ถูกต้อง"} +$4 +PXAT +$13 +1644690346254 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::85 +$347 +[{"multiple_choice_id":84,"content":"Super Key","is_correct":false},{"multiple_choice_id":85,"content":"Primary Key","is_correct":false},{"multiple_choice_id":86,"content":"Foreign Key","is_correct":false},{"multiple_choice_id":87,"content":"Candidate Key","is_correct":false},{"multiple_choice_id":88,"content":"President Key","is_correct":true}] +$4 +PXAT +$13 +1644690346254 +*5 +$3 +SET +$35 +learningRepository::GetActivity::93 +$205 +{"activity_id":93,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นส่วนประกอบของ Attribute Data Dictionary"} +$4 +PXAT +$13 +1644690346255 +*5 +$3 +SET +$35 +learningRepository::GetActivity::81 +$352 +{"activity_id":81,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ในการออกแบบฐานข้อมูลโรงเรียน ซึ่งประกอบไปด้วย นักเรียน เเละครู จงเลือกข้อที่เป็น Entity"} +$4 +PXAT +$13 +1644690346255 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::84 +$384 +[{"multiple_choice_id":79,"content":"Composite Attribute","is_correct":false},{"multiple_choice_id":80,"content":"Simple Attribute","is_correct":false},{"multiple_choice_id":81,"content":"Derived Attribute","is_correct":false},{"multiple_choice_id":82,"content":"Group Attribute","is_correct":true},{"multiple_choice_id":83,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690346255 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::94 +$753 +[{"multiple_choice_id":129,"content":"มี Relation ที่เหมือนกันหลายอันในฐานข้อมูล","is_correct":false},{"multiple_choice_id":130,"content":"มี Attribute ที่เหมือนกันหลายอันในฐานข้อมูล","is_correct":false},{"multiple_choice_id":131,"content":"มีข้อมูลที่เหมือนกันในหลาย ๆ Tuple","is_correct":true},{"multiple_choice_id":132,"content":"มี Relation ที่มีAttribute มากจนเกินไป\r\n","is_correct":false},{"multiple_choice_id":133,"content":"ทุกเหตุการณ์ทำให้เกิด Data Redundancy","is_correct":false}] +$4 +PXAT +$13 +1644690346256 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::80 +$917 +[{"multiple_choice_id":59,"content":"Entity เป็นข้อมูลที่เล็กที่สุดใน Relational Database","is_correct":false},{"multiple_choice_id":60,"content":"Entity คือกลุ่มของข้อมูลที่มีคุณสมบัติเหมือนกัน\r\n","is_correct":true},{"multiple_choice_id":61,"content":"ข้อมูลที่อยู่ใน Entity สามารถเป็นข้อมูลใดก็ได้ เพียงเเค่ชื่อเหมือนกับ Entity","is_correct":false},{"multiple_choice_id":62,"content":"การออกแบบ Entity ต้องคำนึงว่ามีสิ่งไหนบ้างที่สามารถจับต้องได","is_correct":false},{"multiple_choice_id":63,"content":"ถูกต้องข้อ\r\n","is_correct":false}] +$4 +PXAT +$13 +1644690346258 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::78 +$715 +[{"multiple_choice_id":25,"content":"ข้อมูลที่ทุกคนได้รับเป็นข้อมูลที่สอดคล้องกัน\r\n","is_correct":false},{"multiple_choice_id":26,"content":"สามารถเเชร์ข้อมูลระหว่างคอมพิวเตอร์ได","is_correct":false},{"multiple_choice_id":27,"content":"ง่ายต่อการใช้งาน","is_correct":false},{"multiple_choice_id":28,"content":"บุคคลใดก็สามารถเข้าถึงได้\r\n","is_correct":true},{"multiple_choice_id":29,"content":"ทุกข้อไม่เป็นผลดี","is_correct":false}] +$4 +PXAT +$13 +1644690346260 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::92 +$578 +[{"multiple_choice_id":119,"content":"ชื่อเรียกความสัมพันธ์ของระหว่าง Entity\r\n","is_correct":false},{"multiple_choice_id":120,"content":"Multiplicity","is_correct":false},{"multiple_choice_id":121,"content":"เหตุการณ์ที่ทำให้เกิดความสัมพันธ์\r\n","is_correct":true},{"multiple_choice_id":122,"content":"ชื่อของสอง Entity","is_correct":false},{"multiple_choice_id":123,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690346260 +*5 +$3 +SET +$35 +learningRepository::GetActivity::78 +$237 +{"activity_id":78,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ข้อใดไม่เป็นผลดีที่เกิดขึ้นจากการใช้ Database System"} +$4 +PXAT +$13 +1644690346260 +*5 +$3 +SET +$35 +learningRepository::GetActivity::83 +$233 +{"activity_id":83,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ในการพิจารณา Relationship ของ Relation ข้อใดกล่าวถูกต้อง"} +$4 +PXAT +$13 +1644690346261 +*5 +$3 +SET +$35 +learningRepository::GetActivity::80 +$210 +{"activity_id":80,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถูกต้องเกี่ยวกับการออกแบบ Entity"} +$4 +PXAT +$13 +1644690346262 +*5 +$3 +SET +$35 +learningRepository::GetActivity::77 +$248 +{"activity_id":77,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถือเป็นข้อจำกัดของการเก็บข้อมูลแบบ File-based System"} +$4 +PXAT +$13 +1644690346262 +*5 +$3 +SET +$35 +learningRepository::GetActivity::79 +$196 +{"activity_id":79,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถูกต้องเกี่ยวกับ Relational Database"} +$4 +PXAT +$13 +1644690346262 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::83 +$844 +[{"multiple_choice_id":74,"content":"พิจารณาโดยคำนึงถึงความแตกต่างระหว่าง Relation ","is_correct":false},{"multiple_choice_id":75,"content":"พิจารณาจากความสัมพันธ์ระหว่าง Relation ว่าแต่ละ Relation มีความเชื่อมโยงกันอย่างไร","is_correct":true},{"multiple_choice_id":76,"content":"พิจารณาว่าในแต่ละ Relation ต้องมีข้อมูลจากอีก Relation เท่าไร อย่างไร\r\n","is_correct":false},{"multiple_choice_id":77,"content":"ิผิดทุกข้อ\r\n","is_correct":false},{"multiple_choice_id":78,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690346263 +*5 +$3 +SET +$35 +learningRepository::GetActivity::96 +$186 +{"activity_id":96,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่ประเภทของ Data Anomaly"} +$4 +PXAT +$13 +1644690346264 +*5 +$3 +SET +$35 +learningRepository::GetActivity::98 +$380 +{"activity_id":98,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ในการเก็บข้อมูลคุณครู ประกอบด้วย รหัสบุคลากร ชื่อจริง นามสกุล และเบอร์โทร ข้อใดเป็น Full Functional\r\nDependency"} +$4 +PXAT +$13 +1644690346264 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::97 +$650 +[{"multiple_choice_id":144,"content":"{รหัสนักเรียน} -\u003e {ชื่อจริง, นามสกุล}","is_correct":true},{"multiple_choice_id":145,"content":"{ชื่อจริง} -\u003e {รหัสนักเรียน}","is_correct":false},{"multiple_choice_id":146,"content":"{ชื่อจริง} -\u003e {นามสกุล}","is_correct":false},{"multiple_choice_id":147,"content":"{นามสกุล} -\u003e {ชื่อจริง}","is_correct":false},{"multiple_choice_id":148,"content":"{นามสกุล} -\u003e {รหัสนักเรียน}","is_correct":false}] +$4 +PXAT +$13 +1644690346265 +*5 +$3 +SET +$35 +learningRepository::GetActivity::94 +$198 +{"activity_id":94,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"เหตุการณ์ใดที่ทำให้เกิด Data Redundancy"} +$4 +PXAT +$13 +1644690346265 +*5 +$3 +SET +$35 +learningRepository::GetActivity::87 +$214 +{"activity_id":87,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช้จุดประสงค์ของการวาด ER-Diagram"} +$4 +PXAT +$13 +1644690346266 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::86 +$682 +[{"multiple_choice_id":89,"content":"สามารถใช้บ่งบอก Tuple ใน Relation นั้น ๆ ได้","is_correct":false},{"multiple_choice_id":90,"content":"Candidate Key สามารถถูกใช้เป็น Primary Key ได้","is_correct":false},{"multiple_choice_id":91,"content":"Candidate Key ที่ไม่เป็น Primary Key จะเรียกว่า Super Key","is_correct":true},{"multiple_choice_id":92,"content":"Candidate Key สามารถประกอบไปด้วยหลาย Attribute","is_correct":false},{"multiple_choice_id":93,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644690346266 +*5 +$3 +SET +$35 +learningRepository::GetActivity::84 +$197 +{"activity_id":84,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่ประเภทของ Database Attributes\r\n"} +$4 +PXAT +$13 +1644690346266 +*5 +$3 +SET +$35 +learningRepository::GetActivity::99 +$149 +{"activity_id":99,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถูกต้อง"} +$4 +PXAT +$13 +1644690346267 +*5 +$3 +SET +$35 +learningRepository::GetActivity::91 +$211 +{"activity_id":91,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่เป็นส่วนประกอบของ Entity Data Dictionary"} +$4 +PXAT +$13 +1644690346269 +*5 +$3 +SET +$35 +learningRepository::GetActivity::88 +$270 +{"activity_id":88,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นลำดับขั้นตอนของการออกแบบแบบ Top-Down Approach ที่ถูกต้อง"} +$4 +PXAT +$13 +1644690346271 +*5 +$3 +SET +$36 +learningRepository::GetActivity::100 +$176 +{"activity_id":100,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่อยู่ใน Relational Database"} +$4 +PXAT +$13 +1644690346271 +*5 +$3 +SET +$35 +learningRepository::GetActivity::92 +$220 +{"activity_id":92,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"้ข้อใดไม่เป็นส่วนประกอบของ Relationship Data Dictionary"} +$4 +PXAT +$13 +1644690346271 +*5 +$3 +SET +$35 +learningRepository::GetActivity::89 +$272 +{"activity_id":89,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นลำดับขั้นตอนของการออกแบบแบบ Inside-Out Approach ที่ถูกต้อง"} +$4 +PXAT +$13 +1644690346282 +*2 +$3 +DEL +$26 +examRepository::GetExam::1 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::77 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::85 +*2 +$3 +DEL +$42 +learningRepository::getMultipleChoice::100 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::99 +*2 +$3 +DEL +$36 +examRepository::GetExamActivities::1 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::82 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::94 +*2 +$3 +DEL +$36 +learningRepository::GetActivity::100 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::98 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::88 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::91 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::79 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::83 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::98 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::89 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::84 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::79 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::80 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::94 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::99 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::97 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::82 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::92 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::87 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::80 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::78 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::95 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::96 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::93 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::93 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::91 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::90 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::81 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::90 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::83 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::95 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::78 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::88 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::86 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::96 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::86 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::77 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::92 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::87 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::97 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::81 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::84 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::89 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::85 +*5 +$3 +SET +$36 +examRepository::GetExamActivities::1 +$890 +[{"ActivityID":77,"ActivityTypeID":2},{"ActivityID":78,"ActivityTypeID":2},{"ActivityID":79,"ActivityTypeID":2},{"ActivityID":100,"ActivityTypeID":2},{"ActivityID":80,"ActivityTypeID":2},{"ActivityID":81,"ActivityTypeID":2},{"ActivityID":82,"ActivityTypeID":2},{"ActivityID":83,"ActivityTypeID":2},{"ActivityID":84,"ActivityTypeID":2},{"ActivityID":85,"ActivityTypeID":2},{"ActivityID":86,"ActivityTypeID":2},{"ActivityID":87,"ActivityTypeID":2},{"ActivityID":88,"ActivityTypeID":2},{"ActivityID":89,"ActivityTypeID":2},{"ActivityID":90,"ActivityTypeID":2},{"ActivityID":91,"ActivityTypeID":2},{"ActivityID":92,"ActivityTypeID":2},{"ActivityID":93,"ActivityTypeID":2},{"ActivityID":94,"ActivityTypeID":2},{"ActivityID":95,"ActivityTypeID":2},{"ActivityID":96,"ActivityTypeID":2},{"ActivityID":97,"ActivityTypeID":2},{"ActivityID":98,"ActivityTypeID":2},{"ActivityID":99,"ActivityTypeID":2}] +$4 +PXAT +$13 +1644691624752 +*5 +$3 +SET +$26 +examRepository::GetExam::1 +$517 +{"exam_id":1,"exam_type":"PRE","instruction":"ข้อสอบมีทั้งสิ้น 24 ข้อ เพื่อทดสอบความเข้าใจทุกเนื้อหาในการเรียน หลังจากทำข้อสอบเสร็จจะได้รับคำแนะนำว่าควรเรียนกลุ่มเนื้อหาใด","created_timestamp":"2022-02-12T16:56:43+07:00","content_group_id":0,"content_group_name":"","badge_id":0} +$4 +PXAT +$13 +1644691624756 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::77 +$642 +[{"multiple_choice_id":20,"content":"รูปแบบไฟล์แตกต่างกัน","is_correct":true},{"multiple_choice_id":21,"content":"ค่าใช้จ่ายสูง","is_correct":false},{"multiple_choice_id":22,"content":"ต้องใช้ผู้เชี่ยวชาญในการจัดการ","is_correct":false},{"multiple_choice_id":23,"content":"ข้อมูลมีความเเม่นยำสูง\r\n","is_correct":false},{"multiple_choice_id":24,"content":"คนไม่เกี่ยวข้องสามารถเข้าถึงได้","is_correct":false}] +$4 +PXAT +$13 +1644691624871 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::99 +$699 +[{"multiple_choice_id":154,"content":"Normalization คือการทำให้ฐานข้อมูลเก็บข้อมูลได้มากขึ้น","is_correct":false},{"multiple_choice_id":155,"content":"ฐานข้อมูลที่ดีควรจะเป็นแบบ First Normal Form\r\n","is_correct":false},{"multiple_choice_id":156,"content":"First Normal Form ไม่เป็น Second Normal Form","is_correct":true},{"multiple_choice_id":157,"content":"Third Normal Form ไม่เป็น First Normal Form\r\n","is_correct":false},{"multiple_choice_id":158,"content":"ไม่มีข้อใดถูกต้อง","is_correct":false}] +$4 +PXAT +$13 +1644691624875 +*5 +$3 +SET +$35 +learningRepository::GetActivity::89 +$272 +{"activity_id":89,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นลำดับขั้นตอนของการออกแบบแบบ Inside-Out Approach ที่ถูกต้อง"} +$4 +PXAT +$13 +1644691624970 +*5 +$3 +SET +$36 +learningRepository::GetActivity::100 +$176 +{"activity_id":100,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่อยู่ใน Relational Database"} +$4 +PXAT +$13 +1644691624974 +*5 +$3 +SET +$35 +learningRepository::GetActivity::90 +$271 +{"activity_id":90,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นลำดับขั้นตอนของการออกแบบแบบ Bottom-Up Approach ที่ถูกต้อง"} +$4 +PXAT +$13 +1644691624983 +*5 +$3 +SET +$35 +learningRepository::GetActivity::99 +$149 +{"activity_id":99,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถูกต้อง"} +$4 +PXAT +$13 +1644691624985 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::81 +$466 +[{"multiple_choice_id":64,"content":"ชื่อครู\r\n","is_correct":false},{"multiple_choice_id":65,"content":"นามสกุลครู\r\n","is_correct":false},{"multiple_choice_id":66,"content":"เบอร์โทรนักเรียน","is_correct":false},{"multiple_choice_id":67,"content":"นักเรียน","is_correct":true},{"multiple_choice_id":68,"content":"วันเกิดนักเรียน","is_correct":false}] +$4 +PXAT +$13 +1644691624986 +*5 +$3 +SET +$35 +learningRepository::GetActivity::77 +$248 +{"activity_id":77,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถือเป็นข้อจำกัดของการเก็บข้อมูลแบบ File-based System"} +$4 +PXAT +$13 +1644691624988 +*5 +$3 +SET +$35 +learningRepository::GetActivity::94 +$198 +{"activity_id":94,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"เหตุการณ์ใดที่ทำให้เกิด Data Redundancy"} +$4 +PXAT +$13 +1644691624988 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::92 +$578 +[{"multiple_choice_id":119,"content":"ชื่อเรียกความสัมพันธ์ของระหว่าง Entity\r\n","is_correct":false},{"multiple_choice_id":120,"content":"Multiplicity","is_correct":false},{"multiple_choice_id":121,"content":"เหตุการณ์ที่ทำให้เกิดความสัมพันธ์\r\n","is_correct":true},{"multiple_choice_id":122,"content":"ชื่อของสอง Entity","is_correct":false},{"multiple_choice_id":123,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691624989 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::87 +$864 +[{"multiple_choice_id":94,"content":"เพื่อสื่อสารกับผู้ร่วมงานเกี่ยวกับการออกแบบฐานข้อมูล","is_correct":false},{"multiple_choice_id":95,"content":"เพื่อให้ลูกค้าเห็นถึงความเป็นมืออาชีพในการทำงาน\r\n","is_correct":true},{"multiple_choice_id":96,"content":"เพื่อเป็นบันทึกเอกสารในรายละเอียดการทำงาน","is_correct":false},{"multiple_choice_id":97,"content":"เพื่อทำให้เห็นภาพรวมของการออกแบบฐานข้อมูล","is_correct":false},{"multiple_choice_id":98,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691624989 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::79 +$868 +[{"multiple_choice_id":54,"content":"สามารถมี Relation ที่เหมือนกันได้หลาย ๆ อัน เพื่อเก็บข้อมูลที่เกิดขึ้นซ้ำ ๆ","is_correct":false},{"multiple_choice_id":55,"content":"ใน Relation สามารถมี Attribute ที่เหมือนกันได้หลาย ๆ อัน เพื่อเก็บข้อมูลเดียวกันหลาย ๆ ข้อมูล","is_correct":false},{"multiple_choice_id":56,"content":"ข้อมูลใน Attribute ต้องอยู่ใน Domain เดียวกัน","is_correct":true},{"multiple_choice_id":57,"content":"ิผิดทุกข้อ","is_correct":false},{"multiple_choice_id":58,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691624989 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::93 +$418 +[{"multiple_choice_id":124,"content":"Data Type","is_correct":true},{"multiple_choice_id":125,"content":"Multiplicity","is_correct":false},{"multiple_choice_id":126,"content":"Tuple","is_correct":false},{"multiple_choice_id":127,"content":"เหตุการณ์ที่ทำให้เกิด Entity","is_correct":false},{"multiple_choice_id":128,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691624992 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::89 +$520 +[{"multiple_choice_id":104,"content":"Entity -\u003e Relationship -\u003e Attribute","is_correct":false},{"multiple_choice_id":105,"content":"Attribute -\u003e Relationship -\u003e Entity","is_correct":false},{"multiple_choice_id":106,"content":"Attribute -\u003e Entity -\u003e Relationship","is_correct":false},{"multiple_choice_id":107,"content":"Relationship -\u003e Entity -\u003e Attribute","is_correct":false},{"multiple_choice_id":108,"content":"Entity -\u003e Attribute -\u003e Relationship","is_correct":true}] +$4 +PXAT +$13 +1644691624992 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::91 +$528 +[{"multiple_choice_id":114,"content":"คำอธิบาย Entity","is_correct":false},{"multiple_choice_id":115,"content":"ความเกี่ยวข้องกับ Entity อื่น","is_correct":true},{"multiple_choice_id":116,"content":"ชื่อเรียก Entity","is_correct":false},{"multiple_choice_id":117,"content":"เหตุการณ์ที่ทำให้เกิด Entity","is_correct":false},{"multiple_choice_id":118,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691624992 +*5 +$3 +SET +$35 +learningRepository::GetActivity::88 +$270 +{"activity_id":88,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นลำดับขั้นตอนของการออกแบบแบบ Top-Down Approach ที่ถูกต้อง"} +$4 +PXAT +$13 +1644691624993 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::94 +$753 +[{"multiple_choice_id":129,"content":"มี Relation ที่เหมือนกันหลายอันในฐานข้อมูล","is_correct":false},{"multiple_choice_id":130,"content":"มี Attribute ที่เหมือนกันหลายอันในฐานข้อมูล","is_correct":false},{"multiple_choice_id":131,"content":"มีข้อมูลที่เหมือนกันในหลาย ๆ Tuple","is_correct":true},{"multiple_choice_id":132,"content":"มี Relation ที่มีAttribute มากจนเกินไป\r\n","is_correct":false},{"multiple_choice_id":133,"content":"ทุกเหตุการณ์ทำให้เกิด Data Redundancy","is_correct":false}] +$4 +PXAT +$13 +1644691624995 +*5 +$3 +SET +$35 +learningRepository::GetActivity::82 +$185 +{"activity_id":82,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่Relationship ใน Relational Database"} +$4 +PXAT +$13 +1644691624995 +*5 +$3 +SET +$35 +learningRepository::GetActivity::78 +$237 +{"activity_id":78,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ข้อใดไม่เป็นผลดีที่เกิดขึ้นจากการใช้ Database System"} +$4 +PXAT +$13 +1644691624995 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::78 +$715 +[{"multiple_choice_id":25,"content":"ข้อมูลที่ทุกคนได้รับเป็นข้อมูลที่สอดคล้องกัน\r\n","is_correct":false},{"multiple_choice_id":26,"content":"สามารถเเชร์ข้อมูลระหว่างคอมพิวเตอร์ได","is_correct":false},{"multiple_choice_id":27,"content":"ง่ายต่อการใช้งาน","is_correct":false},{"multiple_choice_id":28,"content":"บุคคลใดก็สามารถเข้าถึงได้\r\n","is_correct":true},{"multiple_choice_id":29,"content":"ทุกข้อไม่เป็นผลดี","is_correct":false}] +$4 +PXAT +$13 +1644691624995 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::88 +$523 +[{"multiple_choice_id":99,"content":"Entity -\u003e Relationship -\u003e Attribute","is_correct":true},{"multiple_choice_id":100,"content":"Attribute -\u003e Relationship -\u003e Entity","is_correct":false},{"multiple_choice_id":101,"content":"Attribute -\u003e Entity -\u003e Relationship","is_correct":false},{"multiple_choice_id":102,"content":"Relationship -\u003e Entity -\u003e Attribute\r\n","is_correct":false},{"multiple_choice_id":103,"content":"Entity -\u003e Attribute -\u003e Relationship","is_correct":false}] +$4 +PXAT +$13 +1644691624995 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::86 +$682 +[{"multiple_choice_id":89,"content":"สามารถใช้บ่งบอก Tuple ใน Relation นั้น ๆ ได้","is_correct":false},{"multiple_choice_id":90,"content":"Candidate Key สามารถถูกใช้เป็น Primary Key ได้","is_correct":false},{"multiple_choice_id":91,"content":"Candidate Key ที่ไม่เป็น Primary Key จะเรียกว่า Super Key","is_correct":true},{"multiple_choice_id":92,"content":"Candidate Key สามารถประกอบไปด้วยหลาย Attribute","is_correct":false},{"multiple_choice_id":93,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691624996 +*5 +$3 +SET +$35 +learningRepository::GetActivity::96 +$186 +{"activity_id":96,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่ประเภทของ Data Anomaly"} +$4 +PXAT +$13 +1644691624996 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::97 +$650 +[{"multiple_choice_id":144,"content":"{รหัสนักเรียน} -\u003e {ชื่อจริง, นามสกุล}","is_correct":true},{"multiple_choice_id":145,"content":"{ชื่อจริง} -\u003e {รหัสนักเรียน}","is_correct":false},{"multiple_choice_id":146,"content":"{ชื่อจริง} -\u003e {นามสกุล}","is_correct":false},{"multiple_choice_id":147,"content":"{นามสกุล} -\u003e {ชื่อจริง}","is_correct":false},{"multiple_choice_id":148,"content":"{นามสกุล} -\u003e {รหัสนักเรียน}","is_correct":false}] +$4 +PXAT +$13 +1644691624996 +*5 +$3 +SET +$35 +learningRepository::GetActivity::92 +$220 +{"activity_id":92,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"้ข้อใดไม่เป็นส่วนประกอบของ Relationship Data Dictionary"} +$4 +PXAT +$13 +1644691624996 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::90 +$524 +[{"multiple_choice_id":109,"content":"Entity -\u003e Relationship -\u003e Attribute\r\n","is_correct":false},{"multiple_choice_id":110,"content":"Attribute -\u003e Relationship -\u003e Entity","is_correct":false},{"multiple_choice_id":111,"content":"Attribute -\u003e Entity -\u003e Relationship","is_correct":true},{"multiple_choice_id":112,"content":"Relationship -\u003e Entity -\u003e Attribute","is_correct":false},{"multiple_choice_id":113,"content":"Entity -\u003e Attribute -\u003e Relationship","is_correct":false}] +$4 +PXAT +$13 +1644691624997 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::85 +$347 +[{"multiple_choice_id":84,"content":"Super Key","is_correct":false},{"multiple_choice_id":85,"content":"Primary Key","is_correct":false},{"multiple_choice_id":86,"content":"Foreign Key","is_correct":false},{"multiple_choice_id":87,"content":"Candidate Key","is_correct":false},{"multiple_choice_id":88,"content":"President Key","is_correct":true}] +$4 +PXAT +$13 +1644691624997 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::82 +$417 +[{"multiple_choice_id":69,"content":"One to One Relationship\r\n","is_correct":false},{"multiple_choice_id":70,"content":"One to Many Relationship","is_correct":false},{"multiple_choice_id":71,"content":"Many to Many Relationship","is_correct":false},{"multiple_choice_id":72,"content":"None to One Relationship","is_correct":true},{"multiple_choice_id":73,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691624997 +*5 +$3 +SET +$35 +learningRepository::GetActivity::85 +$197 +{"activity_id":85,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่ประเภทของ Database Relational Key"} +$4 +PXAT +$13 +1644691624997 +*5 +$3 +SET +$35 +learningRepository::GetActivity::91 +$211 +{"activity_id":91,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่เป็นส่วนประกอบของ Entity Data Dictionary"} +$4 +PXAT +$13 +1644691624997 +*5 +$3 +SET +$35 +learningRepository::GetActivity::95 +$192 +{"activity_id":95,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ข้อใดถูกต้องเกี่ยวกับ Data Redundancy"} +$4 +PXAT +$13 +1644691624998 +*5 +$3 +SET +$35 +learningRepository::GetActivity::93 +$205 +{"activity_id":93,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นส่วนประกอบของ Attribute Data Dictionary"} +$4 +PXAT +$13 +1644691624999 +*5 +$3 +SET +$35 +learningRepository::GetActivity::81 +$352 +{"activity_id":81,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ในการออกแบบฐานข้อมูลโรงเรียน ซึ่งประกอบไปด้วย นักเรียน เเละครู จงเลือกข้อที่เป็น Entity"} +$4 +PXAT +$13 +1644691625001 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::95 +$1065 +[{"multiple_choice_id":134,"content":"การเก็บข้อมูลที่ซ้ำกันใน Relation เดียว ทำให้สามารถจัดการได้ง่ายขึ้น\r\n","is_correct":false},{"multiple_choice_id":135,"content":"การเเก้ปัญหา Data Redundancy สามารถทำได้ด้วยการเเยกข้อมูลที่ซ้ำ ไปเก็บใน Relation ใหม","is_correct":true},{"multiple_choice_id":136,"content":"การทำให้เกิด Data Redundancy ทำให้เมื่อต้องการอัพเดทข้อมูล\r\nสามารถทำได้ง่ายเนื่องจากมีข้อมูลที่เหมือนกันอยู่ในที่เดียวกัน","is_correct":false},{"multiple_choice_id":137,"content":"ถูกทุกข้อ\r\n","is_correct":false},{"multiple_choice_id":138,"content":"ผิดทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691625001 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::84 +$384 +[{"multiple_choice_id":79,"content":"Composite Attribute","is_correct":false},{"multiple_choice_id":80,"content":"Simple Attribute","is_correct":false},{"multiple_choice_id":81,"content":"Derived Attribute","is_correct":false},{"multiple_choice_id":82,"content":"Group Attribute","is_correct":true},{"multiple_choice_id":83,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691625001 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::98 +$728 +[{"multiple_choice_id":149,"content":"{รหัสบุคลากร} -\u003e {ชื่อจริง}","is_correct":true},{"multiple_choice_id":150,"content":"{เบอร์โทร, รหัสบุคลากร} -\u003e {ชื่อจริง}","is_correct":false},{"multiple_choice_id":151,"content":"{ชื่อจริง, นามสกุล} -\u003e {เบอร์โทร}\r\n","is_correct":false},{"multiple_choice_id":152,"content":"{ชื่อจริง, รหัสบุคลากร, เบอร์โทร} -\u003e {ชื่อจริง, เบอร์โทร}\r\n","is_correct":false},{"multiple_choice_id":153,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691625001 +*5 +$3 +SET +$35 +learningRepository::GetActivity::79 +$196 +{"activity_id":79,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถูกต้องเกี่ยวกับ Relational Database"} +$4 +PXAT +$13 +1644691625002 +*5 +$3 +SET +$35 +learningRepository::GetActivity::97 +$386 +{"activity_id":97,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ในการเก็บข้อมูลนักเรียน ประกอบด้วย รหัสประจำตัวนักเรียน ชื่อจริง เเละนามสกุล ข้อใดเป็น Functional\r\nDependency"} +$4 +PXAT +$13 +1644691625002 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::83 +$844 +[{"multiple_choice_id":74,"content":"พิจารณาโดยคำนึงถึงความแตกต่างระหว่าง Relation ","is_correct":false},{"multiple_choice_id":75,"content":"พิจารณาจากความสัมพันธ์ระหว่าง Relation ว่าแต่ละ Relation มีความเชื่อมโยงกันอย่างไร","is_correct":true},{"multiple_choice_id":76,"content":"พิจารณาว่าในแต่ละ Relation ต้องมีข้อมูลจากอีก Relation เท่าไร อย่างไร\r\n","is_correct":false},{"multiple_choice_id":77,"content":"ิผิดทุกข้อ\r\n","is_correct":false},{"multiple_choice_id":78,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691625003 +*5 +$3 +SET +$35 +learningRepository::GetActivity::84 +$197 +{"activity_id":84,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่ประเภทของ Database Attributes\r\n"} +$4 +PXAT +$13 +1644691625003 +*5 +$3 +SET +$42 +learningRepository::getMultipleChoice::100 +$329 +[{"multiple_choice_id":30,"content":"Tuple","is_correct":false},{"multiple_choice_id":31,"content":"Attribute","is_correct":false},{"multiple_choice_id":32,"content":"Degree","is_correct":false},{"multiple_choice_id":33,"content":"Cardinality","is_correct":false},{"multiple_choice_id":34,"content":"Template","is_correct":true}] +$4 +PXAT +$13 +1644691625005 +*5 +$3 +SET +$35 +learningRepository::GetActivity::83 +$233 +{"activity_id":83,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ในการพิจารณา Relationship ของ Relation ข้อใดกล่าวถูกต้อง"} +$4 +PXAT +$13 +1644691625005 +*5 +$3 +SET +$35 +learningRepository::GetActivity::86 +$214 +{"activity_id":86,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ข้อใดกล่าวไม่ถูกต้องเกี่ยวกับ Candidate Key"} +$4 +PXAT +$13 +1644691625007 +*5 +$3 +SET +$35 +learningRepository::GetActivity::80 +$210 +{"activity_id":80,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถูกต้องเกี่ยวกับการออกแบบ Entity"} +$4 +PXAT +$13 +1644691625007 +*5 +$3 +SET +$35 +learningRepository::GetActivity::87 +$214 +{"activity_id":87,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช้จุดประสงค์ของการวาด ER-Diagram"} +$4 +PXAT +$13 +1644691625010 +*5 +$3 +SET +$35 +learningRepository::GetActivity::98 +$380 +{"activity_id":98,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ในการเก็บข้อมูลคุณครู ประกอบด้วย รหัสบุคลากร ชื่อจริง นามสกุล และเบอร์โทร ข้อใดเป็น Full Functional\r\nDependency"} +$4 +PXAT +$13 +1644691625012 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::96 +$388 +[{"multiple_choice_id":139,"content":"Insert Anomaly","is_correct":false},{"multiple_choice_id":140,"content":"Modification Anomaly","is_correct":false},{"multiple_choice_id":141,"content":"Delete Anomaly","is_correct":false},{"multiple_choice_id":142,"content":"Manipulate Anomaly","is_correct":true},{"multiple_choice_id":143,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691625014 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::80 +$917 +[{"multiple_choice_id":59,"content":"Entity เป็นข้อมูลที่เล็กที่สุดใน Relational Database","is_correct":false},{"multiple_choice_id":60,"content":"Entity คือกลุ่มของข้อมูลที่มีคุณสมบัติเหมือนกัน\r\n","is_correct":true},{"multiple_choice_id":61,"content":"ข้อมูลที่อยู่ใน Entity สามารถเป็นข้อมูลใดก็ได้ เพียงเเค่ชื่อเหมือนกับ Entity","is_correct":false},{"multiple_choice_id":62,"content":"การออกแบบ Entity ต้องคำนึงว่ามีสิ่งไหนบ้างที่สามารถจับต้องได","is_correct":false},{"multiple_choice_id":63,"content":"ถูกต้องข้อ\r\n","is_correct":false}] +$4 +PXAT +$13 +1644691625014 +*2 +$3 +DEL +$36 +examRepository::GetExamActivities::1 +*2 +$3 +DEL +$26 +examRepository::GetExam::1 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::98 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::89 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::80 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::84 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::79 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::94 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::77 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::99 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::82 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::97 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::92 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::87 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::85 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::80 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::78 +*2 +$3 +DEL +$42 +learningRepository::getMultipleChoice::100 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::95 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::96 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::99 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::93 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::93 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::91 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::90 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::90 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::81 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::83 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::95 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::78 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::88 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::86 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::96 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::77 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::86 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::87 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::92 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::97 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::81 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::84 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::89 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::85 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::82 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::94 +*2 +$3 +DEL +$36 +learningRepository::GetActivity::100 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::98 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::91 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::88 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::79 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::83 +*5 +$3 +SET +$36 +examRepository::GetExamActivities::1 +$890 +[{"ActivityID":77,"ActivityTypeID":2},{"ActivityID":78,"ActivityTypeID":2},{"ActivityID":79,"ActivityTypeID":2},{"ActivityID":100,"ActivityTypeID":2},{"ActivityID":80,"ActivityTypeID":2},{"ActivityID":81,"ActivityTypeID":2},{"ActivityID":82,"ActivityTypeID":2},{"ActivityID":83,"ActivityTypeID":2},{"ActivityID":84,"ActivityTypeID":2},{"ActivityID":85,"ActivityTypeID":2},{"ActivityID":86,"ActivityTypeID":2},{"ActivityID":87,"ActivityTypeID":2},{"ActivityID":88,"ActivityTypeID":2},{"ActivityID":89,"ActivityTypeID":2},{"ActivityID":90,"ActivityTypeID":2},{"ActivityID":91,"ActivityTypeID":2},{"ActivityID":92,"ActivityTypeID":2},{"ActivityID":93,"ActivityTypeID":2},{"ActivityID":94,"ActivityTypeID":2},{"ActivityID":95,"ActivityTypeID":2},{"ActivityID":96,"ActivityTypeID":2},{"ActivityID":97,"ActivityTypeID":2},{"ActivityID":98,"ActivityTypeID":2},{"ActivityID":99,"ActivityTypeID":2}] +$4 +PXAT +$13 +1644691703721 +*5 +$3 +SET +$26 +examRepository::GetExam::1 +$517 +{"exam_id":1,"exam_type":"PRE","instruction":"ข้อสอบมีทั้งสิ้น 24 ข้อ เพื่อทดสอบความเข้าใจทุกเนื้อหาในการเรียน หลังจากทำข้อสอบเสร็จจะได้รับคำแนะนำว่าควรเรียนกลุ่มเนื้อหาใด","created_timestamp":"2022-02-12T16:56:43+07:00","content_group_id":0,"content_group_name":"","badge_id":0} +$4 +PXAT +$13 +1644691703844 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::99 +$699 +[{"multiple_choice_id":154,"content":"Normalization คือการทำให้ฐานข้อมูลเก็บข้อมูลได้มากขึ้น","is_correct":false},{"multiple_choice_id":155,"content":"ฐานข้อมูลที่ดีควรจะเป็นแบบ First Normal Form\r\n","is_correct":false},{"multiple_choice_id":156,"content":"First Normal Form ไม่เป็น Second Normal Form","is_correct":true},{"multiple_choice_id":157,"content":"Third Normal Form ไม่เป็น First Normal Form\r\n","is_correct":false},{"multiple_choice_id":158,"content":"ไม่มีข้อใดถูกต้อง","is_correct":false}] +$4 +PXAT +$13 +1644691703931 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::77 +$642 +[{"multiple_choice_id":20,"content":"รูปแบบไฟล์แตกต่างกัน","is_correct":true},{"multiple_choice_id":21,"content":"ค่าใช้จ่ายสูง","is_correct":false},{"multiple_choice_id":22,"content":"ต้องใช้ผู้เชี่ยวชาญในการจัดการ","is_correct":false},{"multiple_choice_id":23,"content":"ข้อมูลมีความเเม่นยำสูง\r\n","is_correct":false},{"multiple_choice_id":24,"content":"คนไม่เกี่ยวข้องสามารถเข้าถึงได้","is_correct":false}] +$4 +PXAT +$13 +1644691703934 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::97 +$650 +[{"multiple_choice_id":144,"content":"{รหัสนักเรียน} -\u003e {ชื่อจริง, นามสกุล}","is_correct":true},{"multiple_choice_id":145,"content":"{ชื่อจริง} -\u003e {รหัสนักเรียน}","is_correct":false},{"multiple_choice_id":146,"content":"{ชื่อจริง} -\u003e {นามสกุล}","is_correct":false},{"multiple_choice_id":147,"content":"{นามสกุล} -\u003e {ชื่อจริง}","is_correct":false},{"multiple_choice_id":148,"content":"{นามสกุล} -\u003e {รหัสนักเรียน}","is_correct":false}] +$4 +PXAT +$13 +1644691704043 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::91 +$528 +[{"multiple_choice_id":114,"content":"คำอธิบาย Entity","is_correct":false},{"multiple_choice_id":115,"content":"ความเกี่ยวข้องกับ Entity อื่น","is_correct":true},{"multiple_choice_id":116,"content":"ชื่อเรียก Entity","is_correct":false},{"multiple_choice_id":117,"content":"เหตุการณ์ที่ทำให้เกิด Entity","is_correct":false},{"multiple_choice_id":118,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691704048 +*5 +$3 +SET +$35 +learningRepository::GetActivity::90 +$271 +{"activity_id":90,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นลำดับขั้นตอนของการออกแบบแบบ Bottom-Up Approach ที่ถูกต้อง"} +$4 +PXAT +$13 +1644691704048 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::85 +$347 +[{"multiple_choice_id":84,"content":"Super Key","is_correct":false},{"multiple_choice_id":85,"content":"Primary Key","is_correct":false},{"multiple_choice_id":86,"content":"Foreign Key","is_correct":false},{"multiple_choice_id":87,"content":"Candidate Key","is_correct":false},{"multiple_choice_id":88,"content":"President Key","is_correct":true}] +$4 +PXAT +$13 +1644691704050 +*5 +$3 +SET +$35 +learningRepository::GetActivity::82 +$185 +{"activity_id":82,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่Relationship ใน Relational Database"} +$4 +PXAT +$13 +1644691704051 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::98 +$728 +[{"multiple_choice_id":149,"content":"{รหัสบุคลากร} -\u003e {ชื่อจริง}","is_correct":true},{"multiple_choice_id":150,"content":"{เบอร์โทร, รหัสบุคลากร} -\u003e {ชื่อจริง}","is_correct":false},{"multiple_choice_id":151,"content":"{ชื่อจริง, นามสกุล} -\u003e {เบอร์โทร}\r\n","is_correct":false},{"multiple_choice_id":152,"content":"{ชื่อจริง, รหัสบุคลากร, เบอร์โทร} -\u003e {ชื่อจริง, เบอร์โทร}\r\n","is_correct":false},{"multiple_choice_id":153,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691704053 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::94 +$753 +[{"multiple_choice_id":129,"content":"มี Relation ที่เหมือนกันหลายอันในฐานข้อมูล","is_correct":false},{"multiple_choice_id":130,"content":"มี Attribute ที่เหมือนกันหลายอันในฐานข้อมูล","is_correct":false},{"multiple_choice_id":131,"content":"มีข้อมูลที่เหมือนกันในหลาย ๆ Tuple","is_correct":true},{"multiple_choice_id":132,"content":"มี Relation ที่มีAttribute มากจนเกินไป\r\n","is_correct":false},{"multiple_choice_id":133,"content":"ทุกเหตุการณ์ทำให้เกิด Data Redundancy","is_correct":false}] +$4 +PXAT +$13 +1644691704054 +*5 +$3 +SET +$36 +learningRepository::GetActivity::100 +$176 +{"activity_id":100,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่อยู่ใน Relational Database"} +$4 +PXAT +$13 +1644691704055 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::78 +$715 +[{"multiple_choice_id":25,"content":"ข้อมูลที่ทุกคนได้รับเป็นข้อมูลที่สอดคล้องกัน\r\n","is_correct":false},{"multiple_choice_id":26,"content":"สามารถเเชร์ข้อมูลระหว่างคอมพิวเตอร์ได","is_correct":false},{"multiple_choice_id":27,"content":"ง่ายต่อการใช้งาน","is_correct":false},{"multiple_choice_id":28,"content":"บุคคลใดก็สามารถเข้าถึงได้\r\n","is_correct":true},{"multiple_choice_id":29,"content":"ทุกข้อไม่เป็นผลดี","is_correct":false}] +$4 +PXAT +$13 +1644691704056 +*5 +$3 +SET +$35 +learningRepository::GetActivity::94 +$198 +{"activity_id":94,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"เหตุการณ์ใดที่ทำให้เกิด Data Redundancy"} +$4 +PXAT +$13 +1644691704056 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::92 +$578 +[{"multiple_choice_id":119,"content":"ชื่อเรียกความสัมพันธ์ของระหว่าง Entity\r\n","is_correct":false},{"multiple_choice_id":120,"content":"Multiplicity","is_correct":false},{"multiple_choice_id":121,"content":"เหตุการณ์ที่ทำให้เกิดความสัมพันธ์\r\n","is_correct":true},{"multiple_choice_id":122,"content":"ชื่อของสอง Entity","is_correct":false},{"multiple_choice_id":123,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691704056 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::83 +$844 +[{"multiple_choice_id":74,"content":"พิจารณาโดยคำนึงถึงความแตกต่างระหว่าง Relation ","is_correct":false},{"multiple_choice_id":75,"content":"พิจารณาจากความสัมพันธ์ระหว่าง Relation ว่าแต่ละ Relation มีความเชื่อมโยงกันอย่างไร","is_correct":true},{"multiple_choice_id":76,"content":"พิจารณาว่าในแต่ละ Relation ต้องมีข้อมูลจากอีก Relation เท่าไร อย่างไร\r\n","is_correct":false},{"multiple_choice_id":77,"content":"ิผิดทุกข้อ\r\n","is_correct":false},{"multiple_choice_id":78,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691704057 +*5 +$3 +SET +$35 +learningRepository::GetActivity::78 +$237 +{"activity_id":78,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ข้อใดไม่เป็นผลดีที่เกิดขึ้นจากการใช้ Database System"} +$4 +PXAT +$13 +1644691704057 +*5 +$3 +SET +$35 +learningRepository::GetActivity::92 +$220 +{"activity_id":92,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"้ข้อใดไม่เป็นส่วนประกอบของ Relationship Data Dictionary"} +$4 +PXAT +$13 +1644691704058 +*5 +$3 +SET +$35 +learningRepository::GetActivity::83 +$233 +{"activity_id":83,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ในการพิจารณา Relationship ของ Relation ข้อใดกล่าวถูกต้อง"} +$4 +PXAT +$13 +1644691704058 +*5 +$3 +SET +$35 +learningRepository::GetActivity::87 +$214 +{"activity_id":87,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช้จุดประสงค์ของการวาด ER-Diagram"} +$4 +PXAT +$13 +1644691704058 +*5 +$3 +SET +$35 +learningRepository::GetActivity::88 +$270 +{"activity_id":88,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นลำดับขั้นตอนของการออกแบบแบบ Top-Down Approach ที่ถูกต้อง"} +$4 +PXAT +$13 +1644691704058 +*5 +$3 +SET +$35 +learningRepository::GetActivity::81 +$352 +{"activity_id":81,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ในการออกแบบฐานข้อมูลโรงเรียน ซึ่งประกอบไปด้วย นักเรียน เเละครู จงเลือกข้อที่เป็น Entity"} +$4 +PXAT +$13 +1644691704059 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::90 +$524 +[{"multiple_choice_id":109,"content":"Entity -\u003e Relationship -\u003e Attribute\r\n","is_correct":false},{"multiple_choice_id":110,"content":"Attribute -\u003e Relationship -\u003e Entity","is_correct":false},{"multiple_choice_id":111,"content":"Attribute -\u003e Entity -\u003e Relationship","is_correct":true},{"multiple_choice_id":112,"content":"Relationship -\u003e Entity -\u003e Attribute","is_correct":false},{"multiple_choice_id":113,"content":"Entity -\u003e Attribute -\u003e Relationship","is_correct":false}] +$4 +PXAT +$13 +1644691704061 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::96 +$388 +[{"multiple_choice_id":139,"content":"Insert Anomaly","is_correct":false},{"multiple_choice_id":140,"content":"Modification Anomaly","is_correct":false},{"multiple_choice_id":141,"content":"Delete Anomaly","is_correct":false},{"multiple_choice_id":142,"content":"Manipulate Anomaly","is_correct":true},{"multiple_choice_id":143,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691704061 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::93 +$418 +[{"multiple_choice_id":124,"content":"Data Type","is_correct":true},{"multiple_choice_id":125,"content":"Multiplicity","is_correct":false},{"multiple_choice_id":126,"content":"Tuple","is_correct":false},{"multiple_choice_id":127,"content":"เหตุการณ์ที่ทำให้เกิด Entity","is_correct":false},{"multiple_choice_id":128,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691704064 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::86 +$682 +[{"multiple_choice_id":89,"content":"สามารถใช้บ่งบอก Tuple ใน Relation นั้น ๆ ได้","is_correct":false},{"multiple_choice_id":90,"content":"Candidate Key สามารถถูกใช้เป็น Primary Key ได้","is_correct":false},{"multiple_choice_id":91,"content":"Candidate Key ที่ไม่เป็น Primary Key จะเรียกว่า Super Key","is_correct":true},{"multiple_choice_id":92,"content":"Candidate Key สามารถประกอบไปด้วยหลาย Attribute","is_correct":false},{"multiple_choice_id":93,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691704064 +*5 +$3 +SET +$35 +learningRepository::GetActivity::77 +$248 +{"activity_id":77,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถือเป็นข้อจำกัดของการเก็บข้อมูลแบบ File-based System"} +$4 +PXAT +$13 +1644691704065 +*5 +$3 +SET +$35 +learningRepository::GetActivity::91 +$211 +{"activity_id":91,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่เป็นส่วนประกอบของ Entity Data Dictionary"} +$4 +PXAT +$13 +1644691704066 +*5 +$3 +SET +$35 +learningRepository::GetActivity::84 +$197 +{"activity_id":84,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่ประเภทของ Database Attributes\r\n"} +$4 +PXAT +$13 +1644691704066 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::95 +$1065 +[{"multiple_choice_id":134,"content":"การเก็บข้อมูลที่ซ้ำกันใน Relation เดียว ทำให้สามารถจัดการได้ง่ายขึ้น\r\n","is_correct":false},{"multiple_choice_id":135,"content":"การเเก้ปัญหา Data Redundancy สามารถทำได้ด้วยการเเยกข้อมูลที่ซ้ำ ไปเก็บใน Relation ใหม","is_correct":true},{"multiple_choice_id":136,"content":"การทำให้เกิด Data Redundancy ทำให้เมื่อต้องการอัพเดทข้อมูล\r\nสามารถทำได้ง่ายเนื่องจากมีข้อมูลที่เหมือนกันอยู่ในที่เดียวกัน","is_correct":false},{"multiple_choice_id":137,"content":"ถูกทุกข้อ\r\n","is_correct":false},{"multiple_choice_id":138,"content":"ผิดทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691704066 +*5 +$3 +SET +$35 +learningRepository::GetActivity::96 +$186 +{"activity_id":96,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่ประเภทของ Data Anomaly"} +$4 +PXAT +$13 +1644691704067 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::87 +$864 +[{"multiple_choice_id":94,"content":"เพื่อสื่อสารกับผู้ร่วมงานเกี่ยวกับการออกแบบฐานข้อมูล","is_correct":false},{"multiple_choice_id":95,"content":"เพื่อให้ลูกค้าเห็นถึงความเป็นมืออาชีพในการทำงาน\r\n","is_correct":true},{"multiple_choice_id":96,"content":"เพื่อเป็นบันทึกเอกสารในรายละเอียดการทำงาน","is_correct":false},{"multiple_choice_id":97,"content":"เพื่อทำให้เห็นภาพรวมของการออกแบบฐานข้อมูล","is_correct":false},{"multiple_choice_id":98,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691704067 +*5 +$3 +SET +$35 +learningRepository::GetActivity::89 +$272 +{"activity_id":89,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นลำดับขั้นตอนของการออกแบบแบบ Inside-Out Approach ที่ถูกต้อง"} +$4 +PXAT +$13 +1644691704067 +*5 +$3 +SET +$35 +learningRepository::GetActivity::79 +$196 +{"activity_id":79,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถูกต้องเกี่ยวกับ Relational Database"} +$4 +PXAT +$13 +1644691704068 +*5 +$3 +SET +$35 +learningRepository::GetActivity::80 +$210 +{"activity_id":80,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถูกต้องเกี่ยวกับการออกแบบ Entity"} +$4 +PXAT +$13 +1644691704070 +*5 +$3 +SET +$35 +learningRepository::GetActivity::93 +$205 +{"activity_id":93,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นส่วนประกอบของ Attribute Data Dictionary"} +$4 +PXAT +$13 +1644691704070 +*5 +$3 +SET +$35 +learningRepository::GetActivity::95 +$192 +{"activity_id":95,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ข้อใดถูกต้องเกี่ยวกับ Data Redundancy"} +$4 +PXAT +$13 +1644691704070 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::88 +$523 +[{"multiple_choice_id":99,"content":"Entity -\u003e Relationship -\u003e Attribute","is_correct":true},{"multiple_choice_id":100,"content":"Attribute -\u003e Relationship -\u003e Entity","is_correct":false},{"multiple_choice_id":101,"content":"Attribute -\u003e Entity -\u003e Relationship","is_correct":false},{"multiple_choice_id":102,"content":"Relationship -\u003e Entity -\u003e Attribute\r\n","is_correct":false},{"multiple_choice_id":103,"content":"Entity -\u003e Attribute -\u003e Relationship","is_correct":false}] +$4 +PXAT +$13 +1644691704071 +*5 +$3 +SET +$35 +learningRepository::GetActivity::97 +$386 +{"activity_id":97,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ในการเก็บข้อมูลนักเรียน ประกอบด้วย รหัสประจำตัวนักเรียน ชื่อจริง เเละนามสกุล ข้อใดเป็น Functional\r\nDependency"} +$4 +PXAT +$13 +1644691704073 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::84 +$384 +[{"multiple_choice_id":79,"content":"Composite Attribute","is_correct":false},{"multiple_choice_id":80,"content":"Simple Attribute","is_correct":false},{"multiple_choice_id":81,"content":"Derived Attribute","is_correct":false},{"multiple_choice_id":82,"content":"Group Attribute","is_correct":true},{"multiple_choice_id":83,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691704073 +*5 +$3 +SET +$35 +learningRepository::GetActivity::99 +$149 +{"activity_id":99,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถูกต้อง"} +$4 +PXAT +$13 +1644691704073 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::89 +$520 +[{"multiple_choice_id":104,"content":"Entity -\u003e Relationship -\u003e Attribute","is_correct":false},{"multiple_choice_id":105,"content":"Attribute -\u003e Relationship -\u003e Entity","is_correct":false},{"multiple_choice_id":106,"content":"Attribute -\u003e Entity -\u003e Relationship","is_correct":false},{"multiple_choice_id":107,"content":"Relationship -\u003e Entity -\u003e Attribute","is_correct":false},{"multiple_choice_id":108,"content":"Entity -\u003e Attribute -\u003e Relationship","is_correct":true}] +$4 +PXAT +$13 +1644691704074 +*5 +$3 +SET +$35 +learningRepository::GetActivity::98 +$380 +{"activity_id":98,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ในการเก็บข้อมูลคุณครู ประกอบด้วย รหัสบุคลากร ชื่อจริง นามสกุล และเบอร์โทร ข้อใดเป็น Full Functional\r\nDependency"} +$4 +PXAT +$13 +1644691704078 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::81 +$466 +[{"multiple_choice_id":64,"content":"ชื่อครู\r\n","is_correct":false},{"multiple_choice_id":65,"content":"นามสกุลครู\r\n","is_correct":false},{"multiple_choice_id":66,"content":"เบอร์โทรนักเรียน","is_correct":false},{"multiple_choice_id":67,"content":"นักเรียน","is_correct":true},{"multiple_choice_id":68,"content":"วันเกิดนักเรียน","is_correct":false}] +$4 +PXAT +$13 +1644691704079 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::79 +$868 +[{"multiple_choice_id":54,"content":"สามารถมี Relation ที่เหมือนกันได้หลาย ๆ อัน เพื่อเก็บข้อมูลที่เกิดขึ้นซ้ำ ๆ","is_correct":false},{"multiple_choice_id":55,"content":"ใน Relation สามารถมี Attribute ที่เหมือนกันได้หลาย ๆ อัน เพื่อเก็บข้อมูลเดียวกันหลาย ๆ ข้อมูล","is_correct":false},{"multiple_choice_id":56,"content":"ข้อมูลใน Attribute ต้องอยู่ใน Domain เดียวกัน","is_correct":true},{"multiple_choice_id":57,"content":"ิผิดทุกข้อ","is_correct":false},{"multiple_choice_id":58,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691704080 +*5 +$3 +SET +$35 +learningRepository::GetActivity::86 +$214 +{"activity_id":86,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ข้อใดกล่าวไม่ถูกต้องเกี่ยวกับ Candidate Key"} +$4 +PXAT +$13 +1644691704081 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::82 +$417 +[{"multiple_choice_id":69,"content":"One to One Relationship\r\n","is_correct":false},{"multiple_choice_id":70,"content":"One to Many Relationship","is_correct":false},{"multiple_choice_id":71,"content":"Many to Many Relationship","is_correct":false},{"multiple_choice_id":72,"content":"None to One Relationship","is_correct":true},{"multiple_choice_id":73,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691704081 +*5 +$3 +SET +$42 +learningRepository::getMultipleChoice::100 +$329 +[{"multiple_choice_id":30,"content":"Tuple","is_correct":false},{"multiple_choice_id":31,"content":"Attribute","is_correct":false},{"multiple_choice_id":32,"content":"Degree","is_correct":false},{"multiple_choice_id":33,"content":"Cardinality","is_correct":false},{"multiple_choice_id":34,"content":"Template","is_correct":true}] +$4 +PXAT +$13 +1644691704081 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::80 +$917 +[{"multiple_choice_id":59,"content":"Entity เป็นข้อมูลที่เล็กที่สุดใน Relational Database","is_correct":false},{"multiple_choice_id":60,"content":"Entity คือกลุ่มของข้อมูลที่มีคุณสมบัติเหมือนกัน\r\n","is_correct":true},{"multiple_choice_id":61,"content":"ข้อมูลที่อยู่ใน Entity สามารถเป็นข้อมูลใดก็ได้ เพียงเเค่ชื่อเหมือนกับ Entity","is_correct":false},{"multiple_choice_id":62,"content":"การออกแบบ Entity ต้องคำนึงว่ามีสิ่งไหนบ้างที่สามารถจับต้องได","is_correct":false},{"multiple_choice_id":63,"content":"ถูกต้องข้อ\r\n","is_correct":false}] +$4 +PXAT +$13 +1644691704082 +*5 +$3 +SET +$35 +learningRepository::GetActivity::85 +$197 +{"activity_id":85,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่ประเภทของ Database Relational Key"} +$4 +PXAT +$13 +1644691704083 +*2 +$3 +DEL +$36 +examRepository::GetExamActivities::1 +*2 +$3 +DEL +$26 +examRepository::GetExam::1 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::85 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::89 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::94 +*2 +$3 +DEL +$36 +learningRepository::GetActivity::100 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::98 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::91 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::88 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::83 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::79 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::84 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::94 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::77 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::97 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::82 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::92 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::87 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::80 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::78 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::95 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::96 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::99 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::93 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::93 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::91 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::90 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::90 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::83 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::78 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::95 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::88 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::96 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::86 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::77 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::92 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::87 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::81 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::97 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::84 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::82 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::79 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::98 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::89 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::80 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::99 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::85 +*2 +$3 +DEL +$42 +learningRepository::getMultipleChoice::100 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::81 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::86 +*5 +$3 +SET +$36 +examRepository::GetExamActivities::1 +$890 +[{"ActivityID":77,"ActivityTypeID":2},{"ActivityID":78,"ActivityTypeID":2},{"ActivityID":79,"ActivityTypeID":2},{"ActivityID":100,"ActivityTypeID":2},{"ActivityID":80,"ActivityTypeID":2},{"ActivityID":81,"ActivityTypeID":2},{"ActivityID":82,"ActivityTypeID":2},{"ActivityID":83,"ActivityTypeID":2},{"ActivityID":84,"ActivityTypeID":2},{"ActivityID":85,"ActivityTypeID":2},{"ActivityID":86,"ActivityTypeID":2},{"ActivityID":87,"ActivityTypeID":2},{"ActivityID":88,"ActivityTypeID":2},{"ActivityID":89,"ActivityTypeID":2},{"ActivityID":90,"ActivityTypeID":2},{"ActivityID":91,"ActivityTypeID":2},{"ActivityID":92,"ActivityTypeID":2},{"ActivityID":93,"ActivityTypeID":2},{"ActivityID":94,"ActivityTypeID":2},{"ActivityID":95,"ActivityTypeID":2},{"ActivityID":96,"ActivityTypeID":2},{"ActivityID":97,"ActivityTypeID":2},{"ActivityID":98,"ActivityTypeID":2},{"ActivityID":99,"ActivityTypeID":2}] +$4 +PXAT +$13 +1644691863783 +*5 +$3 +SET +$26 +examRepository::GetExam::1 +$517 +{"exam_id":1,"exam_type":"PRE","instruction":"ข้อสอบมีทั้งสิ้น 24 ข้อ เพื่อทดสอบความเข้าใจทุกเนื้อหาในการเรียน หลังจากทำข้อสอบเสร็จจะได้รับคำแนะนำว่าควรเรียนกลุ่มเนื้อหาใด","created_timestamp":"2022-02-12T16:56:43+07:00","content_group_id":0,"content_group_name":"","badge_id":0} +$4 +PXAT +$13 +1644691863906 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::99 +$699 +[{"multiple_choice_id":154,"content":"Normalization คือการทำให้ฐานข้อมูลเก็บข้อมูลได้มากขึ้น","is_correct":false},{"multiple_choice_id":155,"content":"ฐานข้อมูลที่ดีควรจะเป็นแบบ First Normal Form\r\n","is_correct":false},{"multiple_choice_id":156,"content":"First Normal Form ไม่เป็น Second Normal Form","is_correct":true},{"multiple_choice_id":157,"content":"Third Normal Form ไม่เป็น First Normal Form\r\n","is_correct":false},{"multiple_choice_id":158,"content":"ไม่มีข้อใดถูกต้อง","is_correct":false}] +$4 +PXAT +$13 +1644691864018 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::77 +$642 +[{"multiple_choice_id":20,"content":"รูปแบบไฟล์แตกต่างกัน","is_correct":true},{"multiple_choice_id":21,"content":"ค่าใช้จ่ายสูง","is_correct":false},{"multiple_choice_id":22,"content":"ต้องใช้ผู้เชี่ยวชาญในการจัดการ","is_correct":false},{"multiple_choice_id":23,"content":"ข้อมูลมีความเเม่นยำสูง\r\n","is_correct":false},{"multiple_choice_id":24,"content":"คนไม่เกี่ยวข้องสามารถเข้าถึงได้","is_correct":false}] +$4 +PXAT +$13 +1644691864019 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::79 +$868 +[{"multiple_choice_id":54,"content":"สามารถมี Relation ที่เหมือนกันได้หลาย ๆ อัน เพื่อเก็บข้อมูลที่เกิดขึ้นซ้ำ ๆ","is_correct":false},{"multiple_choice_id":55,"content":"ใน Relation สามารถมี Attribute ที่เหมือนกันได้หลาย ๆ อัน เพื่อเก็บข้อมูลเดียวกันหลาย ๆ ข้อมูล","is_correct":false},{"multiple_choice_id":56,"content":"ข้อมูลใน Attribute ต้องอยู่ใน Domain เดียวกัน","is_correct":true},{"multiple_choice_id":57,"content":"ิผิดทุกข้อ","is_correct":false},{"multiple_choice_id":58,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691864116 +*5 +$3 +SET +$42 +learningRepository::getMultipleChoice::100 +$329 +[{"multiple_choice_id":30,"content":"Tuple","is_correct":false},{"multiple_choice_id":31,"content":"Attribute","is_correct":false},{"multiple_choice_id":32,"content":"Degree","is_correct":false},{"multiple_choice_id":33,"content":"Cardinality","is_correct":false},{"multiple_choice_id":34,"content":"Template","is_correct":true}] +$4 +PXAT +$13 +1644691864128 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::93 +$418 +[{"multiple_choice_id":124,"content":"Data Type","is_correct":true},{"multiple_choice_id":125,"content":"Multiplicity","is_correct":false},{"multiple_choice_id":126,"content":"Tuple","is_correct":false},{"multiple_choice_id":127,"content":"เหตุการณ์ที่ทำให้เกิด Entity","is_correct":false},{"multiple_choice_id":128,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691864132 +*5 +$3 +SET +$35 +learningRepository::GetActivity::90 +$271 +{"activity_id":90,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นลำดับขั้นตอนของการออกแบบแบบ Bottom-Up Approach ที่ถูกต้อง"} +$4 +PXAT +$13 +1644691864142 +*5 +$3 +SET +$35 +learningRepository::GetActivity::91 +$211 +{"activity_id":91,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่เป็นส่วนประกอบของ Entity Data Dictionary"} +$4 +PXAT +$13 +1644691864151 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::98 +$728 +[{"multiple_choice_id":149,"content":"{รหัสบุคลากร} -\u003e {ชื่อจริง}","is_correct":true},{"multiple_choice_id":150,"content":"{เบอร์โทร, รหัสบุคลากร} -\u003e {ชื่อจริง}","is_correct":false},{"multiple_choice_id":151,"content":"{ชื่อจริง, นามสกุล} -\u003e {เบอร์โทร}\r\n","is_correct":false},{"multiple_choice_id":152,"content":"{ชื่อจริง, รหัสบุคลากร, เบอร์โทร} -\u003e {ชื่อจริง, เบอร์โทร}\r\n","is_correct":false},{"multiple_choice_id":153,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691864151 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::95 +$1065 +[{"multiple_choice_id":134,"content":"การเก็บข้อมูลที่ซ้ำกันใน Relation เดียว ทำให้สามารถจัดการได้ง่ายขึ้น\r\n","is_correct":false},{"multiple_choice_id":135,"content":"การเเก้ปัญหา Data Redundancy สามารถทำได้ด้วยการเเยกข้อมูลที่ซ้ำ ไปเก็บใน Relation ใหม","is_correct":true},{"multiple_choice_id":136,"content":"การทำให้เกิด Data Redundancy ทำให้เมื่อต้องการอัพเดทข้อมูล\r\nสามารถทำได้ง่ายเนื่องจากมีข้อมูลที่เหมือนกันอยู่ในที่เดียวกัน","is_correct":false},{"multiple_choice_id":137,"content":"ถูกทุกข้อ\r\n","is_correct":false},{"multiple_choice_id":138,"content":"ผิดทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691864151 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::97 +$650 +[{"multiple_choice_id":144,"content":"{รหัสนักเรียน} -\u003e {ชื่อจริง, นามสกุล}","is_correct":true},{"multiple_choice_id":145,"content":"{ชื่อจริง} -\u003e {รหัสนักเรียน}","is_correct":false},{"multiple_choice_id":146,"content":"{ชื่อจริง} -\u003e {นามสกุล}","is_correct":false},{"multiple_choice_id":147,"content":"{นามสกุล} -\u003e {ชื่อจริง}","is_correct":false},{"multiple_choice_id":148,"content":"{นามสกุล} -\u003e {รหัสนักเรียน}","is_correct":false}] +$4 +PXAT +$13 +1644691864151 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::89 +$520 +[{"multiple_choice_id":104,"content":"Entity -\u003e Relationship -\u003e Attribute","is_correct":false},{"multiple_choice_id":105,"content":"Attribute -\u003e Relationship -\u003e Entity","is_correct":false},{"multiple_choice_id":106,"content":"Attribute -\u003e Entity -\u003e Relationship","is_correct":false},{"multiple_choice_id":107,"content":"Relationship -\u003e Entity -\u003e Attribute","is_correct":false},{"multiple_choice_id":108,"content":"Entity -\u003e Attribute -\u003e Relationship","is_correct":true}] +$4 +PXAT +$13 +1644691864151 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::94 +$753 +[{"multiple_choice_id":129,"content":"มี Relation ที่เหมือนกันหลายอันในฐานข้อมูล","is_correct":false},{"multiple_choice_id":130,"content":"มี Attribute ที่เหมือนกันหลายอันในฐานข้อมูล","is_correct":false},{"multiple_choice_id":131,"content":"มีข้อมูลที่เหมือนกันในหลาย ๆ Tuple","is_correct":true},{"multiple_choice_id":132,"content":"มี Relation ที่มีAttribute มากจนเกินไป\r\n","is_correct":false},{"multiple_choice_id":133,"content":"ทุกเหตุการณ์ทำให้เกิด Data Redundancy","is_correct":false}] +$4 +PXAT +$13 +1644691864151 +*5 +$3 +SET +$35 +learningRepository::GetActivity::78 +$237 +{"activity_id":78,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ข้อใดไม่เป็นผลดีที่เกิดขึ้นจากการใช้ Database System"} +$4 +PXAT +$13 +1644691864151 +*5 +$3 +SET +$35 +learningRepository::GetActivity::94 +$198 +{"activity_id":94,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"เหตุการณ์ใดที่ทำให้เกิด Data Redundancy"} +$4 +PXAT +$13 +1644691864151 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::80 +$917 +[{"multiple_choice_id":59,"content":"Entity เป็นข้อมูลที่เล็กที่สุดใน Relational Database","is_correct":false},{"multiple_choice_id":60,"content":"Entity คือกลุ่มของข้อมูลที่มีคุณสมบัติเหมือนกัน\r\n","is_correct":true},{"multiple_choice_id":61,"content":"ข้อมูลที่อยู่ใน Entity สามารถเป็นข้อมูลใดก็ได้ เพียงเเค่ชื่อเหมือนกับ Entity","is_correct":false},{"multiple_choice_id":62,"content":"การออกแบบ Entity ต้องคำนึงว่ามีสิ่งไหนบ้างที่สามารถจับต้องได","is_correct":false},{"multiple_choice_id":63,"content":"ถูกต้องข้อ\r\n","is_correct":false}] +$4 +PXAT +$13 +1644691864151 +*5 +$3 +SET +$35 +learningRepository::GetActivity::82 +$185 +{"activity_id":82,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่Relationship ใน Relational Database"} +$4 +PXAT +$13 +1644691864151 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::88 +$523 +[{"multiple_choice_id":99,"content":"Entity -\u003e Relationship -\u003e Attribute","is_correct":true},{"multiple_choice_id":100,"content":"Attribute -\u003e Relationship -\u003e Entity","is_correct":false},{"multiple_choice_id":101,"content":"Attribute -\u003e Entity -\u003e Relationship","is_correct":false},{"multiple_choice_id":102,"content":"Relationship -\u003e Entity -\u003e Attribute\r\n","is_correct":false},{"multiple_choice_id":103,"content":"Entity -\u003e Attribute -\u003e Relationship","is_correct":false}] +$4 +PXAT +$13 +1644691864152 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::87 +$864 +[{"multiple_choice_id":94,"content":"เพื่อสื่อสารกับผู้ร่วมงานเกี่ยวกับการออกแบบฐานข้อมูล","is_correct":false},{"multiple_choice_id":95,"content":"เพื่อให้ลูกค้าเห็นถึงความเป็นมืออาชีพในการทำงาน\r\n","is_correct":true},{"multiple_choice_id":96,"content":"เพื่อเป็นบันทึกเอกสารในรายละเอียดการทำงาน","is_correct":false},{"multiple_choice_id":97,"content":"เพื่อทำให้เห็นภาพรวมของการออกแบบฐานข้อมูล","is_correct":false},{"multiple_choice_id":98,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691864153 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::91 +$528 +[{"multiple_choice_id":114,"content":"คำอธิบาย Entity","is_correct":false},{"multiple_choice_id":115,"content":"ความเกี่ยวข้องกับ Entity อื่น","is_correct":true},{"multiple_choice_id":116,"content":"ชื่อเรียก Entity","is_correct":false},{"multiple_choice_id":117,"content":"เหตุการณ์ที่ทำให้เกิด Entity","is_correct":false},{"multiple_choice_id":118,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691864153 +*5 +$3 +SET +$35 +learningRepository::GetActivity::79 +$196 +{"activity_id":79,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถูกต้องเกี่ยวกับ Relational Database"} +$4 +PXAT +$13 +1644691864155 +*5 +$3 +SET +$35 +learningRepository::GetActivity::98 +$380 +{"activity_id":98,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ในการเก็บข้อมูลคุณครู ประกอบด้วย รหัสบุคลากร ชื่อจริง นามสกุล และเบอร์โทร ข้อใดเป็น Full Functional\r\nDependency"} +$4 +PXAT +$13 +1644691864155 +*5 +$3 +SET +$35 +learningRepository::GetActivity::99 +$149 +{"activity_id":99,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถูกต้อง"} +$4 +PXAT +$13 +1644691864155 +*5 +$3 +SET +$35 +learningRepository::GetActivity::97 +$386 +{"activity_id":97,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ในการเก็บข้อมูลนักเรียน ประกอบด้วย รหัสประจำตัวนักเรียน ชื่อจริง เเละนามสกุล ข้อใดเป็น Functional\r\nDependency"} +$4 +PXAT +$13 +1644691864156 +*5 +$3 +SET +$35 +learningRepository::GetActivity::92 +$220 +{"activity_id":92,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"้ข้อใดไม่เป็นส่วนประกอบของ Relationship Data Dictionary"} +$4 +PXAT +$13 +1644691864156 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::92 +$578 +[{"multiple_choice_id":119,"content":"ชื่อเรียกความสัมพันธ์ของระหว่าง Entity\r\n","is_correct":false},{"multiple_choice_id":120,"content":"Multiplicity","is_correct":false},{"multiple_choice_id":121,"content":"เหตุการณ์ที่ทำให้เกิดความสัมพันธ์\r\n","is_correct":true},{"multiple_choice_id":122,"content":"ชื่อของสอง Entity","is_correct":false},{"multiple_choice_id":123,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691864157 +*5 +$3 +SET +$35 +learningRepository::GetActivity::85 +$197 +{"activity_id":85,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่ประเภทของ Database Relational Key"} +$4 +PXAT +$13 +1644691864158 +*5 +$3 +SET +$36 +learningRepository::GetActivity::100 +$176 +{"activity_id":100,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่อยู่ใน Relational Database"} +$4 +PXAT +$13 +1644691864159 +*5 +$3 +SET +$35 +learningRepository::GetActivity::89 +$272 +{"activity_id":89,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นลำดับขั้นตอนของการออกแบบแบบ Inside-Out Approach ที่ถูกต้อง"} +$4 +PXAT +$13 +1644691864159 +*5 +$3 +SET +$35 +learningRepository::GetActivity::93 +$205 +{"activity_id":93,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นส่วนประกอบของ Attribute Data Dictionary"} +$4 +PXAT +$13 +1644691864162 +*5 +$3 +SET +$35 +learningRepository::GetActivity::96 +$186 +{"activity_id":96,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่ประเภทของ Data Anomaly"} +$4 +PXAT +$13 +1644691864162 +*5 +$3 +SET +$35 +learningRepository::GetActivity::86 +$214 +{"activity_id":86,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ข้อใดกล่าวไม่ถูกต้องเกี่ยวกับ Candidate Key"} +$4 +PXAT +$13 +1644691864164 +*5 +$3 +SET +$35 +learningRepository::GetActivity::95 +$192 +{"activity_id":95,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ข้อใดถูกต้องเกี่ยวกับ Data Redundancy"} +$4 +PXAT +$13 +1644691864165 +*5 +$3 +SET +$35 +learningRepository::GetActivity::77 +$248 +{"activity_id":77,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถือเป็นข้อจำกัดของการเก็บข้อมูลแบบ File-based System"} +$4 +PXAT +$13 +1644691864165 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::84 +$384 +[{"multiple_choice_id":79,"content":"Composite Attribute","is_correct":false},{"multiple_choice_id":80,"content":"Simple Attribute","is_correct":false},{"multiple_choice_id":81,"content":"Derived Attribute","is_correct":false},{"multiple_choice_id":82,"content":"Group Attribute","is_correct":true},{"multiple_choice_id":83,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691864166 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::82 +$417 +[{"multiple_choice_id":69,"content":"One to One Relationship\r\n","is_correct":false},{"multiple_choice_id":70,"content":"One to Many Relationship","is_correct":false},{"multiple_choice_id":71,"content":"Many to Many Relationship","is_correct":false},{"multiple_choice_id":72,"content":"None to One Relationship","is_correct":true},{"multiple_choice_id":73,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691864166 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::81 +$466 +[{"multiple_choice_id":64,"content":"ชื่อครู\r\n","is_correct":false},{"multiple_choice_id":65,"content":"นามสกุลครู\r\n","is_correct":false},{"multiple_choice_id":66,"content":"เบอร์โทรนักเรียน","is_correct":false},{"multiple_choice_id":67,"content":"นักเรียน","is_correct":true},{"multiple_choice_id":68,"content":"วันเกิดนักเรียน","is_correct":false}] +$4 +PXAT +$13 +1644691864167 +*5 +$3 +SET +$35 +learningRepository::GetActivity::81 +$352 +{"activity_id":81,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ในการออกแบบฐานข้อมูลโรงเรียน ซึ่งประกอบไปด้วย นักเรียน เเละครู จงเลือกข้อที่เป็น Entity"} +$4 +PXAT +$13 +1644691864168 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::96 +$388 +[{"multiple_choice_id":139,"content":"Insert Anomaly","is_correct":false},{"multiple_choice_id":140,"content":"Modification Anomaly","is_correct":false},{"multiple_choice_id":141,"content":"Delete Anomaly","is_correct":false},{"multiple_choice_id":142,"content":"Manipulate Anomaly","is_correct":true},{"multiple_choice_id":143,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691864168 +*5 +$3 +SET +$35 +learningRepository::GetActivity::87 +$214 +{"activity_id":87,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช้จุดประสงค์ของการวาด ER-Diagram"} +$4 +PXAT +$13 +1644691864168 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::78 +$715 +[{"multiple_choice_id":25,"content":"ข้อมูลที่ทุกคนได้รับเป็นข้อมูลที่สอดคล้องกัน\r\n","is_correct":false},{"multiple_choice_id":26,"content":"สามารถเเชร์ข้อมูลระหว่างคอมพิวเตอร์ได","is_correct":false},{"multiple_choice_id":27,"content":"ง่ายต่อการใช้งาน","is_correct":false},{"multiple_choice_id":28,"content":"บุคคลใดก็สามารถเข้าถึงได้\r\n","is_correct":true},{"multiple_choice_id":29,"content":"ทุกข้อไม่เป็นผลดี","is_correct":false}] +$4 +PXAT +$13 +1644691864169 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::83 +$844 +[{"multiple_choice_id":74,"content":"พิจารณาโดยคำนึงถึงความแตกต่างระหว่าง Relation ","is_correct":false},{"multiple_choice_id":75,"content":"พิจารณาจากความสัมพันธ์ระหว่าง Relation ว่าแต่ละ Relation มีความเชื่อมโยงกันอย่างไร","is_correct":true},{"multiple_choice_id":76,"content":"พิจารณาว่าในแต่ละ Relation ต้องมีข้อมูลจากอีก Relation เท่าไร อย่างไร\r\n","is_correct":false},{"multiple_choice_id":77,"content":"ิผิดทุกข้อ\r\n","is_correct":false},{"multiple_choice_id":78,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691864175 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::86 +$682 +[{"multiple_choice_id":89,"content":"สามารถใช้บ่งบอก Tuple ใน Relation นั้น ๆ ได้","is_correct":false},{"multiple_choice_id":90,"content":"Candidate Key สามารถถูกใช้เป็น Primary Key ได้","is_correct":false},{"multiple_choice_id":91,"content":"Candidate Key ที่ไม่เป็น Primary Key จะเรียกว่า Super Key","is_correct":true},{"multiple_choice_id":92,"content":"Candidate Key สามารถประกอบไปด้วยหลาย Attribute","is_correct":false},{"multiple_choice_id":93,"content":"ถูกทุกข้อ","is_correct":false}] +$4 +PXAT +$13 +1644691864175 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::85 +$347 +[{"multiple_choice_id":84,"content":"Super Key","is_correct":false},{"multiple_choice_id":85,"content":"Primary Key","is_correct":false},{"multiple_choice_id":86,"content":"Foreign Key","is_correct":false},{"multiple_choice_id":87,"content":"Candidate Key","is_correct":false},{"multiple_choice_id":88,"content":"President Key","is_correct":true}] +$4 +PXAT +$13 +1644691864176 +*5 +$3 +SET +$41 +learningRepository::getMultipleChoice::90 +$524 +[{"multiple_choice_id":109,"content":"Entity -\u003e Relationship -\u003e Attribute\r\n","is_correct":false},{"multiple_choice_id":110,"content":"Attribute -\u003e Relationship -\u003e Entity","is_correct":false},{"multiple_choice_id":111,"content":"Attribute -\u003e Entity -\u003e Relationship","is_correct":true},{"multiple_choice_id":112,"content":"Relationship -\u003e Entity -\u003e Attribute","is_correct":false},{"multiple_choice_id":113,"content":"Entity -\u003e Attribute -\u003e Relationship","is_correct":false}] +$4 +PXAT +$13 +1644691864177 +*5 +$3 +SET +$35 +learningRepository::GetActivity::80 +$210 +{"activity_id":80,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดถูกต้องเกี่ยวกับการออกแบบ Entity"} +$4 +PXAT +$13 +1644691864182 +*5 +$3 +SET +$35 +learningRepository::GetActivity::84 +$197 +{"activity_id":84,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดไม่ใช่ประเภทของ Database Attributes\r\n"} +$4 +PXAT +$13 +1644691864183 +*5 +$3 +SET +$35 +learningRepository::GetActivity::88 +$270 +{"activity_id":88,"activity_type_id":2,"content_id":null,"activity_order":1,"story":"","point":300,"question":"ข้อใดเป็นลำดับขั้นตอนของการออกแบบแบบ Top-Down Approach ที่ถูกต้อง"} +$4 +PXAT +$13 +1644691864183 +*5 +$3 +SET +$35 +learningRepository::GetActivity::83 +$233 +{"activity_id":83,"activity_type_id":2,"content_id":null,"activity_order":2,"story":"","point":300,"question":"ในการพิจารณา Relationship ของ Relation ข้อใดกล่าวถูกต้อง"} +$4 +PXAT +$13 +1644691864185 +*5 +$3 +SET +$33 +examService::GetExamResult::1::13 +$1420 +{"exam_id":1,"exam_result_id":13,"exam_type":"PRE","content_group_name":"","created_timestamp":"2022-02-13T01:50:54.1866424+07:00","score":2400,"is_passed":false,"activities_result":[{"exam_result_id":13,"activity_id":94,"score":0},{"exam_result_id":13,"activity_id":91,"score":0},{"exam_result_id":13,"activity_id":79,"score":300},{"exam_result_id":13,"activity_id":99,"score":300},{"exam_result_id":13,"activity_id":98,"score":600},{"exam_result_id":13,"activity_id":97,"score":600},{"exam_result_id":13,"activity_id":92,"score":600},{"exam_result_id":13,"activity_id":100,"score":900},{"exam_result_id":13,"activity_id":89,"score":900},{"exam_result_id":13,"activity_id":93,"score":1200},{"exam_result_id":13,"activity_id":95,"score":1500},{"exam_result_id":13,"activity_id":77,"score":1500},{"exam_result_id":13,"activity_id":82,"score":1500},{"exam_result_id":13,"activity_id":81,"score":1800},{"exam_result_id":13,"activity_id":96,"score":1800},{"exam_result_id":13,"activity_id":87,"score":1800},{"exam_result_id":13,"activity_id":78,"score":1800},{"exam_result_id":13,"activity_id":86,"score":1800},{"exam_result_id":13,"activity_id":85,"score":2100},{"exam_result_id":13,"activity_id":90,"score":2100},{"exam_result_id":13,"activity_id":80,"score":2400},{"exam_result_id":13,"activity_id":84,"score":2400},{"exam_result_id":13,"activity_id":88,"score":2400},{"exam_result_id":13,"activity_id":83,"score":2400}]} +$4 +PXAT +$13 +1644691864368 +*2 +$3 +DEL +$36 +examRepository::GetExamActivities::1 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::99 +*2 +$3 +DEL +$26 +examRepository::GetExam::1 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::81 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::97 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::84 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::85 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::89 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::82 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::94 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::98 +*2 +$3 +DEL +$36 +learningRepository::GetActivity::100 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::88 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::91 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::79 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::83 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::89 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::98 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::84 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::80 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::79 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::94 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::77 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::97 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::82 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::99 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::92 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::87 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::85 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::80 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::78 +*2 +$3 +DEL +$42 +learningRepository::getMultipleChoice::100 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::95 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::96 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::93 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::93 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::91 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::90 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::81 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::90 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::83 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::78 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::95 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::88 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::86 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::96 +*2 +$3 +DEL +$41 +learningRepository::getMultipleChoice::86 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::77 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::87 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::92 +*2 +$3 +DEL +$33 +examService::GetExamResult::1::13 +*5 +$3 +SET +$39 +examRepository::GetActivitiesResult::14 +$1239 +[{"exam_result_id":14,"activity_id":77,"score":1800},{"exam_result_id":14,"activity_id":78,"score":2100},{"exam_result_id":14,"activity_id":79,"score":2100},{"exam_result_id":14,"activity_id":80,"score":2400},{"exam_result_id":14,"activity_id":81,"score":1800},{"exam_result_id":14,"activity_id":82,"score":1500},{"exam_result_id":14,"activity_id":83,"score":1200},{"exam_result_id":14,"activity_id":84,"score":2100},{"exam_result_id":14,"activity_id":85,"score":1200},{"exam_result_id":14,"activity_id":86,"score":1200},{"exam_result_id":14,"activity_id":87,"score":1200},{"exam_result_id":14,"activity_id":88,"score":1200},{"exam_result_id":14,"activity_id":89,"score":1200},{"exam_result_id":14,"activity_id":90,"score":1200},{"exam_result_id":14,"activity_id":91,"score":900},{"exam_result_id":14,"activity_id":92,"score":900},{"exam_result_id":14,"activity_id":93,"score":1200},{"exam_result_id":14,"activity_id":94,"score":600},{"exam_result_id":14,"activity_id":95,"score":600},{"exam_result_id":14,"activity_id":96,"score":300},{"exam_result_id":14,"activity_id":97,"score":0},{"exam_result_id":14,"activity_id":98,"score":0},{"exam_result_id":14,"activity_id":99,"score":1800},{"exam_result_id":14,"activity_id":100,"score":2400}] +$4 +PXAT +$13 +1644693369528 +*5 +$3 +SET +$36 +examRepository::GetExamResult::8::14 +$154 +{"exam_result_id":0,"exam_id":0,"user_id":0,"score":0,"exam_type":"","content_group_name":"","is_passed":false,"created_timestamp":"0001-01-01T00:00:00Z"} +$4 +PXAT +$13 +1644693369654 +*2 +$3 +DEL +$39 +examRepository::GetActivitiesResult::14 +*2 +$3 +DEL +$36 +examRepository::GetExamResult::8::14 +*5 +$3 +SET +$35 +learningRepository::GetActivity::49 +$1213 +{"activity_id":49,"activity_type_id":6,"content_id":10,"activity_order":1,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมเเห่งหนึ่ง ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี้\u003c/li\u003e\nนายแกนจะสามารถออกแบบ Entity ที่จะต้องมีภายในฐานข้อมูลนี้อย่างไร","point":300,"question":"สร้าง Relation ต่าง ๆ ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"} +$4 +PXAT +$13 +1644697058946 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::49 +*5 +$3 +SET +$35 +learningRepository::GetActivity::49 +$1213 +{"activity_id":49,"activity_type_id":6,"content_id":10,"activity_order":1,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมเเห่งหนึ่ง ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี้\u003c/li\u003e\nนายแกนจะสามารถออกแบบ Entity ที่จะต้องมีภายในฐานข้อมูลนี้อย่างไร","point":300,"question":"สร้าง Relation ต่าง ๆ ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"} +$4 +PXAT +$13 +1644697094542 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::49 +*5 +$3 +SET +$35 +learningRepository::GetActivity::49 +$1213 +{"activity_id":49,"activity_type_id":6,"content_id":10,"activity_order":1,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมเเห่งหนึ่ง ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี้\u003c/li\u003e\nนายแกนจะสามารถออกแบบ Entity ที่จะต้องมีภายในฐานข้อมูลนี้อย่างไร","point":300,"question":"สร้าง Relation ต่าง ๆ ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"} +$4 +PXAT +$13 +1644697199885 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::49 +*5 +$3 +SET +$35 +learningRepository::GetActivity::49 +$1213 +{"activity_id":49,"activity_type_id":6,"content_id":10,"activity_order":1,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมเเห่งหนึ่ง ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี้\u003c/li\u003e\nนายแกนจะสามารถออกแบบ Entity ที่จะต้องมีภายในฐานข้อมูลนี้อย่างไร","point":300,"question":"สร้าง Relation ต่าง ๆ ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"} +$4 +PXAT +$13 +1644697212086 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::49 +*5 +$3 +SET +$35 +learningRepository::GetActivity::49 +$1213 +{"activity_id":49,"activity_type_id":6,"content_id":10,"activity_order":1,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมเเห่งหนึ่ง ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี้\u003c/li\u003e\nนายแกนจะสามารถออกแบบ Entity ที่จะต้องมีภายในฐานข้อมูลนี้อย่างไร","point":300,"question":"สร้าง Relation ต่าง ๆ ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"} +$4 +PXAT +$13 +1644697246510 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::49 +*5 +$3 +SET +$35 +learningRepository::GetActivity::49 +$1213 +{"activity_id":49,"activity_type_id":6,"content_id":10,"activity_order":1,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมเเห่งหนึ่ง ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี้\u003c/li\u003e\nนายแกนจะสามารถออกแบบ Entity ที่จะต้องมีภายในฐานข้อมูลนี้อย่างไร","point":300,"question":"สร้าง Relation ต่าง ๆ ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"} +$4 +PXAT +$13 +1644697290895 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::49 +*5 +$3 +SET +$35 +learningRepository::GetActivity::49 +$1213 +{"activity_id":49,"activity_type_id":6,"content_id":10,"activity_order":1,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมเเห่งหนึ่ง ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี้\u003c/li\u003e\nนายแกนจะสามารถออกแบบ Entity ที่จะต้องมีภายในฐานข้อมูลนี้อย่างไร","point":300,"question":"สร้าง Relation ต่าง ๆ ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"} +$4 +PXAT +$13 +1644697383653 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::49 +*5 +$3 +SET +$35 +learningRepository::GetActivity::49 +$1213 +{"activity_id":49,"activity_type_id":6,"content_id":10,"activity_order":1,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมเเห่งหนึ่ง ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี้\u003c/li\u003e\nนายแกนจะสามารถออกแบบ Entity ที่จะต้องมีภายในฐานข้อมูลนี้อย่างไร","point":300,"question":"สร้าง Relation ต่าง ๆ ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"} +$4 +PXAT +$13 +1644697760801 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::49 +*5 +$3 +SET +$35 +learningRepository::GetActivity::49 +$1213 +{"activity_id":49,"activity_type_id":6,"content_id":10,"activity_order":1,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมเเห่งหนึ่ง ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี้\u003c/li\u003e\nนายแกนจะสามารถออกแบบ Entity ที่จะต้องมีภายในฐานข้อมูลนี้อย่างไร","point":300,"question":"สร้าง Relation ต่าง ๆ ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"} +$4 +PXAT +$13 +1644697818347 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::49 +*5 +$3 +SET +$35 +learningRepository::GetActivity::49 +$1213 +{"activity_id":49,"activity_type_id":6,"content_id":10,"activity_order":1,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมเเห่งหนึ่ง ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี้\u003c/li\u003e\nนายแกนจะสามารถออกแบบ Entity ที่จะต้องมีภายในฐานข้อมูลนี้อย่างไร","point":300,"question":"สร้าง Relation ต่าง ๆ ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"} +$4 +PXAT +$13 +1644697845777 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::49 +*5 +$3 +SET +$35 +learningRepository::GetActivity::49 +$1213 +{"activity_id":49,"activity_type_id":6,"content_id":10,"activity_order":1,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมเเห่งหนึ่ง ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี้\u003c/li\u003e\nนายแกนจะสามารถออกแบบ Entity ที่จะต้องมีภายในฐานข้อมูลนี้อย่างไร","point":300,"question":"สร้าง Relation ต่าง ๆ ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"} +$4 +PXAT +$13 +1644697902081 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::49 +*5 +$3 +SET +$35 +learningRepository::GetActivity::49 +$1213 +{"activity_id":49,"activity_type_id":6,"content_id":10,"activity_order":1,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมเเห่งหนึ่ง ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี้\u003c/li\u003e\nนายแกนจะสามารถออกแบบ Entity ที่จะต้องมีภายในฐานข้อมูลนี้อย่างไร","point":300,"question":"สร้าง Relation ต่าง ๆ ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"} +$4 +PXAT +$13 +1644697926783 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::49 +*5 +$3 +SET +$35 +learningRepository::GetActivity::49 +$1213 +{"activity_id":49,"activity_type_id":6,"content_id":10,"activity_order":1,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมเเห่งหนึ่ง ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี้\u003c/li\u003e\nนายแกนจะสามารถออกแบบ Entity ที่จะต้องมีภายในฐานข้อมูลนี้อย่างไร","point":300,"question":"สร้าง Relation ต่าง ๆ ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"} +$4 +PXAT +$13 +1644698025637 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::49 +*5 +$3 +SET +$35 +learningRepository::GetActivity::49 +$1213 +{"activity_id":49,"activity_type_id":6,"content_id":10,"activity_order":1,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมเเห่งหนึ่ง ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี้\u003c/li\u003e\nนายแกนจะสามารถออกแบบ Entity ที่จะต้องมีภายในฐานข้อมูลนี้อย่างไร","point":300,"question":"สร้าง Relation ต่าง ๆ ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"} +$4 +PXAT +$13 +1644698081550 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::49 +*5 +$3 +SET +$35 +learningRepository::GetActivity::50 +$2032 +{"activity_id":50,"activity_type_id":6,"content_id":11,"activity_order":2,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมเเห่งหนึ่ง ซึ่งในต้องการเก็บข้อมูลดังนี้ \u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้\u003c/li\u003e \u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี\u003c/li\u003e แต่หลังจากที่นายแกนทราบ Entity ต่าง ๆ เเล้ว นายแกนจะต้องเก็บข้อมูลใดบ้างลงใน Entity ต่าง ๆ โดยข้อมูลมีดังนี้ \u003cli\u003eแขกผู้เข้าพัก เก็บข้อมูลดังนี้\u003c/li\u003e \u003cli\u003eรหัสแขก (guest_id) บ่งบอกถึงแขกที่เข้าพัก\u003c/li\u003e \u003cli\u003eชื่อจริง (first_name)\u003c/li\u003e \u003cli\u003eนามสกุล (last_name)\u003c/li\u003e \u003cli\u003eเบอร์โทร (contact_tel)\u003c/li\u003e \u003cli\u003eห้องพัก เก็บข้อมูลดังนี้\u003c/li\u003e \u003cli\u003eหมายเลขห้อง (room_no)\u003c/li\u003e \u003cli\u003eชื่อห้องพัก (room_name)\u003c/li\u003e","point":300,"question":"เพิ่ม Attribute ต่าง ๆ ลงใน Relation ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"} +$4 +PXAT +$13 +1644698900766 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::50 +$2 +[] +$4 +PXAT +$13 +1644698900781 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::50 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::50 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::50 +$2 +[] +$4 +PXAT +$13 +1644698959682 +*5 +$3 +SET +$35 +learningRepository::GetActivity::50 +$2032 +{"activity_id":50,"activity_type_id":6,"content_id":11,"activity_order":2,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมเเห่งหนึ่ง ซึ่งในต้องการเก็บข้อมูลดังนี้ \u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้\u003c/li\u003e \u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี\u003c/li\u003e แต่หลังจากที่นายแกนทราบ Entity ต่าง ๆ เเล้ว นายแกนจะต้องเก็บข้อมูลใดบ้างลงใน Entity ต่าง ๆ โดยข้อมูลมีดังนี้ \u003cli\u003eแขกผู้เข้าพัก เก็บข้อมูลดังนี้\u003c/li\u003e \u003cli\u003eรหัสแขก (guest_id) บ่งบอกถึงแขกที่เข้าพัก\u003c/li\u003e \u003cli\u003eชื่อจริง (first_name)\u003c/li\u003e \u003cli\u003eนามสกุล (last_name)\u003c/li\u003e \u003cli\u003eเบอร์โทร (contact_tel)\u003c/li\u003e \u003cli\u003eห้องพัก เก็บข้อมูลดังนี้\u003c/li\u003e \u003cli\u003eหมายเลขห้อง (room_no)\u003c/li\u003e \u003cli\u003eชื่อห้องพัก (room_name)\u003c/li\u003e","point":300,"question":"เพิ่ม Attribute ต่าง ๆ ลงใน Relation ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"} +$4 +PXAT +$13 +1644698959686 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::50 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::50 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::50 +$2 +[] +$4 +PXAT +$13 +1644699026699 +*5 +$3 +SET +$35 +learningRepository::GetActivity::50 +$2032 +{"activity_id":50,"activity_type_id":6,"content_id":11,"activity_order":2,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมเเห่งหนึ่ง ซึ่งในต้องการเก็บข้อมูลดังนี้ \u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้\u003c/li\u003e \u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี\u003c/li\u003e แต่หลังจากที่นายแกนทราบ Entity ต่าง ๆ เเล้ว นายแกนจะต้องเก็บข้อมูลใดบ้างลงใน Entity ต่าง ๆ โดยข้อมูลมีดังนี้ \u003cli\u003eแขกผู้เข้าพัก เก็บข้อมูลดังนี้\u003c/li\u003e \u003cli\u003eรหัสแขก (guest_id) บ่งบอกถึงแขกที่เข้าพัก\u003c/li\u003e \u003cli\u003eชื่อจริง (first_name)\u003c/li\u003e \u003cli\u003eนามสกุล (last_name)\u003c/li\u003e \u003cli\u003eเบอร์โทร (contact_tel)\u003c/li\u003e \u003cli\u003eห้องพัก เก็บข้อมูลดังนี้\u003c/li\u003e \u003cli\u003eหมายเลขห้อง (room_no)\u003c/li\u003e \u003cli\u003eชื่อห้องพัก (room_name)\u003c/li\u003e","point":300,"question":"เพิ่ม Attribute ต่าง ๆ ลงใน Relation ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"} +$4 +PXAT +$13 +1644699026701 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::50 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::50 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::50 +$2 +[] +$4 +PXAT +$13 +1644699159338 +*5 +$3 +SET +$35 +learningRepository::GetActivity::50 +$2032 +{"activity_id":50,"activity_type_id":6,"content_id":11,"activity_order":2,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมเเห่งหนึ่ง ซึ่งในต้องการเก็บข้อมูลดังนี้ \u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้\u003c/li\u003e \u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี\u003c/li\u003e แต่หลังจากที่นายแกนทราบ Entity ต่าง ๆ เเล้ว นายแกนจะต้องเก็บข้อมูลใดบ้างลงใน Entity ต่าง ๆ โดยข้อมูลมีดังนี้ \u003cli\u003eแขกผู้เข้าพัก เก็บข้อมูลดังนี้\u003c/li\u003e \u003cli\u003eรหัสแขก (guest_id) บ่งบอกถึงแขกที่เข้าพัก\u003c/li\u003e \u003cli\u003eชื่อจริง (first_name)\u003c/li\u003e \u003cli\u003eนามสกุล (last_name)\u003c/li\u003e \u003cli\u003eเบอร์โทร (contact_tel)\u003c/li\u003e \u003cli\u003eห้องพัก เก็บข้อมูลดังนี้\u003c/li\u003e \u003cli\u003eหมายเลขห้อง (room_no)\u003c/li\u003e \u003cli\u003eชื่อห้องพัก (room_name)\u003c/li\u003e","point":300,"question":"เพิ่ม Attribute ต่าง ๆ ลงใน Relation ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"} +$4 +PXAT +$13 +1644699159366 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::50 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::50 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::50 +$2 +[] +$4 +PXAT +$13 +1644699249921 +*5 +$3 +SET +$35 +learningRepository::GetActivity::50 +$2032 +{"activity_id":50,"activity_type_id":6,"content_id":11,"activity_order":2,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมเเห่งหนึ่ง ซึ่งในต้องการเก็บข้อมูลดังนี้ \u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้\u003c/li\u003e \u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี\u003c/li\u003e แต่หลังจากที่นายแกนทราบ Entity ต่าง ๆ เเล้ว นายแกนจะต้องเก็บข้อมูลใดบ้างลงใน Entity ต่าง ๆ โดยข้อมูลมีดังนี้ \u003cli\u003eแขกผู้เข้าพัก เก็บข้อมูลดังนี้\u003c/li\u003e \u003cli\u003eรหัสแขก (guest_id) บ่งบอกถึงแขกที่เข้าพัก\u003c/li\u003e \u003cli\u003eชื่อจริง (first_name)\u003c/li\u003e \u003cli\u003eนามสกุล (last_name)\u003c/li\u003e \u003cli\u003eเบอร์โทร (contact_tel)\u003c/li\u003e \u003cli\u003eห้องพัก เก็บข้อมูลดังนี้\u003c/li\u003e \u003cli\u003eหมายเลขห้อง (room_no)\u003c/li\u003e \u003cli\u003eชื่อห้องพัก (room_name)\u003c/li\u003e","point":300,"question":"เพิ่ม Attribute ต่าง ๆ ลงใน Relation ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"} +$4 +PXAT +$13 +1644699249926 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::50 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::50 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::50 +$2 +[] +$4 +PXAT +$13 +1644699416765 +*5 +$3 +SET +$35 +learningRepository::GetActivity::50 +$2032 +{"activity_id":50,"activity_type_id":6,"content_id":11,"activity_order":2,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมเเห่งหนึ่ง ซึ่งในต้องการเก็บข้อมูลดังนี้ \u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้\u003c/li\u003e \u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี\u003c/li\u003e แต่หลังจากที่นายแกนทราบ Entity ต่าง ๆ เเล้ว นายแกนจะต้องเก็บข้อมูลใดบ้างลงใน Entity ต่าง ๆ โดยข้อมูลมีดังนี้ \u003cli\u003eแขกผู้เข้าพัก เก็บข้อมูลดังนี้\u003c/li\u003e \u003cli\u003eรหัสแขก (guest_id) บ่งบอกถึงแขกที่เข้าพัก\u003c/li\u003e \u003cli\u003eชื่อจริง (first_name)\u003c/li\u003e \u003cli\u003eนามสกุล (last_name)\u003c/li\u003e \u003cli\u003eเบอร์โทร (contact_tel)\u003c/li\u003e \u003cli\u003eห้องพัก เก็บข้อมูลดังนี้\u003c/li\u003e \u003cli\u003eหมายเลขห้อง (room_no)\u003c/li\u003e \u003cli\u003eชื่อห้องพัก (room_name)\u003c/li\u003e","point":300,"question":"เพิ่ม Attribute ต่าง ๆ ลงใน Relation ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"} +$4 +PXAT +$13 +1644699416891 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::50 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::50 +*5 +$3 +SET +$35 +learningRepository::GetActivity::50 +$2032 +{"activity_id":50,"activity_type_id":6,"content_id":11,"activity_order":2,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมเเห่งหนึ่ง ซึ่งในต้องการเก็บข้อมูลดังนี้ \u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้\u003c/li\u003e \u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี\u003c/li\u003e แต่หลังจากที่นายแกนทราบ Entity ต่าง ๆ เเล้ว นายแกนจะต้องเก็บข้อมูลใดบ้างลงใน Entity ต่าง ๆ โดยข้อมูลมีดังนี้ \u003cli\u003eแขกผู้เข้าพัก เก็บข้อมูลดังนี้\u003c/li\u003e \u003cli\u003eรหัสแขก (guest_id) บ่งบอกถึงแขกที่เข้าพัก\u003c/li\u003e \u003cli\u003eชื่อจริง (first_name)\u003c/li\u003e \u003cli\u003eนามสกุล (last_name)\u003c/li\u003e \u003cli\u003eเบอร์โทร (contact_tel)\u003c/li\u003e \u003cli\u003eห้องพัก เก็บข้อมูลดังนี้\u003c/li\u003e \u003cli\u003eหมายเลขห้อง (room_no)\u003c/li\u003e \u003cli\u003eชื่อห้องพัก (room_name)\u003c/li\u003e","point":300,"question":"เพิ่ม Attribute ต่าง ๆ ลงใน Relation ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"} +$4 +PXAT +$13 +1644699444557 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::50 +$2 +[] +$4 +PXAT +$13 +1644699444573 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::50 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::50 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::50 +$2 +[] +$4 +PXAT +$13 +1644699464986 +*5 +$3 +SET +$35 +learningRepository::GetActivity::50 +$2032 +{"activity_id":50,"activity_type_id":6,"content_id":11,"activity_order":2,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมเเห่งหนึ่ง ซึ่งในต้องการเก็บข้อมูลดังนี้ \u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้\u003c/li\u003e \u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี\u003c/li\u003e แต่หลังจากที่นายแกนทราบ Entity ต่าง ๆ เเล้ว นายแกนจะต้องเก็บข้อมูลใดบ้างลงใน Entity ต่าง ๆ โดยข้อมูลมีดังนี้ \u003cli\u003eแขกผู้เข้าพัก เก็บข้อมูลดังนี้\u003c/li\u003e \u003cli\u003eรหัสแขก (guest_id) บ่งบอกถึงแขกที่เข้าพัก\u003c/li\u003e \u003cli\u003eชื่อจริง (first_name)\u003c/li\u003e \u003cli\u003eนามสกุล (last_name)\u003c/li\u003e \u003cli\u003eเบอร์โทร (contact_tel)\u003c/li\u003e \u003cli\u003eห้องพัก เก็บข้อมูลดังนี้\u003c/li\u003e \u003cli\u003eหมายเลขห้อง (room_no)\u003c/li\u003e \u003cli\u003eชื่อห้องพัก (room_name)\u003c/li\u003e","point":300,"question":"เพิ่ม Attribute ต่าง ๆ ลงใน Relation ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"} +$4 +PXAT +$13 +1644699464987 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::50 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::50 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::50 +$2 +[] +$4 +PXAT +$13 +1644699505877 +*5 +$3 +SET +$35 +learningRepository::GetActivity::50 +$2032 +{"activity_id":50,"activity_type_id":6,"content_id":11,"activity_order":2,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมเเห่งหนึ่ง ซึ่งในต้องการเก็บข้อมูลดังนี้ \u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้\u003c/li\u003e \u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี\u003c/li\u003e แต่หลังจากที่นายแกนทราบ Entity ต่าง ๆ เเล้ว นายแกนจะต้องเก็บข้อมูลใดบ้างลงใน Entity ต่าง ๆ โดยข้อมูลมีดังนี้ \u003cli\u003eแขกผู้เข้าพัก เก็บข้อมูลดังนี้\u003c/li\u003e \u003cli\u003eรหัสแขก (guest_id) บ่งบอกถึงแขกที่เข้าพัก\u003c/li\u003e \u003cli\u003eชื่อจริง (first_name)\u003c/li\u003e \u003cli\u003eนามสกุล (last_name)\u003c/li\u003e \u003cli\u003eเบอร์โทร (contact_tel)\u003c/li\u003e \u003cli\u003eห้องพัก เก็บข้อมูลดังนี้\u003c/li\u003e \u003cli\u003eหมายเลขห้อง (room_no)\u003c/li\u003e \u003cli\u003eชื่อห้องพัก (room_name)\u003c/li\u003e","point":300,"question":"เพิ่ม Attribute ต่าง ๆ ลงใน Relation ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"} +$4 +PXAT +$13 +1644699505891 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::50 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::50 +*2 +$6 +SELECT +$1 +0 +*5 +$3 +SET +$33 +learningRepository::GetContent::9 +$140 +{"content_id":9,"content_group_id":3,"name":"Top-down database design","video_path":"video/group3 top-down database design.mp4","slide":"-"} +$4 +PXAT +$13 +1644718502380 +*5 +$3 +SET +$41 +learningRepository::GetContentActivity::9 +$15849 +[{"activity_id":40,"activity_type_id":6,"content_id":9,"activity_order":1,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงพยาบาลเเห่งหนึ่ง ซึ่งต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eคนไข้ (patient) สำหรับเก็บข้อมูลเกี่ยวกับคนไข้ที่เข้ามารักษาภายใน​โรงพยาบาล\u003c/li\u003e\n\u003cli\u003eบุคลากรทางการเเพทย์ (staff) สำหรับเก็บข้อมูลเกี่ยวกับบุคลากรทางการเเพทย์ภายในโรงพยาบาล โดยข้อมูลต่าง ๆ\u003c/li\u003e\n\u003cli\u003eประวัติการรักษา (record) สำหรับเก็บประวัติการรักษาที่คนไข้เข้ามารักษาภายในโรงพยาบาล\u003c/li\u003e\n\u003cli\u003eห้องพัก (room) สำหรับเก็บข้อมูลห้องพักที่อยู่ภายในโรงพยาบาล\u003c/li\u003e","point":300,"question":"สร้าง Relation ต่าง ๆ ตามความต้องการของระบบให้ถูกต้อง โดยไม่ต้องเชื่อมต่อความสัมพันธ์"},{"activity_id":41,"activity_type_id":6,"content_id":9,"activity_order":2,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงพยาบาลเเห่งหนึ่ง ซึ่งต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eคนไข้ (patient) สำหรับเก็บข้อมูลเกี่ยวกับคนไข้ที่เข้ามารักษาภายใน​โรงพยาบาล\u003c/li\u003e\n\u003cli\u003eบุคลากรทางการเเพทย์ (staff) สำหรับเก็บข้อมูลเกี่ยวกับบุคลากรทางการเเพทย์ภายในโรงพยาบาล โดยข้อมูลต่าง ๆ\u003c/li\u003e\n\u003cli\u003eประวัติการรักษา (record) สำหรับเก็บประวัติการรักษาที่คนไข้เข้ามารักษาภายในโรงพยาบาล\u003c/li\u003e\n\u003cli\u003eห้องพัก (room) สำหรับเก็บข้อมูลห้องพักที่อยู่ภายในโรงพยาบาล\u003c/li\u003e\nหลังจากที่นายแกนได้ออกแบบ Entity ต่าง ๆ ที่มีภายในโรงพยาบาลเเล้ว นายแกนพบความความสัมพันธ์ของ Entity ต่าง ๆ มีดังนี้\n\u003cli\u003eประวัติการรักษา จะเกิดขึ้นเมื่อมีคนไข้เข้ามารักษาในโรงพยาบาล โดยคนไข้หนึ่งคนสามารถเข้ามารักษาในโรงพยาบาลได้หลาบครั้ง ทำให้คนไข้หนึ่งคนอาจมีได้หลายประวัติการรักษา\u003c/li\u003e\n\u003cli\u003eประวัติการรักษา ต้องเก็บประวัติบุคลากรทางการเเพทย์ที่รับผิดชอบการรักษา โดยจะมีบุคลลาการเพียงท่านเดียวที่รับผิดชอบการรักษาหนึ่งครั้งของคนไข้\u003c/li\u003e\n\u003cli\u003eประวัติการรักษา ต้องเก็บข้อมูลห้องพักที่คนไข้เข้าพัก โดยการรักษาครึ่งครั้งสามารถเข้าพักได้เพียงห้องเดียว \u003c/li\u003e","point":300,"question":"จงเชื่อมต่อความสัมพันธ์ของ Relation เหล่านี้ให้ถูกต้องตามความต้องการของระบบ"},{"activity_id":42,"activity_type_id":6,"content_id":9,"activity_order":3,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงพยาบาลเเห่งหนึ่ง ซึ่งต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eคนไข้ (patient) สำหรับเก็บข้อมูลเกี่ยวกับคนไข้ที่เข้ามารักษาภายใน​โรงพยาบาล โดยข้อมูลต่าง ๆ มีดังนี้ \u003c/li\u003e\n\u003cli\u003eรหัสคนไข้ (patient_id) \u003c/li\u003e\n\u003cli\u003eชื่อจริง (first_name) \u003c/li\u003e\n\u003cli\u003eนามสกุล (last_name) \u003c/li\u003e\n\u003cli\u003eหมู่เลือด (blood_type) \u003c/li\u003e\n\u003cli\u003eเบอร์โทรติดต่อ (contact_tel) \u003c/li\u003e\nบุคลากรทางการเเพทย์ (staff) สำหรับเก็บข้อมูลเกี่ยวกับบุคลากรทางการเเพทย์ภายในโรงพยาบาล โดยข้อมูลต่าง ๆ โดยข้อมูลต่าง ๆ มีดังนี้\n\u003cli\u003eรหัสบุคลากร (staff_id)\u003c/li\u003e\n\u003cli\u003eชื่อจริง (first_name)\u003c/li\u003e\n\u003cli\u003eนามสกุล (last_name)\u003c/li\u003e\n\u003cli\u003eเบอร์โทรติดต่อ (contact_tel)\u003c/li\u003e\n\u003cli\u003eตำเเหน่ง (position)\u003c/li\u003e\nประวัติการรักษา (record) สำหรับเก็บประวัติการรักษาที่คนไข้เข้ามารักษาภายในโรงพยาบาล โดยข้อมูลต่าง ๆ มีดังนี้\n\u003cli\u003eรหัสคนไข้ (patient_id)\u003c/li\u003e\n\u003cli\u003eวัน-เวลาที่เข้ารักษา (creatd_at)\u003c/li\u003e\n\u003cli\u003eรหัสบุคลากรที่รับผิดชอบ (staff_id)\u003c/li\u003e\n\u003cli\u003eหมายเลขห้องพักที่เข้ารับการรักษา (room_no)\u003c/li\u003e\n\u003cli\u003eอาการ (description)\u003c/li\u003e\nห้องพัก (room) สำหรับเก็บข้อมูลห้องพักที่อยู่ภายในโรงพยาบาล โดยข้อมูลต่าง ๆ มีดังนี้\n\u003cli\u003eหมายเลขห้องพัก (room_no)\u003c/li\u003e\n\u003cli\u003eชื่อห้องพัก (room_name)\u003c/li\u003e","point":300,"question":"เพิ่ม Attribute ต่าง ๆ ใน Relation ให้ถูกต้องตามความต้องการของระบบ"},{"activity_id":10,"activity_type_id":6,"content_id":9,"activity_order":4,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมซึ่งมีหลายสาขา ซึ่งนายแกนต้องการสร้างฐานข้อมูล ที่สามารถใช้ในทุกโรงเเรม เเละสามารถติดต่อข้อมูลของทุกสาขาได้พร้อมกัน ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้ เช่น ข้อมูลติดต่อ (first_name, last_name) เเต้มสะสม (points) ซึ่งสามารถนำไปใช้เเลกส่วนลดได้ ประเภทผู้เข้าพัก (ลูกค้าทั่วไป, ลูกพิเศษ สามารถเพิ่มหรือเปลี่ยนชื่อได้)(CustomerType)(type_id, type_name) เป็นต้น\u003c/li\u003e\n\u003cli\u003eสาขา (Branch) เก็บข้อมูลเกี่ยวกับสาขาโรงเเรม เช่น รหัสสาข(branch_id) ชื่.อ(branch_name) ที่อยู่(branch_address) เบอร์ติดต่อ(contact_no) เว็ปไซต์(website) เป็นต้น\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี เช่น ชื่อ (room_name) เลขที่ห้อง(room_no) ชั้น(level) สาขา(branch) เป็นต้น\u003c/li\u003e\n\u003cli\u003eข้อมูลการเข้าพัก (Record) เก็บข้อมูลเกี่ยวกับการเข้าพักภายในโรงเเรมเเห่งนี้ เช่น ข้อมูลเเขกผู้เข้าพัก(guest_id) วันเวลาที่เข้าพัก(created_at) เเละข้อมูลห้อง(room_no) เป็นต้น\u003c/li\u003e\n\nโดยความสัมพันธ์ของ Entity ต่าง ๆ มีดังนี้\n\u003cli\u003eเมื่อแขกเข้าพักภายในโรงเเรม ข้อมูลจะถูกเก็บใน ข้อมูลการเข้าพัก แขกหนึ่งคนสามารถเข้าพักภายในโรงเเรมได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยเเขกที่จองเพียงคนเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eการเข้าพักจะเก็บข้อมูลห้องพักที่เข้าพัก โดยหนึ่งห้องพักสามารถเข้าพักได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยห้องพักเพียงห้องเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eในเเต่ละสาขาสามารถห้องพักได้หลายห้องพัก เเละห้องพักจะเป็นของสาขานั้น ๆ เพียงสาขาเดียวเท่านั้น\u003c/li\u003e","point":300,"question":"ออกแบบฐานข้อมูลจากความต้องการของระบบให้ถูกต้อง"},{"activity_id":44,"activity_type_id":6,"content_id":9,"activity_order":5,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมซึ่งมีหลายสาขา ซึ่งนายแกนต้องการสร้างฐานข้อมูล ที่สามารถใช้ในทุกโรงเเรม เเละสามารถติดต่อข้อมูลของทุกสาขาได้พร้อมกัน ซึ่งในต้องการเก็บข้อมูลดังนี้\r\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้ เช่น ข้อมูลติดต่อ เเต้มสะสม ซึ่งสามารถนำไปใช้เเลกส่วนลดได้ ประเภทผู้เข้าพัก (ลูกค้าทั่วไป, ลูกพิเศษ สามารถเพิ่มหรือเปลี่ยนชื่อได้) เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eสาขา (Branch) เก็บข้อมูลเกี่ยวกับสาขาโรงเเรม เช่น ชื่อ ที่อยู่ เบอร์ติดต่อ เว็ปไซต์ เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี เช่น ชื่อ เลขที่ห้อง ชั้น สาขา เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eข้อมูลการเข้าพัก (Record) เก็บข้อมูลเกี่ยวกับการเข้าพักภายในโรงเเรมเเห่งนี้ เช่น ข้อมูลเเขกผู้เข้าพัก วันเวลาที่เข้าพัก เเละข้อมูลห้อง เป็นต้น\u003c/li\u003e\r\n\r\nโดยความสัมพันธ์ของ Entity ต่าง ๆ มีดังนี้\r\n\u003cli\u003eเมื่อแขกเข้าพักภายในโรงเเรม ข้อมูลจะถูกเก็บใน ข้อมูลการเข้าพัก แขกหนึ่งคนสามารถเข้าพักภายในโรงเเรมได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยเเขกที่จองเพียงคนเดียวเท่านั้น\u003c/li\u003e\r\n\u003cli\u003eการเข้าพักจะเก็บข้อมูลห้องพักที่เข้าพัก โดยหนึ่งห้องพักสามารถเข้าพักได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยห้องพักเพียงห้องเดียวเท่านั้น\u003c/li\u003e\r\n\u003cli\u003eในเเต่ละสาขาสามารถห้องพักได้หลายห้องพัก เเละห้องพักจะเป็นของสาขานั้น ๆ เพียงสาขาเดียวเท่านั้น\u003c/li\u003e","point":300,"question":"จงพิจารณาการออกแบบฐานข้อมูลนี้ เเล้วเลือกคำตอบที่ถูกต้อง"}] +$4 +PXAT +$13 +1644718502426 +*2 +$3 +DEL +$33 +learningRepository::GetContent::9 +*2 +$3 +DEL +$41 +learningRepository::GetContentActivity::9 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$4140 +{"activity_id":10,"activity_type_id":6,"content_id":9,"activity_order":4,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมซึ่งมีหลายสาขา ซึ่งนายแกนต้องการสร้างฐานข้อมูล ที่สามารถใช้ในทุกโรงเเรม เเละสามารถติดต่อข้อมูลของทุกสาขาได้พร้อมกัน ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้ เช่น ข้อมูลติดต่อ (first_name, last_name) เเต้มสะสม (points) ซึ่งสามารถนำไปใช้เเลกส่วนลดได้ ประเภทผู้เข้าพัก (CustomerType) (ลูกค้าทั่วไป, ลูกพิเศษ) สามารถเพิ่มหรือเปลี่ยนชื่อได้ (type_id, type_name) เป็นต้น\u003c/li\u003e\n\u003cli\u003eสาขา (Branch) เก็บข้อมูลเกี่ยวกับสาขาโรงเเรม เช่น รหัสสาข(branch_id) ชื่.อ(branch_name) ที่อยู่(branch_address) เบอร์ติดต่อ(contact_no) เว็ปไซต์(website) เป็นต้น\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี เช่น ชื่อ (room_name) เลขที่ห้อง(room_no) ชั้น(level) สาขา(branch) เป็นต้น\u003c/li\u003e\n\u003cli\u003eข้อมูลการเข้าพัก (Record) เก็บข้อมูลเกี่ยวกับการเข้าพักภายในโรงเเรมเเห่งนี้ เช่น ข้อมูลเเขกผู้เข้าพัก(guest_id) วันเวลาที่เข้าพัก(created_at) ข้อมูลห้อง(room_no) และรหัสการเข้าพัก(record_id)เป็นต้น\u003c/li\u003e\n\nโดยความสัมพันธ์ของ Entity ต่าง ๆ มีดังนี้\n\u003cli\u003eเมื่อแขกเข้าพักภายในโรงเเรม ข้อมูลจะถูกเก็บใน ข้อมูลการเข้าพัก แขกหนึ่งคนสามารถเข้าพักภายในโรงเเรมได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยเเขกที่จองเพียงคนเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eการเข้าพักจะเก็บข้อมูลห้องพักที่เข้าพัก โดยหนึ่งห้องพักสามารถเข้าพักได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยห้องพักเพียงห้องเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eในเเต่ละสาขาสามารถห้องพักได้หลายห้องพัก เเละห้องพักจะเป็นของสาขานั้น ๆ เพียงสาขาเดียวเท่านั้น\u003c/li\u003e","point":300,"question":"ออกแบบฐานข้อมูลจากความต้องการของระบบให้ถูกต้อง"} +$4 +PXAT +$13 +1644718894916 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$4140 +{"activity_id":10,"activity_type_id":6,"content_id":9,"activity_order":4,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมซึ่งมีหลายสาขา ซึ่งนายแกนต้องการสร้างฐานข้อมูล ที่สามารถใช้ในทุกโรงเเรม เเละสามารถติดต่อข้อมูลของทุกสาขาได้พร้อมกัน ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้ เช่น ข้อมูลติดต่อ (first_name, last_name) เเต้มสะสม (points) ซึ่งสามารถนำไปใช้เเลกส่วนลดได้ ประเภทผู้เข้าพัก (CustomerType) (ลูกค้าทั่วไป, ลูกพิเศษ) สามารถเพิ่มหรือเปลี่ยนชื่อได้ (type_id, type_name) เป็นต้น\u003c/li\u003e\n\u003cli\u003eสาขา (Branch) เก็บข้อมูลเกี่ยวกับสาขาโรงเเรม เช่น รหัสสาข(branch_id) ชื่.อ(branch_name) ที่อยู่(branch_address) เบอร์ติดต่อ(contact_no) เว็ปไซต์(website) เป็นต้น\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี เช่น ชื่อ (room_name) เลขที่ห้อง(room_no) ชั้น(level) สาขา(branch) เป็นต้น\u003c/li\u003e\n\u003cli\u003eข้อมูลการเข้าพัก (Record) เก็บข้อมูลเกี่ยวกับการเข้าพักภายในโรงเเรมเเห่งนี้ เช่น ข้อมูลเเขกผู้เข้าพัก(guest_id) วันเวลาที่เข้าพัก(created_at) ข้อมูลห้อง(room_no) และรหัสการเข้าพัก(record_id)เป็นต้น\u003c/li\u003e\n\nโดยความสัมพันธ์ของ Entity ต่าง ๆ มีดังนี้\n\u003cli\u003eเมื่อแขกเข้าพักภายในโรงเเรม ข้อมูลจะถูกเก็บใน ข้อมูลการเข้าพัก แขกหนึ่งคนสามารถเข้าพักภายในโรงเเรมได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยเเขกที่จองเพียงคนเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eการเข้าพักจะเก็บข้อมูลห้องพักที่เข้าพัก โดยหนึ่งห้องพักสามารถเข้าพักได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยห้องพักเพียงห้องเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eในเเต่ละสาขาสามารถห้องพักได้หลายห้องพัก เเละห้องพักจะเป็นของสาขานั้น ๆ เพียงสาขาเดียวเท่านั้น\u003c/li\u003e","point":300,"question":"ออกแบบฐานข้อมูลจากความต้องการของระบบให้ถูกต้อง"} +$4 +PXAT +$13 +1644718965486 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$4140 +{"activity_id":10,"activity_type_id":6,"content_id":9,"activity_order":4,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมซึ่งมีหลายสาขา ซึ่งนายแกนต้องการสร้างฐานข้อมูล ที่สามารถใช้ในทุกโรงเเรม เเละสามารถติดต่อข้อมูลของทุกสาขาได้พร้อมกัน ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้ เช่น ข้อมูลติดต่อ (first_name, last_name) เเต้มสะสม (points) ซึ่งสามารถนำไปใช้เเลกส่วนลดได้ ประเภทผู้เข้าพัก (CustomerType) (ลูกค้าทั่วไป, ลูกพิเศษ) สามารถเพิ่มหรือเปลี่ยนชื่อได้ (type_id, type_name) เป็นต้น\u003c/li\u003e\n\u003cli\u003eสาขา (Branch) เก็บข้อมูลเกี่ยวกับสาขาโรงเเรม เช่น รหัสสาข(branch_id) ชื่.อ(branch_name) ที่อยู่(branch_address) เบอร์ติดต่อ(contact_no) เว็ปไซต์(website) เป็นต้น\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี เช่น ชื่อ (room_name) เลขที่ห้อง(room_no) ชั้น(level) สาขา(branch) เป็นต้น\u003c/li\u003e\n\u003cli\u003eข้อมูลการเข้าพัก (Record) เก็บข้อมูลเกี่ยวกับการเข้าพักภายในโรงเเรมเเห่งนี้ เช่น ข้อมูลเเขกผู้เข้าพัก(guest_id) วันเวลาที่เข้าพัก(created_at) ข้อมูลห้อง(room_no) และรหัสการเข้าพัก(record_id)เป็นต้น\u003c/li\u003e\n\nโดยความสัมพันธ์ของ Entity ต่าง ๆ มีดังนี้\n\u003cli\u003eเมื่อแขกเข้าพักภายในโรงเเรม ข้อมูลจะถูกเก็บใน ข้อมูลการเข้าพัก แขกหนึ่งคนสามารถเข้าพักภายในโรงเเรมได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยเเขกที่จองเพียงคนเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eการเข้าพักจะเก็บข้อมูลห้องพักที่เข้าพัก โดยหนึ่งห้องพักสามารถเข้าพักได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยห้องพักเพียงห้องเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eในเเต่ละสาขาสามารถห้องพักได้หลายห้องพัก เเละห้องพักจะเป็นของสาขานั้น ๆ เพียงสาขาเดียวเท่านั้น\u003c/li\u003e","point":300,"question":"ออกแบบฐานข้อมูลจากความต้องการของระบบให้ถูกต้อง"} +$4 +PXAT +$13 +1644719057320 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$4140 +{"activity_id":10,"activity_type_id":6,"content_id":9,"activity_order":4,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมซึ่งมีหลายสาขา ซึ่งนายแกนต้องการสร้างฐานข้อมูล ที่สามารถใช้ในทุกโรงเเรม เเละสามารถติดต่อข้อมูลของทุกสาขาได้พร้อมกัน ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้ เช่น ข้อมูลติดต่อ (first_name, last_name) เเต้มสะสม (points) ซึ่งสามารถนำไปใช้เเลกส่วนลดได้ ประเภทผู้เข้าพัก (CustomerType) (ลูกค้าทั่วไป, ลูกพิเศษ) สามารถเพิ่มหรือเปลี่ยนชื่อได้ (type_id, type_name) เป็นต้น\u003c/li\u003e\n\u003cli\u003eสาขา (Branch) เก็บข้อมูลเกี่ยวกับสาขาโรงเเรม เช่น รหัสสาข(branch_id) ชื่.อ(branch_name) ที่อยู่(branch_address) เบอร์ติดต่อ(contact_no) เว็ปไซต์(website) เป็นต้น\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี เช่น ชื่อ (room_name) เลขที่ห้อง(room_no) ชั้น(level) สาขา(branch) เป็นต้น\u003c/li\u003e\n\u003cli\u003eข้อมูลการเข้าพัก (Record) เก็บข้อมูลเกี่ยวกับการเข้าพักภายในโรงเเรมเเห่งนี้ เช่น ข้อมูลเเขกผู้เข้าพัก(guest_id) วันเวลาที่เข้าพัก(created_at) ข้อมูลห้อง(room_no) และรหัสการเข้าพัก(record_id)เป็นต้น\u003c/li\u003e\n\nโดยความสัมพันธ์ของ Entity ต่าง ๆ มีดังนี้\n\u003cli\u003eเมื่อแขกเข้าพักภายในโรงเเรม ข้อมูลจะถูกเก็บใน ข้อมูลการเข้าพัก แขกหนึ่งคนสามารถเข้าพักภายในโรงเเรมได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยเเขกที่จองเพียงคนเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eการเข้าพักจะเก็บข้อมูลห้องพักที่เข้าพัก โดยหนึ่งห้องพักสามารถเข้าพักได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยห้องพักเพียงห้องเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eในเเต่ละสาขาสามารถห้องพักได้หลายห้องพัก เเละห้องพักจะเป็นของสาขานั้น ๆ เพียงสาขาเดียวเท่านั้น\u003c/li\u003e","point":300,"question":"ออกแบบฐานข้อมูลจากความต้องการของระบบให้ถูกต้อง"} +$4 +PXAT +$13 +1644719123930 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$4140 +{"activity_id":10,"activity_type_id":6,"content_id":9,"activity_order":4,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมซึ่งมีหลายสาขา ซึ่งนายแกนต้องการสร้างฐานข้อมูล ที่สามารถใช้ในทุกโรงเเรม เเละสามารถติดต่อข้อมูลของทุกสาขาได้พร้อมกัน ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้ เช่น ข้อมูลติดต่อ (first_name, last_name) เเต้มสะสม (points) ซึ่งสามารถนำไปใช้เเลกส่วนลดได้ ประเภทผู้เข้าพัก (CustomerType) (ลูกค้าทั่วไป, ลูกพิเศษ) สามารถเพิ่มหรือเปลี่ยนชื่อได้ (type_id, type_name) เป็นต้น\u003c/li\u003e\n\u003cli\u003eสาขา (Branch) เก็บข้อมูลเกี่ยวกับสาขาโรงเเรม เช่น รหัสสาข(branch_id) ชื่.อ(branch_name) ที่อยู่(branch_address) เบอร์ติดต่อ(contact_no) เว็ปไซต์(website) เป็นต้น\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี เช่น ชื่อ (room_name) เลขที่ห้อง(room_no) ชั้น(level) สาขา(branch) เป็นต้น\u003c/li\u003e\n\u003cli\u003eข้อมูลการเข้าพัก (Record) เก็บข้อมูลเกี่ยวกับการเข้าพักภายในโรงเเรมเเห่งนี้ เช่น ข้อมูลเเขกผู้เข้าพัก(guest_id) วันเวลาที่เข้าพัก(created_at) ข้อมูลห้อง(room_no) และรหัสการเข้าพัก(record_id)เป็นต้น\u003c/li\u003e\n\nโดยความสัมพันธ์ของ Entity ต่าง ๆ มีดังนี้\n\u003cli\u003eเมื่อแขกเข้าพักภายในโรงเเรม ข้อมูลจะถูกเก็บใน ข้อมูลการเข้าพัก แขกหนึ่งคนสามารถเข้าพักภายในโรงเเรมได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยเเขกที่จองเพียงคนเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eการเข้าพักจะเก็บข้อมูลห้องพักที่เข้าพัก โดยหนึ่งห้องพักสามารถเข้าพักได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยห้องพักเพียงห้องเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eในเเต่ละสาขาสามารถห้องพักได้หลายห้องพัก เเละห้องพักจะเป็นของสาขานั้น ๆ เพียงสาขาเดียวเท่านั้น\u003c/li\u003e","point":300,"question":"ออกแบบฐานข้อมูลจากความต้องการของระบบให้ถูกต้อง"} +$4 +PXAT +$13 +1644719753212 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$4140 +{"activity_id":10,"activity_type_id":6,"content_id":9,"activity_order":4,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมซึ่งมีหลายสาขา ซึ่งนายแกนต้องการสร้างฐานข้อมูล ที่สามารถใช้ในทุกโรงเเรม เเละสามารถติดต่อข้อมูลของทุกสาขาได้พร้อมกัน ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้ เช่น ข้อมูลติดต่อ (first_name, last_name) เเต้มสะสม (points) ซึ่งสามารถนำไปใช้เเลกส่วนลดได้ ประเภทผู้เข้าพัก (CustomerType) (ลูกค้าทั่วไป, ลูกพิเศษ) สามารถเพิ่มหรือเปลี่ยนชื่อได้ (type_id, type_name) เป็นต้น\u003c/li\u003e\n\u003cli\u003eสาขา (Branch) เก็บข้อมูลเกี่ยวกับสาขาโรงเเรม เช่น รหัสสาข(branch_id) ชื่.อ(branch_name) ที่อยู่(branch_address) เบอร์ติดต่อ(contact_no) เว็ปไซต์(website) เป็นต้น\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี เช่น ชื่อ (room_name) เลขที่ห้อง(room_no) ชั้น(level) สาขา(branch) เป็นต้น\u003c/li\u003e\n\u003cli\u003eข้อมูลการเข้าพัก (Record) เก็บข้อมูลเกี่ยวกับการเข้าพักภายในโรงเเรมเเห่งนี้ เช่น ข้อมูลเเขกผู้เข้าพัก(guest_id) วันเวลาที่เข้าพัก(created_at) ข้อมูลห้อง(room_no) และรหัสการเข้าพัก(record_id)เป็นต้น\u003c/li\u003e\n\nโดยความสัมพันธ์ของ Entity ต่าง ๆ มีดังนี้\n\u003cli\u003eเมื่อแขกเข้าพักภายในโรงเเรม ข้อมูลจะถูกเก็บใน ข้อมูลการเข้าพัก แขกหนึ่งคนสามารถเข้าพักภายในโรงเเรมได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยเเขกที่จองเพียงคนเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eการเข้าพักจะเก็บข้อมูลห้องพักที่เข้าพัก โดยหนึ่งห้องพักสามารถเข้าพักได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยห้องพักเพียงห้องเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eในเเต่ละสาขาสามารถห้องพักได้หลายห้องพัก เเละห้องพักจะเป็นของสาขานั้น ๆ เพียงสาขาเดียวเท่านั้น\u003c/li\u003e","point":300,"question":"ออกแบบฐานข้อมูลจากความต้องการของระบบให้ถูกต้อง"} +$4 +PXAT +$13 +1644719963190 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$4140 +{"activity_id":10,"activity_type_id":6,"content_id":9,"activity_order":4,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมซึ่งมีหลายสาขา ซึ่งนายแกนต้องการสร้างฐานข้อมูล ที่สามารถใช้ในทุกโรงเเรม เเละสามารถติดต่อข้อมูลของทุกสาขาได้พร้อมกัน ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้ เช่น ข้อมูลติดต่อ (first_name, last_name) เเต้มสะสม (points) ซึ่งสามารถนำไปใช้เเลกส่วนลดได้ ประเภทผู้เข้าพัก (CustomerType) (ลูกค้าทั่วไป, ลูกพิเศษ) สามารถเพิ่มหรือเปลี่ยนชื่อได้ (type_id, type_name) เป็นต้น\u003c/li\u003e\n\u003cli\u003eสาขา (Branch) เก็บข้อมูลเกี่ยวกับสาขาโรงเเรม เช่น รหัสสาข(branch_id) ชื่.อ(branch_name) ที่อยู่(branch_address) เบอร์ติดต่อ(contact_no) เว็ปไซต์(website) เป็นต้น\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี เช่น ชื่อ (room_name) เลขที่ห้อง(room_no) ชั้น(level) สาขา(branch) เป็นต้น\u003c/li\u003e\n\u003cli\u003eข้อมูลการเข้าพัก (Record) เก็บข้อมูลเกี่ยวกับการเข้าพักภายในโรงเเรมเเห่งนี้ เช่น ข้อมูลเเขกผู้เข้าพัก(guest_id) วันเวลาที่เข้าพัก(created_at) ข้อมูลห้อง(room_no) และรหัสการเข้าพัก(record_id)เป็นต้น\u003c/li\u003e\n\nโดยความสัมพันธ์ของ Entity ต่าง ๆ มีดังนี้\n\u003cli\u003eเมื่อแขกเข้าพักภายในโรงเเรม ข้อมูลจะถูกเก็บใน ข้อมูลการเข้าพัก แขกหนึ่งคนสามารถเข้าพักภายในโรงเเรมได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยเเขกที่จองเพียงคนเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eการเข้าพักจะเก็บข้อมูลห้องพักที่เข้าพัก โดยหนึ่งห้องพักสามารถเข้าพักได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยห้องพักเพียงห้องเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eในเเต่ละสาขาสามารถห้องพักได้หลายห้องพัก เเละห้องพักจะเป็นของสาขานั้น ๆ เพียงสาขาเดียวเท่านั้น\u003c/li\u003e","point":300,"question":"ออกแบบฐานข้อมูลจากความต้องการของระบบให้ถูกต้อง"} +$4 +PXAT +$13 +1644720007513 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::10 +$2 +[] +$4 +PXAT +$13 +1644720346923 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$4140 +{"activity_id":10,"activity_type_id":6,"content_id":9,"activity_order":4,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมซึ่งมีหลายสาขา ซึ่งนายแกนต้องการสร้างฐานข้อมูล ที่สามารถใช้ในทุกโรงเเรม เเละสามารถติดต่อข้อมูลของทุกสาขาได้พร้อมกัน ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้ เช่น ข้อมูลติดต่อ (first_name, last_name) เเต้มสะสม (points) ซึ่งสามารถนำไปใช้เเลกส่วนลดได้ ประเภทผู้เข้าพัก (CustomerType) (ลูกค้าทั่วไป, ลูกพิเศษ) สามารถเพิ่มหรือเปลี่ยนชื่อได้ (type_id, type_name) เป็นต้น\u003c/li\u003e\n\u003cli\u003eสาขา (Branch) เก็บข้อมูลเกี่ยวกับสาขาโรงเเรม เช่น รหัสสาข(branch_id) ชื่.อ(branch_name) ที่อยู่(branch_address) เบอร์ติดต่อ(contact_no) เว็ปไซต์(website) เป็นต้น\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี เช่น ชื่อ (room_name) เลขที่ห้อง(room_no) ชั้น(level) สาขา(branch) เป็นต้น\u003c/li\u003e\n\u003cli\u003eข้อมูลการเข้าพัก (Record) เก็บข้อมูลเกี่ยวกับการเข้าพักภายในโรงเเรมเเห่งนี้ เช่น ข้อมูลเเขกผู้เข้าพัก(guest_id) วันเวลาที่เข้าพัก(created_at) ข้อมูลห้อง(room_no) และรหัสการเข้าพัก(record_id)เป็นต้น\u003c/li\u003e\n\nโดยความสัมพันธ์ของ Entity ต่าง ๆ มีดังนี้\n\u003cli\u003eเมื่อแขกเข้าพักภายในโรงเเรม ข้อมูลจะถูกเก็บใน ข้อมูลการเข้าพัก แขกหนึ่งคนสามารถเข้าพักภายในโรงเเรมได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยเเขกที่จองเพียงคนเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eการเข้าพักจะเก็บข้อมูลห้องพักที่เข้าพัก โดยหนึ่งห้องพักสามารถเข้าพักได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยห้องพักเพียงห้องเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eในเเต่ละสาขาสามารถห้องพักได้หลายห้องพัก เเละห้องพักจะเป็นของสาขานั้น ๆ เพียงสาขาเดียวเท่านั้น\u003c/li\u003e","point":300,"question":"ออกแบบฐานข้อมูลจากความต้องการของระบบให้ถูกต้อง"} +$4 +PXAT +$13 +1644720346924 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::10 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$4140 +{"activity_id":10,"activity_type_id":6,"content_id":9,"activity_order":4,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมซึ่งมีหลายสาขา ซึ่งนายแกนต้องการสร้างฐานข้อมูล ที่สามารถใช้ในทุกโรงเเรม เเละสามารถติดต่อข้อมูลของทุกสาขาได้พร้อมกัน ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้ เช่น ข้อมูลติดต่อ (first_name, last_name) เเต้มสะสม (points) ซึ่งสามารถนำไปใช้เเลกส่วนลดได้ ประเภทผู้เข้าพัก (CustomerType) (ลูกค้าทั่วไป, ลูกพิเศษ) สามารถเพิ่มหรือเปลี่ยนชื่อได้ (type_id, type_name) เป็นต้น\u003c/li\u003e\n\u003cli\u003eสาขา (Branch) เก็บข้อมูลเกี่ยวกับสาขาโรงเเรม เช่น รหัสสาข(branch_id) ชื่.อ(branch_name) ที่อยู่(branch_address) เบอร์ติดต่อ(contact_no) เว็ปไซต์(website) เป็นต้น\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี เช่น ชื่อ (room_name) เลขที่ห้อง(room_no) ชั้น(level) สาขา(branch) เป็นต้น\u003c/li\u003e\n\u003cli\u003eข้อมูลการเข้าพัก (Record) เก็บข้อมูลเกี่ยวกับการเข้าพักภายในโรงเเรมเเห่งนี้ เช่น ข้อมูลเเขกผู้เข้าพัก(guest_id) วันเวลาที่เข้าพัก(created_at) ข้อมูลห้อง(room_no) และรหัสการเข้าพัก(record_id)เป็นต้น\u003c/li\u003e\n\nโดยความสัมพันธ์ของ Entity ต่าง ๆ มีดังนี้\n\u003cli\u003eเมื่อแขกเข้าพักภายในโรงเเรม ข้อมูลจะถูกเก็บใน ข้อมูลการเข้าพัก แขกหนึ่งคนสามารถเข้าพักภายในโรงเเรมได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยเเขกที่จองเพียงคนเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eการเข้าพักจะเก็บข้อมูลห้องพักที่เข้าพัก โดยหนึ่งห้องพักสามารถเข้าพักได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยห้องพักเพียงห้องเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eในเเต่ละสาขาสามารถห้องพักได้หลายห้องพัก เเละห้องพักจะเป็นของสาขานั้น ๆ เพียงสาขาเดียวเท่านั้น\u003c/li\u003e","point":300,"question":"ออกแบบฐานข้อมูลจากความต้องการของระบบให้ถูกต้อง"} +$4 +PXAT +$13 +1644720413083 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::10 +$2 +[] +$4 +PXAT +$13 +1644720413093 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::10 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::10 +$2 +[] +$4 +PXAT +$13 +1644720448750 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$4140 +{"activity_id":10,"activity_type_id":6,"content_id":9,"activity_order":4,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมซึ่งมีหลายสาขา ซึ่งนายแกนต้องการสร้างฐานข้อมูล ที่สามารถใช้ในทุกโรงเเรม เเละสามารถติดต่อข้อมูลของทุกสาขาได้พร้อมกัน ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้ เช่น ข้อมูลติดต่อ (first_name, last_name) เเต้มสะสม (points) ซึ่งสามารถนำไปใช้เเลกส่วนลดได้ ประเภทผู้เข้าพัก (CustomerType) (ลูกค้าทั่วไป, ลูกพิเศษ) สามารถเพิ่มหรือเปลี่ยนชื่อได้ (type_id, type_name) เป็นต้น\u003c/li\u003e\n\u003cli\u003eสาขา (Branch) เก็บข้อมูลเกี่ยวกับสาขาโรงเเรม เช่น รหัสสาข(branch_id) ชื่.อ(branch_name) ที่อยู่(branch_address) เบอร์ติดต่อ(contact_no) เว็ปไซต์(website) เป็นต้น\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี เช่น ชื่อ (room_name) เลขที่ห้อง(room_no) ชั้น(level) สาขา(branch) เป็นต้น\u003c/li\u003e\n\u003cli\u003eข้อมูลการเข้าพัก (Record) เก็บข้อมูลเกี่ยวกับการเข้าพักภายในโรงเเรมเเห่งนี้ เช่น ข้อมูลเเขกผู้เข้าพัก(guest_id) วันเวลาที่เข้าพัก(created_at) ข้อมูลห้อง(room_no) และรหัสการเข้าพัก(record_id)เป็นต้น\u003c/li\u003e\n\nโดยความสัมพันธ์ของ Entity ต่าง ๆ มีดังนี้\n\u003cli\u003eเมื่อแขกเข้าพักภายในโรงเเรม ข้อมูลจะถูกเก็บใน ข้อมูลการเข้าพัก แขกหนึ่งคนสามารถเข้าพักภายในโรงเเรมได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยเเขกที่จองเพียงคนเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eการเข้าพักจะเก็บข้อมูลห้องพักที่เข้าพัก โดยหนึ่งห้องพักสามารถเข้าพักได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยห้องพักเพียงห้องเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eในเเต่ละสาขาสามารถห้องพักได้หลายห้องพัก เเละห้องพักจะเป็นของสาขานั้น ๆ เพียงสาขาเดียวเท่านั้น\u003c/li\u003e","point":300,"question":"ออกแบบฐานข้อมูลจากความต้องการของระบบให้ถูกต้อง"} +$4 +PXAT +$13 +1644720448773 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::10 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$4140 +{"activity_id":10,"activity_type_id":6,"content_id":9,"activity_order":4,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมซึ่งมีหลายสาขา ซึ่งนายแกนต้องการสร้างฐานข้อมูล ที่สามารถใช้ในทุกโรงเเรม เเละสามารถติดต่อข้อมูลของทุกสาขาได้พร้อมกัน ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้ เช่น ข้อมูลติดต่อ (first_name, last_name) เเต้มสะสม (points) ซึ่งสามารถนำไปใช้เเลกส่วนลดได้ ประเภทผู้เข้าพัก (CustomerType) (ลูกค้าทั่วไป, ลูกพิเศษ) สามารถเพิ่มหรือเปลี่ยนชื่อได้ (type_id, type_name) เป็นต้น\u003c/li\u003e\n\u003cli\u003eสาขา (Branch) เก็บข้อมูลเกี่ยวกับสาขาโรงเเรม เช่น รหัสสาข(branch_id) ชื่.อ(branch_name) ที่อยู่(branch_address) เบอร์ติดต่อ(contact_no) เว็ปไซต์(website) เป็นต้น\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี เช่น ชื่อ (room_name) เลขที่ห้อง(room_no) ชั้น(level) สาขา(branch) เป็นต้น\u003c/li\u003e\n\u003cli\u003eข้อมูลการเข้าพัก (Record) เก็บข้อมูลเกี่ยวกับการเข้าพักภายในโรงเเรมเเห่งนี้ เช่น ข้อมูลเเขกผู้เข้าพัก(guest_id) วันเวลาที่เข้าพัก(created_at) ข้อมูลห้อง(room_no) และรหัสการเข้าพัก(record_id)เป็นต้น\u003c/li\u003e\n\nโดยความสัมพันธ์ของ Entity ต่าง ๆ มีดังนี้\n\u003cli\u003eเมื่อแขกเข้าพักภายในโรงเเรม ข้อมูลจะถูกเก็บใน ข้อมูลการเข้าพัก แขกหนึ่งคนสามารถเข้าพักภายในโรงเเรมได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยเเขกที่จองเพียงคนเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eการเข้าพักจะเก็บข้อมูลห้องพักที่เข้าพัก โดยหนึ่งห้องพักสามารถเข้าพักได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยห้องพักเพียงห้องเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eในเเต่ละสาขาสามารถห้องพักได้หลายห้องพัก เเละห้องพักจะเป็นของสาขานั้น ๆ เพียงสาขาเดียวเท่านั้น\u003c/li\u003e","point":300,"question":"ออกแบบฐานข้อมูลจากความต้องการของระบบให้ถูกต้อง"} +$4 +PXAT +$13 +1644720573975 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::10 +$2 +[] +$4 +PXAT +$13 +1644720573999 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::10 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::12 +$2 +[] +$4 +PXAT +$13 +1644720969102 +*5 +$3 +SET +$35 +learningRepository::GetActivity::12 +$3916 +{"activity_id":12,"activity_type_id":7,"content_id":9,"activity_order":5,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมซึ่งมีหลายสาขา ซึ่งนายแกนต้องการสร้างฐานข้อมูล ที่สามารถใช้ในทุกโรงเเรม เเละสามารถติดต่อข้อมูลของทุกสาขาได้พร้อมกัน ซึ่งในต้องการเก็บข้อมูลดังนี้\r\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้ เช่น ข้อมูลติดต่อ เเต้มสะสม ซึ่งสามารถนำไปใช้เเลกส่วนลดได้ ประเภทผู้เข้าพัก (ลูกค้าทั่วไป, ลูกพิเศษ สามารถเพิ่มหรือเปลี่ยนชื่อได้) เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eสาขา (Branch) เก็บข้อมูลเกี่ยวกับสาขาโรงเเรม เช่น ชื่อ ที่อยู่ เบอร์ติดต่อ เว็ปไซต์ เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี เช่น ชื่อ เลขที่ห้อง ชั้น สาขา เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eข้อมูลการเข้าพัก (Record) เก็บข้อมูลเกี่ยวกับการเข้าพักภายในโรงเเรมเเห่งนี้ เช่น ข้อมูลเเขกผู้เข้าพัก วันเวลาที่เข้าพัก เเละข้อมูลห้อง เป็นต้น\u003c/li\u003e\r\n\r\nโดยความสัมพันธ์ของ Entity ต่าง ๆ มีดังนี้\r\n\u003cli\u003eเมื่อแขกเข้าพักภายในโรงเเรม ข้อมูลจะถูกเก็บใน ข้อมูลการเข้าพัก แขกหนึ่งคนสามารถเข้าพักภายในโรงเเรมได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยเเขกที่จองเพียงคนเดียวเท่านั้น\u003c/li\u003e\r\n\u003cli\u003eการเข้าพักจะเก็บข้อมูลห้องพักที่เข้าพัก โดยหนึ่งห้องพักสามารถเข้าพักได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยห้องพักเพียงห้องเดียวเท่านั้น\u003c/li\u003e\r\n\u003cli\u003eในเเต่ละสาขาสามารถห้องพักได้หลายห้องพัก เเละห้องพักจะเป็นของสาขานั้น ๆ เพียงสาขาเดียวเท่านั้น\u003c/li\u003e","point":300,"question":"จงพิจารณาการออกแบบฐานข้อมูลนี้ เเล้วเลือกคำตอบที่ถูกต้อง"} +$4 +PXAT +$13 +1644720969104 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::12 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::12 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::12 +$2 +[] +$4 +PXAT +$13 +1644721014767 +*5 +$3 +SET +$35 +learningRepository::GetActivity::12 +$3916 +{"activity_id":12,"activity_type_id":7,"content_id":9,"activity_order":5,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมซึ่งมีหลายสาขา ซึ่งนายแกนต้องการสร้างฐานข้อมูล ที่สามารถใช้ในทุกโรงเเรม เเละสามารถติดต่อข้อมูลของทุกสาขาได้พร้อมกัน ซึ่งในต้องการเก็บข้อมูลดังนี้\r\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้ เช่น ข้อมูลติดต่อ เเต้มสะสม ซึ่งสามารถนำไปใช้เเลกส่วนลดได้ ประเภทผู้เข้าพัก (ลูกค้าทั่วไป, ลูกพิเศษ สามารถเพิ่มหรือเปลี่ยนชื่อได้) เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eสาขา (Branch) เก็บข้อมูลเกี่ยวกับสาขาโรงเเรม เช่น ชื่อ ที่อยู่ เบอร์ติดต่อ เว็ปไซต์ เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี เช่น ชื่อ เลขที่ห้อง ชั้น สาขา เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eข้อมูลการเข้าพัก (Record) เก็บข้อมูลเกี่ยวกับการเข้าพักภายในโรงเเรมเเห่งนี้ เช่น ข้อมูลเเขกผู้เข้าพัก วันเวลาที่เข้าพัก เเละข้อมูลห้อง เป็นต้น\u003c/li\u003e\r\n\r\nโดยความสัมพันธ์ของ Entity ต่าง ๆ มีดังนี้\r\n\u003cli\u003eเมื่อแขกเข้าพักภายในโรงเเรม ข้อมูลจะถูกเก็บใน ข้อมูลการเข้าพัก แขกหนึ่งคนสามารถเข้าพักภายในโรงเเรมได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยเเขกที่จองเพียงคนเดียวเท่านั้น\u003c/li\u003e\r\n\u003cli\u003eการเข้าพักจะเก็บข้อมูลห้องพักที่เข้าพัก โดยหนึ่งห้องพักสามารถเข้าพักได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยห้องพักเพียงห้องเดียวเท่านั้น\u003c/li\u003e\r\n\u003cli\u003eในเเต่ละสาขาสามารถห้องพักได้หลายห้องพัก เเละห้องพักจะเป็นของสาขานั้น ๆ เพียงสาขาเดียวเท่านั้น\u003c/li\u003e","point":300,"question":"จงพิจารณาการออกแบบฐานข้อมูลนี้ เเล้วเลือกคำตอบที่ถูกต้อง"} +$4 +PXAT +$13 +1644721014768 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::12 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::12 +*5 +$3 +SET +$35 +learningRepository::GetActivity::12 +$3916 +{"activity_id":12,"activity_type_id":7,"content_id":9,"activity_order":5,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมซึ่งมีหลายสาขา ซึ่งนายแกนต้องการสร้างฐานข้อมูล ที่สามารถใช้ในทุกโรงเเรม เเละสามารถติดต่อข้อมูลของทุกสาขาได้พร้อมกัน ซึ่งในต้องการเก็บข้อมูลดังนี้\r\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้ เช่น ข้อมูลติดต่อ เเต้มสะสม ซึ่งสามารถนำไปใช้เเลกส่วนลดได้ ประเภทผู้เข้าพัก (ลูกค้าทั่วไป, ลูกพิเศษ สามารถเพิ่มหรือเปลี่ยนชื่อได้) เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eสาขา (Branch) เก็บข้อมูลเกี่ยวกับสาขาโรงเเรม เช่น ชื่อ ที่อยู่ เบอร์ติดต่อ เว็ปไซต์ เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี เช่น ชื่อ เลขที่ห้อง ชั้น สาขา เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eข้อมูลการเข้าพัก (Record) เก็บข้อมูลเกี่ยวกับการเข้าพักภายในโรงเเรมเเห่งนี้ เช่น ข้อมูลเเขกผู้เข้าพัก วันเวลาที่เข้าพัก เเละข้อมูลห้อง เป็นต้น\u003c/li\u003e\r\n\r\nโดยความสัมพันธ์ของ Entity ต่าง ๆ มีดังนี้\r\n\u003cli\u003eเมื่อแขกเข้าพักภายในโรงเเรม ข้อมูลจะถูกเก็บใน ข้อมูลการเข้าพัก แขกหนึ่งคนสามารถเข้าพักภายในโรงเเรมได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยเเขกที่จองเพียงคนเดียวเท่านั้น\u003c/li\u003e\r\n\u003cli\u003eการเข้าพักจะเก็บข้อมูลห้องพักที่เข้าพัก โดยหนึ่งห้องพักสามารถเข้าพักได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยห้องพักเพียงห้องเดียวเท่านั้น\u003c/li\u003e\r\n\u003cli\u003eในเเต่ละสาขาสามารถห้องพักได้หลายห้องพัก เเละห้องพักจะเป็นของสาขานั้น ๆ เพียงสาขาเดียวเท่านั้น\u003c/li\u003e","point":300,"question":"จงพิจารณาการออกแบบฐานข้อมูลนี้ เเล้วเลือกคำตอบที่ถูกต้อง"} +$4 +PXAT +$13 +1644721145254 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::12 +$2 +[] +$4 +PXAT +$13 +1644721145381 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::12 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::12 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::12 +$2 +[] +$4 +PXAT +$13 +1644721167586 +*5 +$3 +SET +$35 +learningRepository::GetActivity::12 +$3916 +{"activity_id":12,"activity_type_id":7,"content_id":9,"activity_order":5,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมซึ่งมีหลายสาขา ซึ่งนายแกนต้องการสร้างฐานข้อมูล ที่สามารถใช้ในทุกโรงเเรม เเละสามารถติดต่อข้อมูลของทุกสาขาได้พร้อมกัน ซึ่งในต้องการเก็บข้อมูลดังนี้\r\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้ เช่น ข้อมูลติดต่อ เเต้มสะสม ซึ่งสามารถนำไปใช้เเลกส่วนลดได้ ประเภทผู้เข้าพัก (ลูกค้าทั่วไป, ลูกพิเศษ สามารถเพิ่มหรือเปลี่ยนชื่อได้) เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eสาขา (Branch) เก็บข้อมูลเกี่ยวกับสาขาโรงเเรม เช่น ชื่อ ที่อยู่ เบอร์ติดต่อ เว็ปไซต์ เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี เช่น ชื่อ เลขที่ห้อง ชั้น สาขา เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eข้อมูลการเข้าพัก (Record) เก็บข้อมูลเกี่ยวกับการเข้าพักภายในโรงเเรมเเห่งนี้ เช่น ข้อมูลเเขกผู้เข้าพัก วันเวลาที่เข้าพัก เเละข้อมูลห้อง เป็นต้น\u003c/li\u003e\r\n\r\nโดยความสัมพันธ์ของ Entity ต่าง ๆ มีดังนี้\r\n\u003cli\u003eเมื่อแขกเข้าพักภายในโรงเเรม ข้อมูลจะถูกเก็บใน ข้อมูลการเข้าพัก แขกหนึ่งคนสามารถเข้าพักภายในโรงเเรมได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยเเขกที่จองเพียงคนเดียวเท่านั้น\u003c/li\u003e\r\n\u003cli\u003eการเข้าพักจะเก็บข้อมูลห้องพักที่เข้าพัก โดยหนึ่งห้องพักสามารถเข้าพักได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยห้องพักเพียงห้องเดียวเท่านั้น\u003c/li\u003e\r\n\u003cli\u003eในเเต่ละสาขาสามารถห้องพักได้หลายห้องพัก เเละห้องพักจะเป็นของสาขานั้น ๆ เพียงสาขาเดียวเท่านั้น\u003c/li\u003e","point":300,"question":"จงพิจารณาการออกแบบฐานข้อมูลนี้ เเล้วเลือกคำตอบที่ถูกต้อง"} +$4 +PXAT +$13 +1644721167616 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::12 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::12 +*5 +$3 +SET +$35 +learningRepository::GetActivity::10 +$4140 +{"activity_id":10,"activity_type_id":6,"content_id":9,"activity_order":4,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมซึ่งมีหลายสาขา ซึ่งนายแกนต้องการสร้างฐานข้อมูล ที่สามารถใช้ในทุกโรงเเรม เเละสามารถติดต่อข้อมูลของทุกสาขาได้พร้อมกัน ซึ่งในต้องการเก็บข้อมูลดังนี้\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้ เช่น ข้อมูลติดต่อ (first_name, last_name) เเต้มสะสม (points) ซึ่งสามารถนำไปใช้เเลกส่วนลดได้ ประเภทผู้เข้าพัก (CustomerType) (ลูกค้าทั่วไป, ลูกพิเศษ) สามารถเพิ่มหรือเปลี่ยนชื่อได้ (type_id, type_name) เป็นต้น\u003c/li\u003e\n\u003cli\u003eสาขา (Branch) เก็บข้อมูลเกี่ยวกับสาขาโรงเเรม เช่น รหัสสาข(branch_id) ชื่.อ(branch_name) ที่อยู่(branch_address) เบอร์ติดต่อ(contact_no) เว็ปไซต์(website) เป็นต้น\u003c/li\u003e\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี เช่น ชื่อ (room_name) เลขที่ห้อง(room_no) ชั้น(level) สาขา(branch) เป็นต้น\u003c/li\u003e\n\u003cli\u003eข้อมูลการเข้าพัก (Record) เก็บข้อมูลเกี่ยวกับการเข้าพักภายในโรงเเรมเเห่งนี้ เช่น ข้อมูลเเขกผู้เข้าพัก(guest_id) วันเวลาที่เข้าพัก(created_at) ข้อมูลห้อง(room_no) และรหัสการเข้าพัก(record_id)เป็นต้น\u003c/li\u003e\n\nโดยความสัมพันธ์ของ Entity ต่าง ๆ มีดังนี้\n\u003cli\u003eเมื่อแขกเข้าพักภายในโรงเเรม ข้อมูลจะถูกเก็บใน ข้อมูลการเข้าพัก แขกหนึ่งคนสามารถเข้าพักภายในโรงเเรมได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยเเขกที่จองเพียงคนเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eการเข้าพักจะเก็บข้อมูลห้องพักที่เข้าพัก โดยหนึ่งห้องพักสามารถเข้าพักได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยห้องพักเพียงห้องเดียวเท่านั้น\u003c/li\u003e\n\u003cli\u003eในเเต่ละสาขาสามารถห้องพักได้หลายห้องพัก เเละห้องพักจะเป็นของสาขานั้น ๆ เพียงสาขาเดียวเท่านั้น\u003c/li\u003e","point":300,"question":"ออกแบบฐานข้อมูลจากความต้องการของระบบให้ถูกต้อง"} +$4 +PXAT +$13 +1644721248828 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::10 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::12 +$2 +[] +$4 +PXAT +$13 +1644721435867 +*5 +$3 +SET +$35 +learningRepository::GetActivity::12 +$3916 +{"activity_id":12,"activity_type_id":7,"content_id":9,"activity_order":5,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมซึ่งมีหลายสาขา ซึ่งนายแกนต้องการสร้างฐานข้อมูล ที่สามารถใช้ในทุกโรงเเรม เเละสามารถติดต่อข้อมูลของทุกสาขาได้พร้อมกัน ซึ่งในต้องการเก็บข้อมูลดังนี้\r\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้ เช่น ข้อมูลติดต่อ เเต้มสะสม ซึ่งสามารถนำไปใช้เเลกส่วนลดได้ ประเภทผู้เข้าพัก (ลูกค้าทั่วไป, ลูกพิเศษ สามารถเพิ่มหรือเปลี่ยนชื่อได้) เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eสาขา (Branch) เก็บข้อมูลเกี่ยวกับสาขาโรงเเรม เช่น ชื่อ ที่อยู่ เบอร์ติดต่อ เว็ปไซต์ เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี เช่น ชื่อ เลขที่ห้อง ชั้น สาขา เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eข้อมูลการเข้าพัก (Record) เก็บข้อมูลเกี่ยวกับการเข้าพักภายในโรงเเรมเเห่งนี้ เช่น ข้อมูลเเขกผู้เข้าพัก วันเวลาที่เข้าพัก เเละข้อมูลห้อง เป็นต้น\u003c/li\u003e\r\n\r\nโดยความสัมพันธ์ของ Entity ต่าง ๆ มีดังนี้\r\n\u003cli\u003eเมื่อแขกเข้าพักภายในโรงเเรม ข้อมูลจะถูกเก็บใน ข้อมูลการเข้าพัก แขกหนึ่งคนสามารถเข้าพักภายในโรงเเรมได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยเเขกที่จองเพียงคนเดียวเท่านั้น\u003c/li\u003e\r\n\u003cli\u003eการเข้าพักจะเก็บข้อมูลห้องพักที่เข้าพัก โดยหนึ่งห้องพักสามารถเข้าพักได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยห้องพักเพียงห้องเดียวเท่านั้น\u003c/li\u003e\r\n\u003cli\u003eในเเต่ละสาขาสามารถห้องพักได้หลายห้องพัก เเละห้องพักจะเป็นของสาขานั้น ๆ เพียงสาขาเดียวเท่านั้น\u003c/li\u003e","point":300,"question":"จงพิจารณาการออกแบบฐานข้อมูลนี้ เเล้วเลือกคำตอบที่ถูกต้อง"} +$4 +PXAT +$13 +1644721435867 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::12 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::12 +*5 +$3 +SET +$35 +learningRepository::GetActivity::12 +$3916 +{"activity_id":12,"activity_type_id":7,"content_id":9,"activity_order":5,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมซึ่งมีหลายสาขา ซึ่งนายแกนต้องการสร้างฐานข้อมูล ที่สามารถใช้ในทุกโรงเเรม เเละสามารถติดต่อข้อมูลของทุกสาขาได้พร้อมกัน ซึ่งในต้องการเก็บข้อมูลดังนี้\r\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้ เช่น ข้อมูลติดต่อ เเต้มสะสม ซึ่งสามารถนำไปใช้เเลกส่วนลดได้ ประเภทผู้เข้าพัก (ลูกค้าทั่วไป, ลูกพิเศษ สามารถเพิ่มหรือเปลี่ยนชื่อได้) เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eสาขา (Branch) เก็บข้อมูลเกี่ยวกับสาขาโรงเเรม เช่น ชื่อ ที่อยู่ เบอร์ติดต่อ เว็ปไซต์ เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี เช่น ชื่อ เลขที่ห้อง ชั้น สาขา เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eข้อมูลการเข้าพัก (Record) เก็บข้อมูลเกี่ยวกับการเข้าพักภายในโรงเเรมเเห่งนี้ เช่น ข้อมูลเเขกผู้เข้าพัก วันเวลาที่เข้าพัก เเละข้อมูลห้อง เป็นต้น\u003c/li\u003e\r\n\r\nโดยความสัมพันธ์ของ Entity ต่าง ๆ มีดังนี้\r\n\u003cli\u003eเมื่อแขกเข้าพักภายในโรงเเรม ข้อมูลจะถูกเก็บใน ข้อมูลการเข้าพัก แขกหนึ่งคนสามารถเข้าพักภายในโรงเเรมได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยเเขกที่จองเพียงคนเดียวเท่านั้น\u003c/li\u003e\r\n\u003cli\u003eการเข้าพักจะเก็บข้อมูลห้องพักที่เข้าพัก โดยหนึ่งห้องพักสามารถเข้าพักได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยห้องพักเพียงห้องเดียวเท่านั้น\u003c/li\u003e\r\n\u003cli\u003eในเเต่ละสาขาสามารถห้องพักได้หลายห้องพัก เเละห้องพักจะเป็นของสาขานั้น ๆ เพียงสาขาเดียวเท่านั้น\u003c/li\u003e","point":300,"question":"จงพิจารณาการออกแบบฐานข้อมูลนี้ เเล้วเลือกคำตอบที่ถูกต้อง"} +$4 +PXAT +$13 +1644721462562 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::12 +$2 +[] +$4 +PXAT +$13 +1644721462568 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::12 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::12 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::12 +$2 +[] +$4 +PXAT +$13 +1644721486877 +*5 +$3 +SET +$35 +learningRepository::GetActivity::12 +$3916 +{"activity_id":12,"activity_type_id":7,"content_id":9,"activity_order":5,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมซึ่งมีหลายสาขา ซึ่งนายแกนต้องการสร้างฐานข้อมูล ที่สามารถใช้ในทุกโรงเเรม เเละสามารถติดต่อข้อมูลของทุกสาขาได้พร้อมกัน ซึ่งในต้องการเก็บข้อมูลดังนี้\r\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้ เช่น ข้อมูลติดต่อ เเต้มสะสม ซึ่งสามารถนำไปใช้เเลกส่วนลดได้ ประเภทผู้เข้าพัก (ลูกค้าทั่วไป, ลูกพิเศษ สามารถเพิ่มหรือเปลี่ยนชื่อได้) เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eสาขา (Branch) เก็บข้อมูลเกี่ยวกับสาขาโรงเเรม เช่น ชื่อ ที่อยู่ เบอร์ติดต่อ เว็ปไซต์ เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี เช่น ชื่อ เลขที่ห้อง ชั้น สาขา เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eข้อมูลการเข้าพัก (Record) เก็บข้อมูลเกี่ยวกับการเข้าพักภายในโรงเเรมเเห่งนี้ เช่น ข้อมูลเเขกผู้เข้าพัก วันเวลาที่เข้าพัก เเละข้อมูลห้อง เป็นต้น\u003c/li\u003e\r\n\r\nโดยความสัมพันธ์ของ Entity ต่าง ๆ มีดังนี้\r\n\u003cli\u003eเมื่อแขกเข้าพักภายในโรงเเรม ข้อมูลจะถูกเก็บใน ข้อมูลการเข้าพัก แขกหนึ่งคนสามารถเข้าพักภายในโรงเเรมได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยเเขกที่จองเพียงคนเดียวเท่านั้น\u003c/li\u003e\r\n\u003cli\u003eการเข้าพักจะเก็บข้อมูลห้องพักที่เข้าพัก โดยหนึ่งห้องพักสามารถเข้าพักได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยห้องพักเพียงห้องเดียวเท่านั้น\u003c/li\u003e\r\n\u003cli\u003eในเเต่ละสาขาสามารถห้องพักได้หลายห้องพัก เเละห้องพักจะเป็นของสาขานั้น ๆ เพียงสาขาเดียวเท่านั้น\u003c/li\u003e","point":300,"question":"จงพิจารณาการออกแบบฐานข้อมูลนี้ เเล้วเลือกคำตอบที่ถูกต้อง"} +$4 +PXAT +$13 +1644721486993 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::12 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::12 +*5 +$3 +SET +$35 +learningRepository::GetActivity::12 +$3916 +{"activity_id":12,"activity_type_id":7,"content_id":9,"activity_order":5,"story":"นายแกนต้องการออกแบบฐานข้อมูลสำหรับใช้ในโรงแรมซึ่งมีหลายสาขา ซึ่งนายแกนต้องการสร้างฐานข้อมูล ที่สามารถใช้ในทุกโรงเเรม เเละสามารถติดต่อข้อมูลของทุกสาขาได้พร้อมกัน ซึ่งในต้องการเก็บข้อมูลดังนี้\r\n\u003cli\u003eแขกผู้เข้าพัก (Guest) เก็บข้อมูลเกี่ยวกับแขกที่ทำการลงชื่อเข้าพักโรงแรมเเห่งนี้ เช่น ข้อมูลติดต่อ เเต้มสะสม ซึ่งสามารถนำไปใช้เเลกส่วนลดได้ ประเภทผู้เข้าพัก (ลูกค้าทั่วไป, ลูกพิเศษ สามารถเพิ่มหรือเปลี่ยนชื่อได้) เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eสาขา (Branch) เก็บข้อมูลเกี่ยวกับสาขาโรงเเรม เช่น ชื่อ ที่อยู่ เบอร์ติดต่อ เว็ปไซต์ เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eห้องพัก (Room) เก็บข้อมูลเกี่ยวกับห้องพักภายในโรงแรมเเห่งนี เช่น ชื่อ เลขที่ห้อง ชั้น สาขา เป็นต้น\u003c/li\u003e\r\n\u003cli\u003eข้อมูลการเข้าพัก (Record) เก็บข้อมูลเกี่ยวกับการเข้าพักภายในโรงเเรมเเห่งนี้ เช่น ข้อมูลเเขกผู้เข้าพัก วันเวลาที่เข้าพัก เเละข้อมูลห้อง เป็นต้น\u003c/li\u003e\r\n\r\nโดยความสัมพันธ์ของ Entity ต่าง ๆ มีดังนี้\r\n\u003cli\u003eเมื่อแขกเข้าพักภายในโรงเเรม ข้อมูลจะถูกเก็บใน ข้อมูลการเข้าพัก แขกหนึ่งคนสามารถเข้าพักภายในโรงเเรมได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยเเขกที่จองเพียงคนเดียวเท่านั้น\u003c/li\u003e\r\n\u003cli\u003eการเข้าพักจะเก็บข้อมูลห้องพักที่เข้าพัก โดยหนึ่งห้องพักสามารถเข้าพักได้หลายครั้ง เเต่ข้อมูลการเข้าพักจะประกอบไปด้วยห้องพักเพียงห้องเดียวเท่านั้น\u003c/li\u003e\r\n\u003cli\u003eในเเต่ละสาขาสามารถห้องพักได้หลายห้องพัก เเละห้องพักจะเป็นของสาขานั้น ๆ เพียงสาขาเดียวเท่านั้น\u003c/li\u003e","point":300,"question":"จงพิจารณาการออกแบบฐานข้อมูลนี้ เเล้วเลือกคำตอบที่ถูกต้อง"} +$4 +PXAT +$13 +1644721680058 +*5 +$3 +SET +$40 +learningRepository::GetActivityHints::12 +$2 +[] +$4 +PXAT +$13 +1644721680064 +*2 +$3 +DEL +$40 +learningRepository::GetActivityHints::12 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::12 +*2 +$6 +SELECT +$1 +0 +*5 +$3 +SET +$36 +examRepository::GetExamActivities::3 +$38 +[{"ActivityID":16,"ActivityTypeID":3}] +$4 +PXAT +$13 +1644723200163 +*5 +$3 +SET +$26 +examRepository::GetExam::3 +$494 +{"exam_id":3,"exam_type":"MINI","instruction":"ข้อสอบมีทั้งสิ้น 5 ข้อ เพื่อทดสอบความเข้าใจเกี่ยวกับ Relational Database โดยต้องทำข้อสอบได้ 50 เปอร์เซนต์ขึ้นไปจึงผ่านแบบทดสอบ","created_timestamp":"2022-01-23T05:31:52+07:00","content_group_id":1,"content_group_name":"Relational database","badge_id":1} +$4 +PXAT +$13 +1644723200283 +*5 +$3 +SET +$35 +learningRepository::GetActivity::16 +$673 +{"activity_id":16,"activity_type_id":3,"content_id":1,"activity_order":2,"story":"นายแกนต้องการจะเก็บข้อมูลในการทำธุรกิจขายก๋วยเตี๋ยวไก่ แต่นายแกนไม่เข้าใจว่าทำไมถึงต้องใช้ระบบฐานข้อมูล คุณจะสามารถช่วยอธิบายให้นายแกนเข้าใจได้อย่างไรบ้าง","point":300,"question":"จงเลือกคำมาเติมในช่องว่างให้ถูกต้อง"} +$4 +PXAT +$13 +1644723200345 +*5 +$3 +SET +$43 +learningRepository::getCompletionChoice::16 +$1365 +[{"completion_choice_id":12,"content":"แชร์ข้อมูล","question_first":"การใช้ระบบฐานข้อมูลจะช่วยให้นายแกนสามารถ","question_last":"คอมพิวเตอร์ได้"},{"completion_choice_id":13,"content":"DBMS","question_first":"","question_last":"ใช้ในการจัดการข้อมูลในฐานข้อมูล แก้ไข้ปัญหา Fixed Query ใน File-based system"},{"completion_choice_id":14,"content":"Database Application","question_first":"ระบบจัดการการขายที่นายแกนใช้เป็น ","question_last":""},{"completion_choice_id":15,"content":"ออกแบบฐานข้อมูล","question_first":"นายแกนสามารถ","question_last":"เพื่อใช้สำหรับรวบรวมข้อมูลทีมีความสัมพันธ์กัน"},{"completion_choice_id":16,"content":"ฐานข้อมูล","question_first":"การออกแบบ","question_last":"ที่ดี ช่วยให้นายแกนเก็บข้อมูลได้อย่างถูกต้องและง่ายต่อการเข้าถึงและใช้งาน"}] +$4 +PXAT +$13 +1644723200346 +*2 +$3 +DEL +$36 +examRepository::GetExamActivities::3 +*2 +$3 +DEL +$26 +examRepository::GetExam::3 +*2 +$3 +DEL +$43 +learningRepository::getCompletionChoice::16 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::16 +*5 +$3 +SET +$27 +userRepository::GetAllBadge +$1468 +[{"ID":1,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/relational_database_realizer.png","Name":"ผู้ตระหนักรู้ Relational Database","IsCollected":false},{"ID":2,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/god_of_model.png","Name":"เทพแห่ง ER-Model","IsCollected":false},{"ID":3,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/database_design.png","Name":"นักออกแบบ Database","IsCollected":false},{"ID":4,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/document_maker.png","Name":"ผู้จัดทำเอกสาร","IsCollected":false},{"ID":5,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/problem_awareness.png","Name":"ผู้หยั่งรู้ปัญหา","IsCollected":false},{"ID":6,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/dependency_analyser.png","Name":"นักวิเคราะห์ Dependency","IsCollected":false},{"ID":7,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/professional_normalizer.png","Name":"ผู้เชี่ยวชาญ Normalization","IsCollected":false},{"ID":8,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/database_camp_conquere.png","Name":"ผู้พิชิต DatabaseCamp","IsCollected":false}] +$4 +PXAT +$13 +1644723249166 +*2 +$3 +DEL +$27 +userRepository::GetAllBadge +*5 +$3 +SET +$27 +userRepository::GetAllBadge +$1468 +[{"ID":1,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/relational_database_realizer.png","Name":"ผู้ตระหนักรู้ Relational Database","IsCollected":false},{"ID":2,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/god_of_model.png","Name":"เทพแห่ง ER-Model","IsCollected":false},{"ID":3,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/database_design.png","Name":"นักออกแบบ Database","IsCollected":false},{"ID":4,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/document_maker.png","Name":"ผู้จัดทำเอกสาร","IsCollected":false},{"ID":5,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/problem_awareness.png","Name":"ผู้หยั่งรู้ปัญหา","IsCollected":false},{"ID":6,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/dependency_analyser.png","Name":"นักวิเคราะห์ Dependency","IsCollected":false},{"ID":7,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/professional_normalizer.png","Name":"ผู้เชี่ยวชาญ Normalization","IsCollected":false},{"ID":8,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/database_camp_conquere.png","Name":"ผู้พิชิต DatabaseCamp","IsCollected":false}] +$4 +PXAT +$13 +1644723368120 +*2 +$3 +DEL +$27 +userRepository::GetAllBadge +*5 +$3 +SET +$27 +userRepository::GetAllBadge +$1468 +[{"ID":1,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/relational_database_realizer.png","Name":"ผู้ตระหนักรู้ Relational Database","IsCollected":false},{"ID":2,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/god_of_model.png","Name":"เทพแห่ง ER-Model","IsCollected":false},{"ID":3,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/database_design.png","Name":"นักออกแบบ Database","IsCollected":false},{"ID":4,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/document_maker.png","Name":"ผู้จัดทำเอกสาร","IsCollected":false},{"ID":5,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/problem_awareness.png","Name":"ผู้หยั่งรู้ปัญหา","IsCollected":false},{"ID":6,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/dependency_analyser.png","Name":"นักวิเคราะห์ Dependency","IsCollected":false},{"ID":7,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/professional_normalizer.png","Name":"ผู้เชี่ยวชาญ Normalization","IsCollected":false},{"ID":8,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/database_camp_conquere.png","Name":"ผู้พิชิต DatabaseCamp","IsCollected":false}] +$4 +PXAT +$13 +1644723454009 +*2 +$3 +DEL +$27 +userRepository::GetAllBadge +*5 +$3 +SET +$27 +userRepository::GetAllBadge +$1468 +[{"ID":1,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/relational_database_realizer.png","Name":"ผู้ตระหนักรู้ Relational Database","IsCollected":false},{"ID":2,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/god_of_model.png","Name":"เทพแห่ง ER-Model","IsCollected":false},{"ID":3,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/database_design.png","Name":"นักออกแบบ Database","IsCollected":false},{"ID":4,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/document_maker.png","Name":"ผู้จัดทำเอกสาร","IsCollected":false},{"ID":5,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/problem_awareness.png","Name":"ผู้หยั่งรู้ปัญหา","IsCollected":false},{"ID":6,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/dependency_analyser.png","Name":"นักวิเคราะห์ Dependency","IsCollected":false},{"ID":7,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/professional_normalizer.png","Name":"ผู้เชี่ยวชาญ Normalization","IsCollected":false},{"ID":8,"ImagePath":"https://storage.googleapis.com/databasecamp-public/badge-icons/database_camp_conquere.png","Name":"ผู้พิชิต DatabaseCamp","IsCollected":false}] +$4 +PXAT +$13 +1644724461197 +*2 +$3 +DEL +$27 +userRepository::GetAllBadge +*5 +$3 +SET +$41 +learningRepository::getMatchingChoice::20 +$197 +[{"matching_choice_id":4,"pair_item1":"Relation","pair_item2":"1"},{"matching_choice_id":5,"pair_item1":"Attribute","pair_item2":"2"},{"matching_choice_id":6,"pair_item1":"Tuple","pair_item2":"3"}] +$4 +PXAT +$13 +1644724767797 +*5 +$3 +SET +$35 +learningRepository::GetActivity::20 +$576 +{"activity_id":20,"activity_type_id":1,"content_id":2,"activity_order":2,"story":"นายแกนต้องการเริ่มต้นออกแบบฐานข้อมูลของตนเอง นายแกนอยากจะทราบว่าองค์ประกอบของตารางที่นายแกนเห็นมีชื่อเรียกว่าอะไรบ้าง","point":300,"question":"จงจับคู่ตัวเลขในภาพและคำศัพท์ให้ถูกต้อง"} +$4 +PXAT +$13 +1644724767798 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::20 +*2 +$3 +DEL +$41 +learningRepository::getMatchingChoice::20 +*5 +$3 +SET +$35 +learningRepository::GetActivity::20 +$576 +{"activity_id":20,"activity_type_id":1,"content_id":2,"activity_order":2,"story":"นายแกนต้องการเริ่มต้นออกแบบฐานข้อมูลของตนเอง นายแกนอยากจะทราบว่าองค์ประกอบของตารางที่นายแกนเห็นมีชื่อเรียกว่าอะไรบ้าง","point":300,"question":"จงจับคู่ตัวเลขในภาพและคำศัพท์ให้ถูกต้อง"} +$4 +PXAT +$13 +1644724844001 +*5 +$3 +SET +$41 +learningRepository::getMatchingChoice::20 +$197 +[{"matching_choice_id":4,"pair_item1":"Relation","pair_item2":"1"},{"matching_choice_id":5,"pair_item1":"Attribute","pair_item2":"2"},{"matching_choice_id":6,"pair_item1":"Tuple","pair_item2":"3"}] +$4 +PXAT +$13 +1644724844004 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::20 +*2 +$3 +DEL +$41 +learningRepository::getMatchingChoice::20 +*5 +$3 +SET +$41 +learningRepository::getMatchingChoice::20 +$197 +[{"matching_choice_id":4,"pair_item1":"Relation","pair_item2":"1"},{"matching_choice_id":5,"pair_item1":"Attribute","pair_item2":"2"},{"matching_choice_id":6,"pair_item1":"Tuple","pair_item2":"3"}] +$4 +PXAT +$13 +1644724880473 +*5 +$3 +SET +$35 +learningRepository::GetActivity::20 +$576 +{"activity_id":20,"activity_type_id":1,"content_id":2,"activity_order":2,"story":"นายแกนต้องการเริ่มต้นออกแบบฐานข้อมูลของตนเอง นายแกนอยากจะทราบว่าองค์ประกอบของตารางที่นายแกนเห็นมีชื่อเรียกว่าอะไรบ้าง","point":300,"question":"จงจับคู่ตัวเลขในภาพและคำศัพท์ให้ถูกต้อง"} +$4 +PXAT +$13 +1644724880607 +*2 +$3 +DEL +$41 +learningRepository::getMatchingChoice::20 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::20 +*5 +$3 +SET +$35 +learningRepository::GetActivity::20 +$576 +{"activity_id":20,"activity_type_id":1,"content_id":2,"activity_order":2,"story":"นายแกนต้องการเริ่มต้นออกแบบฐานข้อมูลของตนเอง นายแกนอยากจะทราบว่าองค์ประกอบของตารางที่นายแกนเห็นมีชื่อเรียกว่าอะไรบ้าง","point":300,"question":"จงจับคู่ตัวเลขในภาพและคำศัพท์ให้ถูกต้อง"} +$4 +PXAT +$13 +1644724894146 +*5 +$3 +SET +$41 +learningRepository::getMatchingChoice::20 +$197 +[{"matching_choice_id":4,"pair_item1":"Relation","pair_item2":"1"},{"matching_choice_id":5,"pair_item1":"Attribute","pair_item2":"2"},{"matching_choice_id":6,"pair_item1":"Tuple","pair_item2":"3"}] +$4 +PXAT +$13 +1644724894251 +*2 +$3 +DEL +$35 +learningRepository::GetActivity::20 +*2 +$3 +DEL +$41 +learningRepository::getMatchingChoice::20 diff --git a/database/database.go b/database/database.go deleted file mode 100644 index c295593..0000000 --- a/database/database.go +++ /dev/null @@ -1,100 +0,0 @@ -package database - -// database.go -/** - * This file used to create connection to the RDBMS - */ - -import ( - "fmt" - "os" - - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -/** - * This class manage database connection - */ -type database struct { - db *gorm.DB -} - -// Instance of database class for singleton pattern -var instantiated *database = nil - -/** - * Constructor creates a new database instance or geting a database instance - * - * @return instance of database - */ -func New() *database { - if instantiated == nil { - instantiated = new(database) - } - return instantiated -} - -/** - * Get a name of the database from the environment - * - * @return name of the database - */ -func getDBName() string { - if os.Getenv("MODE") == "develop" { - return os.Getenv("DB_NAME_DEVELOP") - } else { - return os.Getenv("DB_NAME_PRODUCTION") - } -} - -/** - * Get a DSN of the database from the environment - * - * @return DSN of the database - */ -func getDSN() string { - return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", - os.Getenv("DB_USERNAME"), - os.Getenv("DB_PASSWORD"), - os.Getenv("DB_HOST"), - os.Getenv("DB_PORT"), - getDBName(), - ) -} - -/** - * Get the database for data manipulation - * - * @return the database that can be used to manipulate data in the database - */ -func (db *database) GetDB() *gorm.DB { - return db.db -} - -/** - * Open the database connection - * - * @return the error of opening the database - */ -func (db *database) OpenConnection() error { - var err error - dsn := getDSN() - sql := mysql.Open(dsn) - db.db, err = gorm.Open(sql) - return err -} - -/** - * Close the database connection - * - * @return the error of closing the database - */ -func (db *database) CloseDB() error { - sql, err := db.db.DB() - if err != nil { - return err - } - err = sql.Close() - return err -} diff --git a/database/interface.go b/database/interface.go deleted file mode 100644 index daafd05..0000000 --- a/database/interface.go +++ /dev/null @@ -1,70 +0,0 @@ -package database - -// interface.go -/** - * This file used to be a interface of database - */ - -import "gorm.io/gorm" - -/** - * Interface to show function in database that others can use - */ -type IDatabase interface { - - /** - * Get the database for data manipulation - * - * @return the database that can be used to manipulate data in the database - */ - GetDB() *gorm.DB - - /** - * Open the database connection - * - * @return the error of opening the database - */ - OpenConnection() error - - /** - * Close the database connection - * - * @return the error of closing the database - */ - CloseDB() error -} - -/** - * Interface to show function in transaction that others can use - */ -type ITransaction interface { - - /** - * Get the database for data manipulation - * - * @return the database that can be used to manipulate data in the database - */ - GetDB() *gorm.DB - - /** - * Begin database transaction - */ - Begin() - - /** - * Commit database transaction - */ - Commit() - - /** - * Rollback database transaction - */ - Rollback() - - /** - * Close database transaction - * - * @return the error of closing the database transaction - */ - Close() error -} diff --git a/database/transaction.go b/database/transaction.go deleted file mode 100644 index 756a7bb..0000000 --- a/database/transaction.go +++ /dev/null @@ -1,74 +0,0 @@ -package database - -// database.go -/** - * This file used to create database transaction - */ - -import ( - "gorm.io/driver/mysql" - "gorm.io/gorm" -) - -/** - * This class manage database transaction - */ -type transaction struct { - db *gorm.DB -} - -/** - * Constructor creates a new transaction instance - * - * @return instance of transaction - */ -func NewTransaction() *transaction { - dsn := getDSN() - sql := mysql.Open(dsn) - db, _ := gorm.Open(sql) - return &transaction{db: db} -} - -/** - * Get the database for data manipulation - * - * @return the database that can be used to manipulate data in the database - */ -func (t *transaction) GetDB() *gorm.DB { - return t.db -} - -/** - * Begin database transaction - */ -func (t *transaction) Begin() { - t.db = t.db.Begin() -} - -/** - * Commit database transaction - */ -func (t *transaction) Commit() { - t.db = t.db.Commit() -} - -/** - * Rollback database transaction - */ -func (t *transaction) Rollback() { - t.db = t.db.Rollback() -} - -/** - * Close database transaction - * - * @return the error of closing the database transaction - */ -func (t *transaction) Close() error { - sql, err := t.db.DB() - if err != nil { - return err - } - err = sql.Close() - return err -} diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..6aad41a --- /dev/null +++ b/deploy.sh @@ -0,0 +1,8 @@ +TAG=$1 +docker build --no-cache -t ganinw13120/dbc-backend:$TAG . +docker tag ganinw13120/dbc-backend:$TAG ganinw13120/dbc-backend:$TAG +docker push ganinw13120/dbc-backend:$TAG + +# docker pull ganinw13120/dbc-backend:$1 +# docker stop backend +# docker run --rm --name backend -p 8080:8080 -d ganinw13120/dbc-backend:$1 \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..c010b11 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,24 @@ +version: '3.3' +services: + redis: + image: redis + container_name: redis + ports: + - 6379:6379 + volumes: + - ./data/redis:/data + - ./config/redis.conf:/redis.conf + command: redis-server /redis.conf + + networks: + - cache-net + backend: + image: ganinw13120/dbc-backend:1.0.63 + container_name: backend + ports: + - 80:80 + networks: + - cache-net + +networks: + cache-net: diff --git a/go.mod b/go.mod index ab9ef21..d950d82 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,48 @@ -module DatabaseCamp +module database-camp -go 1.16 +go 1.17 require ( cloud.google.com/go/storage v1.18.2 - github.com/aws/aws-sdk-go v1.41.9 + github.com/go-redis/redis/v8 v8.11.4 github.com/go-sql-driver/mysql v1.6.0 - github.com/gofiber/fiber/v2 v2.20.1 + github.com/gofiber/fiber/v2 v2.24.0 github.com/golang-jwt/jwt v3.2.2+incompatible - github.com/joho/godotenv v1.4.0 github.com/satori/go.uuid v1.2.0 - go.uber.org/zap v1.19.1 - golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 + go.uber.org/zap v1.20.0 + golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 - google.golang.org/api v0.63.0 // indirect - gorm.io/driver/mysql v1.1.2 - gorm.io/gorm v1.21.15 + gorm.io/driver/mysql v1.2.2 + gorm.io/gorm v1.22.4 +) + +require github.com/joho/godotenv v1.4.0 + +require ( + cloud.google.com/go v0.97.0 // indirect + github.com/andybalholm/brotli v1.0.2 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/go-cmp v0.5.6 // indirect + github.com/googleapis/gax-go/v2 v2.1.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.3 // indirect + github.com/klauspost/compress v1.13.4 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.31.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + go.opencensus.io v0.23.0 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect + golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 // indirect + golang.org/x/text v0.3.6 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + google.golang.org/api v0.58.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20211016002631-37fc39342514 // indirect + google.golang.org/grpc v1.40.0 // indirect + google.golang.org/protobuf v1.27.1 // indirect ) diff --git a/go.sum b/go.sum index af105d6..b100898 100644 --- a/go.sum +++ b/go.sum @@ -24,9 +24,8 @@ cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWc cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0 h1:3DXvAyifywvq64LfkKaMOmkWPS1CikIQdMe2lY9vxU8= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= -cloud.google.com/go v0.99.0 h1:y/cM2iqGgGi5D5DQZl6D9STN/3dR/Vx5Mp8s752oJTY= -cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -49,50 +48,52 @@ cloud.google.com/go/storage v1.18.2/go.mod h1:AiIj7BWXyhO5gGVmYJ+S8tbkCx3yb0IMju dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E= github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/aws/aws-sdk-go v1.41.9 h1:Xb4gWjA90ju0u6Fr2lMAsMOGuhw1g4sTFOqh9SUHgN0= -github.com/aws/aws-sdk-go v1.41.9/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 h1:cqQfy1jclcSy/FwLjemeg3SR1yaINm74aQyupQ0Bl8M= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed h1:OZmjad4L3H8ncOIR8rnb5MREYqG8ixi5+WbeUsquF0c= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0 h1:dulLQAYQFYtG5MTplgNGHWuV2D+OBD+Z8lmDBmbLg+s= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg= +github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/gofiber/fiber/v2 v2.20.1 h1:p463gd/RI8YeYxP4WMGS+u1UtBS88yk8oLiPkEiDYx4= -github.com/gofiber/fiber/v2 v2.20.1/go.mod h1:/LdZHMUXZvTTo7gU4+b1hclqCAdoQphNQ9bi9gutPyI= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gofiber/fiber/v2 v2.24.0 h1:18rpLoQMJBVlLtX/PwgHj3hIxPSeWfN1YeDJ2lEnzjU= +github.com/gofiber/fiber/v2 v2.24.0/go.mod h1:MR1usVH3JHYRyQwMe2eZXRSZHRX38fkV+A7CPB+DlDQ= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -164,7 +165,6 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -174,16 +174,13 @@ github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI= -github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jinzhu/now v1.1.3 h1:PlHq1bSCSZL9K0wUhbm2pGLoTWs2GwVhsP6emvGV/ZI= +github.com/jinzhu/now v1.1.3/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -196,9 +193,19 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= +github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -206,7 +213,6 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 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= @@ -217,8 +223,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.29.0 h1:F5GKpytwFk5OhCuRh6H+d4vZAcEeNAwPTdwQnm6IERY= -github.com/valyala/fasthttp v1.29.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= +github.com/valyala/fasthttp v1.31.0 h1:lrauRLII19afgCs2fnWRJ4M5IkV0lo2FqA61uGkNBfE= +github.com/valyala/fasthttp v1.31.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -237,20 +243,19 @@ go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4= -go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= -go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go.uber.org/zap v1.20.0 h1:N4oPlghZwYG55MlU6LXk/Zp00FVNE9X9wrYO8CEs4lc= +go.uber.org/zap v1.20.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -288,6 +293,7 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -308,6 +314,7 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= @@ -320,10 +327,10 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I= golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -354,6 +361,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -362,7 +370,10 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -383,6 +394,7 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -393,16 +405,13 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 h1:J27LZFQBFoihqXoegpscI10HpjZ7B5WQLLKL2FZXQKw= golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211210111614-af8b64212486 h1:5hpz5aRr+W1erYCL5JRhSUBJRph7l9XkNveoExlrKYk= -golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -460,6 +469,7 @@ golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82u golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -501,10 +511,8 @@ google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6 google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.58.0 h1:MDkAbYIB1JpSgCTOCYYoIec/coMlKK4oVbpnBLLcyT0= google.golang.org/api v0.58.0/go.mod h1:cAbP2FsxoGVNwtgNAmmn3y5G1TWAiVYRmg4yku3lv+E= -google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.63.0 h1:n2bqqK895ygnBpdPDYetfy23K7fJ22wsrZKCyfuRkkA= -google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -570,11 +578,8 @@ google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEc google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211016002631-37fc39342514 h1:Rp1vYDPD4TdkMH5S/bZbopsGCsWhPcrLBUwOVhAQCxM= google.golang.org/genproto v0.0.0-20211016002631-37fc39342514/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa h1:I0YcKz0I7OAhddo7ya8kMnvprhcWM045PmkBdMO9zN0= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -599,9 +604,8 @@ google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.40.1 h1:pnP7OclFFFgFi4VHQDQDaoXUVauOFyktqTsqqgzFKbc= -google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -621,18 +625,23 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/mysql v1.1.2 h1:OofcyE2lga734MxwcCW9uB4mWNXMr50uaGRVwQL2B0M= -gorm.io/driver/mysql v1.1.2/go.mod h1:4P/X9vSc3WTrhTLZ259cpFd6xKNYiSSdSZngkSBGIMM= -gorm.io/gorm v1.21.12/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= -gorm.io/gorm v1.21.15 h1:gAyaDoPw0lCyrSFWhBlahbUA1U4P5RViC1uIqoB+1Rk= -gorm.io/gorm v1.21.15/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= +gorm.io/driver/mysql v1.2.2 h1:2qoqhOun1maoJOfLtnzJwq+bZlHkEF34rGntgySqp48= +gorm.io/driver/mysql v1.2.2/go.mod h1:qsiz+XcAyMrS6QY+X3M9R6b/lKM1imKmcuK9kac5LTo= +gorm.io/gorm v1.22.4 h1:8aPcyEJhY0MAt8aY6Dc524Pn+pO29K+ydu+e/cXSpQM= +gorm.io/gorm v1.22.4/go.mod h1:1aeVC+pe9ZmvKZban/gW4QPra7PRoTEssyc922qCAkk= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/handlers/handler.exam.go b/handlers/handler.exam.go deleted file mode 100644 index a174039..0000000 --- a/handlers/handler.exam.go +++ /dev/null @@ -1,126 +0,0 @@ -package handlers - -// handler.exam.go -/** - * This file is a part of handler, used to handle request of the exam - */ - -import ( - "DatabaseCamp/controllers" - "DatabaseCamp/models/request" - "DatabaseCamp/utils" - "net/http" - - "github.com/gofiber/fiber/v2" -) - -/** - * This class handle request of the exam - */ -type examHandler struct { - Controller controllers.IExamController // Exam controller for doing business logic of the exam -} - -/** - * Constructor creates a new examHandler instance - * - * @param controller Exam controller for doing business logic of the exam - * - * @return instance of examHandler - */ -func NewExamHandler(controller controllers.IExamController) examHandler { - return examHandler{Controller: controller} -} - -/** - * Get the exam to use for the test - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ -func (h examHandler) GetExam(c *fiber.Ctx) error { - handleUtil := utils.NewHandle() - - examID := utils.NewType().ParseInt(c.Params("id")) - userID := utils.NewType().ParseInt(c.Locals("id")) - - response, err := h.Controller.GetExam(examID, userID) - if err != nil { - return handleUtil.HandleError(c, err) - } - - return c.Status(http.StatusOK).JSON(response) -} - -/** - * Check answer of the exam - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ -func (h examHandler) CheckExam(c *fiber.Ctx) error { - handleUtil := utils.NewHandle() - - userID := utils.NewType().ParseInt(c.Locals("id")) - request := request.ExamAnswerRequest{} - - err := handleUtil.BindRequest(c, &request) - if err != nil { - return handleUtil.HandleError(c, err) - } - - err = request.Validate() - if err != nil { - return handleUtil.HandleError(c, err) - } - - response, err := h.Controller.CheckExam(userID, request) - if err != nil { - return handleUtil.HandleError(c, err) - } - - return c.Status(http.StatusOK).JSON(response) -} - -/** - * Get overview of the exam - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ -func (h examHandler) GetExamOverview(c *fiber.Ctx) error { - handleUtil := utils.NewHandle() - - userID := utils.NewType().ParseInt(c.Locals("id")) - - response, err := h.Controller.GetOverview(userID) - if err != nil { - return handleUtil.HandleError(c, err) - } - - return c.Status(http.StatusOK).JSON(response) -} - -/** - * Get exam result of the user - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ -func (h examHandler) GetExamResult(c *fiber.Ctx) error { - handleUtil := utils.NewHandle() - - userID := utils.NewType().ParseInt(c.Locals("id")) - examResultID := utils.NewType().ParseInt(c.Params("id")) - - response, err := h.Controller.GetExamResult(userID, examResultID) - if err != nil { - return handleUtil.HandleError(c, err) - } - - return c.Status(http.StatusOK).JSON(response) -} diff --git a/handlers/handler.learning.go b/handlers/handler.learning.go deleted file mode 100644 index c0d4314..0000000 --- a/handlers/handler.learning.go +++ /dev/null @@ -1,229 +0,0 @@ -package handlers - -// handler.learning.go -/** - * This file is a part of handler, used to handle request of the learning - */ - -import ( - "DatabaseCamp/controllers" - "DatabaseCamp/models/request" - "DatabaseCamp/utils" - "net/http" - - "github.com/gofiber/fiber/v2" -) - -/** - * This class handle request of the learning - */ -type learningHandler struct { - Controller controllers.ILearningController // Learning controller for doing business logic of the learning -} - -/** - * Constructor creates a new learningHandler instance - * - * @param controller Learning controller for doing business logic of the learning - * - * @return instance of learningHandler - */ -func NewLearningHandler(controller controllers.ILearningController) learningHandler { - return learningHandler{Controller: controller} -} - -/** - * Get content roadmap - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ -func (h learningHandler) GetContentRoadmap(c *fiber.Ctx) error { - handleUtil := utils.NewHandle() - - userID := utils.NewType().ParseInt(c.Locals("id")) - contentID := utils.NewType().ParseInt(c.Params("id")) - - response, err := h.Controller.GetContentRoadmap(userID, contentID) - if err != nil { - return handleUtil.HandleError(c, err) - } - - return c.Status(http.StatusOK).JSON(response) -} - -/** - * Get video lecture of the content - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ -func (h learningHandler) GetVideo(c *fiber.Ctx) error { - handleUtil := utils.NewHandle() - - contentID := c.Params("id") - - response, err := h.Controller.GetVideoLecture(utils.NewType().ParseInt(contentID)) - if err != nil { - return handleUtil.HandleError(c, err) - } - - return c.Status(http.StatusOK).JSON(response) -} - -/** - * Get content overview - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ -func (h learningHandler) GetOverview(c *fiber.Ctx) error { - handleUtil := utils.NewHandle() - - id := c.Locals("id") - - response, err := h.Controller.GetOverview(utils.NewType().ParseInt(id)) - if err != nil { - return handleUtil.HandleError(c, err) - } - - return c.Status(http.StatusOK).JSON(response) -} - -/** - * Get activity for user to do - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ -func (h learningHandler) GetActivity(c *fiber.Ctx) error { - handleUtil := utils.NewHandle() - - userID := utils.NewType().ParseInt(c.Locals("id")) - activityID := utils.NewType().ParseInt(c.Params("id")) - - response, err := h.Controller.GetActivity(userID, activityID) - if err != nil { - return handleUtil.HandleError(c, err) - } - - return c.Status(http.StatusOK).JSON(response) -} - -/** - * Use hint of the activity - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ -func (h learningHandler) UseHint(c *fiber.Ctx) error { - handleUtil := utils.NewHandle() - - userID := utils.NewType().ParseInt(c.Locals("id")) - activityID := utils.NewType().ParseInt(c.Params("id")) - - response, err := h.Controller.UseHint(userID, activityID) - if err != nil { - return handleUtil.HandleError(c, err) - } - - return c.Status(http.StatusOK).JSON(response) -} - -/** - * Check matching choice answer of the activity - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ -func (h learningHandler) CheckMatchingAnswer(c *fiber.Ctx) error { - handleUtil := utils.NewHandle() - - userID := utils.NewType().ParseInt(c.Locals("id")) - request := request.MatchingChoiceAnswerRequest{} - - err := handleUtil.BindRequest(c, &request) - if err != nil { - return handleUtil.HandleError(c, err) - } - - err = request.Validate() - if err != nil { - return handleUtil.HandleError(c, err) - } - - response, err := h.Controller.CheckAnswer(userID, *request.ActivityID, 1, request.Answer) - if err != nil { - return handleUtil.HandleError(c, err) - } - - return c.Status(http.StatusOK).JSON(response) -} - -/** - * Check multiple choice answer of the activity - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ -func (h learningHandler) CheckMultipleAnswer(c *fiber.Ctx) error { - handleUtil := utils.NewHandle() - - userID := utils.NewType().ParseInt(c.Locals("id")) - request := request.MultipleChoiceAnswerRequest{} - - err := handleUtil.BindRequest(c, &request) - if err != nil { - return handleUtil.HandleError(c, err) - } - - err = request.Validate() - if err != nil { - return handleUtil.HandleError(c, err) - } - - response, err := h.Controller.CheckAnswer(userID, *request.ActivityID, 2, *request.Answer) - if err != nil { - return handleUtil.HandleError(c, err) - } - - return c.Status(http.StatusOK).JSON(response) -} - -/** - * Check completion choice answer of the activity - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ -func (h learningHandler) CheckCompletionAnswer(c *fiber.Ctx) error { - handleUtil := utils.NewHandle() - - userID := utils.NewType().ParseInt(c.Locals("id")) - request := request.CompletionAnswerRequest{} - - err := handleUtil.BindRequest(c, &request) - if err != nil { - return handleUtil.HandleError(c, err) - } - - err = request.Validate() - if err != nil { - return handleUtil.HandleError(c, err) - } - - response, err := h.Controller.CheckAnswer(userID, *request.ActivityID, 3, request.Answer) - if err != nil { - return handleUtil.HandleError(c, err) - } - - return c.Status(http.StatusOK).JSON(response) -} diff --git a/handlers/handler.user.go b/handlers/handler.user.go deleted file mode 100644 index 034f1e0..0000000 --- a/handlers/handler.user.go +++ /dev/null @@ -1,201 +0,0 @@ -package handlers - -// handler.user.go -/** - * This file is a part of handler, used to handle request of the user - */ - -import ( - "DatabaseCamp/controllers" - "DatabaseCamp/middleware" - "DatabaseCamp/models/request" - "DatabaseCamp/utils" - "net/http" - - "github.com/gofiber/fiber/v2" -) - -/** - * This class handle request of the user - */ -type userHandler struct { - Controller controllers.IUserController // User controller for doing business logic of the user - Jwt middleware.IJwt // Jwt middleware for user verification -} - -/** - * Constructor creates a new userHandler instance - * - * @param controller User controller for doing business logic of the user - * @param jwt Jwt middleware for user verification - * - * @return instance of userHandler - */ -func NewUserHandler(controller controllers.IUserController, jwt middleware.IJwt) userHandler { - return userHandler{Controller: controller, Jwt: jwt} -} - -/** - * Register - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ -func (h userHandler) Register(c *fiber.Ctx) error { - handleUtil := utils.NewHandle() - - request := request.UserRequest{} - - err := handleUtil.BindRequest(c, &request) - if err != nil { - return handleUtil.HandleError(c, err) - } - - err = request.ValidateRegister() - if err != nil { - return handleUtil.HandleError(c, err) - } - - response, err := h.Controller.Register(request) - if err != nil { - return handleUtil.HandleError(c, err) - } - - token, err := h.Jwt.JwtSign(response.ID) - if err != nil { - return handleUtil.HandleError(c, err) - } - - response.AccessToken = token - - return c.Status(http.StatusOK).JSON(response) -} - -/** - * Login - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ -func (h userHandler) Login(c *fiber.Ctx) error { - handleUtil := utils.NewHandle() - - request := request.UserRequest{} - - err := handleUtil.BindRequest(c, &request) - if err != nil { - return handleUtil.HandleError(c, err) - } - - err = request.ValidateLogin() - if err != nil { - return handleUtil.HandleError(c, err) - } - - response, err := h.Controller.Login(request) - if err != nil { - return handleUtil.HandleError(c, err) - } - - token, err := h.Jwt.JwtSign(response.ID) - if err != nil { - return handleUtil.HandleError(c, err) - } - - response.AccessToken = token - - return c.Status(http.StatusOK).JSON(response) -} - -/** - * Get user profile - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ -func (h userHandler) GetProfile(c *fiber.Ctx) error { - handleUtil := utils.NewHandle() - - id := c.Params("id") - - response, err := h.Controller.GetProfile(utils.NewType().ParseInt(id)) - if err != nil { - return handleUtil.HandleError(c, err) - } - - return c.Status(http.StatusOK).JSON(response) -} - -/** - * Get own profile - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ -func (h userHandler) GetOwnProfile(c *fiber.Ctx) error { - handleUtil := utils.NewHandle() - - id := c.Locals("id") - - response, err := h.Controller.GetProfile(utils.NewType().ParseInt(id)) - if err != nil { - return handleUtil.HandleError(c, err) - } - - return c.Status(http.StatusOK).JSON(response) -} - -/** - * Get ranking - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ -func (h userHandler) GetUserRanking(c *fiber.Ctx) error { - handleUtil := utils.NewHandle() - - id := c.Locals("id") - - response, err := h.Controller.GetRanking(utils.NewType().ParseInt(id)) - if err != nil { - return handleUtil.HandleError(c, err) - } - - return c.Status(http.StatusOK).JSON(response) -} - -/** - * Edit user profile - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ -func (h userHandler) Edit(c *fiber.Ctx) error { - handleUtil := utils.NewHandle() - - request := request.UserRequest{} - userID := utils.NewType().ParseInt(c.Locals("id")) - - err := handleUtil.BindRequest(c, &request) - if err != nil { - return handleUtil.HandleError(c, err) - } - - err = request.ValidateEdit() - if err != nil { - return handleUtil.HandleError(c, err) - } - - response, err := h.Controller.EditProfile(userID, request) - if err != nil { - return handleUtil.HandleError(c, err) - } - - return c.Status(http.StatusOK).JSON(response) -} diff --git a/handlers/interface.go b/handlers/interface.go deleted file mode 100644 index 89d32ed..0000000 --- a/handlers/interface.go +++ /dev/null @@ -1,188 +0,0 @@ -package handlers - -// interface.go -/** - * This file used to be a interface of handlers - */ - -import "github.com/gofiber/fiber/v2" - -/** - * Interface to show function in exam handler that others can use - */ -type IExamHandler interface { - - /** - * Get the exam to use for the test - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ - GetExam(c *fiber.Ctx) error - - /** - * Check answer of the exam - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ - CheckExam(c *fiber.Ctx) error - - /** - * Get overview of the exam - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ - GetExamOverview(c *fiber.Ctx) error - - /** - * Get exam result of the user - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ - GetExamResult(c *fiber.Ctx) error -} - -/** - * Interface to show function in learning handler that others can use - */ -type ILearningHandler interface { - - /** - * Get content roadmap - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ - GetContentRoadmap(c *fiber.Ctx) error - - /** - * Get video lecture of the content - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ - GetVideo(c *fiber.Ctx) error - - /** - * Get content overview - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ - GetOverview(c *fiber.Ctx) error - - /** - * Get activity for user to do - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ - GetActivity(c *fiber.Ctx) error - - /** - * Use hint of the activity - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ - UseHint(c *fiber.Ctx) error - - /** - * Check matching choice answer of the activity - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ - CheckMatchingAnswer(c *fiber.Ctx) error - - /** - * Check multiple choice answer of the activity - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ - CheckMultipleAnswer(c *fiber.Ctx) error - - /** - * Check completion choice answer of the activity - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ - CheckCompletionAnswer(c *fiber.Ctx) error -} - -/** - * Interface to show function in user handler that others can use - */ -type IUserHandler interface { - - /** - * Register - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ - Register(c *fiber.Ctx) error - - /** - * Login - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ - Login(c *fiber.Ctx) error - - /** - * Get user profile - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ - GetProfile(c *fiber.Ctx) error - - /** - * Get own profile - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ - GetOwnProfile(c *fiber.Ctx) error - - /** - * Get ranking - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ - GetUserRanking(c *fiber.Ctx) error - - /** - * Edit user profile - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ - Edit(c *fiber.Ctx) error -} diff --git a/errs/errs.go b/internal/errs/errs.go similarity index 100% rename from errs/errs.go rename to internal/errs/errs.go diff --git a/errs/messages.go b/internal/errs/messages.go similarity index 74% rename from errs/messages.go rename to internal/errs/messages.go index f823a93..d6b1ca2 100644 --- a/errs/messages.go +++ b/internal/errs/messages.go @@ -27,6 +27,9 @@ const ( UPDATE_ERROR_TH = "เกิดข้อผิดพลาดในการอัพเดตข้อมูล" UPDATE_ERROR_EN = "Update data error" + + NOT_FOUD_DATA_TH = "ไม่พบข้อมูล" + NOT_FOUD_DATA_EN = "Not found data" ) // Thai and english message about exam and activity error @@ -43,6 +46,12 @@ const ( ACTIVITIES_NUMBER_INCORRECT_TH = "จำนวนของกิจกรรมไม่ถูกต้อง" ACTIVITIES_NUMBER_INCORRECT_EN = "Number of activities incorrect" + ACTIVITYID_NOTFOUND_TH = "ไม่พบรหัสของกิจกรรมในคำร้องขอ" + ACTIVITYID_NOTFOUND_EN = "Activity ID not found" + + ACTIVITY_TYPEID_NOTFOUND_TH = "ไม่พบรหัสของประเภทกิจกรรมในคำร้องขอ" + ACTIVITY_TYPEID_NOTFOUND_EN = "Activity Type ID not found" + HINTS_ALREADY_USED_TH = "ได้ใช้คำใบ้ทั้งหมดของกิจกรรมแล้ว" HINTS_ALREADY_USED_EN = "Activity hints has been used" @@ -54,6 +63,15 @@ const ( FINAL_EXAM_BAGES_NOT_ENOUGH_TH = "จำนวนเหรียญตราไม่เพียงพอในการทำข้อสอบ" FINAL_EXAM_BAGES_NOT_ENOUGH_EN = "Not enough badges to do final exam" + + ANSWER_INVALID_TH = "รูปแบบของคำตอบไม่ถูกต้อง" + ANSWER_INVALID_EN = "Answer invalid" + + ANSWER_NOT_FOUND_TH = "ไม่พบคำตอบในคำร้องขอ" + ANSWER_NOT_FOUND_EN = "Answer not found" + + ER_ANSWER_ID_NOT_FOUND_TH = "ไม่พบรหัสของคำตอบในคำร้องขอ" + ER_ANSWER_ID_NOT_FOUND_EN = "ER answer ID not found" ) // Thai and english message about user error @@ -86,9 +104,10 @@ var ( // Manipulation error var ( - ErrInsertError = NewInternalServerError(INSERT_ERROR_TH, INSERT_ERROR_EN) - ErrLoadError = NewInternalServerError(LOAD_ERROR_TH, LOAD_ERROR_EN) - ErrUpdateError = NewInternalServerError(UPDATE_ERROR_TH, UPDATE_ERROR_EN) + ErrInsertError = NewInternalServerError(INSERT_ERROR_TH, INSERT_ERROR_EN) + ErrLoadError = NewInternalServerError(LOAD_ERROR_TH, LOAD_ERROR_EN) + ErrUpdateError = NewInternalServerError(UPDATE_ERROR_TH, UPDATE_ERROR_EN) + ErrNotFoundError = NewInternalServerError(NOT_FOUD_DATA_TH, NOT_FOUD_DATA_EN) ) // Exam and Activity error @@ -96,11 +115,16 @@ var ( ErrExamNotFound = NewNotFoundError(EXAM_NOT_FOUND_TH, EXAM_NOT_FOUND_EN) ErrContentNotFound = NewNotFoundError(CONTENT_NOT_FOUND_TH, CONTENT_NOT_FOUND_EN) ErrActivitiesNotFound = NewNotFoundError(ACTIVITIES_NOT_FOUND_TH, ACTIVITIES_NOT_FOUND_EN) + ErrActivittyIDNotFound = NewNotFoundError(ACTIVITYID_NOTFOUND_TH, ACTIVITYID_NOTFOUND_EN) + ErrActivittyTypeIDNotFound = NewNotFoundError(ACTIVITY_TYPEID_NOTFOUND_TH, ACTIVITY_TYPEID_NOTFOUND_EN) ErrActivitiesNumberIncorrect = NewBadRequestError(ACTIVITIES_NUMBER_INCORRECT_TH, ACTIVITIES_NUMBER_INCORRECT_EN) ErrHintAlreadyUsed = NewBadRequestError(HINTS_ALREADY_USED_TH, HINTS_ALREADY_USED_EN) ErrHintPointsNotEnough = NewBadRequestError(HINT_POINTS_NOT_ENOUGH_TH, HINT_POINTS_NOT_ENOUGH_EN) ErrActivityTypeInvalid = NewInternalServerError(ACTIVITY_TYPE_INVALID_TH, ACTIVITY_TYPE_INVALID_EN) + ErrAnswerInvalid = NewBadRequestError(ANSWER_INVALID_TH, ANSWER_INVALID_EN) + ErrAnswerNotFound = NewBadRequestError(ANSWER_NOT_FOUND_TH, ANSWER_NOT_FOUND_EN) ErrFinalExamBadgesNotEnough = NewBadRequestError(FINAL_EXAM_BAGES_NOT_ENOUGH_TH, FINAL_EXAM_BAGES_NOT_ENOUGH_EN) + ErrERAnswerIDNotFound = NewBadRequestError(ER_ANSWER_ID_NOT_FOUND_TH, ER_ANSWER_ID_NOT_FOUND_EN) ) // User error diff --git a/internal/handler/exam.go b/internal/handler/exam.go new file mode 100644 index 0000000..753889a --- /dev/null +++ b/internal/handler/exam.go @@ -0,0 +1,87 @@ +package handler + +import ( + "database-camp/internal/infrastructure/application" + "database-camp/internal/models/request" + "database-camp/internal/services" + "database-camp/internal/utils" + "net/http" +) + +type ExamHandler interface { + GetExam(c application.Context) + GetExamOverview(c application.Context) + GetExamResult(c application.Context) + CheckExam(c application.Context) +} + +type examHandler struct { + service services.ExamService +} + +func NewExamHandler(service services.ExamService) *examHandler { + return &examHandler{service: service} +} + +func (h examHandler) GetExam(c application.Context) { + examID := utils.ParseInt(c.Params("id")) + userID := utils.ParseInt(c.Locals("id")) + + response, err := h.service.GetExam(examID, userID) + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, response) +} + +func (h examHandler) CheckExam(c application.Context) { + userID := utils.ParseInt(c.Locals("id")) + request := request.ExamAnswerRequest{} + + err := c.Bind(&request) + if err != nil { + c.Error(err) + return + } + + err = request.Validate() + if err != nil { + c.Error(err) + return + } + + response, err := h.service.CheckExam(userID, request) + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, response) +} + +func (h examHandler) GetExamOverview(c application.Context) { + userID := utils.ParseInt(c.Locals("id")) + + response, err := h.service.GetOverview(userID) + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, response) +} + +func (h examHandler) GetExamResult(c application.Context) { + userID := utils.ParseInt(c.Locals("id")) + examResultID := utils.ParseInt(c.Params("id")) + + response, err := h.service.GetExamResult(userID, examResultID) + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, response) +} diff --git a/internal/handler/learning.go b/internal/handler/learning.go new file mode 100644 index 0000000..30018a4 --- /dev/null +++ b/internal/handler/learning.go @@ -0,0 +1,153 @@ +package handler + +import ( + "database-camp/internal/infrastructure/application" + "database-camp/internal/models/request" + "database-camp/internal/services" + "database-camp/internal/utils" + "net/http" +) + +type LearningHandler interface { + GetContentRoadmap(c application.Context) + GetVideo(c application.Context) + GetOverview(c application.Context) + GetActivity(c application.Context) + GetRecommend(c application.Context) + UseHint(c application.Context) + CheckAnswer(c application.Context) + PeerReview(c application.Context) +} + +type learningHandler struct { + service services.LearningService +} + +func NewLearningHandler(service services.LearningService) *learningHandler { + return &learningHandler{service: service} +} + +func (h learningHandler) GetContentRoadmap(c application.Context) { + userID := utils.ParseInt(c.Locals("id")) + contentID := utils.ParseInt(c.Params("id")) + + response, err := h.service.GetContentRoadmap(userID, contentID) + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, response) +} + +func (h learningHandler) GetVideo(c application.Context) { + contentID := c.Params("id") + + response, err := h.service.GetVideoLecture(utils.ParseInt(contentID)) + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, response) +} + +func (h learningHandler) GetOverview(c application.Context) { + id := c.Locals("id") + + response, err := h.service.GetOverview(utils.ParseInt(id)) + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, response) +} + +func (h learningHandler) GetActivity(c application.Context) { + userID := utils.ParseInt(c.Locals("id")) + activityID := utils.ParseInt(c.Params("id")) + + response, err := h.service.GetActivity(userID, activityID) + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, response) +} + +func (h learningHandler) GetRecommend(c application.Context) { + userID := utils.ParseInt(c.Locals("id")) + + response, err := h.service.GetRecommend(userID) + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, response) +} + +func (h learningHandler) UseHint(c application.Context) { + userID := utils.ParseInt(c.Locals("id")) + activityID := utils.ParseInt(c.Params("id")) + + response, err := h.service.UseHint(userID, activityID) + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, response) +} + +func (h learningHandler) CheckAnswer(c application.Context) { + userID := utils.ParseInt(c.Locals("id")) + request := request.CheckAnswerRequest{} + + err := c.Bind(&request) + if err != nil { + c.Error(err) + return + } + + err = request.Validate() + if err != nil { + c.Error(err) + return + } + + response, err := h.service.CheckAnswer(userID, request) + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, response) +} + +func (h learningHandler) PeerReview(c application.Context) { + userID := utils.ParseInt(c.Locals("id")) + request := request.PeerReviewRequest{} + + err := c.Bind(&request) + if err != nil { + c.Error(err) + return + } + + err = request.Validate() + if err != nil { + c.Error(err) + return + } + + response, err := h.service.CheckPeerReview(userID, request) + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, response) +} diff --git a/internal/handler/user.go b/internal/handler/user.go new file mode 100644 index 0000000..7dc5b90 --- /dev/null +++ b/internal/handler/user.go @@ -0,0 +1,135 @@ +package handler + +import ( + "database-camp/internal/infrastructure/application" + "database-camp/internal/models/request" + "database-camp/internal/services" + "database-camp/internal/utils" + "net/http" +) + +type UserHandler interface { + Register(c application.Context) + Login(c application.Context) + GetProfile(c application.Context) + GetOwnProfile(c application.Context) + GetUserRanking(c application.Context) + Edit(c application.Context) +} + +type userHandler struct { + service services.UserService +} + +func NewUserHandler(service services.UserService) userHandler { + return userHandler{service: service} +} + +func (h userHandler) Register(c application.Context) { + request := request.UserRequest{} + + err := c.Bind(&request) + if err != nil { + c.Error(err) + return + } + + err = request.ValidateRegister() + if err != nil { + c.Error(err) + return + } + + response, err := h.service.Register(request) + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, response) +} + +func (h userHandler) Login(c application.Context) { + request := request.UserRequest{} + + err := c.Bind(&request) + if err != nil { + c.Error(err) + return + } + + err = request.ValidateLogin() + if err != nil { + c.Error(err) + return + } + + response, err := h.service.Login(request) + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, response) +} + +func (h userHandler) GetProfile(c application.Context) { + id := utils.ParseInt(c.Params("id")) + + response, err := h.service.GetProfile(id) + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, response) +} + +func (h userHandler) GetOwnProfile(c application.Context) { + id := utils.ParseInt(c.Locals("id")) + + response, err := h.service.GetProfile(id) + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, response) +} + +func (h userHandler) GetUserRanking(c application.Context) { + id := utils.ParseInt(c.Locals("id")) + + response, err := h.service.GetRanking(id) + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, response) +} + +func (h userHandler) Edit(c application.Context) { + request := request.UserRequest{} + userID := utils.ParseInt(c.Locals("id")) + + err := c.Bind(&request) + if err != nil { + c.Error(err) + return + } + + err = request.ValidateEdit() + if err != nil { + c.Error(err) + return + } + + response, err := h.service.EditProfile(userID, request) + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, response) +} diff --git a/internal/infrastructure/application/app.go b/internal/infrastructure/application/app.go new file mode 100644 index 0000000..04ffe71 --- /dev/null +++ b/internal/infrastructure/application/app.go @@ -0,0 +1,29 @@ +package application + +type App interface { + Get(path string, handles ...func(Context)) + Post(path string, handles ...func(Context)) + Put(path string, handles ...func(Context)) + Delete(path string, handle ...func(Context)) + Group(path string, handles ...func(Context)) Router +} + +type Router interface { + Get(path string, handles ...func(Context)) + Post(path string, handles ...func(Context)) + Put(path string, handles ...func(Context)) + Delete(path string, handle ...func(Context)) + Group(path string, handles ...func(Context)) Router +} + +type Context interface { + Bind(v interface{}) error + JSON(statuscode int, v interface{}) + Error(err error) error + Params(key string, defaultValue ...string) string + Locals(key string, value ...interface{}) (val interface{}) + + Next() error + + GetHeader(key string) string +} diff --git a/internal/infrastructure/application/fiber.go b/internal/infrastructure/application/fiber.go new file mode 100644 index 0000000..359738a --- /dev/null +++ b/internal/infrastructure/application/fiber.go @@ -0,0 +1,152 @@ +package application + +import ( + "database-camp/internal/errs" + "database-camp/internal/logs" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" + "github.com/gofiber/fiber/v2/middleware/logger" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func toFiberHandle(handle func(Context)) func(c *fiber.Ctx) error { + return func(c *fiber.Ctx) error { + handle(NewFiberCtx(c)) + return nil + } +} + +func toFiberHandles(handles []func(Context)) []func(c *fiber.Ctx) error { + fiberHandles := make([]func(c *fiber.Ctx) error, len(handles), cap(handles)) + for index := range handles { + fiberHandles[index] = toFiberHandle(handles[index]) + } + return fiberHandles +} + +type FiberApp struct { + *fiber.App +} + +func NewFiberApp() *FiberApp { + r := fiber.New(fiber.Config{ + Prefork: true, + CaseSensitive: true, + StrictRouting: true, + ServerHeader: "Fiber", + AppName: "Database Camp", + }) + + r.Use(cors.New()) + r.Use(logger.New()) + r.Use(recover.New()) + + return &FiberApp{r} +} + +func (r *FiberApp) Post(path string, handles ...func(Context)) { + r.App.Post(path, toFiberHandles(handles)...) +} + +func (r *FiberApp) Get(path string, handles ...func(Context)) { + r.App.Get(path, toFiberHandles(handles)...) +} + +func (r *FiberApp) Put(path string, handles ...func(Context)) { + r.App.Put(path, toFiberHandles(handles)...) +} + +func (r *FiberApp) Delete(path string, handles ...func(Context)) { + r.App.Delete(path, toFiberHandles(handles)...) +} + +func (r *FiberApp) Group(path string, handles ...func(Context)) Router { + router := r.App.Group(path, toFiberHandles(handles)...) + return NewFiberRouter(router) +} + +type FiberCtx struct { + *fiber.Ctx +} + +func NewFiberCtx(c *fiber.Ctx) *FiberCtx { + return &FiberCtx{Ctx: c} +} + +func (c *FiberCtx) Next() error { + return c.Ctx.Next() +} + +func (c *FiberCtx) GetHeader(key string) string { + return c.Ctx.Get(key) +} + +func (c *FiberCtx) Locals(key string, value ...interface{}) (val interface{}) { + return c.Ctx.Locals(key, value...) +} + +func (c *FiberCtx) Params(key string, defaultValue ...string) string { + return c.Ctx.Params(key, defaultValue...) +} + +func (c *FiberCtx) Bind(v interface{}) error { + err := c.BodyParser(v) + if err != nil { + logs.GetInstance().Error(err) + return errs.ErrBadRequestError + } + return nil +} + +func (c *FiberCtx) JSON(statuscode int, v interface{}) { + c.Ctx.Status(statuscode).JSON(v) +} + +func (c *FiberCtx) Error(err error) error { + + type message struct { + Th string `json:"th_message"` + En string `json:"en_message"` + } + + switch e := err.(type) { + case errs.AppError: + return c.Status(e.Code).JSON(message{Th: e.ThMessage, En: e.EnMessage}) + case error: + return c.Status(fiber.StatusInternalServerError).JSON(message{ + Th: errs.INTERNAL_SERVER_ERROR_TH, + En: errs.INTERNAL_SERVER_ERROR_EN, + }) + } + return nil +} + +type FiberRouter struct { + fiber.Router +} + +func NewFiberRouter(r fiber.Router) FiberRouter { + return FiberRouter{Router: r} +} + +func (r FiberRouter) Post(path string, handles ...func(Context)) { + r.Router.Post(path, toFiberHandles(handles)...) +} + +func (r FiberRouter) Get(path string, handles ...func(Context)) { + r.Router.Get(path, toFiberHandles(handles)...) +} + +func (r FiberRouter) Put(path string, handles ...func(Context)) { + r.Router.Put(path, toFiberHandles(handles)...) +} + +func (r FiberRouter) Delete(path string, handles ...func(Context)) { + r.Router.Delete(path, toFiberHandles(handles)...) +} + +func (r FiberRouter) Group(path string, handles ...func(Context)) Router { + r.Router = r.Router.Group(path, toFiberHandles(handles)...) + return r +} diff --git a/internal/infrastructure/cache/cache.go b/internal/infrastructure/cache/cache.go new file mode 100644 index 0000000..902acb0 --- /dev/null +++ b/internal/infrastructure/cache/cache.go @@ -0,0 +1,8 @@ +package cache + +import "time" + +type Cache interface { + Get(key string) (string, error) + Set(key string, value interface{}, expiration time.Duration) error +} diff --git a/internal/infrastructure/cache/redis.go b/internal/infrastructure/cache/redis.go new file mode 100644 index 0000000..05e754c --- /dev/null +++ b/internal/infrastructure/cache/redis.go @@ -0,0 +1,28 @@ +package cache + +import ( + "context" + "time" + + "github.com/go-redis/redis/v8" +) + +type redisClient struct { + *redis.Client +} + +func NewRedisClient() *redisClient { + client := redis.NewClient(&redis.Options{ + Addr: "redis:6379", + }) + + return &redisClient{client} +} + +func (c *redisClient) Get(key string) (string, error) { + return c.Client.Get(context.Background(), key).Result() +} + +func (c *redisClient) Set(key string, value interface{}, expiration time.Duration) error { + return c.Client.Set(context.Background(), key, value, time.Second*10).Err() +} diff --git a/internal/infrastructure/database/mysql.go b/internal/infrastructure/database/mysql.go new file mode 100644 index 0000000..c0d012b --- /dev/null +++ b/internal/infrastructure/database/mysql.go @@ -0,0 +1,67 @@ +package database + +import ( + "fmt" + "os" + + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +type MysqlDB interface { + GetDB() *gorm.DB + OpenConnection() error + CloseConnection() error +} + +type mysqlDB struct { + db *gorm.DB +} + +var instantiated *mysqlDB = nil + +func GetMySqlDBInstance() *mysqlDB { + if instantiated == nil { + instantiated = new(mysqlDB) + } + return instantiated +} + +func getDBName() string { + if os.Getenv("MODE") == "develop" { + return os.Getenv("DB_NAME_DEVELOP") + } else { + return os.Getenv("DB_NAME_PRODUCTION") + } +} + +func getDSN() string { + return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", + os.Getenv("DB_USERNAME"), + os.Getenv("DB_PASSWORD"), + os.Getenv("DB_HOST"), + os.Getenv("DB_PORT"), + getDBName(), + ) +} + +func (db *mysqlDB) GetDB() *gorm.DB { + return db.db +} + +func (db *mysqlDB) OpenConnection() error { + var err error + dsn := getDSN() + sql := mysql.Open(dsn) + db.db, err = gorm.Open(sql) + return err +} + +func (db *mysqlDB) CloseConnection() error { + sql, err := db.db.DB() + if err != nil { + return err + } + err = sql.Close() + return err +} diff --git a/internal/infrastructure/environment/env.go b/internal/infrastructure/environment/env.go new file mode 100644 index 0000000..1763201 --- /dev/null +++ b/internal/infrastructure/environment/env.go @@ -0,0 +1,15 @@ +package environment + +import ( + "github.com/joho/godotenv" +) + +type dotEnv struct{} + +func New() *dotEnv { + return &dotEnv{} +} + +func (env dotEnv) Load(path string) error { + return godotenv.Load(path) +} diff --git a/services/service.cloud_storage.go b/internal/infrastructure/storage/cloud_storage.go similarity index 98% rename from services/service.cloud_storage.go rename to internal/infrastructure/storage/cloud_storage.go index b13e2eb..28c46f0 100644 --- a/services/service.cloud_storage.go +++ b/internal/infrastructure/storage/cloud_storage.go @@ -1,4 +1,4 @@ -package services +package storage import ( "io/ioutil" diff --git a/logs/logs.go b/internal/logs/logs.go similarity index 79% rename from logs/logs.go rename to internal/logs/logs.go index 4e189ac..5936750 100644 --- a/logs/logs.go +++ b/internal/logs/logs.go @@ -6,6 +6,8 @@ package logs */ import ( + "fmt" + "go.uber.org/zap" "go.uber.org/zap/zapcore" ) @@ -25,12 +27,18 @@ var instantiated *log = nil * * @return instance of log */ -func New() *log { +func GetInstance() *log { var buildedLog *zap.Logger + var err error + if instantiated == nil { - buildedLog, _ = initLog() + buildedLog, err = initLog() + if err != nil { + panic(err) + } instantiated = &log{log: buildedLog} } + return instantiated } @@ -54,8 +62,8 @@ func initLog() (*zap.Logger, error) { * @param message infomation log message * @param fields optional for used to add a key-value pair to a logger's context */ -func (l log) Info(message string, fields ...zapcore.Field) { - l.log.Info(message, fields...) +func (l log) Info(message interface{}) { + l.log.Info(fmt.Sprint(message)) } /** @@ -64,8 +72,8 @@ func (l log) Info(message string, fields ...zapcore.Field) { * @param message debug log message * @param fields optional for used to add a key-value pair to a logger's context */ -func (l log) Debug(message string, fields ...zapcore.Field) { - l.log.Debug(message, fields...) +func (l log) Debug(message string) { + l.log.Debug(message) } /** @@ -74,11 +82,11 @@ func (l log) Debug(message string, fields ...zapcore.Field) { * @param message error log message * @param fields optional for used to add a key-value pair to a logger's context */ -func (l log) Error(message interface{}, fields ...zapcore.Field) { +func (l log) Error(message interface{}) { switch v := message.(type) { case error: - l.log.Error(v.Error(), fields...) + l.log.Error(v.Error()) case string: - l.log.Error(v, fields...) + l.log.Error(v) } } diff --git a/internal/middleware/jwt/jwt.go b/internal/middleware/jwt/jwt.go new file mode 100644 index 0000000..919010b --- /dev/null +++ b/internal/middleware/jwt/jwt.go @@ -0,0 +1,146 @@ +package jwt + +import ( + "database-camp/internal/errs" + "database-camp/internal/infrastructure/application" + "database-camp/internal/logs" + "database-camp/internal/repositories" + "database-camp/internal/utils" + "fmt" + "os" + "strings" + "time" + + "github.com/golang-jwt/jwt" + uuid "github.com/satori/go.uuid" +) + +type Jwt interface { + Sign(id int) (string, error) + Verify(application.Context) +} +type jwtMiddleware struct { + repo repositories.UserRepository +} + +func New(repo repositories.UserRepository) jwtMiddleware { + return jwtMiddleware{repo: repo} +} + +func (j jwtMiddleware) Sign(id int) (string, error) { + atClaims := jwt.MapClaims{ + "id": id, + "secret": uuid.NewV4(), + } + + at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims) + + token, err := at.SignedString([]byte(os.Getenv("JWT_SECRET"))) + if err != nil { + return "", err + } + + err = j.updateToken(id, token) + if err != nil { + return "", err + } + + return token, nil +} + +func (j jwtMiddleware) Verify(c application.Context) { + bearer, err := j.jwtFromHeader(c) + if err != nil { + logs.GetInstance().Error(err) + c.Error(err) + return + } + + token, err := jwt.Parse(bearer, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + EnMessage := fmt.Sprintf("unexpected signing method: %v", token.Header["alg"]) + ThMessage := fmt.Sprintf("วิธีการลงนามที่ไม่คาดคิด: %v", token.Header["alg"]) + return nil, c.Error(errs.NewForbiddenError(ThMessage, EnMessage)) + } + + return []byte(os.Getenv("JWT_SECRET")), nil + }) + if err != nil { + logs.GetInstance().Error(err) + c.Error(errs.NewForbiddenError("โทเค็นไม่ถูกต้อง", "Token Invalid")) + return + } + + claims, err := j.getClaims(token) + if err != nil { + logs.GetInstance().Error(err) + c.Error(err) + return + } + + id := utils.ParseInt(claims["id"]) + + if !j.validUser(bearer, id) { + c.Error(errs.NewForbiddenError("โทเค็นไม่ถูกต้อง", "Token Invalid")) + return + } + + err = j.updateToken(id, bearer) + if err != nil { + logs.GetInstance().Error(err) + c.Error(errs.NewInternalServerError("เกิดข้อผิดพลาด", "Internal Server Error")) + return + } + + j.setClaims(c, claims) + c.Next() +} + +func (j jwtMiddleware) updateToken(id int, token string) error { + tokenExpireHour := time.Hour * utils.ParseDuration(os.Getenv("TOKEN_EXPIRE_HOUR")) + expiredTokenTimestamp := time.Now().Local().Add(tokenExpireHour) + err := j.repo.UpdatesByID(id, map[string]interface{}{ + "access_token": token, + "expired_token_timestamp": expiredTokenTimestamp, + }) + return err +} + +func (j jwtMiddleware) getClaims(token *jwt.Token) (jwt.MapClaims, error) { + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return claims, errs.NewForbiddenError("โทเค็นไม่ถูกต้อง", "Token Invalid") + } else { + return claims, nil + } +} + +func (j jwtMiddleware) setClaims(c application.Context, claims jwt.MapClaims) { + for k, v := range claims { + if k != "secret" { + c.Locals(k, utils.ParseString(v)) + } + } +} + +func (j jwtMiddleware) validUser(token string, id int) bool { + user, err := j.repo.GetUserByID(id) + if err != nil || user == nil { + return false + } + + if user.AccessToken != token || user.ExpiredTokenTimestamp.Before(time.Now().Local()) { + return false + } + + return true +} + +func (j jwtMiddleware) jwtFromHeader(c application.Context) (string, error) { + auth := c.GetHeader("Authorization") + l := len("Bearer") + if len(auth) > l+1 && strings.EqualFold(auth[:l], "Bearer") { + return auth[l+1:], nil + } + return "", errs.NewBadRequestError("ไม่พบ JWT Token ในส่วนหัวของคำร้องขอ", "JWT Token Not found") +} diff --git a/internal/models/entities/activity/activity.go b/internal/models/entities/activity/activity.go new file mode 100644 index 0000000..b657ac2 --- /dev/null +++ b/internal/models/entities/activity/activity.go @@ -0,0 +1,37 @@ +package activity + +import "database-camp/internal/models/entities/content" + +type ContentRoadmapItem struct { + ActivityID int `json:"activity_id"` + IsLearned bool `json:"is_learned"` + Order int `json:"order"` +} + +type Activity struct { + ID int `gorm:"primaryKey;column:activity_id" json:"activity_id"` + TypeID int `gorm:"column:activity_type_id" json:"activity_type_id"` + ContentID *int `gorm:"column:content_id" json:"content_id"` + Order int `gorm:"column:activity_order" json:"activity_order"` + Story string `gorm:"column:story" json:"story"` + Point int `gorm:"column:point" json:"point"` + Question string `gorm:"column:question" json:"question"` +} + +func (activity Activity) IsAnswerCorrect(answer interface{}, choices Choices) bool { + return true +} + +type Activities []Activity + +func (activities Activities) GetContentRoadmap(progression content.LearningProgressionList) (items []ContentRoadmapItem) { + for _, activity := range activities { + isLearned := progression.IsLearned(activity.ID) + items = append(items, ContentRoadmapItem{ + ActivityID: activity.ID, + IsLearned: isLearned, + Order: activity.Order, + }) + } + return +} diff --git a/internal/models/entities/activity/answer.go b/internal/models/entities/activity/answer.go new file mode 100644 index 0000000..73e0ffa --- /dev/null +++ b/internal/models/entities/activity/answer.go @@ -0,0 +1,308 @@ +package activity + +import ( + "database-camp/internal/errs" + "database-camp/internal/utils" +) + +func FormatAnswer(answer interface{}, activityTypeID int) (Answer, error) { + switch activityTypeID { + case 1: + var matchingChoiceAnswer MatchingChoiceAnswer + err := utils.StructToStruct(answer, &matchingChoiceAnswer) + return matchingChoiceAnswer, err + case 2: + var multipleChoiceAnswer MultipleChoiceAnswer + err := utils.StructToStruct(answer, &multipleChoiceAnswer) + return multipleChoiceAnswer, err + case 3: + var completionChoiceAnswer CompletionChoiceAnswer + err := utils.StructToStruct(answer, &completionChoiceAnswer) + return completionChoiceAnswer, err + case 4: + var vocabGroupChoiceAnswer VocabGroupChoiceAnswer + err := utils.StructToStruct(answer, &vocabGroupChoiceAnswer) + return vocabGroupChoiceAnswer, err + case 5: + var dependencyChoiceAnswer DependencyChoiceAnswer + err := utils.StructToStruct(answer, &dependencyChoiceAnswer) + return dependencyChoiceAnswer, err + default: + return nil, errs.ErrActivityTypeInvalid + } +} + +type Answer interface { + IsCorrect(choices Choices) (bool, error) +} + +type MatchingItem struct { + Item1 string `json:"item1"` + Item2 string `json:"item2"` +} + +type MatchingChoiceAnswer []MatchingItem + +func (answer MatchingChoiceAnswer) IsCorrect(choices Choices) (bool, error) { + matchingChoices, ok := choices.(MatchingChoices) + if !ok { + return false, errs.ErrAnswerInvalid + } + + Item1Item2Map := map[string]string{} + for _, correct := range matchingChoices { + Item1Item2Map[correct.PairItem1] = correct.PairItem2 + } + + for _, item := range answer { + if Item1Item2Map[item.Item1] != item.Item2 && Item1Item2Map[item.Item2] != item.Item1 { + return false, nil + } + } + + return true, nil +} + +type MultipleChoiceAnswer []int + +func (answer MultipleChoiceAnswer) IsCorrect(choices Choices) (bool, error) { + multipleChoices, ok := choices.(MultipleChoices) + if !ok { + return false, errs.ErrAnswerInvalid + } + + countCorrect := 0 + + solution := map[int]bool{} + for _, choice := range multipleChoices { + solution[choice.ID] = choice.IsCorrect + + if choice.IsCorrect { + countCorrect++ + } + } + + if countCorrect != len(answer) { + return false, nil + } + + for _, v := range answer { + + if _, ok := solution[v]; !ok { + return false, nil + } + + if !solution[v] { + return false, nil + } + + } + + return true, nil +} + +type completionItem struct { + ID *int `json:"completion_choice_id"` + Content *string `json:"content"` +} + +type CompletionChoiceAnswer []completionItem + +func (answer CompletionChoiceAnswer) IsCorrect(choices Choices) (bool, error) { + completionChoices, ok := choices.(CompletionChoices) + if !ok { + return false, errs.ErrAnswerInvalid + } + + for _, choice := range completionChoices { + for _, v := range answer { + if (choice.ID == *v.ID) && (choice.Content != *v.Content) { + return false, nil + } + } + } + + return true, nil +} + +type VocabGroupChoiceAnswer struct { + Groups []VocabGroup `json:"groups"` +} + +func (answer VocabGroupChoiceAnswer) IsCorrect(choices Choices) (bool, error) { + + vocabGroupChoice, ok := choices.(VocabGroupChoice) + if !ok { + return false, errs.ErrAnswerInvalid + } + + if len(answer.Groups) != len(vocabGroupChoice.Groups) { + return false, nil + } + + solution := map[string]map[string]bool{} + for _, choice := range vocabGroupChoice.Groups { + if _, ok := solution[choice.GroupName]; !ok { + solution[choice.GroupName] = map[string]bool{} + } + + for _, vocab := range choice.Vocabs { + solution[choice.GroupName][vocab] = true + } + } + + for _, group := range answer.Groups { + + if len(solution[group.GroupName]) != len(group.Vocabs) { + return false, nil + } + + for _, vocab := range group.Vocabs { + if _, ok := solution[group.GroupName]; !ok { + return false, nil + } + + if !solution[group.GroupName][vocab] { + return false, nil + } + } + } + + return true, nil +} + +type DependencyChoiceAnswer []Dependency + +func (answer DependencyChoiceAnswer) IsCorrect(choices Choices) (bool, error) { + choice, ok := choices.(DependencyChoice) + if !ok { + return false, errs.ErrAnswerInvalid + } + + solution := map[string]map[string]bool{} + for _, dependency := range choice.Dependencies { + for _, determinant := range dependency.Determinants { + if _, ok := solution[dependency.Dependent]; !ok { + solution[dependency.Dependent] = map[string]bool{} + } + + solution[dependency.Dependent][determinant.Value] = true + } + } + + for _, dependency := range answer { + for _, determinant := range dependency.Determinants { + if _, ok := solution[dependency.Dependent]; !ok { + return false, nil + } + + if !solution[dependency.Dependent][determinant.Value] { + return false, nil + } + } + } + + return true, nil +} + +type ERChoiceAnswer struct { + Tables Tables `json:"tables"` + Relationships Relationships `json:"relationships"` +} + +func (answer ERChoiceAnswer) IsCorrect(choice ERChoice) (bool, string) { + + if len(answer.Tables) < len(choice.Tables) { + return false, RelationSuggestions[SUGGESTION_LESS_RELATION] + } + + if len(answer.Tables) > len(choice.Tables) { + return false, RelationSuggestions[SUGGESTION_MORE_RELATION] + } + + if len(answer.Relationships) != len(choice.Relationships) { + return false, RelationshipSuggestions[SUGGESTION_INCORRECT_NUMBER_RELATIONSHIP] + } + + tableSolutionMap := map[string]map[string]*Attribute{} + for _, table := range choice.Tables { + tableSolutionMap[table.Title] = map[string]*Attribute{} + for _, attribute := range table.Attributes { + if attribute != (Attribute{}) { + tableSolutionMap[table.Title][attribute.Value] = &attribute + } + } + + } + + idMap := map[string]string{} + for _, a := range answer.Tables { + for _, s := range choice.Tables { + if a.Title == s.Title { + idMap[a.ID] = s.ID + } + } + } + + if len(idMap) != len(answer.Tables) { + return false, RelationSuggestions[SUGGESTION_INCORRECT_RELATION] + } + + for _, table := range answer.Tables { + if _, ok := tableSolutionMap[table.Title]; !ok { + return false, RelationSuggestions[SUGGESTION_INCORRECT_RELATION] + } else { + + if len(table.Attributes) < len(tableSolutionMap[table.Title]) { + return false, AttributeSuggestions[SUGGESTION_LESS_ATTRIBUTE] + } + + if len(table.Attributes) > len(tableSolutionMap[table.Title]) { + return false, AttributeSuggestions[SUGGESTION_MORE_ATTRIBUTE] + } + + for _, attribute := range table.Attributes { + if _, ok := tableSolutionMap[table.Title][attribute.Value]; !ok { + + return false, AttributeSuggestions[SUGGESTION_INCORRECT_ATTRIBUTE] + } else { + + if attribute.Key == nil && tableSolutionMap[table.Title][attribute.Value].Key != attribute.Key { + return false, AttributeSuggestions[SUGGESTION_INCORRECT_KEY_ATTRIBUTE] + } + + if tableSolutionMap[table.Title][attribute.Value].Key != nil && *tableSolutionMap[table.Title][attribute.Value].Key != *attribute.Key { + return false, AttributeSuggestions[SUGGESTION_INCORRECT_KEY_ATTRIBUTE] + } + + } + } + + } + } + + relationshipMap := map[string]map[string]bool{} + for _, r := range choice.Relationships { + if _, ok := relationshipMap[r.Table1ID]; !ok { + relationshipMap[r.Table1ID] = map[string]bool{} + } + + relationshipMap[r.Table1ID][r.Table2ID] = true + } + + for _, a := range answer.Relationships { + if !relationshipMap[idMap[a.Table1ID]][idMap[a.Table2ID]] { + return false, RelationshipSuggestions[SUGGESTION_INCORRECT_RELATIONSHIP] + } + } + + for _, s := range choice.Relationships { + for _, a := range answer.Relationships { + if s.Table1ID == idMap[a.Table1ID] && s.Table2ID == idMap[a.Table1ID] { + return false, RelationshipSuggestions[SUGGESTION_INVALID_TYPE_RELATIONSHIP] + } + } + } + + return true, "" +} diff --git a/internal/models/entities/activity/choice.go b/internal/models/entities/activity/choice.go new file mode 100644 index 0000000..45601e1 --- /dev/null +++ b/internal/models/entities/activity/choice.go @@ -0,0 +1,454 @@ +package activity + +import ( + "database-camp/internal/logs" + "database-camp/internal/utils" +) + +type Choices interface { + CreatePropositionChoices() interface{} +} + +type MultipleChoice struct { + ID int `gorm:"primaryKey;column:multiple_choice_id" json:"multiple_choice_id"` + Content string `gorm:"column:content" json:"content"` + IsCorrect bool `gorm:"column:is_correct" json:"is_correct"` +} + +type MultipleChoices []MultipleChoice + +func (choices MultipleChoices) CreatePropositionChoices() interface{} { + countCorrect := 0 + preparedChoices := make([]map[string]interface{}, 0) + + utils.Shuffle(choices) + + for _, v := range choices { + if v.IsCorrect { + countCorrect++ + } + + preparedChoice, _ := utils.StructToMap(v) + delete(preparedChoice, "is_correct") + preparedChoices = append(preparedChoices, preparedChoice) + } + + isMultipleAnswers := countCorrect > 1 + + result := map[string]interface{}{ + "is_multiple_answers": isMultipleAnswers, + "choices": preparedChoices, + } + + return result +} + +type CompletionChoice struct { + ID int `gorm:"primaryKey;column:completion_choice_id" json:"completion_choice_id"` + Content string `gorm:"column:content" json:"content"` + QuestionFirst string `gorm:"column:question_first" json:"question_first"` + QuestionLast string `gorm:"column:question_last" json:"question_last"` +} + +type CompletionChoices []CompletionChoice + +func (choices CompletionChoices) CreatePropositionChoices() interface{} { + contents := make([]interface{}, 0) + questions := make([]interface{}, 0) + + for _, v := range choices { + contents = append(contents, v.Content) + questions = append(questions, map[string]interface{}{ + "id": v.ID, + "first": v.QuestionFirst, + "last": v.QuestionLast, + }) + } + + utils.Shuffle(contents) + utils.Shuffle(questions) + + prepared := map[string]interface{}{ + "contents": contents, + "questions": questions, + } + + return prepared +} + +type MatchingChoice struct { + ID int `gorm:"primaryKey;column:matching_choice_id" json:"matching_choice_id"` + PairItem1 string `gorm:"column:pair_item1" json:"pair_item1"` + PairItem2 string `gorm:"column:pair_item2" json:"pair_item2"` +} + +type MatchingChoices []MatchingChoice + +func (choices MatchingChoices) CreatePropositionChoices() interface{} { + pairItem1List := make([]interface{}, 0) + pairItem2List := make([]interface{}, 0) + + for _, v := range choices { + pairItem1List = append(pairItem1List, v.PairItem1) + pairItem2List = append(pairItem2List, v.PairItem2) + } + + utils.Shuffle(pairItem1List) + utils.Shuffle(pairItem2List) + + prepared := map[string]interface{}{ + "items_left": pairItem1List, + "items_right": pairItem2List, + } + + return prepared +} + +type VocabGroup struct { + GroupName string `json:"group_name"` + Vocabs []string `json:"vocabs"` +} + +type VocabGroupChoice struct { + Groups []VocabGroup `json:"groups"` +} + +func (choice VocabGroupChoice) CreatePropositionChoices() interface{} { + groups := make([]string, 0) + vocabs := make([]string, 0) + + for _, v := range choice.Groups { + groups = append(groups, v.GroupName) + vocabs = append(vocabs, v.Vocabs...) + + } + + preparedChoices := map[string]interface{}{ + "groups": groups, + "vocabs": vocabs, + } + + return preparedChoices +} + +type Determinant struct { + Value string `gorm:"column:value" json:"value"` + Fixed bool `gorm:"column:fixed" json:"-"` +} + +func (Determinant) TableName() string { + return "Determinant" +} + +type Dependency struct { + Dependent string `gorm:"column:dependent" json:"dependent"` + Fixed bool `gorm:"column:fixed" json:"-"` + Determinants []Determinant `gorm:"foreignKey:determinant_id" json:"determinants"` +} + +func (Dependency) TableName() string { + return "Dependency" +} + +type DependencyChoice struct { + ID int `gorm:"column:dependency_choice_id"` + Dependencies []Dependency `gorm:"foreignKey:dependency_id"` +} + +func (DependencyChoice) TableName() string { + return "DependencyChoice" +} + +func (choice DependencyChoice) CreatePropositionChoices() interface{} { + + type dependency struct { + Dependent *string `json:"dependent"` + DeterminantsCount int `json:"determinants_count"` + Determinants []string `json:"determinants"` + } + + type result struct { + Vocabs []string `json:"vocabs"` + Dependencies []dependency `json:"dependencies"` + } + + vocabs := make([]string, 0) + dependencies := make([]dependency, 0) + + for _, v := range choice.Dependencies { + dependencyResult := dependency{ + DeterminantsCount: len(v.Determinants), + Determinants: make([]string, 0), + } + + if v.Fixed { + ch := v.Dependent + dependencyResult.Dependent = &ch + } else { + vocabs = append(vocabs, v.Dependent) + } + + for _, d := range v.Determinants { + if d.Fixed { + dependencyResult.Determinants = append(dependencyResult.Determinants, d.Value) + } else { + vocabs = append(vocabs, d.Value) + } + } + + dependencies = append(dependencies, dependencyResult) + } + + utils.Shuffle(vocabs) + utils.Shuffle(dependencies) + + propositionChoices := result{ + Vocabs: vocabs, + Dependencies: dependencies, + } + + return propositionChoices +} + +const ( + ER_CHOICE_FILL_TABLE = "FILL_TABLE" + ER_CHOICE_DRAW = "DRAW" +) + +type ERChoice struct { + Type string `gorm:"column:type" json:"-"` + Tables Tables `json:"tables"` + Relationships Relationships `json:"relationships"` +} + +type ProblemGroups []ProblemGroup + +func (g ProblemGroups) Compare(list []string) bool { + + solution := map[string]bool{} + for _, p := range g { + for _, c := range p.Choices { + solution[c] = true + } + } + + if len(solution) != len(list) { + return false + } + + for _, c := range list { + if !solution[c] { + return false + } + } + + return true +} + +func (choice ERChoice) GetSuggestionsList(answer ERChoiceAnswer) ProblemGroups { + problemMap := map[string][]string{ + SUGGESTION_RELATION_GROUP: make([]string, 0), + SUGGESTION_RELATIONSHIP_GROUP: make([]string, 0), + SUGGESTION_ATTRIBUTE_GROUP: make([]string, 0), + } + + if len(answer.Tables) < len(choice.Tables) { + problemMap[SUGGESTION_RELATION_GROUP] = append(problemMap[SUGGESTION_RELATION_GROUP], RelationSuggestions[SUGGESTION_LESS_RELATION]) + } + + if len(answer.Tables) > len(choice.Tables) { + problemMap[SUGGESTION_RELATION_GROUP] = append(problemMap[SUGGESTION_RELATION_GROUP], RelationSuggestions[SUGGESTION_MORE_RELATION]) + } + + if len(answer.Relationships) != len(choice.Relationships) { + problemMap[SUGGESTION_RELATIONSHIP_GROUP] = append(problemMap[SUGGESTION_RELATIONSHIP_GROUP], RelationshipSuggestions[SUGGESTION_INCORRECT_NUMBER_RELATIONSHIP]) + } + + tableSolutionMap := map[string]map[string]Attribute{} + for _, table := range choice.Tables { + tableSolutionMap[table.Title] = map[string]Attribute{} + for _, attribute := range table.Attributes { + tableSolutionMap[table.Title][attribute.Value] = attribute + } + + } + + idMap := map[string]string{} + for _, a := range answer.Tables { + for _, s := range choice.Tables { + if a.Title == s.Title { + idMap[a.ID] = s.ID + } + } + } + + logs.GetInstance().Info(idMap) + + if len(idMap) != len(choice.Tables) { + problemMap[SUGGESTION_RELATION_GROUP] = append(problemMap[SUGGESTION_RELATION_GROUP], RelationSuggestions[SUGGESTION_INCORRECT_RELATION]) + } + + for _, table := range answer.Tables { + if _, ok := tableSolutionMap[table.Title]; !ok { + problemMap[SUGGESTION_RELATION_GROUP] = append(problemMap[SUGGESTION_RELATION_GROUP], RelationSuggestions[SUGGESTION_INCORRECT_RELATION]) + } else { + + if len(table.Attributes) < len(tableSolutionMap[table.Title]) { + problemMap[SUGGESTION_ATTRIBUTE_GROUP] = append(problemMap[SUGGESTION_ATTRIBUTE_GROUP], AttributeSuggestions[SUGGESTION_LESS_ATTRIBUTE]) + } + + if len(table.Attributes) > len(tableSolutionMap[table.Title]) { + problemMap[SUGGESTION_ATTRIBUTE_GROUP] = append(problemMap[SUGGESTION_ATTRIBUTE_GROUP], AttributeSuggestions[SUGGESTION_MORE_ATTRIBUTE]) + } + + for _, attribute := range table.Attributes { + if _, ok := tableSolutionMap[table.Title][attribute.Value]; !ok { + problemMap[SUGGESTION_ATTRIBUTE_GROUP] = append(problemMap[SUGGESTION_ATTRIBUTE_GROUP], AttributeSuggestions[SUGGESTION_INCORRECT_ATTRIBUTE]) + } else { + + if attribute.Key == nil && tableSolutionMap[table.Title][attribute.Value].Key != attribute.Key { + problemMap[SUGGESTION_ATTRIBUTE_GROUP] = append(problemMap[SUGGESTION_ATTRIBUTE_GROUP], AttributeSuggestions[SUGGESTION_INCORRECT_KEY_ATTRIBUTE]) + } else if tableSolutionMap[table.Title][attribute.Value].Key != nil && *tableSolutionMap[table.Title][attribute.Value].Key != *attribute.Key { + problemMap[SUGGESTION_ATTRIBUTE_GROUP] = append(problemMap[SUGGESTION_ATTRIBUTE_GROUP], AttributeSuggestions[SUGGESTION_INCORRECT_KEY_ATTRIBUTE]) + } + + } + } + + } + } + + relationshipMap := map[string]map[string]bool{} + for _, r := range choice.Relationships { + if _, ok := relationshipMap[r.Table1ID]; !ok { + relationshipMap[r.Table1ID] = map[string]bool{} + } + + relationshipMap[r.Table1ID][r.Table2ID] = true + } + + for _, a := range answer.Relationships { + if !relationshipMap[idMap[a.Table1ID]][idMap[a.Table2ID]] { + problemMap[SUGGESTION_RELATIONSHIP_GROUP] = append(problemMap[SUGGESTION_RELATIONSHIP_GROUP], RelationshipSuggestions[SUGGESTION_INCORRECT_RELATIONSHIP]) + } + } + + for _, s := range choice.Relationships { + for _, a := range answer.Relationships { + if s.Table1ID == idMap[a.Table1ID] && s.Table2ID == idMap[a.Table1ID] { + problemMap[SUGGESTION_RELATIONSHIP_GROUP] = append(problemMap[SUGGESTION_RELATIONSHIP_GROUP], RelationshipSuggestions[SUGGESTION_INVALID_TYPE_RELATIONSHIP]) + } + } + } + + problemGroup := make([]ProblemGroup, 0) + + for i, v := range problemMap { + problemGroup = append(problemGroup, ProblemGroup{ + Name: i, + Choices: v, + }) + } + + logs.GetInstance().Info(problemGroup) + + return problemGroup + +} + +func (choice ERChoice) CreatePropositionChoices() interface{} { + vocabs := make([]string, 0) + + type TableChoice struct { + TableID *string `json:"table_id"` + Title *string `json:"title"` + AttributesCount *int `json:"attributes_count"` + Attributes Attributes `json:"attributes"` + } + + tablesChoice := make([]TableChoice, 0) + + for i, table := range choice.Tables { + + tableChoice := TableChoice{ + Attributes: []Attribute{}, + } + + if choice.Type == ER_CHOICE_FILL_TABLE { + count := len(table.Attributes) + tableChoice.AttributesCount = &count + } + + if table.Fixed { + tableChoice.TableID = &choice.Tables[i].ID + tableChoice.Title = &choice.Tables[i].Title + } else { + vocabs = append(vocabs, table.Title) + } + + for _, a := range table.Attributes { + if a.Fixed { + tableChoice.Attributes = append(tableChoice.Attributes, a) + } else { + vocabs = append(vocabs, a.Value) + } + } + + utils.Shuffle(tableChoice.Attributes) + + tablesChoice = append(tablesChoice, tableChoice) + } + + if choice.Type == ER_CHOICE_FILL_TABLE { + + utils.Shuffle(vocabs) + utils.Shuffle(tablesChoice) + + return map[string]interface { + }{ + "vocabs": vocabs, + "tables": tablesChoice, + } + + } + + relationships := choice.Relationships + _relationships := make([]Relationship, 0) + + for i, v := range choice.Relationships { + + if v.Fixed { + _relationships = append(_relationships, relationships[i]) + } + + } + + utils.Shuffle(_relationships) + + return map[string]interface { + }{ + "tables": tablesChoice, + "relationships": _relationships, + } +} + +type ERAnswerTables struct { + ERAnswerID int `gorm:"column:er_answer_id" json:"-"` + TableID string `gorm:"column:table_id" json:"-"` +} + +type ERAnswer struct { + ID int `gorm:"column:er_answer_id" json:"er_answer_id"` + UserID int `gorm:"column:user_id" json:"-"` + Tables Tables `gorm:"-" json:"tables"` + Relationships Relationships `gorm:"-" json:"relationships"` +} + +func (choice ERAnswer) CreatePropositionChoices() interface{} { + return map[string]interface{}{ + "er_answe_id": choice.ID, + "tables": choice.Tables, + "relationships": choice.Relationships, + "problems": GetPeerProblem(), + } +} diff --git a/internal/models/entities/activity/er_diagram.go b/internal/models/entities/activity/er_diagram.go new file mode 100644 index 0000000..75260d8 --- /dev/null +++ b/internal/models/entities/activity/er_diagram.go @@ -0,0 +1,41 @@ +package activity + +const ( + ATTRIBUTE_KEY_PK = "PK" + ATTRIBUTE_KEY_FK = "FK" +) + +type Table struct { + ID string `gorm:"column:table_id" json:"table_id"` + Title string `gorm:"column:title" json:"title"` + Fixed bool `gorm:"column:fixed" json:"-"` + Attributes Attributes `gorm:"-" json:"attributes"` +} + +type Tables []Table + +type Attribute struct { + ID int `gorm:"column:attribute_id" json:"attribute_id"` + TableID string `gorm:"column:table_id" json:"-"` + Key *string `gorm:"column:key" json:"key"` + Value string `gorm:"column:value" json:"value"` + Fixed bool `gorm:"column:fixed" json:"-"` +} + +type Attributes []Attribute + +const ( + RELATIONSHIP_MANY_TO_MANY = "MANY_TO_MANY" + RELATIONSHIP_ONE_TO_MANY = "ONE_TO_MANY" + RELATIONSHIP_ONE_TO_ONE = "ONE_TO_ONE" +) + +type Relationship struct { + ID int `gorm:"column:relationship_id" json:"relationship_id"` + RelationshipType string `gorm:"column:relationship_type" json:"relationship_type"` + Table1ID string `gorm:"column:table1_id" json:"table1_id"` + Table2ID string `gorm:"column:table2_id" json:"table2_id"` + Fixed bool `gorm:"column:fixed" json:"-"` +} + +type Relationships []Relationship diff --git a/internal/models/entities/activity/hint.go b/internal/models/entities/activity/hint.go new file mode 100644 index 0000000..1fc412e --- /dev/null +++ b/internal/models/entities/activity/hint.go @@ -0,0 +1,69 @@ +package activity + +import "time" + +type HintRoadMap struct { + Level int `json:"level"` + ReducePoint int `json:"reduce_point"` +} + +type ActivityHint struct { + TotalHint int `json:"total_hint"` + UsedHints []Hint `json:"used_hints"` + HintRoadMap []HintRoadMap `json:"hint_roadmap"` +} + +type UserHint struct { + UserID int `gorm:"primaryKey;column:user_id" json:"user_id"` + HintID int `gorm:"primaryKey;column:hint_id" json:"hint_id"` + CreatedTimestamp time.Time `gorm:"column:created_timestamp" json:"created_timestamp"` +} + +type UserHints []UserHint + +func (hints UserHints) IsUsed(hintID int) bool { + for _, hint := range hints { + if hint.HintID == hintID { + return true + } + } + return false +} + +type Hint struct { + ID int `gorm:"primaryKey;column:hint_id" json:"hint_id"` + ActivityID int `gorm:"column:activity_id" json:"activity_id"` + Content string `gorm:"column:content" json:"content"` + PointReduce int `gorm:"column:point_reduce" json:"point_reduce"` + Level int `gorm:"column:level" json:"level"` +} + +type Hints []Hint + +func (hints Hints) GetUsedHints(userHints UserHints) (usedHints Hints) { + for _, hint := range hints { + if userHints.IsUsed(hint.ID) { + usedHints = append(usedHints, hint) + } + } + return +} + +func (hints Hints) CreateRoadmap() (roadmap []HintRoadMap) { + for _, hint := range hints { + roadmap = append(roadmap, HintRoadMap{ + Level: hint.Level, + ReducePoint: hint.PointReduce, + }) + } + return +} + +func (hints Hints) GetNextLevelHint(userHints UserHints) *Hint { + for _, hint := range hints { + if !userHints.IsUsed(hint.ID) { + return &hint + } + } + return nil +} diff --git a/internal/models/entities/activity/peer_problems.go b/internal/models/entities/activity/peer_problems.go new file mode 100644 index 0000000..98c5e99 --- /dev/null +++ b/internal/models/entities/activity/peer_problems.go @@ -0,0 +1,100 @@ +package activity + +const ( + PEER_ACTIVITY_ID = 10 + PEER_ACTIVITY_TYPE_ID = 6 + PEER_ACTIVITY_POINT = 300 +) + +const ( + SUGGESTION_LESS_RELATION = iota + SUGGESTION_MORE_RELATION + SUGGESTION_DUPLICATION_RELATION + SUGGESTION_INCORRECT_RELATION + SUGGESTION_INCORRECT_NUMBER_RELATIONSHIP + SUGGESTION_INCORRECT_RELATIONSHIP + SUGGESTION_INVALID_TYPE_RELATIONSHIP + SUGGESTION_LESS_ATTRIBUTE + SUGGESTION_MORE_ATTRIBUTE + SUGGESTION_INCORRECT_ATTRIBUTE + SUGGESTION_INCORRECT_KEY_ATTRIBUTE +) + +type SuggestionGroup struct { + Name string + Suggestions Suggestions +} + +const ( + SUGGESTION_RELATION_GROUP = "ด้าน Relation" + SUGGESTION_RELATIONSHIP_GROUP = "ด้าน Relationship" + SUGGESTION_ATTRIBUTE_GROUP = "ด้าน Attribute" +) + +var SuggestionGroups = []SuggestionGroup{ + { + Name: SUGGESTION_RELATION_GROUP, + Suggestions: RelationSuggestions, + }, + { + Name: SUGGESTION_RELATIONSHIP_GROUP, + Suggestions: RelationshipSuggestions, + }, + { + Name: SUGGESTION_ATTRIBUTE_GROUP, + Suggestions: AttributeSuggestions, + }, +} + +type Suggestions map[int]string + +func (s Suggestions) Strings() []string { + suggestions := make([]string, 0) + + for _, v := range s { + suggestions = append(suggestions, v) + } + + return suggestions +} + +var RelationSuggestions Suggestions = Suggestions{ + SUGGESTION_LESS_RELATION: "จำนวนของ Relation น้อยเกินไป", + SUGGESTION_MORE_RELATION: "จำนวนของ Relation มากเกินไป", + SUGGESTION_INCORRECT_RELATION: "Relation ไม่สอดคล้องกับความต้องการของระบบ", +} + +var RelationshipSuggestions Suggestions = Suggestions{ + SUGGESTION_INCORRECT_NUMBER_RELATIONSHIP: "จำนวนของ Relationship ไม่ถูกต้อง", + SUGGESTION_INCORRECT_RELATIONSHIP: "Relationship ระหว่าง Relation ไม่ถูกต้อง", + SUGGESTION_INVALID_TYPE_RELATIONSHIP: "ประเภทของ Relationship ไม่ถูกต้อง", +} + +var AttributeSuggestions Suggestions = Suggestions{ + SUGGESTION_LESS_ATTRIBUTE: "จำนวนของ Attribute น้อยเกินไป", + SUGGESTION_MORE_ATTRIBUTE: "จำนวนของ Attribute มากเกินไป", + SUGGESTION_INCORRECT_ATTRIBUTE: "Attribute ไม่สอดคล้องกับความต้องการของระบบ", + SUGGESTION_INCORRECT_KEY_ATTRIBUTE: "Key ของ Attribute ไม่ถูกต้อง", +} + +type ProblemGroup struct { + Name string `json:"name"` + Choices []string `json:"choices"` +} + +type PeerProblems struct { + Groups []ProblemGroup `json:"groups"` +} + +func GetPeerProblem() PeerProblems { + peerProblems := PeerProblems{} + + for _, group := range SuggestionGroups { + peerProblems.Groups = append(peerProblems.Groups, ProblemGroup{ + Name: group.Name, + Choices: group.Suggestions.Strings(), + }) + } + + return peerProblems +} diff --git a/internal/models/entities/badge/badge.go b/internal/models/entities/badge/badge.go new file mode 100644 index 0000000..29f668e --- /dev/null +++ b/internal/models/entities/badge/badge.go @@ -0,0 +1,41 @@ +package badge + +type UserBadge struct { + UserID int `gorm:"primaryKey;column:user_id" json:"user_id"` + BadgeID int `gorm:"primaryKey;column:badge_id" json:"badge_id"` +} + +type Badge struct { + ID int `gorm:"primaryKey;column:badge_id"` + ImagePath string `gorm:"column:icon_path"` + Name string `gorm:"column:name"` + IsCollected bool `gorm:"-"` +} + +func (b *Badge) isCollected(correctedBadgesDB []UserBadge) bool { + for _, correctedBadgeDB := range correctedBadgesDB { + if b.ID == correctedBadgeDB.BadgeID { + return true + } + } + + return false +} + +func NewBadges(allBadges []Badge, collectedBadges []UserBadge) []Badge { + badges := make([]Badge, 0, len(allBadges)) + + for _, badgeDB := range allBadges { + badge := Badge{ + ID: badgeDB.ID, + ImagePath: badgeDB.ImagePath, + Name: badgeDB.Name, + } + + badge.IsCollected = badge.isCollected(collectedBadges) + + badges = append(badges, badge) + } + + return badges +} diff --git a/internal/models/entities/content/content.go b/internal/models/entities/content/content.go new file mode 100644 index 0000000..2469d80 --- /dev/null +++ b/internal/models/entities/content/content.go @@ -0,0 +1,16 @@ +package content + +type Content struct { + ID int `gorm:"primaryKey;column:content_id" json:"content_id"` + GroupID int `gorm:"column:content_group_id" json:"content_group_id"` + Name string `gorm:"column:name" json:"name"` + VideoPath string `gorm:"column:video_path" json:"video_path"` + SlidePath string `gorm:"column:slide_path" json:"slide"` +} + +type ContentGroup struct { + ID int `gorm:"column:content_group_id"` + Name string `gorm:"column:name"` +} + +type ContentGroups []ContentGroup diff --git a/internal/models/entities/content/overview.go b/internal/models/entities/content/overview.go new file mode 100644 index 0000000..634e28d --- /dev/null +++ b/internal/models/entities/content/overview.go @@ -0,0 +1,160 @@ +package content + +type content struct { + id int + name string + activities []int +} + +type group struct { + id int + name string + contents map[int]*content +} + +type contentOverview struct { + ContentID int `json:"content_id"` + ContentName string `json:"content_name"` + IsLasted bool `json:"is_lasted"` + Progress int `json:"progress"` +} + +type LastedGroupOverview struct { + GroupID int `json:"group_id"` + ContentID int `json:"content_id"` + ActivityID int `json:"activity_id"` + GroupName string `json:"group_name"` + ContentName string `json:"content_name"` + Progress int `json:"progress"` +} + +type ContentGroupOverview struct { + GroupID int `json:"group_id"` + IsLasted bool `json:"is_lasted"` + GroupName string `json:"group_name"` + Progress int `json:"progress"` + Contents []contentOverview `json:"contents"` +} + +type Overview struct { + GroupID int `gorm:"column:content_group_id" json:"group_id"` + ContentID int `gorm:"column:content_id" json:"content_id"` + ActivityID *int `gorm:"column:activity_id" json:"activity_id"` + GroupName string `gorm:"column:group_name" json:"group_name"` + ContentName string `gorm:"column:content_name" json:"content_name"` +} + +type ActivityContentIDMap map[int]int + +func (m ActivityContentIDMap) getLastedContentID(lastedActivityID *int) *int { + if lastedActivityID == nil { + return nil + } else { + contentID := m[*lastedActivityID] + return &contentID + } +} + +type OverviewList []Overview + +func (l OverviewList) GetLearningOverview(progressionList LearningProgressionList) (*LastedGroupOverview, []ContentGroupOverview) { + + var lastedGroupOverview *LastedGroupOverview + var contentGroupOverview []ContentGroupOverview + + groupMap := l.createGroupMap() + activityContentIDMap := l.createActivityContentIDMap() + userActivityCountByContentID := progressionList.createUserActivityCountByContentID(activityContentIDMap) + lastedActivityID := progressionList.getLastedActivityID() + lastedContentID := activityContentIDMap.getLastedContentID(lastedActivityID) + + for _, group := range groupMap { + groupActivityCount := 0 + groupUserActivityCount := 0 + isGroupLasted := false + contents := make([]contentOverview, 0) + for _, content := range group.contents { + groupActivityCount += len(content.activities) + groupUserActivityCount += userActivityCountByContentID[content.id] + isContentLasted := lastedContentID != nil && *lastedContentID == content.id + contentProgress := calculateProgress(userActivityCountByContentID[content.id], len(content.activities)) + contents = append(contents, contentOverview{ + ContentID: content.id, + ContentName: content.name, + IsLasted: isContentLasted, + Progress: contentProgress, + }) + if isContentLasted { + lastedGroupOverview = &LastedGroupOverview{ + GroupID: group.id, + ContentID: content.id, + GroupName: group.name, + ActivityID: *lastedActivityID, + ContentName: content.name, + Progress: contentProgress, + } + } + } + groupProgress := calculateProgress(groupUserActivityCount, groupActivityCount) + contentGroupOverview = append(contentGroupOverview, ContentGroupOverview{ + GroupID: group.id, + IsLasted: isGroupLasted, + GroupName: group.name, + Progress: groupProgress, + Contents: contents, + }) + } + + return lastedGroupOverview, contentGroupOverview +} + +func (l OverviewList) createGroupMap() map[int]*group { + groupMap := map[int]*group{} + for _, overview := range l { + _group := groupMap[overview.GroupID] + if _group == nil { + _group = &group{ + id: overview.GroupID, + name: overview.GroupName, + contents: map[int]*content{}, + } + groupMap[overview.GroupID] = _group + } + + _content := _group.contents[overview.ContentID] + if _content == nil { + _content = &content{ + id: overview.ContentID, + name: overview.ContentName, + activities: []int{}, + } + _group.contents[overview.ContentID] = _content + } + + if overview.ActivityID != nil { + _content.activities = append(_content.activities, *overview.ActivityID) + } + } + + return groupMap +} + +func (l OverviewList) createActivityContentIDMap() ActivityContentIDMap { + activityContentIDMap := map[int]int{} + for _, overview := range l { + if overview.ActivityID != nil { + activityContentIDMap[*overview.ActivityID] = overview.ContentID + } + } + return activityContentIDMap +} + +func calculateProgress(progress int, total int) int { + if total == 0 { + return 0 + } else { + ratio := float64(progress) / float64(total) + return int(ratio * 100) + } + +} diff --git a/internal/models/entities/content/progression.go b/internal/models/entities/content/progression.go new file mode 100644 index 0000000..38781e5 --- /dev/null +++ b/internal/models/entities/content/progression.go @@ -0,0 +1,38 @@ +package content + +import "time" + +type LearningProgression struct { + ID int `gorm:"primaryKey;column:learning_progression_id" json:"learning_progression_id"` + UserID int `gorm:"column:user_id" json:"user_id"` + ActivityID int `gorm:"column:activity_id" json:"activity_id"` + IsCorrect bool `gorm:"column:is_correct" json:"is_correct"` + CreatedTimestamp time.Time `gorm:"column:created_timestamp" json:"created_timestamp"` +} + +type LearningProgressionList []LearningProgression + +func (l LearningProgressionList) getLastedActivityID() *int { + if len(l) == 0 { + return nil + } else { + return &l[0].ActivityID + } +} + +func (l LearningProgressionList) createUserActivityCountByContentID(activityContentIDMap ActivityContentIDMap) map[int]int { + userActivityCount := map[int]int{} + for _, learningProgression := range l { + userActivityCount[activityContentIDMap[learningProgression.ActivityID]]++ + } + return userActivityCount +} + +func (l LearningProgressionList) IsLearned(activityID int) bool { + for _, v := range l { + if v.ActivityID == activityID { + return true + } + } + return false +} diff --git a/internal/models/entities/exam/activity.go b/internal/models/entities/exam/activity.go new file mode 100644 index 0000000..f0ed35c --- /dev/null +++ b/internal/models/entities/exam/activity.go @@ -0,0 +1,80 @@ +package exam + +import ( + "database-camp/internal/models/entities/activity" + "time" +) + +type ExamActivityAnswer struct { + ActivityID int `json:"activity_id"` + Answer interface{} `json:"answer"` +} + +type ExamActivity struct { + ActivityID int `gorm:"column:activity_id"` + ActivityTypeID int `gorm:"column:activity_type_id"` +} + +type ExamActivities []ExamActivity + +type Activity struct { + activity.Activity + activity.Choices +} + +type Activities []Activity + +func (activities Activities) CheckAnswers(examID int, userID int, answers []ExamActivityAnswer) (*Result, error) { + answerScore := 0 + totalScore := 0 + + activitiesResult := make([]ResultActivity, 0) + + for _, examActivity := range activities { + for _, answer := range answers { + if examActivity.Activity.ID == answer.ActivityID { + formatedAnswer, err := activity.FormatAnswer(answer.Answer, examActivity.Activity.TypeID) + if err != nil { + return nil, err + } + + isCorrect, err := formatedAnswer.IsCorrect(examActivity.Choices) + if err != nil { + return nil, err + } + + if isCorrect { + answerScore += examActivity.Activity.Point + } + + totalScore += examActivity.Activity.Point + + activitiesResult = append(activitiesResult, ResultActivity{ + ActivityID: examActivity.Activity.ID, + Score: answerScore, + }) + } + } + } + + return &Result{ + ActivitiesResult: activitiesResult, + ExamResult: ExamResult{ + ExamID: examID, + UserID: userID, + Score: answerScore, + IsPassed: isPassed(answerScore, totalScore), + CreatedTimestamp: time.Now().Local(), + }, + }, nil +} + +func isPassed(answerTotalScore int, activitiesTotalScore int) bool { + passedRate := 0.5 + if activitiesTotalScore == 0 { + return true + } else { + return (float64)(answerTotalScore/activitiesTotalScore) > passedRate + } + +} diff --git a/internal/models/entities/exam/exam.go b/internal/models/entities/exam/exam.go new file mode 100644 index 0000000..f0a0681 --- /dev/null +++ b/internal/models/entities/exam/exam.go @@ -0,0 +1,92 @@ +package exam + +import ( + "time" +) + +type ExamType string + +const ( + POST ExamType = "POST" + PRE ExamType = "PRE" + MINI ExamType = "MINI" +) + +type Exam struct { + ID int `gorm:"primaryKey;column:exam_id" json:"exam_id"` + Type string `gorm:"column:type" json:"exam_type"` + Instruction string `gorm:"column:instruction" json:"instruction"` + CreatedTimestamp time.Time `gorm:"column:created_timestamp" json:"created_timestamp"` + ContentGroupID int `gorm:"column:content_group_id" json:"content_group_id"` + ContentGroupName string `gorm:"column:content_group_name" json:"content_group_name"` + BadgeID int `gorm:"column:badge_id" json:"badge_id"` +} + +type Exams []Exam + +func (exams Exams) GetPreExam(examResults ExamResults) *DetailOverview { + var detail DetailOverview + + for _, exam := range exams { + cando := !examResults.FinishedExam(exam.ID) + results := examResults.GetResultOverview(exam.ID) + + if exam.Type == string(PRE) { + detail = DetailOverview{ + ExamID: exam.ID, + ExamType: exam.Type, + CanDo: &cando, + Results: &results, + } + return &detail + } + + } + + return nil +} + +func (exams Exams) GetMiniExam(examResults ExamResults) *[]DetailOverview { + found := false + details := make([]DetailOverview, 0) + + for _, exam := range exams { + if exam.Type == string(MINI) { + found = true + results := examResults.GetResultOverview(exam.ID) + _exam := exam + details = append(details, DetailOverview{ + ExamID: _exam.ID, + ExamType: _exam.Type, + ContentGroupID: &_exam.ContentGroupID, + ContentGroupName: &_exam.ContentGroupName, + Results: &results, + }) + } + } + + if !found { + return nil + } else { + return &details + } +} + +func (exams Exams) GetFinalExam(examResults ExamResults, canDo bool) *DetailOverview { + var detail DetailOverview + + for _, exam := range exams { + if exam.Type == string(POST) { + results := examResults.GetResultOverview(exam.ID) + detail = DetailOverview{ + ExamID: exam.ID, + ExamType: exam.Type, + CanDo: &canDo, + Results: &results, + } + return &detail + } + } + + return nil +} diff --git a/internal/models/entities/exam/overview.go b/internal/models/entities/exam/overview.go new file mode 100644 index 0000000..0ae2993 --- /dev/null +++ b/internal/models/entities/exam/overview.go @@ -0,0 +1,19 @@ +package exam + +import "time" + +type ResultOverview struct { + ExamResultID int `json:"exam_result_id"` + TotalScore int `json:"score"` + IsPassed bool `json:"is_passed"` + CreatedTimestamp time.Time `json:"created_timestamp"` +} + +type DetailOverview struct { + ExamID int `json:"exam_id"` + ExamType string `json:"exam_type"` + ContentGroupID *int `json:"content_group_id,omitempty"` + ContentGroupName *string `json:"content_group_name,omitempty"` + CanDo *bool `json:"can_do,omitempty"` + Results *[]ResultOverview `json:"results,omitempty"` +} diff --git a/internal/models/entities/exam/result.go b/internal/models/entities/exam/result.go new file mode 100644 index 0000000..de1420b --- /dev/null +++ b/internal/models/entities/exam/result.go @@ -0,0 +1,67 @@ +package exam + +import "time" + +type ExamResult struct { + ID int `gorm:"primaryKey;column:exam_result_id" json:"exam_result_id"` + ExamID int `gorm:"column:exam_id" json:"exam_id"` + UserID int `gorm:"column:user_id" json:"user_id"` + Score int `gorm:"->;column:score" json:"score"` + ExamType string `gorm:"->;column:type" json:"exam_type"` + ExamContentGroupName string `gorm:"->;column:content_group_name" json:"content_group_name"` + IsPassed bool `gorm:"column:is_passed" json:"is_passed"` + CreatedTimestamp time.Time `gorm:"column:created_timestamp" json:"created_timestamp"` +} + +type ExamResults []ExamResult + +func (results ExamResults) FinishedExam(examID int) bool { + for _, result := range results { + if result.ExamID == examID { + return true + } + } + return false +} + +func (results ExamResults) GetResultOverview(examID int) []ResultOverview { + overview := make([]ResultOverview, 0) + for _, result := range results { + if result.ExamID == examID { + overview = append(overview, ResultOverview{ + ExamResultID: result.ID, + TotalScore: result.Score, + IsPassed: result.IsPassed, + CreatedTimestamp: result.CreatedTimestamp, + }) + } + } + return overview +} + +type Result struct { + ActivitiesResult ResultActivities + ExamResult ExamResult +} + +type ResultActivity struct { + ExamResultID int `gorm:"primaryKey;column:exam_result_id" json:"exam_result_id"` + ActivityID int `gorm:"primaryKey;column:activity_id" json:"activity_id"` + Score int `gorm:"column:score" json:"score"` +} + +type ResultActivities []ResultActivity + +func (activities *ResultActivities) SetExamResultID(id int) { + newActivities := make(ResultActivities, 0) + + for _, activity := range *activities { + newActivities = append(newActivities, ResultActivity{ + ExamResultID: id, + ActivityID: activity.ActivityID, + Score: activity.Score, + }) + } + + *activities = newActivities +} diff --git a/internal/models/entities/user/corrected_badge.go b/internal/models/entities/user/corrected_badge.go new file mode 100644 index 0000000..29c019e --- /dev/null +++ b/internal/models/entities/user/corrected_badge.go @@ -0,0 +1,18 @@ +package user + +type CorrectedBadge struct { + BadgeID int `gorm:"column:badge_id" json:"badge_id"` + Name string `gorm:"column:badge_name" json:"badge_name"` + UserID *int `gorm:"column:user_id" json:"user_id"` +} + +type CorrectedBadges []CorrectedBadge + +func (badges CorrectedBadges) CanDoFianlExam() bool { + for _, badge := range badges { + if badge.UserID == nil && badge.BadgeID != 3 { + return false + } + } + return true +} diff --git a/internal/models/entities/user/pretest.go b/internal/models/entities/user/pretest.go new file mode 100644 index 0000000..ed83eab --- /dev/null +++ b/internal/models/entities/user/pretest.go @@ -0,0 +1,58 @@ +package user + +import "database-camp/internal/models/entities/content" + +type RecommendGroup struct { + ContentGroupID int `json:"content_group_id"` + IsRecommend bool `json:"is_recommend"` +} + +type PreTestResult struct { + ActivityID int `gorm:"column:activity_id"` + Score int `gorm:"column:score"` + Point int `gorm:"column:point"` + ContentGroupID int `gorm:"column:content_group_id"` +} + +func (result PreTestResult) IsRecommend() bool { + if result.Point == 0 { + return false + } + + return float64(result.Score)/float64(result.Point) < 0.3 +} + +type PreTestResultsMap map[int]*PreTestResult + +func NewPreTestResultsMap(results PreTestResults) (resultMap PreTestResultsMap) { + resultMap = make(PreTestResultsMap) + for _, result := range results { + resultMap[result.ContentGroupID] = &result + } + return +} + +func (resultsMap PreTestResultsMap) IsRecommend(groupID int) bool { + if resultsMap[groupID] == nil { + return true + } else { + return resultsMap[groupID].IsRecommend() + } +} + +type PreTestResults []PreTestResult + +func (results PreTestResults) GetRecommend(contentGroups content.ContentGroups) []RecommendGroup { + + resultsMap := NewPreTestResultsMap(results) + + recommend := make([]RecommendGroup, 0, len(contentGroups)) + for _, group := range contentGroups { + recommend = append(recommend, RecommendGroup{ + ContentGroupID: group.ID, + IsRecommend: resultsMap.IsRecommend(group.ID), + }) + } + + return recommend +} diff --git a/internal/models/entities/user/spider_data.go b/internal/models/entities/user/spider_data.go new file mode 100644 index 0000000..7190980 --- /dev/null +++ b/internal/models/entities/user/spider_data.go @@ -0,0 +1,33 @@ +package user + +import ( + "database-camp/internal/models/entities/content" +) + +type SpiderData struct { + ContentGroupID int `gorm:"column:content_group_id" json:"content_group_id"` + ContentGroupName string `gorm:"-" json:"content_group_name"` + Stat float64 `gorm:"column:stat" json:"stat"` +} + +type SpiderDataset []SpiderData + +func (dataset *SpiderDataset) FillContentGroups(contentGroups content.ContentGroups) { + newDataset := make(SpiderDataset, 0) + oldDataset := *dataset + + mapData := map[int]SpiderData{} + for _, data := range oldDataset { + mapData[data.ContentGroupID] = data + } + + for _, group := range contentGroups { + newDataset = append(newDataset, SpiderData{ + ContentGroupID: group.ID, + ContentGroupName: group.Name, + Stat: mapData[group.ID].Stat, + }) + } + + *dataset = newDataset +} diff --git a/internal/models/entities/user/user.go b/internal/models/entities/user/user.go new file mode 100644 index 0000000..2e7fb51 --- /dev/null +++ b/internal/models/entities/user/user.go @@ -0,0 +1,32 @@ +package user + +import ( + "time" +) + +type User struct { + ID int `gorm:"primaryKey;column:user_id"` + Name string `gorm:"column:name"` + Email string `gorm:"column:email"` + Password string `gorm:"column:password"` + AccessToken string `gorm:"column:access_token"` + Point int `gorm:"column:point"` + ExpiredTokenTimestamp time.Time `gorm:"column:expired_token_timestamp"` + CreatedTimestamp time.Time `gorm:"column:created_timestamp"` + UpdatedTimestamp time.Time `gorm:"column:updated_timestamp"` +} + +type Profile struct { + ID int `gorm:"primaryKey;column:user_id"` + Name string `gorm:"column:name"` + Point int `gorm:"column:point"` + ActivityCount int `gorm:"column:activity_count"` + CreatedTimestamp time.Time `gorm:"column:created_timestamp"` +} + +type Ranking struct { + ID int `gorm:"primaryKey;column:user_id" json:"user_id"` + Name string `gorm:"column:name" json:"name"` + Point int `gorm:"column:point" json:"point"` + Ranking int `gorm:"column:ranking" json:"ranking"` +} diff --git a/internal/models/request/exam.go b/internal/models/request/exam.go new file mode 100644 index 0000000..f79cac1 --- /dev/null +++ b/internal/models/request/exam.go @@ -0,0 +1,20 @@ +package request + +import ( + "database-camp/internal/errs" + "database-camp/internal/models/entities/exam" +) + +type ExamAnswerRequest struct { + ExamID *int `json:"exam_id"` + Activities []exam.ExamActivityAnswer `json:"activities"` +} + +func (r ExamAnswerRequest) Validate() error { + if r.ExamID == nil { + return errs.NewBadRequestError("ไม่พบรหัสของข้อสอบในคำร้องขอ", "Exam ID Not Found") + } else if len(r.Activities) == 0 { + return errs.NewBadRequestError("ไม่พบกิจกรรมของข้อสอบในคำร้องขอ", "Activities Exam Not Found") + } + return nil +} diff --git a/internal/models/request/learning.go b/internal/models/request/learning.go new file mode 100644 index 0000000..be0e97d --- /dev/null +++ b/internal/models/request/learning.go @@ -0,0 +1,36 @@ +package request + +import ( + "database-camp/internal/errs" +) + +type CheckAnswerRequest struct { + ActivityID *int `json:"activity_id"` + ActivityTypeID *int `json:"activity_type_id"` + Answer interface{} `json:"answer"` +} + +func (r CheckAnswerRequest) Validate() error { + if r.ActivityID == nil { + return errs.ErrActivittyIDNotFound + } else if r.ActivityTypeID == nil { + return errs.ErrActivittyTypeIDNotFound + } else if r.Answer == nil { + return errs.ErrAnswerNotFound + } else { + return nil + } +} + +type PeerReviewRequest struct { + ERAnswerID *int `json:"er_answer_id"` + Reviews []string `json:"reviews"` +} + +func (r PeerReviewRequest) Validate() error { + if r.ERAnswerID == nil { + return errs.ErrERAnswerIDNotFound + } else { + return nil + } +} diff --git a/models/request/request.user.go b/internal/models/request/user.go similarity index 94% rename from models/request/request.user.go rename to internal/models/request/user.go index 652d9ed..3a6ccb4 100644 --- a/models/request/request.user.go +++ b/internal/models/request/user.go @@ -6,8 +6,8 @@ package request */ import ( - "DatabaseCamp/errs" - "DatabaseCamp/utils" + "database-camp/internal/errs" + "database-camp/internal/utils" ) /** @@ -43,7 +43,7 @@ func (r UserRequest) ValidateLogin() error { var err error if r.Email == "" { err = errs.NewBadRequestError("ไม่พบอีเมลในคำร้องขอ", "Email Not Found") - } else if !utils.NewHelper().IsEmailValid(r.Email) { + } else if !utils.IsEmailValid(r.Email) { err = errs.NewBadRequestError("รูปแบบ email ไม่ถูกต้อง", "Email Invalid") } else if r.Password == "" { err = errs.NewBadRequestError("ไม่พบรหัสผ่านในคำร้องขอ", "Password Not Found") diff --git a/internal/models/response/exam.go b/internal/models/response/exam.go new file mode 100644 index 0000000..864eb70 --- /dev/null +++ b/internal/models/response/exam.go @@ -0,0 +1,28 @@ +package response + +import ( + "database-camp/internal/models/entities/exam" + "time" +) + +type ExamOverviewResponse struct { + PreExam *exam.DetailOverview `json:"pre_exam"` + MiniExam *[]exam.DetailOverview `json:"mini_exam"` + FinalExam *exam.DetailOverview `json:"final_exam"` +} + +type ExamResultOverviewResponse struct { + ExamID int `json:"exam_id"` + ExamResultID int `json:"exam_result_id"` + ExamType string `json:"exam_type"` + ContentGroupName string `json:"content_group_name"` + CreatedTimestamp time.Time `json:"created_timestamp"` + Score int `json:"score"` + IsPassed bool `json:"is_passed"` + ActivitiesResult exam.ResultActivities `json:"activities_result"` +} + +type ExamResponse struct { + Exam exam.Exam `json:"exam"` + Activities []ActivityResponse `json:"activities"` +} diff --git a/internal/models/response/learning.go b/internal/models/response/learning.go new file mode 100644 index 0000000..a878414 --- /dev/null +++ b/internal/models/response/learning.go @@ -0,0 +1,41 @@ +package response + +import ( + "database-camp/internal/models/entities/activity" + "database-camp/internal/models/entities/content" +) + +type ContentOverviewResponse struct { + PreExam *int `json:"pre_exam_id"` + LastedGroup *content.LastedGroupOverview `json:"lasted_group"` + ContentGroupOverview []content.ContentGroupOverview `json:"content_group_overview"` +} + +type ContentRoadmapResponse struct { + ContentID int `json:"content_id"` + ContentName string `json:"content_name"` + Items []activity.ContentRoadmapItem `json:"items"` +} + +type VideoLectureResponse struct { + ContentID int `json:"content_id"` + ContentName string `json:"content_name"` + VideoLink string `json:"video_link"` +} + +type ActivityResponse struct { + Activity activity.Activity `json:"activity"` + Choices interface{} `json:"choice"` + Hint *activity.ActivityHint `json:"hint"` +} + +type AnswerResponse struct { + ActivityID int `json:"activity_id"` + IsCorrect bool `json:"is_correct"` + UpdatedPoint int `json:"updated_point"` + ErrMessage *string `json:"err_message"` +} + +type UsedHintResponse struct { + HintDB activity.Hint `json:"hint"` +} diff --git a/internal/models/response/user.go b/internal/models/response/user.go new file mode 100644 index 0000000..d0419ec --- /dev/null +++ b/internal/models/response/user.go @@ -0,0 +1,40 @@ +package response + +import ( + "database-camp/internal/models/entities/badge" + "database-camp/internal/models/entities/user" + "time" +) + +type UserResponse struct { + ID int `json:"user_id"` + Name string `json:"name"` + Email string `json:"email"` + Point int `json:"point"` + AccessToken string `json:"access_token"` + CreatedTimestamp time.Time `json:"created_timestamp"` + UpdatedTimestamp time.Time `json:"updated_timestamp"` +} + +type GetProfileResponse struct { + ID int `json:"user_id"` + Name string `json:"name"` + Point int `json:"point"` + ActivityCount int `json:"activity_count"` + Badges []badge.Badge `json:"badges"` + SpiderDataset user.SpiderDataset `json:"spider"` + CreatedTimestamp time.Time ` json:"created_timestamp"` +} + +type EditProfileResponse struct { + UpdatedName string `json:"updated_name"` +} + +type RankingResponse struct { + UserRanking user.Ranking `json:"user_ranking"` + LeaderBoard []user.Ranking `json:"leader_board"` +} + +type RecommendResponse struct { + Recommend []user.RecommendGroup `json:"recommend_group"` +} diff --git a/internal/registry/registry.go b/internal/registry/registry.go new file mode 100644 index 0000000..0622642 --- /dev/null +++ b/internal/registry/registry.go @@ -0,0 +1,70 @@ +package registry + +import ( + "database-camp/internal/handler" + "database-camp/internal/infrastructure/cache" + "database-camp/internal/infrastructure/database" + "database-camp/internal/middleware/jwt" + "database-camp/internal/repositories" + "database-camp/internal/services" +) + +type middlewares struct { + Jwt jwt.Jwt +} + +type handlers struct { + UserHandler handler.UserHandler + LearningHandler handler.LearningHandler + ExamHandler handler.ExamHandler +} + +type Registry interface { + GetMiddlewares() *middlewares + GetHandlers() *handlers +} + +type registry struct { + middlewares middlewares + handlers handlers +} + +func Regis() *registry { + + db := database.GetMySqlDBInstance() + + cache := cache.NewRedisClient() + + userRepo := repositories.NewUserRepository(db, cache) + learningRepo := repositories.NewLearningRepository(db, cache) + examRepo := repositories.NewExamRepository(db, cache) + + userService := services.NewUserService(userRepo, learningRepo) + learningService := services.NewLearningService(learningRepo, userRepo) + examService := services.NewExamService(examRepo, userRepo, learningRepo, cache) + + userHandler := handler.NewUserHandler(userService) + learningHandler := handler.NewLearningHandler(learningService) + examHandler := handler.NewExamHandler(examService) + + jwt := jwt.New(userRepo) + + return ®istry{ + middlewares: middlewares{ + Jwt: jwt, + }, + handlers: handlers{ + UserHandler: userHandler, + LearningHandler: learningHandler, + ExamHandler: examHandler, + }, + } +} + +func (r registry) GetMiddlewares() *middlewares { + return &r.middlewares +} + +func (r registry) GetHandlers() *handlers { + return &r.handlers +} diff --git a/internal/repositories/data.go b/internal/repositories/data.go new file mode 100644 index 0000000..5859c77 --- /dev/null +++ b/internal/repositories/data.go @@ -0,0 +1,125 @@ +package repositories + +var TableName = struct { + User string + Content string + ContentGroup string + LearningProgression string + Exam string + ExamResult string + ContentExam string + UserBadge string + Badge string + Activity string + ActivityType string + MatchingChoice string + CompletionChoice string + MultipleChoice string + Hint string + UserHint string + ExamResultActivity string + VocabGroupChoice string + VocabGroup string + DependencyChoice string + Dependency string + Determinant string + ERChoice string + ERChoiceTables string + Tables string + Attributes string + Relationship string + ERAnswer string + ERAnswerTables string +}{ + "User", + "Content", + "ContentGroup", + "LearningProgression", + "Exam", + "ExamResult", + "ContentExam", + "UserBadge", + "Badge", + "Activity", + "ActivityType", + "MatchingChoice", + "CompletionChoice", + "MultipleChoice", + "Hint", + "UserHint", + "ExamResultActivity", + "VocabGroupChoice", + "VocabGroup", + "DependencyChoice", + "Dependency", + "Determinant", + "ERChoice", + "ERChoiceTables", + "Tables", + "Attributes", + "Relationship", + "ERAnswer", + "ERAnswerTables", +} + +var IDName = struct { + User string + Activity string + Hint string + Content string + ContentGroup string + Exam string + MiniExam string + Badge string + ExamResult string + VocabGroup string + Dependency string + Determinant string + DependencyChoice string + ERChoice string + Table string + Attribute string + Relationship string + ERAnswer string +}{ + "user_id", + "activity_id", + "hint_id", + "content_id", + "content_group_id", + "exam_id", + "mini_exam_id", + "badge_id", + "exam_result_id", + "vocab_group_id", + "dependency_id", + "determinant_id", + "dependency_choice_id", + "er_choice_id", + "table_id", + "attribute_id", + "relationship_id", + "er_answer_id", +} + +var ViewName = struct { + Profile string + Ranking string + ExamInfo string + ExamResultSummary string + UserPreTest string + UserPreTestResult string + SpiderData string + RandomERAnswer string + AllERAnswer string +}{ + "Profile", + "Ranking", + "ExamInfo", + "ExamResultSummary", + "UserPreTest", + "UserPreTestResult", + "SpiderData", + "RandomERAnswer", + "AllERAnswer", +} diff --git a/internal/repositories/exam.go b/internal/repositories/exam.go new file mode 100644 index 0000000..3e48719 --- /dev/null +++ b/internal/repositories/exam.go @@ -0,0 +1,266 @@ +package repositories + +import ( + "database-camp/internal/infrastructure/cache" + "database-camp/internal/infrastructure/database" + "database-camp/internal/models/entities/exam" + "database-camp/internal/utils" + "encoding/json" + "fmt" + "time" +) + +type ExamRepository interface { + GetExam(id int) (*exam.Exam, error) + GetExams() ([]exam.Exam, error) + GetExamActivities(id int) ([]exam.ExamActivity, error) + GetExamResult(userID int, examResultID int) (*exam.ExamResult, error) + GetExamResults(userID int) ([]exam.ExamResult, error) + GetActivitiesResult(examResultID int) ([]exam.ResultActivity, error) + SaveResult(result exam.Result) (*exam.Result, error) +} + +type examRepository struct { + db database.MysqlDB + cache cache.Cache +} + +func NewExamRepository(db database.MysqlDB, cache cache.Cache) *examRepository { + return &examRepository{db: db, cache: cache} +} + +func (r examRepository) GetExam(id int) (*exam.Exam, error) { + exam := exam.Exam{} + + key := "examRepository::GetExam::" + utils.ParseString(id) + + if cacheData, err := r.cache.Get(key); err == nil { + if err = json.Unmarshal([]byte(cacheData), &exam); err == nil { + return &exam, nil + } + } + + err := r.db.GetDB(). + Table(ViewName.ExamInfo). + Where(IDName.Exam+" = ?", id). + Find(&exam). + Error + + if data, err := json.Marshal(exam); err != nil { + return nil, err + } else { + if err = r.cache.Set(key, string(data), time.Minute*300); err != nil { + return nil, err + } + } + + return &exam, err +} + +func (r examRepository) GetExams() ([]exam.Exam, error) { + exam := make([]exam.Exam, 0) + + key := "examRepository::GetExams" + + if cacheData, err := r.cache.Get(key); err == nil { + if err = json.Unmarshal([]byte(cacheData), &exam); err == nil { + return exam, nil + } + } + + err := r.db.GetDB(). + Table(TableName.Exam). + Select( + TableName.Exam+".exam_id AS exam_id", + TableName.Exam+".type AS type", + TableName.Exam+".instruction AS instruction", + TableName.Exam+".created_timestamp AS created_timestamp", + TableName.ContentGroup+".content_group_id AS content_group_id", + TableName.ContentGroup+".name AS content_group_name", + TableName.ContentGroup+".badge_id AS badge_id", + ). + Joins(fmt.Sprintf("LEFT JOIN %s ON %s.%s = %s.%s", + TableName.ContentGroup, + TableName.ContentGroup, + IDName.MiniExam, + TableName.Exam, + IDName.Exam, + )). + Order(TableName.ContentGroup + ".content_group_id"). + Order(TableName.Exam + ".created_timestamp DESC"). + Find(&exam). + Error + + if data, err := json.Marshal(exam); err != nil { + return nil, err + } else { + if err = r.cache.Set(key, string(data), time.Minute*300); err != nil { + return nil, err + } + } + + return exam, err +} + +func (r examRepository) GetExamActivities(id int) ([]exam.ExamActivity, error) { + var activities []exam.ExamActivity + + key := "examRepository::GetExamActivities::" + utils.ParseString(id) + + if cacheData, err := r.cache.Get(key); err == nil { + if err = json.Unmarshal([]byte(cacheData), &activities); err == nil { + return activities, nil + } + } + + err := r.db.GetDB(). + Table(TableName.ContentExam). + Select( + TableName.ContentExam+".activity_id AS activity_id", + TableName.Activity+".activity_type_id AS activity_type_id", + ). + Joins(fmt.Sprintf( + "INNER JOIN %s ON %s.%s = %s.%s", + TableName.Activity, + TableName.ContentExam, + IDName.Activity, + TableName.Activity, + IDName.Activity, + )). + Where(IDName.Exam+" = ?", id). + Find(&activities). + Error + + if data, err := json.Marshal(activities); err != nil { + return nil, err + } else { + if err = r.cache.Set(key, string(data), time.Minute*300); err != nil { + return nil, err + } + } + + return activities, err +} + +func (r examRepository) GetExamResult(userID int, examResultID int) (*exam.ExamResult, error) { + result := exam.ExamResult{} + + key := "examRepository::GetExamResult::" + utils.ParseString(userID) + "::" + utils.ParseString(examResultID) + + if cacheData, err := r.cache.Get(key); err == nil { + if err = json.Unmarshal([]byte(cacheData), &result); err == nil { + return &result, nil + } + } + + err := r.db.GetDB(). + Table(ViewName.ExamResultSummary). + Where(IDName.ExamResult+" = ?", examResultID). + Where(IDName.User+" = ?", userID). + Find(&result). + Error + + if data, err := json.Marshal(result); err != nil { + return nil, err + } else { + if err = r.cache.Set(key, string(data), time.Minute*300); err != nil { + return nil, err + } + } + + return &result, err +} + +func (r examRepository) GetExamResults(userID int) ([]exam.ExamResult, error) { + examResults := make([]exam.ExamResult, 0) + + key := "examRepository::GetExamResults::" + utils.ParseString(userID) + + if cacheData, err := r.cache.Get(key); err == nil { + if err = json.Unmarshal([]byte(cacheData), &examResults); err == nil { + return examResults, nil + } + } + + err := r.db.GetDB(). + Table(TableName.ExamResult). + Select( + TableName.ExamResult+".exam_result_id AS exam_result_id", + TableName.ExamResult+".exam_id AS exam_id", + TableName.ExamResult+".user_id AS user_id", + TableName.ExamResult+".is_passed AS is_passed", + TableName.ExamResult+".created_timestamp AS created_timestamp", + fmt.Sprintf("COUNT(%s.score) AS score", TableName.ExamResultActivity), + ). + Joins(fmt.Sprintf("LEFT JOIN %s ON %s.%s = %s.%s", + TableName.ExamResultActivity, + TableName.ExamResultActivity, + IDName.ExamResult, + TableName.ExamResult, + IDName.ExamResult, + )). + Where(IDName.User+" = ?", userID). + Group(TableName.ExamResult + ".exam_result_id"). + Find(&examResults). + Error + + if data, err := json.Marshal(examResults); err != nil { + return nil, err + } else { + if err = r.cache.Set(key, string(data), time.Minute*300); err != nil { + return nil, err + } + } + + return examResults, err +} + +func (r examRepository) GetActivitiesResult(examResultID int) ([]exam.ResultActivity, error) { + activities := make([]exam.ResultActivity, 0) + + key := "examRepository::GetActivitiesResult::" + utils.ParseString(examResultID) + + if cacheData, err := r.cache.Get(key); err == nil { + if err = json.Unmarshal([]byte(cacheData), &activities); err == nil { + return activities, nil + } + } + + err := r.db.GetDB(). + Table(TableName.ExamResultActivity). + Where(IDName.ExamResult+" = ?", examResultID). + Find(&activities). + Error + + if data, err := json.Marshal(activities); err != nil { + return nil, err + } else { + if err = r.cache.Set(key, string(data), time.Minute*300); err != nil { + return nil, err + } + } + + return activities, err +} + +func (r examRepository) SaveResult(result exam.Result) (*exam.Result, error) { + tx := r.db.GetDB().Begin() + + err := tx.Table(TableName.ExamResult).Create(&result.ExamResult).Error + if err != nil { + tx.Rollback() + return nil, err + } + + examResultID := result.ExamResult.ID + result.ActivitiesResult.SetExamResultID(examResultID) + + err = tx.Table(TableName.ExamResultActivity).Create(&result.ActivitiesResult).Error + if err != nil { + tx.Rollback() + return nil, err + } + + tx.Commit() + return &result, err +} diff --git a/internal/repositories/learning.go b/internal/repositories/learning.go new file mode 100644 index 0000000..06a0b0b --- /dev/null +++ b/internal/repositories/learning.go @@ -0,0 +1,801 @@ +package repositories + +import ( + "database-camp/internal/errs" + "database-camp/internal/infrastructure/cache" + "database-camp/internal/infrastructure/database" + "database-camp/internal/infrastructure/storage" + "database-camp/internal/models/entities/activity" + "database-camp/internal/models/entities/content" + "database-camp/internal/utils" + "encoding/json" + "fmt" + "sync" + "time" +) + +type LearningRepository interface { + GetContent(id int) (*content.Content, error) + GetOverview() ([]content.Overview, error) + GetActivity(id int) (*activity.Activity, error) + GetActivityHints(activityID int) ([]activity.Hint, error) + GetContentActivity(contentID int) ([]activity.Activity, error) + GetVideoFileLink(imagekey string) (string, error) + GetActivityChoices(activityID int, activityTypeID int) (activity.Choices, error) + GetContentGroups() (groups content.ContentGroups, err error) + GetCorrectProgression(activityID int) (progression *content.LearningProgression, err error) + GetPeerChoice(erAnswerID *int) (activity.ERAnswer, error) + GetERChoice(activityID int) (activity.ERChoice, error) + UseHint(userID int, reducePoint int, hintID int) error + InsertERAnswer(answer activity.ERAnswer, userID int) error +} + +type learningRepository struct { + db database.MysqlDB + cache cache.Cache +} + +func NewLearningRepository(db database.MysqlDB, cache cache.Cache) *learningRepository { + return &learningRepository{db: db, cache: cache} +} + +func (r learningRepository) GetContent(id int) (*content.Content, error) { + content := content.Content{} + + key := "learningRepository::GetContent::" + utils.ParseString(id) + + if cacheData, err := r.cache.Get(key); err == nil { + if err = json.Unmarshal([]byte(cacheData), &content); err == nil { + return &content, nil + } + } + + err := r.db.GetDB(). + Table(TableName.Content). + Where(IDName.Content+" = ?", id). + Find(&content). + Error + + if data, err := json.Marshal(content); err != nil { + return nil, err + } else { + if err = r.cache.Set(key, string(data), time.Minute*300); err != nil { + return nil, err + } + } + + return &content, err +} + +func (r learningRepository) GetOverview() ([]content.Overview, error) { + overview := make([]content.Overview, 0) + + key := "learningRepository::GetOverview" + + if cacheData, err := r.cache.Get(key); err == nil { + if err = json.Unmarshal([]byte(cacheData), &overview); err == nil { + return overview, nil + } + } + + err := r.db.GetDB(). + Table(TableName.ContentGroup). + Select("ContentGroup.content_group_id AS content_group_id", + "Content.content_id AS content_id", + "Activity.activity_id AS activity_id", + "ContentGroup.name AS group_name", + "Content.name AS content_name", + ). + Joins("LEFT JOIN Content ON ContentGroup.content_group_id = Content.content_group_id"). + Joins("LEFT JOIN Activity ON Content.content_id = Activity.content_id"). + Order("content_group_id ASC"). + Find(&overview). + Error + + if data, err := json.Marshal(overview); err != nil { + return nil, err + } else { + if err = r.cache.Set(key, string(data), time.Minute*300); err != nil { + return nil, err + } + } + + return overview, err +} + +func (r learningRepository) GetContentActivity(contentID int) ([]activity.Activity, error) { + activity := make([]activity.Activity, 0) + + key := "learningRepository::GetContentActivity::" + utils.ParseString(contentID) + + if cacheData, err := r.cache.Get(key); err == nil { + if err = json.Unmarshal([]byte(cacheData), &activity); err == nil { + return activity, nil + } + } + + err := r.db.GetDB(). + Table(TableName.Activity). + Where(IDName.Content+" = ?", contentID). + Find(&activity). + Error + + if data, err := json.Marshal(activity); err != nil { + return nil, err + } else { + if err = r.cache.Set(key, string(data), time.Minute*300); err != nil { + return nil, err + } + } + + return activity, err +} + +func (r learningRepository) GetActivity(id int) (*activity.Activity, error) { + activity := activity.Activity{} + + key := "learningRepository::GetActivity::" + utils.ParseString(id) + + if cacheData, err := r.cache.Get(key); err == nil { + if err = json.Unmarshal([]byte(cacheData), &activity); err == nil { + return &activity, nil + } + } + + err := r.db.GetDB(). + Table(TableName.Activity). + Where(IDName.Activity+" = ?", id). + Find(&activity). + Error + + if data, err := json.Marshal(activity); err != nil { + return nil, err + } else { + if err = r.cache.Set(key, string(data), time.Minute*300); err != nil { + return nil, err + } + } + + return &activity, err +} + +func (r learningRepository) getMatchingChoice(activityID int) (activity.MatchingChoices, error) { + matchingChoice := make([]activity.MatchingChoice, 0) + + key := "learningRepository::getMatchingChoice::" + utils.ParseString(activityID) + + if cacheData, err := r.cache.Get(key); err == nil { + if err = json.Unmarshal([]byte(cacheData), &matchingChoice); err == nil { + return matchingChoice, nil + } + } + + err := r.db.GetDB(). + Table(TableName.MatchingChoice). + Where(IDName.Activity+" = ?", activityID). + Find(&matchingChoice). + Error + + if data, err := json.Marshal(matchingChoice); err != nil { + return nil, err + } else { + if err = r.cache.Set(key, string(data), time.Minute*300); err != nil { + return nil, err + } + } + + return matchingChoice, err +} + +func (r learningRepository) getMultipleChoice(activityID int) (activity.MultipleChoices, error) { + multipleChoice := make([]activity.MultipleChoice, 0) + + key := "learningRepository::getMultipleChoice::" + utils.ParseString(activityID) + + if cacheData, err := r.cache.Get(key); err == nil { + if err = json.Unmarshal([]byte(cacheData), &multipleChoice); err == nil { + return multipleChoice, nil + } + } + + err := r.db.GetDB(). + Table(TableName.MultipleChoice). + Where(IDName.Activity+" = ?", activityID). + Find(&multipleChoice). + Error + + if data, err := json.Marshal(multipleChoice); err != nil { + return nil, err + } else { + if err = r.cache.Set(key, string(data), time.Minute*300); err != nil { + return nil, err + } + } + + return multipleChoice, err +} + +func (r learningRepository) getCompletionChoice(activityID int) (activity.CompletionChoices, error) { + completionChoice := make([]activity.CompletionChoice, 0) + + key := "learningRepository::getCompletionChoice::" + utils.ParseString(activityID) + + if cacheData, err := r.cache.Get(key); err == nil { + if err = json.Unmarshal([]byte(cacheData), &completionChoice); err == nil { + return completionChoice, nil + } + } + + err := r.db.GetDB(). + Table(TableName.CompletionChoice). + Where(IDName.Activity+" = ?", activityID). + Find(&completionChoice). + Error + + if data, err := json.Marshal(completionChoice); err != nil { + return nil, err + } else { + if err = r.cache.Set(key, string(data), time.Minute*300); err != nil { + return nil, err + } + } + + return completionChoice, err +} + +func (r learningRepository) getVocabGroupChoice(activityID int) (activity.VocabGroupChoice, error) { + vocalGroupChoice := activity.VocabGroupChoice{} + + key := "learningRepository::getVocabGroupChoice::" + utils.ParseString(activityID) + if cacheData, err := r.cache.Get(key); err == nil { + if err = json.Unmarshal([]byte(cacheData), &vocalGroupChoice); err == nil { + return vocalGroupChoice, nil + } + } + + rows, err := r.db.GetDB(). + Select("name", "vocab"). + Table(TableName.VocabGroupChoice). + Joins(fmt.Sprintf("INNER JOIN %s ON %s.%s = %s.%s", + TableName.VocabGroup, + TableName.VocabGroup, + IDName.VocabGroup, + TableName.VocabGroupChoice, + IDName.VocabGroup, + )). + Where(IDName.Activity+" = ?", activityID). + Rows() + + groupMap := map[string]*activity.VocabGroup{} + + for rows.Next() { + var name string + var vocab string + + err = rows.Scan(&name, &vocab) + if err != nil { + return vocalGroupChoice, err + } + + if _, ok := groupMap[name]; !ok { + groupMap[name] = &activity.VocabGroup{ + GroupName: name, + Vocabs: []string{vocab}, + } + } else { + groupMap[name].Vocabs = append(groupMap[name].Vocabs, vocab) + } + } + + for _, v := range groupMap { + vocalGroupChoice.Groups = append(vocalGroupChoice.Groups, *v) + } + + if data, err := json.Marshal(vocalGroupChoice); err != nil { + return vocalGroupChoice, err + } else { + if err = r.cache.Set(key, string(data), time.Minute*300); err != nil { + return vocalGroupChoice, err + } + } + + return vocalGroupChoice, err +} + +func (r learningRepository) getDependencyChoice(activityID int) (activity.DependencyChoice, error) { + choice := activity.DependencyChoice{} + + rows, err := r.db.GetDB(). + Table(TableName.DependencyChoice). + Select( + TableName.DependencyChoice+"."+IDName.DependencyChoice, + TableName.Dependency+"."+IDName.Dependency, + TableName.Dependency+".dependent", + TableName.Dependency+".fixed", + TableName.Determinant+".value", + TableName.Determinant+".fixed", + ). + Joins(fmt.Sprintf("INNER JOIN %s ON %s.%s = %s.%s", + TableName.Dependency, + TableName.Dependency, + IDName.DependencyChoice, + TableName.DependencyChoice, + IDName.DependencyChoice, + )). + Joins(fmt.Sprintf("INNER JOIN %s ON %s.%s = %s.%s", + TableName.Determinant, + TableName.Determinant, + IDName.Dependency, + TableName.Dependency, + IDName.Dependency, + )). + Where(IDName.Activity+" = ?", activityID). + Rows() + + if err != nil { + return choice, err + } + + defer rows.Close() + + choice.Dependencies = make([]activity.Dependency, 0) + + dependencyMap := map[int]*activity.Dependency{} + + for rows.Next() { + var id int + dependency := activity.Dependency{} + determinant := activity.Determinant{} + + err = rows.Scan(&choice.ID, &id, &dependency.Dependent, &dependency.Fixed, &determinant.Value, &determinant.Fixed) + if err != nil { + return choice, err + } + + if _, ok := dependencyMap[id]; !ok { + dependencyMap[id] = &dependency + dependencyMap[id].Determinants = make([]activity.Determinant, 0) + } + + dependencyMap[id].Determinants = append(dependencyMap[id].Determinants, determinant) + } + + for _, v := range dependencyMap { + choice.Dependencies = append(choice.Dependencies, *v) + } + + return choice, err +} + +func (r learningRepository) GetERChoice(activityID int) (activity.ERChoice, error) { + choice := activity.ERChoice{} + + rows, err := r.db.GetDB().Debug(). + Select( + TableName.ERChoice+".type", + TableName.Tables+"."+IDName.Table, + TableName.Tables+".title", + TableName.Tables+".fixed", + TableName.Attributes+"."+IDName.Attribute, + TableName.Attributes+".value", + TableName.Attributes+".key", + TableName.Attributes+".fixed", + ). + Table(TableName.ERChoice). + Joins(fmt.Sprintf("LEFT JOIN %s ON %s.%s = %s.%s", + TableName.ERChoiceTables, + TableName.ERChoiceTables, + IDName.ERChoice, + TableName.ERChoice, + IDName.ERChoice, + )). + Joins(fmt.Sprintf("LEFT JOIN %s ON %s.%s = %s.%s", + TableName.Tables, + TableName.Tables, + IDName.Table, + TableName.ERChoiceTables, + IDName.Table, + )). + Joins(fmt.Sprintf("LEFT JOIN %s ON %s.%s = %s.%s", + TableName.Attributes, + TableName.Attributes, + IDName.Table, + TableName.Tables, + IDName.Table, + )). + Where(IDName.Activity+" = ?", activityID). + Rows() + + if err != nil { + return choice, err + } + + tablesMap := map[string]*activity.Table{} + + for rows.Next() { + + var attributeID *int + var attributeValue *string + var attributeKey *string + var attributeFixed *bool + + var attribute activity.Attribute + + table := activity.Table{} + + err = rows.Scan(&choice.Type, &table.ID, &table.Title, &table.Fixed, &attributeID, &attributeValue, &attributeKey, &attributeFixed) + if err != nil { + return choice, err + } + + if attributeID != nil { + attribute = activity.Attribute{ + ID: *attributeID, + TableID: table.ID, + Key: attributeKey, + Value: *attributeValue, + Fixed: *attributeFixed, + } + + } + + if _, ok := tablesMap[table.ID]; !ok { + tablesMap[table.ID] = &table + tablesMap[table.ID].Attributes = make(activity.Attributes, 0) + } + + tablesMap[table.ID].Attributes = append(tablesMap[table.ID].Attributes, attribute) + + } + + tableIDs := make([]interface{}, 0) + + for _, v := range tablesMap { + choice.Tables = append(choice.Tables, *v) + tableIDs = append(tableIDs, v.ID) + } + + relationships := make(activity.Relationships, 0) + + if len(tableIDs) > 0 { + err = r.db.GetDB(). + Table(TableName.Relationship). + Where("table1_id IN (" + utils.ToStrings(tableIDs) + ") OR table2_id IN (" + utils.ToStrings(tableIDs) + ")"). + Find(&relationships). + Error + + if err != nil { + return choice, err + } + } + + choice.Relationships = append(choice.Relationships, relationships...) + + return choice, nil +} + +func (r learningRepository) GetPeerChoice(erAnswerID *int) (activity.ERAnswer, error) { + + answer := activity.ERAnswer{} + + query := r.db.GetDB().Debug(). + Select(IDName.Table, "title", IDName.Attribute, "value", "attribute_key", IDName.ERAnswer). + Table(ViewName.RandomERAnswer) + + if erAnswerID != nil { + query = query.Table(ViewName.AllERAnswer).Where(IDName.ERAnswer+" = ?", *erAnswerID) + } else { + query = query.Table(ViewName.RandomERAnswer) + } + + rows, err := query.Rows() + + if err != nil { + return answer, err + } + + tablesMap := map[string]*activity.Table{} + + for rows.Next() { + table := activity.Table{} + + var attributeID *int + var attributeValue *string + var attributeKey *string + + var attribute activity.Attribute + + err = rows.Scan(&table.ID, &table.Title, &attributeID, &attributeValue, &attributeKey, &answer.ID) + if err != nil { + return answer, err + } + + if attributeID != nil { + attribute = activity.Attribute{ + ID: *attributeID, + TableID: table.ID, + Key: attributeKey, + Value: *attributeValue, + Fixed: false, + } + + } + + if _, ok := tablesMap[table.ID]; !ok { + tablesMap[table.ID] = &table + tablesMap[table.ID].Attributes = make(activity.Attributes, 0) + } + + tablesMap[table.ID].Attributes = append(tablesMap[table.ID].Attributes, attribute) + } + + tableIDs := make([]interface{}, 0) + + for _, v := range tablesMap { + answer.Tables = append(answer.Tables, *v) + tableIDs = append(tableIDs, v.ID) + } + + if len(tableIDs) == 0 { + return answer, errs.ErrNotFoundError + } + + relationships := make(activity.Relationships, 0) + + err = r.db.GetDB(). + Table(TableName.Relationship). + Where("table1_id IN (" + utils.ToStrings(tableIDs) + ") OR table2_id IN (" + utils.ToStrings(tableIDs) + ")"). + Find(&relationships). + Error + + if err != nil { + return answer, err + } + + answer.Relationships = append(answer.Relationships, relationships...) + + return answer, err + +} + +func (r learningRepository) GetActivityHints(activityID int) ([]activity.Hint, error) { + hints := make([]activity.Hint, 0) + + key := "learningRepository::GetActivityHints::" + utils.ParseString(activityID) + + if cacheData, err := r.cache.Get(key); err == nil { + if err = json.Unmarshal([]byte(cacheData), &hints); err == nil { + return hints, nil + } + } + + err := r.db.GetDB(). + Table(TableName.Hint). + Where(IDName.Activity+" = ?", activityID). + Order("level ASC"). + Find(&hints). + Error + + if data, err := json.Marshal(hints); err != nil { + return nil, err + } else { + if err = r.cache.Set(key, string(data), time.Minute*300); err != nil { + return nil, err + } + } + + return hints, err +} + +func (r learningRepository) GetVideoFileLink(objectName string) (string, error) { + storage := storage.GetCloudStorageServiceInstance() + + var link string + var err error + + key := "learningRepository::GetVideoFileLink::" + objectName + + if cacheData, err := r.cache.Get(key); err == nil { + return cacheData, nil + } + + link, err = storage.GetFileLink(objectName) + if err != nil { + return "", err + } + + err = r.cache.Set(key, link, time.Minute*15) + if err != nil { + return "", err + } + + return link, nil +} + +func (r learningRepository) GetActivityChoices(activityID int, activityTypeID int) (activity.Choices, error) { + switch activityTypeID { + case 1: + return r.getMatchingChoice(activityID) + case 2: + return r.getMultipleChoice(activityID) + case 3: + return r.getCompletionChoice(activityID) + case 4: + return r.getVocabGroupChoice(activityID) + case 5: + return r.getDependencyChoice(activityID) + case 6: + return r.GetERChoice(activityID) + case 7: + return r.GetPeerChoice(nil) + default: + return nil, errs.ErrActivityTypeInvalid + } +} + +func (r learningRepository) GetContentGroups() (groups content.ContentGroups, err error) { + err = r.db.GetDB(). + Table(TableName.ContentGroup). + Find(&groups). + Error + return +} + +func (r learningRepository) GetCorrectProgression(activityID int) (progression *content.LearningProgression, err error) { + err = r.db.GetDB(). + Table(TableName.LearningProgression). + Where("is_correct = 1"). + Find(&progression). + Error + return +} + +func (r learningRepository) UseHint(userID int, reducePoint int, hintID int) error { + routine := 0 + errs := make(chan error, 2) + tx := r.db.GetDB().Begin() + + go func() { + statement := fmt.Sprintf("UPDATE %s SET point = point - %d WHERE %s = %d", + TableName.User, + reducePoint, + IDName.User, + userID, + ) + temp := map[string]interface{}{} + errs <- tx.Raw(statement).Find(&temp).Error + }() + + go func() { + hint := activity.UserHint{ + UserID: userID, + HintID: hintID, + CreatedTimestamp: time.Now().Local(), + } + errs <- tx.Table(TableName.UserHint).Create(&hint).Error + }() + + for err := range errs { + routine++ + if err != nil { + close(errs) + tx.Rollback() + return err + } + if routine == 2 { + close(errs) + } + } + + tx.Commit() + return nil +} + +func (r learningRepository) InsertERAnswer(answer activity.ERAnswer, userID int) error { + var wg sync.WaitGroup + var err error + + a := activity.ERAnswer{} + + tx := r.db.GetDB().Debug().Begin() + + wg.Add(3) + + go func() { + defer wg.Done() + e := r.db.GetDB().Table(TableName.ERAnswer).Where(IDName.User+" = ?", userID).Find(&a).Error + if e != nil { + err = e + } + }() + + go func() { + defer wg.Done() + e := tx.Table(TableName.Tables).Create(&answer.Tables).Error + if e != nil { + err = e + } + }() + + go func() { + defer wg.Done() + e := tx.Table(TableName.ERAnswer).Create(&answer).Error + if e != nil { + err = e + } + }() + + wg.Wait() + + if a.UserID == userID { + tx.Rollback() + return nil + } + + if err != nil { + tx.Rollback() + return err + } + + wg.Add(3) + + go func() { + defer wg.Done() + e := tx.Table(TableName.Relationship).Create(&answer.Relationships).Error + if e != nil { + err = e + } + }() + + go func() { + defer wg.Done() + + attributes := make([]activity.Attribute, 0) + + for i, v := range answer.Tables { + + for j := range v.Attributes { + answer.Tables[i].Attributes[j].TableID = v.ID + } + + attributes = append(attributes, v.Attributes...) + } + + if len(attributes) == 0 { + return + } + + e := tx.Table(TableName.Attributes).Create(&attributes).Error + if e != nil { + err = e + } + }() + + go func() { + defer wg.Done() + + erAnswerTables := make([]activity.ERAnswerTables, 0) + for _, v := range answer.Tables { + erAnswerTables = append(erAnswerTables, activity.ERAnswerTables{ + ERAnswerID: answer.ID, + TableID: v.ID, + }) + + } + + e := tx.Table(TableName.ERAnswerTables).Create(&erAnswerTables).Error + if e != nil { + err = e + } + }() + + wg.Wait() + + if err != nil { + tx.Rollback() + return err + } + + tx.Commit() + return nil +} diff --git a/internal/repositories/user.go b/internal/repositories/user.go new file mode 100644 index 0000000..6942832 --- /dev/null +++ b/internal/repositories/user.go @@ -0,0 +1,350 @@ +package repositories + +import ( + "database-camp/internal/infrastructure/cache" + "database-camp/internal/infrastructure/database" + "database-camp/internal/logs" + "database-camp/internal/models/entities/activity" + "database-camp/internal/models/entities/badge" + "database-camp/internal/models/entities/content" + "database-camp/internal/models/entities/user" + "database-camp/internal/utils" + "encoding/json" + "fmt" + "time" +) + +type UserRepository interface { + GetUserByEmail(email string) (*user.User, error) + GetUserByID(id int) (*user.User, error) + GetProfile(id int) (*user.Profile, error) + GetLearningProgression(id int) ([]content.LearningProgression, error) + GetAllBadge() ([]badge.Badge, error) + GetUserBadge(id int) ([]badge.UserBadge, error) + GetCollectedBadge(userID int) ([]user.CorrectedBadge, error) + GetPointRanking(id int) (*user.Ranking, error) + GetRankingLeaderBoard() ([]user.Ranking, error) + GetUserHint(userID int, activityID int) ([]activity.UserHint, error) + GetPreExamID(userID int) (*int, error) + GetPreTestResults(userID int) (user.PreTestResults, error) + GetSpiderDataset(userID int) (dataset user.SpiderDataset, err error) + InsertUser(user user.User) (*user.User, error) + InsertUserHint(userHint activity.UserHint) (*activity.UserHint, error) + InsertBadge(userBadge badge.UserBadge) (*badge.UserBadge, error) + InsertLearningProgression(userID int, activityID int, point int, isCorrect bool, hasProgression bool) error + UpdatesByID(id int, updateData map[string]interface{}) error +} + +type userRepository struct { + db database.MysqlDB + cache cache.Cache +} + +func NewUserRepository(db database.MysqlDB, cache cache.Cache) *userRepository { + return &userRepository{db: db, cache: cache} +} + +func (r userRepository) GetUserByEmail(email string) (*user.User, error) { + user := user.User{} + err := r.db.GetDB(). + Table(TableName.User). + Where("email = ?", email). + Find(&user). + Error + return &user, err +} + +func (r userRepository) GetUserByID(id int) (*user.User, error) { + user := user.User{} + + err := r.db.GetDB(). + Table(TableName.User). + Where(IDName.User+" = ?", id). + Find(&user). + Error + + return &user, err +} + +func (r userRepository) GetProfile(id int) (*user.Profile, error) { + profile := user.Profile{} + + err := r.db.GetDB(). + Table(ViewName.Profile). + Where(IDName.User+" = ?", id). + Find(&profile). + Error + + return &profile, err +} + +func (r userRepository) GetLearningProgression(id int) ([]content.LearningProgression, error) { + progresstion := make([]content.LearningProgression, 0) + + err := r.db.GetDB(). + Table(TableName.LearningProgression). + Where(IDName.User+" = ? AND is_correct = 1", id). + Order("created_timestamp desc"). + Group(IDName.Activity). + Find(&progresstion). + Error + + return progresstion, err +} + +func (r userRepository) GetAllBadge() ([]badge.Badge, error) { + badge := make([]badge.Badge, 0) + + key := "userRepository::GetAllBadge" + + if cacheData, err := r.cache.Get(key); err == nil { + if err = json.Unmarshal([]byte(cacheData), &badge); err == nil { + return badge, nil + } + } + + err := r.db.GetDB(). + Table(TableName.Badge). + Find(&badge). + Error + + if data, err := json.Marshal(badge); err != nil { + return nil, err + } else { + if err = r.cache.Set(key, string(data), time.Minute*10); err != nil { + return nil, err + } + } + + return badge, err +} + +func (r userRepository) GetUserBadge(id int) ([]badge.UserBadge, error) { + userBadgeDB := make([]badge.UserBadge, 0) + err := r.db.GetDB(). + Table(TableName.UserBadge). + Where(IDName.User+" = ?", id). + Find(&userBadgeDB). + Error + return userBadgeDB, err +} + +func (r userRepository) GetCollectedBadge(userID int) ([]user.CorrectedBadge, error) { + correctedBadge := make([]user.CorrectedBadge, 0) + err := r.db.GetDB(). + Table(TableName.Badge). + Select( + TableName.Badge+".badge_id AS badge_id", + TableName.Badge+".name AS badge_name", + TableName.UserBadge+".user_id AS user_id", + ). + Joins(fmt.Sprintf("LEFT JOIN %s ON %s.%s = %s.%s AND %s.%s = %d", + TableName.UserBadge, + TableName.UserBadge, + IDName.Badge, + TableName.Badge, + IDName.Badge, + TableName.UserBadge, + IDName.User, + userID, + )). + Find(&correctedBadge). + Error + return correctedBadge, err +} + +func (r userRepository) GetPointRanking(id int) (*user.Ranking, error) { + ranking := user.Ranking{} + + key := "userRepository::GetPointRanking::" + utils.ParseString(id) + + if cacheData, err := r.cache.Get(key); err == nil { + if err = json.Unmarshal([]byte(cacheData), &ranking); err == nil { + return &ranking, nil + } + } + + err := r.db.GetDB(). + Table(ViewName.Ranking). + Where(IDName.User+" = ?", id). + Find(&ranking). + Error + + if data, err := json.Marshal(ranking); err != nil { + return nil, err + } else { + if err = r.cache.Set(key, string(data), time.Minute*10); err != nil { + return nil, err + } + } + + return &ranking, err +} + +func (r userRepository) GetRankingLeaderBoard() ([]user.Ranking, error) { + ranking := make([]user.Ranking, 0) + + key := "userRepository::GetRankingLeaderBoard" + + if cacheData, err := r.cache.Get(key); err == nil { + if err = json.Unmarshal([]byte(cacheData), &ranking); err == nil { + return ranking, nil + } + } + + err := r.db.GetDB(). + Table(ViewName.Ranking). + Limit(20). + Find(&ranking). + Error + + if data, err := json.Marshal(ranking); err != nil { + return nil, err + } else { + if err = r.cache.Set(key, string(data), time.Minute*10); err != nil { + return nil, err + } + } + + return ranking, err +} + +func (r userRepository) GetUserHint(userID int, activityID int) ([]activity.UserHint, error) { + userhint := make([]activity.UserHint, 0) + + hintSubquery := r.db.GetDB(). + Select("hint_id"). + Table(TableName.Hint). + Where(IDName.Activity+" = ?", activityID) + + err := r.db.GetDB(). + Table(TableName.UserHint). + Where(IDName.Hint+" IN (?)", hintSubquery). + Where(IDName.User+" = ?", userID). + Find(&userhint). + Error + + return userhint, err +} + +func (r userRepository) GetSpiderDataset(userID int) (dataset user.SpiderDataset, err error) { + err = r.db.GetDB(). + Table(ViewName.SpiderData). + Where(IDName.User+" = ?", userID). + Find(&dataset). + Error + + return +} + +func (r userRepository) InsertUser(user user.User) (*user.User, error) { + err := r.db.GetDB(). + Table(TableName.User). + Create(&user). + Error + return &user, err +} + +func (r userRepository) InsertUserHint(userHint activity.UserHint) (*activity.UserHint, error) { + err := r.db.GetDB(). + Table(TableName.UserHint). + Create(&userHint). + Error + return &userHint, err +} + +func (r userRepository) InsertBadge(userBadge badge.UserBadge) (*badge.UserBadge, error) { + err := r.db.GetDB(). + Table(TableName.UserBadge). + Create(&userBadge). + Error + + if utils.IsSqlDuplicateError(err) { + return &userBadge, nil + } else { + return &userBadge, err + } + +} + +func (r userRepository) InsertLearningProgression(userID int, activityID int, point int, isCorrect bool, hasProgression bool) error { + tx := r.db.GetDB().Begin() + + progression := content.LearningProgression{ + UserID: userID, + ActivityID: activityID, + IsCorrect: isCorrect, + CreatedTimestamp: time.Now().Local(), + } + + err := tx.Table(TableName.LearningProgression).Create(&progression).Error + if err != nil { + tx.Rollback() + return err + } + + logs.GetInstance().Info(hasProgression) + + if !hasProgression && isCorrect { + logs.GetInstance().Info("update point") + + statement := fmt.Sprintf("UPDATE %s SET point = point + %d WHERE %s = %d", + TableName.User, + point, + IDName.User, + userID, + ) + temp := map[string]interface{}{} + err = tx.Debug().Raw(statement).Find(&temp).Error + } + + logs.GetInstance().Info("not update point") + + if err != nil { + tx.Rollback() + return err + } + + tx.Commit() + return nil +} + +func (r userRepository) UpdatesByID(id int, updateData map[string]interface{}) error { + err := r.db.GetDB(). + Table(TableName.User). + Select("", utils.GetKeyList(updateData)). + Where(IDName.User+" = ?", id). + Updates(updateData). + Error + + return err +} + +func (r userRepository) GetPreExamID(userID int) (*int, error) { + data := struct { + UserID int `gorm:"column:user_id"` + ExamID int `gorm:"column:exam_id"` + }{} + + err := r.db.GetDB(). + Table(ViewName.UserPreTest). + Where(IDName.User+"=?", userID). + Find(&data). + Error + + return &data.ExamID, err +} + +func (r userRepository) GetPreTestResults(userID int) (user.PreTestResults, error) { + var results user.PreTestResults + + err := r.db.GetDB(). + Table(ViewName.UserPreTestResult). + Where(IDName.User+"=?", userID). + Order("created_timestamp ASC"). + Limit(1). + Find(&results). + Error + + return results, err +} diff --git a/internal/routes/router.go b/internal/routes/router.go new file mode 100644 index 0000000..f39590d --- /dev/null +++ b/internal/routes/router.go @@ -0,0 +1,104 @@ +package routes + +import ( + "database-camp/internal/infrastructure/application" + "database-camp/internal/registry" + "net/http" +) + +var ( + BuildCommit string + BuildTime string +) + +type router struct { + app application.App + route application.Router + regis registry.Registry +} + +var instantiated *router = nil + +func NewRouter(app application.App, regis registry.Registry) *router { + if instantiated == nil { + instantiated = &router{ + app: app, + route: app.Group("api/v1"), + regis: regis, + } + instantiated.setup() + } + return instantiated +} + +func (r *router) setup() { + r.setupProbe() + r.setupUser() + r.setupLearning() + r.setupExam() +} + +func (r *router) setupProbe() { + r.route.Get("/x", func(c application.Context) { + c.JSON(http.StatusOK, map[string]string{ + "build_commit": BuildCommit, + "build_time": BuildTime, + }) + }) + + r.route.Get("/healthz", func(c application.Context) { + c.JSON(http.StatusOK, map[string]string{ + "status": "OK", + }) + }) +} + +func (r *router) setupUser() { + jwt := r.regis.GetMiddlewares().Jwt + handler := r.regis.GetHandlers().UserHandler + userRoute := r.route.Group("user") + { + userRoute.Post("/register", handler.Register) + userRoute.Post("/login", handler.Login) + } + + { + userRoute.Get("/info", jwt.Verify, handler.GetOwnProfile) + userRoute.Get("/profile/:id", jwt.Verify, handler.GetProfile) + userRoute.Get("/ranking", jwt.Verify, handler.GetUserRanking) + userRoute.Put("/profile", jwt.Verify, handler.Edit) + } +} + +func (r *router) setupLearning() { + jwt := r.regis.GetMiddlewares().Jwt + handler := r.regis.GetHandlers().LearningHandler + learningRoute := r.route.Group("learning", jwt.Verify) + activityRoute := learningRoute.Group("activity") + { + learningRoute.Get("/video/:id", handler.GetVideo) + learningRoute.Get("/overview", handler.GetOverview) + learningRoute.Get("/content/roadmap/:id", handler.GetContentRoadmap) + learningRoute.Get("/recommend", handler.GetRecommend) + + } + + { + activityRoute.Get("/:id", handler.GetActivity) + activityRoute.Post("/hint/:id", handler.UseHint) + activityRoute.Post("/check-answer", handler.CheckAnswer) + activityRoute.Post("/peer", handler.PeerReview) + } +} + +func (r *router) setupExam() { + jwt := r.regis.GetMiddlewares().Jwt + handler := r.regis.GetHandlers().ExamHandler + examRoute := r.route.Group("exam", jwt.Verify) + { + examRoute.Get("/proposition/:id", handler.GetExam) + examRoute.Get("/overview", handler.GetExamOverview) + examRoute.Get("/result/:id", handler.GetExamResult) + examRoute.Post("/check", handler.CheckExam) + } +} diff --git a/internal/services/exam.go b/internal/services/exam.go new file mode 100644 index 0000000..fff4908 --- /dev/null +++ b/internal/services/exam.go @@ -0,0 +1,241 @@ +package services + +import ( + "database-camp/internal/errs" + "database-camp/internal/infrastructure/cache" + "database-camp/internal/logs" + "database-camp/internal/models/entities/badge" + "database-camp/internal/models/entities/exam" + "database-camp/internal/models/request" + "database-camp/internal/models/response" + "database-camp/internal/repositories" + "database-camp/internal/services/loaders" + "database-camp/internal/utils" + "encoding/json" + "time" +) + +type ExamService interface { + GetExam(examID int, userID int) (*response.ExamResponse, error) + GetOverview(userID int) (*response.ExamOverviewResponse, error) + CheckExam(userID int, request request.ExamAnswerRequest) (*response.ExamResultOverviewResponse, error) + GetExamResult(userID int, examResultID int) (*response.ExamResultOverviewResponse, error) +} + +type examService struct { + examRepo repositories.ExamRepository + userRepo repositories.UserRepository + learningRepo repositories.LearningRepository + + cache cache.Cache +} + +func NewExamService( + examRepo repositories.ExamRepository, + userRepo repositories.UserRepository, + learningRepo repositories.LearningRepository, + cache cache.Cache, +) *examService { + return &examService{ + examRepo: examRepo, + userRepo: userRepo, + learningRepo: learningRepo, + cache: cache, + } +} + +func (s examService) GetExam(examID int, userID int) (*response.ExamResponse, error) { + examLoader := loaders.NewExamLoader(s.examRepo, s.userRepo) + + err := examLoader.Load(userID, examID) + if err != nil { + logs.GetInstance().Error(err) + return nil, errs.ErrExamNotFound + } + + examActivities := examLoader.GetExamActivities() + correctedBadges := examLoader.GetCorrectedBadge() + _exam := examLoader.GetExam() + + logs.GetInstance().Info(*_exam) + + if len(examActivities) == 0 || *_exam == (exam.Exam{}) { + return nil, errs.ErrExamNotFound + } + + if _exam.Type == string(exam.POST) && !correctedBadges.CanDoFianlExam() { + return nil, errs.ErrFinalExamBadgesNotEnough + } + + activitiesExamLoader := loaders.NewActivityExamLoader(s.learningRepo) + + err = activitiesExamLoader.Load(examActivities) + if err != nil { + logs.GetInstance().Error(err) + return nil, errs.ErrExamNotFound + } + + activities := activitiesExamLoader.GetActivities() + + activitiesResponse := make([]response.ActivityResponse, 0) + + for _, activity := range activities { + choices := activity.Choices.CreatePropositionChoices() + activitiesResponse = append(activitiesResponse, response.ActivityResponse{ + Activity: activity.Activity, + Choices: choices, + Hint: nil, + }) + } + + response := response.ExamResponse{ + Exam: *_exam, + Activities: activitiesResponse, + } + + return &response, nil +} + +func (s examService) GetOverview(userID int) (*response.ExamOverviewResponse, error) { + loader := loaders.NewExamOverviewLoader(s.examRepo, s.userRepo) + + err := loader.Load(userID) + if err != nil { + logs.GetInstance().Error(err) + return nil, errs.ErrLoadError + } + + examResults := loader.GetExamResults() + exams := loader.GetExams() + correctedBadges := loader.GetCorrectedBadges() + + canDo := correctedBadges.CanDoFianlExam() + + response := response.ExamOverviewResponse{ + MiniExam: exams.GetMiniExam(examResults), + FinalExam: exams.GetFinalExam(examResults, canDo), + } + + return &response, nil +} + +func (s examService) CheckExam(userID int, request request.ExamAnswerRequest) (*response.ExamResultOverviewResponse, error) { + checkExamLoader := loaders.NewCheckExamLoader(s.examRepo) + + err := checkExamLoader.Load(*request.ExamID) + if err != nil { + logs.GetInstance().Error(err) + return nil, errs.ErrExamNotFound + } + + examActivities := checkExamLoader.GetExamActivities() + exam := checkExamLoader.GetExam() + + if len(examActivities) == 0 { + return nil, errs.ErrExamNotFound + } + + activitiesExamLoader := loaders.NewActivityExamLoader(s.learningRepo) + + err = activitiesExamLoader.Load(examActivities) + if err != nil { + logs.GetInstance().Error(err) + return nil, errs.ErrExamNotFound + } + + activities := activitiesExamLoader.GetActivities() + + if len(request.Activities) != len(activities) { + return nil, errs.ErrActivitiesNumberIncorrect + } + + result, err := activities.CheckAnswers(*request.ExamID, userID, request.Activities) + if err != nil { + logs.GetInstance().Info(err) + + return nil, err + } + + result, err = s.examRepo.SaveResult(*result) + if err != nil { + return nil, err + } + + if exam.BadgeID != 0 { + _, err = s.userRepo.InsertBadge(badge.UserBadge{ + UserID: userID, + BadgeID: exam.BadgeID, + }) + if err != nil { + logs.GetInstance().Error(err) + return nil, errs.ErrInsertError + } + } + + response := response.ExamResultOverviewResponse{ + ExamID: exam.ID, + ExamResultID: result.ExamResult.ID, + ExamType: exam.Type, + ContentGroupName: exam.ContentGroupName, + CreatedTimestamp: result.ExamResult.CreatedTimestamp, + Score: result.ExamResult.Score, + IsPassed: result.ExamResult.IsPassed, + ActivitiesResult: result.ActivitiesResult, + } + + key := "examService::GetExamResult::" + utils.ParseString(userID) + "::" + utils.ParseString(result.ExamResult.ID) + + if data, err := json.Marshal(response); err != nil { + return nil, err + } else { + if err = s.cache.Set(key, string(data), time.Minute*10); err != nil { + return nil, err + } + } + + return &response, nil +} + +func (s examService) GetExamResult(userID int, examResultID int) (*response.ExamResultOverviewResponse, error) { + var res response.ExamResultOverviewResponse + + key := "examService::GetExamResult::" + utils.ParseString(userID) + "::" + utils.ParseString(examResultID) + + if cacheData, err := s.cache.Get(key); err == nil { + if err = json.Unmarshal([]byte(cacheData), &res); err == nil { + return &res, nil + } + } + + loader := loaders.NewExamResultLoader(s.examRepo) + + err := loader.Load(userID, examResultID) + if err != nil { + logs.GetInstance().Error(err) + return nil, errs.ErrExamNotFound + } + + examResult := loader.GetExamResult() + resultActivities := loader.GetResultActivities() + + res = response.ExamResultOverviewResponse{ + ExamResultID: examResult.ID, + ExamID: examResult.ExamID, + ExamType: examResult.ExamType, + ContentGroupName: examResult.ExamType, + CreatedTimestamp: examResult.CreatedTimestamp, + Score: examResult.Score, + IsPassed: examResult.IsPassed, + ActivitiesResult: resultActivities, + } + + if data, err := json.Marshal(res); err != nil { + return nil, err + } else { + if err = s.cache.Set(key, string(data), time.Minute*10); err != nil { + return nil, err + } + } + + return &res, nil +} diff --git a/internal/services/learning.go b/internal/services/learning.go new file mode 100644 index 0000000..fe02117 --- /dev/null +++ b/internal/services/learning.go @@ -0,0 +1,374 @@ +package services + +import ( + "database-camp/internal/errs" + "database-camp/internal/logs" + "database-camp/internal/models/entities/activity" + "database-camp/internal/models/entities/content" + "database-camp/internal/models/request" + "database-camp/internal/models/response" + "database-camp/internal/repositories" + "database-camp/internal/services/loaders" + "database-camp/internal/utils" +) + +type LearningService interface { + GetVideoLecture(id int) (*response.VideoLectureResponse, error) + GetOverview(userID int) (*response.ContentOverviewResponse, error) + GetActivity(userID int, activityID int) (*response.ActivityResponse, error) + GetRecommend(userID int) (*response.RecommendResponse, error) + UseHint(userID int, activityID int) (*response.UsedHintResponse, error) + GetContentRoadmap(userID int, contentID int) (*response.ContentRoadmapResponse, error) + CheckAnswer(userID int, request request.CheckAnswerRequest) (*response.AnswerResponse, error) + CheckPeerReview(userID int, request request.PeerReviewRequest) (*response.AnswerResponse, error) +} + +type learningService struct { + learningRepo repositories.LearningRepository + userRepo repositories.UserRepository +} + +func NewLearningService(learningRepo repositories.LearningRepository, userRepo repositories.UserRepository) *learningService { + return &learningService{learningRepo: learningRepo, userRepo: userRepo} +} + +func (s learningService) GetVideoLecture(id int) (*response.VideoLectureResponse, error) { + contentDB, err := s.learningRepo.GetContent(id) + if err != nil || contentDB == nil { + logs.GetInstance().Error(err) + return nil, errs.ErrContentNotFound + } + + videoLink, err := s.learningRepo.GetVideoFileLink(contentDB.VideoPath) + if err != nil { + logs.GetInstance().Error(err) + return nil, errs.ErrServiceUnavailableError + } + + response := response.VideoLectureResponse{ + ContentID: contentDB.ID, + ContentName: contentDB.Name, + VideoLink: videoLink, + } + + return &response, nil +} + +func (s learningService) GetOverview(userID int) (*response.ContentOverviewResponse, error) { + loader := loaders.NewLearningOverviewLoader(s.learningRepo, s.userRepo) + + err := loader.Load(userID) + if err != nil { + logs.GetInstance().Error(err) + return nil, errs.ErrLoadError + } + + overview := loader.GetOverview() + preExamID := loader.GetPreExamID() + learningProgression := loader.GetLearningProgression() + lastedGroup, contentGroup := overview.GetLearningOverview(learningProgression) + + response := response.ContentOverviewResponse{ + PreExam: preExamID, + LastedGroup: lastedGroup, + ContentGroupOverview: contentGroup, + } + + return &response, nil +} + +func (s learningService) GetActivity(userID int, activityID int) (*response.ActivityResponse, error) { + loader := loaders.NewActivityLoader(s.learningRepo, s.userRepo) + + err := loader.Load(userID, activityID) + if err != nil { + logs.GetInstance().Error(err) + return nil, errs.ErrLoadError + } + + _activity := loader.GetActivity() + activityHints := loader.GetActivityHints() + userHints := loader.GetUserHints() + + if *_activity == (activity.Activity{}) { + return nil, errs.ErrActivitiesNotFound + } + + choices, err := s.learningRepo.GetActivityChoices(_activity.ID, _activity.TypeID) + if err != nil { + logs.GetInstance().Error(err) + return nil, errs.ErrActivitiesNotFound + } + + response := response.ActivityResponse{ + Activity: *_activity, + Choices: choices.CreatePropositionChoices(), + Hint: &activity.ActivityHint{ + TotalHint: len(activityHints), + UsedHints: activityHints.GetUsedHints(userHints), + HintRoadMap: activityHints.CreateRoadmap(), + }, + } + + return &response, nil +} + +func (s learningService) UseHint(userID int, activityID int) (*response.UsedHintResponse, error) { + loader := loaders.NewHintLoader(s.learningRepo, s.userRepo) + + err := loader.Load(userID, activityID) + if err != nil { + logs.GetInstance().Error(err) + return nil, errs.ErrLoadError + } + + activityHints := loader.GetActivityHintsDB() + userHints := loader.GetUserHintsDB() + user := loader.GetUser() + + if len(activityHints) == 0 { + return nil, errs.ErrLoadError + } + + nextLevelHint := activityHints.GetNextLevelHint(userHints) + if nextLevelHint == nil { + return nil, errs.ErrHintAlreadyUsed + } + + if user.Point < nextLevelHint.PointReduce { + return nil, errs.ErrHintPointsNotEnough + } + + err = s.learningRepo.UseHint(userID, nextLevelHint.PointReduce, nextLevelHint.ID) + if err != nil { + logs.GetInstance().Error(err) + return nil, errs.ErrInsertError + } + + response := response.UsedHintResponse{ + HintDB: *nextLevelHint, + } + + return &response, nil +} + +func (s learningService) GetContentRoadmap(userID int, contentID int) (*response.ContentRoadmapResponse, error) { + loader := loaders.NewContentRoadmapLoader(s.learningRepo, s.userRepo) + + err := loader.Load(userID, contentID) + if err != nil { + logs.GetInstance().Error(err) + return nil, errs.ErrContentNotFound + } + + content := loader.GetContent() + contentActivity := loader.GetContentActivity() + learningProgression := loader.GetLearningProgression() + + if content == nil { + return nil, errs.ErrContentNotFound + } + + roadmapItems := contentActivity.GetContentRoadmap(learningProgression) + + response := response.ContentRoadmapResponse{ + ContentID: content.ID, + ContentName: content.Name, + Items: roadmapItems, + } + + return &response, nil +} + +func (s learningService) finishActivityAnswer( + progression *content.LearningProgression, + activityID int, + activityTypeID int, + activityPoint int, + isCorrect bool, + userID int, +) (int, error) { + hasProgression := *progression != (content.LearningProgression{}) + + err := s.userRepo.InsertLearningProgression(userID, activityID, activityPoint, isCorrect, hasProgression) + if err != nil { + logs.GetInstance().Error(err) + return 0, errs.ErrInsertError + } + + user, err := s.userRepo.GetUserByID(userID) + if err != nil || user == nil { + logs.GetInstance().Error(err) + return 0, errs.ErrUserNotFound + } + + logs.GetInstance().Info(user.Point) + + return user.Point, nil +} + +func (s learningService) CheckAnswer(userID int, request request.CheckAnswerRequest) (*response.AnswerResponse, error) { + + loader := loaders.NewCheckAnswerLoader(s.learningRepo) + + err := loader.Load(*request.ActivityID, *request.ActivityTypeID) + if err != nil { + logs.GetInstance().Error(err) + return nil, errs.ErrLoadError + } + + _activity := loader.GetActivity() + choices := loader.GetChoices() + progression := loader.GetProgression() + + if _activity == nil { + return nil, errs.ErrLoadError + } + + if _activity.TypeID != *request.ActivityTypeID { + return nil, errs.ErrActivityTypeInvalid + } + + var isCorrect bool + var errMessage *string + var erChoiceAnswer activity.ERChoiceAnswer + var choice activity.ERChoice + + if *request.ActivityTypeID == 6 { + var ok bool + choice, ok = choices.(activity.ERChoice) + if !ok { + return nil, errs.ErrAnswerInvalid + } + + err := utils.StructToStruct(request.Answer, &erChoiceAnswer) + + if err != nil { + logs.GetInstance().Error(err) + return nil, errs.ErrInternalServerError + } + + var message string + isCorrect, message = erChoiceAnswer.IsCorrect(choice) + + errMessage = &message + } else { + formatedAnswer, err := activity.FormatAnswer(request.Answer, *request.ActivityTypeID) + if err != nil { + logs.GetInstance().Error(err) + return nil, err + } + + isCorrect, err = formatedAnswer.IsCorrect(choices) + if err != nil { + return nil, err + } + } + + updatedPoint, err := s.finishActivityAnswer( + progression, + *request.ActivityID, + *request.ActivityTypeID, + _activity.Point, + isCorrect, + userID, + ) + + if err != nil { + return nil, err + } + + if *request.ActivityTypeID == activity.PEER_ACTIVITY_TYPE_ID && *request.ActivityID == activity.PEER_ACTIVITY_ID && choice.Type == activity.ER_CHOICE_DRAW { + + erAnswer := activity.ERAnswer{ + UserID: userID, + Tables: erChoiceAnswer.Tables, + Relationships: erChoiceAnswer.Relationships, + } + + err = s.learningRepo.InsertERAnswer(erAnswer, userID) + if err != nil && !utils.IsSqlDuplicateError(err) { + logs.GetInstance().Error(err) + return nil, errs.ErrInsertError + } + } + + response := response.AnswerResponse{ + ActivityID: _activity.ID, + IsCorrect: isCorrect, + UpdatedPoint: updatedPoint, + ErrMessage: errMessage, + } + + return &response, nil +} + +func (s learningService) GetRecommend(userID int) (*response.RecommendResponse, error) { + loader := loaders.NewRecommendLoader(s.learningRepo, s.userRepo) + + err := loader.Load(userID) + if err != nil { + logs.GetInstance().Error(err) + return nil, errs.ErrLoadError + } + + contentGroups := loader.GetContentGroups() + preTestResults := loader.GetPreTestResults() + + recommend := preTestResults.GetRecommend(contentGroups) + + response := response.RecommendResponse{ + Recommend: recommend, + } + + return &response, err +} + +func (s learningService) CheckPeerReview(userID int, request request.PeerReviewRequest) (*response.AnswerResponse, error) { + loader := loaders.NewCheckPeerReviewLoader(s.learningRepo) + + err := loader.Load(*request.ERAnswerID, activity.PEER_ACTIVITY_ID) + if err != nil { + logs.GetInstance().Error(err) + + if err == errs.ErrNotFoundError { + return nil, errs.ErrNotFoundError + } else { + return nil, errs.ErrLoadError + } + } + + _erAnswer := loader.GetERAnswer() + _erChoice := loader.GetERChoice() + _progression := loader.GetProgression() + + suggestionsList := _erChoice.GetSuggestionsList(activity.ERChoiceAnswer{ + Tables: _erAnswer.Tables, + Relationships: _erAnswer.Relationships, + }) + + logs.GetInstance().Info(suggestionsList) + + correct := suggestionsList.Compare(request.Reviews) + + updatedPoint, err := s.finishActivityAnswer( + _progression, + activity.PEER_ACTIVITY_ID, + activity.PEER_ACTIVITY_TYPE_ID, + activity.PEER_ACTIVITY_POINT, + correct, + userID, + ) + + if err != nil { + return nil, err + } + + res := response.AnswerResponse{ + ActivityID: activity.PEER_ACTIVITY_ID, + IsCorrect: correct, + UpdatedPoint: updatedPoint, + } + + return &res, nil +} diff --git a/internal/services/loaders/activity.go b/internal/services/loaders/activity.go new file mode 100644 index 0000000..9518a37 --- /dev/null +++ b/internal/services/loaders/activity.go @@ -0,0 +1,71 @@ +package loaders + +import ( + "database-camp/internal/models/entities/activity" + "database-camp/internal/repositories" + "sync" +) + +type activityLoader struct { + learningRepo repositories.LearningRepository + userRepo repositories.UserRepository + + activity *activity.Activity + activityHints []activity.Hint + userHints []activity.UserHint +} + +func NewActivityLoader(learningRepo repositories.LearningRepository, userRepo repositories.UserRepository) *activityLoader { + return &activityLoader{learningRepo: learningRepo, userRepo: userRepo} +} + +func (l *activityLoader) GetActivity() *activity.Activity { + return l.activity +} + +func (l *activityLoader) GetActivityHints() activity.Hints { + return l.activityHints +} + +func (l *activityLoader) GetUserHints() activity.UserHints { + return l.userHints +} + +func (l *activityLoader) Load(userID int, activityID int) error { + var wg sync.WaitGroup + var err error + concurrent := Concurrent{Wg: &wg, Err: &err} + wg.Add(3) + go l.loadActivityAsync(&concurrent, activityID) + go l.loadActivityHints(&concurrent, activityID) + go l.loadUserHintsAsync(&concurrent, userID, activityID) + wg.Wait() + return err +} + +func (l *activityLoader) loadActivityAsync(concurrent *Concurrent, activityID int) { + defer concurrent.Wg.Done() + var err error + l.activity, err = l.learningRepo.GetActivity(activityID) + if err != nil { + *concurrent.Err = err + } +} + +func (l *activityLoader) loadUserHintsAsync(concurrent *Concurrent, userID int, activityID int) { + defer concurrent.Wg.Done() + result, e := l.userRepo.GetUserHint(userID, activityID) + if e != nil { + *concurrent.Err = e + } + l.userHints = append(l.userHints, result...) +} + +func (l *activityLoader) loadActivityHints(concurrent *Concurrent, activityID int) { + defer concurrent.Wg.Done() + result, e := l.learningRepo.GetActivityHints(activityID) + if e != nil { + *concurrent.Err = e + } + l.activityHints = append(l.activityHints, result...) +} diff --git a/internal/services/loaders/activity_exam.go b/internal/services/loaders/activity_exam.go new file mode 100644 index 0000000..f894fc2 --- /dev/null +++ b/internal/services/loaders/activity_exam.go @@ -0,0 +1,77 @@ +package loaders + +import ( + "database-camp/internal/models/entities/activity" + "database-camp/internal/models/entities/exam" + "database-camp/internal/repositories" + "sync" +) + +type activityExamLoader struct { + learningRepo repositories.LearningRepository + + activities []exam.Activity + mutex sync.Mutex +} + +func NewActivityExamLoader(learningRepo repositories.LearningRepository) *activityExamLoader { + return &activityExamLoader{learningRepo: learningRepo} +} + +func (l *activityExamLoader) GetActivities() exam.Activities { + return l.activities +} + +func (l *activityExamLoader) Load(examActivities []exam.ExamActivity) error { + var wg sync.WaitGroup + var err error + + wg.Add(len(examActivities)) + + for _, activity := range examActivities { + go func(activityID int, typeID int) { + defer wg.Done() + l.loadActivity(activityID, typeID, &err) + }(activity.ActivityID, activity.ActivityTypeID) + } + + wg.Wait() + return err +} + +func (l *activityExamLoader) loadActivity(activityID int, typeID int, err *error) { + var wg sync.WaitGroup + var _activity *activity.Activity + var choices activity.Choices + + wg.Add(2) + + go func() { + defer wg.Done() + var e error + _activity, e = l.learningRepo.GetActivity(activityID) + if e != nil { + *err = e + } + }() + + go func() { + defer wg.Done() + var e error + choices, e = l.learningRepo.GetActivityChoices(activityID, typeID) + if e != nil { + *err = e + } + }() + + wg.Wait() + + l.mutex.Lock() + + l.activities = append(l.activities, exam.Activity{ + Activity: *_activity, + Choices: choices, + }) + + l.mutex.Unlock() +} diff --git a/internal/services/loaders/check_answer.go b/internal/services/loaders/check_answer.go new file mode 100644 index 0000000..79e972a --- /dev/null +++ b/internal/services/loaders/check_answer.go @@ -0,0 +1,71 @@ +package loaders + +import ( + "database-camp/internal/models/entities/activity" + "database-camp/internal/models/entities/content" + "database-camp/internal/repositories" + "sync" +) + +type checkAnswerLoader struct { + learningRepo repositories.LearningRepository + + choices activity.Choices + activity *activity.Activity + progression *content.LearningProgression +} + +func NewCheckAnswerLoader(learningRepo repositories.LearningRepository) *checkAnswerLoader { + return &checkAnswerLoader{learningRepo: learningRepo} +} + +func (c *checkAnswerLoader) GetChoices() activity.Choices { + return c.choices +} + +func (c *checkAnswerLoader) GetActivity() *activity.Activity { + return c.activity +} + +func (c *checkAnswerLoader) GetProgression() *content.LearningProgression { + return c.progression +} + +func (c *checkAnswerLoader) Load(activityID int, activityTypeID int) error { + var wg sync.WaitGroup + var err error + concurrent := Concurrent{Wg: &wg, Err: &err} + wg.Add(3) + go c.loadActivityAsync(&concurrent, activityID) + go c.loadChioces(&concurrent, activityID, activityTypeID) + go c.loadProgression(&concurrent, activityID) + wg.Wait() + return err +} + +func (c *checkAnswerLoader) loadActivityAsync(concurrent *Concurrent, activityID int) { + defer concurrent.Wg.Done() + var err error + c.activity, err = c.learningRepo.GetActivity(activityID) + if err != nil { + *concurrent.Err = err + } +} + +func (c *checkAnswerLoader) loadChioces(concurrent *Concurrent, activityID int, activityTypeID int) { + defer concurrent.Wg.Done() + var err error + c.choices, err = c.learningRepo.GetActivityChoices(activityID, activityTypeID) + if err != nil { + *concurrent.Err = err + } +} + +func (c *checkAnswerLoader) loadProgression(concurrent *Concurrent, activityID int) { + defer concurrent.Wg.Done() + var err error + c.progression, err = c.learningRepo.GetCorrectProgression(activityID) + if err != nil { + *concurrent.Err = err + } +} diff --git a/internal/services/loaders/check_exam.go b/internal/services/loaders/check_exam.go new file mode 100644 index 0000000..71f0219 --- /dev/null +++ b/internal/services/loaders/check_exam.go @@ -0,0 +1,57 @@ +package loaders + +import ( + "database-camp/internal/models/entities/exam" + "database-camp/internal/repositories" + "sync" +) + +type checkExamLoader struct { + examRepo repositories.ExamRepository + + exam *exam.Exam + examActivities []exam.ExamActivity +} + +func NewCheckExamLoader(examRepo repositories.ExamRepository) *checkExamLoader { + return &checkExamLoader{examRepo: examRepo} +} + +func (l checkExamLoader) GetExam() *exam.Exam { + return l.exam +} + +func (l checkExamLoader) GetExamActivities() []exam.ExamActivity { + return l.examActivities +} + +func (l *checkExamLoader) Load(examID int) error { + var wg sync.WaitGroup + var err error + concurrent := Concurrent{Wg: &wg, Err: &err} + wg.Add(2) + go l.loadExamActivies(&concurrent, examID) + go l.loadExam(&concurrent, examID) + wg.Wait() + return err +} + +func (l *checkExamLoader) loadExamActivies(concurrent *Concurrent, examID int) { + defer concurrent.Wg.Done() + + result, err := l.examRepo.GetExamActivities(examID) + if err != nil { + *concurrent.Err = err + } + + l.examActivities = append(l.examActivities, result...) +} + +func (l *checkExamLoader) loadExam(concurrent *Concurrent, examID int) { + defer concurrent.Wg.Done() + result, err := l.examRepo.GetExam(examID) + if err != nil { + *concurrent.Err = err + } + l.exam = result +} diff --git a/internal/services/loaders/check_peer_review.go b/internal/services/loaders/check_peer_review.go new file mode 100644 index 0000000..8809151 --- /dev/null +++ b/internal/services/loaders/check_peer_review.go @@ -0,0 +1,71 @@ +package loaders + +import ( + "database-camp/internal/models/entities/activity" + "database-camp/internal/models/entities/content" + "database-camp/internal/repositories" + "sync" +) + +type checkPeerReviewLoader struct { + learningRepo repositories.LearningRepository + + erAnswer *activity.ERAnswer + erChoice *activity.ERChoice + progresstion *content.LearningProgression +} + +func NewCheckPeerReviewLoader(learningRepo repositories.LearningRepository) *checkPeerReviewLoader { + return &checkPeerReviewLoader{learningRepo: learningRepo} +} + +func (l checkPeerReviewLoader) GetERAnswer() *activity.ERAnswer { + return l.erAnswer +} + +func (l checkPeerReviewLoader) GetERChoice() *activity.ERChoice { + return l.erChoice +} + +func (l checkPeerReviewLoader) GetProgression() *content.LearningProgression { + return l.progresstion +} + +func (l *checkPeerReviewLoader) Load(erAnswerID int, activityID int) error { + var wg sync.WaitGroup + var err error + concurrent := Concurrent{Wg: &wg, Err: &err} + wg.Add(3) + go l.loadERAnswer(&concurrent, erAnswerID) + go l.loadERChoice(&concurrent, activityID) + go l.loadProgression(&concurrent, activityID) + wg.Wait() + return err +} + +func (l *checkPeerReviewLoader) loadERAnswer(concurrent *Concurrent, erAnswerID int) { + defer concurrent.Wg.Done() + result, err := l.learningRepo.GetPeerChoice(&erAnswerID) + if err != nil { + *concurrent.Err = err + } + l.erAnswer = &result +} + +func (l *checkPeerReviewLoader) loadERChoice(concurrent *Concurrent, activityID int) { + defer concurrent.Wg.Done() + result, err := l.learningRepo.GetERChoice(activityID) + if err != nil { + *concurrent.Err = err + } + l.erChoice = &result +} + +func (l *checkPeerReviewLoader) loadProgression(concurrent *Concurrent, activityID int) { + defer concurrent.Wg.Done() + var err error + l.progresstion, err = l.learningRepo.GetCorrectProgression(activityID) + if err != nil { + *concurrent.Err = err + } +} diff --git a/internal/services/loaders/content_roadmap.go b/internal/services/loaders/content_roadmap.go new file mode 100644 index 0000000..15af888 --- /dev/null +++ b/internal/services/loaders/content_roadmap.go @@ -0,0 +1,72 @@ +package loaders + +import ( + "database-camp/internal/models/entities/activity" + "database-camp/internal/models/entities/content" + "database-camp/internal/repositories" + "sync" +) + +type contentRoadmapLoader struct { + learningRepo repositories.LearningRepository + userRepo repositories.UserRepository + + content *content.Content + contentActivity []activity.Activity + learningProgression []content.LearningProgression +} + +func NewContentRoadmapLoader(learningRepo repositories.LearningRepository, userRepo repositories.UserRepository) *contentRoadmapLoader { + return &contentRoadmapLoader{learningRepo: learningRepo, userRepo: userRepo} +} + +func (l *contentRoadmapLoader) GetContent() *content.Content { + return l.content +} + +func (l *contentRoadmapLoader) GetContentActivity() activity.Activities { + return l.contentActivity +} + +func (l *contentRoadmapLoader) GetLearningProgression() []content.LearningProgression { + return l.learningProgression +} + +func (l *contentRoadmapLoader) Load(userID int, contentID int) error { + var wg sync.WaitGroup + var err error + concurrent := Concurrent{Wg: &wg, Err: &err} + wg.Add(3) + go l.loadLearningProgressionAsync(&concurrent, userID) + go l.loadContentActivityAsync(&concurrent, contentID) + go l.loadContentAsync(&concurrent, contentID) + wg.Wait() + return err +} + +func (l *contentRoadmapLoader) loadContentAsync(concurrent *Concurrent, contentID int) { + defer concurrent.Wg.Done() + result, err := l.learningRepo.GetContent(contentID) + if err != nil { + *concurrent.Err = err + } + l.content = result +} + +func (l *contentRoadmapLoader) loadContentActivityAsync(concurrent *Concurrent, contentID int) { + defer concurrent.Wg.Done() + result, err := l.learningRepo.GetContentActivity(contentID) + if err != nil { + *concurrent.Err = err + } + l.contentActivity = append(l.contentActivity, result...) +} + +func (l *contentRoadmapLoader) loadLearningProgressionAsync(concurrent *Concurrent, userID int) { + defer concurrent.Wg.Done() + result, err := l.userRepo.GetLearningProgression(userID) + if err != nil { + *concurrent.Err = err + } + l.learningProgression = append(l.learningProgression, result...) +} diff --git a/internal/services/loaders/exam.go b/internal/services/loaders/exam.go new file mode 100644 index 0000000..f4cdea7 --- /dev/null +++ b/internal/services/loaders/exam.go @@ -0,0 +1,72 @@ +package loaders + +import ( + "database-camp/internal/models/entities/exam" + "database-camp/internal/models/entities/user" + "database-camp/internal/repositories" + "sync" +) + +type examLoader struct { + examRepo repositories.ExamRepository + userRepo repositories.UserRepository + + correctedBadge []user.CorrectedBadge + exam *exam.Exam + examActivities []exam.ExamActivity +} + +func NewExamLoader(examRepo repositories.ExamRepository, userRepo repositories.UserRepository) *examLoader { + return &examLoader{examRepo: examRepo, userRepo: userRepo} +} + +func (l *examLoader) GetCorrectedBadge() user.CorrectedBadges { + return l.correctedBadge +} + +func (l *examLoader) GetExamActivities() exam.ExamActivities { + return l.examActivities +} + +func (l *examLoader) GetExam() *exam.Exam { + return l.exam +} + +func (l *examLoader) Load(userID int, examID int) error { + var wg sync.WaitGroup + var err error + concurrent := Concurrent{Wg: &wg, Err: &err} + wg.Add(3) + go l.loadCorrectedBadgeAsync(&concurrent, userID) + go l.loadExamActivityAsync(&concurrent, examID) + go l.loadExam(&concurrent, examID) + wg.Wait() + return err +} + +func (l *examLoader) loadCorrectedBadgeAsync(concurrent *Concurrent, userID int) { + defer concurrent.Wg.Done() + result, err := l.userRepo.GetCollectedBadge(userID) + if err != nil { + *concurrent.Err = err + } + l.correctedBadge = append(l.correctedBadge, result...) +} + +func (l *examLoader) loadExamActivityAsync(concurrent *Concurrent, examID int) { + defer concurrent.Wg.Done() + result, err := l.examRepo.GetExamActivities(examID) + if err != nil { + *concurrent.Err = err + } + l.examActivities = append(l.examActivities, result...) +} + +func (l *examLoader) loadExam(concurrent *Concurrent, examID int) { + defer concurrent.Wg.Done() + result, err := l.examRepo.GetExam(examID) + if err != nil { + *concurrent.Err = err + } + l.exam = result +} diff --git a/internal/services/loaders/exam_overview.go b/internal/services/loaders/exam_overview.go new file mode 100644 index 0000000..9d22f19 --- /dev/null +++ b/internal/services/loaders/exam_overview.go @@ -0,0 +1,72 @@ +package loaders + +import ( + "database-camp/internal/models/entities/exam" + "database-camp/internal/models/entities/user" + "database-camp/internal/repositories" + "sync" +) + +type examOverviewLoader struct { + examRepo repositories.ExamRepository + userRepo repositories.UserRepository + + correctedBadges []user.CorrectedBadge + exams []exam.Exam + examResults []exam.ExamResult +} + +func NewExamOverviewLoader(examRepo repositories.ExamRepository, userRepo repositories.UserRepository) *examOverviewLoader { + return &examOverviewLoader{examRepo: examRepo, userRepo: userRepo} +} + +func (l *examOverviewLoader) GetCorrectedBadges() user.CorrectedBadges { + return l.correctedBadges +} + +func (l *examOverviewLoader) GetExams() exam.Exams { + return l.exams +} + +func (l *examOverviewLoader) GetExamResults() exam.ExamResults { + return l.examResults +} + +func (l *examOverviewLoader) Load(userID int) error { + var wg sync.WaitGroup + var err error + concurrent := Concurrent{Wg: &wg, Err: &err} + wg.Add(3) + go l.loadCorrectedBadgeAsync(&concurrent, userID) + go l.loadExamAsync(&concurrent) + go l.loadExamResultAsync(&concurrent, userID) + wg.Wait() + return err +} + +func (l *examOverviewLoader) loadExamResultAsync(concurrent *Concurrent, userID int) { + defer concurrent.Wg.Done() + result, err := l.examRepo.GetExamResults(userID) + if err != nil { + *concurrent.Err = err + } + l.examResults = append(l.examResults, result...) +} + +func (l *examOverviewLoader) loadExamAsync(concurrent *Concurrent) { + defer concurrent.Wg.Done() + result, err := l.examRepo.GetExams() + if err != nil { + *concurrent.Err = err + } + l.exams = append(l.exams, result...) +} + +func (l *examOverviewLoader) loadCorrectedBadgeAsync(concurrent *Concurrent, userID int) { + defer concurrent.Wg.Done() + result, err := l.userRepo.GetCollectedBadge(userID) + if err != nil { + *concurrent.Err = err + } + l.correctedBadges = append(l.correctedBadges, result...) +} diff --git a/internal/services/loaders/exam_result.go b/internal/services/loaders/exam_result.go new file mode 100644 index 0000000..f099b8a --- /dev/null +++ b/internal/services/loaders/exam_result.go @@ -0,0 +1,59 @@ +package loaders + +import ( + "database-camp/internal/models/entities/exam" + "database-camp/internal/repositories" + "sync" +) + +type examResultLoader struct { + examRepo repositories.ExamRepository + + resultActivities []exam.ResultActivity + examResult *exam.ExamResult +} + +func NewExamResultLoader(examRepo repositories.ExamRepository) *examResultLoader { + return &examResultLoader{examRepo: examRepo} +} + +func (l examResultLoader) GetExamResult() *exam.ExamResult { + return l.examResult +} + +func (l examResultLoader) GetResultActivities() exam.ResultActivities { + return l.resultActivities +} + +func (l *examResultLoader) Load(userID int, examResultID int) error { + var wg sync.WaitGroup + var err error + concurrent := Concurrent{Wg: &wg, Err: &err} + wg.Add(2) + go l.loadExamResult(&concurrent, userID, examResultID) + go l.loadResultActivities(&concurrent, examResultID) + wg.Wait() + return err +} + +func (l *examResultLoader) loadExamResult(concurrent *Concurrent, userID int, examResultID int) { + defer concurrent.Wg.Done() + + result, err := l.examRepo.GetExamResult(userID, examResultID) + if err != nil { + *concurrent.Err = err + } + + l.examResult = result +} + +func (l *examResultLoader) loadResultActivities(concurrent *Concurrent, examResultID int) { + defer concurrent.Wg.Done() + + result, err := l.examRepo.GetActivitiesResult(examResultID) + if err != nil { + *concurrent.Err = err + } + + l.resultActivities = append(l.resultActivities, result...) +} diff --git a/internal/services/loaders/hint.go b/internal/services/loaders/hint.go new file mode 100644 index 0000000..e423b7e --- /dev/null +++ b/internal/services/loaders/hint.go @@ -0,0 +1,72 @@ +package loaders + +import ( + "database-camp/internal/models/entities/activity" + "database-camp/internal/models/entities/user" + "database-camp/internal/repositories" + "sync" +) + +type hintLoader struct { + learningRepo repositories.LearningRepository + userRepo repositories.UserRepository + + activityHints []activity.Hint + userHints []activity.UserHint + user *user.User +} + +func NewHintLoader(learningRepo repositories.LearningRepository, userRepo repositories.UserRepository) *hintLoader { + return &hintLoader{learningRepo: learningRepo, userRepo: userRepo} +} + +func (l *hintLoader) GetActivityHintsDB() activity.Hints { + return l.activityHints +} + +func (l *hintLoader) GetUserHintsDB() activity.UserHints { + return l.userHints +} + +func (l *hintLoader) GetUser() *user.User { + return l.user +} + +func (l *hintLoader) Load(userID int, activityID int) error { + var wg sync.WaitGroup + var err error + concurrent := Concurrent{Wg: &wg, Err: &err} + wg.Add(3) + go l.loadActivityHints(&concurrent, activityID) + go l.loadUserHintsAsync(&concurrent, userID, activityID) + go l.loadUser(&concurrent, userID) + wg.Wait() + return err +} + +func (l *hintLoader) loadUser(concurrent *Concurrent, userID int) { + defer concurrent.Wg.Done() + var err error + l.user, err = l.userRepo.GetUserByID(userID) + if err != nil { + *concurrent.Err = err + } +} + +func (l *hintLoader) loadUserHintsAsync(concurrent *Concurrent, userID int, activityID int) { + defer concurrent.Wg.Done() + result, e := l.userRepo.GetUserHint(userID, activityID) + if e != nil { + *concurrent.Err = e + } + l.userHints = append(l.userHints, result...) +} + +func (l *hintLoader) loadActivityHints(concurrent *Concurrent, activityID int) { + defer concurrent.Wg.Done() + result, e := l.learningRepo.GetActivityHints(activityID) + if e != nil { + *concurrent.Err = e + } + l.activityHints = append(l.activityHints, result...) +} diff --git a/internal/services/loaders/learning_overview.go b/internal/services/loaders/learning_overview.go new file mode 100644 index 0000000..cf9f31a --- /dev/null +++ b/internal/services/loaders/learning_overview.go @@ -0,0 +1,72 @@ +package loaders + +import ( + "database-camp/internal/models/entities/content" + "database-camp/internal/repositories" + "sync" +) + +type learningOverviewLoader struct { + learningRepo repositories.LearningRepository + userRepo repositories.UserRepository + + overview []content.Overview + learningProgression []content.LearningProgression + preExamID *int +} + +func NewLearningOverviewLoader(learningRepo repositories.LearningRepository, userRepo repositories.UserRepository) *learningOverviewLoader { + return &learningOverviewLoader{learningRepo: learningRepo, userRepo: userRepo} +} + +func (l *learningOverviewLoader) GetOverview() content.OverviewList { + return l.overview +} + +func (l *learningOverviewLoader) GetLearningProgression() content.LearningProgressionList { + return l.learningProgression +} + +func (l *learningOverviewLoader) GetPreExamID() *int { + return l.preExamID +} + +func (l *learningOverviewLoader) Load(userID int) error { + var wg sync.WaitGroup + var err error + concurrent := Concurrent{Wg: &wg, Err: &err} + wg.Add(3) + go l.loadOverviewAsync(&concurrent) + go l.loadPreExamIDAsync(&concurrent, userID) + go l.loadLearningProgressionAsync(&concurrent, userID) + wg.Wait() + return err +} + +func (l *learningOverviewLoader) loadOverviewAsync(concurrent *Concurrent) { + defer concurrent.Wg.Done() + result, err := l.learningRepo.GetOverview() + if err != nil { + *concurrent.Err = err + } + l.overview = append(l.overview, result...) +} + +func (l *learningOverviewLoader) loadLearningProgressionAsync(concurrent *Concurrent, id int) { + defer concurrent.Wg.Done() + result, err := l.userRepo.GetLearningProgression(id) + if err != nil { + *concurrent.Err = err + } + l.learningProgression = append(l.learningProgression, result...) +} + +func (l *learningOverviewLoader) loadPreExamIDAsync(concurrent *Concurrent, id int) { + defer concurrent.Wg.Done() + result, err := l.userRepo.GetPreExamID(id) + if err != nil { + *concurrent.Err = err + } + + l.preExamID = result +} diff --git a/internal/services/loaders/loaders.go b/internal/services/loaders/loaders.go new file mode 100644 index 0000000..a69f556 --- /dev/null +++ b/internal/services/loaders/loaders.go @@ -0,0 +1,9 @@ +package loaders + +import "sync" + +type Concurrent struct { + Wg *sync.WaitGroup + Err *error + Mutex *sync.Mutex +} diff --git a/internal/services/loaders/profile.go b/internal/services/loaders/profile.go new file mode 100644 index 0000000..0b3797f --- /dev/null +++ b/internal/services/loaders/profile.go @@ -0,0 +1,103 @@ +package loaders + +import ( + "database-camp/internal/models/entities/badge" + "database-camp/internal/models/entities/content" + "database-camp/internal/models/entities/user" + "database-camp/internal/repositories" + "sync" +) + +type profileLoader struct { + userRepo repositories.UserRepository + learningRepo repositories.LearningRepository + + spiderDataset user.SpiderDataset + contentGroups content.ContentGroups + profile *user.Profile + badges []badge.Badge + userBadges []badge.UserBadge +} + +func NewProfileLoader(learningRepo repositories.LearningRepository, userRepo repositories.UserRepository) *profileLoader { + return &profileLoader{learningRepo: learningRepo, userRepo: userRepo} +} + +func (l *profileLoader) GetSpiderDataset() user.SpiderDataset { + return l.spiderDataset +} + +func (l *profileLoader) GetContentGroups() content.ContentGroups { + return l.contentGroups +} + +func (l *profileLoader) GetProfile() *user.Profile { + return l.profile +} + +func (l *profileLoader) GetUserBadges() []badge.UserBadge { + return l.userBadges +} + +func (l *profileLoader) GetBadges() []badge.Badge { + return l.badges +} + +func (l *profileLoader) Load(userID int) error { + var wg sync.WaitGroup + var err error + concurrent := Concurrent{Wg: &wg, Err: &err} + wg.Add(5) + go l.loadSpiderDatasetAsync(&concurrent, userID) + go l.loadContentGroupsAsync(&concurrent) + go l.loadProfileAsync(&concurrent, userID) + go l.loadBadgesAsync(&concurrent) + go l.loadUserBadgesAsync(&concurrent, userID) + wg.Wait() + return err +} + +func (l *profileLoader) loadSpiderDatasetAsync(concurrent *Concurrent, id int) { + defer concurrent.Wg.Done() + var err error + l.spiderDataset, err = l.userRepo.GetSpiderDataset(id) + if err != nil { + *concurrent.Err = err + } +} + +func (l *profileLoader) loadContentGroupsAsync(concurrent *Concurrent) { + defer concurrent.Wg.Done() + var err error + l.contentGroups, err = l.learningRepo.GetContentGroups() + if err != nil { + *concurrent.Err = err + } +} + +func (l *profileLoader) loadProfileAsync(concurrent *Concurrent, id int) { + defer concurrent.Wg.Done() + var err error + l.profile, err = l.userRepo.GetProfile(id) + if err != nil { + *concurrent.Err = err + } +} + +func (l *profileLoader) loadBadgesAsync(concurrent *Concurrent) { + defer concurrent.Wg.Done() + var err error + l.badges, err = l.userRepo.GetAllBadge() + if err != nil { + *concurrent.Err = err + } +} + +func (l *profileLoader) loadUserBadgesAsync(concurrent *Concurrent, id int) { + defer concurrent.Wg.Done() + var err error + l.userBadges, err = l.userRepo.GetUserBadge(id) + if err != nil { + *concurrent.Err = err + } +} diff --git a/internal/services/loaders/recommend.go b/internal/services/loaders/recommend.go new file mode 100644 index 0000000..c2f8c0a --- /dev/null +++ b/internal/services/loaders/recommend.go @@ -0,0 +1,57 @@ +package loaders + +import ( + "database-camp/internal/models/entities/content" + "database-camp/internal/models/entities/user" + "database-camp/internal/repositories" + "sync" +) + +type recommendLoader struct { + userRepo repositories.UserRepository + learningRepo repositories.LearningRepository + + preTestResults user.PreTestResults + contentGroups content.ContentGroups +} + +func NewRecommendLoader(learningRepo repositories.LearningRepository, userRepo repositories.UserRepository) *recommendLoader { + return &recommendLoader{learningRepo: learningRepo, userRepo: userRepo} +} + +func (l *recommendLoader) GetPreTestResults() user.PreTestResults { + return l.preTestResults +} + +func (l *recommendLoader) GetContentGroups() content.ContentGroups { + return l.contentGroups +} + +func (l *recommendLoader) Load(userID int) error { + var wg sync.WaitGroup + var err error + concurrent := Concurrent{Wg: &wg, Err: &err} + wg.Add(2) + go l.loadPreTestResultsAsync(&concurrent, userID) + go l.loadContentGroupsAsync(&concurrent) + wg.Wait() + return err +} + +func (l *recommendLoader) loadPreTestResultsAsync(concurrent *Concurrent, id int) { + defer concurrent.Wg.Done() + var err error + l.preTestResults, err = l.userRepo.GetPreTestResults(id) + if err != nil { + *concurrent.Err = err + } +} + +func (l *recommendLoader) loadContentGroupsAsync(concurrent *Concurrent) { + defer concurrent.Wg.Done() + var err error + l.contentGroups, err = l.learningRepo.GetContentGroups() + if err != nil { + *concurrent.Err = err + } +} diff --git a/internal/services/user.go b/internal/services/user.go new file mode 100644 index 0000000..6505bc2 --- /dev/null +++ b/internal/services/user.go @@ -0,0 +1,170 @@ +package services + +import ( + "database-camp/internal/errs" + "database-camp/internal/logs" + "database-camp/internal/middleware/jwt" + "database-camp/internal/models/entities/badge" + "database-camp/internal/models/entities/user" + "database-camp/internal/models/request" + "database-camp/internal/models/response" + "database-camp/internal/repositories" + "database-camp/internal/services/loaders" + "database-camp/internal/utils" + "time" +) + +type UserService interface { + Register(request request.UserRequest) (*response.UserResponse, error) + Login(request request.UserRequest) (*response.UserResponse, error) + GetProfile(userID int) (*response.GetProfileResponse, error) + EditProfile(userID int, request request.UserRequest) (*response.EditProfileResponse, error) + GetRanking(id int) (*response.RankingResponse, error) +} + +type userService struct { + userRepo repositories.UserRepository + learningRepo repositories.LearningRepository +} + +func NewUserService(userRepo repositories.UserRepository, learningRepo repositories.LearningRepository) *userService { + return &userService{userRepo: userRepo, learningRepo: learningRepo} +} + +func (s userService) Register(request request.UserRequest) (*response.UserResponse, error) { + user, err := s.userRepo.InsertUser(user.User{ + Name: request.Name, + Email: request.Email, + Password: utils.HashAndSalt(request.Password), + Point: 0, + CreatedTimestamp: time.Now().Local(), + UpdatedTimestamp: time.Now().Local(), + }) + + if err != nil { + logs.GetInstance().Error(err) + + if utils.IsSqlDuplicateError(err) { + return nil, errs.ErrEmailAlreadyExists + } else { + return nil, errs.ErrInsertError + } + } + + jwt := jwt.New(s.userRepo) + + token, err := jwt.Sign(user.ID) + if err != nil { + logs.GetInstance().Error(err) + return nil, errs.ErrInternalServerError + } + + response := response.UserResponse{ + ID: user.ID, + Name: user.Name, + Email: user.Email, + Point: user.Point, + AccessToken: token, + CreatedTimestamp: user.CreatedTimestamp, + UpdatedTimestamp: user.UpdatedTimestamp, + } + + return &response, nil +} + +func (s userService) Login(request request.UserRequest) (*response.UserResponse, error) { + user, err := s.userRepo.GetUserByEmail(request.Email) + + correctPassword := utils.ComparePasswords(user.Password, request.Password) + + if err != nil || !correctPassword { + logs.GetInstance().Error(err) + return nil, errs.ErrEmailOrPasswordNotCorrect + } + + jwt := jwt.New(s.userRepo) + + token, err := jwt.Sign(user.ID) + if err != nil { + logs.GetInstance().Error(err) + return nil, errs.ErrInternalServerError + } + + response := response.UserResponse{ + ID: user.ID, + Name: user.Name, + Email: user.Email, + Point: user.Point, + AccessToken: token, + CreatedTimestamp: user.CreatedTimestamp, + UpdatedTimestamp: user.UpdatedTimestamp, + } + + return &response, nil +} + +func (s userService) GetProfile(id int) (*response.GetProfileResponse, error) { + + loader := loaders.NewProfileLoader(s.learningRepo, s.userRepo) + + err := loader.Load(id) + if err != nil { + logs.GetInstance().Error(err) + return nil, errs.ErrContentNotFound + } + + spiderDataset := loader.GetSpiderDataset() + contentGroups := loader.GetContentGroups() + badges := loader.GetBadges() + profile := loader.GetProfile() + userBadges := loader.GetUserBadges() + + spiderDataset.FillContentGroups(contentGroups) + + _badges := badge.NewBadges(badges, userBadges) + + response := response.GetProfileResponse{ + ID: profile.ID, + Name: profile.Name, + Point: profile.Point, + ActivityCount: profile.ActivityCount, + Badges: _badges, + SpiderDataset: spiderDataset, + CreatedTimestamp: profile.CreatedTimestamp, + } + + return &response, nil +} + +func (s userService) EditProfile(userID int, request request.UserRequest) (*response.EditProfileResponse, error) { + err := s.userRepo.UpdatesByID(userID, map[string]interface{}{"name": request.Name}) + if err != nil { + logs.GetInstance().Error(err) + return nil, errs.ErrUpdateError + } + + response := response.EditProfileResponse{UpdatedName: request.Name} + + return &response, nil +} + +func (s userService) GetRanking(userID int) (*response.RankingResponse, error) { + userRankingDB, err := s.userRepo.GetPointRanking(userID) + if err != nil || userRankingDB == nil { + logs.GetInstance().Error(err) + return nil, errs.ErrUserNotFound + } + + leaderBoardDB, err := s.userRepo.GetRankingLeaderBoard() + if err != nil || leaderBoardDB == nil { + logs.GetInstance().Error(err) + return nil, errs.ErrLeaderBoardNotFound + } + + response := response.RankingResponse{ + UserRanking: *userRankingDB, + LeaderBoard: leaderBoardDB, + } + + return &response, nil +} diff --git a/internal/utils/helper.go b/internal/utils/helper.go new file mode 100644 index 0000000..a6d6176 --- /dev/null +++ b/internal/utils/helper.go @@ -0,0 +1,68 @@ +package utils + +import ( + "fmt" + "math/rand" + "net/mail" + "reflect" + + m "github.com/go-sql-driver/mysql" + "golang.org/x/crypto/bcrypt" +) + +func HashAndSalt(pwd string) string { + bytePassword := []byte(pwd) + hash, err := bcrypt.GenerateFromPassword(bytePassword, 4) + if err != nil { + panic(err.Error()) + } + return string(hash) +} + +func ComparePasswords(hashedPwd string, plainPwd string) bool { + bytePlainPassword := []byte(plainPwd) + byteHash := []byte(hashedPwd) + err := bcrypt.CompareHashAndPassword(byteHash, bytePlainPassword) + return err == nil +} + +func IsEmailValid(email string) bool { + _, err := mail.ParseAddress(email) + return err == nil +} + +func IsSqlDuplicateError(err error) bool { + sqlError, ok := err.(*m.MySQLError) + return ok && sqlError.Number == 1062 +} + +func GetKeyList(value map[string]interface{}) (result []string) { + for k := range value { + result = append(result, k) + } + return +} + +func Shuffle(slice interface{}) { + rv := reflect.ValueOf(slice) + swap := reflect.Swapper(slice) + length := rv.Len() + for i := length - 1; i > 0; i-- { + j := rand.Intn(i + 1) + swap(i, j) + } +} + +func ToStrings(strs []interface{}) string { + result := "" + + if len(strs) == 0 { + return result + } + + for _, str := range strs { + result += fmt.Sprintf("'%v',", str) + } + + return result[:len(result)-1] +} diff --git a/internal/utils/type_conversion.go b/internal/utils/type_conversion.go new file mode 100644 index 0000000..38afb93 --- /dev/null +++ b/internal/utils/type_conversion.go @@ -0,0 +1,60 @@ +package utils + +import ( + "encoding/json" + "fmt" + "strconv" + "time" +) + +func StructToMap(obj interface{}) (newMap map[string]interface{}, err error) { + data, err := json.Marshal(obj) + + if err != nil { + return + } + + err = json.Unmarshal(data, &newMap) + return +} + +func StructToStruct(x1 interface{}, x2 interface{}) error { + jsonBody, err := json.Marshal(x1) + if err != nil { + return err + } + + if err := json.Unmarshal(jsonBody, x2); err != nil { + return err + } + + return nil +} + +func ParseDuration(item interface{}) time.Duration { + intType := ParseInt(item) + return time.Duration(intType) +} + +func ParseInt(item interface{}) int { + _item := item + switch item.(type) { + case int: + return _item.(int) + case int64: + return int(_item.(int64)) + case int32: + return int(_item.(int32)) + case float64: + return int(_item.(float64)) + case string: + _int, _ := strconv.Atoi(_item.(string)) + return _int + default: + return 0 + } +} + +func ParseString(item interface{}) string { + return fmt.Sprintf("%v", item) +} diff --git a/middleware/interface.go b/middleware/interface.go deleted file mode 100644 index f5abf0f..0000000 --- a/middleware/interface.go +++ /dev/null @@ -1,33 +0,0 @@ -package middleware - -// interface.go -/** - * This file used to be a interface of middleware - */ - -import "github.com/gofiber/fiber/v2" - -/** - * Interface to show function in JWT middleware that others can use - */ -type IJwt interface { - - /** - * Sign user for verification - * - * @param id User ID to sign - * - * @return signing token - * @return the error of signing - */ - JwtSign(id int) (string, error) - - /** - * Verify request by token - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ - JwtVerify(c *fiber.Ctx) error -} diff --git a/middleware/middleware.jwt.go b/middleware/middleware.jwt.go deleted file mode 100644 index f4cf61e..0000000 --- a/middleware/middleware.jwt.go +++ /dev/null @@ -1,203 +0,0 @@ -package middleware - -// middleware.jwt.go -/** - * This file is a part of middleware, used to verificate user - */ - -import ( - "DatabaseCamp/errs" - "DatabaseCamp/logs" - "DatabaseCamp/repositories" - "DatabaseCamp/utils" - "fmt" - "os" - "strings" - "time" - - "github.com/gofiber/fiber/v2" - "github.com/golang-jwt/jwt" - uuid "github.com/satori/go.uuid" -) - -/** - * This class verificate user - */ -type jwtMiddleware struct { - Repo repositories.IUserRepository // User repository for getting user data -} - -/** - * Constructor creates a new jwtMiddleware instance - * - * @param repo User repository for getting user data - * - * @return instance of jwtMiddleware - */ -func NewJwtMiddleware(repo repositories.IUserRepository) jwtMiddleware { - return jwtMiddleware{Repo: repo} -} - -/** - * Sign user for verification - * - * @param id User ID to sign - * - * @return signing token - * @return the error of signing - */ -func (j jwtMiddleware) JwtSign(id int) (string, error) { - atClaims := jwt.MapClaims{} - atClaims["id"] = id - atClaims["secret"] = uuid.NewV4() - at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims) - - token, err := at.SignedString([]byte(os.Getenv("JWT_SECRET"))) - if err != nil { - logs.New().Error(err) - return "", errs.ErrInternalServerError - } - - err = j.updateToken(id, token) - if err != nil { - logs.New().Error(err) - return "", errs.ErrInternalServerError - } - - return token, nil -} - -/** - * Verify request by token - * - * @param c Context of the web framework - * - * @return the error of getting exam - */ -func (j jwtMiddleware) JwtVerify(c *fiber.Ctx) error { - handleUtil := utils.NewHandle() - bearer, err := j.jwtFromHeader(c) - if err != nil { - return handleUtil.HandleError(c, err) - } - - token, err := jwt.Parse(bearer, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - EnMessage := fmt.Sprintf("unexpected signing method: %v", token.Header["alg"]) - ThMessage := fmt.Sprintf("วิธีการลงนามที่ไม่คาดคิด: %v", token.Header["alg"]) - return nil, handleUtil.HandleError(c, errs.NewForbiddenError(ThMessage, EnMessage)) - } - return []byte(os.Getenv("JWT_SECRET")), nil - }) - if err != nil { - logs.New().Error(err) - return handleUtil.HandleError(c, errs.NewForbiddenError("โทเค็นไม่ถูกต้อง", "Token Invalid")) - } - - claims, err := j.getClaims(token) - if err != nil { - return handleUtil.HandleError(c, err) - } - - id := utils.NewType().ParseInt(claims["id"]) - - if !j.validUser(bearer, id) { - return handleUtil.HandleError(c, errs.NewForbiddenError("โทเค็นไม่ถูกต้อง", "Token Invalid")) - } - - err = j.updateToken(id, bearer) - if err != nil { - logs.New().Error(err) - return handleUtil.HandleError(c, errs.NewInternalServerError("เกิดข้อผิดพลาด", "Internal Server Error")) - } - - j.setClaims(c, claims) - return c.Next() -} - -/** - * Update user token into the database - * - * @param id User ID to update token - * @param token token to be updated - * - * @return the error of updating token - */ -func (j jwtMiddleware) updateToken(id int, token string) error { - tokenExpireHour := time.Hour * utils.NewType().ParseDuration(os.Getenv("TOKEN_EXPIRE_HOUR")) - expiredTokenTimestamp := time.Now().Local().Add(tokenExpireHour) - err := j.Repo.UpdatesByID(id, map[string]interface{}{ - "access_token": token, - "expired_token_timestamp": expiredTokenTimestamp, - }) - return err -} - -/** - * Get claims of the token - * - * @param token JWT token - * - * @return claims of the token - * @return the error of getting claims - */ -func (j jwtMiddleware) getClaims(token *jwt.Token) (jwt.MapClaims, error) { - claims, ok := token.Claims.(jwt.MapClaims) - if !ok { - return claims, errs.NewForbiddenError("โทเค็นไม่ถูกต้อง", "Token Invalid") - } else { - return claims, nil - } -} - -/** - * Set claims to the header of the request - * - * @param c Context of the web framework - * @param claims claims of the token to set - */ -func (j jwtMiddleware) setClaims(c *fiber.Ctx, claims jwt.MapClaims) { - for k, v := range claims { - if k != "secret" { - c.Locals(k, utils.NewType().ParseString(v)) - } - } -} - -/** - * Check user token - * - * @param token Token of the request - * @param id User ID to check token - * - * @return true if user token is valid, false otherwise - */ -func (j jwtMiddleware) validUser(token string, id int) bool { - userDB, err := j.Repo.GetUserByID(id) - if err != nil || userDB == nil { - return false - } - - if userDB.AccessToken != token || userDB.ExpiredTokenTimestamp.Before(time.Now().Local()) { - return false - } - - return true -} - -/** - * Get token from the header of the request - * - * @param c Context of the web framework - * - * @return token from the header - * @return the error of the getting token - */ -func (j jwtMiddleware) jwtFromHeader(c *fiber.Ctx) (string, error) { - auth := c.Get("Authorization") - l := len("Bearer") - if len(auth) > l+1 && strings.EqualFold(auth[:l], "Bearer") { - return auth[l+1:], nil - } - return "", errs.NewBadRequestError("ไม่พบ JWT Token ในส่วนหัวของคำร้องขอ", "JWT Token Not found") -} diff --git a/models/.DS_Store b/models/.DS_Store deleted file mode 100644 index 872181b..0000000 Binary files a/models/.DS_Store and /dev/null differ diff --git a/models/entities/entity.activity.go b/models/entities/entity.activity.go deleted file mode 100644 index 8bdc5ec..0000000 --- a/models/entities/entity.activity.go +++ /dev/null @@ -1,416 +0,0 @@ -package entities - -// entity.activity.go -/** - * This file is a part of models, used to collect model for entities of activity - */ - -import ( - "DatabaseCamp/errs" - "DatabaseCamp/models/request" - "DatabaseCamp/models/storages" - "DatabaseCamp/utils" -) - -// Model of hint roadmap result for activity -type HintRoadMap struct { - Level int `json:"level"` - ReducePoint int `json:"reduce_point"` -} - -// Model of activity hint result for Exam activity -type ActivityHint struct { - TotalHint int `json:"total_hint"` - UsedHints []storages.HintDB `json:"used_hints"` - HintRoadMap []HintRoadMap `json:"hint_roadmap"` -} - -// Model of activity detail result for activity -type ActivityDetail struct { - ID int `json:"activity_id"` - TypeID int `json:"activity_type_id"` - ContentID *int `json:"content_id"` - Order int `json:"activity_order"` - Story string `json:"story"` - Point int `json:"point"` - Question string `json:"question"` -} - -/** - * This class manage activity model - */ -type Activity struct { - info ActivityDetail - propositionChoices interface{} - choices interface{} - hint *ActivityHint -} - -/** - * Getter for getting hint - * - * @return hint - */ -func (a *Activity) GetHint() *ActivityHint { - return a.hint -} - -/** - * Getter for getting proposition choices of the activity - * - * @return propositionChoices of the activity - */ -func (a *Activity) GetPropositionChoices() interface{} { - return a.propositionChoices -} - -/** - * Getter for getting information of the activity - * - * @return information of the activity - */ -func (a *Activity) GetInfo() ActivityDetail { - return a.info -} - -/** - * Setter for set activity - * - * @param activityDB to set activity data - */ -func (a *Activity) SetActivity(activityDB storages.ActivityDB) { - utils.NewType().StructToStruct(activityDB, &a.info) -} - -/** - * Setter for set choice by choiceDB - * - * @param choiceDB to set choice - * - * @return the error of setting - */ -func (a *Activity) SetChoicesByChoiceDB(choiceDB interface{}) error { - a.choices = choiceDB - if a.info.TypeID == 1 { - propositionChoices, choiceOK := choiceDB.([]storages.MatchingChoiceDB) - if !choiceOK { - return errs.NewInternalServerError("รูปแบบของตัวเลือกไม่ถูกต้อง", "Invalid Choice") - } - a.propositionChoices = a.PrepareMatchingChoice(propositionChoices) - } else if a.info.TypeID == 2 { - propositionChoices, choiceOK := choiceDB.([]storages.MultipleChoiceDB) - if !choiceOK { - return errs.NewInternalServerError("รูปแบบของตัวเลือกไม่ถูกต้อง", "Invalid Choice") - } - a.propositionChoices = a.PrepareMultipleChoice(propositionChoices) - } else if a.info.TypeID == 3 { - propositionChoices, choiceOK := choiceDB.([]storages.CompletionChoiceDB) - if !choiceOK { - return errs.NewInternalServerError("รูปแบบของตัวเลือกไม่ถูกต้อง", "Invalid Choice") - } - a.propositionChoices = a.PrepareCompletionChoice(propositionChoices) - } else { - a.propositionChoices = nil - } - return nil -} - -/** - * Setter for set hint - * - * @param activityHints Activity hints model from database to set hint - * @param userHintsDB User hints model from database to set hint - */ -func (a *Activity) SetHint(activityHints []storages.HintDB, userHintsDB []storages.UserHintDB) { - a.hint = &ActivityHint{ - TotalHint: len(activityHints), - } - for _, hint := range activityHints { - if a.isUsedHint(userHintsDB, hint.ID) { - a.hint.UsedHints = append(a.hint.UsedHints, hint) - } - a.hint.HintRoadMap = append(a.hint.HintRoadMap, HintRoadMap{ - Level: hint.Level, - ReducePoint: hint.PointReduce, - }) - } -} - -/** - * Check if hint is used - * - * @param userHintsDB Hints of the user to check - * @param hintID Hint ID to check - * - * @return true if hint is used, false if not - */ -func (a *Activity) isUsedHint(userHintsDB []storages.UserHintDB, hintID int) bool { - for _, userHint := range userHintsDB { - if userHint.HintID == hintID { - return true - } - } - return false -} - -/** - * Prepare multipleChoice - * - * @param multipleChoice Multiple choice to prepare - * - * @return prepared multiple choice - */ -func (a *Activity) PrepareMultipleChoice(multipleChoice []storages.MultipleChoiceDB) interface{} { - preparedChoices := make([]map[string]interface{}, 0) - utils.NewHelper().Shuffle(multipleChoice) - for _, v := range multipleChoice { - preparedChoice, _ := utils.NewType().StructToMap(v) - delete(preparedChoice, "is_correct") - preparedChoices = append(preparedChoices, preparedChoice) - } - - return preparedChoices -} - -/** - * Prepare matchingchoice - * - * @param matchingChoice Matching choice to prepare - * - * @return prepared matching choice - */ -func (a *Activity) PrepareMatchingChoice(matchingChoice []storages.MatchingChoiceDB) interface{} { - pairItem1List := make([]interface{}, 0) - pairItem2List := make([]interface{}, 0) - for _, v := range matchingChoice { - pairItem1List = append(pairItem1List, v.PairItem1) - pairItem2List = append(pairItem2List, v.PairItem2) - } - utils.NewHelper().Shuffle(pairItem1List) - utils.NewHelper().Shuffle(pairItem2List) - prepared := map[string]interface{}{ - "items_left": pairItem1List, - "items_right": pairItem2List, - } - return prepared -} - -/** - * Prepare completionchoice - * - * @param completionchoice Completion choice to prepare - * - * @return prepared completion choice - */ -func (a *Activity) PrepareCompletionChoice(completionChoice []storages.CompletionChoiceDB) interface{} { - contents := make([]interface{}, 0) - questions := make([]interface{}, 0) - for _, v := range completionChoice { - contents = append(contents, v.Content) - questions = append(questions, map[string]interface{}{ - "id": v.ID, - "first": v.QuestionFirst, - "last": v.QuestionLast, - }) - } - utils.NewHelper().Shuffle(contents) - utils.NewHelper().Shuffle(questions) - prepared := map[string]interface{}{ - "contents": contents, - "questions": questions, - } - return prepared -} - -/** - * Check answer for matching choice - * - * @param choices Choices of activity - * @param answer Answer of matching choice - * - * @return true if input answer is correct, false if not - */ -func (a *Activity) IsMatchingCorrect(choices []storages.MatchingChoiceDB, answer []request.PairItemRequest) bool { - Item1Item2Map := map[string]string{} - for _, correct := range choices { - Item1Item2Map[correct.PairItem1] = correct.PairItem2 - } - for _, answer := range answer { - if Item1Item2Map[*answer.Item1] != *answer.Item2 && Item1Item2Map[*answer.Item2] != *answer.Item1 { - return false - } - } - return true -} - -/** - * Check answer for completion choice - * - * @param choices Choices of activity - * @param answer Answer of completion choice - * - * @return true if input answer is correct, false if not - */ -func (a *Activity) IsCompletionCorrect(choices []storages.CompletionChoiceDB, answer []request.PairContentRequest) bool { - for _, correct := range choices { - for _, answer := range answer { - if (correct.ID == *answer.ID) && (correct.Content != *answer.Content) { - return false - } - } - } - return true -} - -/** - * Check answer for multiple choice - * - * @param choices Choices of activity - * @param answer Answer of multiple choice - * - * @return true if input answer is correct, false if not - */ -func (a *Activity) IsMultipleCorrect(choices []storages.MultipleChoiceDB, answer int) bool { - for _, v := range choices { - if v.IsCorrect && v.ID == answer { - return true - } - } - return false -} - -/** - * Convert type of choice to pair item - * - * @param raw Item for convert to pair item - * - * @return choice that converted to pair item - * @return the error converting - */ -func (a *Activity) convertToPairItem(raw interface{}) ([]request.PairItemRequest, error) { - result := make([]request.PairItemRequest, 0) - list, ok := raw.([]interface{}) - if !ok { - return nil, errs.NewBadRequestError("รูปแบบของคำตอบไม่ถูกต้อง", "Invalid Answer Format") - } - for _, v := range list { - temp := request.PairItemRequest{} - utils.NewType().StructToStruct(v, &temp) - err := temp.Validate() - if err != nil { - return nil, err - } - result = append(result, temp) - } - return result, nil -} - -/** - * Check answer for matching choice - * - * @param answer Answer of activity - * - * @return true if input answer is correct, false if not - * @return the error of checking matching choice - */ -func (a *Activity) checkMatchingCorrect(answer interface{}) (bool, error) { - matchingChoices, choiceOK := a.choices.([]storages.MatchingChoiceDB) - _answer, answerOK := answer.([]request.PairItemRequest) - if !answerOK { - var err error - _answer, err = a.convertToPairItem(answer) - if err != nil { - return false, err - } - } - if len(matchingChoices) != len(_answer) || !choiceOK { - return false, errs.NewBadRequestError("รูปแบบของคำตอบไม่ถูกต้อง", "Invalid Answer Format") - } - return a.IsMatchingCorrect(matchingChoices, _answer), nil -} - -/** - * Check answer for multiple choice - * - * @param answer answer of activity - * - * @return true if input answer is correct, false if not - * @return the error of checking multiple choice - */ -func (a *Activity) checkMultipleCorrect(answer interface{}) (bool, error) { - multipleChoices, choiceOK := a.choices.([]storages.MultipleChoiceDB) - if !choiceOK { - return false, errs.NewBadRequestError("รูปแบบของคำตอบไม่ถูกต้อง", "Invalid Answer Format") - } - return a.IsMultipleCorrect(multipleChoices, utils.NewType().ParseInt(answer)), nil -} - -/** - * Convert type of content to pair content - * - * @param raw item for convert to pair content request - * - * @return choice that converted to pair content - * @return the error of converting - */ -func (a *Activity) convertToPairContent(raw interface{}) ([]request.PairContentRequest, error) { - result := make([]request.PairContentRequest, 0) - list, ok := raw.([]interface{}) - if !ok { - return nil, errs.NewBadRequestError("รูปแบบของคำตอบไม่ถูกต้อง", "Invalid Answer Format") - } - for _, v := range list { - temp := request.PairContentRequest{} - utils.NewType().StructToStruct(v, &temp) - err := temp.Validate() - if err != nil { - return nil, err - } - result = append(result, temp) - } - return result, nil -} - -/** - * Check answer for completion choice - * - * @param answer Answer of activity - * - * @return true if input answer is correct, false if not - * @return the error of checking completion choice - */ -func (a *Activity) checkCompletionCorrect(answer interface{}) (bool, error) { - completionChoices, choiceOK := a.choices.([]storages.CompletionChoiceDB) - _answer, answerOK := answer.([]request.PairContentRequest) - if !answerOK || !choiceOK { - var err error - _answer, err = a.convertToPairContent(answer) - if err != nil { - return false, err - } - } - if len(completionChoices) != len(_answer) || !choiceOK { - return false, errs.NewBadRequestError("รูปแบบของคำตอบไม่ถูกต้อง", "Invalid Answer Format") - } - return a.IsCompletionCorrect(completionChoices, _answer), nil -} - -/** - * Check if answer is correct - * - * @param answer Answer of activity - * - * @return true if the answer is correct, false if not - * @return the error of checking answer - */ -func (a *Activity) IsAnswerCorrect(answer interface{}) (bool, error) { - if a.info.TypeID == 1 { - return a.checkMatchingCorrect(answer) - } else if a.info.TypeID == 2 { - return a.checkMultipleCorrect(answer) - } else if a.info.TypeID == 3 { - return a.checkCompletionCorrect(answer) - } else { - return false, errs.NewBadRequestError("ประเภทของกิจกรรมไม่ถูกต้อง", "Invalid Activity Type") - } -} diff --git a/models/entities/entity.exam.go b/models/entities/entity.exam.go deleted file mode 100644 index c434796..0000000 --- a/models/entities/entity.exam.go +++ /dev/null @@ -1,365 +0,0 @@ -package entities - -// entity.exam.go -/** - * This file is a part of models, used to collect model for entities of exam - */ - -import ( - "DatabaseCamp/models/general" - "DatabaseCamp/models/request" - "DatabaseCamp/models/storages" - "DatabaseCamp/utils" - "sync" - "time" -) - -// Exam type -var ExamType = struct { - Pretest string - MiniExam string - Posttest string -}{ - "PRE", - "MINI", - "POST", -} - -// Model of exam activity result for Exam entity -type ExamActivityResult struct { - ActivityID int - Score int -} - -// Model of exam result overview for Exam entity -type ExamResultOverview struct { - ExamResultID int `json:"exam_result_id"` - TotalScore int `json:"score"` - IsPassed bool `json:"is_passed"` - ActivitiesResult []ExamActivityResult `json:"activities_result,omitempty"` - CreatedTimestamp time.Time `json:"created_timestamp"` -} - -// Model of exam info for Exam entity -type ExamInfo struct { - ID int `json:"exam_id"` - Type string `json:"exam_type"` - Instruction string `json:"instruction"` - CreatedTimestamp time.Time `json:"created_timestamp"` - ContentGroupID int `json:"content_group_id"` - ContentGroupName string `json:"content_group_name"` - BadgeID int `json:"badge_id"` -} - -/** - * This class manage exam model - */ -type Exam struct { - info ExamInfo - activities []Activity - result *ExamResultOverview -} - -/** - * Getter for getting exam result - * - * @return exam result - */ -func (e *Exam) GetResult() *ExamResultOverview { - return e.result -} - -/** - * Getter for getting activities of the exam - * - * @return activities of the exam - */ -func (e *Exam) GetActivities() []Activity { - return e.activities -} - -/** - * Getter for getting information of the exam - * - * @return information of the exam - */ -func (e *Exam) GetInfo() ExamInfo { - return e.info -} - -/** - * Setter for set exam result ID - * - * @param id Exam result id to set - */ -func (e *Exam) SetResultID(id int) { - e.result.ExamResultID = id -} - -/** - * To exam result activities database model - * - * @return exam result activities database model - */ -func (e *Exam) ToExamResultActivitiesDB() []storages.ExamResultActivityDB { - resultActivities := make([]storages.ExamResultActivityDB, 0) - for _, resultActivity := range e.result.ActivitiesResult { - resultActivities = append(resultActivities, storages.ExamResultActivityDB{ - ActivityID: resultActivity.ActivityID, - Score: resultActivity.Score, - }) - } - return resultActivities -} - -/** - * To exam result database model - * - * @param userID User id to set exam result model - * - * @return exam result database model - */ -func (e *Exam) ToExamResultDB(userID int) *storages.ExamResultDB { - return &storages.ExamResultDB{ - ExamID: e.info.ID, - UserID: userID, - Score: e.result.TotalScore, - IsPassed: e.result.IsPassed, - CreatedTimestamp: time.Now().Local(), - } -} - -/** - * Prepare exam result - * - * @param examResultDB Exam result to prepare - */ -func (e *Exam) PrepareResult(examResultDB storages.ExamResultDB) { - e.result = &ExamResultOverview{ - ExamResultID: examResultDB.ID, - TotalScore: examResultDB.Score, - IsPassed: examResultDB.IsPassed, - CreatedTimestamp: examResultDB.CreatedTimestamp, - } -} - -/** - * Prepare exam - * - * @param examActivitiesDB Exam activities to prepare - */ -func (e *Exam) Prepare(examActivitiesDB []storages.ExamActivityDB) { - activityChoiceDBMap := map[int]interface{}{} - examActivityDBMap := map[int]storages.ActivityDB{} - for _, examActivityDB := range examActivitiesDB { - activity := storages.ActivityDB{} - utils.NewType().StructToStruct(examActivityDB, &e.info) - if examActivityDB.ExamType == string(ExamType.Posttest) { - e.info.BadgeID = 3 - } - utils.NewType().StructToStruct(examActivityDB, &activity) - examActivityDBMap[examActivityDB.ActivityID] = activity - if activityChoiceDBMap[examActivityDB.ActivityID] == nil { - e.initialActivityChoiceMap(examActivityDB.ActivityID, examActivityDB.ActivityTypeID, activityChoiceDBMap) - } - e.appendExamActivityChoice(examActivityDB, activityChoiceDBMap) - } - - for _, examActivityDB := range examActivityDBMap { - activity := Activity{} - activity.SetActivity(examActivityDB) - activity.SetChoicesByChoiceDB(activityChoiceDBMap[examActivityDB.ID]) - e.activities = append(e.activities, activity) - } -} - -/** - * Initail Map of activity choice - * - * @param activityID Activity ID to initialize - * @param typeID Activity type ID to initialize - * @param activityChoiceMap Map of activity choice - */ -func (e *Exam) initialActivityChoiceMap(activityID int, typeID int, activityChoiceMap map[int]interface{}) { - if typeID == 1 { - temp := make([]storages.MatchingChoiceDB, 0) - activityChoiceMap[activityID] = temp - } else if typeID == 2 { - temp := make([]storages.MultipleChoiceDB, 0) - activityChoiceMap[activityID] = temp - } else if typeID == 3 { - temp := make([]storages.CompletionChoiceDB, 0) - activityChoiceMap[activityID] = temp - } else { - temp := make([]interface{}, 0) - activityChoiceMap[activityID] = temp - } -} - -/** - * Append exam activity choice to map of activity choice - * - * @param examActivity exam activity to append - * @param activityChoiceMap Map of activity choice - */ -func (e *Exam) appendExamActivityChoice(examActivity storages.ExamActivityDB, activityChoiceMap map[int]interface{}) { - choices := activityChoiceMap[examActivity.ActivityID] - if examActivity.ActivityTypeID == 1 { - temp := choices.([]storages.MatchingChoiceDB) - temp = append(temp, e.examActivityToChoice(examActivity).(storages.MatchingChoiceDB)) - activityChoiceMap[examActivity.ActivityID] = temp - } else if examActivity.ActivityTypeID == 2 { - temp := choices.([]storages.MultipleChoiceDB) - temp = append(temp, e.examActivityToChoice(examActivity).(storages.MultipleChoiceDB)) - activityChoiceMap[examActivity.ActivityID] = temp - } else if examActivity.ActivityTypeID == 3 { - temp := choices.([]storages.CompletionChoiceDB) - temp = append(temp, e.examActivityToChoice(examActivity).(storages.CompletionChoiceDB)) - activityChoiceMap[examActivity.ActivityID] = temp - } -} - -/** - * Convert exam activity model from the database to choice - * - * @param examActivity exam activity model from the database - * - * @return choice that converted from exam activity model - */ -func (e *Exam) examActivityToChoice(examActivity storages.ExamActivityDB) interface{} { - if examActivity.ActivityTypeID == 1 { - choice := storages.MatchingChoiceDB{} - utils.NewType().StructToStruct(examActivity, &choice) - return choice - } else if examActivity.ActivityTypeID == 2 { - choice := storages.MultipleChoiceDB{} - examActivity.Content = examActivity.MultipleChoiceContent - utils.NewType().StructToStruct(examActivity, &choice) - return choice - } else if examActivity.ActivityTypeID == 3 { - choice := storages.CompletionChoiceDB{} - examActivity.Content = examActivity.CompletionChoiceContent - utils.NewType().StructToStruct(examActivity, &choice) - return choice - } else { - return nil - } -} - -/** - * Check exam answers - * - * @param answers Answers of the exam - * - * @return exam result overview - * @return the error of the checking exam - */ -func (e *Exam) CheckAnswer(answers []request.ExamActivityAnswer) (*ExamResultOverview, error) { - var wg sync.WaitGroup - var mutex sync.Mutex - var err error - concurrent := general.Concurrent{Wg: &wg, Err: &err, Mutex: &mutex} - - activityMap := map[int]Activity{} - for _, activity := range e.activities { - activityMap[activity.GetInfo().ID] = activity - } - - e.result = &ExamResultOverview{ - TotalScore: 0, - IsPassed: false, - CreatedTimestamp: time.Now().Local(), - } - - wg.Add(len(answers)) - for _, answer := range answers { - go e.checkActivityAsync(&concurrent, activityMap[answer.ActivityID], answer.Answer) - } - wg.Wait() - - e.summaryResult() - return e.result, err -} - -/** - * Summary exam result - */ -func (e *Exam) summaryResult() { - answerTotalScore := e.GetAnswerTotalScore() - activitiesTotalScore := e.GetActivitiesTotalScore() - e.result.IsPassed = e.isPassed(answerTotalScore, activitiesTotalScore) -} - -/** - * Calculate total score for the answer - * - * @return score of the answers - */ -func (e *Exam) GetAnswerTotalScore() int { - sum := 0 - for _, activityResult := range e.result.ActivitiesResult { - sum += activityResult.Score - } - e.result.TotalScore = sum - return sum -} - -/** - * Get total score of the activities - * - * @return total score of the activities - */ -func (e *Exam) GetActivitiesTotalScore() int { - sum := 0 - for _, activity := range e.activities { - sum += activity.GetInfo().Point - } - return sum -} - -/** - * Check pass exam - * - * @param answerTotalScore Total score of the Answers - * @param activitiesTotalScore Total score of the activities - * - * @return true of passed the exam, false otherwise - */ -func (e *Exam) isPassed(answerTotalScore int, activitiesTotalScore int) bool { - passedRate := 0.5 - if activitiesTotalScore == 0 { - return true - } else { - return (float64)(answerTotalScore/activitiesTotalScore) > passedRate - } - -} - -/** - * Check concurrency activity answer - * - * @param concurrent Concurrency model for doing concurrent - * @param activity Activity to check - * @param answer Answer of the activity - */ -func (e *Exam) checkActivityAsync(concurrent *general.Concurrent, activity Activity, answer interface{}) { - defer concurrent.Wg.Done() - score := 0 - isCorrect := true - isCorrect, err := activity.IsAnswerCorrect(answer) - if err != nil { - *concurrent.Err = err - return - } - - if isCorrect { - score += activity.GetInfo().Point - } - - concurrent.Mutex.Lock() - e.result.ActivitiesResult = append(e.result.ActivitiesResult, ExamActivityResult{ - ActivityID: activity.GetInfo().ID, - Score: score, - }) - concurrent.Mutex.Unlock() -} diff --git a/models/entities/entity.user.go b/models/entities/entity.user.go deleted file mode 100644 index 671137c..0000000 --- a/models/entities/entity.user.go +++ /dev/null @@ -1,224 +0,0 @@ -package entities - -// entity.user.go -/** - * This file is a part of models, used to collect model for entities of user - */ - -import ( - "DatabaseCamp/models/request" - "DatabaseCamp/models/storages" - "DatabaseCamp/utils" - "os" - "time" -) - -// Type for changes user points mode -type ChangePointMode string - -// Change mode for update user point -var Mode = struct { - Add ChangePointMode - Reduce ChangePointMode -}{ - "+", - "-", -} - -// Model of badge for user entity -type Badge struct { - ID int `json:"badge_id"` - ImagePath string `json:"icon_path"` - Name string `json:"name"` - IsCollected bool `json:"is_collect"` -} - -/** - * This class manage user model - */ -type User struct { - id int - name string - email string - password string - badges []Badge - createdTimestamp time.Time - updatedTimestamp time.Time -} - -/** - * Constructor creates a new user instance by user request - * - * @param request User request - * - * @return instance of user - */ -func NewUserByRequest(request request.UserRequest) User { - user := User{ - name: request.Name, - email: request.Email, - password: request.Password, - } - user.SetTimestamp() - user.HashPassword() - return user -} - -/** - * Constructor creates a new user instance by user database model - * - * @param request User database model - * - * @return instance of user - */ -func NewUserByUserDB(userDB storages.UserDB) User { - return User{ - id: userDB.ID, - name: userDB.Name, - email: userDB.Email, - password: userDB.Password, - createdTimestamp: userDB.CreatedTimestamp, - updatedTimestamp: userDB.UpdatedTimestamp, - } -} - -/** - * Getter for getting user ID - * - * @return user ID - */ -func (u *User) GetID() int { - return u.id -} - -/** - * Getter for getting user name - * - * @return user name - */ -func (u *User) GetName() string { - return u.name -} - -/** - * Getter for getting user email - * - * @return user email - */ -func (u *User) GetEmail() string { - return u.email -} - -/** - * Getter for getting user badges - * - * @return user badges - */ -func (u *User) GetBadges() []Badge { - return u.badges -} - -/** - * Getter for getting user created timestamp - * - * @return user created timestamp - */ -func (u *User) GetCreatedTimestamp() time.Time { - return u.createdTimestamp -} - -/** - * Getter for getting user updated timestamp - * - * @return user updated timestamp - */ -func (u *User) GetUpdatedTimstamp() time.Time { - return u.updatedTimestamp -} - -/** - * Setter user ID - * - * @param id User ID to set - */ -func (u *User) SetID(id int) { - u.id = id -} - -/** - * Setter user ID - * - * @param allBadgesDB All badges in the application - * @param correctedBadgesDB Corrected badges of the user - */ -func (u *User) SetCorrectedBadges(allBadgesDB []storages.BadgeDB, correctedBadgesDB []storages.UserBadgeDB) { - for _, badgeDB := range allBadgesDB { - u.badges = append(u.badges, Badge{ - ID: badgeDB.ID, - ImagePath: badgeDB.ImagePath, - Name: badgeDB.Name, - IsCollected: u.isCorrectedBadge(correctedBadgesDB, badgeDB.ID), - }) - } -} - -/** - * Setter user timestamp to now - */ -func (u *User) SetTimestamp() { - u.createdTimestamp = time.Now().Local() - u.updatedTimestamp = time.Now().Local() -} - -/** - * Hash user password - */ -func (u *User) HashPassword() { - u.password = utils.NewHelper().HashAndSalt(u.password) -} - -/** - * Check corrected badge - * - * @param allBadgesDB All badges in the application - * @param badgeID Badge ID to check - * - * @return true if badge id is correct, false otherwise - */ -func (u *User) isCorrectedBadge(allBadgesDB []storages.UserBadgeDB, badgeID int) bool { - for _, correctedBadgeDB := range allBadgesDB { - if badgeID == correctedBadgeDB.BadgeID { - return true - } - } - return false -} - -/** - * To database model - * - * @return user database model - */ -func (u *User) ToDB() storages.UserDB { - tokenExpireHour := time.Hour * utils.NewType().ParseDuration(os.Getenv("TOKEN_EXPIRE_HOUR")) - expiredTokenTimestamp := time.Now().Local().Add(tokenExpireHour) - return storages.UserDB{ - Name: u.name, - Email: u.email, - Password: u.password, - CreatedTimestamp: u.createdTimestamp, - UpdatedTimestamp: u.updatedTimestamp, - ExpiredTokenTimestamp: expiredTokenTimestamp, - } -} - -/** - * Check user password - * - * @param password Password to check - * - * @return true if input password is correct, false if not - */ -func (u *User) IsPasswordCorrect(password string) bool { - return utils.NewHelper().ComparePasswords(u.password, password) -} diff --git a/models/general/controller.go b/models/general/controller.go deleted file mode 100644 index d0443fb..0000000 --- a/models/general/controller.go +++ /dev/null @@ -1,24 +0,0 @@ -package general - -// controller.go -/** - * This file is a part of models, used to collect model for controller - */ - -import ( - "DatabaseCamp/database" - "sync" -) - -// Model for do concurrent -type Concurrent struct { - Wg *sync.WaitGroup - Err *error - Mutex *sync.Mutex -} - -// Model for do concurrent with transaction -type ConcurrentTransaction struct { - Concurrent *Concurrent - Transaction database.ITransaction -} diff --git a/models/request/request.exam.go b/models/request/request.exam.go deleted file mode 100644 index 97d5edf..0000000 --- a/models/request/request.exam.go +++ /dev/null @@ -1,36 +0,0 @@ -package request - -// request.exam.go -/** - * This file is a part of models, used to collect request of exam - */ - -import "DatabaseCamp/errs" - -// Model for correct exam activity answer request -type ExamActivityAnswer struct { - ActivityID int `json:"activity_id"` - Answer interface{} `json:"answer"` -} - -/** - * This class represent exam answer request - */ -type ExamAnswerRequest struct { - ExamID *int `json:"exam_id"` - Activities []ExamActivityAnswer `json:"activities"` -} - -/** - * Validate exam answer request - * - * @return the error of validating request - */ -func (r ExamAnswerRequest) Validate() error { - if r.ExamID == nil { - return errs.NewBadRequestError("ไม่พบรหัสของข้อสอบในคำร้องขอ", "Exam ID Not Found") - } else if len(r.Activities) == 0 { - return errs.NewBadRequestError("ไม่พบกิจกรรมของข้อสอบในคำร้องขอ", "Activities Exam Not Found") - } - return nil -} diff --git a/models/request/request.learning.go b/models/request/request.learning.go deleted file mode 100644 index 31fe5f1..0000000 --- a/models/request/request.learning.go +++ /dev/null @@ -1,154 +0,0 @@ -package request - -// request.learning.go -/** - * This file is a part of models, used to collect request of learning - */ - -import "DatabaseCamp/errs" - -/** - * This class represent multiple choice answer request - */ -type MultipleChoiceAnswerRequest struct { - ActivityID *int `json:"activity_id"` - Answer *int `json:"answer"` -} - -/** - * Validate multiple choice answer request - * - * @return the error of validating request - */ -func (r MultipleChoiceAnswerRequest) Validate() error { - if r.ActivityID == nil { - return errs.NewBadRequestError("ไม่พบไอดีของกิจกรรมในคำร้องขอ", "Activity ID Not Found") - } else if r.Answer == nil { - return errs.NewBadRequestError("ไม่พบคำตอบในคำร้องขอ", "Answer Not Found") - } - return nil -} - -/** - * This class represent matching choice answer request - */ -type MatchingChoiceAnswerRequest struct { - ActivityID *int `json:"activity_id"` - Answer []PairItemRequest `json:"answer"` -} - -/** - * Validate pair item request - * - * @param pairItem PairItemRequest - * - * @return the error of validating request - */ -func (r MatchingChoiceAnswerRequest) validatePairItem(pairItem PairItemRequest) error { - if pairItem.Item1 == nil || pairItem.Item2 == nil { - return errs.NewBadRequestError("ไม่พบคำตอบในคำร้องขอ", "Answer Not Found") - } - return nil -} - -/** - * Validate matching choice answer request - * - * @return the error of validating request - */ -func (r MatchingChoiceAnswerRequest) Validate() error { - if r.ActivityID == nil { - return errs.NewBadRequestError("ไม่พบไอดีของกิจกรรมในคำร้องขอ", "Activity ID Not Found") - } else if len(r.Answer) == 0 { - return errs.NewBadRequestError("ไม่พบคำตอบในคำร้องขอ", "Answer Not Found") - } else { - for _, v := range r.Answer { - e := r.validatePairItem(v) - if e != nil { - return e - } - } - } - return nil -} - -/** - * This class represent completion choice answer request - */ -type CompletionAnswerRequest struct { - ActivityID *int `json:"activity_id"` - Answer []PairContentRequest `json:"answer"` -} - -/** - * Validate pair content request - * - * @param pairContent PairContent request - * - * @return the error of validating request - */ -func (r CompletionAnswerRequest) validatePairItem(pairContent PairContentRequest) error { - if pairContent.Content == nil || pairContent.ID == nil { - return errs.NewBadRequestError("ไม่พบคำตอบในคำร้องขอ", "Answer Not Found") - } - return nil -} - -/** - * Validate completion choice answer request - * - * @return the error of validating request - */ -func (r CompletionAnswerRequest) Validate() error { - if r.ActivityID == nil { - return errs.NewBadRequestError("ไม่พบไอดีของกิจกรรมในคำร้องขอ", "Activity ID Not Found") - } else if len(r.Answer) == 0 { - return errs.NewBadRequestError("ไม่พบคำตอบในคำร้องขอ", "Answer Not Found") - } else { - for _, v := range r.Answer { - e := r.validatePairItem(v) - if e != nil { - return e - } - } - } - return nil -} - -// Model for matching choice answer request -type PairItemRequest struct { - Item1 *string `json:"item1"` - Item2 *string `json:"item2"` -} - -/** - * Validate pair item request request - * - * @return the error of validating request - */ -func (m PairItemRequest) Validate() error { - if m.Item1 == nil || m.Item2 == nil { - return errs.NewBadRequestError("ไม่พบเนื้อหาของคำตอบในคำร้องขอ", "Content Answer Not Found") - } - return nil -} - -// Model for completion choice answer request -type PairContentRequest struct { - ID *int `json:"completion_choice_id"` - Content *string `json:"content"` -} - -/** - * Validate pair content request request - * - * @return the error of validating request - */ -func (m PairContentRequest) Validate() error { - if m.ID == nil { - return errs.NewBadRequestError("ไม่พบไอดีของช้อยในคำร้องขอ", "Choice ID Not Found") - } else if m.Content == nil { - return errs.NewBadRequestError("ไม่พบเนื้อหาของคำตอบในคำร้องขอ", "Content Answer Not Found") - } - return nil -} diff --git a/models/response/response.content_overview.go b/models/response/response.content_overview.go deleted file mode 100644 index 9599827..0000000 --- a/models/response/response.content_overview.go +++ /dev/null @@ -1,250 +0,0 @@ -package response - -// response.content_overview.go -/** - * This file is a part of models, used to collect response of content overview - */ - -import "DatabaseCamp/models/storages" - -// Model of content to prepare Content overview response -type content struct { - id int - name string - activities []int -} - -// Model of group to prepare Content overview response -type group struct { - id int - name string - contents map[int]*content -} - -// Model of lasted group overview to prepare Content overview response -type lastedGroupOverview struct { - GroupID int `json:"group_id"` - ContentID int `json:"content_id"` - ActivityID int `json:"activity_id"` - GroupName string `json:"group_name"` - ContentName string `json:"content_name"` - Progress int `json:"progress"` -} - -// Model of content overview to prepare Content overview response -type contentOverview struct { - ContentID int `json:"content_id"` - ContentName string `json:"content_name"` - IsLasted bool `json:"is_lasted"` - Progress int `json:"progress"` -} - -// Model of content group overview to prepare Content overview response -type contentGroupOverview struct { - GroupID int `json:"group_id"` - IsRecommend bool `json:"is_recommend"` - IsLasted bool `json:"is_lasted"` - GroupName string `json:"group_name"` - Progress int `json:"progress"` - Contents []contentOverview `json:"contents"` -} - -/** - * This class represent content overview response - */ -type ContentOverviewResponse struct { - LastedGroup *lastedGroupOverview `json:"lasted_group"` - ContentGroupOverview []contentGroupOverview `json:"content_group_overview"` -} - -/** - * Constructor creates a new ContentOverviewResponse instance - * - * @param overviewDB Overview model from database to prepare overview response - * @param learningProgressionDB Learning progression from database to prepare overview response - * - * @return instance of ContentOverviewResponse - */ -func NewContentOverviewResponse(overviewDB []storages.OverviewDB, learningProgressionDB []storages.LearningProgressionDB) *ContentOverviewResponse { - response := ContentOverviewResponse{} - response.prepare(overviewDB, learningProgressionDB) - return &response -} - -/** - * Prepare content overview response - * - * @param overviewDB Overview model from database to prepare overview response - * @param learningProgressionDB Learning progression from database to prepare overview response - */ -func (o *ContentOverviewResponse) prepare(overviewDB []storages.OverviewDB, learningProgressionDB []storages.LearningProgressionDB) { - groupMap := o.createGroupMap(overviewDB) - activityContentIDMap := o.createActivityContentIDMap(overviewDB) - userActivityCountByContentID := o.createUserActivityCountByContentID(learningProgressionDB, activityContentIDMap) - lastedActivityID := o.getLastedActivityID(learningProgressionDB) - lastedContentID := o.getLastedContentID(activityContentIDMap, lastedActivityID) - for _, group := range groupMap { - groupActivityCount := 0 - groupUserActivityCount := 0 - isGroupLasted := false - contents := make([]contentOverview, 0) - for _, content := range group.contents { - groupActivityCount += len(content.activities) - groupUserActivityCount += userActivityCountByContentID[content.id] - isContentLasted := lastedContentID != nil && *lastedContentID == content.id - contentProgress := o.calculateProgress(userActivityCountByContentID[content.id], len(content.activities)) - contents = append(contents, contentOverview{ - ContentID: content.id, - ContentName: content.name, - IsLasted: isContentLasted, - Progress: contentProgress, - }) - if isContentLasted { - o.LastedGroup = &lastedGroupOverview{ - GroupID: group.id, - ContentID: content.id, - GroupName: group.name, - ActivityID: *lastedActivityID, - ContentName: content.name, - Progress: contentProgress, - } - } - } - groupProgress := o.calculateProgress(groupUserActivityCount, groupActivityCount) - o.ContentGroupOverview = append(o.ContentGroupOverview, contentGroupOverview{ - GroupID: group.id, - IsRecommend: false, - IsLasted: isGroupLasted, - GroupName: group.name, - Progress: groupProgress, - Contents: contents, - }) - } -} - -/** - * Create content group map to used for prepare - * [key] content id - * [value] group model - * - * @param overviewDB Overview model from database to prepare overview response - * - * @return map of the content group - */ -func (o *ContentOverviewResponse) createGroupMap(overviewDB []storages.OverviewDB) map[int]*group { - groupMap := map[int]*group{} - for _, overview := range overviewDB { - _group := groupMap[overview.GroupID] - if _group == nil { - _group = &group{ - id: overview.GroupID, - name: overview.GroupName, - contents: map[int]*content{}, - } - groupMap[overview.GroupID] = _group - } - - _content := _group.contents[overview.ContentID] - if _content == nil { - _content = &content{ - id: overview.ContentID, - name: overview.ContentName, - activities: []int{}, - } - _group.contents[overview.ContentID] = _content - } - - if overview.ActivityID != nil { - _content.activities = append(_content.activities, *overview.ActivityID) - } - } - - return groupMap -} - -/** - * Create activity content id map to used for prepare - * [key] activity id - * [value] content id - * - * @param overviewDB Overview model from database to prepare overview response - * - * @return map of the activity content - */ -func (o *ContentOverviewResponse) createActivityContentIDMap(overviewDB []storages.OverviewDB) map[int]int { - activityContentIDMap := map[int]int{} - for _, overview := range overviewDB { - if overview.ActivityID != nil { - activityContentIDMap[*overview.ActivityID] = overview.ContentID - } - } - return activityContentIDMap -} - -/** - * Get lasted activity ID - * - * @param learningProgressionDB Learning progression from database to prepare overview response - * - * @return lasted activity ID - */ -func (o *ContentOverviewResponse) getLastedActivityID(learningProgressionDB []storages.LearningProgressionDB) *int { - if len(learningProgressionDB) == 0 { - return nil - } else { - return &learningProgressionDB[0].ActivityID - } -} - -/** - * Create user activity count map by content id to used for prepare - * [key] content id - * [value] activity count - * - * @param learningProgressionDB Learning progression from database to prepare overview response - * @param activityContentIDMap Map of the activity content - * - * @return map of the activity count - */ -func (o *ContentOverviewResponse) createUserActivityCountByContentID(learningProgressionDB []storages.LearningProgressionDB, activityContentIDMap map[int]int) map[int]int { - userActivityCount := map[int]int{} - for _, learningProgression := range learningProgressionDB { - userActivityCount[activityContentIDMap[learningProgression.ActivityID]]++ - } - return userActivityCount -} - -/** - * Calculate progression percent - * - * @param progress Number of progress - * @param total Total of progress - * - * @return progression percent - */ -func (o *ContentOverviewResponse) calculateProgress(progress int, total int) int { - if total == 0 { - return 0 - } else { - ratio := float64(progress) / float64(total) - return int(ratio * 100) - } - -} - -/** - * Get lasted learning content ID - * - * @param activityContentIDMap Map of the activity count - * @param lastedActivityID Lasted learning Activity ID - * - * @return lasted learning content ID - */ -func (o *ContentOverviewResponse) getLastedContentID(activityContentIDMap map[int]int, lastedActivityID *int) *int { - if lastedActivityID == nil { - return nil - } else { - contentID := activityContentIDMap[*lastedActivityID] - return &contentID - } -} diff --git a/models/response/response.content_roadmap.go b/models/response/response.content_roadmap.go deleted file mode 100644 index 970f087..0000000 --- a/models/response/response.content_roadmap.go +++ /dev/null @@ -1,76 +0,0 @@ -package response - -// response.content_roadmap.go -/** - * This file is a part of models, used to collect response of content roadmap - */ - -import "DatabaseCamp/models/storages" - -// Model of content roadmap item to prepare content roadmap response -type contentRoadmapItem struct { - ActivityID int `json:"activity_id"` - IsLearned bool `json:"is_learned"` - Order int `json:"order"` -} - -/** - * This class represent content roadmap response - */ -type ContentRoadmapResponse struct { - ContentID int `json:"content_id"` - ContentName string `json:"content_name"` - Items []contentRoadmapItem `json:"items"` -} - -/** - * Constructor creates a new ContentRoadmapResponse instance - * - * @param contentDB Content model from database to prepare overview response - * @param contentActivitiesDB Content Activity progression from database to prepare overview response - * @param learningProgressionDB Learning progression from database to prepare overview response - * - * @return instance of ContentRoadmapResponse - */ -func NewContentRoadmapResponse(contentDB storages.ContentDB, contentActivitiesDB []storages.ActivityDB, learningProgressionDB []storages.LearningProgressionDB) *ContentRoadmapResponse { - response := ContentRoadmapResponse{} - response.prepare(contentDB, contentActivitiesDB, learningProgressionDB) - return &response -} - -/** - * Prepare content roadmap response - * - * @param contentDB Content model from database to prepare overview response - * @param contentActivitiesDB Content Activity progression from database to prepare overview response - * @param learningProgressionDB Learning progression from database to prepare overview response - */ -func (c *ContentRoadmapResponse) prepare(contentDB storages.ContentDB, contentActivitiesDB []storages.ActivityDB, learningProgressionDB []storages.LearningProgressionDB) { - c.ContentID = contentDB.ID - c.ContentName = contentDB.Name - for _, activity := range contentActivitiesDB { - isLearned := c.isLearnedActivity(learningProgressionDB, activity.ID) - c.Items = append(c.Items, contentRoadmapItem{ - ActivityID: activity.ID, - IsLearned: isLearned, - Order: activity.Order, - }) - } -} - -/** - * Check learned activity - * - * @param activityID Activity ID to check - * @param learningProgressionDB Learning progression from database to prepare overview response - * - * @return true if the activity is learned, false otherwise - */ -func (c *ContentRoadmapResponse) isLearnedActivity(progression []storages.LearningProgressionDB, activityID int) bool { - for _, v := range progression { - if v.ActivityID == activityID { - return true - } - } - return false -} diff --git a/models/response/response.exam.go b/models/response/response.exam.go deleted file mode 100644 index cd0acde..0000000 --- a/models/response/response.exam.go +++ /dev/null @@ -1,48 +0,0 @@ -package response - -// response.exam.go -/** - * This file is a part of models, used to collect response of exam - */ - -import ( - "DatabaseCamp/models/entities" -) - -/** - * This class represent exam response - */ -type ExamResponse struct { - Exam entities.ExamInfo `json:"exam"` - Activities []ActivityResponse `json:"activities"` -} - -/** - * Constructor creates a new ExamResponse instance - * - * @param exam Entities exam for create exam response - * - * @return instance of ExamOverviewResponse - */ -func NewExamResponse(exam entities.Exam) *ExamResponse { - response := ExamResponse{} - response.prepare(exam) - return &response -} - -/** -* Prepare exam response -* -* @param exam Entities exam for create exam response - */ -func (e *ExamResponse) prepare(exam entities.Exam) { - activitiesResponse := make([]ActivityResponse, 0) - for _, activity := range exam.GetActivities() { - activitiesResponse = append(activitiesResponse, ActivityResponse{ - Activity: activity.GetInfo(), - Choices: activity.GetPropositionChoices(), - }) - } - e.Activities = activitiesResponse - e.Exam = exam.GetInfo() -} diff --git a/models/response/response.exam_overview.go b/models/response/response.exam_overview.go deleted file mode 100644 index 3cbab84..0000000 --- a/models/response/response.exam_overview.go +++ /dev/null @@ -1,130 +0,0 @@ -package response - -// response.exam_overview.go -/** - * This file is a part of models, used to collect response of exam overview - */ - -import ( - "DatabaseCamp/models/entities" - "DatabaseCamp/models/storages" -) - -// Model of exam overview item to prepare exam overview response -type examDetailOverview struct { - ExamID int `json:"exam_id"` - ExamType string `json:"exam_type"` - ContentGroupID *int `json:"content_group_id,omitempty"` - ContentGroupName *string `json:"content_group_name,omitempty"` - CanDo *bool `json:"can_do,omitempty"` - Results *[]entities.ExamResultOverview `json:"results,omitempty"` -} - -/** - * This class represent exam overview response - */ -type ExamOverviewResponse struct { - PreExam *examDetailOverview `json:"pre_exam"` - MiniExam *[]examDetailOverview `json:"mini_exam"` - FinalExam *examDetailOverview `json:"final_exam"` -} - -/** - * Constructor creates a new ExamOverviewResponse instance - * - * @param examResultsDB Exam result from database to prepare exam overview response - * @param examsDB Exam from database to prepare exam overview response - * @param canDoFinalExam Boolean that indicate user can do the final exam - * - * @return instance of ExamOverviewResponse - */ -func NewExamOverviewResponse(examResultsDB []storages.ExamResultDB, examsDB []storages.ExamDB, canDoFinalExam bool) *ExamOverviewResponse { - response := ExamOverviewResponse{} - response.prepare(examResultsDB, examsDB, canDoFinalExam) - return &response -} - -/** -* Prepare exam overview response -* -* @param examResultsDB Exam result from database to prepare exam overview response -* @param examsDB Exam from database to prepare exam overview response -* @param canDoFinalExam Boolean that indicate user can do the final exam - */ -func (o *ExamOverviewResponse) prepare(examResultsDB []storages.ExamResultDB, examsDB []storages.ExamDB, canDoFinalExam bool) { - examResultMap := o.createExamResultMap(examResultsDB) - for _, examDB := range examsDB { - if examDB.Type == entities.ExamType.Pretest { - o.PreExam = &examDetailOverview{ - ExamID: examDB.ID, - ExamType: examDB.Type, - Results: examResultMap[examDB.ID], - } - } else if examDB.Type == entities.ExamType.MiniExam { - if o.MiniExam == nil { - temp := make([]examDetailOverview, 0) - o.MiniExam = &temp - } - contentGroupID := examDB.ContentGroupID - contentGroupName := examDB.ContentGroupName - *o.MiniExam = append(*o.MiniExam, examDetailOverview{ - ExamID: examDB.ID, - ExamType: examDB.Type, - ContentGroupID: &contentGroupID, - ContentGroupName: &contentGroupName, - Results: examResultMap[examDB.ID], - }) - } else if examDB.Type == entities.ExamType.Posttest { - o.FinalExam = &examDetailOverview{ - ExamID: examDB.ID, - ExamType: examDB.Type, - CanDo: &canDoFinalExam, - Results: examResultMap[examDB.ID], - } - } - } -} - -/** - * Create map of exam result - * [key] exam id - * [value] exam result overview - * - * @param examResultsDB exam result from database to create exam result map - * - * @return map of exam result - */ -func (o *ExamOverviewResponse) createExamResultMap(examResultsDB []storages.ExamResultDB) map[int]*[]entities.ExamResultOverview { - examResultMap := map[int]*[]entities.ExamResultOverview{} - examScoreCount := o.countExamScore(examResultsDB) - for _, examResult := range examResultsDB { - if examResultMap[examResult.ExamID] == nil { - temp := make([]entities.ExamResultOverview, 0) - examResultMap[examResult.ExamID] = &temp - } - *examResultMap[examResult.ExamID] = append(*examResultMap[examResult.ExamID], entities.ExamResultOverview{ - ExamResultID: examResult.ID, - TotalScore: examScoreCount[examResult.ID], - IsPassed: examResult.IsPassed, - CreatedTimestamp: examResult.CreatedTimestamp, - }) - } - return examResultMap -} - -/** - * Create map of count exam score - * [key] exam result id - * [value] score of the exam result - * - * @param examResultsDB exam result from database to calculate exam score - * - * @return exam score - */ -func (o *ExamOverviewResponse) countExamScore(examResultsDB []storages.ExamResultDB) map[int]int { - examCountScore := map[int]int{} - for _, examResultDB := range examResultsDB { - examCountScore[examResultDB.ID] += examResultDB.Score - } - return examCountScore -} diff --git a/models/response/response.exam_result_overview.go b/models/response/response.exam_result_overview.go deleted file mode 100644 index 46e6774..0000000 --- a/models/response/response.exam_result_overview.go +++ /dev/null @@ -1,45 +0,0 @@ -package response - -// response.exam_result_overview.go -/** - * This file is a part of models, used to collect response of exam result overview - */ - -import ( - "DatabaseCamp/models/entities" - "time" -) - -/** - * This class represent exam result overview response - */ -type ExamResultOverviewResponse struct { - ExamID int `json:"exam_id"` - ExamResultID int `json:"exam_result_id"` - ExamType string `json:"exam_type"` - ContentGroupName string `json:"content_group_name"` - CreatedTimestamp time.Time `json:"created_timestamp"` - Score int `json:"score"` - IsPassed bool `json:"is_passed"` - ActivitiesResult []entities.ExamActivityResult `json:"activities_result"` -} - -/** - * Constructor creates a new ExamResultOverviewResponse instance - * - * @param exam Entities exam for create exam result overview response - * - * @return instance of ExamOverviewResponse - */ -func NewExamResultOverviewResponse(exam entities.Exam) *ExamResultOverviewResponse { - return &ExamResultOverviewResponse{ - ExamID: exam.GetInfo().ID, - ExamResultID: exam.GetResult().ExamResultID, - ExamType: exam.GetInfo().Type, - ContentGroupName: exam.GetInfo().ContentGroupName, - CreatedTimestamp: exam.GetResult().CreatedTimestamp, - Score: exam.GetResult().TotalScore, - IsPassed: exam.GetResult().IsPassed, - ActivitiesResult: exam.GetResult().ActivitiesResult, - } -} diff --git a/models/response/response.learning.go b/models/response/response.learning.go deleted file mode 100644 index 7ab9e94..0000000 --- a/models/response/response.learning.go +++ /dev/null @@ -1,82 +0,0 @@ -package response - -// response.learning.go -/** - * This file is a part of models, used to collect response of learning - */ - -import "DatabaseCamp/models/entities" - -// Model of video lecture item to prepare video lecture response -type VideoLectureResponse struct { - ContentID int `json:"content_id"` - ContentName string `json:"content_name"` - VideoLink string `json:"video_link"` -} - -/** - * Constructor creates a new VideoLectureResponse instance - * - * @param contentID Content id from database to create video lecture response - * @param contentName Content name from database to create video lecture response - * @param videoLink Video link from database to create video lecture response - * - * @return instance of VideoLectureResponse - */ -func NewVideoLectureResponse(contentID int, contentName string, videoLink string) *VideoLectureResponse { - return &VideoLectureResponse{ - ContentID: contentID, - ContentName: contentName, - VideoLink: videoLink, - } -} - -/** - * This class represent activity response - */ -type ActivityResponse struct { - Activity entities.ActivityDetail `json:"activity"` - Choices interface{} `json:"choice"` - Hint entities.ActivityHint `json:"hint"` -} - -/** - * Constructor creates a new ActivityResponse instance - * - * @param activity Entities activity from database to create activity response - * - * @return instance of ActivityResponse - */ -func NewActivityResponse(activity entities.Activity) *ActivityResponse { - return &ActivityResponse{ - Activity: activity.GetInfo(), - Choices: activity.GetPropositionChoices(), - Hint: *activity.GetHint(), - } -} - -/** - * This class represent answer response - */ -type AnswerResponse struct { - ActivityID int `json:"activity_id"` - IsCorrect bool `json:"is_correct"` - UpdatedPoint int `json:"updated_point"` -} - -/** - * Constructor creates a new ActivityAnswerResponse instance - * - * @param activity Entities activity from database to create activity answer response - * @param updatedPoint Update point from database to create activity answer response - * @param isCorrect Is correct from database to create activity answer response - * - * @return instance of ActivityAnswerResponse - */ -func NewActivityAnswerResponse(activity entities.Activity, updatedPoint int, isCorrect bool) *AnswerResponse { - return &AnswerResponse{ - ActivityID: activity.GetInfo().ID, - IsCorrect: isCorrect, - UpdatedPoint: updatedPoint, - } -} diff --git a/models/response/response.user.go b/models/response/response.user.go deleted file mode 100644 index 6aa8b92..0000000 --- a/models/response/response.user.go +++ /dev/null @@ -1,92 +0,0 @@ -package response - -// response.user.go -/** - * This file is a part of models, used to collect response of user - */ - -import ( - "DatabaseCamp/models/entities" - "DatabaseCamp/models/storages" - "time" -) - -/** - * This class represent user response - */ -type UserResponse struct { - ID int `json:"user_id"` - Name string `json:"name"` - Email string `json:"email"` - Point int `json:"point"` - AccessToken string `json:"access_token"` - CreatedTimestamp time.Time ` json:"created_timestamp"` - UpdatedTimestamp time.Time ` json:"updated_timestamp"` -} - -/** - * Constructor creates a new VideoLectureResponse instance - * - * @param user Entities user from database to create user response - * - * @return instance of UserReponse - */ -func NewUserReponse(user entities.User) UserResponse { - response := UserResponse{ - ID: user.GetID(), - Name: user.GetName(), - Email: user.GetEmail(), - Point: 0, - AccessToken: "", - CreatedTimestamp: user.GetCreatedTimestamp(), - UpdatedTimestamp: user.GetUpdatedTimstamp(), - } - return response -} - -/** - * This class represent get profile response - */ -type GetProfileResponse struct { - ID int `json:"user_id"` - Name string `json:"name"` - Point int `json:"point"` - ActivityCount int `json:"activity_count"` - Badges []entities.Badge `json:"badges"` - CreatedTimestamp time.Time ` json:"created_timestamp"` -} - -/** - * Constructor creates a new VideoLectureResponse instance - * - * @param profileDB Profile model from database to create get profile response - * @param badges Badge model from database to create get profile response - * - * @return instance of GetProfileResponse - */ -func NewGetProfileResponse(profileDB storages.ProfileDB, badges []entities.Badge) GetProfileResponse { - response := GetProfileResponse{ - ID: profileDB.ID, - Name: profileDB.Name, - Point: profileDB.Point, - ActivityCount: profileDB.ActivityCount, - Badges: badges, - CreatedTimestamp: profileDB.CreatedTimestamp, - } - return response -} - -/** - * This class represent edit profile response - */ -type EditProfileResponse struct { - UpdatedName string `json:"updated_name"` -} - -/** - * This class represent ranking response - */ -type RankingResponse struct { - UserRanking storages.RankingDB `json:"user_ranking"` - LeaderBoard []storages.RankingDB `json:"leader_board"` -} diff --git a/models/storages/database.go b/models/storages/database.go deleted file mode 100644 index 4d53573..0000000 --- a/models/storages/database.go +++ /dev/null @@ -1,275 +0,0 @@ -package storages - -// database.go -/** - * This file is a part of models, used to collect data structure of data in the database - */ - -import "time" - -// Table name in the database -var TableName = struct { - User string - Content string - ContentGroup string - LearningProgression string - Exam string - ExamResult string - ContentExam string - UserBadge string - Badge string - Activity string - ActivityType string - MatchingChoice string - CompletionChoice string - MultipleChoice string - Hint string - UserHint string - ExamResultActivity string -}{ - "User", - "Content", - "ContentGroup", - "LearningProgression", - "Exam", - "ExamResult", - "ContentExam", - "UserBadge", - "Badge", - "Activity", - "ActivityType", - "MatchingChoice", - "CompletionChoice", - "MultipleChoice", - "Hint", - "UserHint", - "ExamResultActivity", -} - -// Primary key of the table in the database -var IDName = struct { - User string - Activity string - Hint string - Content string - Exam string - MiniExam string - Badge string - ExamResult string -}{ - "user_id", - "activity_id", - "hint_id", - "content_id", - "exam_id", - "mini_exam_id", - "badge_id", - "exam_result_id", -} - -// View name in the database -var ViewName = struct { - Profile string - Ranking string -}{ - "Profile", - "Ranking", -} - -// Model mapped User table in the database -type UserDB struct { - ID int `gorm:"primaryKey;column:user_id" json:"user_id"` - Name string `gorm:"column:name" json:"name"` - Email string `gorm:"column:email" json:"email"` - Password string `gorm:"column:password" json:"password"` - AccessToken string `gorm:"column:access_token" json:"access_token"` - Point int `gorm:"column:point" json:"point"` - ExpiredTokenTimestamp time.Time `gorm:"column:expired_token_timestamp" json:"expired_token_timestamp"` - CreatedTimestamp time.Time `gorm:"column:created_timestamp" json:"created_timestamp"` - UpdatedTimestamp time.Time `gorm:"column:updated_timestamp" json:"updated_timestamp"` -} - -// Model mapped UserBage table in the database -type UserBadgeDB struct { - UserID int `gorm:"primaryKey;column:user_id" json:"user_id"` - BadgeID int `gorm:"primaryKey;column:badge_id" json:"badge_id"` -} - -// Model mapped Bage table in the database -type BadgeDB struct { - ID int `gorm:"primaryKey;column:badge_id" json:"badge_id"` - ImagePath string `gorm:"column:icon_path" json:"icon_path"` - Name string `gorm:"column:name" json:"name"` -} - -// Model mapped Probile view in the database -type ProfileDB struct { - ID int `gorm:"primaryKey;column:user_id" json:"user_id"` - Name string `gorm:"column:name" json:"name"` - Point int `gorm:"column:point" json:"point"` - ActivityCount int `gorm:"column:activity_count" json:"activity_count"` - CreatedTimestamp time.Time `gorm:"column:created_timestamp" json:"created_timestamp"` -} - -// Model mapped Ranking view in the database -type RankingDB struct { - ID int `gorm:"primaryKey;column:user_id" json:"user_id"` - Name string `gorm:"column:name" json:"name"` - Point int `gorm:"column:point" json:"point"` - Ranking int `gorm:"column:ranking" json:"ranking"` -} - -// Model mapped LearningProgression table in the database -type LearningProgressionDB struct { - ID int `gorm:"primaryKey;column:learning_progression_id" json:"learning_progression_id"` - UserID int `gorm:"column:user_id" json:"user_id"` - ActivityID int `gorm:"column:activity_id" json:"activity_id"` - CreatedTimestamp time.Time `gorm:"column:created_timestamp" json:"created_timestamp"` -} - -// Model mapped joined table in the database -// Table - UserBadge -// Table - Badge -type CorrectedBadgeDB struct { - BadgeID int `gorm:"column:badge_id" json:"badge_id"` - Name string `gorm:"column:badge_name" json:"badge_name"` - UserID *int `gorm:"column:user_id" json:"user_id"` -} - -// Model mapped Hint table in the database -type HintDB struct { - ID int `gorm:"primaryKey;column:hint_id" json:"hint_id"` - ActivityID int `gorm:"column:activity_id" json:"activity_id"` - Content string `gorm:"column:content" json:"content"` - PointReduce int `gorm:"column:point_reduce" json:"point_reduce"` - Level int `gorm:"column:level" json:"level"` -} - -// Model mapped UserHint table in the database -type UserHintDB struct { - UserID int `gorm:"primaryKey;column:user_id" json:"user_id"` - HintID int `gorm:"primaryKey;column:hint_id" json:"hint_id"` - CreatedTimestamp time.Time `gorm:"column:created_timestamp" json:"created_timestamp"` -} - -// Model mapped Content table in the database -type ContentDB struct { - ID int `gorm:"primaryKey;column:content_id" json:"content_id"` - GroupID int `gorm:"column:content_group_id" json:"content_group_id"` - Name string `gorm:"column:name" json:"name"` - VideoPath string `gorm:"column:video_path" json:"video_path"` - SlidePath string `gorm:"column:slide_path" json:"slide"` -} - -// Model mapped Activity table in the database -type ActivityDB struct { - ID int `gorm:"primaryKey;column:activity_id" json:"activity_id"` - TypeID int `gorm:"column:activity_type_id" json:"activity_type_id"` - ContentID *int `gorm:"column:content_id" json:"content_id"` - Order int `gorm:"column:activity_order" json:"activity_order"` - Story string `gorm:"column:story" json:"story"` - Point int `gorm:"column:point" json:"point"` - Question string `gorm:"column:question" json:"question"` -} - -// Model mapped MultipleChoice table in the database -type MultipleChoiceDB struct { - ID int `gorm:"primaryKey;column:multiple_choice_id" json:"multiple_choice_id"` - Content string `gorm:"column:content" json:"content"` - IsCorrect bool `gorm:"column:is_correct" json:"is_correct"` -} - -// Model mapped CompletionChoice table in the database -type CompletionChoiceDB struct { - ID int `gorm:"primaryKey;column:completion_choice_id" json:"completion_choice_id"` - Content string `gorm:"column:content" json:"content"` - QuestionFirst string `gorm:"column:question_first" json:"question_first"` - QuestionLast string `gorm:"column:question_last" json:"question_last"` -} - -// Model mapped MatchingChoice table in the database -type MatchingChoiceDB struct { - ID int `gorm:"primaryKey;column:matching_choice_id" json:"matching_choice_id"` - PairItem1 string `gorm:"column:pair_item1" json:"pair_item1"` - PairItem2 string `gorm:"column:pair_item2" json:"pair_item2"` -} - -// Model mapped joined table in the database -// Table - ContentGroup -// Table - Content -// Table - Activity -type OverviewDB struct { - GroupID int `gorm:"column:content_group_id" json:"group_id"` - ContentID int `gorm:"column:content_id" json:"content_id"` - ActivityID *int `gorm:"column:activity_id" json:"activity_id"` - GroupName string `gorm:"column:group_name" json:"group_name"` - ContentName string `gorm:"column:content_name" json:"content_name"` -} - -// Model mapped ContentExam table in the database -type ContentExamDB struct { - ExamID int `gorm:"primaryKey;column:exam_id" json:"exam_id"` - GroupID int `gorm:"primaryKey;column:content_group_id" json:"group_id"` - ActivityID int `gorm:"primaryKey;column:activity_id" json:"activity_id"` -} - -// Model mapped ExamResult table in the database -type ExamResultDB struct { - ID int `gorm:"primaryKey;column:exam_result_id" json:"exam_result_id"` - ExamID int `gorm:"column:exam_id" json:"exam_id"` - UserID int `gorm:"column:user_id" json:"user_id"` - Score int `gorm:"->;column:score" json:"score"` - IsPassed bool `gorm:"column:is_passed" json:"is_passed"` - CreatedTimestamp time.Time `gorm:"column:created_timestamp" json:"created_timestamp"` -} - -// Model mapped Exam table in the database -type ExamDB struct { - ID int `gorm:"primaryKey;column:exam_id" json:"exam_id"` - Type string `gorm:"column:type" json:"exam_type"` - Instruction string `gorm:"column:instruction" json:"instruction"` - CreatedTimestamp time.Time `gorm:"column:created_timestamp" json:"created_timestamp"` - ContentGroupID int `gorm:"column:content_group_id" json:"content_group_id"` - ContentGroupName string `gorm:"column:content_group_name" json:"content_group_name"` - BadgeID int `gorm:"column:badge_id" json:"badge_id"` -} - -// Model mapped ExamResultActivity table in the database -type ExamResultActivityDB struct { - ExamResultID int `gorm:"primaryKey;column:exam_result_id" json:"exam_result_id"` - ActivityID int `gorm:"primaryKey;column:activity_id" json:"activity_id"` - Score int `gorm:"column:score" json:"score"` -} - -// Model mapped joined table in the database -// Table - Exam -// Table - ContentExam -// Table - ContentGroup -// Table - Activity -// Table - MatchingChoice -// Table - MultipleChoice -// Table - CompletionChoice -type ExamActivityDB struct { - ExamID int `gorm:"column:exam_id" json:"exam_id"` - ExamType string `gorm:"column:exam_type" json:"exam_type"` - Instruction string `gorm:"column:instruction" json:"instruction"` - CreatedTimestamp time.Time `gorm:"column:created_timestamp" json:"created_timestamp"` - ActivityID int `gorm:"column:activity_id" json:"activity_id"` - Point int `gorm:"column:point" json:"point"` - ActivityTypeID int `gorm:"column:activity_type_id" json:"activity_type_id"` - Question string `gorm:"column:question" json:"question"` - Story string `gorm:"column:story" json:"story"` - PairItem1 string `gorm:"column:pair_item1" json:"pair_item1"` - PairItem2 string `gorm:"column:pair_item2" json:"pair_item2"` - CompletionChoiceID int `gorm:"column:completion_choice_id" json:"completion_choice_id"` - CompletionChoiceContent string `gorm:"column:completion_choice_content" json:"completion_choice_content"` - QuestionFirst string `gorm:"column:question_first" json:"question_first"` - QuestionLast string `gorm:"column:question_last" json:"question_last"` - MultipleChoiceID int `gorm:"column:multiple_choice_id" json:"multiple_choice_id"` - MultipleChoiceContent string `gorm:"column:multiple_choice_content" json:"multiple_choice_content"` - IsCorrect bool `gorm:"column:is_correct" json:"is_correct"` - Content string `json:"content"` - ContentGroupID int `gorm:"column:content_group_id" json:"content_group_id"` - ContentGroupName string `gorm:"column:content_group_name" json:"content_group_name"` - BadgeID int `gorm:"column:badge_id" json:"badge_id"` -} diff --git a/repositories/interface.go b/repositories/interface.go deleted file mode 100644 index 4e70f25..0000000 --- a/repositories/interface.go +++ /dev/null @@ -1,359 +0,0 @@ -package repositories - -// interface.go -/** - * This file used to be a interface of repositories - */ - -import ( - "DatabaseCamp/database" - "DatabaseCamp/models/entities" - "DatabaseCamp/models/storages" -) - -/** - * Interface to show function in exam respository that others can use - */ -type IExamRepository interface { - - /** - * Get activities of the exam from the database - * - * @param examID Exam ID for getting exam data - * - * @return activities of the exam - * @return the error of getting exam - */ - GetExamActivity(examID int) ([]storages.ExamActivityDB, error) - - /** - * Get exam overview from the database - * - * @return all exam in the application - * @return the error of getting exam - */ - GetExamOverview() ([]storages.ExamDB, error) - - /** - * Insert exam result into the database by database transaction - * - * @param tx Transaction model to do database transaction - * @param examResult Exam result for insert into the database - * - * @return inserted exam result - * @return the error of inserting exam result - */ - InsertExamResultTransaction(tx database.ITransaction, examResult storages.ExamResultDB) (storages.ExamResultDB, error) - - /** - * Insert activities of the exam result into the database by database transaction - * - * @param tx Transaction model to do database transaction - * @param examResultActivity Activities of the exam result for insert into the database - * - * @return inserted activities of the exam result - * @return the error of inserting activities of the exam result - */ - InsertExamResultActivityTransaction(tx database.ITransaction, examResultActivity []storages.ExamResultActivityDB) ([]storages.ExamResultActivityDB, error) -} - -/** - * Interface to show function in learning respository that others can use - */ -type ILearningRepository interface { - - /** - * Get learning content from the database - * - * @param contentID Content ID for getting content data - * - * @return learning content - * @return the error of getting content - */ - GetContent(id int) (*storages.ContentDB, error) - - /** - * Get content overview from the database - * - * @return all content in the application - * @return the error of getting content - */ - GetOverview() ([]storages.OverviewDB, error) - - /** - * Get content of the exam from the database - * - * @param examType Type of the exam for getting content of the exam - * - * @return all content of the exam in the application - * @return the error of getting content - */ - GetContentExam(examType string) ([]storages.ContentExamDB, error) - - /** - * Get activity from the database - * - * @param id Activity ID for getting activity data - * - * @return activity data - * @return the error of getting activity - */ - GetActivity(id int) (*storages.ActivityDB, error) - - /** - * Get matching choices of the activity from the database - * - * @param activityID Activity ID for getting matching choices of the activity - * - * @return matching choice of the activity - * @return the error of getting choices - */ - GetMatchingChoice(activityID int) ([]storages.MatchingChoiceDB, error) - - /** - * Get multiple choices of the activity from the database - * - * @param activityID Activity ID for getting multiple choices of the activity - * - * @return multiple choice of the activity - * @return the error of getting choices - */ - GetMultipleChoice(activityID int) ([]storages.MultipleChoiceDB, error) - - /** - * Get completion choices of the activity from the database - * - * @param activityID Activity ID for getting completion choices of the activity - * - * @return completion choice of the activity - * @return the error of getting choices - */ - GetCompletionChoice(activityID int) ([]storages.CompletionChoiceDB, error) - - /** - * Get hints of the activity from the database - * - * @param activityID Activity ID for getting hints of the activity - * - * @return hints of the activity - * @return the error of getting hints - */ - GetActivityHints(activityID int) ([]storages.HintDB, error) - - /** - * Get activities of the content from the database - * - * @param contentID Content ID for getting activities of the content - * - * @return activities of the content - * @return the error of getting activities - */ - GetContentActivity(contentID int) ([]storages.ActivityDB, error) - - /** - * Get video file link from the AWS service - * - * @param imagekey Image key of the file in amazon s3 - * - * @return file link - * @return the error of getting file link - */ - GetVideoFileLink(imagekey string) (string, error) -} - -//Interface that show how others function call and use function in module user respository -type IUserRepository interface { - - /** - * Get user by email from the database - * - * @param email Email for getting user data - * - * @return user data - * @return the error of getting user data - */ - GetUserByEmail(email string) (*storages.UserDB, error) - - /** - * Get user by id from the database - * - * @param id User ID for getting user data - * - * @return user data - * @return the error of getting user data - */ - GetUserByID(id int) (*storages.UserDB, error) - - /** - * Get user profile from the database - * - * @param id User ID for getting user profile - * - * @return user profile - * @return the error of getting user profile - */ - GetProfile(id int) (*storages.ProfileDB, error) - - /** - * Get learning progression of the user from the database - * - * @param id User ID for getting learning progression of the user - * - * @return learning progression of the user - * @return the error of getting learning progression of the user - */ - GetLearningProgression(id int) ([]storages.LearningProgressionDB, error) - - /** - * Get all badges of the application from the database - * - * @return all badges of the application - * @return the error of getting all badges of the application - */ - GetAllBadge() ([]storages.BadgeDB, error) - - /** - * Get user badge data by user badge id from the database - * - * @param id User Badge ID for getting user badge data - * - * @return user badge data - * @return the error of getting user badge data - */ - GetUserBadge(id int) ([]storages.UserBadgeDB, error) - - /** - * Get collected badges of the user from the database - * - * @param userID User ID for getting collected badges of the user - * - * @return collected badges of the user - * @return the error of getting collected badges of the user - */ - GetCollectedBadge(userID int) ([]storages.CorrectedBadgeDB, error) - - /** - * Get point ranking of the user from the database - * - * @param id User ID for getting point ranking of the user - * - * @return point ranking of the user - * @return the error of getting point ranking of the user - */ - GetPointRanking(id int) (*storages.RankingDB, error) - - /** - * Get all user point ranking from the database - * - * @return all user point ranking - * @return the error of getting all user point ranking - */ - GetRankingLeaderBoard() ([]storages.RankingDB, error) - - /** - * Get User Hint data from the database - * - * @param userID User ID for getting User Hint data - * @param activityID Activity ID for getting User Hint data - * - * @return User Hint data - * @return the error of getting User Hint data - */ - GetUserHint(userID int, activityID int) ([]storages.UserHintDB, error) - - /** - * Get exam results of the user from the database - * - * @param userID User ID for getting exam results of the user - * - * @return exam results of the user - * @return the error of getting exam results of the user - */ - GetExamResult(userID int) ([]storages.ExamResultDB, error) - - /** - * Get exam results of the user by exam result id from the database - * - * @param userID User ID for getting exam results of the user - * - * @return exam results of the user - * @return the error of getting exam results of the user - */ - GetExamResultByID(userID int, examResultID int) ([]storages.ExamResultDB, error) - - /** - * Insert user data into the database - * - * @param user User model for insert into the database - * - * @return inserted user - * @return the error of inserting user - */ - InsertUser(user storages.UserDB) (*storages.UserDB, error) - - /** - * Insert user hint into the database - * - * @param userHint User hint model for insert into the database - * - * @return inserted user hint - * @return the error of inserting user hint - */ - InsertUserHint(userHint storages.UserHintDB) (*storages.UserHintDB, error) - - /** - * Update user data into the database - * - * @param id User id to update into the database - * @param updateData User data to update into the database - * - * @return inserted user hint - * @return the error of updating user data - */ - UpdatesByID(id int, updateData map[string]interface{}) error - - /** - * Insert user hint into the database by database transaction - * - * @param tx Transaction model to do database transaction - * @param userHint User hint model for insert into the database - * - * @return inserted user hint - * @return the error of inserting user hint - */ - InsertUserHintTransaction(tx database.ITransaction, userHint storages.UserHintDB) (*storages.UserHintDB, error) - - /** - * Insert learning progression of the user into the database by database transaction - * - * @param tx Transaction model to do database transaction - * @param progression Learning progession model for insert into the database - * - * @return inserted learning progression - * @return the error of inserting learning progression - */ - InsertLearningProgressionTransaction(tx database.ITransaction, progression storages.LearningProgressionDB) (*storages.LearningProgressionDB, error) - - /** - * Insert user badge into the database by database transaction - * - * @param tx Transaction model to do database transaction - * @param userBadge User badge model for insert into the database - * - * @return inserted user badge - * @return the error of inserting user badge - */ - InsertUserBadgeTransaction(tx database.ITransaction, userBadge storages.UserBadgeDB) (*storages.UserBadgeDB, error) - - /** - * Update user point into the database by database transaction - * - * @param tx Transaction model to do database transaction - * @param userID User ID to update point - * @param point Point to change - * @param mode Mode to change user point - * - * @return the error of updating user point - */ - ChangePointTransaction(tx database.ITransaction, userID int, point int, mode entities.ChangePointMode) error -} diff --git a/repositories/repository.exam.go b/repositories/repository.exam.go deleted file mode 100644 index abd85bf..0000000 --- a/repositories/repository.exam.go +++ /dev/null @@ -1,179 +0,0 @@ -package repositories - -// repository.exam.go -/** - * This file is a part of repositories, used to do data manipulation of exam - */ - -import ( - "DatabaseCamp/database" - "DatabaseCamp/models/storages" - "fmt" -) - -/** - * This class manipulation exam data to other application - */ -type examRepository struct { - Database database.IDatabase // Database to do database manipulation -} - -/** - * Constructor creates a new examRepository instance - * - * @param db Database to data manipulation - * - * @return instance of examRepository - */ -func NewExamRepository(db database.IDatabase) examRepository { - return examRepository{Database: db} -} - -/** - * Get activities of the exam from the database - * - * @param examID Exam ID for getting exam data - * - * @return activities of the exam - * @return the error of getting exam - */ -func (r examRepository) GetExamActivity(examID int) ([]storages.ExamActivityDB, error) { - examActivity := make([]storages.ExamActivityDB, 0) - err := r.Database.GetDB(). - Table(storages.TableName.Exam). - Select( - storages.TableName.Exam+".exam_id AS exam_id", - storages.TableName.Exam+".type AS exam_type", - storages.TableName.Exam+".instruction AS instruction", - storages.TableName.Exam+".created_timestamp AS created_timestamp", - storages.TableName.Activity+".activity_id AS activity_id", - storages.TableName.Activity+".activity_type_id AS activity_type_id", - storages.TableName.Activity+".question AS question", - storages.TableName.Activity+".story AS story", - storages.TableName.Activity+".point AS point", - storages.TableName.MatchingChoice+".pair_item1 AS pair_item1", - storages.TableName.MatchingChoice+".pair_item2 AS pair_item2", - storages.TableName.CompletionChoice+".content AS completion_choice_content", - storages.TableName.CompletionChoice+".completion_choice_id AS completion_choice_id", - storages.TableName.CompletionChoice+".question_first AS question_first", - storages.TableName.CompletionChoice+".question_last AS question_last", - storages.TableName.MultipleChoice+".multiple_choice_id AS multiple_choice_id", - storages.TableName.MultipleChoice+".content AS multiple_choice_content", - storages.TableName.MultipleChoice+".is_correct AS is_correct", - storages.TableName.ContentGroup+".content_group_id AS content_group_id", - storages.TableName.ContentGroup+".name AS content_group_name", - storages.TableName.ContentGroup+".badge_id AS badge_id", - ). - Joins(fmt.Sprintf("LEFT JOIN %s ON %s.%s = %s.%s", - storages.TableName.ContentExam, - storages.TableName.ContentExam, - storages.IDName.Exam, - storages.TableName.Exam, - storages.IDName.Exam, - )). - Joins(fmt.Sprintf("LEFT JOIN %s ON %s.%s = %s.%s", - storages.TableName.Activity, - storages.TableName.ContentExam, - storages.IDName.Activity, - storages.TableName.Activity, - storages.IDName.Activity, - )). - Joins(fmt.Sprintf("LEFT JOIN %s ON %s.%s = %s.%s", - storages.TableName.MatchingChoice, - storages.TableName.MatchingChoice, - storages.IDName.Activity, - storages.TableName.Activity, - storages.IDName.Activity, - )). - Joins(fmt.Sprintf("LEFT JOIN %s ON %s.%s = %s.%s", - storages.TableName.MultipleChoice, - storages.TableName.MultipleChoice, - storages.IDName.Activity, - storages.TableName.Activity, - storages.IDName.Activity, - )). - Joins(fmt.Sprintf("LEFT JOIN %s ON %s.%s = %s.%s", - storages.TableName.CompletionChoice, - storages.TableName.CompletionChoice, - storages.IDName.Activity, - storages.TableName.Activity, - storages.IDName.Activity, - )). - Joins(fmt.Sprintf("LEFT JOIN %s ON %s.%s = %s.%s", - storages.TableName.ContentGroup, - storages.TableName.ContentGroup, - storages.IDName.MiniExam, - storages.TableName.Exam, - storages.IDName.Exam, - )). - Where(storages.TableName.Exam+"."+storages.IDName.Exam+" = ?", examID). - Find(&examActivity). - Error - return examActivity, err -} - -/** - * Get exam overview from the database - * - * @return all exam in the application - * @return the error of getting exam - */ -func (r examRepository) GetExamOverview() ([]storages.ExamDB, error) { - exam := make([]storages.ExamDB, 0) - err := r.Database.GetDB(). - Table(storages.TableName.Exam). - Select( - storages.TableName.Exam+".exam_id AS exam_id", - storages.TableName.Exam+".type AS type", - storages.TableName.Exam+".instruction AS instruction", - storages.TableName.Exam+".created_timestamp AS created_timestamp", - storages.TableName.ContentGroup+".content_group_id AS content_group_id", - storages.TableName.ContentGroup+".name AS content_group_name", - storages.TableName.ContentGroup+".badge_id AS badge_id", - ). - Joins(fmt.Sprintf("LEFT JOIN %s ON %s.%s = %s.%s", - storages.TableName.ContentGroup, - storages.TableName.ContentGroup, - storages.IDName.MiniExam, - storages.TableName.Exam, - storages.IDName.Exam, - )). - Find(&exam). - Error - return exam, err -} - -/** - * Insert exam result into the database by database transaction - * - * @param tx Transaction model to do database transaction - * @param examResult Exam result for insert into the database - * - * @return inserted exam result - * @return the error of inserting exam result - */ -func (r examRepository) InsertExamResultTransaction(tx database.ITransaction, examResult storages.ExamResultDB) (storages.ExamResultDB, error) { - err := tx.GetDB(). - Table(storages.TableName.ExamResult). - Create(&examResult). - Error - return examResult, err -} - -/** - * Insert activities of the exam result into the database by database transaction - * - * @param tx Transaction model to do database transaction - * @param examResultActivity Activities of the exam result for insert into the database - * - * @return inserted activities of the exam result - * @return the error of inserting activities of the exam result - */ -func (r examRepository) InsertExamResultActivityTransaction(tx database.ITransaction, examResultActivity []storages.ExamResultActivityDB) ([]storages.ExamResultActivityDB, error) { - err := tx.GetDB(). - Table(storages.TableName.ExamResultActivity). - Create(&examResultActivity). - Error - - return examResultActivity, err -} diff --git a/repositories/repository.learning.go b/repositories/repository.learning.go deleted file mode 100644 index 6c0a975..0000000 --- a/repositories/repository.learning.go +++ /dev/null @@ -1,231 +0,0 @@ -package repositories - -// repository.learning.go -/** - * This file is a part of repositories, used to do data manipulation of learning - */ - -import ( - "DatabaseCamp/database" - "DatabaseCamp/models/storages" - "DatabaseCamp/services" -) - -/** - * This class manipulation learning data to other application - */ -type learningRepository struct { - Database database.IDatabase // Database to do database manipulation - Service services.IStroageService // Service to do database manipulation -} - -/** - * Constructor creates a new learningRepository instance - * - * @param db Database to data manipulation - * @param service service to data manipulation - * - * @return instance of learningRepository - */ -func NewLearningRepository(db database.IDatabase, service services.IStroageService) learningRepository { - return learningRepository{Database: db, Service: service} -} - -/** - * Get learning content from the database - * - * @param contentID Content ID for getting content data - * - * @return learning content - * @return the error of getting content - */ -func (r learningRepository) GetContent(id int) (*storages.ContentDB, error) { - content := storages.ContentDB{} - err := r.Database.GetDB(). - Table(storages.TableName.Content). - Where(storages.IDName.Content+" = ?", id). - Find(&content). - Error - return &content, err -} - -/** - * Get content overview from the database - * - * @return all content in the application - * @return the error of getting content - */ -func (r learningRepository) GetOverview() ([]storages.OverviewDB, error) { - overview := make([]storages.OverviewDB, 0) - err := r.Database.GetDB(). - Table(storages.TableName.ContentGroup). - Select("ContentGroup.content_group_id AS content_group_id", - "Content.content_id AS content_id", - "Activity.activity_id AS activity_id", - "ContentGroup.name AS group_name", - "Content.name AS content_name", - ). - Joins("LEFT JOIN Content ON ContentGroup.content_group_id = Content.content_group_id"). - Joins("LEFT JOIN Activity ON Content.content_id = Activity.content_id"). - Order("content_group_id ASC"). - Find(&overview). - Error - return overview, err -} - -/** - * Get content of the exam from the database - * - * @param examType Type of the exam for getting content of the exam - * - * @return all content of the exam in the application - * @return the error of getting content - */ -func (r learningRepository) GetContentExam(examType string) ([]storages.ContentExamDB, error) { - contentExam := make([]storages.ContentExamDB, 0) - db := r.Database.GetDB() - examSubquery := db.Table(storages.TableName.Exam). - Select("exam_id"). - Where("type = ?", string(examType)). - Order("created_timestamp desc"). - Limit(1) - err := r.Database.GetDB(). - Table(storages.TableName.ContentExam). - Where("exam_id = (?)", examSubquery). - Find(&contentExam). - Error - return contentExam, err -} - -/** - * Get activities of the content from the database - * - * @param contentID Content ID for getting activities of the content - * - * @return activities of the content - * @return the error of getting activities - */ -func (r learningRepository) GetContentActivity(contentID int) ([]storages.ActivityDB, error) { - activity := make([]storages.ActivityDB, 0) - - err := r.Database.GetDB(). - Table(storages.TableName.Activity). - Where(storages.IDName.Content+" = ?", contentID). - Find(&activity). - Error - - return activity, err -} - -/** - * Get activity from the database - * - * @param id Activity ID for getting activity data - * - * @return activity data - * @return the error of getting activity - */ -func (r learningRepository) GetActivity(id int) (*storages.ActivityDB, error) { - activity := storages.ActivityDB{} - - err := r.Database.GetDB(). - Table(storages.TableName.Activity). - Where(storages.IDName.Activity+" = ?", id). - Find(&activity). - Error - - return &activity, err -} - -/** - * Get matching choices of the activity from the database - * - * @param activityID Activity ID for getting matching choices of the activity - * - * @return matching choice of the activity - * @return the error of getting choices - */ -func (r learningRepository) GetMatchingChoice(activityID int) ([]storages.MatchingChoiceDB, error) { - matchingChoice := make([]storages.MatchingChoiceDB, 0) - - err := r.Database.GetDB(). - Table(storages.TableName.MatchingChoice). - Where(storages.IDName.Activity+" = ?", activityID). - Find(&matchingChoice). - Error - - return matchingChoice, err -} - -/** - * Get multiple choices of the activity from the database - * - * @param activityID Activity ID for getting multiple choices of the activity - * - * @return multiple choice of the activity - * @return the error of getting choices - */ -func (r learningRepository) GetMultipleChoice(activityID int) ([]storages.MultipleChoiceDB, error) { - multipleChoice := make([]storages.MultipleChoiceDB, 0) - - err := r.Database.GetDB(). - Table(storages.TableName.MultipleChoice). - Where(storages.IDName.Activity+" = ?", activityID). - Find(&multipleChoice). - Error - - return multipleChoice, err -} - -/** - * Get completion choices of the activity from the database - * - * @param activityID Activity ID for getting completion choices of the activity - * - * @return completion choice of the activity - * @return the error of getting choices - */ -func (r learningRepository) GetCompletionChoice(activityID int) ([]storages.CompletionChoiceDB, error) { - completionChoice := make([]storages.CompletionChoiceDB, 0) - - err := r.Database.GetDB(). - Table(storages.TableName.CompletionChoice). - Where(storages.IDName.Activity+" = ?", activityID). - Find(&completionChoice). - Error - - return completionChoice, err -} - -/** - * Get hints of the activity from the database - * - * @param activityID Activity ID for getting hints of the activity - * - * @return hints of the activity - * @return the error of getting hints - */ -func (r learningRepository) GetActivityHints(activityID int) ([]storages.HintDB, error) { - hints := make([]storages.HintDB, 0) - - err := r.Database.GetDB(). - Table(storages.TableName.Hint). - Where(storages.IDName.Activity+" = ?", activityID). - Order("level ASC"). - Find(&hints). - Error - - return hints, err -} - -/** - * Get video file link from the Stroage service - * - * @param objectName object name in the storage - * - * @return file link - * @return the error of getting file link - */ -func (r learningRepository) GetVideoFileLink(objectName string) (string, error) { - return r.Service.GetFileLink(objectName) -} diff --git a/repositories/repository.user.go b/repositories/repository.user.go deleted file mode 100644 index c194581..0000000 --- a/repositories/repository.user.go +++ /dev/null @@ -1,427 +0,0 @@ -package repositories - -// repository.user.go -/** - * This file is a part of repositories, used to do data manipulation of user - */ - -import ( - "DatabaseCamp/database" - "DatabaseCamp/models/entities" - "DatabaseCamp/models/storages" - "DatabaseCamp/utils" - "fmt" -) - -/** - * This class manipulation user data to other application - */ -type userRepository struct { - Database database.IDatabase // Database to do database manipulation -} - -/** - * Constructor creates a new userRepository instance - * - * @param db Database to data manipulation - * - * @return instance of userRepository - */ -func NewUserRepository(db database.IDatabase) userRepository { - return userRepository{Database: db} -} - -/** - * Get user by email from the database - * - * @param email Email for getting user data - * - * @return user data - * @return the error of getting user data - */ -func (r userRepository) GetUserByEmail(email string) (*storages.UserDB, error) { - user := storages.UserDB{} - err := r.Database.GetDB(). - Table(storages.TableName.User). - Where("email = ?", email). - Find(&user). - Error - return &user, err -} - -/** - * Get user by id from the database - * - * @param id User ID for getting user data - * - * @return user data - * @return the error of getting user data - */ -func (r userRepository) GetUserByID(id int) (*storages.UserDB, error) { - user := storages.UserDB{} - err := r.Database.GetDB(). - Table(storages.TableName.User). - Where(storages.IDName.User+" = ?", id). - Find(&user). - Error - return &user, err -} - -/** - * Get user profile from the database - * - * @param id User ID for getting user profile - * - * @return user profile - * @return the error of getting user profile - */ -func (r userRepository) GetProfile(id int) (*storages.ProfileDB, error) { - profile := storages.ProfileDB{} - err := r.Database.GetDB(). - Table(storages.ViewName.Profile). - Where(storages.IDName.User+" = ?", id). - Find(&profile). - Error - if profile == (storages.ProfileDB{}) { - return nil, nil - } - return &profile, err -} - -/** - * Get learning progression of the user from the database - * - * @param id User ID for getting learning progression of the user - * - * @return learning progression of the user - * @return the error of getting learning progression of the user - */ -func (r userRepository) GetLearningProgression(id int) ([]storages.LearningProgressionDB, error) { - learningProgrogression := make([]storages.LearningProgressionDB, 0) - err := r.Database.GetDB(). - Table(storages.TableName.LearningProgression). - Where(storages.IDName.User+" = ?", id). - Order("created_timestamp desc"). - Find(&learningProgrogression). - Error - return learningProgrogression, err -} - -/** - * Get all badges of the application from the database - * - * @return all badges of the application - * @return the error of getting all badges of the application - */ -func (r userRepository) GetAllBadge() ([]storages.BadgeDB, error) { - badge := make([]storages.BadgeDB, 0) - err := r.Database.GetDB(). - Table(storages.TableName.Badge). - Find(&badge). - Error - return badge, err -} - -/** - * Get user badge data by user badge id from the database - * - * @param id User Badge ID for getting user badge data - * - * @return user badge data - * @return the error of getting user badge data - */ -func (r userRepository) GetUserBadge(id int) ([]storages.UserBadgeDB, error) { - userBadgeDB := make([]storages.UserBadgeDB, 0) - err := r.Database.GetDB(). - Table(storages.TableName.UserBadge). - Where(storages.IDName.User+" = ?", id). - Find(&userBadgeDB). - Error - return userBadgeDB, err -} - -/** - * Get collected badges of the user from the database - * - * @param userID User ID for getting collected badges of the user - * - * @return collected badges of the user - * @return the error of getting collected badges of the user - */ -func (r userRepository) GetCollectedBadge(userID int) ([]storages.CorrectedBadgeDB, error) { - correctedBadge := make([]storages.CorrectedBadgeDB, 0) - err := r.Database.GetDB(). - Table(storages.TableName.Badge). - Select( - storages.TableName.Badge+".badge_id AS badge_id", - storages.TableName.Badge+".name AS badge_name", - storages.TableName.UserBadge+".user_id AS user_id", - ). - Joins(fmt.Sprintf("LEFT JOIN %s ON %s.%s = %s.%s AND %s.%s = %d", - storages.TableName.UserBadge, - storages.TableName.UserBadge, - storages.IDName.Badge, - storages.TableName.Badge, - storages.IDName.Badge, - storages.TableName.UserBadge, - storages.IDName.User, - userID, - )). - Find(&correctedBadge). - Error - return correctedBadge, err -} - -/** - * Get point ranking of the user from the database - * - * @param id User ID for getting point ranking of the user - * - * @return point ranking of the user - * @return the error of getting point ranking of the user - */ -func (r userRepository) GetPointRanking(id int) (*storages.RankingDB, error) { - ranking := storages.RankingDB{} - err := r.Database.GetDB(). - Table(storages.ViewName.Ranking). - Where(storages.IDName.User+" = ?", id). - Find(&ranking). - Error - return &ranking, err -} - -/** - * Get all user point ranking from the database - * - * @return all user point ranking - * @return the error of getting all user point ranking - */ -func (r userRepository) GetRankingLeaderBoard() ([]storages.RankingDB, error) { - ranking := make([]storages.RankingDB, 0) - err := r.Database.GetDB(). - Table(storages.ViewName.Ranking). - Limit(20). - Find(&ranking). - Error - return ranking, err -} - -/** - * Get User Hint data from the database - * - * @param userID User ID for getting User Hint data - * @param activityID Activity ID for getting User Hint data - * - * @return User Hint data - * @return the error of getting User Hint data - */ -func (r userRepository) GetUserHint(userID int, activityID int) ([]storages.UserHintDB, error) { - userhint := make([]storages.UserHintDB, 0) - - hintSubquery := r.Database.GetDB(). - Select("hint_id"). - Table(storages.TableName.Hint). - Where(storages.IDName.Activity+" = ?", activityID) - - err := r.Database.GetDB(). - Table(storages.TableName.UserHint). - Where(storages.IDName.Hint+" IN (?)", hintSubquery). - Where(storages.IDName.User+" = ?", userID). - Find(&userhint). - Error - - return userhint, err -} - -/** - * Get exam results of the user from the database - * - * @param userID User ID for getting exam results of the user - * - * @return exam results of the user - * @return the error of getting exam results of the user - */ -func (r userRepository) GetExamResult(userID int) ([]storages.ExamResultDB, error) { - examResults := make([]storages.ExamResultDB, 0) - err := r.Database.GetDB(). - Table(storages.TableName.ExamResult). - Select( - storages.TableName.ExamResult+".exam_result_id AS exam_result_id", - storages.TableName.ExamResult+".exam_id AS exam_id", - storages.TableName.ExamResult+".user_id AS user_id", - storages.TableName.ExamResult+".is_passed AS is_passed", - storages.TableName.ExamResult+".created_timestamp AS created_timestamp", - - storages.TableName.ExamResultActivity+".score AS score", - ). - Joins(fmt.Sprintf("LEFT JOIN %s ON %s.%s = %s.%s", - storages.TableName.ExamResultActivity, - storages.TableName.ExamResultActivity, - storages.IDName.ExamResult, - storages.TableName.ExamResult, - storages.IDName.ExamResult, - )). - Where(storages.IDName.User+" = ?", userID). - Find(&examResults). - Error - return examResults, err -} - -/** - * Get exam results of the user by exam result id from the database - * - * @param userID User ID for getting exam results of the user - * - * @return exam results of the user - * @return the error of getting exam results of the user - */ -func (r userRepository) GetExamResultByID(userID int, examResultID int) ([]storages.ExamResultDB, error) { - examResults := make([]storages.ExamResultDB, 0) - err := r.Database.GetDB(). - Table(storages.TableName.ExamResult). - Select( - storages.TableName.ExamResult+".exam_result_id AS exam_result_id", - storages.TableName.ExamResult+".exam_id AS exam_id", - storages.TableName.ExamResult+".user_id AS user_id", - storages.TableName.ExamResult+".is_passed AS is_passed", - storages.TableName.ExamResult+".created_timestamp AS created_timestamp", - storages.TableName.ExamResultActivity+".score AS score", - ). - Joins(fmt.Sprintf("LEFT JOIN %s ON %s.%s = %s.%s", - storages.TableName.ExamResultActivity, - storages.TableName.ExamResultActivity, - storages.IDName.ExamResult, - storages.TableName.ExamResult, - storages.IDName.ExamResult, - )). - Where(storages.IDName.User+" = ?", userID). - Where(storages.TableName.ExamResult+"."+storages.IDName.ExamResult+" = ?", examResultID). - Find(&examResults). - Error - return examResults, err -} - -/** - * Insert user data into the database - * - * @param user User model for insert into the database - * - * @return inserted user - * @return the error of inserting user - */ -func (r userRepository) InsertUser(user storages.UserDB) (*storages.UserDB, error) { - err := r.Database.GetDB(). - Table(storages.TableName.User). - Create(&user). - Error - return &user, err -} - -/** - * Insert user hint into the database - * - * @param userHint User hint model for insert into the database - * - * @return inserted user hint - * @return the error of inserting user hint - */ -func (r userRepository) InsertUserHint(userHint storages.UserHintDB) (*storages.UserHintDB, error) { - err := r.Database.GetDB(). - Table(storages.TableName.UserHint). - Create(&userHint). - Error - return &userHint, err -} - -/** - * Update user data into the database - * - * @param id User id to update into the database - * @param updateData User data to update into the database - * - * @return inserted user hint - * @return the error of updating user data - */ -func (r userRepository) UpdatesByID(id int, updateData map[string]interface{}) error { - err := r.Database.GetDB(). - Table(storages.TableName.User). - Select("", utils.NewHelper().GetKeyList(updateData)). - Where(storages.IDName.User+" = ?", id). - Updates(updateData). - Error - return err -} - -/** - * Insert user hint into the database by database transaction - * - * @param tx Transaction model to do database transaction - * @param userHint User hint model for insert into the database - * - * @return inserted user hint - * @return the error of inserting user hint - */ -func (r userRepository) InsertUserHintTransaction(tx database.ITransaction, userHint storages.UserHintDB) (*storages.UserHintDB, error) { - err := tx.GetDB(). - Table(storages.TableName.UserHint). - Create(&userHint). - Error - return &userHint, err -} - -/** - * Insert learning progression of the user into the database by database transaction - * - * @param tx Transaction model to do database transaction - * @param progression Learning progession model for insert into the database - * - * @return inserted learning progression - * @return the error of inserting learning progression - */ -func (r userRepository) InsertLearningProgressionTransaction(tx database.ITransaction, progression storages.LearningProgressionDB) (*storages.LearningProgressionDB, error) { - err := tx.GetDB(). - Table(storages.TableName.LearningProgression). - Create(&progression). - Error - return &progression, err -} - -/** - * Insert user badge into the database by database transaction - * - * @param tx Transaction model to do database transaction - * @param userBadge User badge model for insert into the database - * - * @return inserted user badge - * @return the error of inserting user badge - */ -func (r userRepository) InsertUserBadgeTransaction(tx database.ITransaction, userBadge storages.UserBadgeDB) (*storages.UserBadgeDB, error) { - err := tx.GetDB(). - Table(storages.TableName.UserBadge). - Create(&userBadge). - Error - return &userBadge, err -} - -/** - * Update user point into the database by database transaction - * - * @param tx Transaction model to do database transaction - * @param userID User ID to update point - * @param point Point to change - * @param mode Mode to change user point - * - * @return the error of updating user point - */ -func (r userRepository) ChangePointTransaction(tx database.ITransaction, userID int, point int, mode entities.ChangePointMode) error { - statement := fmt.Sprintf("UPDATE %s SET point = point %s %d WHERE %s = %d", - storages.TableName.User, - mode, - point, - storages.IDName.User, - userID, - ) - temp := map[string]interface{}{} - err := tx.GetDB().Raw(statement).Find(&temp).Error - return err -} diff --git a/router/router.go b/router/router.go deleted file mode 100644 index 5e832d9..0000000 --- a/router/router.go +++ /dev/null @@ -1,118 +0,0 @@ -package router - -// router.go -/** - * This file used to define path of the API - */ - -import ( - "DatabaseCamp/handlers" - "DatabaseCamp/middleware" - - "github.com/gofiber/fiber/v2" -) - -/** - * This class manage API route of the application - */ -type router struct { - App *fiber.App // Web framework application - Router fiber.Router // Router of the web framework application - - ExamHandler handlers.IExamHandler // Exam handler for handle exam requests - LearningHandler handlers.ILearningHandler // Learning handler for handle learning requests - UserHandler handlers.IUserHandler // User handler for handle user requests - - Jwt middleware.IJwt // JWT middleware for verification -} - -// Instance of router class for singleton pattern -var instantiated *router = nil - -/** - * Constructor creates a new router instance or geting a router instance - * - * @param app Web framework application - * @param examHandler Exam handler for handle exam requests - * @param learningHandler Learning handler for handle learning requests - * @param userHandler User handler for handle user requests - * @param jwt JWT middleware for verification - * - * @return instance of router - */ -func New( - app *fiber.App, - examHandler handlers.IExamHandler, - learningHandler handlers.ILearningHandler, - userHandler handlers.IUserHandler, - jwt middleware.IJwt, -) *router { - if instantiated == nil { - instantiated = &router{ - App: app, - Router: app.Group("api/v1"), - ExamHandler: examHandler, - LearningHandler: learningHandler, - UserHandler: userHandler, - Jwt: jwt, - } - instantiated.init() - } - return instantiated -} - -/** - * Setup route path for each module - */ -func (r *router) init() { - r.setupLearning() - r.setupUser() - r.setupExam() -} - -/** - * Setup route path for exam module - */ -func (r *router) setupExam() { - group := r.Router.Group("exam") - group.Use(r.Jwt.JwtVerify) - { - group.Get("/proposition/:id", r.ExamHandler.GetExam) - group.Get("/overview", r.ExamHandler.GetExamOverview) - group.Get("/result/:id", r.ExamHandler.GetExamResult) - group.Post("/check", r.ExamHandler.CheckExam) - } -} - -/** - * Setup route path for learning module - */ -func (r *router) setupLearning() { - group := r.Router.Group("learning") - group.Use(r.Jwt.JwtVerify) - { - group.Get("/video/:id", r.LearningHandler.GetVideo) - group.Get("/overview", r.LearningHandler.GetOverview) - group.Get("/content/roadmap/:id", r.LearningHandler.GetContentRoadmap) - group.Get("/activity/:id", r.LearningHandler.GetActivity) - group.Post("/activity/hint/:id", r.LearningHandler.UseHint) - group.Post("/activity/matching/check-answer", r.LearningHandler.CheckMatchingAnswer) - group.Post("/activity/multiple/check-answer", r.LearningHandler.CheckMultipleAnswer) - group.Post("/activity/completion/check-answer", r.LearningHandler.CheckCompletionAnswer) - } -} - -/** - * Setup route path for user module - */ -func (r *router) setupUser() { - group := r.Router.Group("user") - { - group.Get("/info", r.Jwt.JwtVerify, r.UserHandler.GetOwnProfile) - group.Get("/profile/:id", r.Jwt.JwtVerify, r.UserHandler.GetProfile) - group.Get("/ranking", r.Jwt.JwtVerify, r.UserHandler.GetUserRanking) - group.Put("/profile", r.Jwt.JwtVerify, r.UserHandler.Edit) - group.Post("/register", r.UserHandler.Register) - group.Post("/login", r.UserHandler.Login) - } -} diff --git a/server.go b/server.go deleted file mode 100644 index 1485b67..0000000 --- a/server.go +++ /dev/null @@ -1,135 +0,0 @@ -package main - -// server.go -/** - * This file used to run server - */ - -import ( - "DatabaseCamp/controllers" - "DatabaseCamp/database" - "DatabaseCamp/handlers" - "DatabaseCamp/logs" - "DatabaseCamp/middleware" - "DatabaseCamp/repositories" - "DatabaseCamp/router" - "DatabaseCamp/services" - "os" - "time" - - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/cors" - "github.com/gofiber/fiber/v2/middleware/recover" - "github.com/joho/godotenv" -) - -/** - * Set up server time zone - * - * @return the error of set up server time zone - */ -func setupTimeZone() error { - location, err := time.LoadLocation("Asia/Bangkok") - if err != nil { - return err - } - time.Local = location - return nil -} - -/** - * Config Web Server - * - * @return config of the web server - */ -func getConfig() fiber.Config { - return fiber.Config{ - Prefork: false, - CaseSensitive: true, - StrictRouting: true, - ServerHeader: "Fiber", - AppName: "Database Camp", - } -} - -/** - * Set up web framework - * - * @return the error of set up web framework - */ -func setupFiber() error { - - // Create application - app := fiber.New(getConfig()) - - // Use middleware - app.Use(cors.New()) - app.Use(recover.New()) - - // Create database - db := database.New() - - // Create Storage Service - service := services.GetCloudStorageServiceInstance() - - // Create Repository - userRepo := repositories.NewUserRepository(db) - learningRepo := repositories.NewLearningRepository(db, service) - examRepo := repositories.NewExamRepository(db) - - // Create Middleware - jwt := middleware.NewJwtMiddleware(userRepo) - - // Create Controller - learningController := controllers.NewLearningController(learningRepo, userRepo) - examController := controllers.NewExamController(examRepo, userRepo) - userController := controllers.NewUserController(userRepo) - - // Create Handler - learningHandler := handlers.NewLearningHandler(learningController) - examHandler := handlers.NewExamHandler(examController) - userHandler := handlers.NewUserHandler(userController, jwt) - - // Create router - router.New(app, examHandler, learningHandler, userHandler, jwt) - - // Running application - err := app.Listen(":" + os.Getenv("PORT")) - return err -} - -/** - * Main function of the application - */ -func main() { - - // Load environment variables - err := godotenv.Load() - if err != nil { - logs.New().Error(err) - return - } - - // Setup time zone - err = setupTimeZone() - if err != nil { - logs.New().Error(err) - return - } - - // Setup database - db := database.New() - err = db.OpenConnection() - if err != nil { - logs.New().Error(err) - return - } - defer db.CloseDB() - - // Setup web framework - err = setupFiber() - if err != nil { - logs.New().Error(err) - return - } -} diff --git a/services/interface.go b/services/interface.go deleted file mode 100644 index e4b64a6..0000000 --- a/services/interface.go +++ /dev/null @@ -1,22 +0,0 @@ -package services - -// interface.go -/** - * This file used to be a interface of services - */ - -/** - * Interface to show function in stroage service that others can use - */ -type IStroageService interface { - - /** - * Get signed File link from stroage service - * - * @param imagekey object key for getting file link - * - * @return file link - * @return the error of getting file link - */ - GetFileLink(objectName string) (string, error) -} diff --git a/services/service.aws.go b/services/service.aws.go deleted file mode 100644 index 8951421..0000000 --- a/services/service.aws.go +++ /dev/null @@ -1,98 +0,0 @@ -package services - -// service.aws.go -/** - * This file is a part of services, used to connect to the Amason S3 - */ - -import ( - "os" - "time" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/s3" -) - -/** - * This class manage connection to Amazon S3 - */ -type awsService struct { - Sess *session.Session -} - -// Instance of awsService class for singleton pattern -var awsInstantiated *awsService = nil - -/** - * Constructor creates a new awsService instance - * - * @return instance of awsService - */ -func GetAwsServiceInstance() *awsService { - if awsInstantiated == nil { - sess, _ := session.NewSession(getConfig()) - awsInstantiated = &awsService{Sess: sess} - } - return awsInstantiated -} - -/** - * Get credential of the amazon s3 - * - * @return credential of the amazon s3 - */ -func getCredentials() *credentials.Credentials { - accessKeyID := os.Getenv("AWS_ACCESS_KEY_ID") - secretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY") - return credentials.NewStaticCredentials( - accessKeyID, - secretAccessKey, - "", - ) -} - -/** - * Get config of the amazon s3 - * - * @return config of the amazon s3 - */ -func getConfig() *aws.Config { - region := os.Getenv("AWS_REGION") - return &aws.Config{ - Region: aws.String(region), - Credentials: getCredentials(), - } -} - -/** - * Get object Input of the amazon s3 - * - * @param imagekey object key for getting file link - * - * @return object Input of the amazon s3 - */ -func (s awsService) getObjectInput(imageKey string) *s3.GetObjectInput { - bucket := os.Getenv("AWS_BUCKET_NAME") - return &s3.GetObjectInput{ - Bucket: aws.String(bucket), - Key: aws.String(imageKey), - } -} - -/** - * Get File link from amazon s3 - * - * @param imagekey object key for getting file link - * - * @return file link - * @return the error of getting file link - */ -func (s awsService) GetFileLink(objectName string) (string, error) { - svc := s3.New(s.Sess) - req, _ := svc.GetObjectRequest(s.getObjectInput(objectName)) - expireTime := 60 * time.Minute - url, err := req.Presign(expireTime) - return url, err -} diff --git a/sql/database_camp.sql b/sql/database_camp.sql index 935b897..b914183 100644 --- a/sql/database_camp.sql +++ b/sql/database_camp.sql @@ -1,481 +1,481 @@ --- phpMyAdmin SQL Dump --- version 5.1.1 --- https://www.phpmyadmin.net/ --- --- Host: db --- Generation Time: Dec 05, 2021 at 02:39 PM --- Server version: 10.6.4-MariaDB-1:10.6.4+maria~focal --- PHP Version: 7.4.20 - -SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; -START TRANSACTION; -SET time_zone = "+00:00"; - - -/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; -/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; -/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; -/*!40101 SET NAMES utf8mb4 */; - --- --- Database: `database_camp` --- - --- -------------------------------------------------------- - --- --- Table structure for table `Activity` --- - -CREATE TABLE `Activity` ( - `activity_id` int(11) NOT NULL, - `activity_type_id` int(11) NOT NULL, - `content_id` int(11) DEFAULT NULL, - `point` int(11) NOT NULL, - `question` text NOT NULL, - `story` text NOT NULL, - `activity_order` int(11) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- -------------------------------------------------------- - --- --- Table structure for table `ActivityType` --- - -CREATE TABLE `ActivityType` ( - `activity_type_id` int(11) NOT NULL, - `name` varchar(20) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- -------------------------------------------------------- - --- --- Table structure for table `Badge` --- - -CREATE TABLE `Badge` ( - `badge_id` int(11) NOT NULL, - `name` varchar(50) NOT NULL, - `icon_path` text NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- -------------------------------------------------------- - --- --- Table structure for table `CompletionChoice` --- - -CREATE TABLE `CompletionChoice` ( - `completion_choice_id` int(11) NOT NULL, - `activity_id` int(11) NOT NULL, - `content` text NOT NULL, - `question_first` text NOT NULL, - `question_last` text NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- -------------------------------------------------------- - --- --- Table structure for table `Content` --- - -CREATE TABLE `Content` ( - `content_id` int(11) NOT NULL, - `content_group_id` int(11) NOT NULL, - `name` varchar(50) NOT NULL, - `video_path` text NOT NULL, - `slide_path` text NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- -------------------------------------------------------- - --- --- Table structure for table `ContentExam` --- - -CREATE TABLE `ContentExam` ( - `exam_id` int(11) NOT NULL, - `content_group_id` int(11) NOT NULL, - `activity_id` int(11) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- -------------------------------------------------------- - --- --- Table structure for table `ContentGroup` --- - -CREATE TABLE `ContentGroup` ( - `content_group_id` int(11) NOT NULL, - `name` varchar(50) NOT NULL, - `badge_id` int(11) NOT NULL, - `mini_exam_id` int(11) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- -------------------------------------------------------- - --- --- Table structure for table `Exam` --- - -CREATE TABLE `Exam` ( - `exam_id` int(11) NOT NULL, - `type` enum('PRE','MINI','POST') NOT NULL, - `instruction` text NOT NULL, - `created_timestamp` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- -------------------------------------------------------- - --- --- Table structure for table `ExamResult` --- - -CREATE TABLE `ExamResult` ( - `exam_result_id` int(11) NOT NULL, - `user_id` int(11) NOT NULL, - `exam_id` int(11) NOT NULL, - `is_passed` tinyint(1) NOT NULL, - `created_timestamp` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- -------------------------------------------------------- - --- --- Table structure for table `ExamResultActivity` --- - -CREATE TABLE `ExamResultActivity` ( - `exam_result_id` int(11) NOT NULL, - `activity_id` int(11) NOT NULL, - `score` int(11) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- -------------------------------------------------------- - --- --- Table structure for table `Hint` --- - -CREATE TABLE `Hint` ( - `hint_id` int(11) NOT NULL, - `activity_id` int(11) NOT NULL, - `content` text NOT NULL, - `point_reduce` int(11) NOT NULL, - `level` int(11) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- -------------------------------------------------------- - --- --- Table structure for table `LearningProgression` --- - -CREATE TABLE `LearningProgression` ( - `user_id` int(11) NOT NULL, - `activity_id` int(11) NOT NULL, - `created_timestamp` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- -------------------------------------------------------- - --- --- Table structure for table `MatchingChoice` --- - -CREATE TABLE `MatchingChoice` ( - `matching_choice_id` int(11) NOT NULL, - `activity_id` int(11) NOT NULL, - `pair_item1` varchar(50) NOT NULL, - `pair_item2` varchar(50) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- -------------------------------------------------------- - --- --- Table structure for table `MultipleChoice` --- - -CREATE TABLE `MultipleChoice` ( - `multiple_choice_id` int(11) NOT NULL, - `activity_id` int(11) NOT NULL, - `content` text NOT NULL, - `is_correct` tinyint(4) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- -------------------------------------------------------- - --- --- Stand-in structure for view `Profile` --- (See below for the actual view) --- -CREATE TABLE `Profile` ( -`user_id` int(11) -,`name` varchar(100) -,`point` int(11) -,`created_timestamp` timestamp -,`activity_count` bigint(21) -); - --- -------------------------------------------------------- - --- --- Stand-in structure for view `Ranking` --- (See below for the actual view) --- -CREATE TABLE `Ranking` ( -`user_id` int(11) -,`name` varchar(100) -,`email` varchar(50) -,`point` int(11) -,`ranking` bigint(21) -); - --- -------------------------------------------------------- - --- --- Table structure for table `User` --- - -CREATE TABLE `User` ( - `user_id` int(11) NOT NULL, - `name` varchar(100) NOT NULL, - `email` varchar(50) NOT NULL, - `password` text NOT NULL, - `access_token` text NOT NULL, - `point` int(11) NOT NULL, - `expired_token_timestamp` timestamp NULL DEFAULT NULL, - `created_timestamp` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', - `updated_timestamp` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- -------------------------------------------------------- - --- --- Table structure for table `UserBadge` --- - -CREATE TABLE `UserBadge` ( - `user_id` int(11) NOT NULL, - `badge_id` int(11) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- -------------------------------------------------------- - --- --- Table structure for table `UserHint` --- - -CREATE TABLE `UserHint` ( - `user_id` int(11) NOT NULL, - `hint_id` int(11) NOT NULL, - `created_timestamp` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- -------------------------------------------------------- - --- --- Structure for view `Profile` --- -DROP TABLE IF EXISTS `Profile`; - -CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`%` SQL SECURITY DEFINER VIEW `Profile` AS SELECT `User`.`user_id` AS `user_id`, `User`.`name` AS `name`, `User`.`point` AS `point`, `User`.`created_timestamp` AS `created_timestamp`, count(`LearningProgression`.`activity_id`) AS `activity_count` FROM (`User` left join `LearningProgression` on(`User`.`user_id` = `LearningProgression`.`user_id`)) GROUP BY `User`.`user_id` ; - --- -------------------------------------------------------- - --- --- Structure for view `Ranking` --- -DROP TABLE IF EXISTS `Ranking`; - -CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`%` SQL SECURITY DEFINER VIEW `Ranking` AS SELECT `User`.`user_id` AS `user_id`, `User`.`name` AS `name`, `User`.`email` AS `email`, `User`.`point` AS `point`, row_number() ( order by `User`.`point` desc) AS `over` FROM `User` WHERE 1 ; - --- --- Indexes for dumped tables --- - --- --- Indexes for table `Activity` --- -ALTER TABLE `Activity` - ADD PRIMARY KEY (`activity_id`), - ADD UNIQUE KEY `content_id` (`content_id`,`activity_order`); - --- --- Indexes for table `ActivityType` --- -ALTER TABLE `ActivityType` - ADD PRIMARY KEY (`activity_type_id`); - --- --- Indexes for table `Badge` --- -ALTER TABLE `Badge` - ADD PRIMARY KEY (`badge_id`); - --- --- Indexes for table `CompletionChoice` --- -ALTER TABLE `CompletionChoice` - ADD PRIMARY KEY (`completion_choice_id`); - --- --- Indexes for table `Content` --- -ALTER TABLE `Content` - ADD PRIMARY KEY (`content_id`); - --- --- Indexes for table `ContentExam` --- -ALTER TABLE `ContentExam` - ADD PRIMARY KEY (`content_group_id`,`activity_id`,`exam_id`); - --- --- Indexes for table `ContentGroup` --- -ALTER TABLE `ContentGroup` - ADD PRIMARY KEY (`content_group_id`); - --- --- Indexes for table `Exam` --- -ALTER TABLE `Exam` - ADD PRIMARY KEY (`exam_id`); - --- --- Indexes for table `ExamResult` --- -ALTER TABLE `ExamResult` - ADD PRIMARY KEY (`exam_result_id`); - --- --- Indexes for table `ExamResultActivity` --- -ALTER TABLE `ExamResultActivity` - ADD PRIMARY KEY (`exam_result_id`,`activity_id`); - --- --- Indexes for table `Hint` --- -ALTER TABLE `Hint` - ADD PRIMARY KEY (`hint_id`); - --- --- Indexes for table `LearningProgression` --- -ALTER TABLE `LearningProgression` - ADD PRIMARY KEY (`user_id`,`activity_id`); - --- --- Indexes for table `MatchingChoice` --- -ALTER TABLE `MatchingChoice` - ADD PRIMARY KEY (`matching_choice_id`); - --- --- Indexes for table `MultipleChoice` --- -ALTER TABLE `MultipleChoice` - ADD PRIMARY KEY (`multiple_choice_id`); - --- --- Indexes for table `User` --- -ALTER TABLE `User` - ADD PRIMARY KEY (`user_id`), - ADD UNIQUE KEY `email` (`email`); - --- --- Indexes for table `UserBadge` --- -ALTER TABLE `UserBadge` - ADD PRIMARY KEY (`user_id`,`badge_id`); - --- --- Indexes for table `UserHint` --- -ALTER TABLE `UserHint` - ADD PRIMARY KEY (`user_id`,`hint_id`); - --- --- AUTO_INCREMENT for dumped tables --- - --- --- AUTO_INCREMENT for table `Activity` --- -ALTER TABLE `Activity` - MODIFY `activity_id` int(11) NOT NULL AUTO_INCREMENT; - --- --- AUTO_INCREMENT for table `ActivityType` --- -ALTER TABLE `ActivityType` - MODIFY `activity_type_id` int(11) NOT NULL AUTO_INCREMENT; - --- --- AUTO_INCREMENT for table `Badge` --- -ALTER TABLE `Badge` - MODIFY `badge_id` int(11) NOT NULL AUTO_INCREMENT; - --- --- AUTO_INCREMENT for table `CompletionChoice` --- -ALTER TABLE `CompletionChoice` - MODIFY `completion_choice_id` int(11) NOT NULL AUTO_INCREMENT; - --- --- AUTO_INCREMENT for table `Content` --- -ALTER TABLE `Content` - MODIFY `content_id` int(11) NOT NULL AUTO_INCREMENT; - --- --- AUTO_INCREMENT for table `ContentGroup` --- -ALTER TABLE `ContentGroup` - MODIFY `content_group_id` int(11) NOT NULL AUTO_INCREMENT; - --- --- AUTO_INCREMENT for table `Exam` --- -ALTER TABLE `Exam` - MODIFY `exam_id` int(11) NOT NULL AUTO_INCREMENT; - --- --- AUTO_INCREMENT for table `ExamResult` --- -ALTER TABLE `ExamResult` - MODIFY `exam_result_id` int(11) NOT NULL AUTO_INCREMENT; - --- --- AUTO_INCREMENT for table `Hint` --- -ALTER TABLE `Hint` - MODIFY `hint_id` int(11) NOT NULL AUTO_INCREMENT; - --- --- AUTO_INCREMENT for table `MatchingChoice` --- -ALTER TABLE `MatchingChoice` - MODIFY `matching_choice_id` int(11) NOT NULL AUTO_INCREMENT; - --- --- AUTO_INCREMENT for table `MultipleChoice` --- -ALTER TABLE `MultipleChoice` - MODIFY `multiple_choice_id` int(11) NOT NULL AUTO_INCREMENT; - --- --- AUTO_INCREMENT for table `User` --- -ALTER TABLE `User` - MODIFY `user_id` int(11) NOT NULL AUTO_INCREMENT; -COMMIT; - -/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; -/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; -/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +-- phpMyAdmin SQL Dump +-- version 5.1.1 +-- https://www.phpmyadmin.net/ +-- +-- Host: db +-- Generation Time: Dec 05, 2021 at 02:39 PM +-- Server version: 10.6.4-MariaDB-1:10.6.4+maria~focal +-- PHP Version: 7.4.20 + +SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; +START TRANSACTION; +SET time_zone = "+00:00"; + + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8mb4 */; + +-- +-- Database: `database_camp` +-- + +-- -------------------------------------------------------- + +-- +-- Table structure for table `Activity` +-- + +CREATE TABLE `Activity` ( + `activity_id` int(11) NOT NULL, + `activity_type_id` int(11) NOT NULL, + `content_id` int(11) DEFAULT NULL, + `point` int(11) NOT NULL, + `question` text NOT NULL, + `story` text NOT NULL, + `activity_order` int(11) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `ActivityType` +-- + +CREATE TABLE `ActivityType` ( + `activity_type_id` int(11) NOT NULL, + `name` varchar(20) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `Badge` +-- + +CREATE TABLE `Badge` ( + `badge_id` int(11) NOT NULL, + `name` varchar(50) NOT NULL, + `icon_path` text NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `CompletionChoice` +-- + +CREATE TABLE `CompletionChoice` ( + `completion_choice_id` int(11) NOT NULL, + `activity_id` int(11) NOT NULL, + `content` text NOT NULL, + `question_first` text NOT NULL, + `question_last` text NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `Content` +-- + +CREATE TABLE `Content` ( + `content_id` int(11) NOT NULL, + `content_group_id` int(11) NOT NULL, + `name` varchar(50) NOT NULL, + `video_path` text NOT NULL, + `slide_path` text NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `ContentExam` +-- + +CREATE TABLE `ContentExam` ( + `exam_id` int(11) NOT NULL, + `content_group_id` int(11) NOT NULL, + `activity_id` int(11) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `ContentGroup` +-- + +CREATE TABLE `ContentGroup` ( + `content_group_id` int(11) NOT NULL, + `name` varchar(50) NOT NULL, + `badge_id` int(11) NOT NULL, + `mini_exam_id` int(11) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `Exam` +-- + +CREATE TABLE `Exam` ( + `exam_id` int(11) NOT NULL, + `type` enum('PRE','MINI','POST') NOT NULL, + `instruction` text NOT NULL, + `created_timestamp` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `ExamResult` +-- + +CREATE TABLE `ExamResult` ( + `exam_result_id` int(11) NOT NULL, + `user_id` int(11) NOT NULL, + `exam_id` int(11) NOT NULL, + `is_passed` tinyint(1) NOT NULL, + `created_timestamp` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `ExamResultActivity` +-- + +CREATE TABLE `ExamResultActivity` ( + `exam_result_id` int(11) NOT NULL, + `activity_id` int(11) NOT NULL, + `score` int(11) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `Hint` +-- + +CREATE TABLE `Hint` ( + `hint_id` int(11) NOT NULL, + `activity_id` int(11) NOT NULL, + `content` text NOT NULL, + `point_reduce` int(11) NOT NULL, + `level` int(11) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `LearningProgression` +-- + +CREATE TABLE `LearningProgression` ( + `user_id` int(11) NOT NULL, + `activity_id` int(11) NOT NULL, + `created_timestamp` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `MatchingChoice` +-- + +CREATE TABLE `MatchingChoice` ( + `matching_choice_id` int(11) NOT NULL, + `activity_id` int(11) NOT NULL, + `pair_item1` varchar(50) NOT NULL, + `pair_item2` varchar(50) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `MultipleChoice` +-- + +CREATE TABLE `MultipleChoice` ( + `multiple_choice_id` int(11) NOT NULL, + `activity_id` int(11) NOT NULL, + `content` text NOT NULL, + `is_correct` tinyint(4) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- -------------------------------------------------------- + +-- +-- Stand-in structure for view `Profile` +-- (See below for the actual view) +-- +CREATE TABLE `Profile` ( +`user_id` int(11) +,`name` varchar(100) +,`point` int(11) +,`created_timestamp` timestamp +,`activity_count` bigint(21) +); + +-- -------------------------------------------------------- + +-- +-- Stand-in structure for view `Ranking` +-- (See below for the actual view) +-- +CREATE TABLE `Ranking` ( +`user_id` int(11) +,`name` varchar(100) +,`email` varchar(50) +,`point` int(11) +,`ranking` bigint(21) +); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `User` +-- + +CREATE TABLE `User` ( + `user_id` int(11) NOT NULL, + `name` varchar(100) NOT NULL, + `email` varchar(50) NOT NULL, + `password` text NOT NULL, + `access_token` text NOT NULL, + `point` int(11) NOT NULL, + `expired_token_timestamp` timestamp NULL DEFAULT NULL, + `created_timestamp` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + `updated_timestamp` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `UserBadge` +-- + +CREATE TABLE `UserBadge` ( + `user_id` int(11) NOT NULL, + `badge_id` int(11) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `UserHint` +-- + +CREATE TABLE `UserHint` ( + `user_id` int(11) NOT NULL, + `hint_id` int(11) NOT NULL, + `created_timestamp` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- -------------------------------------------------------- + +-- +-- Structure for view `Profile` +-- +DROP TABLE IF EXISTS `Profile`; + +CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`%` SQL SECURITY DEFINER VIEW `Profile` AS SELECT `User`.`user_id` AS `user_id`, `User`.`name` AS `name`, `User`.`point` AS `point`, `User`.`created_timestamp` AS `created_timestamp`, count(`LearningProgression`.`activity_id`) AS `activity_count` FROM (`User` left join `LearningProgression` on(`User`.`user_id` = `LearningProgression`.`user_id`)) GROUP BY `User`.`user_id` ; + +-- -------------------------------------------------------- + +-- +-- Structure for view `Ranking` +-- +DROP TABLE IF EXISTS `Ranking`; + +CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`%` SQL SECURITY DEFINER VIEW `Ranking` AS SELECT `User`.`user_id` AS `user_id`, `User`.`name` AS `name`, `User`.`email` AS `email`, `User`.`point` AS `point`, row_number() ( order by `User`.`point` desc) AS `over` FROM `User` WHERE 1 ; + +-- +-- Indexes for dumped tables +-- + +-- +-- Indexes for table `Activity` +-- +ALTER TABLE `Activity` + ADD PRIMARY KEY (`activity_id`), + ADD UNIQUE KEY `content_id` (`content_id`,`activity_order`); + +-- +-- Indexes for table `ActivityType` +-- +ALTER TABLE `ActivityType` + ADD PRIMARY KEY (`activity_type_id`); + +-- +-- Indexes for table `Badge` +-- +ALTER TABLE `Badge` + ADD PRIMARY KEY (`badge_id`); + +-- +-- Indexes for table `CompletionChoice` +-- +ALTER TABLE `CompletionChoice` + ADD PRIMARY KEY (`completion_choice_id`); + +-- +-- Indexes for table `Content` +-- +ALTER TABLE `Content` + ADD PRIMARY KEY (`content_id`); + +-- +-- Indexes for table `ContentExam` +-- +ALTER TABLE `ContentExam` + ADD PRIMARY KEY (`content_group_id`,`activity_id`,`exam_id`); + +-- +-- Indexes for table `ContentGroup` +-- +ALTER TABLE `ContentGroup` + ADD PRIMARY KEY (`content_group_id`); + +-- +-- Indexes for table `Exam` +-- +ALTER TABLE `Exam` + ADD PRIMARY KEY (`exam_id`); + +-- +-- Indexes for table `ExamResult` +-- +ALTER TABLE `ExamResult` + ADD PRIMARY KEY (`exam_result_id`); + +-- +-- Indexes for table `ExamResultActivity` +-- +ALTER TABLE `ExamResultActivity` + ADD PRIMARY KEY (`exam_result_id`,`activity_id`); + +-- +-- Indexes for table `Hint` +-- +ALTER TABLE `Hint` + ADD PRIMARY KEY (`hint_id`); + +-- +-- Indexes for table `LearningProgression` +-- +ALTER TABLE `LearningProgression` + ADD PRIMARY KEY (`user_id`,`activity_id`); + +-- +-- Indexes for table `MatchingChoice` +-- +ALTER TABLE `MatchingChoice` + ADD PRIMARY KEY (`matching_choice_id`); + +-- +-- Indexes for table `MultipleChoice` +-- +ALTER TABLE `MultipleChoice` + ADD PRIMARY KEY (`multiple_choice_id`); + +-- +-- Indexes for table `User` +-- +ALTER TABLE `User` + ADD PRIMARY KEY (`user_id`), + ADD UNIQUE KEY `email` (`email`); + +-- +-- Indexes for table `UserBadge` +-- +ALTER TABLE `UserBadge` + ADD PRIMARY KEY (`user_id`,`badge_id`); + +-- +-- Indexes for table `UserHint` +-- +ALTER TABLE `UserHint` + ADD PRIMARY KEY (`user_id`,`hint_id`); + +-- +-- AUTO_INCREMENT for dumped tables +-- + +-- +-- AUTO_INCREMENT for table `Activity` +-- +ALTER TABLE `Activity` + MODIFY `activity_id` int(11) NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `ActivityType` +-- +ALTER TABLE `ActivityType` + MODIFY `activity_type_id` int(11) NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `Badge` +-- +ALTER TABLE `Badge` + MODIFY `badge_id` int(11) NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `CompletionChoice` +-- +ALTER TABLE `CompletionChoice` + MODIFY `completion_choice_id` int(11) NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `Content` +-- +ALTER TABLE `Content` + MODIFY `content_id` int(11) NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `ContentGroup` +-- +ALTER TABLE `ContentGroup` + MODIFY `content_group_id` int(11) NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `Exam` +-- +ALTER TABLE `Exam` + MODIFY `exam_id` int(11) NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `ExamResult` +-- +ALTER TABLE `ExamResult` + MODIFY `exam_result_id` int(11) NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `Hint` +-- +ALTER TABLE `Hint` + MODIFY `hint_id` int(11) NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `MatchingChoice` +-- +ALTER TABLE `MatchingChoice` + MODIFY `matching_choice_id` int(11) NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `MultipleChoice` +-- +ALTER TABLE `MultipleChoice` + MODIFY `multiple_choice_id` int(11) NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `User` +-- +ALTER TABLE `User` + MODIFY `user_id` int(11) NOT NULL AUTO_INCREMENT; +COMMIT; + +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; diff --git a/sql/sql_view.txt b/sql/sql_view.txt index 6f62335..c2cfa3a 100644 --- a/sql/sql_view.txt +++ b/sql/sql_view.txt @@ -1,39 +1,39 @@ -=============== Profile View SQL =============== - -SELECT - `database_camp`.`User`.`user_id` AS `user_id`, - `database_camp`.`User`.`name` AS `name`, - `database_camp`.`User`.`point` AS `point`, - `database_camp`.`User`.`created_timestamp` AS `created_timestamp`, - COUNT( - `database_camp`.`LearningProgression`.`activity_id` - ) AS `activity_count` -FROM - ( - `database_camp`.`User` - LEFT JOIN `database_camp`.`LearningProgression` ON - ( - `database_camp`.`User`.`user_id` = `database_camp`.`LearningProgression`.`user_id` - ) - ) -GROUP BY - `database_camp`.`User`.`user_id` - -=============== Ranking View SQL =============== - -SELECT - `database_camp`.`User`.`user_id` AS `user_id`, - `database_camp`.`User`.`name` AS `name`, - `database_camp`.`User`.`email` AS `email`, - `database_camp`.`User`.`point` AS `point`, - row_number() over( - ORDER BY - `database_camp`.`User`.`point` - DESC - , - `database_camp`.`User`.`user_id` -) AS `ranking` -FROM - `database_camp`.`User` -WHERE +=============== Profile View SQL =============== + +SELECT + `database_camp`.`User`.`user_id` AS `user_id`, + `database_camp`.`User`.`name` AS `name`, + `database_camp`.`User`.`point` AS `point`, + `database_camp`.`User`.`created_timestamp` AS `created_timestamp`, + COUNT( + `database_camp`.`LearningProgression`.`activity_id` + ) AS `activity_count` +FROM + ( + `database_camp`.`User` + LEFT JOIN `database_camp`.`LearningProgression` ON + ( + `database_camp`.`User`.`user_id` = `database_camp`.`LearningProgression`.`user_id` + ) + ) +GROUP BY + `database_camp`.`User`.`user_id` + +=============== Ranking View SQL =============== + +SELECT + `database_camp`.`User`.`user_id` AS `user_id`, + `database_camp`.`User`.`name` AS `name`, + `database_camp`.`User`.`email` AS `email`, + `database_camp`.`User`.`point` AS `point`, + row_number() over( + ORDER BY + `database_camp`.`User`.`point` + DESC + , + `database_camp`.`User`.`user_id` +) AS `ranking` +FROM + `database_camp`.`User` +WHERE 1 \ No newline at end of file diff --git a/utils/util.handle.go b/utils/util.handle.go deleted file mode 100644 index dcaf9a4..0000000 --- a/utils/util.handle.go +++ /dev/null @@ -1,73 +0,0 @@ -package utils - -// util.handle.go -/** - * This file is a part of utilities, used to help handle module - */ - -import ( - "DatabaseCamp/errs" - "DatabaseCamp/logs" - - "github.com/gofiber/fiber/v2" -) - -/** - * This class is data model for message in Thai and English - */ -type message struct { - Th string `json:"th_message"` - En string `json:"en_message"` -} - -/** - * This class help handle module - */ -type handle struct{} - -/** - * Constructor creates a new handle instance - * - * @return instance of handle - */ -func NewHandle() handle { - return handle{} -} - -/** - * Handle error - * - * @param c Context of the web framework - * @param err Error in type App error - * - * @return error response - */ -func (h *handle) HandleError(c *fiber.Ctx, err error) error { - switch e := err.(type) { - case errs.AppError: - return c.Status(e.Code).JSON(message{Th: e.ThMessage, En: e.EnMessage}) - case error: - return c.Status(fiber.StatusInternalServerError).JSON(message{ - Th: errs.INTERNAL_SERVER_ERROR_TH, - En: errs.INTERNAL_SERVER_ERROR_EN, - }) - } - return nil -} - -/** - * Bind user request in format json to struct model - * - * @param c Context of the web framework - * @param request Struct model - * - * @return the error of response - */ -func (h *handle) BindRequest(c *fiber.Ctx, request interface{}) error { - err := c.BodyParser(&request) - if err != nil { - logs.New().Error(err) - return errs.ErrBadRequestError - } - return nil -} diff --git a/utils/util.helper.go b/utils/util.helper.go deleted file mode 100644 index 466a5ea..0000000 --- a/utils/util.helper.go +++ /dev/null @@ -1,113 +0,0 @@ -package utils - -// util.helper.go -/** - * This file is a part of utilities, used collect normal functions of the application - */ - -import ( - "math/rand" - "net/mail" - "reflect" - - m "github.com/go-sql-driver/mysql" - "golang.org/x/crypto/bcrypt" -) - -/** - * This class normal functions of the application - */ -type helper struct{} - -/** - * Constructor creates a new helper instance - * - * @return instance of helper - */ -func NewHelper() helper { - return helper{} -} - -/** - * Hash password with salt - * - * @param paw Raw password to hash - * - * @return hashed password - */ -func (h helper) HashAndSalt(pwd string) string { - bytePassword := []byte(pwd) - hash, err := bcrypt.GenerateFromPassword(bytePassword, 4) - if err != nil { - panic(err.Error()) - } - return string(hash) -} - -/** - * Compare password with hashed password - * - * @param hashedPwd Hashed password - * @param plainPwd Raw password to compare - * - * @return true if equal, false if not - */ -func (h helper) ComparePasswords(hashedPwd string, plainPwd string) bool { - bytePlainPassword := []byte(plainPwd) - byteHash := []byte(hashedPwd) - err := bcrypt.CompareHashAndPassword(byteHash, bytePlainPassword) - return err == nil -} - -/** - * Check email address format - * - * @param Email Email to validate - * - * @return true if email valid, false if not - */ -func (h helper) IsEmailValid(email string) bool { - _, err := mail.ParseAddress(email) - return err == nil -} - -/** - * Check SQL duplicate error - * - * @param err Error to check - * - * @return true if error is sql duplicate value, false if not - */ -func (h helper) IsSqlDuplicateError(err error) bool { - sqlError, ok := err.(*m.MySQLError) - return ok && sqlError.Number == 1062 -} - -/** - * Get list of the key of map - * - * @param value Map to get key - * - * @return list of the key of map - */ -func (h helper) GetKeyList(value map[string]interface{}) (result []string) { - for k := range value { - result = append(result, k) - } - return -} - -/** - * Shuffle member in list - * - * @param slice List to shuffle - */ -func (h helper) Shuffle(slice interface{}) { - rv := reflect.ValueOf(slice) - swap := reflect.Swapper(slice) - length := rv.Len() - for i := length - 1; i > 0; i-- { - j := rand.Intn(i + 1) - swap(i, j) - } -} diff --git a/utils/util.type.go b/utils/util.type.go deleted file mode 100644 index 97310a7..0000000 --- a/utils/util.type.go +++ /dev/null @@ -1,122 +0,0 @@ -package utils - -// util.type.go -/** - * This file is a part of utilities, used to convert type - */ - -import ( - "encoding/json" - "fmt" - "strconv" - "time" -) - -/** - * This class convert any type - */ -type typeUtils struct{} - -/** - * Constructor creates a new typeUtils instance - * - * @return instance of typeUtils - */ -func NewType() typeUtils { - return typeUtils{} -} - -/** - * Convert struct to map - * - * @param obj StructToMap to be converted to map - * - * @return map from obj converted - * @return the error of converting - */ -func (t typeUtils) StructToMap(obj interface{}) (newMap map[string]interface{}, err error) { - data, err := json.Marshal(obj) - - if err != nil { - return - } - - err = json.Unmarshal(data, &newMap) - return -} - -/** - * Convert struct to struct - * - * @param x1 Original struct to be converted - * @param x2 Output struct - * - * @return the error of converting - */ -func (t typeUtils) StructToStruct(x1 interface{}, x2 interface{}) error { - temp, err := t.StructToMap(x1) - if err != nil { - return err - } - - jsonBody, err := json.Marshal(temp) - if err != nil { - - return err - } - - if err := json.Unmarshal(jsonBody, x2); err != nil { - - return err - } - return nil -} - -/** - * Convert any type to duration - * - * @param item Item to be converted - * - * @return duration by converted item - */ -func (t typeUtils) ParseDuration(item interface{}) time.Duration { - intType := t.ParseInt(item) - return time.Duration(intType) -} - -/** - * Convert any type to integer value - * - * @param item Item to be converted - * - * @return integer value by converted item - */ -func (t typeUtils) ParseInt(item interface{}) int { - _item := item - switch item.(type) { - case int: - return _item.(int) - case int64: - return int(_item.(int64)) - case int32: - return int(_item.(int32)) - case float64: - return int(_item.(float64)) - case string: - _int, _ := strconv.Atoi(_item.(string)) - return _int - default: - return 0 - } -} - -/** - * Convert any type to string value - * - * @param item Item to be converted - * - * @return string value by converted item - */ -func (t typeUtils) ParseString(item interface{}) string { - return fmt.Sprintf("%v", item) -}