Швидке створення CRUD з nest, @nestjsx/crud та TestMace

Швидке створення CRUD з nest, @nestjsx/crud та TestMace

В даний час REST API став стандартом розробки web-додатків, що дозволяє розбити розробку на незалежні частини. Для UI на даний момент використовуються різні популярні фреймворки типу Angular, React, Vue та інші. Розробники ж backend можуть вибрати з великої різноманітності мов та фреймворків. Сьогодні я хотів би поговорити про такий фреймворк як NestJS. Ми в TestMace активно використовуємо його для внутрішніх проектів. Використовуючи nest та пакет @nestjsx/crud, ми створимо просте CRUD програму.

Чому NestJS

Останнім часом у JavaScript спільноті з'явилося чимало backend фреймворків. І якщо в плані функціоналу вони надають схожі з Nest можливості, то в одному він точно виграє це архітектура. Наступні можливості NestJS дозволяють створювати промислові програми та масштабувати розробку на великі команди:

  • використання TypeScript як основна мова розробки. Хоча NestJS і підтримує JavaScript, частина функціоналу може працювати, особливо якщо йдеться про сторонні пакети;
  • наявність DI контейнера, що дозволяє створювати слабопов'язані компоненти;
  • функціонал самого фреймворку розбитий на незалежні взаємозамінні компоненти. Наприклад, під капотом як фреймворк може використовуватися як експрес, Так і fastify, для роботи з БД nest з коробки надає біндинги до типорма, мангуста, продовжити;
  • NestJS не залежить від платформи та підтримує REST, GraphQL, Websockets, gRPC і т.д.

Сам фреймворк натхненний frontend фреймворком Angular і має концептуально багато спільного з ним.

Установка NestJS та розгортання проекту

Nest містить пакет гніздо/cli, що дозволяє швидко розгорнути базовий каркас програми. Встановимо глобально цей пакет:

npm install --global @nest/cli

Після встановлення згенеруємо базовий каркас нашої програми з ім'ям nest-rest. Робиться це за допомогою команди nest new nest-rest.

nest new nest-rest

dmitrii@dmitrii-HP-ZBook-17-G3:~/projects $ nest new nest-rest
  We will scaffold your app in a few seconds..

CREATE /nest-rest/.prettierrc (51 bytes)
CREATE /nest-rest/README.md (3370 bytes)
CREATE /nest-rest/nest-cli.json (84 bytes)
CREATE /nest-rest/nodemon-debug.json (163 bytes)
CREATE /nest-rest/nodemon.json (67 bytes)
CREATE /nest-rest/package.json (1805 bytes)
CREATE /nest-rest/tsconfig.build.json (97 bytes)
CREATE /nest-rest/tsconfig.json (325 bytes)
CREATE /nest-rest/tslint.json (426 bytes)
CREATE /nest-rest/src/app.controller.spec.ts (617 bytes)
CREATE /nest-rest/src/app.controller.ts (274 bytes)
CREATE /nest-rest/src/app.module.ts (249 bytes)
CREATE /nest-rest/src/app.service.ts (142 bytes)
CREATE /nest-rest/src/main.ts (208 bytes)
CREATE /nest-rest/test/app.e2e-spec.ts (561 bytes)
CREATE /nest-rest/test/jest-e2e.json (183 bytes)

? Which package manager would you ️ to use? yarn
 Installation in progress... 

  Successfully created project nest-rest
  Get started with the following commands:

$ cd nest-rest
$ yarn run start

                          Thanks for installing Nest 
                 Please consider donating to our open collective
                        to help us maintain this package.

                 Donate: https://opencollective.com/nest

Як пакетний менеджер ми виберемо yarn.
На даний момент ви можете запустити сервер командою npm start та пройшовши за адресою http://localhost:3000 можете бачити головну сторінку. Однак ми не для цього тут зібралися і рухаємось далі.

Налаштовуємо роботу з базою

Як СУБД для цієї статті я вибрав PostrgreSQL. Про смаки не сперечаються, на мою думку, це найбільш зріла СУБД, яка має всі необхідні можливості. Як було зазначено, для роботи з базами даних Nest надає інтеграцію з різними пакетами. Т.к. мій вибір упав на PostgreSQL, то логічно буде вибрати TypeORM як ORM. Встановимо необхідні пакети для інтеграції з базою даних:

yarn add typeorm @nestjs/typeorm pg

По порядку, навіщо потрібен кожен пакет:

  1. typeorm - пакет безпосередньо з ORM;
  2. @nestjs/typeorm — пакет TypeORM для NestJS. Додає модулі для імпортування у модулі проекту, а також набір декораторів-хелперів;
  3. pg – драйвер для роботи з PostgreSQL.

Окей, пакети встановлені, тепер потрібно запустити саму базу. Для розгортання бази я використовуватиму docker-compose.yml такого змісту:

докер-компост.імл

version: '3.1'

services:
  db:
    image: postgres:11.2
    restart: always
    environment:
      POSTGRES_PASSWORD: example
    volumes:
      - ../db:/var/lib/postgresql/data
      - ./postgresql.conf:/etc/postgresql/postgresql.conf
    ports:
      - 5432:5432
  adminer:
    image: adminer
    restart: always
    ports:
      - 8080:8080

Як бачимо, цей файл конфігурує запуск 2 контейнерів:

  1. db – це контейнер безпосередньо з базою даних. У разі використовується postgresql версії 11.2;
  2. adminer - менеджер роботи з базою даних. Надає web-інтерфейс для перегляду та керування базою.

Для роботи з підключенням tcp я додав конфіг наступного змісту.

postgresql.conf

