Я использую Typescript для записи конечных точек REST API в качестве функций через Firebase, и все методы следуют аналогичному шаблону: проверьте request.body, извлеките соответствующие данные из этих данных тела, поместите их в строго типизированный объект, использование этого объекта для передачи данных в базу данных с помощью некоторого кода доступа к данным. Написав одну и ту же базовую логику извлечения данных несколько раз для работы с request.body, я решил, что должен быть способ абстрагироваться от этой работы. Для этого у меня есть три требования: (1) метод должен работать для извлечения данных из request.body для любой из моих моделей данных. (2) Модели данных должны быть полностью информативными, чтобы они не только описывали свойства, которыми должны обладать данные, но и могли связываться, когда требуется определенный набор свойств. (3) Метод должен уметь определять из моделей данных, какие свойства требуются, и выполнять некоторую проверку данных, передаваемых через request.body.
В качестве примера №2 и модели являются информативными: подумайте, например, что когда я создаю новую запись данных, мне не нужен идентификатор, поскольку, если его нет, я могу создать его в функции и передать обратно. С другой стороны, свойство "name" является обязательным в этом случае. В отличие от этого, метод update требует ID записи (чтобы он знал, какую запись обновлять), но не требует "name", если только это не то, что на самом деле изменен.
Мой подход заключался в использовании (1) статического фабричного метода в отдельном классе, который принимает тип класса для модели данных, которую необходимо создать; предполагаемая операция (например, создать, прочитать, обновить или удалить); и тело запроса. (2) Набор классов модели данных, которые в основном просто описывают данные и включают небольшую логику проверки, где это необходимо, но также включают (статический) список имен полей и связанных значений требований (хранящихся в виде четырех битов, где каждая позиция представляет собой один из четырех операций CRUD.) (3) Общий интерфейс, так что статический фабричный метод знает, как работать с различными объектами данных, чтобы получить эти имена полей и флаги использования.
Вот мой статический фабричный метод:
static create<T extends typeof DataObjectBase>(cls: { new(...args: any[]): T; }, intendedOperation: number, requestBody: any) : T {
let dataObject : T = null;
const sourceData = {};
const objFields = cls.fieldNames;
const flagCollection = cls.requiredUseFlags();
const requiredFields = flagCollection.getFieldsForOperation(intendedOperation);
if (requestBody) {
// parse the request body
// first get all values that are available and match object field names
const allFields = Object.values(objFields); // gets all properties as key/value pairs for easier iteration
// iterate through the allFields array
for (const f in allFields) {
if (requestBody.hasOwnProperty(f)) {
// prop found; add the field to 'sourceData' and copy the value from requestBody
sourceData[f] = requestBody[f];
} else if (requiredFields.indexOf(f)>-1) {
// field is required but not available; throw error
throw new InvalidArgumentError(`${cls}.${f} is a required field, but no value found for it in request.body.`, requestBody);
}
}
dataObject = (<any>Object).assign(dataObject, sourceData);
} else {
throw new ArgumentNullError('"requestBody" argument cannot be null.', requestBody);
}
return new cls();
}
Вот пример класса модели данных:
export class Address extends DataObjectBase {
constructor(
public id : string,
public street1 : string,
public street2 : string = "",
public city : string,
public state : string,
public zip : string) {
// call base constructor
super();
}
static fieldNames = {
ID = "id",
STREET1 = "street1",
STREET2 = "street2",
// you get the idea...
}
static requiredUseFlags() {
ID = READ | UPDATE | DELETE,
STREET1 = 0,
// again, you get the idea...
// CREATE, READ, UPDATE, DELETE are all bit-flags set elsewhere
}
}
Я хочу иметь возможность вызывать указанный выше метод create
следующим образом:
const address = create<Address>(Address, CREATE, request.body);
Изначально я пробовал такую подпись:
static create<T extends typeof DataObjectBase>(cls: T, intendedOperation: number, requestBody: any) : T
Однако, когда я сделал это, я получил сообщение об ошибке: «Адрес - это тип, но используется как значение». Как только я изменил его на то, что у меня было выше, я перестал получать эту ошибку и начал получать Property 'fieldNames' does not exist on type 'new (...args: any[]) => T'
Примечание. Я также пробовал использовать два интерфейса для описания (в первом) методов экземпляра и (во втором) статических методов, а затем заставить статический интерфейс расширять интерфейс экземпляра, а базовый класс реализует статический интерфейс и т. д., как описано здесь, здесь и здесь. Это тоже не совсем меня туда привело.
Я, конечно, готов признать, что вполне мог все это перестроить, и я рад принять другие, более простые предложения. Но я полагаю, что должен быть способ достичь того, что я хочу, и избежать необходимости писать один и тот же базовый код для анализа тела запроса снова и снова.
DataObjectBase
абстрактный? - person Titian Cernicova-Dragomir   schedule 05.07.2019