kopia lustrzana https://github.com/FacilMap/facilmap
225 wiersze
8.2 KiB
TypeScript
225 wiersze
8.2 KiB
TypeScript
import Sequelize, { CreationOptional, ForeignKey, InferAttributes, InferCreationAttributes, Model } from "sequelize";
|
|
import { Field, ID, PadId, Type, TypeCreate, TypeUpdate } from "facilmap-types";
|
|
import Database from "./database";
|
|
import { createModel, getDefaultIdType, makeNotNullForeignKey, validateColour } from "./helpers";
|
|
import { PadModel } from "./pad";
|
|
|
|
export interface TypeModel extends Model<InferAttributes<TypeModel>, InferCreationAttributes<TypeModel>> {
|
|
id: CreationOptional<ID>;
|
|
name: string;
|
|
type: "marker" | "line";
|
|
padId: ForeignKey<PadModel["id"]>;
|
|
defaultColour: string | null;
|
|
colourFixed: boolean | null;
|
|
defaultSize: string | null;
|
|
sizeFixed: boolean | null;
|
|
defaultSymbol: string | null;
|
|
symbolFixed: boolean | null;
|
|
defaultShape: string | null;
|
|
shapeFixed: boolean | null;
|
|
defaultWidth: string | null;
|
|
widthFixed: boolean | null;
|
|
defaultMode: string | null;
|
|
modeFixed: boolean | null;
|
|
showInLegend: boolean | null;
|
|
fields: Field[];
|
|
toJSON: () => Type;
|
|
};
|
|
|
|
const DEFAULT_TYPES: TypeCreate[] = [
|
|
{ name: "Marker", type: "marker", fields: [ { name: "Description", type: "textarea" } ] },
|
|
{ name: "Line", type: "line", fields: [ { name: "Description", type: "textarea" } ] }
|
|
];
|
|
|
|
export default class DatabaseTypes {
|
|
|
|
TypeModel = createModel<TypeModel>();
|
|
|
|
_db: Database;
|
|
|
|
constructor(database: Database) {
|
|
this._db = database;
|
|
|
|
this.TypeModel.init({
|
|
id: getDefaultIdType(),
|
|
name: { type: Sequelize.TEXT, allowNull: false },
|
|
type: { type: Sequelize.ENUM("marker", "line"), allowNull: false },
|
|
defaultColour: { type: Sequelize.STRING(6), allowNull: true, validate: validateColour },
|
|
colourFixed: { type: Sequelize.BOOLEAN, allowNull: true },
|
|
defaultSize: { type: Sequelize.INTEGER.UNSIGNED, allowNull: true, validate: { min: 15 } },
|
|
sizeFixed: { type: Sequelize.BOOLEAN, allowNull: true },
|
|
defaultSymbol: { type: Sequelize.TEXT, allowNull: true},
|
|
symbolFixed: { type: Sequelize.BOOLEAN, allowNull: true},
|
|
defaultShape: { type: Sequelize.TEXT, allowNull: true },
|
|
shapeFixed: { type: Sequelize.BOOLEAN, allowNull: true },
|
|
defaultWidth: { type: Sequelize.INTEGER.UNSIGNED, allowNull: true, validate: { min: 1 } },
|
|
widthFixed: { type: Sequelize.BOOLEAN, allowNull: true },
|
|
defaultMode: { type: Sequelize.TEXT, allowNull: true },
|
|
modeFixed: { type: Sequelize.BOOLEAN, allowNull: true },
|
|
showInLegend: { type: Sequelize.BOOLEAN, allowNull: true },
|
|
|
|
fields: {
|
|
type: Sequelize.TEXT,
|
|
allowNull: false,
|
|
get: function(this: TypeModel) {
|
|
const fields = this.getDataValue("fields") as any as string;
|
|
return fields == null ? [] : JSON.parse(fields);
|
|
},
|
|
set: function(this: TypeModel, v: Field[]) {
|
|
for(const field of v) {
|
|
if(field.controlSymbol) {
|
|
for(const option of field.options ?? []) {
|
|
if(!option.symbol)
|
|
option.symbol = ""; // Avoid "undefined" ending up there, which messes everything up
|
|
}
|
|
}
|
|
if(field.controlShape) {
|
|
for(const option of field.options ?? []) {
|
|
if(!option.shape)
|
|
option.shape = ""; // Avoid "undefined" ending up there, which messes everything up
|
|
}
|
|
}
|
|
}
|
|
|
|
return this.setDataValue("fields", JSON.stringify(v) as any);
|
|
},
|
|
validate: {
|
|
checkUniqueFieldName: (value: string) => {
|
|
const fields = JSON.parse(value) as Field[];
|
|
const fieldNames = new Set<string>();
|
|
for (const field of fields) {
|
|
if(field.name.trim().length == 0)
|
|
throw new Error("Empty field name.");
|
|
if(fieldNames.has(field.name))
|
|
throw new Error("field name "+field.name+" is not unique.");
|
|
|
|
fieldNames.add(field.name);
|
|
|
|
if([ "textarea", "dropdown", "checkbox", "input" ].indexOf(field.type) == -1)
|
|
throw new Error("Invalid field type "+field.type+" for field "+field.name+".");
|
|
|
|
if(field.controlColour) {
|
|
if(!field.options || field.options.length < 1)
|
|
throw new Error("No options specified for colour-controlling field "+field.name+".");
|
|
for (const option of field.options) {
|
|
if(!option.colour || !option.colour.match(validateColour.is))
|
|
throw new Error("Invalid colour "+option.colour+" in field "+field.name+".");
|
|
}
|
|
}
|
|
|
|
if(field.controlSize) {
|
|
if(!field.options || field.options.length < 1)
|
|
throw new Error("No options specified for size-controlling field "+field.name+".");
|
|
for(const option of field.options) {
|
|
if(!option.size || !isFinite(option.size) || option.size < 15)
|
|
throw new Error("Invalid size "+option.size+" in field "+field.name+".");
|
|
}
|
|
}
|
|
|
|
if(field.controlSymbol) {
|
|
if(!field.options || field.options.length < 1)
|
|
throw new Error("No options specified for icon-controlling field "+field.name+".");
|
|
}
|
|
|
|
if(field.controlWidth) {
|
|
if(!field.options || field.options.length < 1)
|
|
throw new Error("No options specified for width-controlling field "+field.name+".");
|
|
for(const option of field.options) {
|
|
if(!option.width || !(1*option.width >= 1))
|
|
throw new Error("Invalid width "+option.width+" in field "+field.name+".");
|
|
}
|
|
}
|
|
|
|
// Validate unique dropdown entries
|
|
if(field.type == "dropdown") {
|
|
const existingValues = new Set<string>();
|
|
for(const option of (field.options || [])) {
|
|
if(existingValues.has(option.value))
|
|
throw new Error(`Duplicate option "${option.value}" for field "${field.name}".`);
|
|
existingValues.add(option.value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, {
|
|
sequelize: this._db._conn,
|
|
validate: {
|
|
defaultValsNotNull: function() {
|
|
if(this.colourFixed && this.defaultColour == null)
|
|
throw new Error("Fixed colour cannot be undefined.");
|
|
if(this.sizeFixed && this.defaultSize == null)
|
|
throw new Error("Fixed size cannot be undefined.");
|
|
if(this.widthFixed && this.defaultWidth == null)
|
|
throw new Error("Fixed width cannot be undefined.");
|
|
}
|
|
},
|
|
modelName: "Type"
|
|
});
|
|
}
|
|
|
|
afterInit(): void {
|
|
const PadModel = this._db.pads.PadModel;
|
|
this.TypeModel.belongsTo(PadModel, makeNotNullForeignKey("pad", "padId"));
|
|
PadModel.hasMany(this.TypeModel, { foreignKey: "padId" });
|
|
}
|
|
|
|
getTypes(padId: PadId): Highland.Stream<Type> {
|
|
return this._db.helpers._getPadObjects<Type>("Type", padId);
|
|
}
|
|
|
|
getType(padId: PadId, typeId: ID): Promise<Type> {
|
|
return this._db.helpers._getPadObject<Type>("Type", padId, typeId);
|
|
}
|
|
|
|
async createType(padId: PadId, data: TypeCreate): Promise<Type> {
|
|
if(data.name == null || data.name.trim().length == 0)
|
|
throw new Error("No name provided.");
|
|
|
|
const createdType = await this._db.helpers._createPadObject<Type>("Type", padId, data);
|
|
this._db.emit("type", createdType.padId, createdType);
|
|
return createdType;
|
|
}
|
|
|
|
async updateType(padId: PadId, typeId: ID, data: TypeUpdate, _doNotUpdateStyles?: boolean): Promise<Type> {
|
|
if(data.name == null || data.name.trim().length == 0)
|
|
throw new Error("No name provided.");
|
|
|
|
const result = await this._db.helpers._updatePadObject<Type>("Type", padId, typeId, data);
|
|
this._db.emit("type", result.padId, result);
|
|
|
|
if(!_doNotUpdateStyles)
|
|
await this.recalculateObjectStylesForType(result.padId, typeId, result.type == "line");
|
|
|
|
return result;
|
|
}
|
|
|
|
async recalculateObjectStylesForType(padId: PadId, typeId: ID, isLine: boolean): Promise<void> {
|
|
await this._db.helpers._updateObjectStyles(isLine ? this._db.lines.getPadLinesByType(padId, typeId) : this._db.markers.getPadMarkersByType(padId, typeId));
|
|
}
|
|
|
|
async isTypeUsed(padId: PadId, typeId: ID): Promise<boolean> {
|
|
const [ marker, line ] = await Promise.all([
|
|
this._db.markers.MarkerModel.findOne({ where: { padId: padId, typeId: typeId } }),
|
|
this._db.lines.LineModel.findOne({ where: { padId: padId, typeId: typeId } })
|
|
]);
|
|
|
|
return !!marker || !!line;
|
|
}
|
|
|
|
async deleteType(padId: PadId, typeId: ID): Promise<Type> {
|
|
if (await this.isTypeUsed(padId, typeId))
|
|
throw new Error("This type is in use.");
|
|
|
|
const type = await this._db.helpers._deletePadObject<Type>("Type", padId, typeId);
|
|
|
|
this._db.emit("deleteType", padId, { id: type.id });
|
|
|
|
return type;
|
|
}
|
|
|
|
async createDefaultTypes(padId: PadId): Promise<Type[]> {
|
|
return await Promise.all(DEFAULT_TYPES.map((it) => this.createType(padId, it)));
|
|
}
|
|
} |