# -----------------------------
# PostgreSQL configuration file
# -----------------------------
#
# This file consists of lines of the form:
#
#   name = value
#
# (The "=" is optional.)  Whitespace may be used.  Comments are introduced with
# "#" anywhere on a line.  The complete list of parameter names and allowed
# values can be found in the PostgreSQL documentation.
#
# The commented-out settings shown in this file represent the default values.
# Re-commenting a setting is NOT sufficient to revert it to the default value;
# you need to reload the server.
#
# This file is read on server startup and when the server receives a SIGHUP
# signal.  If you edit the file on a running system, you have to SIGHUP the
# server for the changes to take effect, run "pg_ctl reload", or execute
# "SELECT pg_reload_conf()".  Some parameters, which are marked below,
# require a server shutdown and restart to take effect.
#
# Any parameter can also be given as a command-line option to the server, e.g.,
# "postgres -c log_connections=on".  Some parameters can be changed at run time
# with the "SET" SQL command.
#
# Memory units:  kB = kilobytes        Time units:  ms  = milliseconds
#                MB = megabytes                     s   = seconds
#                GB = gigabytes                     min = minutes
#                TB = terabytes                     h   = hours
#                                                   d   = days
#------------------------------------------------------------------------------
# FILE LOCATIONS
#------------------------------------------------------------------------------
# The default values of these variables are driven from the -D command-line
# option or PGDATA environment variable, represented here as ConfigDir.
#data_directory = 'ConfigDir'       # use data in another directory
# (change requires restart)
#hba_file = 'ConfigDir/pg_hba.conf' # host-based authentication file
# (change requires restart)
#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file
# (change requires restart)
# If external_pid_file is not explicitly set, no extra PID file is written.
#external_pid_file = ''         # write an extra PID file
# (change requires restart)
#------------------------------------------------------------------------------
# CONNECTIONS AND AUTHENTICATION
#------------------------------------------------------------------------------
# - Connection Settings -
listen_addresses = '*'
#listen_addresses = 'localhost'     # what IP address(es) to listen on;
# comma-separated list of addresses;
# defaults to 'localhost'; use '*' for all
# (change requires restart)
#port = 5432                # (change requires restart)
#max_connections = 100          # (change requires restart)
#superuser_reserved_connections = 3 # (change requires restart)
#unix_socket_directories = '/tmp'   # comma-separated list of directories
# (change requires restart)
#unix_socket_group = ''         # (change requires restart)
#unix_socket_permissions = 0777     # begin with 0 to use octal notation
# (change requires restart)
#bonjour = off              # advertise server via Bonjour
# (change requires restart)
#bonjour_name = ''          # defaults to the computer name
# (change requires restart)
# - TCP Keepalives -
# see "man 7 tcp" for details
#tcp_keepalives_idle = 0        # TCP_KEEPIDLE, in seconds;
# 0 selects the system default
#tcp_keepalives_interval = 0        # TCP_KEEPINTVL, in seconds;
# 0 selects the system default
#tcp_keepalives_count = 0       # TCP_KEEPCNT;
# 0 selects the system default
# - Authentication -
#authentication_timeout = 1min      # 1s-600s
#password_encryption = md5      # md5 or scram-sha-256
#db_user_namespace = off
# GSSAPI using Kerberos
#krb_server_keyfile = ''
#krb_caseins_users = off
# - SSL -
#ssl = off
#ssl_ca_file = ''
#ssl_cert_file = 'server.crt'
#ssl_crl_file = ''
#ssl_key_file = 'server.key'
#ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers
#ssl_prefer_server_ciphers = on
#ssl_ecdh_curve = 'prime256v1'
#ssl_min_protocol_version = 'TLSv1'
#ssl_max_protocol_version = ''
#ssl_dh_params_file = ''
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = off
#------------------------------------------------------------------------------
# RESOURCE USAGE (except WAL)
#------------------------------------------------------------------------------
# - Memory -
#shared_buffers = 32MB          # min 128kB
# (change requires restart)
#huge_pages = try           # on, off, or try
# (change requires restart)
#temp_buffers = 8MB         # min 800kB
#max_prepared_transactions = 0      # zero disables the feature
# (change requires restart)
# Caution: it is not advisable to set max_prepared_transactions nonzero unless
# you actively intend to use prepared transactions.
#work_mem = 4MB             # min 64kB
#maintenance_work_mem = 64MB        # min 1MB
#autovacuum_work_mem = -1       # min 1MB, or -1 to use maintenance_work_mem
#max_stack_depth = 2MB          # min 100kB
#shared_memory_type = mmap      # the default is the first option
# supported by the operating system:
#   mmap
#   sysv
#   windows
# (change requires restart)
#dynamic_shared_memory_type = posix # the default is the first option
# supported by the operating system:
#   posix
#   sysv
#   windows
#   mmap
# (change requires restart)
# - Disk -
#temp_file_limit = -1           # limits per-process temp file space
# in kB, or -1 for no limit
# - Kernel Resources -
#max_files_per_process = 1000       # min 25
# (change requires restart)
# - Cost-Based Vacuum Delay -
#vacuum_cost_delay = 0          # 0-100 milliseconds (0 disables)
#vacuum_cost_page_hit = 1       # 0-10000 credits
#vacuum_cost_page_miss = 10     # 0-10000 credits
#vacuum_cost_page_dirty = 20        # 0-10000 credits
#vacuum_cost_limit = 200        # 1-10000 credits
# - Background Writer -
#bgwriter_delay = 200ms         # 10-10000ms between rounds
#bgwriter_lru_maxpages = 100        # max buffers written/round, 0 disables
#bgwriter_lru_multiplier = 2.0      # 0-10.0 multiplier on buffers scanned/round
#bgwriter_flush_after = 0       # measured in pages, 0 disables
# - Asynchronous Behavior -
#effective_io_concurrency = 1       # 1-1000; 0 disables prefetching
#max_worker_processes = 8       # (change requires restart)
#max_parallel_maintenance_workers = 2   # taken from max_parallel_workers
#max_parallel_workers_per_gather = 2    # taken from max_parallel_workers
#parallel_leader_participation = on
#max_parallel_workers = 8       # maximum number of max_worker_processes that
# can be used in parallel operations
#old_snapshot_threshold = -1        # 1min-60d; -1 disables; 0 is immediate
# (change requires restart)
#backend_flush_after = 0        # measured in pages, 0 disables
#------------------------------------------------------------------------------
# WRITE-AHEAD LOG
#------------------------------------------------------------------------------
# - Settings -
#wal_level = replica            # minimal, replica, or logical
# (change requires restart)
#fsync = on             # flush data to disk for crash safety
# (turning this off can cause
# unrecoverable data corruption)
#synchronous_commit = on        # synchronization level;
# off, local, remote_write, remote_apply, or on
#wal_sync_method = fsync        # the default is the first option
# supported by the operating system:
#   open_datasync
#   fdatasync (default on Linux)
#   fsync
#   fsync_writethrough
#   open_sync
#full_page_writes = on          # recover from partial page writes
#wal_compression = off          # enable compression of full-page writes
#wal_log_hints = off            # also do full page writes of non-critical updates
# (change requires restart)
#wal_buffers = -1           # min 32kB, -1 sets based on shared_buffers
# (change requires restart)
#wal_writer_delay = 200ms       # 1-10000 milliseconds
#wal_writer_flush_after = 1MB       # measured in pages, 0 disables
#commit_delay = 0           # range 0-100000, in microseconds
#commit_siblings = 5            # range 1-1000
# - Checkpoints -
#checkpoint_timeout = 5min      # range 30s-1d
#max_wal_size = 1GB
#min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
#checkpoint_flush_after = 0     # measured in pages, 0 disables
#checkpoint_warning = 30s       # 0 disables
# - Archiving -
#archive_mode = off     # enables archiving; off, on, or always
# (change requires restart)
#archive_command = ''       # command to use to archive a logfile segment
# placeholders: %p = path of file to archive
#               %f = file name only
# e.g. 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f'
#archive_timeout = 0        # force a logfile segment switch after this
# number of seconds; 0 disables
# - Archive Recovery -
# These are only used in recovery mode.
#restore_command = ''       # command to use to restore an archived logfile segment
# placeholders: %p = path of file to restore
#               %f = file name only
# e.g. 'cp /mnt/server/archivedir/%f %p'
# (change requires restart)
#archive_cleanup_command = ''   # command to execute at every restartpoint
#recovery_end_command = ''  # command to execute at completion of recovery
# - Recovery Target -
# Set these only when performing a targeted recovery.
#recovery_target = ''       # 'immediate' to end recovery as soon as a
# consistent state is reached
# (change requires restart)
#recovery_target_name = ''  # the named restore point to which recovery will proceed
# (change requires restart)
#recovery_target_time = ''  # the time stamp up to which recovery will proceed
# (change requires restart)
#recovery_target_xid = ''   # the transaction ID up to which recovery will proceed
# (change requires restart)
#recovery_target_lsn = ''   # the WAL LSN up to which recovery will proceed
# (change requires restart)
#recovery_target_inclusive = on # Specifies whether to stop:
# just after the specified recovery target (on)
# just before the recovery target (off)
# (change requires restart)
#recovery_target_timeline = 'latest'    # 'current', 'latest', or timeline ID
# (change requires restart)
#recovery_target_action = 'pause'   # 'pause', 'promote', 'shutdown'
# (change requires restart)
#------------------------------------------------------------------------------
# REPLICATION
#------------------------------------------------------------------------------
# - Sending Servers -
# Set these on the master and on any standby that will send replication data.
#max_wal_senders = 10       # max number of walsender processes
# (change requires restart)
#wal_keep_segments = 0      # in logfile segments; 0 disables
#wal_sender_timeout = 60s   # in milliseconds; 0 disables
#max_replication_slots = 10 # max number of replication slots
# (change requires restart)
#track_commit_timestamp = off   # collect timestamp of transaction commit
# (change requires restart)
# - Master Server -
# These settings are ignored on a standby server.
#synchronous_standby_names = '' # standby servers that provide sync rep
# method to choose sync standbys, number of sync standbys,
# and comma-separated list of application_name
# from standby(s); '*' = all
#vacuum_defer_cleanup_age = 0   # number of xacts by which cleanup is delayed
# - Standby Servers -
# These settings are ignored on a master server.
#primary_conninfo = ''          # connection string to sending server
# (change requires restart)
#primary_slot_name = ''         # replication slot on sending server
# (change requires restart)
#promote_trigger_file = ''      # file name whose presence ends recovery
#hot_standby = on           # "off" disallows queries during recovery
# (change requires restart)
#max_standby_archive_delay = 30s    # max delay before canceling queries
# when reading WAL from archive;
# -1 allows indefinite delay
#max_standby_streaming_delay = 30s  # max delay before canceling queries
# when reading streaming WAL;
# -1 allows indefinite delay
#wal_receiver_status_interval = 10s # send replies at least this often
# 0 disables
#hot_standby_feedback = off     # send info from standby to prevent
# query conflicts
#wal_receiver_timeout = 60s     # time that receiver waits for
# communication from master
# in milliseconds; 0 disables
#wal_retrieve_retry_interval = 5s   # time to wait before retrying to
# retrieve WAL after a failed attempt
#recovery_min_apply_delay = 0       # minimum delay for applying changes during recovery
# - Subscribers -
# These settings are ignored on a publisher.
#max_logical_replication_workers = 4    # taken from max_worker_processes
# (change requires restart)
#max_sync_workers_per_subscription = 2  # taken from max_logical_replication_workers
#------------------------------------------------------------------------------
# QUERY TUNING
#------------------------------------------------------------------------------
# - Planner Method Configuration -
#enable_bitmapscan = on
#enable_hashagg = on
#enable_hashjoin = on
#enable_indexscan = on
#enable_indexonlyscan = on
#enable_material = on
#enable_mergejoin = on
#enable_nestloop = on
#enable_parallel_append = on
#enable_seqscan = on
#enable_sort = on
#enable_tidscan = on
#enable_partitionwise_join = off
#enable_partitionwise_aggregate = off
#enable_parallel_hash = on
#enable_partition_pruning = on
# - Planner Cost Constants -
#seq_page_cost = 1.0            # measured on an arbitrary scale
#random_page_cost = 4.0         # same scale as above
#cpu_tuple_cost = 0.01          # same scale as above
#cpu_index_tuple_cost = 0.005       # same scale as above
#cpu_operator_cost = 0.0025     # same scale as above
#parallel_tuple_cost = 0.1      # same scale as above
#parallel_setup_cost = 1000.0   # same scale as above
#jit_above_cost = 100000        # perform JIT compilation if available
# and query more expensive than this;
# -1 disables
#jit_inline_above_cost = 500000     # inline small functions if query is
# more expensive than this; -1 disables
#jit_optimize_above_cost = 500000   # use expensive JIT optimizations if
# query is more expensive than this;
# -1 disables
#min_parallel_table_scan_size = 8MB
#min_parallel_index_scan_size = 512kB
#effective_cache_size = 4GB
# - Genetic Query Optimizer -
#geqo = on
#geqo_threshold = 12
#geqo_effort = 5            # range 1-10
#geqo_pool_size = 0         # selects default based on effort
#geqo_generations = 0           # selects default based on effort
#geqo_selection_bias = 2.0      # range 1.5-2.0
#geqo_seed = 0.0            # range 0.0-1.0
# - Other Planner Options -
#default_statistics_target = 100    # range 1-10000
#constraint_exclusion = partition   # on, off, or partition
#cursor_tuple_fraction = 0.1        # range 0.0-1.0
#from_collapse_limit = 8
#join_collapse_limit = 8        # 1 disables collapsing of explicit
# JOIN clauses
#force_parallel_mode = off
#jit = on               # allow JIT compilation
#plan_cache_mode = auto         # auto, force_generic_plan or
# force_custom_plan
#------------------------------------------------------------------------------
# REPORTING AND LOGGING
#------------------------------------------------------------------------------
# - Where to Log -
#log_destination = 'stderr'     # Valid values are combinations of
# stderr, csvlog, syslog, and eventlog,
# depending on platform.  csvlog
# requires logging_collector to be on.
# This is used when logging to stderr:
#logging_collector = off        # Enable capturing of stderr and csvlog
# into log files. Required to be on for
# csvlogs.
# (change requires restart)
# These are only used if logging_collector is on:
#log_directory = 'log'          # directory where log files are written,
# can be absolute or relative to PGDATA
#log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'    # log file name pattern,
# can include strftime() escapes
#log_file_mode = 0600           # creation mode for log files,
# begin with 0 to use octal notation
#log_truncate_on_rotation = off     # If on, an existing log file with the
# same name as the new log file will be
# truncated rather than appended to.
# But such truncation only occurs on
# time-driven rotation, not on restarts
# or size-driven rotation.  Default is
# off, meaning append to existing files
# in all cases.
#log_rotation_age = 1d          # Automatic rotation of logfiles will
# happen after that time.  0 disables.
#log_rotation_size = 10MB       # Automatic rotation of logfiles will
# happen after that much log output.
# 0 disables.
# These are relevant when logging to syslog:
#syslog_facility = 'LOCAL0'
#syslog_ident = 'postgres'
#syslog_sequence_numbers = on
#syslog_split_messages = on
# This is only relevant when logging to eventlog (win32):
# (change requires restart)
#event_source = 'PostgreSQL'
# - When to Log -
#log_min_messages = warning     # values in order of decreasing detail:
#   debug5
#   debug4
#   debug3
#   debug2
#   debug1
#   info
#   notice
#   warning
#   error
#   log
#   fatal
#   panic
#log_min_error_statement = error    # values in order of decreasing detail:
#   debug5
#   debug4
#   debug3
#   debug2
#   debug1
#   info
#   notice
#   warning
#   error
#   log
#   fatal
#   panic (effectively off)
#log_min_duration_statement = -1    # logs statements and their durations
# according to log_statement_sample_rate. -1 is disabled,
# 0 logs all statement, > 0 logs only statements running at
# least this number of milliseconds.
#log_statement_sample_rate = 1  # Fraction of logged statements over
# log_min_duration_statement. 1.0 logs all statements,
# 0 never logs.
# - What to Log -
#debug_print_parse = off
#debug_print_rewritten = off
#debug_print_plan = off
#debug_pretty_print = on
#log_checkpoints = off
#log_connections = off
#log_disconnections = off
#log_duration = off
#log_error_verbosity = default      # terse, default, or verbose messages
#log_hostname = off
#log_line_prefix = '%m [%p] '       # special values:
#   %a = application name
#   %u = user name
#   %d = database name
#   %r = remote host and port
#   %h = remote host
#   %p = process ID
#   %t = timestamp without milliseconds
#   %m = timestamp with milliseconds
#   %n = timestamp with milliseconds (as a Unix epoch)
#   %i = command tag
#   %e = SQL state
#   %c = session ID
#   %l = session line number
#   %s = session start timestamp
#   %v = virtual transaction ID
#   %x = transaction ID (0 if none)
#   %q = stop here in non-session
#        processes
#   %% = '%'
# e.g. '<%u%%%d> '
#log_lock_waits = off           # log lock waits >= deadlock_timeout
#log_statement = 'none'         # none, ddl, mod, all
#log_replication_commands = off
#log_temp_files = -1            # log temporary files equal or larger
# than the specified size in kilobytes;
# -1 disables, 0 logs all temp files
#log_timezone = 'GMT'
#------------------------------------------------------------------------------
# PROCESS TITLE
#------------------------------------------------------------------------------
#cluster_name = ''          # added to process titles if nonempty
# (change requires restart)
#update_process_title = on
#------------------------------------------------------------------------------
# STATISTICS
#------------------------------------------------------------------------------
# - Query and Index Statistics Collector -
#track_activities = on
#track_counts = on
#track_io_timing = off
#track_functions = none         # none, pl, all
#track_activity_query_size = 1024   # (change requires restart)
#stats_temp_directory = 'pg_stat_tmp'
# - Monitoring -
#log_parser_stats = off
#log_planner_stats = off
#log_executor_stats = off
#log_statement_stats = off
#------------------------------------------------------------------------------
# AUTOVACUUM
#------------------------------------------------------------------------------
#autovacuum = on            # Enable autovacuum subprocess?  'on'
# requires track_counts to also be on.
#log_autovacuum_min_duration = -1   # -1 disables, 0 logs all actions and
# their durations, > 0 logs only
# actions running at least this number
# of milliseconds.
#autovacuum_max_workers = 3     # max number of autovacuum subprocesses
# (change requires restart)
#autovacuum_naptime = 1min      # time between autovacuum runs
#autovacuum_vacuum_threshold = 50   # min number of row updates before
# vacuum
#autovacuum_analyze_threshold = 50  # min number of row updates before
# analyze
#autovacuum_vacuum_scale_factor = 0.2   # fraction of table size before vacuum
#autovacuum_analyze_scale_factor = 0.1  # fraction of table size before analyze
#autovacuum_freeze_max_age = 200000000  # maximum XID age before forced vacuum
# (change requires restart)
#autovacuum_multixact_freeze_max_age = 400000000    # maximum multixact age
# before forced vacuum
# (change requires restart)
#autovacuum_vacuum_cost_delay = 2ms # default vacuum cost delay for
# autovacuum, in milliseconds;
# -1 means use vacuum_cost_delay
#autovacuum_vacuum_cost_limit = -1  # default vacuum cost limit for
# autovacuum, -1 means use
# vacuum_cost_limit
#------------------------------------------------------------------------------
# CLIENT CONNECTION DEFAULTS
#------------------------------------------------------------------------------
# - Statement Behavior -
#client_min_messages = notice       # values in order of decreasing detail:
#   debug5
#   debug4
#   debug3
#   debug2
#   debug1
#   log
#   notice
#   warning
#   error
#search_path = '"$user", public'    # schema names
#row_security = on
#default_tablespace = ''        # a tablespace name, '' uses the default
#temp_tablespaces = ''          # a list of tablespace names, '' uses
# only default tablespace
#check_function_bodies = on
#default_transaction_isolation = 'read committed'
#default_transaction_read_only = off
#default_transaction_deferrable = off
#session_replication_role = 'origin'
#statement_timeout = 0          # in milliseconds, 0 is disabled
#lock_timeout = 0           # in milliseconds, 0 is disabled
#idle_in_transaction_session_timeout = 0    # in milliseconds, 0 is disabled
#vacuum_freeze_min_age = 50000000
#vacuum_freeze_table_age = 150000000
#vacuum_multixact_freeze_min_age = 5000000
#vacuum_multixact_freeze_table_age = 150000000
#vacuum_cleanup_index_scale_factor = 0.1    # fraction of total number of tuples
# before index cleanup, 0 always performs
# index cleanup
#bytea_output = 'hex'           # hex, escape
#xmlbinary = 'base64'
#xmloption = 'content'
#gin_fuzzy_search_limit = 0
#gin_pending_list_limit = 4MB
# - Locale and Formatting -
#datestyle = 'iso, mdy'
#intervalstyle = 'postgres'
#timezone = 'GMT'
#timezone_abbreviations = 'Default'     # Select the set of available time zone
# abbreviations.  Currently, there are
#   Default
#   Australia (historical usage)
#   India
# You can create your own file in
# share/timezonesets/.
#extra_float_digits = 1         # min -15, max 3; any value >0 actually
# selects precise output mode
#client_encoding = sql_ascii        # actually, defaults to database
# encoding
# These settings are initialized by initdb, but they can be changed.
#lc_messages = 'C'          # locale for system error message
# strings
#lc_monetary = 'C'          # locale for monetary formatting
#lc_numeric = 'C'           # locale for number formatting
#lc_time = 'C'              # locale for time formatting
# default configuration for text search
#default_text_search_config = 'pg_catalog.simple'
# - Shared Library Preloading -
#shared_preload_libraries = ''  # (change requires restart)
#local_preload_libraries = ''
#session_preload_libraries = ''
#jit_provider = 'llvmjit'       # JIT library to use
# - Other Defaults -
#dynamic_library_path = '$libdir'
#------------------------------------------------------------------------------
# LOCK MANAGEMENT
#------------------------------------------------------------------------------
#deadlock_timeout = 1s
#max_locks_per_transaction = 64     # min 10
# (change requires restart)
#max_pred_locks_per_transaction = 64    # min 10
# (change requires restart)
#max_pred_locks_per_relation = -2   # negative values mean
# (max_pred_locks_per_transaction
#  / -max_pred_locks_per_relation) - 1
#max_pred_locks_per_page = 2            # min 0
#------------------------------------------------------------------------------
# VERSION AND PLATFORM COMPATIBILITY
#------------------------------------------------------------------------------
# - Previous PostgreSQL Versions -
#array_nulls = on
#backslash_quote = safe_encoding    # on, off, or safe_encoding
#escape_string_warning = on
#lo_compat_privileges = off
#operator_precedence_warning = off
#quote_all_identifiers = off
#standard_conforming_strings = on
#synchronize_seqscans = on
# - Other Platforms and Clients -
#transform_null_equals = off
#------------------------------------------------------------------------------
# ERROR HANDLING
#------------------------------------------------------------------------------
#exit_on_error = off            # terminate session on any error?
#restart_after_crash = on       # reinitialize after backend crash?
#data_sync_retry = off          # retry or panic on failure to fsync
# data?
# (change requires restart)
#------------------------------------------------------------------------------
# CONFIG FILE INCLUDES
#------------------------------------------------------------------------------
# These options allow settings to be loaded from files other than the
# default postgresql.conf.
#include_dir = 'conf.d'         # include files ending in '.conf' from
# directory 'conf.d'
#include_if_exists = 'exists.conf'  # include file only if it exists
#include = 'special.conf'       # include file
#------------------------------------------------------------------------------
# CUSTOMIZED OPTIONS
#------------------------------------------------------------------------------
# Add settings for extensions here

