Перейти к содержанию

Типичные задачи: добавление нового поля

Одна из типичных задач при развитии сервиса — это добавление нового поля для сущностей 1С, которое также нужно прокинуть в текущий сервис и позже отдавать потребителям.

Что нужно сделать, чтобы новое поле считывалось от сущностей 1С, записывалось в базу и отдавалось вместе с нужными сущностями при запросе к сервису?

Пример: для транспортных операций в 1С добавилось некое поле "РегистрационныйНомер". Поле строкового типа и содержит произвольный набор символов, например — 2023-ОКТ-21_АБВ-1-2-3.

Тогда для его учёта нужно:

1. Добавить поле в описание модели транспортных операций

Добавить в описание модели transport_operations.go объявление новой колонки:

field.Text(`registration_number`).
    Optional().
    Nillable().
    StructTag(`json:"registration_number,omitempty" cargo:"РегистрационныйНомер" transform:"trim,sanitize"`).
    Comment(`1С:РегистрационныйНомер`),

Такое дополнение означает:

  • Добавляется (в таблицу транспортных операций) поле типа text;
  • Колонка имеет название registration_number;
  • Значение опционально (его не нужно указывать при создании такой модели данных);
  • Значение может быть пустым (NULLABLE);
  • Для поля в сгенерированной структуре объявить тег, который содержит:
    • Название поля при кодировании в JSON — registration_number (omitempty позволяет не выводить поле, если значение пустое)
    • Тег cargo указывает на поле РегистрационныйНомер из 1С, значение которого будет браться для текущего поля при интеграции;
    • Тег transform содержит две функции для преобразования взятого из 1С значения — trim обрежет пробелы по краям строки, sanitize уберёт символ ;
  • Добавится комментарий к создаваемой колонке.

Сгенерированная модель находится в internal/generated/models/transportoperation.go.

После запуска команды make ent в модель будет добавлено новое сгенерированное поле:

type TransportOperation struct {
    // ...

    // 1С:ИнструкцияПоСдачеПорожнего
    RegistrationNumber *string `json:"delivery_note,omitempty" cargo:"ИнструкцияПоСдачеПорожнего" transform:"trim,sanitize"`

    // ...
}

При старте сервиса после подключения к базе данных будет выполнен SQL-запрос для актуализации соответствующей таблицы:

alter table public.transport_operations
    add registration_number text;

comment on column public.transport_operations.registration_number is '1С:РегистрационныйНомер';

2. Добавить поле в конфигурацию сборщика

Конфигурация находится в файле:

internal/data/repository/transport_operation/config_by_filter.go

Поле нужно добавить в поля запроса SelfFields:

    SelfFields: []string{
        // ...
        transportoperation.FieldRegistrationNumber,
        // ...
    }

Также поле нужно добавить в функцию преобразования данных, то есть mapper, для транспортных операций это функция mapSelf() в файле:

internal/data/repository/transport_operation/mappers.go

Поле:

func mapSelf(
    item *models.TransportOperation,
    prefetched collectorTypes.PrefetchedByModelType,
) collectorTypes.ItemMapped {
    // ...
    return collectorTypes.ItemMapped{
        // ...
        transportoperation.FieldRegistrationNumber: item.RegistrationNumber,
        // ...
    }
}

Поле будет запрашиваться и выводиться для базовой структуры.

Однако, для его появления в данных через API этих действий недостаточно.

3. Добавить поле в Protobuf-схему

Схема находится в файле:

api/domain/v1/common.proto

Поле:

message TransportOperationItem {
  // ...
  optional string registrationNumber = 28;
}

Важно задать число, которого ещё не было указано и не менять числа для существующих полей. Иначе, это может привести к невозможности межсервисного взаимодействия после обновления контейнера сервиса.

Чтобы сгенерированные структуры данных обновились, нужно выполнить команду:

make api

4. Добавить поле в функции для вывода данных через API

