From 5e98ced41c57cee40b5a0b316dd640e9a6720e24 Mon Sep 17 00:00:00 2001 From: tim-s-ccs Date: Fri, 17 Dec 2021 10:27:27 +0000 Subject: [PATCH] Update config file name Add to the readme Update the readme further --- README.md | 260 ++++++++++++++++++++++++++- ccsModelInterfaceConfig.json | 4 + package.json | 2 +- src/ccsModelInterfaceConfig.ts | 5 + src/data/activeDataInterface.ts | 4 +- src/data/staticDataInterface.ts | 4 +- src/frameworkConfig.ts | 5 - src/models/activeModel.ts | 4 +- src/types/ccsModelInterfaceConfig.ts | 6 + src/types/frameworkConfig.ts | 6 - src/types/models/model.ts | 6 +- src/types/validation/validator.ts | 1 - src/validation/validator.ts | 17 +- 13 files changed, 296 insertions(+), 28 deletions(-) create mode 100644 ccsModelInterfaceConfig.json create mode 100644 src/ccsModelInterfaceConfig.ts delete mode 100644 src/frameworkConfig.ts create mode 100644 src/types/ccsModelInterfaceConfig.ts delete mode 100644 src/types/frameworkConfig.ts diff --git a/README.md b/README.md index 4a2c94f..2ecc1e8 100644 --- a/README.md +++ b/README.md @@ -1 +1,259 @@ -# ccs-prototype-kit-model-interface \ No newline at end of file +# CCS Prototype Kit Model Interface +The [GOV.UK Prototype Kit](https://github.com/alphagov/govuk-prototype-kit) is designed to allow for the rapid development of prototypes which can then be used to get feedback on new designs. +One of the limitations of this kit is that it is quite simple and if you want to do things like validations or work with data, it is not simple to do. +This project, inspired by Ruby on Rails, is designed to work with the GOV.UK Prototype Kit to create an interface which will allow users to better work with data. +Although it is written in TypeScript, this project can, of course, be used in a JavaScript projects as well. + +## Project status +The project is still in an Alpha phase. +There are still some features that have not been fully developed and there is still some work to do on Type safety. +An example implementation of the project can be found in [Facilities Management RM6232 Prototype](https://github.com/tim-s-ccs/facilities-management-RM6232-prototype). + +## How to install +This package is meant to be used as part of either of the following projects: +- [GOV.UK Prototype Kit](https://github.com/alphagov/govuk-prototype-kit) +- [CCS Prototype Kit](https://github.com/Crown-Commercial-Service/ccs-prototype-kit) +It is not designed to work with anything else, and certainly not designed to be used in any kind of production setting. + +You can install the package by adding `ccs-prototype-kit-model-interface` to your `packag.json` are by running: +``` +npm install ccs-prototype-kit-model-interface +``` +You then need to add `ccsModelInterfaceConfig.config` file into your project directory. +The file should then look like: +``` +{ + "activeDataSchemaPath": "dist/app/data/activeDataSchema", + "staticDataPath": "dist/app/data/staticData" +} +``` +The significant of these files is explained in the [Data Interface](#data-interface) section. + +## Documentation +As previously stated, this project was inspired by Ruby on Rails, so the most important concept to understand is Models. +A `Model` is the object we use to represent tables in the database. +In this project there are two types of Models which are `ActiveModel` and `StaticModel`. + +**NOTE:** *This package is still in development so some parts are not fully finished and other parts are still TODO* + +### ActiveModel +An `ActiveModel` is designed to be used with data that can added, updated, deleted etc. +Essentially, it should be used with models where the data can change. +How we are able to change the data is explained in the [Data Interface](#data-interface) section. + +An `ActiveModel` contains the following attributes: +- `data` - The attributes, i.e. the table columns, for the `ActiveModel` +- `tableName` - The name of the table in the pseudo database +- `modelSchema` - This contains the Type constructors for each attribute in the model +- `validationSchema` - This contains the validations for each attribute (discussed in the [Validations](#validations) section) +- `errors` - The errors for the `ActiveModel` (an empty object by default) + +The `ActiveModel` class contains several static methods which are mainly used to find some or all the tables in the database. +Most of these methods start with an underscore (e.g. `_find`) and are `protected`. +If this is the case, then when you create Class that extends the `ActiveModel` class, then you need cast what the method returns to this Child class. +An example of this is shown in the explanation of [_find](#find) in the next section. +To be honest, the reason this is required is because I've not yet worked out of keeping Type safety for the child class when it calls the method. + +#### Static methods +##### _find +This method uses the `getActiveRow` method, discussed in the [Data Interface](#data-interface) section. +This method takes the Request object, the table name and the ID of the row we want to find the data for. +It will return the row of the table if it can find an entry with the ID, otherwise it will raise `RowNotFoundError`. +If data is found, it can be used to then initialise the `Model`. + +An example of how this method might be used in implementation of `ActiveModel` is shown below. +``` +class Foo extends ActiveModel implements FooInterface { + ... + static find = (req: Request, id: number): Foo => { + return new this(this._find(req, this.tableName, id) as FooRow, req) + } + ... +} +``` +As can be seen, `_find` returns the row data which can then be used to initialise a new instance of the `Foo` class. + +##### _all +This is fairly self-explanatory. +By calling this method, you will get all the rows of the table in an array. + +##### _where +This method allows to get the rows from a table which meet certain conditions. +A `Condition` contains an attribute and one of the following options: +- `value` a specific value we want that attribute to have +- `values` an array of values, any of which we want the attribute to have +- `contents` when the attribute is an array, the contents are an array of values we want the attribute array to contain + +As an example, the table `foo` has the following data: +``` +const foos: Array = [ + { id: 1, name: 'Foo 1', code: 1, items: [1, 2, 3, 4, 5] }, + { id: 2, name: 'Foo 2', code: 2, items: [2, 3, 4, 5, 6] }, + { id: 3, name: 'Foo 3', code: 3, items: [3, 4, 5, 6, 7] }, + { id: 4, name: 'Foo 4', code: 2, items: [4, 5, 6, 7, 8] }, + { id: 5, name: 'Foo 5', code: 1, items: [5, 6, 7, 8, 9] }, +] +``` + +We can then use `where` with the conditions as follows: +``` +Foo.where([{ attribute: 'code', values: [1, 2] }, { attribute: 'items', values: [5, 6] }]) +``` +which returns: +``` +[ + { id: 1, name: 'Foo 1', code: 1, items: [1, 2, 3, 4, 5] }, + { id: 4, name: 'Foo 4', code: 2, items: [4, 5, 6, 7, 8] }, + { id: 5, name: 'Foo 5', code: 1, items: [5, 6, 7, 8, 9] } +] +``` + +##### count +This is an extension of the `_where` method and simply gets the length of the array that is returned. +I decided to add this as I found, when using this package in another project, that it was often useful just to return how many entries met the conditions rather than returning the full array of objects. + +##### nextID +This is needed when creating a new entry into the database. +This method will get a list of IDs for all the rows in the table, find the max and add 1 to get a new ID. +If there are no rows in the table then it will just return 1 as the first ID. + +### Validation +The main inspiration for creating this package was to create a way of doing validations. +When creating a government service, error messages are very important, so I wanted to create a way we could do validations in a straight forward way (if I've been successful in this, that's for you to decide). + +#### Validator +There are three types of validators: + - `InputValidator` - Abstract class which allows for the validation of an input. Several implimentations of this class are provided and they are: + - `inclusionValidator` - validates that an input is in an array of accepted values + - `lengthValidator` - validates the length of an array input + - `numberValidator` - numeric validations on an input, the options can be seen in the `NumberValidatorOptions` + - `stringValidator` - string validations on an input, the options can be seen in the `StringValidatorOptions` + - `StaticModelValidator` - Makes sure the ID for `StaticModel` is present + - `CustomValidator` - Abstract class which allows for the user to make their own validations of the `InputValidators` are not sufficient. + +All validators extend the abstract class `Validator` abstract class which abstracts away the common parts of the validation. +I think most of the part of the `Validator` are self-explanatory except the `ValidatorOptions`. + +##### ValidatorOptions +The `ValidatorOptions` contain important information concerning the specific validation that is going to be run. +All `ValidatorOptions` contain the `on` and `conditions` options, and implimentations of the `Validator` may have additional options. + +The `conditons` option is still in development, but the `on` condition contains the calls that the validator should be run on. +For example, if we are building an `ActiveModel`whoch we will call `Car` over a number of pages. +On the first page we may want to add the `make`. +On the next we may want to add the `colour` etc. +If we have the following validation options for the attributes: +``` +const makeValidatorOptions: ValidatorOptions = { + on: ['make'] +} + +const colourValidatorOptions: ValidatorOptions = { + on: ['colour'] + +``` + +We only need to do the validation for the `make` on that page so we might call the `valid` with `'make'`, the validator will only validate the `make` of the car and will ignore the `colour` validation. + +###### validate +The `validate` method is what is used to make validations consistent across on various implementations of the `Validator` class. +It takes in the 'call' as a parameter and then goes through a few steps. + +It will first check that the call is within the options and if it is not then will return `true`. +It will then check if the validation has any conditions and if does, it will check that those conditions have been met. +If there are no conditions, or the conditions are all true, it will go to the next step. + +This is where the abstract method `_validate` is called. +`_validate` is where the specific validation is implemented. +If the data is not valid, it will return `false` and set the `error` string. +This error and the message are then added to the model and the method returns `false`. +If `_validate` returns `true` then the `validate` will then also return `true` as all the data is valid. + +##### How validations are implimented in the ActiveModel +The previous section was a summary of how the validation is implemented in the `Validator` but is important to also understand how validations are implemented within the `ActiveModel`. + +One of the attributes of `ActiveModel` is the `validationSchema`. +The `ValidationSchema` is an object which contains the `ValidationScheme` for all the input validations, the static model validations and the custom validations. + +All `ValidationScheme` have the following attributes: +- `attribute` - The attribute on which the validations are to be applied +- `options` - These are the validator options, discussed in [ValidatorOptions](#validatoroptions) +- `errorMessages` - An object containing the error messages, depending on what validations fail + +For the input and the custom validations, the specific implementation of the `Validator` must also be provided. + +
An example `ValidationSchema` +``` +const buildingValidationSchema: ValidationSchema = { + inputValidations: [ + { + attribute: 'name', + validator: StringValidator, + options: nameOptions, + errorMessages: { + required: 'Enter a name for your building', + tooLong: 'Building name must be 50 characters or less' + } + }, + ... + ], + customValidations: [ + { + attribute: 'externalArea', + validator: CombinedAreaValidation, + options: { + on: ['area'] + }, + errorMessages: { + combinedAreaExternal: 'External area must be greater than 0, if the internal area is 0' + } + ... + } + ], + staticModelValidations: [ + { + attribute: 'buildingType', + options: { + on: ['building-type'] + }, + errorMessages: { + required: 'You must select a building type' + } + }, + ... + ] +} +``` +
+ +Because all implementations of `Validator` should have the same constructor, even though the implementations differ, they can be initialised and called in the same way. +This is what happens within the `validate` method in the `ActiveModel`. + +The `validate` method goes through the `validationSchema` and runs all the validations. +If the `ActiveModel` has any nested `ActiveModels` then they too will be validated and their errors combined with the parent `ActiveModel`. +After all this has happened, the method will return `false` of there are any errors or `true` if there are none. + +### Saving +**TODO:** Discuss assigning attributes, saving and creating +### StaticModel +A `StaticModel` is designed to be used with data that cannot be changed. +**TODO:** Discuss briefly the `StaticModel` abstract class + +### Data Interface +The [GOV.UK Prototype Kit](https://github.com/alphagov/govuk-prototype-kit) allows for us to use the users session storage to store the users data. +We can than access this data through the `Request` object. + +**TODO:** Discuss how this works and more details on the config file + +## Future Ideas +Below is list of future ideas I have for this project that I hope to one day implement: +- Add tests to be sure the code works as expected +- Improve data structuring to allow for table relationships +- Improve the documentation +- Abstract more code into the abstract classes (building and the static methods) + +## Contributing + +This repository is maintained by the tim-s-ccs at the [Crown Commercial Service](https://github.com/Crown-Commercial-Service). + +If you have a suggestion for improvement, please raise an issue on this repo. diff --git a/ccsModelInterfaceConfig.json b/ccsModelInterfaceConfig.json new file mode 100644 index 0000000..a45de12 --- /dev/null +++ b/ccsModelInterfaceConfig.json @@ -0,0 +1,4 @@ +{ + "activeDataSchemaPath": "dist/app/data/activeDataSchema", + "staticDataPath": "dist/app/data/staticData" +} diff --git a/package.json b/package.json index 9ab7350..e058538 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ccs-prototype-kit-model-interface", "version": "0.0.1", - "description": "An interface for the ccs-prototype-kit to allow for the use of a sudo database and models", + "description": "An interface for the ccs-prototype-kit to allow for the use of a pseudo database and models", "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ diff --git a/src/ccsModelInterfaceConfig.ts b/src/ccsModelInterfaceConfig.ts new file mode 100644 index 0000000..04495f8 --- /dev/null +++ b/src/ccsModelInterfaceConfig.ts @@ -0,0 +1,5 @@ +import CCSModelInterfaceConfig from './types/ccsModelInterfaceConfig' + +const ccsModelInterfaceConfig: CCSModelInterfaceConfig = require.main?.require('./ccsModelInterfaceConfig.json') as CCSModelInterfaceConfig + +export default ccsModelInterfaceConfig \ No newline at end of file diff --git a/src/data/activeDataInterface.ts b/src/data/activeDataInterface.ts index f46f2af..6a55047 100644 --- a/src/data/activeDataInterface.ts +++ b/src/data/activeDataInterface.ts @@ -1,4 +1,4 @@ -import frameworkConfig from '../frameworkConfig' +import ccsModelInterfaceConfig from '../ccsModelInterfaceConfig' import IDMismatchError from '../errors/idMismatchError' import KeysDoNotMatchError from '../errors/keysDoNotMatchError' import RowExistsError from '../errors/rowExistsError' @@ -10,7 +10,7 @@ import { getRow, getTable } from './dataInterface' import { Request } from 'express' import { TableRow, Tables } from '../types/data/tables' -const ACTIVE_DATA_SCHEMA_PATH: string = frameworkConfig.activeDataSchemaPath +const ACTIVE_DATA_SCHEMA_PATH: string = ccsModelInterfaceConfig.activeDataSchemaPath const activeDataSchema: ActiveDataSchema = require.main?.require(`./${ACTIVE_DATA_SCHEMA_PATH}`).default as ActiveDataSchema diff --git a/src/data/staticDataInterface.ts b/src/data/staticDataInterface.ts index c4a79f7..619c1db 100644 --- a/src/data/staticDataInterface.ts +++ b/src/data/staticDataInterface.ts @@ -1,9 +1,9 @@ -import frameworkConfig from '../frameworkConfig' +import ccsModelInterfaceConfig from '../ccsModelInterfaceConfig' import { Condition } from '../types/models/model' import { getRow, getTable } from './dataInterface' import { TableRow, Tables } from '../types/data/tables' -const STATIC_DATA_PATH: string = frameworkConfig.staticDataPath +const STATIC_DATA_PATH: string = ccsModelInterfaceConfig.staticDataPath const staticData: Tables = require.main?.require(`./${STATIC_DATA_PATH}`).default as Tables diff --git a/src/frameworkConfig.ts b/src/frameworkConfig.ts deleted file mode 100644 index ffa5e59..0000000 --- a/src/frameworkConfig.ts +++ /dev/null @@ -1,5 +0,0 @@ -import FrameworkConfig from './types/frameworkConfig' - -const frameworkConfig: FrameworkConfig = require.main?.require('./frameworkConfig.json') as FrameworkConfig - -export default frameworkConfig \ No newline at end of file diff --git a/src/models/activeModel.ts b/src/models/activeModel.ts index 8ee4a99..be95939 100644 --- a/src/models/activeModel.ts +++ b/src/models/activeModel.ts @@ -3,7 +3,7 @@ import Model from './model' import StaticModel from './staticModel' import StaticModelValidator from '../validation/validators/staticModelValidator' import Validator from '../validation/validator' -import { ActiveModelData, ActiveModelInterface, Condition, DataInterfaceFunction, ModelError, ModelSchema } from '../types/models/model' +import { ActiveModelData, ActiveModelErrors, ActiveModelInterface, Condition, DataInterfaceFunction, ModelSchema } from '../types/models/model' import { addActiveRow, getActiveRow, getActiveTable, setActiveRow } from '../data/activeDataInterface' import { ErrorMessages, GenericValidatorOptions } from '../types/validation/validator' import { Request } from 'express' @@ -17,7 +17,7 @@ abstract class ActiveModel extends Model implements ActiveModelInterface { abstract modelSchema: ModelSchema abstract validationSchema: ValidationSchema - errors: {[key: string]: ModelError} = {} + errors: ActiveModelErrors = {} constructor(data: ActiveModelData) { super(data) diff --git a/src/types/ccsModelInterfaceConfig.ts b/src/types/ccsModelInterfaceConfig.ts new file mode 100644 index 0000000..7458a69 --- /dev/null +++ b/src/types/ccsModelInterfaceConfig.ts @@ -0,0 +1,6 @@ +type CCSModelInterfaceConfig = { + staticDataPath: string + activeDataSchemaPath: string +} + +export default CCSModelInterfaceConfig \ No newline at end of file diff --git a/src/types/frameworkConfig.ts b/src/types/frameworkConfig.ts deleted file mode 100644 index d933f31..0000000 --- a/src/types/frameworkConfig.ts +++ /dev/null @@ -1,6 +0,0 @@ -type FrameworkConfig = { - staticDataPath: string - activeDataSchemaPath: string -} - -export default FrameworkConfig \ No newline at end of file diff --git a/src/types/models/model.ts b/src/types/models/model.ts index 8a56ccf..0756874 100644 --- a/src/types/models/model.ts +++ b/src/types/models/model.ts @@ -11,7 +11,7 @@ export interface ActiveModelInterface { tableName: string modelSchema: ModelSchema validationSchema: ValidationSchema - errors: {[key: string]: ModelError} + errors: ActiveModelErrors attributes(): object validate(call: string): boolean addError(attribute: string, error: string, message: string): void @@ -35,6 +35,10 @@ export type ModelError = { errorMessage: string } +export type ActiveModelErrors = { + [key: string]: ModelError +} + export type ListError = { text: string href: string diff --git a/src/types/validation/validator.ts b/src/types/validation/validator.ts index f9e1120..6b3ced4 100644 --- a/src/types/validation/validator.ts +++ b/src/types/validation/validator.ts @@ -39,7 +39,6 @@ export interface ValidatorInterface { attribute: string errorMessages: ErrorMessages options: ValidatorOptions - condition: boolean error?: string valid(call: string): boolean } diff --git a/src/validation/validator.ts b/src/validation/validator.ts index d4b2216..4f9f0f6 100644 --- a/src/validation/validator.ts +++ b/src/validation/validator.ts @@ -7,7 +7,6 @@ abstract class Validator implements ValidatorInterface { options: ValidatorOptions attribute: string errorMessages: ErrorMessages - condition: boolean = true error: string = '' constructor(model: ActiveModel, attribute: string, errorMessages: ErrorMessages, options: ValidatorOptions) { @@ -15,19 +14,23 @@ abstract class Validator implements ValidatorInterface { this.model = model this.attribute = attribute this.errorMessages = errorMessages - - if (options.conditions !== undefined) { - this.condition = options.conditions.every(condition => condition(model)) - } } abstract _validate(): boolean - valid = (call: string) => { - if (!this.condition) return true + private condition = (): boolean => { + if (this.options.conditions === undefined) { + return true + } else { + return this.options.conditions.every(condition => condition(this.model)) + } + } + valid = (call: string) => { if (this.options.on !== undefined && !this.options.on.includes(call)) return true + if (!this.condition()) return true + if (!this._validate()) { this.model.addError(this.attribute, this.error, this.errorMessages[this.error])