На цьому все можна запустити контейнери командою docker-compose up -d. Або в окремій консолі командою docker-compose up.

Отже, пакети встановили, базу запустили, залишилося їх подружити один з одним. Для цього потрібно до кореня проекту додати файл ormconfig.js наступного змісту:

ormconfig.js

const process = require('process');
const username = process.env.POSTGRES_USER || "postgres";
const password = process.env.POSTGRES_PASSWORD || "example";
module.exports = {
"type": "postgres",
"host": "localhost",
"port": 5432,
username,
password,
"database": "postgres",
"synchronize": true,
"dropSchema": false,
"logging": true,
"entities": [__dirname + "/src/**/*.entity.ts", __dirname + "/dist/**/*.entity.js"],
"migrations": ["migrations/**/*.ts"],
"subscribers": ["subscriber/**/*.ts", "dist/subscriber/**/.js"],
"cli": {
"entitiesDir": "src",
"migrationsDir": "migrations",
"subscribersDir": "subscriber"
}
}

Ця конфігурація використовуватиметься для cli typeorm.

Зупинимося на цій конфігурації докладніше. У рядках 3 та 4 ми отримуємо ім'я користувача та пароль зі змінних оточення. Це зручно, коли у вас є кілька оточень (dev, stage, prod, etc). За промовчанням ім'я користувача postgres, пароль - example. В іншому конфіг тривіальний, тому зупинимося тільки на найцікавіших параметрах:

  • synchronize — Вказує, чи схема бази даних повинна автоматично створюватися під час запуску програми. Будьте уважні з цією опцією і не використовуйте її в production, інакше ви втратите дані. Ця опція зручна при розробці та налагодженні програми. Як альтернатива цієї опції, ви можете використовувати команду schema:sync із CLI TypeORM.
  • dropSchema - скидати схему щоразу, коли встановлюється з'єднання. Також, як і попередню, цю опцію слід використовувати тільки в процесі розробки та налагодження програми.
  • entities — якими шляхами шукати опис моделей. Зауважте, що підтримується пошук по масці.
  • cli.entitiesDir — директорія, куди за умовчанням повинні складатися моделі, створені із CLI TypeORM.

Для того, щоб ми могли використовувати всі можливості TypeORM в нашому додатку Nest, необхідно імпортувати модуль TypeOrmModule в AppModule. Тобто. ваш AppModule буде виглядати наступним чином:

app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import * as process from "process";
const username = process.env.POSTGRES_USER || 'postgres';
const password = process.env.POSTGRES_PASSWORD || 'example';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username,
password,
database: 'postgres',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

Як ви встигли помітити, метод forRoot передається та ж конфігурація для роботи з базою, що й у файлі ormconfig.ts

Залишився фінальний штрих - додати кілька тяган для роботи з TypeORM в package.json. Справа в тому, що CLI написана на JavaScript і запускається в середовищі nodejs. Однак усі наші моделі та міграції будуть написані на typescript. Тому необхідно провести транспіляцію наших міграцій та моделей до використання CLI. Для цього нам знадобиться пакет ts-node:

yarn add -D ts-node

Після цього додамо необхідні команди до package.json:

"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js",
"migration:generate": "yarn run typeorm migration:generate -n",
"migration:create": "yarn run typeorm migration:create -n",
"migration:run": "yarn run typeorm migration:run"

Перша команда, typeorm, додає обгортку у вигляді ts-node для запуску cli TypeORM. Інші команди - це зручні скорочення, якими ви як розробник користуватиметеся практично кожен день:
migration:generate створення міграції на основі змін у ваших моделях.
migration:create - Створення порожньої міграції.
migration:run - Запуск міграцій.
Ну тепер точно все, ми додали необхідні пакети, налаштували додаток для роботи з базою як з cli, так і з самого додатка і запустили СУБД. Настав час додати логіку до нашої програми.

