New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Limit choices/enum field in model #759
Comments
A very interesting point you make! An enum type would be desirable. To answer your question, it isn't currently possible. The most convenient workaround is to intersect the data before it is passed to Vuex ORM, specifically for that key who's value needs to be "validated". Alternatively, there's nothing stopping you from creating your own attribute in the form of a plugin and using it as you see fit. That aside, if you feel this feature would be worth considered, create an issue in the vuex-orm/vuex-orm-next repo. |
Hello @samul-1, Below is a simplified version of what I have so far with v0.36.4. utils/JSONSchema.js import Ajv from "ajv";
import { clone, merge } from "lodash";
/**
* Retreive properties from a JSON schema.
*
* @param {object} schema The schema
* @param {Ajv?} validator An Ajv instance
* @returns {object} The list of properties
*/
export const getProperties = (schema, validator = null) => {
const root = !validator;
if (root) {
validator = new Ajv();
validator.addSchema(schema);
}
if (schema.definitions) {
Object.entries(schema.definitions).forEach(([k, d]) => {
validator.addSchema(d, `#/definitions/${k}`);
});
}
if (schema.$ref) {
schema = validator.getSchema(schema.$ref).schema;
}
if (schema.properties) {
return clone(schema.properties);
}
let res = {};
const defs = schema["anyOf"] || schema["allOf"] || (root && schema["oneOf"]);
if (defs) {
defs.forEach((def) => {
res = merge(res, getProperties(def, validator));
});
}
return res;
}; AbstractModel.js import Ajv from "ajv";
import { Model } from "@vuex-orm/core";
import { omitBy } from "lodash";
import { getProperties } from "./utils/JSONSchema";
/**
* An abstract model based on {@link https://vuex-orm.org/|@vuex-orm} and {@link http://json-schema.org/|JSON schema}
* Heavily inspired by {@link https://github.com/chialab/schema-model|schema-model},
* and uses {@link https://ajv.js.org/|AJV} for shcema validation.
*
* @abstract
*/
export default class AbstractModel extends Model {
/**
* Get the ajv instance.
*
* @returns {Ajv} The Ajv instance
*/
static get ajv() {
if (!this._ajv) {
this._ajv = new Ajv();
this._ajv.addFormat("collection", { validate: () => true });
}
return this._ajv;
}
static get schema() {
return {
type: "object",
properties: {},
additionalProperties: false,
};
}
/**
* Get all properties from the schema
* with all definitions merged.
*
* @returns {object} The list of properties in JSON schema format
*/
static get properties() {
return getProperties(this.schema);
}
/**
* Get the list of required properties from the schema.
*
* @returns {string[]} The list of required properties
*/
static get requiredProperties() {
return this.schema.required;
}
/**
* @inheritdoc
*/
static fields() {
const fields = {};
for (const [key, schema] of Object.entries(this.properties)) {
switch (schema.type) {
case "array":
if (schema.format === "collection") {
const model = this.store().$db().model(schema.model);
const foreign_key = schema.foreign_key;
fields[key] = this.hasManyBy(model, foreign_key);
fields[foreign_key] = this.attr([]);
} else {
fields[key] = this.attr(schema.default);
}
break;
case "boolean":
fields[key] = this.boolean(schema.default);
break;
case "string":
if (schema.format === "uuid") {
fields[key] = this.uid(schema.default);
} else {
fields[key] = this.string(schema.default);
}
break;
default:
fields[key] = this.attr(schema.default);
}
if (fields[key].nullable && !this.requiredProperties.includes(key)) {
fields[key].nullable();
}
}
return fields;
}
static beforeCreate(model) {
if (!model.validate()) {
console.error(model.errors);
return false;
}
}
static beforeUpdate(model) {
if (!model.validate()) {
console.error(model.errors);
return false;
}
}
/**
* Alias to the static method of the same name
*
* @returns {object} The list of properties in JSON schema format
*/
get properties() {
return this.constructor.properties;
}
/**
* Get the schema validator.
*
* @returns {Function} A validation function returned by Ajv
*/
get validator() {
if (!this._validator) {
this._validator = this.constructor.ajv.compile(this.constructor.schema);
}
return this._validator;
}
get errors() {
return this.validator.errors;
}
$toJson() {
const internal_fields = [];
for (const schema of Object.values(this.properties)) {
if (
schema.type === "array" &&
schema.format === "collection" &&
"foreign_key" in schema
) {
internal_fields.push(schema.foreign_key);
}
}
return omitBy(super.$toJson(), (value, key) => {
return value === null || internal_fields.includes(key);
});
}
/**
* Validate data.
*
* @param {object} data The data to validate
* @returns {boolean} True if the data is valid, false otherwise
*/
validate() {
return this.validator(this.$toJson());
}
} Example usage: import AbstractModel from "./AbstractModel";
import { merge } from "lodash";
import validateColor from "validate-color";
export class MyModel extends AbstractModel {
static entity = "MyModel";
static get schema() {
const ajv = this.ajv;
ajv.addFormat("color", { validate: validateColor });
return merge(super.schema, {
properties: {
"id": {
"type": "string",
"title": "ID",
"default": "",
},
"type": {
"type": "string",
"title": "Type",
"default": ""
},
"color": {
"type": "string",
"format": "color",
"title": "Color",
"default": "#000",
},
"related": {
"type": "array",
"title": "Related",
"default": [],
"format": "collection",
"foreign_key": "related_ids",
"model": "AnotherModel"
}
},
required: ["type", "id"],
});
}
}
export default MyModel; |
Let's say I have a model that has a field
state
. This is represented as a number, but there is a finite number of states the object could be in. I'd like to be able to somehow assert that the value passed to this field (for example when creating a new instance of the entity) belongs to that set of possible choices. Since I'm using typescript in my project and I have already defined an enum that represents the possible states, I believe the best way would be to somehow enforce that the given value belong to that enum type.Is there a way to accomplish this at the field definition level? Or can this be done somewhere else? Can it be done at all?
The text was updated successfully, but these errors were encountered: