Я использую Typescript для записи конечных точек API REST в качестве функций над Firebase, и все методы следуют похожему шаблону: проверьте на request.body, извлеките соответствующие данные из этих данных тела, поместите их в строготипизированный объект, используйте этот объект для передачи данных в базу данных через некоторый код доступа к данным.После того, как я несколько раз написал одну и ту же базовую логику извлечения данных для работы с request.body, я решил, что должен быть способ абстрагировать эту работу.У меня есть три требования для этого: (1) метод должен работать для извлечения данных из request.body для любой из моих моделей данных.(2) Модели данных должны быть полностью информативными, чтобы они не только описывали свойства, которыми должны обладать данные, но и могли относиться, когда требуется определенный набор свойств.(3) Метод должен иметь возможность определять из моделей данных, какие свойства требуются, и выполнять некоторую проверку данных, передаваемых через request.body.
В качестве примера # 2, а модели являются самостоятельными-описательный: учтите, например, что при создании новой записи данных мне не требуется идентификатор, поскольку, если его нет, я могу создать его в функции и передать обратно.С другой стороны, свойство "name" - это , необходимое в этом случае.В отличие от этого, метод update требует идентификатора записи (поэтому он знает, какую запись обновлять), но не не требует «имени», если только это не то, что на самом деле изменяется.
Мой подход состоял в том, чтобы использовать (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'
Примечание: я также попробовал хитрость с использованием двух интерфейсов для описания (в первом) методов экземпляраи (во втором) статические методы, а затем статический интерфейс расширяют интерфейс экземпляра, а базовый класс реализует статический интерфейс и т. д., как описано здесь , здесь и здесь .Меня это тоже не дошло.
Я, конечно, готов признать, что я вполне мог бы перепроектировать все это, и я рад принять другие, более простые предложения.Но я считаю, что должен быть способ выполнить то, что я хочу, и избегать необходимости писать один и тот же базовый код синтаксического анализа тела запроса снова и снова.