Встановлення пакетів для створення CRUD

Використовуючи лише Nest можна створити API, що дозволяє створювати, читати, оновлювати та видаляти сутність. Таке рішення буде максимально гнучким, проте для деяких випадків надлишковим. Наприклад, якщо вам потрібно швидко створити прототип, то часто можна пожертвувати гнучкістю задля швидкості розробки. Багато фреймворків надають функціонал генерації CRUD за описом моделі даних певної сутності. Та Nest не виняток! Дану функціональність надає пакет @nestjsx/crud. Можливості його дуже цікаві:

  • легке встановлення та налаштування;
  • незалежність від СУБД;
  • потужна мова запитів з можливістю фільтрації, пагінації, сортування, завантаження зв'язків та вкладених сутностей, кешування тощо;
  • пакет формування запитів на front-end;
  • легке перевизначення методів контролера;
  • невеликий конфіг;
  • підтримка swagger документації.

Функціональність розбита на кілька пакетів:

  • @nestjsx/crud - базовий пакет, який надає декоратор сирий() для генерації роутів, конфігурування та валідації;
  • @nestjsx/crud-request - Пакет, що надає білдер/парсер запитів для використання на стороні frontend;
  • @nestjsx/crud-typeorm - Пакет для інтеграції з TypeORM, що надає базовий сервіс TypeOrmCrudService з CRUD методами роботи з сутностями в БД.