Функция для транспортных операций называется TransportOperationItem() и находится в файле:

internal/service/transforms/collect/transport_operation.go

Поле добавляется так:

func TransportOperationItem(item map[string]any) *v1.TransportOperationItem {
    e := extractor.MustMake(item)
    
    return &v1.TransportOperationItem{
        // ...
        RegistrationNumber:                  e.Get(transportoperation.FieldRegistrationNumber).ToString(),
        // ...
    }
}

5. Поправить тесты

Часть тестов может сломаться, следует проверить файлы:

tests/datasets/transport_operations/datasets.json
tests/datasets/transport_operations/datasets.go
tests/api/transport_operations.go
tests/api/transport_operations_expanded.go

6. Выполнить проверки

Запустить тесты можно через

make test

Проверить корректность написания кода с помощью линтера

make lint

Оба этих шага обязательно должны проходить успешно, иначе будут упавшие билды и созданный Merge Request не получится отправить на слияние в master.

7. Запустить локально (необязательный шаг)

Для запуска должен быть поднят Postgres, для этого в каталоге сервиса infra в терминале можно запустить:

./deploy.sh postgres

Это запустит контейнер с PostgreSQL. Позже, аналогичным образом можно будет удалить контейнер из локального Docker-а:

./undeploy.sh postgres

Для локального запуска можно создать файл configs/.env.local и указать в нём значения для локального окружения:

ENV=local

LOG_LEVEL=warn

SENTRY_ENABLED=false
SENTRY_DSN=
SENTRY_LEVEL=warn
SENTRY_FLUSH_TIMEOUT=2s

METRICS_ADDRESS=localhost:8125
METRICS_MUTE=true

SERVER_HTTP_ADDR=0.0.0.0:8030
SERVER_HTTP_TIMEOUT=120s
SERVER_GRPC_ADDR=0.0.0.0:9030
SERVER_GRPC_TIMEOUT=120s

DATA_DATABASE_MIGRATE=hard
DATA_DATABASE_DEBUG=true

AUTH_JWT_SECRET=CHANGE_THIS_JWT_SECRET

POSTGRES_USER=postgres
POSTGRES_PASS=CHANGE_THIS_POSTGRES_PASSWORD
POSTGRES_DB=domain
POSTGRES_HOST=localhost
POSTGRES_PORT=5432

STORAGE_PATH=./storage

CARGO_INTEGRATIONS_PATH=./tmp/export

CLIENT_GRPC_NOTIFICATIONS_ENDPOINT=localhost:9020
CLIENT_GRPC_NOTIFICATIONS_TIMEOUT=10s
CLIENT_GRPC_AUTH_ENDPOINT=localhost:9010
CLIENT_GRPC_AUTH_TIMEOUT=10s

Сервис можно запустить командой:

go run ./cmd/server/... --dotenv=.env.local

Либо, если установлен GoLand, можно запустить отладку с помощью конфигурации:

<component name="ProjectRunConfigurationManager">
  <configuration default="false" name="server.local" type="GoApplicationRunConfiguration" factoryName="Go Application">
    <module name="domain" />
    <working_directory value="$PROJECT_DIR$" />
    <parameters value="--dotenv=.env.local" />
    <EXTENSION ID="net.ashald.envfile">
      <option name="IS_ENABLED" value="false" />
      <option name="IS_SUBST" value="false" />
      <option name="IS_PATH_MACRO_SUPPORTED" value="false" />
      <option name="IS_IGNORE_MISSING_FILES" value="false" />
      <option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
      <ENTRIES>
        <ENTRY IS_ENABLED="true" PARSER="runconfig" IS_EXECUTABLE="false" />
      </ENTRIES>
    </EXTENSION>
    <kind value="PACKAGE" />
    <package value="domain/cmd/server" />
    <directory value="$PROJECT_DIR$" />
    <filePath value="$PROJECT_DIR$/cmd/server/main.go" />
    <method v="2" />
  </configuration>
</component>