У цьому посібнику нам знадобляться пакети гніздоjsx/crud та гніздоjsx/crud-typeorm. Для початку, поставимо їх

yarn add @nestjsx/crud class-transformer class-validator

пакети class-transformer и class-validator у цьому додатку потрібні для декларативного опису правил трансформування екземплярів моделей та валідації вхідних запитів відповідно. Ці пакети від одного автора, тому інтерфейси схожі.

Безпосередня реалізація CRUD

Як приклад моделі ми візьмемо список користувачів. У користувачів будуть такі поля: id, username, displayName, email. id - Автоінкрементне поле, email и username - Унікальні поля. Все просто! Залишилося втілити наш задум у вигляді Nest програми.
Для початку необхідно створити модуль users, який відповідатиме за роботу з користувачами. Скористаємося cli від NestJS, і у кореневій директорії нашого проекту виконаємо команду nest g module users.

nest g module users

dmitrii@dmitrii-HP-ZBook-17-G3:~/projects/nest-rest git:(master*)$ nest g module users
CREATE /src/users/users.module.ts (82 bytes)
UPDATE /src/app.module.ts (312 bytes)

В даному модулі додамо папку entities, де у нас лежатимуть моделі даного модуля. Зокрема, додамо сюди файл user.entity.ts із описом моделі користувачів:

user.entity.ts

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: string;
@Column({unique: true})
email: string;
@Column({unique: true})
username: string;
@Column({nullable: true})
displayName: string;
}

Щоб цю модель «побачив» наш додаток, необхідно в модуль UsersModule імпортувати TypeOrmModule такого змісту:

users.module.ts

import { Module } from '@nestjs/common';
import { UsersController } from './controllers/users/users.controller';
import { UsersService } from './services/users/users.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
@Module({
controllers: [UsersController],
providers: [UsersService],
imports: [
TypeOrmModule.forFeature([User])
]
})
export class UsersModule {}

Тобто тут ми імпортуємо TypeOrmModule, де як параметр методу forFeature вказуємо список моделей, що належать до даного модуля.

Залишається створити відповідну сутність у базі даних. Для цього служить механізм міграцій. Щоб створити міграцію на основі змін у моделях, необхідно виконати команду npm run migration:generate -- CreateUserTable:

Заголовок спойлера

$ npm run migration:generate -- CreateUserTable
Migration /home/dmitrii/projects/nest-rest/migrations/1563346135367-CreateUserTable.ts has been generated successfully.
Done in 1.96s.

Нам не довелося писати міграцію вручну, все сталося магічно. Чи це не диво! Однак це ще не все. Погляньмо на створений файл із міграцією:

1563346135367-CreateUserTable.ts

import {MigrationInterface, QueryRunner} from "typeorm";
export class CreateUserTable1563346816726 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`CREATE TABLE "user" ("id" SERIAL NOT NULL, "email" character varying NOT NULL, "username" character varying NOT NULL, "displayName" character varying, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), CONSTRAINT "UQ_78a916df40e02a9deb1c4b75edb" UNIQUE ("username"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`DROP TABLE "user"`);
}
}

Як можна помітити, було автоматично згенеровано як метод запуску міграції, а й метод її відкату. Фантастика!
Залишається тільки накотити цю міграцію. Робиться це наступною командою:

npm run migration:run.

Все тепер зміни схеми перекочували в базу даних.
Далі створимо сервіс, який відповідатиме за роботу з користувачами та віднаслідуємо його від TypeOrmCrudService. У параметр батьківського конструктора необхідно передати репозиторій сутності, що цікавить, у нашому випадку User репозиторій.

users.service.ts

import { Injectable } from '@nestjs/common';
import { TypeOrmCrudService } from '@nestjsx/crud-typeorm';
import { User } from '../../entities/user.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
@Injectable()
export class UsersService extends TypeOrmCrudService<User>{
constructor(@InjectRepository(User) usersRepository: Repository<User>){
super(usersRepository);
}
}

Даний сервіс нам знадобиться у контролері users. Для створення контролера наберіть у консолі nest g controller users/controllers/users

nest g controller users/controllers/users

dmitrii@dmitrii-HP-ZBook-17-G3:~/projects/nest-rest git:(master*)$ nest g controller users/controllers/users
CREATE /src/users/controllers/users/users.controller.spec.ts (486 bytes)
CREATE /src/users/controllers/users/users.controller.ts (99 bytes)
UPDATE /src/users/users.module.ts (188 bytes)

Відкриємо цей контролер і відредагуємо, щоб додати трохи магії гніздоjsx/crud. На клас UsersController додамо декоратор такого виду:

@Crud({
model: {
type: User
}
})

сирий - Це декоратор, що додає в контролер необхідні методи для роботи з моделлю. Тип моделі вказується у полі model.type конфігурації декоратора.
Другий крок – необхідно реалізувати інтерфейс CrudController<User>. "У зборі" код контролера виглядає наступним чином:

import { Controller } from '@nestjs/common';
import { Crud, CrudController } from '@nestjsx/crud';
import { User } from '../../entities/user.entity';
import { UsersService } from '../../services/users/users.service';
@Crud({
model: {
type: User
}
})
@Controller('users')
export class UsersController implements CrudController<User>{
constructor(public service: UsersService){}
}

І це все! Тепер контролер підтримує весь набір операцій із моделлю! Не вірите? Давайте спробуємо наш додаток у справі!

Створення сценарію запитів у TestMace

Для тестування нашого сервісу ми використовуватимемо IDE для роботи з API TestMace. Чому TestMace? Порівняно з аналогічними продуктами, він має такі переваги:

  • потужна робота із змінними. На даний момент існує кілька видів змінних, кожен із яких виконує певну роль: вбудовані змінні, динамічні змінні, змінні оточення. Кожна змінна належить якомусь вузлу за допомогою механізму спадкування;
  • легке створення сценаріїв без програмування. Про це йтиметься нижче;
  • людиночитаний формат, що дозволяє зберігати проект у системах контролю версій;
  • автодоповнення, підсвічування синтаксису, підсвічування значень змінних;
  • підтримка опису API з можливістю імпорту із Swagger.

Давайте запустимо наш сервер командою npm start та спробуємо звернутися до списку користувачів. Список користувачів, судячи з нашої конфігурації контролера, можна отримати за адресою url localhost:3000/users. Зробимо запит на цей URL.
Після запуску TestMace ви можете побачити такий інтерфейс:

Швидке створення CRUD з nest, @nestjsx/crud та TestMace

Зліва зверху знаходиться дерево проектів із кореневим вузлом. Проекти. Спробуємо створити перший запит на отримання списку користувачів. Для цього створимо RequestStep вузол. Робиться це у контекстному меню Project вузла Add node -> RequestStep.

Швидке створення CRUD з nest, @nestjsx/crud та TestMace

У полі URL вставте localhost:3000/users та виконайте запит. Отримаємо 200 код із порожнім масивом у тілі відповіді. Воно й зрозуміло, ми ще нікого не додавали.
Давайте створимо сценарій, який буде включати наступні кроки:

  1. створення користувача;
  2. запит по id щойно створеного користувача;
  3. видалення id користувача, створеного на кроці 1.

Тож поїхали. Для зручності створимо вузол типу Папка. По суті, це просто папка, в якій збережемо весь сценарій. Для створення Folder вузла необхідно у контекстному меню Project вузла вибрати Add node -> Folder. Назвемо вузол check-create. Усередині вузла check-create створимо наш перший запит на створення користувача. Назвемо новостворений вузол create-user. Тобто на даний момент ієрархія вузлів виглядатиме так:

Швидке створення CRUD з nest, @nestjsx/crud та TestMace

Давайте перейдемо до вкладки відкритого create-user вузла. Введемо такі параметри для запиту:

  • Тип запиту - POST
  • URL-localhost:3000/users
  • Body - JSON зі значенням {"email": "[email protected]", "displayName": "New user", "username": "user"}

Виконаємо цей запит. Наш додаток каже, що запис створено.

Швидке створення CRUD з nest, @nestjsx/crud та TestMace

Що ж, перевіримо цей факт. Щоб у наступних кроках оперувати з ID створеного користувача, цей параметр необхідно зберегти. Для цього чудово підходить механізм динамічних змінних. Давайте на прикладі розглянемо, як відбувається робота з ними. У вкладці parsed відповіді у вузла id у контекстному меню необхідно вибрати пункт Assign to variable. У діалоговому вікні необхідно задати такі параметри:

  • вузол - У якому з предків створювати динамічну змінну. Виберемо check-create
  • Назва змінної - Назва цієї змінної. Назвемо userId.

Ось як виглядає процес створення динамічної змінної:

Швидке створення CRUD з nest, @nestjsx/crud та TestMace

Тепер при кожному виконанні запиту значення динамічної змінної буде оновлюватися. А т.к. динамічні змінні підтримують механізм ієрархічного наслідування, змінна userId буде доступна у нащадках check-create вузла будь-якого рівня вкладеності.
У наступному запиті ця змінна стане в нагоді. А саме, ми запросимо новоствореного користувача. Як нащадок вузла check-create ми створимо запит check-if exists з параметром url рівним localhost:3000/users/${$dynamicVar.userId}. Конструкція виду ${variable_name} це отримання значення змінної. Т.к. у нас динамічна змінна, то щоб отримати її необхідно звернутися до об'єкту $dynamicVar, тобто повністю звернення до динамічної змінної userId буде виглядати так ${$dynamicVar.userId}. Виконаємо запит і переконаємось, що дані запитуються коректно.
Залишився останній штрих зробити запит на видалення. Він нам необхідний як у тому, щоб перевірити роботу видалення, а й, як кажуть, підчистити у себе у основі, т.к. поля email та username унікальні. Отже, у вузлі check-create створимо запит delete-user із наступними параметрами

  • Тип запиту - DELETE
  • URL - localhost:3000/users/${$dynamicVar.userId}

Запускаємо. Чекаємо. Насолоджуємося результатом)

Ну і тепер ми можемо будь-якої миті запустити повністю даний сценарій. Щоб запустити сценарій, необхідно вибрати в контекстному меню check-create вузла пункт прогін.

Швидке створення CRUD з nest, @nestjsx/crud та TestMace

Вузли у сценарії виконуються один за одним
Даний сценарій ви можете зберегти до себе в проект, виконавши File -> Save project.

Висновок

У форматі цієї статті просто не змогли вміститися всі фішки використаних інструментів. Щодо основного винуватця — пакета гніздоjsx/crud - неосвітленими залишилися такі теми:

  • кастомна валідація та трансформація моделей;
  • потужна мова запитів та зручне її використання на фронті;
  • перевизначення та додавання нових методів у crud-контролери;
  • підтримка swagger;
  • керування кешуванням.

Однак навіть описаного в статті достатньо, щоб зрозуміти, що навіть такий ентерпрайсний фреймворк, як NestJS, має в загашнику інструменти для швидкого прототипування додатків. А така класна IDE як TestMace дозволяє підтримати заданий темп.

Вихідний код цієї статті, разом із проектом TestMace, доступний у репозиторії https://github.com/TestMace/nest-rest. Для відкриття проекту TestMace достатньо у додатку виконати File -> Open project.

Джерело: habr.com

Додати коментар або відгук