├── .codeclimate.yml
├── .editorconfig
├── .eslintrc
├── .gitbook.yaml
├── .gitignore
├── .nvmrc
├── .prettierignore
├── .prettierrc.json
├── .travis.yml
├── CHANGELOG.md
├── README.md
├── battlecry
└── generators
│ └── schema
│ ├── Attribute.js
│ ├── schema.generator.js
│ └── templates
│ └── __NaMe__.js
├── benchmark
├── benchmark.js
├── coercion.bm.js
├── instantiation.bm.js
├── suites.js
├── updating.bm.js
└── validation.bm.js
├── contributing.md
├── docs
├── SUMMARY.md
├── battlecry-generators.md
├── cloning.md
├── coercion
│ ├── README.md
│ ├── arrays-and-array-subclasses.md
│ ├── disabling-coercion.md
│ ├── generic-coercion.md
│ ├── primitive-type-coercion.md
│ └── recursive-coercion.md
├── custom-setters-and-getters.md
├── migrating-from-v1.md
├── schema-concept
│ ├── README.md
│ ├── circular-references-and-dynamic-types.md
│ ├── nullable-attributes.md
│ └── shorthand-and-complete-attribute-definition.md
├── serialization.md
├── strict-mode.md
├── support.md
├── testing.md
└── validation
│ ├── README.md
│ ├── array-validations.md
│ ├── attribute-reference.md
│ ├── boolean-validations.md
│ ├── date-validations.md
│ ├── nested-validations.md
│ ├── number-validations.md
│ ├── string-validations.md
│ └── validate-raw-data.md
├── lerna.json
├── license.md
├── package.json
├── packages
├── jest-structure
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── .prettierrc.js
│ ├── README.md
│ ├── extend-expect.js
│ ├── index.js
│ ├── license.md
│ ├── package.json
│ ├── src
│ │ ├── assertions
│ │ │ ├── toBeInvalidStructure.js
│ │ │ ├── toBeValidStructure.js
│ │ │ ├── toHaveInvalidAttribute.js
│ │ │ └── toHaveInvalidAttributes.js
│ │ └── lib
│ │ │ ├── attributePath.js
│ │ │ ├── errors.js
│ │ │ ├── sorting.js
│ │ │ └── validityAssertion.js
│ └── test
│ │ ├── __snapshots__
│ │ └── jest-structure.test.js.snap
│ │ └── jest-structure.test.js
└── structure
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── .prettierignore
│ ├── .prettierrc.js
│ ├── README.md
│ ├── dist
│ └── structure.js
│ ├── license.md
│ ├── package.json
│ ├── src
│ ├── attributes
│ │ └── index.js
│ ├── attributesDecorator.js
│ ├── cloning
│ │ └── index.js
│ ├── coercion
│ │ ├── coercion.js
│ │ ├── coercions
│ │ │ ├── array.js
│ │ │ ├── boolean.js
│ │ │ ├── date.js
│ │ │ ├── generic.js
│ │ │ ├── number.js
│ │ │ └── string.js
│ │ └── index.js
│ ├── descriptors
│ │ └── index.js
│ ├── errors
│ │ ├── DefaultValidationError.js
│ │ └── index.js
│ ├── index.js
│ ├── initialization
│ │ └── index.js
│ ├── schema
│ │ ├── AttributeDefinitions
│ │ │ ├── AttributeDefinition.js
│ │ │ └── index.js
│ │ └── index.js
│ ├── serialization
│ │ └── index.js
│ ├── strictMode
│ │ └── index.js
│ ├── symbols.js
│ └── validation
│ │ ├── forAttribute.js
│ │ ├── forSchema.js
│ │ ├── index.js
│ │ └── validations
│ │ ├── array.js
│ │ ├── boolean.js
│ │ ├── date.js
│ │ ├── nested.js
│ │ ├── number.js
│ │ ├── string.js
│ │ └── utils.js
│ ├── test
│ ├── fixtures
│ │ ├── BooksCollection.js
│ │ ├── BrokenCircularBook.js
│ │ ├── CircularBook.js
│ │ ├── CircularBookCustomIdentifier.js
│ │ ├── CircularUser.js
│ │ └── CircularUserCustomIdentifier.js
│ ├── jest.browser.js
│ ├── jest.node.js
│ ├── support
│ │ └── setup.js
│ ├── unit
│ │ ├── __snapshots__
│ │ │ └── instanceAndUpdate.spec.js.snap
│ │ ├── coercion
│ │ │ ├── array.spec.js
│ │ │ ├── arraySubclass.spec.js
│ │ │ ├── boolean.spec.js
│ │ │ ├── coercion.spec.js
│ │ │ ├── date.spec.js
│ │ │ ├── number.spec.js
│ │ │ ├── pojo.spec.js
│ │ │ ├── string.spec.js
│ │ │ ├── structure.spec.js
│ │ │ └── typeCoercion.spec.js
│ │ ├── creatingStructureClass.spec.js
│ │ ├── featureSwitches
│ │ │ └── coercion.spec.js
│ │ ├── instanceAndUpdate.spec.js
│ │ ├── serialization
│ │ │ ├── array.spec.js
│ │ │ ├── jsonStringifyCompatibility.spec.js
│ │ │ ├── nestedStructure.spec.js
│ │ │ └── structure.spec.js
│ │ ├── subclassingStructureClass.spec.js
│ │ └── validation
│ │ │ ├── array.spec.js
│ │ │ ├── boolean.spec.js
│ │ │ ├── date.spec.js
│ │ │ ├── nestedPojo.spec.js
│ │ │ ├── nestedStructure.spec.js
│ │ │ ├── number.spec.js
│ │ │ ├── staticMethod.spec.js
│ │ │ ├── string.spec.js
│ │ │ └── structureSubclass.spec.js
│ └── webpack.pretest.js
│ └── webpack.config.js
├── structure.jpg
└── yarn.lock
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | engines:
2 | eslint:
3 | enabled: true
4 | channel: 'eslint-6'
5 |
6 | ratings:
7 | paths:
8 | - packages/structure/src/**
9 |
10 | exclude_paths:
11 | - benchmark/**/*
12 | - packages/structure/dist/**/*
13 | - packages/structure/test/**/*
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_style = space
8 | indent_size = 2
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true,
4 | "es6": true,
5 | "jest/globals": true
6 | },
7 | "extends": ["eslint:recommended", "plugin:jest/recommended", "prettier"],
8 | "plugins": ["jest"],
9 | "parserOptions": {
10 | "ecmaVersion": 2018
11 | },
12 | "rules": {
13 | "comma-spacing": ["error", { "before": false, "after": true }],
14 | "complexity": ["error", 4],
15 | "indent": ["error", 2],
16 | "linebreak-style": ["error", "unix"],
17 | "object-shorthand": [
18 | "error",
19 | "always",
20 | { "avoidQuotes": true, "avoidExplicitReturnArrows": true }
21 | ],
22 | "jest/no-disabled-tests": "warn",
23 | "jest/no-focused-tests": "error",
24 | "jest/no-identical-title": "error",
25 | "jest/prefer-to-have-length": "warn",
26 | "jest/valid-expect": "error",
27 | "jest/no-test-callback": "off",
28 | "jest/no-try-expect": "off",
29 | "quotes": ["error", "single", { "avoidEscape": true }],
30 | "semi": ["error", "always"]
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/.gitbook.yaml:
--------------------------------------------------------------------------------
1 | structure:
2 | readme: packages/structure/README.md
3 | summary: docs/SUMMARY.md
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | coverage/
3 | *.log
4 | .env
5 | package-lock.json
6 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 10.13.0
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "semi": true,
5 | "singleQuote": true,
6 | "arrowParens": "always",
7 | "endOfLine": "lf",
8 | "printWidth": 100
9 | }
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '10'
4 | - '11'
5 | - '12'
6 | - 'node'
7 |
8 | addons:
9 | apt:
10 | packages:
11 | - xvfb
12 |
13 | install:
14 | - yarn
15 | - export DISPLAY=':99.0'
16 | - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
17 |
18 | before_script:
19 | - yarn test:browser:build
20 |
21 | script:
22 | - yarn lint
23 | - yarn test
24 | - yarn test:browser:run
25 | - yarn coveralls
26 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 2.0.1 - 2020-06-15
2 |
3 | Fix:
4 |
5 | - Fix deep nested dynamic types validation [[#132](https://github.com/talyssonoc/structure/issues/132)]
6 |
7 | ## 2.0.0 - 2020-03-31
8 |
9 | Refactors:
10 |
11 | - The whole part of schemas and attribute definitions was refactored
12 | - Tests are now run by Jest (and Electron for browser tests)
13 | - Prettier was added
14 | - Move to mono-repo
15 |
16 | Enhancements
17 |
18 | - Implement jest-structure assertions
19 | - It's possible to set custom getters e setters directly in the structure class
20 | - Allows to disable coercion
21 |
22 | Breaking changes:
23 |
24 | - Joi is updated to v16
25 | - Attribute path in validation _errors_ is an array instead of a string
26 | - Attribute path in validation _messages_ contains the whole path joined by '.'
27 | - The name used for the dynamic import should aways be the same as the name of its type or else a custom identifier must be used
28 | - Non-nullable attributes with value null will use default value the same way undefined does
29 | - Structure classes now have two methods to generically set and get the value of the attributes, `.get(attributeName)` and `.set(attributeName, attributeValue)`
30 | - Minimum Node version is now 10
31 |
32 | Docs:
33 |
34 | - Rename the term `type descriptor` to `attribute definition` in the docs and in the code
35 | - Reorganize and add more specific pages to docs
36 |
37 | ## 2.0.0-alpha.4 - 2020-03-21
38 |
39 | - Publish only src folder for jest-structure
40 |
41 | ## 2.0.0-alpha.3 - 2020-03-20
42 |
43 | - Reorganize md files
44 |
45 | ## 2.0.0-alpha.2 - 2020-03-19
46 |
47 | - Invert symlinks
48 |
49 | ## 2.0.0-alpha.1 - 2020-03-19
50 |
51 | - Add symlinks to md files to packages/structure
52 |
53 | ## 2.0.0-alpha.0 - 2020-03-19
54 |
55 | Refactors:
56 |
57 | - The whole part of schemas and attribute definitions was refactored
58 | - Tests are now run by Jest (and Electron for browser tests)
59 | - Prettier was added
60 | - Move to mono-repo
61 |
62 | Enhancements
63 |
64 | - Implement jest-structure assertions
65 | - It's possible to set custom getters e setters directly in the structure class
66 |
67 | Breaking changes:
68 |
69 | - Joi is updated to v16
70 | - Attribute path in validation _errors_ is an array instead of a string
71 | - Attribute path in validation _messages_ contains the whole path joined by '.'
72 | - The name used for the dynamic import should aways be the same as the name of its type or else a custom identifier must be used
73 | - Non-nullable attributes with value null will use default value the same way undefined does
74 | - Structure classes now have two methods to generically set and get the value of the attributes, `.get(attributeName)` and `.set(attributeName, attributeValue)`
75 | - Minimum Node version is now 10
76 |
77 | ## 1.8.0 - 2019-09-16
78 |
79 | Enhancements:
80 |
81 | - Add `unique` validation to arrays
82 |
83 | ## 1.7.0 - 2019-09-14
84 |
85 | Enhancements:
86 |
87 | - Add method to clone structures
88 |
89 | ## 1.6.0 - 2019-08-27
90 |
91 | Enhancements:
92 |
93 | - Allow custom error class to static mode
94 |
95 | ## 1.5.0 - 2019-07-08
96 |
97 | Enhancements:
98 |
99 | - Add `buildStrict` static method
100 |
101 | ## 1.4.0 - 2019-03-26
102 |
103 | Enhancements:
104 |
105 | - Add `nullable` option
106 |
107 | ## 1.3.2 - 2019-03-22
108 |
109 | Fix:
110 |
111 | - The actual instance is passed to the dynamic defaults
112 |
113 | ## 1.3.0 - 2018-03-23
114 |
115 | Enhancements:
116 |
117 | - When using default function to initialize attributes you can now refer to another attribute values to compose value
118 |
119 | ## 1.2.0 - 2017-02-01
120 |
121 | Features:
122 |
123 | - Allow circular reference on type definitions ([@talyssonoc](https://github.com/talyssonoc/structure/pull/30))
124 |
125 | Enhancements:
126 |
127 | - Make validation faster ([@talyssonoc](https://github.com/talyssonoc/structure/pull/28))
128 |
129 | Dependencies update:
130 |
131 | - Update joi from 9.2.0 to 10.2.0 ([@talyssonoc](https://github.com/talyssonoc/structure/pull/26))
132 |
133 | ## 1.1.0 - 2017-01-17
134 |
135 | - Added static method `validate()` to structures ([@talyssonoc](https://github.com/talyssonoc/structure/pull/25))
136 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #
2 |
3 | ## A simple schema/attributes library built on top of modern JavaScript
4 |
5 | Structure provides a simple interface which allows you to add attributes to your classes based on a schema, with validations and type coercion.
6 |
7 | ## Packages
8 |
9 | - [Structure](packages/structure)
10 | - [jest-structure](packages/jest-structure)
11 |
12 | ## [Documentation](https://structure.js.org/)
13 |
14 | ## Example Structure usage
15 |
16 | For each attribute on your schema, a getter and a setter will be created into the given class. It'll also auto-assign those attributes passed to the constructor.
17 |
18 | ```js
19 | const { attributes } = require('structure');
20 |
21 | const User = attributes({
22 | name: String,
23 | age: {
24 | type: Number,
25 | default: 18,
26 | },
27 | birthday: Date,
28 | })(
29 | class User {
30 | greet() {
31 | return `Hello ${this.name}`;
32 | }
33 | }
34 | );
35 |
36 | const user = new User({
37 | name: 'John Foo',
38 | });
39 |
40 | user.name; // 'John Foo'
41 | user.greet(); // 'Hello John Foo'
42 | ```
43 |
44 | ## [Contributing](contributing.md)
45 |
46 | ## [LICENSE](license.md)
47 |
--------------------------------------------------------------------------------
/battlecry/generators/schema/Attribute.js:
--------------------------------------------------------------------------------
1 | import { casex } from 'battlecry';
2 |
3 | const ALIASES = {
4 | Number: ["int", "integer"],
5 | String: ["text"],
6 | Boolean: ["bool"]
7 | };
8 |
9 | export default class Attribute {
10 | constructor(value) {
11 | const [name, type, ...modifiers] = value.split(":");
12 | this.name = name;
13 | this.type = type || "string";
14 | this.modifiers = modifiers;
15 | }
16 |
17 | static alias(type) {
18 | const lowerType = type.toLowerCase();
19 | const keys = Object.keys(ALIASES);
20 |
21 | const alias = keys.find(key => {
22 | return ALIASES[key].find(
23 | alias => alias.toLowerCase() === lowerType
24 | );
25 | });
26 |
27 | return alias || casex(type, 'NaMe');
28 | }
29 |
30 | get aliasedType() {
31 | return this.array ? "Array" : this.itemType;
32 | }
33 |
34 | get itemType() {
35 | return Attribute.alias(this.type.split("[]")[0]);
36 | }
37 |
38 | get required() {
39 | return this.modifiers.includes("required");
40 | }
41 |
42 | get default() {
43 | return this.modifiers.includes("default");
44 | }
45 |
46 | get array() {
47 | return this.type.includes("[]");
48 | }
49 |
50 | get complex() {
51 | return this.array || this.required || this.default;
52 | }
53 |
54 | get text() {
55 | if (!this.complex) return ` __naMe__: ${this.aliasedType},`;
56 |
57 | this.contents = [];
58 | this.push(" __naMe__: {");
59 | this.push(` type: ${this.aliasedType},`);
60 | this.push(` itemType: ${this.itemType},`, this.array);
61 | this.push(" required: true,", this.required);
62 | this.push(" default: [],", this.default);
63 | this.push(" },");
64 |
65 | return this.contents;
66 | }
67 |
68 | push(value, condition = true) {
69 | if (condition) this.contents.push(value);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/battlecry/generators/schema/schema.generator.js:
--------------------------------------------------------------------------------
1 | import { Generator, File } from 'battlecry';
2 | import Attribute from './Attribute';
3 |
4 | const DIST = 'src/domain/__na-me__'
5 |
6 | export default class SchemaGenerator extends Generator {
7 | config = {
8 | generate: {
9 | args: 'name ...props?',
10 | description: 'Create or modify schema adding attributes and methods'
11 | }
12 | };
13 |
14 | get props() {
15 | const props = this.args.props.length ? this.args.props : ['name'];
16 | return props.reverse();
17 | }
18 |
19 | generate() {
20 | const template = this.template();
21 | let file = new File(`${DIST}/${template.filename}`, this.args.name);
22 | if(!file.exists) file = template;
23 |
24 | this.props.forEach(prop => {
25 | if(prop.startsWith(':')) this.addMethod(file, prop)
26 | else this.addAttribute(file, prop);
27 | });
28 |
29 | file.saveAs(`${DIST}/${template.filename}`, this.args.name);
30 | }
31 |
32 | addAttribute(file, name) {
33 | const attribute = new Attribute(name);
34 | file.after('attributes({', attribute.text, attribute.name);
35 | }
36 |
37 | addMethod(file, name) {
38 | file.after('class ', [
39 | ' __naMe__() {',
40 | ' // Do something here',
41 | ' }',
42 | ''
43 | ], name.substring(1));
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/battlecry/generators/schema/templates/__NaMe__.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('structure');
2 |
3 | const __NaMe__ = attributes({
4 | })(class __NaMe__ {
5 | });
6 |
7 | module.exports = __NaMe__;
8 |
--------------------------------------------------------------------------------
/benchmark/benchmark.js:
--------------------------------------------------------------------------------
1 | const Benchmark = require('benchmark');
2 | const suites = require('./suites');
3 |
4 | suites.forEach((suiteData) => {
5 | let suite = new Benchmark.Suite(suiteData.name);
6 |
7 | suite = suiteData.cases.reduce((s, c) => {
8 | return s.add(c.name, c.fn);
9 | }, suite);
10 |
11 | suite
12 | .on('start', function() {
13 | // eslint-disable-next-line no-console
14 | console.log(`# ${this.name}:`);
15 | })
16 |
17 | .on('cycle', (event) => {
18 | // eslint-disable-next-line no-console
19 | console.log(String(event.target));
20 | })
21 |
22 | .on('complete', () => {
23 | // eslint-disable-next-line no-console
24 | console.log('\n=========\n');
25 | })
26 |
27 | .run(true);
28 | });
29 |
--------------------------------------------------------------------------------
/benchmark/coercion.bm.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../packages/structure/src');
2 |
3 | const Book = attributes({
4 | year: Number
5 | })(class Book { });
6 |
7 | class FantasyBooksCollection extends Array { }
8 | class FriendsCollection extends Array { }
9 | class Item { }
10 |
11 | const User = attributes({
12 | name: String,
13 | item: Item,
14 | favoriteBook: Book,
15 | books: {
16 | type: Array,
17 | itemType: Book
18 | },
19 | fantasyBooks: {
20 | type: FantasyBooksCollection,
21 | itemType: Book
22 | },
23 | friendsNames: {
24 | type: FriendsCollection,
25 | itemType: String
26 | }
27 | })(class User { });
28 |
29 | const CircularUser = require('../packages/structure/test/fixtures/CircularUser');
30 |
31 | exports.name = 'Coercion';
32 |
33 | exports.cases = [
34 | {
35 | name: 'Primitive coercion',
36 | fn() {
37 | new User({
38 | name: 50
39 | });
40 | }
41 | },
42 | {
43 | name: 'Nested coercion [x1]',
44 | fn() {
45 | new User({
46 | item: {
47 | name: 'foo'
48 | }
49 | });
50 | }
51 | },
52 | {
53 | name: 'Nested coercion [x2]',
54 | fn() {
55 | new User({
56 | favoriteBook: {
57 | year: 2017
58 | }
59 | });
60 | }
61 | },
62 | {
63 | name: 'Nested coercion [x3]',
64 | fn() {
65 | new User({
66 | favoriteBook: {
67 | year: '2017'
68 | }
69 | });
70 | }
71 | },
72 | {
73 | name: 'Primitive coercion with dynamic types',
74 | fn() {
75 | new CircularUser({
76 | name: 50
77 | });
78 | }
79 | },
80 | {
81 | name: 'Nested coercion with dynamic types [x1]',
82 | fn() {
83 | new CircularUser({
84 | favoriteBook: {
85 | name: 'A Study in Scarlet'
86 | }
87 | });
88 | }
89 | },
90 | {
91 | name: 'Nested coercion with dynamic types [x2]',
92 | fn() {
93 | new CircularUser({
94 | favoriteBook: {
95 | name: 1984
96 | }
97 | });
98 | }
99 | },
100 | {
101 | name: 'Nested coercion with dynamic types [x3]',
102 | fn() {
103 | new CircularUser({
104 | friends: [
105 | new CircularUser(),
106 | new CircularUser()
107 | ]
108 | });
109 | }
110 | },
111 | {
112 | name: 'Nested coercion with dynamic types [x4]',
113 | fn() {
114 | new CircularUser({
115 | friends: [
116 | {},
117 | {}
118 | ]
119 | });
120 | }
121 | },
122 | {
123 | name: 'Nested coercion with dynamic types [x5]',
124 | fn() {
125 | new CircularUser({
126 | friends: [
127 | {
128 | favoriteBook: {}
129 | },
130 | {
131 | favoriteBook: {}
132 | }
133 | ]
134 | });
135 | }
136 | },
137 | {
138 | name: 'Array coercion [1x]',
139 | fn() {
140 | new User({
141 | books: [
142 | { year: 1 },
143 | { year: 2 },
144 | { year: 3 }
145 | ]
146 | });
147 | }
148 | },
149 | {
150 | name: 'Array coercion [2x]',
151 | fn() {
152 | new User({
153 | books: [
154 | { year: '1' },
155 | { year: '2' },
156 | { year: '3' }
157 | ]
158 | });
159 | }
160 | },
161 | {
162 | name: 'Array subclass coercion [1x]',
163 | fn() {
164 | new User({
165 | friendsNames: new FriendsCollection(1, 2, 3)
166 | });
167 | }
168 | },
169 | {
170 | name: 'Array subclass coercion [2x]',
171 | fn() {
172 | new User({
173 | fantasyBooks: new FantasyBooksCollection(
174 | { name: 'A' },
175 | { name: 'B' },
176 | { name: 'C' }
177 | )
178 | });
179 | }
180 | },
181 | {
182 | name: 'Array subclass coercion [3x]',
183 | fn() {
184 | new User({
185 | fantasyBooks: new FantasyBooksCollection(
186 | { year: '1' },
187 | { year: '2' },
188 | { year: '3' }
189 | )
190 | });
191 | }
192 | },
193 | {
194 | name: 'Array subclass coercion [4x]',
195 | fn() {
196 | new User({
197 | fantasyBooks: [
198 | { year: '1' },
199 | { year: '2' },
200 | { year: '3' }
201 | ]
202 | });
203 | }
204 | }
205 | ];
206 |
--------------------------------------------------------------------------------
/benchmark/instantiation.bm.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../packages/structure/src');
2 |
3 | const User = attributes({
4 | name: String,
5 | age: Number,
6 | isAdmin: Boolean
7 | })(class User { });
8 |
9 | const Product = attributes({
10 | name: String,
11 | manufacturer: User,
12 | manufacturedAt: Date,
13 | expiresAt: Date,
14 | purchasers: {
15 | type: Array,
16 | itemType: User
17 | }
18 | })(class Product { });
19 |
20 | const CircularUser = require('../packages/structure/test/fixtures/CircularUser');
21 | const CircularBook = require('../packages/structure/test/fixtures/CircularBook');
22 |
23 | exports.name = 'Instantiation';
24 |
25 | exports.cases = [
26 | {
27 | name: 'Simple instantiation',
28 | fn() {
29 | new User({
30 | name: 'A name',
31 | age: 99,
32 | isAdmin: true
33 | });
34 | }
35 | },
36 | {
37 | name: 'Complex instantiation',
38 | fn() {
39 | new Product({
40 | name: 'A product',
41 | manufacturer: new User({
42 | name: 'A manufacturer',
43 | age: 30,
44 | isAdmin: false
45 | }),
46 | manufacturedAt: new Date(),
47 | expiresAt: new Date(),
48 | purchasers: [
49 | new User(),
50 | new User(),
51 | new User()
52 | ]
53 | });
54 | }
55 | },
56 | {
57 | name: 'Simple instantiation with dynamic types',
58 | fn() {
59 | new CircularUser({
60 | name: 'A name'
61 | });
62 | }
63 | },
64 | {
65 | name: 'Complex instantiation with dynamic types [x1]',
66 | fn() {
67 | new CircularUser({
68 | name: 'A name',
69 | favoriteBook: new CircularBook({
70 | name: 'A book'
71 | })
72 | });
73 | }
74 | },
75 | {
76 | name: 'Complex instantiation with dynamic types [x2]',
77 | fn() {
78 | new CircularUser({
79 | name: 'A name',
80 | friends: [
81 | new CircularUser({
82 | name: 'A friend'
83 | }),
84 | new CircularUser({
85 | name: 'Another friend'
86 | })
87 | ],
88 | favoriteBook: new CircularBook({
89 | name: 'A book'
90 | })
91 | });
92 | }
93 | }
94 | ];
95 |
--------------------------------------------------------------------------------
/benchmark/suites.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | require('./coercion.bm'),
3 | require('./instantiation.bm'),
4 | require('./updating.bm'),
5 | require('./validation.bm')
6 | ];
7 |
--------------------------------------------------------------------------------
/benchmark/updating.bm.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../packages/structure/src');
2 |
3 | const Book = attributes({
4 | name: String
5 | })(class Book { });
6 |
7 | const User = attributes({
8 | name: String,
9 | age: Number,
10 | favoriteBook: Book
11 | })(class User {});
12 |
13 | exports.name = 'Updating';
14 |
15 | exports.cases = [
16 | {
17 | name: 'Updating without coercion',
18 | fn() {
19 | const user = new User();
20 |
21 | user.name = 'Something';
22 | user.age = 42;
23 | }
24 | },
25 | {
26 | name: 'Updating with simple coercion',
27 | fn() {
28 | const user = new User();
29 |
30 | user.name = 1337;
31 | user.age = '50';
32 | }
33 | },
34 | {
35 | name: 'Updating assigning to attributes without coercion',
36 | fn() {
37 | const user = new User();
38 |
39 | user.attributes = {
40 | name: 'Something',
41 | age: 42
42 | };
43 | }
44 | },
45 | {
46 | name: 'Updating assigning to attributes with coercion',
47 | fn() {
48 | const user = new User();
49 |
50 | user.attributes = {
51 | name: 1337,
52 | age: '50'
53 | };
54 | }
55 | },
56 | {
57 | name: 'Updating with nested coercion',
58 | fn() {
59 | const user = new User();
60 |
61 | user.favoriteBook = { name: 'The Silmarillion' };
62 | }
63 | }
64 | ];
65 |
--------------------------------------------------------------------------------
/benchmark/validation.bm.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../packages/structure/src');
2 |
3 | const Order = attributes({
4 | createdAt: Date,
5 | updatedAt: {
6 | type: Date,
7 | min: { attr: 'createdAt' }
8 | }
9 | })(class Order { });
10 |
11 | const User = attributes({
12 | name: {
13 | type: String,
14 | minLength: 3
15 | },
16 | age: {
17 | type: Number,
18 | positive: true
19 | },
20 | order: Order
21 | })(class User {});
22 |
23 | exports.name = 'Validation';
24 |
25 | exports.cases = [
26 | {
27 | name: 'With simple validation and is valid',
28 | fn() {
29 | const user = new User({
30 | name: 'Something',
31 | age: 10
32 | });
33 |
34 | user.validate();
35 | }
36 | },
37 | {
38 | name: 'With simple validation and is invalid',
39 | fn() {
40 | const user = new User({
41 | name: 'AB',
42 | age: -1
43 | });
44 |
45 | user.validate();
46 | }
47 | },
48 | {
49 | name: 'With nested validation and is valid',
50 | fn() {
51 | const user = new User({
52 | name: 'Something',
53 | age: 25,
54 | order: new Order({
55 | createdAt: new Date(),
56 | updatedAt: new Date()
57 | })
58 | });
59 |
60 | user.validate();
61 | }
62 | },
63 | {
64 | name: 'With nested validation and is invalid',
65 | fn() {
66 | const user = new User({
67 | name: 'Something',
68 | age: 25,
69 | order: new Order({
70 | createdAt: new Date(),
71 | updatedAt: new Date(0)
72 | })
73 | });
74 |
75 | user.validate();
76 | }
77 | }
78 | ];
79 |
--------------------------------------------------------------------------------
/contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Contributions are always welcome! When contributing to Structure we ask you to follow our code of conduct:
4 |
5 | ## Code of conduct
6 |
7 | In short: _Be nice_. Pay attention to the fact that Structure is free software, don't be rude with the contributors or with people with questions and we'll be more than glad to help you. Destructive criticism and demanding will be ignored.
8 |
9 | ## Opening issues
10 |
11 | When opening an issue be descriptive about the bug or the feature suggestion, don't simply paste the error message on the issue title or description. Also, **provide code to simulate the bug**, we need to know the exact circumstances in which the bug occurs. Again, follow our [code of conduct](#code-of-conduct).
12 |
13 | ## Pull requests
14 |
15 | When opening a pull request to Structure, follow this steps:
16 |
17 | 1. Fork Structure;
18 | 2. Create a new branch for your changes;
19 | 3. Do your changes;
20 | 4. Write tests for your changes;
21 | 5. Check the coverage;
22 | 6. Open the pull request;
23 | 7. Write a complete description about the bug or the feature the pull request is about.
24 |
25 | Be aware that we keep **100% of code coverage**, any change that makes the code coverage less than 100% covered will be requested to write more tests.
26 |
--------------------------------------------------------------------------------
/docs/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Table of contents
2 |
3 | - [Schema concept](schema-concept/README.md)
4 | - [Shorthand and complete attribute definition](schema-concept/shorthand-and-complete-attribute-definition.md)
5 | - [Circular reference](schema-concept/circular-references-and-dynamic-types.md)
6 | - [Nullable attributes](schema-concept/nullable-attributes.md)
7 | - [Custom setters and getters](custom-setters-and-getters.md)
8 | - [Coercion](coercion/README.md)
9 | - [Primitive type coercion](coercion/primitive-type-coercion.md)
10 | - [Arrays coercion](coercion/arrays-and-array-subclasses.md)
11 | - [Generic coercion](coercion/generic-coercion.md)
12 | - [Recursive coercion](coercion/recursive-coercion.md)
13 | - [Disabling coercion](coercion/disabling-coercion.md)
14 | - [Validation](validation/README.md)
15 | - [String validations](validation/string-validations.md)
16 | - [Number validations](validation/number-validations.md)
17 | - [Boolean validations](validation/boolean-validations.md)
18 | - [Date validations](validation/date-validations.md)
19 | - [Array validations](validation/array-validations.md)
20 | - [Attribute reference](validation/attribute-reference.md)
21 | - [Nested validations](validation/nested-validations.md)
22 | - [Validate raw data](validation/validate-raw-data.md)
23 | - [Strict mode](strict-mode.md)
24 | - [Cloning an instance](cloning.md)
25 | - [Serialization](serialization.md)
26 | - [Testing](testing.md)
27 | - [Battlecry generators](battlecry-generators.md)
28 | - [Migrating from v1](migrating-from-v1.md)
29 | - [Support and compatibility](support.md)
30 | - [Changelog](../CHANGELOG.md)
31 | - [Contributing](../contributing.md)
32 | - [License](../license.md)
33 | - [GitHub](https://github.com/talyssonoc/structure)
34 |
--------------------------------------------------------------------------------
/docs/battlecry-generators.md:
--------------------------------------------------------------------------------
1 | # BattleCry generators
2 |
3 | There are configurable [BattleCry](https://github.com/pedsmoreira/battlecry) generators ready to be downloaded and help scaffolding schema:
4 |
5 | ```sh
6 | npm install -g battlecry
7 | cry download generator talyssonoc/structure
8 | cry g schema user name age:int:required cars:string[] favoriteBook:book friends:user[]:default :updateAge
9 | ```
10 |
11 | Run `cry --help` to check more info about the generators available;
12 |
--------------------------------------------------------------------------------
/docs/cloning.md:
--------------------------------------------------------------------------------
1 | # Cloning an instance
2 |
3 | Structure adds a method `#clone` in order to be able to create a **shallow** copy of an instance. This methods accepts an optional overwrite object that permits you to overwrite some attributes of the copy.
4 |
5 | ```js
6 | const { attributes } = require('structure');
7 |
8 | const User = attributes({
9 | name: String,
10 | })(class User {});
11 |
12 | const user = new User({
13 | name: 'Me',
14 | });
15 |
16 | const cloneUserWithNoOverwrite = user.clone(); // User { name: 'Me }
17 |
18 | const cloneWithOverwrite = user.clone({ name: 'Myself' }); // User { name: 'Myself' }
19 | ```
20 |
21 | If the structure has a nested structure inside of it, the `#clone` method **will not** clone it but just point the new instance to the old value of the nested attribute.
22 |
23 | ```js
24 | const { attributes } = require('structure');
25 |
26 | const Book = attributes({
27 | name: String,
28 | })(class Book {});
29 |
30 | const User = attributes({
31 | name: String,
32 | favoriteBook: Book,
33 | })(class User {});
34 |
35 | const user = new User({
36 | name: 'Me',
37 | favoriteBook: new Book({ name: 'The Silmarillion' }),
38 | });
39 |
40 | const cloneUserWithNoOverwrite = user.clone();
41 | cloneUserWithNoOverwrite.favoriteBook === user.favoriteBook; // true, it was not cloned
42 |
43 | const cloneWithOverwrite = user.clone({
44 | favoriteBook: { name: 'The Lord of the Rings' },
45 | });
46 | cloneWithOverwrite.favoriteBook === user.favoriteBook; // false, it was **replaced** with the new value
47 | cloneWithOverwrite.favoriteBook; // Book { name: 'The Lord of the Rings' }
48 | ```
49 |
50 | ## Strict mode
51 |
52 | When cloning an instance, you can clone it in [strict mode](strict-mode.md) as well, so if the resulting clone is invalid it throws an error. To do that, pass a second argument to the `#clone` method with the option `strict` as `true`.
53 |
54 | ```js
55 | const { attributes } = require('structure');
56 |
57 | const User = attributes({
58 | name: {
59 | type: String,
60 | required: true,
61 | },
62 | age: Number,
63 | })(class User {});
64 |
65 | const user = new User({
66 | name: 'Me',
67 | });
68 |
69 | const clonedUser = user.clone(
70 | { name: null },
71 | { strict: true } // strict mode option
72 | );
73 |
74 | // Error: Invalid Attributes
75 | // details: [
76 | // { message: '"name" is required', path: ['name'] }
77 | // ]
78 | ```
79 |
--------------------------------------------------------------------------------
/docs/coercion/README.md:
--------------------------------------------------------------------------------
1 | # Coercion
2 |
3 | Structure does type coercion based on the declared [schema](../schema-concept/README.md), let's break it into 3 categories:
4 |
5 | - [Primitive type coercion](primitive-type-coercion.md)
6 | - [Arrays coercion](arrays-and-array-subclasses.md)
7 | - [Generic coercion](generic-coercion.md)
8 |
9 | ## Observations
10 |
11 | Structure **never** coerces the following scenarios:
12 |
13 | - value is `undefined`;
14 | - value is `null` when `nullable` option is enabled;
15 | - value is already of the declared type (except for arrays, we'll talk more about this soon).
16 |
--------------------------------------------------------------------------------
/docs/coercion/arrays-and-array-subclasses.md:
--------------------------------------------------------------------------------
1 | # Arrays and Array subclasses
2 |
3 | It's also possible to coerce values to `Array` or some other class that extends `Array`. On these circumstances Structure will use the `itemType` value of the attribute definition on the schema to coerce the items as well. Note that, when coercing arrays, it'll always create a new instance of the type and then push each item of the passed value to the new instance:
4 |
5 | ```javascript
6 | class BooksCollection extends Array {}
7 |
8 | const Library = attributes({
9 | books: {
10 | type: BooksCollection,
11 | itemType: String,
12 | },
13 | users: {
14 | type: Array,
15 | itemType: String,
16 | },
17 | })(class Library {});
18 |
19 | const libraryOne = new Library({
20 | books: ['Brave New World'],
21 | users: ['John', 1],
22 | });
23 |
24 | libraryOne.books; // BooksCollection ['Brave New World'] => coerced the array to BooksCollection
25 | libraryOne.users; // ['John', '1'] => new instance of Array with coerced items
26 | ```
27 |
28 | The passed raw value have to be non-null and have a `length` attribute or implement the `Symbol.iterator` method, otherwise it'll fail to coerce and throw a `TypeError`.
29 |
30 | ## Observations
31 |
32 | Structure only does array **items** coercion during instantiation, so mutating an array (using push, for example) won't coerce the new item:
33 |
34 | ```javascript
35 | const Library = attributes({
36 | books: {
37 | type: Array,
38 | itemType: String,
39 | },
40 | })(class Library {});
41 |
42 | const library = new Library({
43 | books: [1984],
44 | });
45 |
46 | library.books; // ['1984'] => coerced number to string
47 |
48 | library.books.push(42);
49 |
50 | library.books; // ['1984', 42] => new item was not coerced
51 | ```
52 |
--------------------------------------------------------------------------------
/docs/coercion/disabling-coercion.md:
--------------------------------------------------------------------------------
1 | # Disabling coercion
2 |
3 | You can disable coercion for a whole structure or for attributes individually using the `coercion` option in the schema and attribute options, respectively. Notice that it will cause validation to fail when the passed value is not of the expected value:
4 |
5 | ## Disabling for the whole structure
6 |
7 | ```js
8 | const User = attributes(
9 | {
10 | name: String,
11 | age: Number,
12 | },
13 | {
14 | coercion: false,
15 | }
16 | )(class User {});
17 |
18 | const user = new User({ name: 123, age: '42' });
19 |
20 | user.name; // 123
21 | user.age; // '42'
22 |
23 | const { valid, errors } = user.validate();
24 |
25 | valid; // false
26 | errors; /*
27 | [
28 | { message: '"name" must be a string', path: ['name'] },
29 | { message: '"age" must be a number', path: ['age'] }
30 | ]
31 | */
32 | ```
33 |
34 | ## Disabling for specific attributes
35 |
36 | ```js
37 | const User = attributes({
38 | name: { type: String, coercion: false },
39 | age: Number,
40 | })(class User {});
41 |
42 | const user = new User({ name: 123, age: '42' });
43 |
44 | user.name; // 123
45 | user.age; // 42
46 |
47 | const { valid, errors } = user.validate();
48 |
49 | valid; // false
50 | errors; /*
51 | [
52 | { message: '"name" must be a string', path: ['name'] }
53 | ]
54 | */
55 | ```
56 |
57 | ## Overwritting structure option with attribute option
58 |
59 | If you define the `coercion` option both for the structure _and_ for an attribute, the structure one will apply for the whole schema except the specific attributes that overwrite it:
60 |
61 | ```js
62 | const User = attributes(
63 | {
64 | name: { type: String, coercion: true },
65 | age: Number,
66 | },
67 | {
68 | coercion: false,
69 | }
70 | )(class User {});
71 |
72 | const user = new User({ name: 123, age: '42' });
73 |
74 | user.name; // '123'
75 | user.age; // '42'
76 |
77 | const { valid, errors } = user.validate();
78 |
79 | valid; // false
80 | errors; /*
81 | [
82 | { message: '"age" must be a number', path: ['age'] }
83 | ]
84 | */
85 | ```
86 |
--------------------------------------------------------------------------------
/docs/coercion/generic-coercion.md:
--------------------------------------------------------------------------------
1 | # Generic coercion
2 |
3 | If the declared type is not a primitive nor Array (or an array subclass) it'll do generic coercion. When generic coercing a value, Structure will just instantiate the declared type (using `new`) passing the raw value as the parameter (only if the raw value isn't of the declared type already).
4 |
5 | ```javascript
6 | class Location {
7 | constructor({ x, y }) {
8 | this.x = x;
9 | this.y = y;
10 | }
11 | }
12 |
13 | const User = attributes({
14 | location: Location,
15 | })(class User {});
16 |
17 | const userOne = new User({
18 | location: new Location({ x: 1, y: 2 }),
19 | });
20 |
21 | userOne.location; // Location { x: 1, y: 2 } => no coercion was done
22 |
23 | const userTwo = new User({
24 | location: { x: 3, y: 4 },
25 | });
26 |
27 | userTwo.location; // Location { x: 3, y: 4 } => coerced plain object to Location
28 | ```
29 |
30 | Coercion to `Date` type enters in this same category, so if you have an attribute of the type `Date`, it'll use `new Date()` to coerce it. For more info about how this coercion works check the cases for `value` and `dateString` parameters on [Date documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date).
31 |
--------------------------------------------------------------------------------
/docs/coercion/primitive-type-coercion.md:
--------------------------------------------------------------------------------
1 | # Primitive type coercion
2 |
3 | It's said to be primitive type coercion when it tries to coerce values to `String`, `Number` or `Boolean` types.
4 |
5 | For those types we basically use the type as a function (without using `new`), with a subtle difference: When coercing `null` to `String`, it'll coerce to empty string instead of the string `'null'` (unless when the attribute is [nullable](../schema-concept/nullable-attributes.md)). For example:
6 |
7 | ```js
8 | const User = attributes({
9 | name: String,
10 | age: Number,
11 | isAdmin: Boolean,
12 | })(class User {});
13 |
14 | const userOne = new User({
15 | name: 'Foo Bar',
16 | age: 50,
17 | isAdmin: true,
18 | });
19 |
20 | userOne.name; // 'Foo Bar' => no coercion was done
21 | userOne.age; // 50 => no coercion was done
22 | userOne.isAdmin; // true => no coercion was done
23 |
24 | const userTwo = new User({
25 | name: null,
26 | age: '100',
27 | isAdmin: undefined,
28 | });
29 |
30 | userTwo.name; // '' => coerced `null` to empty string
31 | userTwo.age; // 100 => coerced string to number
32 | userTwo.isAdmin; // undefined => it'll never coerce `undefined`
33 | ```
34 |
--------------------------------------------------------------------------------
/docs/coercion/recursive-coercion.md:
--------------------------------------------------------------------------------
1 | # Recursive coercion
2 |
3 | Structure also does recursive coercion so, if your declared type is Array or other Structure, the items/attributes of the raw value will be coerced as well:
4 |
5 | ```javascript
6 | class BooksCollection extends Array {}
7 |
8 | const Book = attributes({
9 | name: String,
10 | })(class Book {});
11 |
12 | const User = attributes({
13 | favoriteBook: Book,
14 | books: {
15 | type: BooksCollection,
16 | itemType: Book,
17 | },
18 | })(class User {});
19 |
20 | const user = new User({
21 | favoriteBook: { name: 'The Silmarillion' },
22 | books: [{ name: '1984' }],
23 | });
24 |
25 | user.favoriteBook; // Book { name: 'The Silmarillion' } => coerced plain object to Book
26 | user.books; // BooksCollection [ Book { name: '1984' } ] => coerced array to BooksCollection and plain object to Book
27 | ```
28 |
--------------------------------------------------------------------------------
/docs/custom-setters-and-getters.md:
--------------------------------------------------------------------------------
1 | # Custom setters and getters
2 |
3 | Sometimes it may be necessary to have custom setters and/or getters for some attributes. Structure allows you to do that using native JavaScript setters and getters. It will even support coercion.
4 |
5 | It's important to notice that you **should not** try to access the attribute directly inside its getter or to set it directly inside its setter because it will cause infinite recursion, this is default JavaScript behavior. To access an attribute value inside its getter you should use `this.get(attributeName)`, and to set the value of an attribute a setter you should use `this.set(attributeName, attributeValue)`:
6 |
7 | ```js
8 | const User = attributes({
9 | firstName: String,
10 | lastName: String,
11 | age: Number,
12 | })(
13 | class User {
14 | get firstName() {
15 | return `-> ${this.get('firstName')}`;
16 | }
17 |
18 | set lastName(newLastname) {
19 | return this.set('lastName', `Mac${newLastName}`);
20 | }
21 |
22 | get age() {
23 | // do NOT do that. Instead, use this.get and this.set inside getters and setters
24 | return this.age * 1000;
25 | }
26 |
27 | // this is NOT an attribute, just a normal getter
28 | get fullName() {
29 | return `${this.firstName} ${this.lastName}`;
30 | }
31 | }
32 | );
33 |
34 | const user = new User({ firstName: 'Connor', lastName: 'Leod' });
35 |
36 | user.firstName; // -> Connor
37 | user.lastName; // MacLeod
38 | user.fullName; // -> Connor MacLeod
39 | ```
40 |
41 | ## Inheritance
42 |
43 | Custom setters and getters are also inherited, be your superclass a pure JavaScript class or another structure:
44 |
45 | ```js
46 | class Person {
47 | // If Person was a structure instead of a pure class, that would work too
48 | get name() {
49 | return 'The person';
50 | }
51 | }
52 |
53 | const User = attributes({
54 | name: String,
55 | })(class User extends Person {});
56 |
57 | const user = new User({ name: 'Will not be used' });
58 |
59 | user.name; // -> The person
60 | ```
61 |
62 | **Important**
63 |
64 | JavaScript nativelly won't let you inherit only one of the accessors (the getter or the setter) if you define the other accessor in a subclass:
65 |
66 | ```js
67 | class Person {
68 | get name() {
69 | return 'Person';
70 | }
71 | }
72 |
73 | class User extends Person {
74 | set name(newName) {
75 | this._name = newName;
76 | }
77 | }
78 |
79 | const user = new Person();
80 | user.name = 'The user';
81 | user.name; // -> The user
82 | ```
83 |
84 | It happens because _once you define one of the accessors in a subclass_, all the accessors for the same attribute inherited from the superclass will be ignored.
85 |
86 | While it's a weird behavior, Structure will follow the same functionality so the Structure classes inheritance work the same way of pure JavaScript classes, avoiding inconsistencies.
87 |
--------------------------------------------------------------------------------
/docs/migrating-from-v1.md:
--------------------------------------------------------------------------------
1 | # Migrating from v1 to v2
2 |
3 | Migrating an app that uses Structure v1 to use Structure v2 requires some changes in the way validation errors are expected to be returned, default fallback of null values and upgrading Node (if you use a version lower than v10.13.0)
4 |
5 | ## Validation errors
6 |
7 | In v1, validations used to be like this:
8 |
9 | ```js
10 | const Book = attributes({
11 | name: {
12 | type: String,
13 | required: true,
14 | },
15 | })(class Book {});
16 |
17 | const User = attributes({
18 | initials: {
19 | type: String,
20 | minLength: 2,
21 | },
22 | favoriteBook: Book,
23 | books: {
24 | type: Array,
25 | itemType: Book,
26 | },
27 | })(class User {});
28 |
29 | const user = new User({
30 | initials: 'A',
31 | favoriteBook: new Book(),
32 | books: [new Book()],
33 | });
34 |
35 | const { valid, errors } = user.validate();
36 |
37 | valid; // false
38 | errors; /*
39 | [
40 | { message: '"initials" length must be at least 2 characters long', path: 'initials' },
41 | { message: '"name" is required', path: 'favoriteBook.name' },
42 | { message: '"name" is required', path: 'books.0.name' }
43 | ]
44 | */
45 | ```
46 |
47 | Notice that the message used to contain only the name of the attribute (so if it's nested it will only show the attribute name of the nested structure) and the path is a string that use a dot `.` to represent that it's a nested attribute.
48 |
49 | If your app relies on the content of the message or the path, you'll have to consider that it's now returned like this:
50 |
51 | ```js
52 | const Book = attributes({
53 | name: {
54 | type: String,
55 | required: true,
56 | },
57 | })(class Book {});
58 |
59 | const User = attributes({
60 | initials: {
61 | type: String,
62 | minLength: 2,
63 | },
64 | favoriteBook: Book,
65 | books: {
66 | type: Array,
67 | itemType: Book,
68 | },
69 | })(class User {});
70 |
71 | const user = new User({
72 | initials: 'A',
73 | favoriteBook: new Book(),
74 | books: [new Book()],
75 | });
76 |
77 | const { valid, errors } = user.validate();
78 |
79 | valid; // false
80 | errors; /*
81 | [
82 | { message: '"initials" length must be at least 2 characters long', path: ['initials'] },
83 | { message: '"favoriteBook.name" is required', path: ['favoriteBook', 'name'] },
84 | { message: '"books[0].name" is required', path: ['books', 0, 'name'] }
85 | ]
86 | */
87 | ```
88 |
89 | In v2 the message contains the whole path for the attribute (using dot `.` to represent it's a nested attribute) and the path is not an array.
90 |
91 | So if your apps relies in the content of the message you'll probably have to do some parsing of this message now. And if it relies in the path to be a string, you can use [`path.join('.')`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join).
92 |
93 | ## Nullability and defaults
94 |
95 | Since v2 now allows having nullable attributes, non-nullable attributes will fallback to the [default](schema-concept/shorthand-and-complete-attribute-definition.md#default) the same way it happens with attributes with `undefined` value since v1.
96 |
97 | ## New public methods
98 |
99 | Structure v2 also adds two new instance methods to the structures: `instance.get(attrName)` and `instance.set(attrName, attrValue)`. They are **not** replacing the normal getters and setters, which are still there, they are just alternatives for the case you use a [custom setter and/or getter](custom-setters-and-getters.md)
100 |
101 | So if any of your structures declare methods with these names you'll have to change it or it's gonna break.
102 |
103 | ## Upgrade Node
104 |
105 | The minimum Node LTS supported by Structure is now v10.13.0 which is the lowest active [LTS](https://nodejs.org/en/about/releases/).
106 |
--------------------------------------------------------------------------------
/docs/schema-concept/README.md:
--------------------------------------------------------------------------------
1 | # Schema Concept
2 |
3 | The schema is an object responsible to map the attributes Structure should handle, it is the parameter of the `attributes` function.
4 |
5 | ```js
6 | attributes({
7 | name: String,
8 | age: Number,
9 | })(class User {});
10 | ```
11 |
12 | There are two ways to declare an attribute of the schema, the **shorthand attribute definition** and the **complete attribute definition**.
13 |
--------------------------------------------------------------------------------
/docs/schema-concept/circular-references-and-dynamic-types.md:
--------------------------------------------------------------------------------
1 | # Circular references and dynamic types
2 |
3 | Sometimes we may need to have a type reference itself in its attributes, or have two types that reference eachother in separate files, it can be a complication because it's not possible to do this using the type definitions like we did before. For cases like this we can use a feature called "dynamic types".
4 |
5 | When using dynamic types you can pass a string instead of the type itself in the type definition, this string will contain the type **identifier**, and then pass an object as the _second_ parameter of the `attributes` function with a key `dynamics` where the concrete type for each identifier is declared:
6 |
7 | ```js
8 | /*
9 | * User.js
10 | */
11 | const User = attributes(
12 | {
13 | name: String,
14 | friends: {
15 | type: Array,
16 | itemType: 'User', // << identifier
17 | },
18 | favoriteBook: {
19 | type: 'BookStructure', // << identifier
20 | required: true,
21 | },
22 | books: {
23 | type: 'BooksCollection', // << identifier
24 | itemType: String,
25 | },
26 | },
27 | {
28 | dynamics: {
29 | /* dynamic types for each identifier */
30 | User: () => User,
31 | BookStructure: () => require('./Book'),
32 | BooksCollection: () => require('./BooksCollection'),
33 | },
34 | }
35 | )(class User {});
36 |
37 | module.exports = User;
38 |
39 | /*
40 | * Book.js
41 | */
42 | const Book = attributes(
43 | {
44 | name: String,
45 | owner: 'User', // << dynamic type with inferred identifier
46 | nextBook: 'BookStructure', // << dynamic type with custom identifier
47 | },
48 | {
49 | identifier: 'BookStructure', // << custom identifier
50 | dynamics: {
51 | /* dynamic types for each identifier */
52 | User: () => require('./User'),
53 | BookStructure: () => Book,
54 | },
55 | }
56 | )(class Book {});
57 |
58 | module.exports = Book;
59 | ```
60 |
61 | If you're using `import` instead of `require` (thus having to import the dynamic value in the top-level of the file), go to the [With ES Modules](#with-es-modules) section of this page.
62 |
63 | ## Dynamic type identifier
64 |
65 | The type's identifier has to be same everywhere it's used, and can be defined in two ways:
66 |
67 | ### Inferred identifier
68 |
69 | The identifier can be inferred based on the class that is wrapped by the `attributes` function. In backend scenarios this will be the most common case:
70 |
71 | ```js
72 | const User = attributes(
73 | {
74 | name: String,
75 | bestFriend: 'User', // [A] type with inferred identifier
76 | },
77 | {
78 | dynamics: {
79 | User: () => User, // [B] inferred identifier concrete type
80 | },
81 | }
82 | )(
83 | class User {
84 | // ⬑-- the name of this class is the identifier
85 | // so if we change this name to UserEntity, we'll have to change
86 | // both [A] and [B] to use the string 'UserEntity' instead of 'User'
87 | }
88 | );
89 | ```
90 |
91 | ### Custom identifier
92 |
93 | If for some reason you can't rely on the class name, be it because you're using a compiler that strips class names or creates a dynamic one, you can explicitly set an indentifier.
94 |
95 | To do that, in the second argument of the `attributes` function (e.g. the options) you should add a `identifier` key and set it to be the string with the type's identifier and then use that custom value everywhere this type is dynamically needed:
96 |
97 | ```js
98 | const User = attributes(
99 | {
100 | name: String,
101 | bestFriend: 'UserEntity', // << type with custom identifier
102 | },
103 | {
104 | identifier: 'UserEntity', // << custom identifier
105 | dynamics: {
106 | // ⬐--- custom identifier concrete type
107 | UserEntity: () => User,
108 | },
109 | }
110 | )(class User {});
111 | ```
112 |
113 | ## Concrete type definition inside `dynamics`
114 |
115 | For the cases where the dynamic type is in a different file, it's important that the actual needs type to be resolved **inside** the function with the identifier. Let's break it down in two cases:
116 |
117 | ### With CommonJS modules
118 |
119 | When using CommonJS modules you have two possibilities:
120 |
121 | 1. Putting the `require` call directly inside the concrete type resolution function:
122 |
123 | ```js
124 | const Book = attributes(
125 | {
126 | name: String,
127 | owner: 'User',
128 | nextBook: 'BookStructure',
129 | },
130 | {
131 | identifier: 'BookStructure',
132 | dynamics: {
133 | User: () => require('./User'), // << like this
134 | BookStructure: () => Book,
135 | },
136 | }
137 | )(class Book {});
138 |
139 | module.exports = Book;
140 | ```
141 |
142 | 2. Exporting an **object** containing your other structure (instead of exporting the structure itself) and only access it inside the concrete type resolution function:
143 |
144 | ```js
145 | /*
146 | * User.js
147 | */
148 | const BookModule = require('./Book');
149 |
150 | const User = attributes(
151 | {
152 | name: String,
153 | favoriteBook: {
154 | type: 'Book',
155 | required: true,
156 | },
157 | },
158 | {
159 | dynamics: {
160 | User: () => User,
161 | Book: () => BookModule.Book,
162 | },
163 | }
164 | )(class User {});
165 |
166 | exports.User = User;
167 |
168 | /*
169 | * Book.js
170 | */
171 | const UserModule = require('./User');
172 |
173 | const Book = attributes(
174 | {
175 | name: String,
176 | owner: 'User',
177 | },
178 | {
179 | dynamics: {
180 | User: () => UserModule.User,
181 | Book: () => Book,
182 | },
183 | }
184 | )(class Book {});
185 |
186 | exports.Book = Book;
187 | ```
188 |
189 | ### With ES Modules
190 |
191 | When using ES Modules you have a single possibility, which is importing the other structure from the top-level of your file and then returning it from the concrete type resolution function. It's important to note that this **only** works with ES Modules, if you're using CommonJS, check the [With CommonJS modules](#with-commonjs-modules) section.
192 |
193 | ```javascript
194 | /*
195 | * User.js
196 | */
197 | import Book from './Book';
198 |
199 | const User = attributes(
200 | {
201 | name: String,
202 | favoriteBook: {
203 | type: 'Book',
204 | required: true,
205 | },
206 | },
207 | {
208 | dynamics: {
209 | User: () => User,
210 | Book: () => Book,
211 | },
212 | }
213 | )(class User {});
214 |
215 | export default User;
216 |
217 | /*
218 | * Book.js
219 | */
220 | import User from './User';
221 |
222 | const Book = attributes(
223 | {
224 | name: String,
225 | owner: 'User',
226 | },
227 | {
228 | dynamics: {
229 | User: () => User,
230 | Book: () => Book,
231 | },
232 | }
233 | )(class Book {});
234 |
235 | export default Book;
236 | ```
237 |
--------------------------------------------------------------------------------
/docs/schema-concept/nullable-attributes.md:
--------------------------------------------------------------------------------
1 | # Nullable attributes
2 |
3 | You can change the way an attribute is treated when the value `null` is assigned to it by using the `nullable` option with the value `true`, this would affect the way the attribute is defaulted, coerced, validated and serialized.
4 |
5 | If you do not set the `nullable` option it will default to `false` and automatically make your attribute **non-nullable**.
6 |
7 | ## Nullability and `default` option
8 |
9 | Non-nullable attributes with the value `null` will fallback to the value set as default the same way `undefined` does. But if the attribute is nullable, it will **only** fallback to the default if its value is `undefined`.
10 |
11 | ```javascript
12 | const User = attributes({
13 | name: {
14 | // automatically non-nullable
15 | type: String,
16 | default: 'Some string',
17 | },
18 | nickname: {
19 | type: String,
20 | nullable: false,
21 | default: 'Some other string',
22 | },
23 | })(class User {});
24 |
25 | const userA = new User({ name: null, nickname: null });
26 | userA.attributes; // { name: 'Some string', nickname: null }
27 |
28 | const userB = new User({ name: null, nickname: undefined });
29 | userB.attributes; // { name: 'Some string', nickname: 'Some other string' }
30 | ```
31 |
32 | ## Coercion
33 |
34 | Non-nullable values will fallback to their null-equivalent values. More details about it can be found at the [coercion](coercion/README.md) section. Nullable attributes will remain `null` as described in the section above.
35 |
36 | ```javascript
37 | /*
38 | * User.js
39 | */
40 | const User = attributes({
41 | name: {
42 | type: String,
43 | nullable: true,
44 | },
45 | nickname: {
46 | // automatically non-nullable
47 | type: String,
48 | empty: true,
49 | },
50 | age: Number, // << automatically non-nullable
51 | active: Boolean, // << automatically non-nullable
52 | createdAt: Date, // << automatically non-nullable
53 | })(class User {});
54 |
55 | const user = new User({
56 | name: null,
57 | nickname: null,
58 | age: null,
59 | active: null,
60 | createdAt: null,
61 | });
62 |
63 | // Only non-nullable values are coerced to their null-equivalent values
64 | user.attributes; // { name: null, nickname: '', age: 0, active: false, createdAt: 1970-01-01T00:00:00.000Z }
65 | user.validate(); // { valid: true }
66 | ```
67 |
68 | ### Nullable optional attributes
69 |
70 | When you set an optional attribute to be **nullable** you are choosing not to assign a default value for it when instantiating your structure passing `null` as the value of this attribute, so the actual value will be `null` and will be considered valid.
71 |
72 | ```javascript
73 | /*
74 | * User.js
75 | */
76 | const User = attributes({
77 | name: {
78 | type: String,
79 | nullable: true,
80 | },
81 | })(class User {});
82 |
83 | const user = new User({
84 | name: null,
85 | });
86 |
87 | user.attributes; // { name: null }
88 | user.validate(); // { valid: true }
89 | ```
90 |
91 | ### Nullable required attributes
92 |
93 | We consider that when an attribute is **required** there should be some value assigned to it even if it's `undefined`, `null` or any other value. It means that coercion will never assign a **default** value to **required** attributes even if **nullable** option is **false**.
94 |
95 | ```javascript
96 | /*
97 | * User.js
98 | */
99 | const User = attributes({
100 | name: {
101 | type: String,
102 | required: true,
103 | nullable: false, // non-nullable required attribute
104 | },
105 | })(class User {});
106 |
107 | const user = new User({
108 | name: null,
109 | });
110 |
111 | user.attributes; // { name: null }
112 | user.validate(); // { valid: false }
113 | ```
114 |
115 | But notice that you can choose to allow **null** values on **required** attributes which will cause the validation to return **true**.
116 |
117 | ```javascript
118 | /*
119 | * User.js
120 | */
121 | const User = attributes({
122 | name: {
123 | type: String,
124 | required: true,
125 | nullable: true,
126 | },
127 | })(class User {});
128 |
129 | const user = new User({
130 | name: null,
131 | });
132 |
133 | user.attributes; // { name: null }
134 | user.validate(); // { valid: true }
135 | ```
136 |
137 | ## Nullability and serialization
138 |
139 | Usually an attribute with the value **undefined** or **null** is not included when you serialize your structure. But when it is **nullable** and its value is `null`, this attribute is going to be returned in your serialized schema.
140 |
141 | ```javascript
142 | const User = attributes({
143 | name: {
144 | type: String,
145 | nullable: false,
146 | },
147 | nickname: {
148 | type: String,
149 | nullable: true,
150 | },
151 | })(class User {});
152 |
153 | const user = new User({ name: undefined, nickname: null });
154 | user.toJSON(); // { nickname: null }
155 | ```
156 |
157 | **Important:**
158 |
159 | - Notice that by not using the `nullable` option the **default** value for **String** is an empty string, which means that you need to accept empty strings to make your schema valid.
160 |
--------------------------------------------------------------------------------
/docs/schema-concept/shorthand-and-complete-attribute-definition.md:
--------------------------------------------------------------------------------
1 | # Shorthand attribute definition
2 |
3 | The shorthand is a pair of `propertyName: Type` key/value like this:
4 |
5 | ```js
6 | const User = attributes({
7 | name: String,
8 | brithday: Date,
9 | })(
10 | class User {
11 | generateRandomBook() {
12 | return '...';
13 | }
14 | }
15 | );
16 | ```
17 |
18 | # Complete attribute definition
19 |
20 | The complete definition allows you to declare additional info for the attribute.
21 | **For Array types it's required to use the complete attribute definition because you _must_ specify the `itemType`**.
22 |
23 | ```js
24 | const User = attributes({
25 | name: {
26 | type: String,
27 | default: 'Anonymous',
28 | },
29 | cars: {
30 | type: Array,
31 | itemType: String,
32 | default: ['Golf', 'Polo'],
33 | },
34 | book: {
35 | type: String,
36 | default: (instance) => instance.generateRandomBook(),
37 | },
38 | })(
39 | class User {
40 | generateRandomBook() {
41 | return '...';
42 | }
43 | }
44 | );
45 | ```
46 |
47 | ## default
48 |
49 | The **default** of an attribute will be used if no value was provided for the specific attribute at construction time.
50 |
51 | You can also use a function which receives the instance as a parameter in order to provide the default. The operation must be synchronous and the function will called after all the other attributes are already assigned,
52 | thus, you can use the other attributes of your class to compose a default value.
53 |
54 | ```js
55 | const User = attributes({
56 | name: {
57 | type: String,
58 | default: 'Anonymous', // static default value
59 | },
60 | greeting: {
61 | type: String,
62 | default: (instance) => instance.greeting(), // dynamic default value
63 | },
64 | })(
65 | class User {
66 | greeting() {
67 | return `Hello ${this.name}`;
68 | }
69 | }
70 | );
71 | ```
72 |
73 | Please note that initializing an attribute with undefined will make it fallback to the default value while instantiating the structure, but it will not fallback when assigning the attribute after the structure is already constructed.
74 |
75 | ```js
76 | const User = attributes({
77 | name: {
78 | type: String,
79 | default: 'Anonymous', // static default value
80 | },
81 | })(class User {});
82 |
83 | const firstUser = new User({ name: undefined });
84 | firstUser.name; // 'Anonymous' => fallbacks to default value
85 |
86 | const secondUser = new User({ name: 'Some name' });
87 |
88 | secondUser.name = undefined;
89 | secondUser.name; // undefined => does not fallback to default value
90 | ```
91 |
92 | ## itemType
93 |
94 | The **itemType** of an attribute is used to validate and coerce the type of each item from the attribute, like when the attribute type is `Array` or some class that extends `Array`.
95 |
96 | - Please refer to [Validation](../validation/README.md) in order to check a bit more on validation properties.
97 |
98 | # Type concept
99 |
100 | Each attribute needs a **type** definition, that's how Structure validates and coerces the attribute's value. It can be divided into three categories (as in right now):
101 |
102 | - Primitives (Number, String, Boolean)
103 | - Classes (Date, Object, regular Classes and Structure classes as well)
104 | - Array/Array-like (Array, extended Array)
105 |
--------------------------------------------------------------------------------
/docs/serialization.md:
--------------------------------------------------------------------------------
1 | # Serialization
2 |
3 | It's possible to obtain a serialized object of a Structure using the method `toJSON()`. This method is also compliant with the [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) specification, so you can use it to serialize your object too.
4 |
5 | **Important:**
6 |
7 | - Be aware that `toJSON()` will return an object, not the JSON in form of a string like `JSON.stringify()` does
8 | - Refer to the [Date#toJSON](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Date/toJSON) specification to see how dates will be serialized by `JSON.stringify`
9 | - Refer to [Dealing with nullable attributes](schema-concept/nullable-attributes.md#nullability-and-serialization) to check how `nullables` are going to be returned on **Serialization**
10 |
11 | ```javascript
12 | const Book = attributes({
13 | name: String
14 | })(class Book { });
15 |
16 | const User = attributes({
17 | name: String,
18 | birth: Date,
19 | books: {
20 | type: Array,
21 | itemType: Book
22 | }
23 | })(class User { });
24 |
25 | const user = new User({
26 | name: 'John Something',
27 | birth: new Date('10/10/1990'),
28 | books: [
29 | new Book({ name: 'The name of the wind' }),
30 | new Book({ name: 'Stonehenge' })
31 | ]
32 | });
33 |
34 | user.toJSON(); /* {
35 | name: 'John Something',
36 | birth: new Date('10/10/1990'),
37 | books: [
38 | { name: 'The name of the wind' },
39 | { name: 'Stonehenge' }
40 | ]
41 | }
42 | */
43 |
44 | JSON.stringify(user)); // {"name":"John Something","birth":"1990-10-10T03:00:00.000Z","books":[{"name":"The name of the wind"},{"name":"Stonehenge"}]}
45 | ```
46 |
--------------------------------------------------------------------------------
/docs/strict-mode.md:
--------------------------------------------------------------------------------
1 | # Strict mode
2 |
3 | To instantiate a structure that automatically throws an error if that is invalid, you can use the buildStrict function.
4 |
5 | ```js
6 | const { attributes } = require('structure');
7 | const User = attributes({
8 | name: {
9 | type: String,
10 | required: true,
11 | },
12 | age: Number,
13 | })(class User {});
14 |
15 | const user = User.buildStrict({
16 | age: 'Twenty',
17 | });
18 |
19 | // Error: Invalid Attributes
20 | // details: [
21 | // { message: '"name" is required', path: ['name'] },
22 | // { message: '"age" must be a number', path: ['age'] }
23 | // ]
24 | ```
25 |
26 | ## Custom error
27 |
28 | Normally `buildStrict` will throw a default `Error` when attributes are invalid but you can customize the error class that will be used passing a `strictValidationErrorClass` to the _second_ parameter of the `attributes` function.
29 |
30 | The value of `strictValidationErrorClass` should be a class that accepts an array of erros in the constructor.
31 |
32 | ```js
33 | const { attributes } = require('structure');
34 |
35 | class InvalidBookError extends Error {
36 | constructor(errors) {
37 | super('Wait, this book is not right');
38 | this.code = 'INVALID_BOOK';
39 | this.errors = errors;
40 | }
41 | }
42 |
43 | const Book = attributes(
44 | {
45 | name: {
46 | type: String,
47 | required: true,
48 | },
49 | year: Number,
50 | },
51 | {
52 | strictValidationErrorClass: InvalidBookError,
53 | }
54 | )(class Book {});
55 |
56 | const book = Book.buildStrict({
57 | year: 'Twenty',
58 | });
59 |
60 | // InvalidBookError: Wait, this book is not right
61 | // code: 'INVALID_BOOK'
62 | // errors: [
63 | // { message: '"name" is required', path: ['name'] },
64 | // { message: '"year" must be a number', path: ['year'] }
65 | // ]
66 | ```
67 |
--------------------------------------------------------------------------------
/docs/support.md:
--------------------------------------------------------------------------------
1 | # Support and compatibility
2 |
3 | Structure is built on top of modern JavaScript, using new features like [Proxy](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy), [Reflect](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Reflect) and [Symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol). That being so, there are some things regarding compatibility you should consider when using Structure.
4 |
5 | ## Node
6 |
7 | Node has only implemented all the used features at version 10, so to use Structure for a backend application you'll need Node 10 or later.
8 |
9 | ## Browser
10 |
11 | We have a UMD version for usage in browsers. Right now the tests are ran both in Node (using the original code) and in Electron (using the UMD version) and the whole test suite passes for both cases. Since we use modern JavaScript features inside the lib (like [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)), older browsers may not support it. Polyfilling them may be an option but it's not oficially supported.
12 |
--------------------------------------------------------------------------------
/docs/testing.md:
--------------------------------------------------------------------------------
1 | # Testing
2 |
3 | If you use Jest, Structure has a Jest extension called [`jest-structure`](https://www.npmjs.com/package/jest-structure) that provides assertions to make it easy to test intances.
4 |
5 | ## Installation
6 |
7 | jest-structure is available in npm, so you can install it with npm or yarn as a development dependency:
8 |
9 | ```sh
10 | npm install --save-dev jest-structure
11 |
12 | # or
13 |
14 | yarn --dev add jest-structure
15 | ```
16 |
17 | ## Setup
18 |
19 | After installing, you need to tell Jest to use jest-structure, this can be done in two ways:
20 |
21 | By importing and manually adding it to Jest:
22 |
23 | ```js
24 | import jestStructure from 'jest-structure';
25 |
26 | expect.extend(jestStructure);
27 | ```
28 |
29 | Or by allowing jest-structure to add itself to Jest matchers:
30 |
31 | ```js
32 | import 'jest-structure/extend-expect';
33 | ```
34 |
35 | Both ways can be done in a [setup file](https://jestjs.io/docs/en/configuration#setupfilesafterenv-array) or directly at the top of your test file
36 |
37 | ## Matchers
38 |
39 | ### `toBeValidStructure()`
40 |
41 | This matcher passes if the structure is _valid_:
42 |
43 | ```js
44 | const User = attributes({
45 | name: { type: String, required: true },
46 | })(class User {});
47 |
48 | const validUser = new User({ name: 'Me' });
49 |
50 | expect(validUser).toBeValidStructure(); // passes
51 |
52 | const invalidUser = new User();
53 |
54 | expect(invalidUser).toBeValidStructure(); // fails
55 | ```
56 |
57 | ### `toBeInvalidStructure()`
58 |
59 | This matcher passes if the structure is _invalid_:
60 |
61 | ```js
62 | const User = attributes({
63 | name: { type: String, required: true },
64 | })(class User {});
65 |
66 | const invalidUser = new User();
67 |
68 | expect(invalidUser).toBeInvalidStructure(); // passes
69 |
70 | const validUser = new User({ name: 'Me' });
71 |
72 | expect(validUser).toBeInvalidStructure(); // fails
73 | ```
74 |
75 | ## `toHaveInvalidAttribute(path, messages)`
76 |
77 | This matcher allows you to assert that a _single attribute_ of the structure is invalid, optionally passing the array of error messages for that attribute:
78 |
79 | ```js
80 | const User = attributes({
81 | name: { type: String, required: true },
82 | age: { type: Number, required: true },
83 | })(class User {});
84 |
85 | const user = new User({ age: 42 });
86 |
87 | // passes, because name is invalid
88 | expect(user).toHaveInvalidAttribute(['name']);
89 |
90 | // fails, because age is valid
91 | expect(user).toHaveInvalidAttribute(['age']);
92 |
93 | // passes, because name is invalid with this message
94 | expect(user).toHaveInvalidAttribute(['name'], ['"name" is required']);
95 |
96 | // fails, because name is invalid but not with this message
97 | expect(user).toHaveInvalidAttribute(['name'], ['"name" is not cool']);
98 |
99 | // passes. Notice that you can even use arrayContaining to check for a subset of the errros
100 | expect(user).toHaveInvalidAttribute(['name'], expect.arrayContaining(['"name" is required']));
101 |
102 | // passes. And stringContaining can be used as well
103 | expect(user).toHaveInvalidAttribute(['name'], [expect.stringContaining('required')]);
104 | ```
105 |
106 | ## `toHaveInvalidAttributes([ { path, messages } ])`
107 |
108 | This matcher allows you to assert that _multiple attributes_ of the structure are invalid, optionally passing the array of error messages for each attribute:
109 |
110 | ```js
111 | const User = attributes({
112 | name: { type: String, required: true },
113 | age: { type: Number, required: true },
114 | })(class User {});
115 |
116 | const user = new User({ age: 42 });
117 |
118 | // passes, because name is invalid
119 | expect(user).toHaveInvalidAttributes([{ path: ['name'] }]);
120 |
121 | // fails, because age is valid
122 | expect(user).toHaveInvalidAttributes([{ path: ['age'] }]);
123 |
124 | // fails, because name is invalid but age is valid
125 | expect(user).toHaveInvalidAttributes([{ path: ['name'] }, { path: ['age'] }]);
126 |
127 | // passes, because name is invalid with this message
128 | expect(user).toHaveInvalidAttributes([{ path: ['name'], messages: ['"name" is required'] }]);
129 |
130 | // fails, because name is invalid but not with this message
131 | expect(user).toHaveInvalidAttributes([{ path: ['name'], messages: ['"name" is not cool'] }]);
132 |
133 | // passes. Notice that you can even use arrayContaining to check for a subset of the errros
134 | expect(user).toHaveInvalidAttributes([
135 | { path: ['name'], messages: expect.arrayContaining(['"name" is required']) },
136 | ]);
137 |
138 | // passes. And stringContaining can be used as well
139 | expect(user).toHaveInvalidAttributes([
140 | { path: ['name'], messages: [expect.stringContaining('required')] },
141 | ]);
142 | ```
143 |
--------------------------------------------------------------------------------
/docs/validation/README.md:
--------------------------------------------------------------------------------
1 | # Validation
2 |
3 | A `validate()` method will be added to the prototype of structures, this method will validate the structure based on its schema. The method will return an object with the property `valid` (with the value `true` if it's valid, and `false` if invalid). If `valid` is `false` the returned object will also have a property `errors`, with an array of validation errors.
4 |
5 | Validations require you to use the complete attribute definition:
6 |
7 | ```javascript
8 | const User = attributes({
9 | name: {
10 | type: String,
11 | minLength: 10,
12 | },
13 | age: {
14 | type: Number,
15 | required: true,
16 | },
17 | })(class User {});
18 |
19 | const user = new User({
20 | name: 'John',
21 | });
22 |
23 | const { valid, errors } = user.validate();
24 |
25 | valid; // false
26 | errors; /*
27 | [
28 | { message: '"name" length must be at least 10 characters long', path: ['name'] },
29 | { message: '"age" is required', path: ['age'] }
30 | ]
31 | */
32 |
33 | const validUser = new User({
34 | name: 'This is my name',
35 | age: 25,
36 | });
37 |
38 | const validation = validUser.validate();
39 |
40 | validation.valid; // true
41 | validation.errors; // undefined, because `valid` is true
42 | ```
43 |
44 | Structure has a set of built-in validations built on top of the awesome [joi](https://www.npmjs.com/package/@hapi/joi) package:
45 |
46 | **Observations**
47 |
48 | Validations marked with **\*** accept a value, an [attribute reference](attribute-reference.md), or an array of values and attribute references mixed.
49 |
50 | Validations marked with **\*\*** accept a value or an [attribute reference](attribute-reference.md).
51 |
--------------------------------------------------------------------------------
/docs/validation/array-validations.md:
--------------------------------------------------------------------------------
1 | # Array validations
2 |
3 | - `required`: can't be undefined (default: `false`)
4 | - `sparse`: can have undefined items (default: `true`)
5 | - `unique`: can't have duplicate items (default: `false`)
6 | - `minLength`: minimum quantity of items
7 | - `maxLength`: maximum quantity of items
8 | - `exactLength`: exact quantity of items
9 |
10 | ```javascript
11 | const Group = attributes({
12 | members: {
13 | type: Array,
14 | itemType: String,
15 | minLength: 2,
16 | maxLength: 5,
17 | sparse: false,
18 | unique: true
19 | },
20 | leaders: {
21 | type: Array,
22 | itemType: String,
23 | minLength: 1,
24 | maxLength: { attr: 'members' }
25 | }
26 | })
27 | ```
28 |
--------------------------------------------------------------------------------
/docs/validation/attribute-reference.md:
--------------------------------------------------------------------------------
1 | # Attribute reference
2 |
3 | You can reference attributes for some validations so the value of the referenced attribute will be used for the comparison:
4 |
5 | ```javascript
6 | const User = attributes({
7 | name: String,
8 | password: String,
9 | passwordConfirmation: {
10 | type: String,
11 | equal: { attr: 'password' },
12 | },
13 | })(class User {});
14 |
15 | const user = new User({
16 | name: 'Gandalf',
17 | password: 'safestpasswordever',
18 | passwordConfirmation: 'notthatsafetho',
19 | });
20 |
21 | const { valid, errors } = user.validate();
22 |
23 | valid; // false
24 | errors; /* [
25 | {
26 | message: '"passwordConfirmation" must be one of [ref:password]',
27 | path: ['passwordConfirmation']
28 | }
29 | ]
30 | */
31 | ```
32 |
--------------------------------------------------------------------------------
/docs/validation/boolean-validations.md:
--------------------------------------------------------------------------------
1 | # Boolean validations
2 |
3 | - `required`: can't be undefined (default: `false`)
4 | - `equal`: __*__ equal to passed value
5 | - `nullable`: accepts null (default: `false`)
6 |
7 | ```javascript
8 | const User = attributes({
9 | isAdmin: {
10 | type: Boolean,
11 | required: true
12 | },
13 | hasAcceptedTerms: {
14 | type: Boolean,
15 | nullable: true
16 | }
17 | })(class User { });
18 | ```
19 |
--------------------------------------------------------------------------------
/docs/validation/date-validations.md:
--------------------------------------------------------------------------------
1 | # Date validations
2 |
3 | - `required`: can't be undefined (default: `false`)
4 | - `equal`: __*__ equal to passed value
5 | - `min`: __**__ must be after passed date
6 | - `max` __**__ must be before passed date
7 | - `nullable`: accepts null (default: `false`)
8 |
9 | ```javascript
10 | const Product = attributes({
11 | fabricationDate: {
12 | type: Date,
13 | default: () => Date.now()
14 | },
15 | expirationDate: {
16 | type: Date,
17 | min: { attr: 'fabricationDate' }
18 | },
19 | createdAt: {
20 | type: Date,
21 | nullable: true
22 | }
23 | })(class Product { });
24 | ```
25 |
--------------------------------------------------------------------------------
/docs/validation/nested-validations.md:
--------------------------------------------------------------------------------
1 | # Nested validations
2 |
3 | Structure will validate nested values, including array items validations and nested structures.
4 |
5 | ```javascript
6 | const Book = attributes({
7 | name: {
8 | type: String,
9 | required: true,
10 | },
11 | })(class Book {});
12 |
13 | const User = attributes({
14 | initials: {
15 | type: String,
16 | minLength: 2,
17 | },
18 | favoriteBook: Book,
19 | books: {
20 | type: Array,
21 | itemType: Book,
22 | },
23 | })(class User {});
24 |
25 | const user = new User({
26 | initials: 'A',
27 | favoriteBook: new Book(),
28 | books: [new Book()],
29 | });
30 |
31 | const { valid, errors } = user.validate();
32 |
33 | valid; // false
34 | errors; /*
35 | [
36 | { message: '"initials" length must be at least 2 characters long', path: ['initials'] },
37 | { message: '"favoriteBook.name" is required', path: ['favoriteBook', 'name'] },
38 | { message: '"books[0].name" is required', path: ['books', 0, 'name'] }
39 | ]
40 | */
41 | ```
42 |
--------------------------------------------------------------------------------
/docs/validation/number-validations.md:
--------------------------------------------------------------------------------
1 | # Number validations
2 |
3 | - `required`: can't be undefined (default: `false`)
4 | - `equal`: __*__ equal to passed value
5 | - `integer`: must be an integer (default: `false`)
6 | - `precision`: maximum number of decimal places
7 | - `positive`: must be positive (default: `false`)
8 | - `negative`: must be negative (default: `false`)
9 | - `multiple`: must be a multiple of the passed value
10 | - `min`: __**__ minimum valid value (works like the `>=` operator)
11 | - `greater`: __**__ must be greater than passed value (works like the `>` operator)
12 | - `max`: __**__ maximum valid value (works like the `<=` operator)
13 | - `less`: __**__ must be smaller than passed value (works like the `<` operator)
14 | - `nullable`: accepts null (default: `false`)
15 |
16 | ```javascript
17 | const Pool = attributes({
18 | depth: {
19 | type: Number,
20 | positive: true
21 | },
22 | width: {
23 | type: Number,
24 | min: { attr: 'depth' }
25 | },
26 | length: {
27 | type: Number,
28 | greater: { attr: 'width' }
29 | },
30 | capacity: {
31 | type: Number,
32 | nullable: true
33 | }
34 | })(class Pool {
35 | getVolume() {
36 | return this.depth * this.width * this.length;
37 | }
38 | });
39 | ```
40 |
--------------------------------------------------------------------------------
/docs/validation/string-validations.md:
--------------------------------------------------------------------------------
1 | # String validations
2 |
3 | - `required`: can't be undefined (default: `false`)
4 | - `empty`: accepts empty string (default: `false`)
5 | - `equal`: **\*** equal to passed value
6 | - `minLength`: can't be shorter than passed value
7 | - `maxLength`: can't be longer than passed value
8 | - `exactLength`: length must be exactly the passed value
9 | - `regex`: matches the passed regex
10 | - `alphanumeric`: composed only by alphabetical and numeric characters
11 | - `lowerCase`: all characters must be lower cased
12 | - `upperCase`: all characters must be upper cased
13 | - `email`: is a valid email (default: `false`)
14 | - `nullable`: accepts null (default: `false`)
15 | - `guid`: is a valid guid. You can pass a boolean or the [options object accepted by Joi](https://hapi.dev/module/joi/api/?v=16.1.8#stringguid---aliases-uuid) (default: `false`)
16 |
17 | ```javascript
18 | const User = attributes({
19 | id: {
20 | type: String,
21 | guid: true,
22 | },
23 | token: {
24 | type: String,
25 | guid: {
26 | version: ['uuidv4'],
27 | },
28 | },
29 | initials: {
30 | type: String,
31 | upperCase: true,
32 | maxLength: 4,
33 | },
34 | gender: {
35 | type: String,
36 | nullable: true,
37 | },
38 | password: String,
39 | passwordConfirmation: {
40 | type: String,
41 | equal: { attr: 'password' },
42 | },
43 | greet: {
44 | type: String,
45 | required: true,
46 | equal: ['Mr', 'Ms', 'Mrs', 'Miss', { attr: 'greetDesc' }],
47 | },
48 | greetDesc: String,
49 | })(
50 | class User {
51 | getFullGreet() {
52 | return `${this.greet} ${this.initials}`;
53 | }
54 | }
55 | );
56 | ```
57 |
--------------------------------------------------------------------------------
/docs/validation/validate-raw-data.md:
--------------------------------------------------------------------------------
1 | # Validate raw data
2 |
3 | In addition to the _instance_ `validate()` method, Structure also adds a _static_ `validate()` to your structure classes that receives a _raw object_ or a _structure instance_ as parameter and has the same return type of the [normal validation](README.md):
4 |
5 | ```javascript
6 | const User = attributes({
7 | name: {
8 | type: String,
9 | minLength: 10,
10 | },
11 | age: {
12 | type: Number,
13 | required: true,
14 | },
15 | })(class User {});
16 |
17 | // Using a raw object
18 | const rawData = {
19 | name: 'John',
20 | };
21 |
22 | const { valid, errors } = User.validate(rawData);
23 |
24 | valid; // false
25 | errors; /*
26 | [
27 | { message: '"name" length must be at least 10 characters long', path: ['name'] },
28 | { message: '"age" is required', path: ['age'] }
29 | ]
30 | */
31 |
32 | // Using a structure instance
33 | const user = new User({
34 | name: 'Some long name',
35 | });
36 |
37 | const validation = User.validate(user);
38 |
39 | validation.valid; // false
40 | validation.errors; /*
41 | [
42 | { message: '"age" is required', path: ['age'] }
43 | ]
44 | */
45 | ```
46 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": ["packages/*"],
3 | "version": "2.0.1",
4 | "npmClient": "yarn",
5 | "useWorkspaces": true
6 | }
7 |
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Structure
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "license": "MIT",
3 | "private": true,
4 | "workspaces": [
5 | "packages/*"
6 | ],
7 | "scripts": {
8 | "build": "lerna run build --stream",
9 | "test": "lerna run test --stream -- --colors",
10 | "test:browser:build": "lerna run test:browser:build --stream -- --colors",
11 | "test:browser:run": "lerna run test:browser:run --stream -- --colors",
12 | "test:browser": "lerna run test:browser --stream -- --colors",
13 | "coverage": "lerna run coverage --stream -- --colors",
14 | "coveralls": "lerna run coveralls --stream",
15 | "lint": "lerna run lint --stream",
16 | "format": "lerna run format --stream",
17 | "benchmark": "node benchmark/benchmark.js"
18 | },
19 | "devDependencies": {
20 | "benchmark": "^2.1.2",
21 | "eslint": "^6.4.0",
22 | "eslint-config-prettier": "^6.3.0",
23 | "eslint-plugin-jest": "^23.8.2",
24 | "jest": "^25.1.0",
25 | "lerna": "^3.22.0",
26 | "prettier": "^2.0.5"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/packages/jest-structure/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [require.resolve('../../.eslintrc')],
3 | };
4 |
--------------------------------------------------------------------------------
/packages/jest-structure/.gitignore:
--------------------------------------------------------------------------------
1 | distTest/
2 |
--------------------------------------------------------------------------------
/packages/jest-structure/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = require('../../.prettierrc.json');
2 |
--------------------------------------------------------------------------------
/packages/jest-structure/README.md:
--------------------------------------------------------------------------------
1 | # jest-structure
2 |
3 | Custom [Jest](https://www.npmjs.com/package/jest) matchers to test [Structure](https://www.npmjs.com/package/structure) instances.
4 |
5 | ## Example usage
6 |
7 | ```js
8 | expect(user).toBeValidStructure();
9 |
10 | expect(user).toBeInvalidStructure();
11 |
12 | expect(user).toHaveInvalidAttribute(['name']);
13 |
14 | expect(user).toHaveInvalidAttribute(['name'], ['"name" is required']);
15 |
16 | expect(user).toHaveInvalidAttribute(['name'], expect.arrayContaining(['"name" is required']));
17 |
18 | expect(user).toHaveInvalidAttributes([
19 | { path: ['name'], messages: expect.arrayContaining(['"name" is required']) },
20 | {
21 | path: ['age'],
22 | messages: ['"age" must be larger than or equal to 2', '"age" must be a positive number'],
23 | },
24 | ]);
25 | ```
26 |
27 | ## Installation
28 |
29 | jest-structure is available in npm, so you can install it with npm or yarn as a development dependency:
30 |
31 | ```sh
32 | npm install --save-dev jest-structure
33 |
34 | # or
35 |
36 | yarn --dev add jest-structure
37 | ```
38 |
39 | ## Setup
40 |
41 | After installing, you need to tell Jest to use jest-structure, this can be done in two ways:
42 |
43 | 1. By importing and manually adding it to Jest (in a [setup file](https://jestjs.io/docs/en/configuration#setupfilesafterenv-array) or directly in the top of your test file):
44 |
45 | ```js
46 | import jestStructure from 'jest-structure';
47 |
48 | expect.extend(jestStructure);
49 | ```
50 |
51 | 2. By allowing jest-structure to add itself to Jest matchers:
52 |
53 | ```js
54 | import 'jest-structure/extend-expect';
55 | ```
56 |
57 | ## Matchers
58 |
59 | ### `toBeValidStructure()`
60 |
61 | This matcher passes if the structure is _valid_:
62 |
63 | ```js
64 | const User = attributes({
65 | name: { type: String, required: true },
66 | })(class User {});
67 |
68 | const validUser = new User({ name: 'Me' });
69 |
70 | expect(validUser).toBeValidStructure(); // passes
71 |
72 | const invalidUser = new User();
73 |
74 | expect(invalidUser).toBeValidStructure(); // fails
75 | ```
76 |
77 | ### `toBeInvalidStructure()`
78 |
79 | This matcher passes if the structure is _invalid_:
80 |
81 | ```js
82 | const User = attributes({
83 | name: { type: String, required: true },
84 | })(class User {});
85 |
86 | const invalidUser = new User();
87 |
88 | expect(invalidUser).toBeInvalidStructure(); // passes
89 |
90 | const validUser = new User({ name: 'Me' });
91 |
92 | expect(validUser).toBeInvalidStructure(); // fails
93 | ```
94 |
95 | ### `toHaveInvalidAttribute(path, messages)`
96 |
97 | This matcher allows you to assert that a _single attribute_ of the structure is invalid, optionally passing the array of error messages for that attribute:
98 |
99 | ```js
100 | const User = attributes({
101 | name: { type: String, required: true },
102 | age: { type: Number, required: true },
103 | })(class User {});
104 |
105 | const user = new User({ age: 42 });
106 |
107 | // passes, because name is invalid
108 | expect(user).toHaveInvalidAttribute(['name']);
109 |
110 | // fails, because age is valid
111 | expect(user).toHaveInvalidAttribute(['age']);
112 |
113 | // passes, because name is invalid with this message
114 | expect(user).toHaveInvalidAttribute(['name'], ['"name" is required']);
115 |
116 | // fails, because name is invalid but not with this message
117 | expect(user).toHaveInvalidAttribute(['name'], ['"name" is not cool']);
118 |
119 | // passes. Notice that you can even use arrayContaining to check for a subset of the errros
120 | expect(user).toHaveInvalidAttribute(['name'], expect.arrayContaining(['"name" is required']));
121 |
122 | // passes. And stringContaining can be used as well
123 | expect(user).toHaveInvalidAttribute(['name'], [expect.stringContaining('required')]);
124 | ```
125 |
126 | ### `toHaveInvalidAttributes([ { path, messages } ])`
127 |
128 | This matcher allows you to assert that _multiple attributes_ of the structure are invalid, optionally passing the array of error messages for each attribute:
129 |
130 | ```js
131 | const User = attributes({
132 | name: { type: String, required: true },
133 | age: { type: Number, required: true },
134 | })(class User {});
135 |
136 | const user = new User({ age: 42 });
137 |
138 | // passes, because name is invalid
139 | expect(user).toHaveInvalidAttributes([{ path: ['name'] }]);
140 |
141 | // fails, because age is valid
142 | expect(user).toHaveInvalidAttributes([{ path: ['age'] }]);
143 |
144 | // fails, because name is invalid but age is valid
145 | expect(user).toHaveInvalidAttributes([{ path: ['name'] }, { path: ['age'] }]);
146 |
147 | // passes, because name is invalid with this message
148 | expect(user).toHaveInvalidAttributes([{ path: ['name'], messages: ['"name" is required'] }]);
149 |
150 | // fails, because name is invalid but not with this message
151 | expect(user).toHaveInvalidAttributes([{ path: ['name'], messages: ['"name" is not cool'] }]);
152 |
153 | // passes. Notice that you can even use arrayContaining to check for a subset of the errros
154 | expect(user).toHaveInvalidAttributes([
155 | { path: ['name'], messages: expect.arrayContaining(['"name" is required']) },
156 | ]);
157 |
158 | // passes. And stringContaining can be used as well
159 | expect(user).toHaveInvalidAttributes([
160 | { path: ['name'], messages: [expect.stringContaining('required')] },
161 | ]);
162 | ```
163 |
--------------------------------------------------------------------------------
/packages/jest-structure/extend-expect.js:
--------------------------------------------------------------------------------
1 | expect.extend(require('./'));
2 |
--------------------------------------------------------------------------------
/packages/jest-structure/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | toBeValidStructure: require('./src/assertions/toBeValidStructure'),
3 | toBeInvalidStructure: require('./src/assertions/toBeInvalidStructure'),
4 | toHaveInvalidAttribute: require('./src/assertions/toHaveInvalidAttribute'),
5 | toHaveInvalidAttributes: require('./src/assertions/toHaveInvalidAttributes'),
6 | };
7 |
--------------------------------------------------------------------------------
/packages/jest-structure/license.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 jest-tructure
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/jest-structure/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jest-structure",
3 | "version": "2.0.1",
4 | "description": "Jest assertions to use with Structure",
5 | "main": "index.js",
6 | "author": "Talysson ",
7 | "license": "MIT",
8 | "scripts": {
9 | "test": "jest"
10 | },
11 | "files": [
12 | "src"
13 | ],
14 | "engines": {
15 | "node": ">=10.13.0"
16 | },
17 | "devDependencies": {
18 | "structure": "2.0.1"
19 | },
20 | "peerDependencies": {
21 | "jest": "^25.1.0"
22 | },
23 | "dependencies": {}
24 | }
25 |
--------------------------------------------------------------------------------
/packages/jest-structure/src/assertions/toBeInvalidStructure.js:
--------------------------------------------------------------------------------
1 | const createValidityAssertion = require('../lib/validityAssertion');
2 |
3 | module.exports = createValidityAssertion({
4 | pass: (valid) => !valid,
5 | passName: 'invalid',
6 | failName: 'valid',
7 | });
8 |
--------------------------------------------------------------------------------
/packages/jest-structure/src/assertions/toBeValidStructure.js:
--------------------------------------------------------------------------------
1 | const createValidityAssertion = require('../lib/validityAssertion');
2 |
3 | module.exports = createValidityAssertion({
4 | pass: (valid) => valid,
5 | passName: 'valid',
6 | failName: 'invalid',
7 | });
8 |
--------------------------------------------------------------------------------
/packages/jest-structure/src/assertions/toHaveInvalidAttribute.js:
--------------------------------------------------------------------------------
1 | const { sortMessagesByExpected } = require('../lib/sorting');
2 | const { isValidPath } = require('../lib/attributePath');
3 | const { failInvalidUsage, failNoNegative, failWrongValidity } = require('../lib/errors');
4 | const matcherName = 'toHaveInvalidAttribute';
5 | const exampleName = 'structure';
6 | const attributePathHint = 'attributePath';
7 | const errorMessagesHint = '[errorMessages]';
8 |
9 | module.exports = function toHaveInvalidAttribute(structure, attributePath, expectedErrorMessages) {
10 | if (this.isNot) {
11 | return failNoNegative(matcherName);
12 | }
13 |
14 | if (!isValidPath(attributePath)) {
15 | return failInvalidUsage(
16 | matcherName,
17 | usageHint(this),
18 | 'must not be called without the attribute path'
19 | );
20 | }
21 |
22 | const { valid, errors } = structure.validate();
23 |
24 | if (valid) {
25 | return failWrongValidity({
26 | pass: false,
27 | passName: 'invalid',
28 | failName: 'valid',
29 | context: this,
30 | });
31 | }
32 |
33 | const attributeErrors = errors.filter((error) => this.equals(error.path, attributePath));
34 |
35 | const joinedAttributeName = attributePath.join('.');
36 |
37 | if (isExpectedAttributeValid(expectedErrorMessages, attributeErrors)) {
38 | return {
39 | pass: Boolean(attributeErrors.length),
40 | message: () =>
41 | `Expected: ${joinedAttributeName} to be ${this.utils.EXPECTED_COLOR('invalid')}\n` +
42 | `Received: ${joinedAttributeName} is ${this.utils.RECEIVED_COLOR('valid')}`,
43 | };
44 | }
45 |
46 | const validationErrorMessages = attributeErrors.map((error) => error.message);
47 | const errorMessages = sortMessagesByExpected(validationErrorMessages, expectedErrorMessages);
48 |
49 | return {
50 | pass: this.equals(errorMessages, expectedErrorMessages),
51 | message: () =>
52 | this.utils.printDiffOrStringify(
53 | expectedErrorMessages,
54 | errorMessages,
55 | `Expected ${joinedAttributeName} error messages`,
56 | `Received ${joinedAttributeName} error messages`,
57 | this.expand
58 | ),
59 | };
60 | };
61 |
62 | const usageHint = (context) =>
63 | context.utils.matcherHint(matcherName, exampleName, attributePathHint, {
64 | secondArgument: errorMessagesHint,
65 | });
66 |
67 | const isExpectedAttributeValid = (expectedErrorMessages, attributeErrors) =>
68 | !(expectedErrorMessages && attributeErrors.length);
69 |
--------------------------------------------------------------------------------
/packages/jest-structure/src/assertions/toHaveInvalidAttributes.js:
--------------------------------------------------------------------------------
1 | const { sortErrorsByExpected } = require('../lib/sorting');
2 | const { areExpectedErrorsPathsValid } = require('../lib/attributePath');
3 | const { failInvalidUsage, failNoNegative, failWrongValidity } = require('../lib/errors');
4 | const matcherName = 'toHaveInvalidAttributes';
5 | const exampleName = 'structure';
6 | const expectedErrorsHint = '[{ path (required), messages (optional) }]';
7 |
8 | module.exports = function toHaveInvalidAttributes(structure, expectedErrors) {
9 | if (this.isNot) {
10 | return failNoNegative(matcherName);
11 | }
12 |
13 | if (!areExpectedErrorsPresent(expectedErrors)) {
14 | return failInvalidUsage(
15 | matcherName,
16 | usageHint(this),
17 | 'must not be called without the expected errros'
18 | );
19 | }
20 |
21 | const { valid, errors } = structure.validate();
22 |
23 | if (valid) {
24 | return failWrongValidity({
25 | pass: false,
26 | passName: 'invalid',
27 | failName: 'valid',
28 | context: this,
29 | });
30 | }
31 |
32 | if (!areExpectedErrorsPathsValid(expectedErrors)) {
33 | return failNoPath(this);
34 | }
35 |
36 | const errorsForComparison = sortErrorsByExpected(errors, expectedErrors, this);
37 |
38 | return {
39 | pass: this.equals(errorsForComparison, expectedErrors),
40 | message: () =>
41 | this.utils.printDiffOrStringify(
42 | expectedErrors,
43 | errorsForComparison,
44 | 'Expected errors',
45 | 'Received errors',
46 | this.expand
47 | ),
48 | };
49 | };
50 |
51 | const areExpectedErrorsPresent = (expectedErrors) => expectedErrors && expectedErrors.length;
52 |
53 | const usageHint = (context) =>
54 | context.utils.matcherHint(matcherName, exampleName, expectedErrorsHint);
55 |
56 | const failNoPath = (context) => ({
57 | pass: false,
58 | message: () =>
59 | `${matcherName} must not be called without the attribute paths\n` +
60 | `Example: ${usageHint(context)}`,
61 | });
62 |
--------------------------------------------------------------------------------
/packages/jest-structure/src/lib/attributePath.js:
--------------------------------------------------------------------------------
1 | const isValidPath = (path) => Boolean(path && path.length);
2 |
3 | const areExpectedErrorsPathsValid = (expectedErrors) => expectedErrors.every(errorHasPath);
4 | const errorHasPath = (error) => isValidPath(error.path);
5 |
6 | const groupByPath = (errors, context) =>
7 | errors.reduce((grouped, error) => {
8 | const group = grouped.find((group) => context.equals(group.path, error.path));
9 |
10 | if (group) {
11 | group.messages.push(error.message);
12 | return grouped;
13 | }
14 |
15 | const newGroup = { path: error.path, messages: [error.message] };
16 |
17 | return [...grouped, newGroup];
18 | }, []);
19 |
20 | module.exports = { isValidPath, areExpectedErrorsPathsValid, groupByPath };
21 |
--------------------------------------------------------------------------------
/packages/jest-structure/src/lib/errors.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | failNoNegative: (matcherName) => ({
3 | pass: true, // it has to be true because it's using .not
4 | message: () => `${matcherName} must not be used with .not`,
5 | }),
6 | failWrongValidity: ({ pass, passName, failName, context }) => ({
7 | pass,
8 | message: () =>
9 | `Expected: to be ${context.utils.EXPECTED_COLOR(context.isNot ? failName : passName)}\n` +
10 | `Received: is ${context.utils.RECEIVED_COLOR(context.isNot ? passName : failName)}`,
11 | }),
12 | failInvalidUsage: (matcherName, usageHint, message) => ({
13 | pass: false,
14 | message: () => `${matcherName} ${message}\n` + `Example: ${usageHint}`,
15 | }),
16 | };
17 |
--------------------------------------------------------------------------------
/packages/jest-structure/src/lib/sorting.js:
--------------------------------------------------------------------------------
1 | const { groupByPath } = require('./attributePath');
2 |
3 | function sortMessagesByExpected(errorMessages, expectedErrorMessages) {
4 | expectedErrorMessages = expectedErrorMessages.sample || expectedErrorMessages;
5 |
6 | const equalMessages = expectedErrorMessages.filter((message) => errorMessages.includes(message));
7 | const differentMessages = errorMessages.filter(
8 | (message) => !expectedErrorMessages.includes(message)
9 | );
10 |
11 | return [...equalMessages, ...differentMessages];
12 | }
13 |
14 | function sortErrorsByExpected(errors, expectedErrors, context) {
15 | const groupedErrors = groupByPath(errors, context);
16 |
17 | const equalErrors = expectedErrors
18 | .filter((error) =>
19 | groupedErrors.find((groupedError) => context.equals(groupedError.path, error.path))
20 | )
21 | .map((expectedError) => {
22 | const error = groupedErrors.find((error) => context.equals(expectedError.path, error.path));
23 |
24 | if (expectedError.messages) {
25 | return {
26 | ...error,
27 | messages: sortMessagesByExpected(error.messages, expectedError.messages),
28 | };
29 | }
30 |
31 | return { path: error.path };
32 | });
33 |
34 | const differentErrors = groupedErrors.filter(
35 | (groupedError) => !expectedErrors.find((error) => context.equals(groupedError.path, error.path))
36 | );
37 |
38 | return [...equalErrors, ...differentErrors];
39 | }
40 |
41 | module.exports = { sortErrorsByExpected, sortMessagesByExpected };
42 |
--------------------------------------------------------------------------------
/packages/jest-structure/src/lib/validityAssertion.js:
--------------------------------------------------------------------------------
1 | const { failWrongValidity } = require('../lib/errors');
2 |
3 | module.exports = function createValidityAssertion({ pass, passName, failName }) {
4 | return function(structure, expected) {
5 | this.utils.ensureNoExpected(expected);
6 |
7 | const { valid } = structure.validate();
8 |
9 | return failWrongValidity({
10 | pass: pass(valid),
11 | passName,
12 | failName,
13 | context: this,
14 | });
15 | };
16 | };
17 |
--------------------------------------------------------------------------------
/packages/structure/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [require.resolve('../../.eslintrc')],
3 | };
4 |
--------------------------------------------------------------------------------
/packages/structure/.gitignore:
--------------------------------------------------------------------------------
1 | distTest/
2 |
--------------------------------------------------------------------------------
/packages/structure/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 |
--------------------------------------------------------------------------------
/packages/structure/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = require('../../.prettierrc.json');
2 |
--------------------------------------------------------------------------------
/packages/structure/README.md:
--------------------------------------------------------------------------------
1 | #
2 |
3 | ## A simple schema/attributes library built on top of modern JavaScript
4 |
5 | ## [](https://www.npmjs.com/package/structure) [](https://travis-ci.org/talyssonoc/structure) [](https://coveralls.io/github/talyssonoc/structure?branch=master) [](https://codeclimate.com/github/talyssonoc/structure) [](https://js.org/)
6 |
7 | Structure provides a simple interface which allows you to add attributes to your ES6 classes based on a schema, with validations and type coercion.
8 |
9 | ## Use cases
10 |
11 | You can use Structure for a lot of different cases, including:
12 |
13 | - Domain entities and value objects
14 | - Model business rules
15 | - Validation and coercion of request data
16 | - Map pure objects and JSON to your application classes
17 | - Add attributes to classes that you can't change the class hierarchy
18 |
19 | What Structure is **not**:
20 |
21 | - It's not a database abstraction
22 | - It's not a Model of a MVC framework
23 | - It's not an attempt to simulate classic inheritance in JavaScript
24 |
25 | ## [Documentation](https://structure.js.org/)
26 |
27 | ## Example usage
28 |
29 | For each attribute on your schema, a getter and a setter will be created into the given class. It'll also auto-assign those attributes passed to the constructor.
30 |
31 | ```js
32 | const { attributes } = require('structure');
33 |
34 | const User = attributes({
35 | name: String,
36 | age: {
37 | type: Number,
38 | default: 18,
39 | },
40 | birthday: Date,
41 | })(
42 | class User {
43 | greet() {
44 | return `Hello ${this.name}`;
45 | }
46 | }
47 | );
48 |
49 | /* The attributes "wraps" the Class, still providing access to its methods: */
50 |
51 | const user = new User({
52 | name: 'John Foo',
53 | });
54 |
55 | user.name; // 'John Foo'
56 | user.greet(); // 'Hello John Foo'
57 | ```
58 |
59 | ## [Contributing](../../contributing.md)
60 |
61 | ## [LICENSE](../../license.md)
62 |
--------------------------------------------------------------------------------
/packages/structure/license.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Structure
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/structure/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "structure",
3 | "version": "2.0.1",
4 | "description": "A simple schema/attributes library built on top of modern JavaScript",
5 | "main": "src/index.js",
6 | "browser": "dist/structure.js",
7 | "files": [
8 | "dist",
9 | "src"
10 | ],
11 | "engines": {
12 | "node": ">=10.13.0"
13 | },
14 | "homepage": "https://structure.js.org/",
15 | "repository": "https://github.com/talyssonoc/structure",
16 | "bugs": "https://github.com/talyssonoc/structure/issues",
17 | "author": "Talysson ",
18 | "contributors": [
19 | "Fernando ",
20 | "Wender Freese "
21 | ],
22 | "publishConfig": {
23 | "registry": "https://registry.npmjs.org/"
24 | },
25 | "license": "MIT",
26 | "keywords": [
27 | "entity",
28 | "model",
29 | "domain"
30 | ],
31 | "scripts": {
32 | "test": "jest --config=test/jest.node.js",
33 | "test:browser:build": "webpack --config test/webpack.pretest.js",
34 | "test:browser:run": "jest --config=test/jest.browser.js",
35 | "test:browser": "yarn run test:browser:build && yarn run test:browser:run",
36 | "coverage": "yarn test --coverage",
37 | "build": "webpack",
38 | "prepublish": "yarn run build",
39 | "coveralls": "yarn run coverage --coverageReporters=text-lcov | coveralls",
40 | "lint": "eslint {src,test}/**/*.js",
41 | "format": "prettier --write {src,test}/**/*.js"
42 | },
43 | "dependencies": {
44 | "@hapi/joi": "^16.1.8",
45 | "lodash": "^4.17.15"
46 | },
47 | "devDependencies": {
48 | "@babel/core": "^7.10.0",
49 | "@babel/plugin-proposal-object-rest-spread": "^7.10.0",
50 | "@babel/preset-env": "^7.10.0",
51 | "@jest-runner/electron": "^3.0.0",
52 | "babel-loader": "^8.1.0",
53 | "coveralls": "^3.1.0",
54 | "electron": "^9.0.0",
55 | "jest-structure": "2.0.1",
56 | "webpack": "^4.41.2",
57 | "webpack-cli": "^3.3.9"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/packages/structure/src/attributes/index.js:
--------------------------------------------------------------------------------
1 | const { ATTRIBUTES } = require('../symbols');
2 |
3 | exports.setInInstance = function setAttributesInInstance(instance, attributes) {
4 | Object.defineProperty(instance, ATTRIBUTES, {
5 | configurable: true,
6 | value: attributes,
7 | });
8 | };
9 |
--------------------------------------------------------------------------------
/packages/structure/src/attributesDecorator.js:
--------------------------------------------------------------------------------
1 | const Schema = require('./schema');
2 | const Descriptors = require('./descriptors');
3 | const Errors = require('./errors');
4 |
5 | module.exports = function attributes(attributeDefinitions, options = {}) {
6 | if (typeof options !== 'object') {
7 | throw Errors.classAsSecondParam(options);
8 | }
9 |
10 | return function decorator(Class) {
11 | const schema = Schema.for({
12 | wrappedClass: Class,
13 | attributeDefinitions,
14 | options,
15 | });
16 |
17 | const StructureClass = new Proxy(Class, {
18 | construct(wrappedClass, constructorArgs, proxy) {
19 | const instance = Reflect.construct(wrappedClass, constructorArgs, proxy);
20 |
21 | const passedAttributes = { ...constructorArgs[0] };
22 |
23 | return schema.initializeInstance(instance, {
24 | attributes: passedAttributes,
25 | });
26 | },
27 | });
28 |
29 | Descriptors.addTo(schema, StructureClass);
30 |
31 | return StructureClass;
32 | };
33 | };
34 |
--------------------------------------------------------------------------------
/packages/structure/src/cloning/index.js:
--------------------------------------------------------------------------------
1 | exports.for = function cloneFor(StructureClass) {
2 | return {
3 | clone(overwrites = {}, options = {}) {
4 | const { strict } = options;
5 |
6 | const newAttributes = {
7 | ...this.attributes,
8 | ...overwrites,
9 | };
10 |
11 | let cloneInstance;
12 |
13 | if (strict) {
14 | cloneInstance = StructureClass.buildStrict(newAttributes);
15 | } else {
16 | cloneInstance = new StructureClass(newAttributes);
17 | }
18 |
19 | return cloneInstance;
20 | },
21 | };
22 | };
23 |
--------------------------------------------------------------------------------
/packages/structure/src/coercion/coercion.js:
--------------------------------------------------------------------------------
1 | const { isFunction } = require('lodash');
2 |
3 | exports.create = function createCoercionFor(coercion, attributeDefinition) {
4 | return {
5 | coerce(value) {
6 | if (value === undefined) {
7 | return;
8 | }
9 |
10 | if (value === null) {
11 | return getNullableValue(coercion, attributeDefinition);
12 | }
13 |
14 | if (coercion.isCoerced(value, attributeDefinition)) {
15 | return value;
16 | }
17 |
18 | return coercion.coerce(value, attributeDefinition);
19 | },
20 | };
21 | };
22 |
23 | exports.disabled = {
24 | coerce: (value) => value,
25 | };
26 |
27 | const getNullableValue = (coercion, attributeDefinition) =>
28 | needsNullableInitialization(attributeDefinition) ? getNullValue(coercion) : null;
29 |
30 | const needsNullableInitialization = (attributeDefinition) =>
31 | !attributeDefinition.options.required && !attributeDefinition.options.nullable;
32 |
33 | const getNullValue = (coercion) =>
34 | isFunction(coercion.nullValue) ? coercion.nullValue() : coercion.nullValue;
35 |
--------------------------------------------------------------------------------
/packages/structure/src/coercion/coercions/array.js:
--------------------------------------------------------------------------------
1 | const Errors = require('../../errors');
2 |
3 | module.exports = {
4 | isCoerced: () => false, // always tries to coerce array types
5 | nullValue() {
6 | throw Errors.arrayOrIterable();
7 | },
8 | coerce(rawValue, attributeDefinition) {
9 | assertIterable(rawValue);
10 |
11 | const items = extractItems(rawValue);
12 |
13 | const instance = createInstance(attributeDefinition);
14 |
15 | return fillInstance(instance, items, attributeDefinition);
16 | },
17 | };
18 |
19 | function assertIterable(value) {
20 | if (!isIterable(value)) {
21 | throw Errors.arrayOrIterable();
22 | }
23 | }
24 |
25 | function isIterable(value) {
26 | return value !== undefined && (value.length != null || value[Symbol.iterator]);
27 | }
28 |
29 | function extractItems(iterable) {
30 | if (!Array.isArray(iterable) && iterable[Symbol.iterator]) {
31 | return Array(...iterable);
32 | }
33 |
34 | return iterable;
35 | }
36 |
37 | function createInstance(attributeDefinition) {
38 | const type = attributeDefinition.resolveType();
39 | return new type();
40 | }
41 |
42 | function fillInstance(instance, items, attributeDefinition) {
43 | for (let i = 0; i < items.length; i++) {
44 | instance.push(attributeDefinition.itemTypeDefinition.coerce(items[i]));
45 | }
46 |
47 | return instance;
48 | }
49 |
--------------------------------------------------------------------------------
/packages/structure/src/coercion/coercions/boolean.js:
--------------------------------------------------------------------------------
1 | const { isBoolean } = require('lodash');
2 |
3 | module.exports = {
4 | type: Boolean,
5 | isCoerced: isBoolean,
6 | nullValue: false,
7 | coerce(value) {
8 | return this.type(value);
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/packages/structure/src/coercion/coercions/date.js:
--------------------------------------------------------------------------------
1 | const { isDate } = require('lodash');
2 |
3 | module.exports = {
4 | type: Date,
5 | isCoerced: isDate,
6 | nullValue: () => new Date(null),
7 | coerce(value) {
8 | return new this.type(value);
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/packages/structure/src/coercion/coercions/generic.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | isCoerced(value, attributeDefinition) {
3 | return value instanceof attributeDefinition.resolveType();
4 | },
5 | coerce(value, attributeDefinition) {
6 | const type = attributeDefinition.resolveType();
7 |
8 | return new type(value);
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/packages/structure/src/coercion/coercions/number.js:
--------------------------------------------------------------------------------
1 | const { isNumber } = require('lodash');
2 |
3 | module.exports = {
4 | type: Number,
5 | isCoerced: isNumber,
6 | nullValue: 0,
7 | coerce(value) {
8 | return this.type(value);
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/packages/structure/src/coercion/coercions/string.js:
--------------------------------------------------------------------------------
1 | const { isString } = require('lodash');
2 |
3 | module.exports = {
4 | type: String,
5 | isCoerced: isString,
6 | nullValue: '',
7 | coerce(value) {
8 | return this.type(value);
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/packages/structure/src/coercion/index.js:
--------------------------------------------------------------------------------
1 | const arrayCoercion = require('./coercions/array');
2 | const genericCoercionFor = require('./coercions/generic');
3 | const Coercion = require('./coercion');
4 |
5 | const types = [
6 | require('./coercions/string'),
7 | require('./coercions/number'),
8 | require('./coercions/boolean'),
9 | require('./coercions/date'),
10 | ];
11 |
12 | exports.for = function coercionFor(attributeDefinition) {
13 | if (!attributeDefinition.options.coercion) {
14 | return Coercion.disabled;
15 | }
16 |
17 | const coercion = getCoercion(attributeDefinition);
18 |
19 | return Coercion.create(coercion, attributeDefinition);
20 | };
21 |
22 | function getCoercion(attributeDefinition) {
23 | if (attributeDefinition.isArrayType) {
24 | return arrayCoercion;
25 | }
26 |
27 | const coercion = types.find((c) => c.type === attributeDefinition.options.type);
28 |
29 | if (coercion) {
30 | return coercion;
31 | }
32 |
33 | return genericCoercionFor;
34 | }
35 |
--------------------------------------------------------------------------------
/packages/structure/src/descriptors/index.js:
--------------------------------------------------------------------------------
1 | const { isObject } = require('lodash');
2 | const { SCHEMA, ATTRIBUTES, DEFAULT_ACCESSOR } = require('../symbols');
3 | const Errors = require('../errors');
4 | const StrictMode = require('../strictMode');
5 | const Cloning = require('../cloning');
6 | const Attributes = require('../attributes');
7 | const { defineProperty, defineProperties } = Object;
8 |
9 | exports.addTo = function addDescriptorsTo(schema, StructureClass) {
10 | setSchema();
11 | setBuildStrict();
12 | setAttributesGetterAndSetter();
13 | setGenericAttributeGetterAndSetter();
14 | setEachAttributeGetterAndSetter();
15 | setValidation();
16 | setSerialization();
17 | setCloning();
18 |
19 | function setSchema() {
20 | defineProperty(StructureClass, SCHEMA, {
21 | value: schema,
22 | });
23 |
24 | defineProperty(StructureClass.prototype, SCHEMA, {
25 | value: schema,
26 | });
27 | }
28 |
29 | function setBuildStrict() {
30 | const strictMode = StrictMode.for(schema, StructureClass);
31 |
32 | defineProperty(StructureClass, 'buildStrict', {
33 | value: strictMode.buildStrict,
34 | });
35 | }
36 |
37 | function setAttributesGetterAndSetter() {
38 | defineProperty(StructureClass.prototype, 'attributes', {
39 | get() {
40 | return this[ATTRIBUTES];
41 | },
42 |
43 | set(newAttributes) {
44 | if (!isObject(newAttributes)) {
45 | throw Errors.nonObjectAttributes();
46 | }
47 |
48 | const coercedAttributes = schema.coerce(newAttributes);
49 |
50 | Attributes.setInInstance(this, coercedAttributes);
51 | },
52 | });
53 | }
54 |
55 | function setGenericAttributeGetterAndSetter() {
56 | defineProperties(StructureClass.prototype, {
57 | get: {
58 | value: function get(attributeName) {
59 | return this.attributes[attributeName];
60 | },
61 | },
62 | set: {
63 | value: function set(attributeName, attributeValue) {
64 | const attributeDefinition = schema.attributeDefinitions[attributeName];
65 |
66 | if (!attributeDefinition) {
67 | throw Errors.inexistentAttribute(attributeName);
68 | }
69 |
70 | const coercedValue = attributeDefinition.coerce(attributeValue);
71 | this.attributes[attributeName] = coercedValue;
72 | },
73 | },
74 | });
75 | }
76 |
77 | function setEachAttributeGetterAndSetter() {
78 | schema.attributeDefinitions.forEach((attrDefinition) => {
79 | defineProperty(
80 | StructureClass.prototype,
81 | attrDefinition.name,
82 | attributeDescriptorFor(attrDefinition)
83 | );
84 | });
85 | }
86 |
87 | function attributeDescriptorFor(attrDefinition) {
88 | const { name } = attrDefinition;
89 |
90 | const attributeDescriptor = findAttributeDescriptor(name);
91 |
92 | if (isDefaultAccessor(attributeDescriptor.get)) {
93 | attributeDescriptor.get = defaultGetterFor(name);
94 | }
95 |
96 | if (isDefaultAccessor(attributeDescriptor.set)) {
97 | attributeDescriptor.set = defaultSetterFor(name);
98 | }
99 |
100 | return attributeDescriptor;
101 | }
102 |
103 | function setValidation() {
104 | defineProperty(StructureClass, 'validate', {
105 | value: function validate(attributes) {
106 | return schema.validateAttributes(attributes);
107 | },
108 | });
109 |
110 | defineProperty(StructureClass.prototype, 'validate', {
111 | value: function validate() {
112 | return schema.validateInstance(this);
113 | },
114 | });
115 | }
116 |
117 | function setSerialization() {
118 | defineProperty(StructureClass.prototype, 'toJSON', {
119 | value: function toJSON() {
120 | return schema.serialize(this);
121 | },
122 | });
123 | }
124 |
125 | function setCloning() {
126 | const cloning = Cloning.for(StructureClass);
127 |
128 | defineProperty(StructureClass.prototype, 'clone', {
129 | value: cloning.clone,
130 | });
131 | }
132 |
133 | function defaultGetterFor(name) {
134 | function get() {
135 | return this.get(name);
136 | }
137 |
138 | get[DEFAULT_ACCESSOR] = true;
139 |
140 | return get;
141 | }
142 |
143 | function defaultSetterFor(name) {
144 | function set(value) {
145 | this.set(name, value);
146 | }
147 |
148 | set[DEFAULT_ACCESSOR] = true;
149 |
150 | return set;
151 | }
152 |
153 | function isDefaultAccessor(accessor) {
154 | return !accessor || accessor[DEFAULT_ACCESSOR];
155 | }
156 |
157 | function findAttributeDescriptor(propertyName) {
158 | let proto = StructureClass.prototype;
159 |
160 | while (proto !== Object.prototype) {
161 | const attributeDescriptor = Object.getOwnPropertyDescriptor(proto, propertyName);
162 |
163 | if (attributeDescriptor) {
164 | return {
165 | ...attributeDescriptor,
166 | enumerable: false,
167 | configurable: true,
168 | };
169 | }
170 |
171 | proto = proto.__proto__;
172 | }
173 |
174 | return {};
175 | }
176 | };
177 |
--------------------------------------------------------------------------------
/packages/structure/src/errors/DefaultValidationError.js:
--------------------------------------------------------------------------------
1 | class DefautValidationError extends Error {
2 | constructor(errors) {
3 | super('Invalid Attributes');
4 | this.details = errors;
5 | }
6 | }
7 |
8 | module.exports = DefautValidationError;
9 |
--------------------------------------------------------------------------------
/packages/structure/src/errors/index.js:
--------------------------------------------------------------------------------
1 | exports.classAsSecondParam = (ErroneousPassedClass) =>
2 | new Error(
3 | `You passed the structure class as the second parameter of attributes(). The expected usage is \`attributes(schema)(${ErroneousPassedClass.name ||
4 | 'StructureClass'})\`.`
5 | );
6 |
7 | exports.nonObjectAttributes = () => new TypeError("#attributes can't be set to a non-object.");
8 |
9 | exports.arrayOrIterable = () => new TypeError('Value must be iterable or array-like.');
10 |
11 | exports.missingDynamicType = (attributeName) =>
12 | new Error(`Missing dynamic type for attribute: ${attributeName}.`);
13 |
14 | exports.invalidType = (attributeName) =>
15 | new TypeError(
16 | `Attribute type must be a constructor or the name of a dynamic type: ${attributeName}.`
17 | );
18 |
19 | exports.invalidAttributes = (errors, StructureValidationError) =>
20 | new StructureValidationError(errors);
21 |
22 | exports.inexistentAttribute = (attributeName) =>
23 | new Error(`${attributeName} is not an attribute of this structure`);
24 |
--------------------------------------------------------------------------------
/packages/structure/src/index.js:
--------------------------------------------------------------------------------
1 | exports.attributes = require('./attributesDecorator');
2 |
--------------------------------------------------------------------------------
/packages/structure/src/initialization/index.js:
--------------------------------------------------------------------------------
1 | const Attributes = require('../attributes');
2 |
3 | exports.for = function initializationForSchema(schema) {
4 | return {
5 | initialize(instance, { attributes }) {
6 | Attributes.setInInstance(instance, Object.create(null));
7 |
8 | for (let attrDefinition of schema.attributeDefinitions) {
9 | const attrPassedValue = attributes[attrDefinition.name];
10 |
11 | // will coerce through setters
12 | instance[attrDefinition.name] = attrDefinition.initialize(instance, attrPassedValue);
13 | }
14 |
15 | return instance;
16 | },
17 | };
18 | };
19 |
--------------------------------------------------------------------------------
/packages/structure/src/schema/AttributeDefinitions/AttributeDefinition.js:
--------------------------------------------------------------------------------
1 | const { isFunction, isString, isUndefined } = require('lodash');
2 | const Coercion = require('../../coercion');
3 | const Validation = require('../../validation');
4 | const Errors = require('../../errors');
5 | const { SCHEMA } = require('../../symbols');
6 |
7 | class AttributeDefinition {
8 | static for(name, options, schema) {
9 | if (options.__isAttributeDefinition) {
10 | return new this({
11 | name,
12 | options: options.options,
13 | schema,
14 | });
15 | }
16 |
17 | options = makeComplete(options);
18 |
19 | this.assertValidType(name, options);
20 | this.assertDynamicExists(name, options, schema);
21 |
22 | return new this({
23 | name,
24 | options,
25 | schema,
26 | });
27 | }
28 |
29 | static compare(definitionA, definitionB) {
30 | if (definitionA.isDynamicDefault === definitionB.isDynamicDefault) {
31 | return 0;
32 | }
33 |
34 | if (definitionA.isDynamicDefault) {
35 | return 1;
36 | }
37 |
38 | return -1;
39 | }
40 |
41 | static assertDynamicExists(name, options, schema) {
42 | if (!hasDynamicType(options)) {
43 | return;
44 | }
45 |
46 | if (!schema.hasDynamicTypeFor(options.type)) {
47 | throw Errors.missingDynamicType(name);
48 | }
49 | }
50 |
51 | static assertValidType(name, options) {
52 | if (hasDynamicType(options) || hasStaticType(options)) {
53 | return;
54 | }
55 |
56 | throw Errors.invalidType(name);
57 | }
58 |
59 | constructor({ name, options, schema }) {
60 | // used to extend schemas when subclassing structures
61 | this.__isAttributeDefinition = true;
62 |
63 | this.name = name;
64 | options = this.options = applyDefaultOptions(options, schema);
65 | this.hasDefault = 'default' in options;
66 | this.isDynamicDefault = isFunction(options.default);
67 | this.hasDynamicType = hasDynamicType(options);
68 | this.schema = schema;
69 |
70 | if (options.itemType) {
71 | this.isArrayType = true;
72 | this.itemTypeDefinition = AttributeDefinition.for('item', options.itemType, schema);
73 | }
74 |
75 | this.coercion = Coercion.for(this);
76 | this.validation = Validation.forAttribute(this);
77 | }
78 |
79 | resolveType() {
80 | if (this.hasDynamicType) {
81 | return this.schema.dynamicTypeFor(this.options.type);
82 | }
83 |
84 | return this.options.type;
85 | }
86 |
87 | get isNestedSchema() {
88 | return Boolean(this.resolveType()[SCHEMA]);
89 | }
90 |
91 | get itemsAreStructures() {
92 | return this.isArrayType && this.itemTypeDefinition.isNestedSchema;
93 | }
94 |
95 | coerce(newValue) {
96 | return this.coercion.coerce(newValue);
97 | }
98 |
99 | shouldSerialize(attributeValue) {
100 | return this.isValuePresent(attributeValue) || this.isValueNullable(attributeValue);
101 | }
102 |
103 | isValuePresent(attributeValue) {
104 | return attributeValue != null;
105 | }
106 |
107 | isValueNullable(attributeValue) {
108 | return attributeValue !== undefined && this.options.nullable;
109 | }
110 |
111 | initialize(instance, attributeValue) {
112 | if (this.shouldInitializeToDefault(attributeValue)) {
113 | return this.defaultValueFor(instance);
114 | }
115 |
116 | return attributeValue;
117 | }
118 |
119 | defaultValueFor(instance) {
120 | if (this.isDynamicDefault) {
121 | return this.options.default(instance);
122 | }
123 |
124 | return this.options.default;
125 | }
126 |
127 | shouldInitializeToDefault(attributeValue) {
128 | const isUndefined = attributeValue === undefined;
129 | const isDefaultableNull = !this.options.nullable && attributeValue === null;
130 |
131 | return this.hasDefault && (isUndefined || isDefaultableNull);
132 | }
133 | }
134 |
135 | const makeComplete = (options) => {
136 | if (!isShorthand(options)) {
137 | return options;
138 | }
139 |
140 | return { type: options };
141 | };
142 |
143 | const applyDefaultOptions = (options, schema) => {
144 | return {
145 | ...options,
146 | coercion: inheritOptionFromSchema(options.coercion, schema.options.coercion),
147 | };
148 | };
149 |
150 | const inheritOptionFromSchema = (option, schemaOption) =>
151 | !isUndefined(option) ? option : schemaOption;
152 |
153 | const isShorthand = (options) => isFunction(options) || isString(options);
154 |
155 | const hasStaticType = (options) => isFunction(options.type);
156 | const hasDynamicType = (options) => isString(options.type);
157 |
158 | module.exports = AttributeDefinition;
159 |
--------------------------------------------------------------------------------
/packages/structure/src/schema/AttributeDefinitions/index.js:
--------------------------------------------------------------------------------
1 | const AttributeDefinition = require('./AttributeDefinition');
2 |
3 | class AttributeDefinitions extends Array {
4 | static for(attributeDefinitions, { schema }) {
5 | attributeDefinitions = Object.keys(attributeDefinitions).map((attributeName) => ({
6 | name: attributeName,
7 | options: attributeDefinitions[attributeName],
8 | }));
9 |
10 | return new this(attributeDefinitions, { schema });
11 | }
12 |
13 | constructor(attributeDefinitions, { schema }) {
14 | attributeDefinitions = attributeDefinitions
15 | .map(({ name, options }) => AttributeDefinition.for(name, options, schema))
16 | .sort(AttributeDefinition.compare);
17 |
18 | super(...attributeDefinitions);
19 |
20 | for (let attributeDefinition of attributeDefinitions) {
21 | this[attributeDefinition.name] = attributeDefinition;
22 | }
23 | }
24 |
25 | byKey() {
26 | return this.reduce(
27 | (attributeDefinitions, attrDefinition) => ({
28 | ...attributeDefinitions,
29 | [attrDefinition.name]: this[attrDefinition.name],
30 | }),
31 | {}
32 | );
33 | }
34 | }
35 |
36 | module.exports = AttributeDefinitions;
37 |
--------------------------------------------------------------------------------
/packages/structure/src/schema/index.js:
--------------------------------------------------------------------------------
1 | const AttributeDefinitions = require('./AttributeDefinitions');
2 | const Initialization = require('../initialization');
3 | const Validation = require('../validation');
4 | const Serialization = require('../serialization');
5 | const { SCHEMA } = require('../symbols');
6 |
7 | class Schema {
8 | static for({ attributeDefinitions, wrappedClass, options }) {
9 | const parentSchema = wrappedClass[SCHEMA];
10 |
11 | if (parentSchema) {
12 | return this.extend(parentSchema, {
13 | attributeDefinitions,
14 | wrappedClass,
15 | options,
16 | });
17 | }
18 |
19 | return new this({
20 | attributeDefinitions,
21 | wrappedClass,
22 | options,
23 | });
24 | }
25 |
26 | static extend(parentSchema, { attributeDefinitions, wrappedClass, options }) {
27 | const parentAttributes = parentSchema.attributeDefinitions.byKey();
28 |
29 | attributeDefinitions = {
30 | ...parentAttributes,
31 | ...attributeDefinitions,
32 | };
33 |
34 | options = {
35 | ...parentSchema.options,
36 | ...options,
37 | dynamics: {
38 | ...parentSchema.dynamics,
39 | ...options.dynamics,
40 | },
41 | };
42 |
43 | return new this({
44 | attributeDefinitions,
45 | wrappedClass,
46 | options,
47 | });
48 | }
49 |
50 | constructor({ attributeDefinitions, wrappedClass, options }) {
51 | this.options = applyDefaultOptions(options);
52 | this.attributeDefinitions = AttributeDefinitions.for(attributeDefinitions, { schema: this });
53 | this.wrappedClass = wrappedClass;
54 | this.identifier = options.identifier || wrappedClass.name;
55 |
56 | this.initialization = Initialization.for(this);
57 | this.validation = Validation.for(this);
58 | this.serialize = Serialization.serialize;
59 | }
60 |
61 | initializeInstance(instance, { attributes }) {
62 | return this.initialization.initialize(instance, { attributes });
63 | }
64 |
65 | hasDynamicTypeFor(typeIdentifier) {
66 | return Boolean(this.options.dynamics[typeIdentifier]);
67 | }
68 |
69 | dynamicTypeFor(typeIdentifier) {
70 | return this.options.dynamics[typeIdentifier]();
71 | }
72 |
73 | validateAttributes(attributes) {
74 | if (attributes[SCHEMA]) {
75 | attributes = attributes.toJSON();
76 | }
77 |
78 | return this.validation.validate(attributes);
79 | }
80 |
81 | validateInstance(instance) {
82 | const attributes = instance.toJSON();
83 |
84 | return this.validation.validate(attributes);
85 | }
86 |
87 | coerce(newAttributes) {
88 | const attributes = Object.create(null);
89 |
90 | for (const attributeDefinition of this.attributeDefinitions) {
91 | const { name } = attributeDefinition;
92 | const value = newAttributes[name];
93 | attributes[name] = attributeDefinition.coerce(value);
94 | }
95 |
96 | return attributes;
97 | }
98 | }
99 |
100 | const defaultOptions = {
101 | coercion: true,
102 | };
103 |
104 | const applyDefaultOptions = (options) => ({
105 | ...defaultOptions,
106 | ...options,
107 | });
108 |
109 | module.exports = Schema;
110 |
--------------------------------------------------------------------------------
/packages/structure/src/serialization/index.js:
--------------------------------------------------------------------------------
1 | const { SCHEMA } = require('../symbols');
2 |
3 | function serialize(structure) {
4 | if (structure == null) {
5 | return structure;
6 | }
7 |
8 | return serializeStructure(structure);
9 | }
10 |
11 | function serializeStructure(structure) {
12 | const schema = structure[SCHEMA];
13 |
14 | const serializedStructure = Object.create(null);
15 |
16 | for (let attributeDefinition of schema.attributeDefinitions) {
17 | let attributeValue = structure[attributeDefinition.name];
18 |
19 | if (attributeDefinition.shouldSerialize(attributeValue)) {
20 | serializedStructure[attributeDefinition.name] = serializeAttribute(
21 | attributeValue,
22 | attributeDefinition
23 | );
24 | }
25 | }
26 |
27 | return serializedStructure;
28 | }
29 |
30 | function serializeAttribute(attributeValue, attributeDefinition) {
31 | if (attributeDefinition.itemsAreStructures) {
32 | return attributeValue.map(serialize);
33 | }
34 |
35 | if (attributeDefinition.isNestedSchema) {
36 | return serialize(attributeValue);
37 | }
38 |
39 | return attributeValue;
40 | }
41 |
42 | exports.serialize = serialize;
43 |
--------------------------------------------------------------------------------
/packages/structure/src/strictMode/index.js:
--------------------------------------------------------------------------------
1 | const Errors = require('../errors');
2 | const DefaultValidationError = require('../errors/DefaultValidationError');
3 |
4 | exports.for = function strictModeFor(schema, StructureClass) {
5 | const StructureValidationError =
6 | schema.options.strictValidationErrorClass || DefaultValidationError;
7 |
8 | return {
9 | buildStrict(...constructorArgs) {
10 | const instance = new StructureClass(...constructorArgs);
11 |
12 | const { valid, errors } = instance.validate();
13 |
14 | if (!valid) {
15 | throw Errors.invalidAttributes(errors, StructureValidationError);
16 | }
17 |
18 | return instance;
19 | },
20 | };
21 | };
22 |
--------------------------------------------------------------------------------
/packages/structure/src/symbols.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | SCHEMA: Symbol('schema'),
3 | ATTRIBUTES: Symbol('attributes'),
4 | DEFAULT_ACCESSOR: Symbol('defaultAccessor'),
5 | };
6 |
--------------------------------------------------------------------------------
/packages/structure/src/validation/forAttribute.js:
--------------------------------------------------------------------------------
1 | const validations = [
2 | require('./validations/string'),
3 | require('./validations/number'),
4 | require('./validations/boolean'),
5 | require('./validations/date'),
6 | ];
7 |
8 | const NestedValidation = require('./validations/nested');
9 | const arrayValidation = require('./validations/array');
10 |
11 | module.exports = function validationForAttribute(attributeDefinition) {
12 | if (attributeDefinition.isArrayType) {
13 | return arrayValidation(attributeDefinition);
14 | }
15 |
16 | const validation = validations.find((v) => v.type === attributeDefinition.options.type);
17 |
18 | if (!validation) {
19 | return NestedValidation.forType(attributeDefinition);
20 | }
21 |
22 | return validation.createJoiSchema(attributeDefinition);
23 | };
24 |
--------------------------------------------------------------------------------
/packages/structure/src/validation/forSchema.js:
--------------------------------------------------------------------------------
1 | const joi = require('@hapi/joi');
2 | const NestedValidation = require('./validations/nested');
3 |
4 | const validatorOptions = {
5 | abortEarly: false,
6 | convert: false,
7 | allowUnknown: false,
8 | };
9 |
10 | module.exports = function validationForSchema(schema) {
11 | const schemaValidation = schema.attributeDefinitions.reduce(
12 | (schemaValidation, attributeDefinition) => ({
13 | ...schemaValidation,
14 | [attributeDefinition.name]: attributeDefinition.validation,
15 | }),
16 | {}
17 | );
18 |
19 | const joiValidation = joi
20 | .object()
21 | .keys(schemaValidation)
22 | .id(schema.identifier);
23 |
24 | return {
25 | joiValidation,
26 | validate(data) {
27 | const validationWithDynamicLinks = NestedValidation.resolveDynamicLinks({
28 | schema,
29 | joiValidation,
30 | });
31 |
32 | const { error } = validationWithDynamicLinks.validate(data, validatorOptions);
33 |
34 | if (error) {
35 | const errors = error.details.map(mapDetail);
36 |
37 | return {
38 | valid: false,
39 | errors,
40 | };
41 | }
42 |
43 | return { valid: true };
44 | },
45 | };
46 | };
47 |
48 | const mapDetail = ({ message, path }) => ({ message, path });
49 |
--------------------------------------------------------------------------------
/packages/structure/src/validation/index.js:
--------------------------------------------------------------------------------
1 | exports.for = require('./forSchema.js');
2 | exports.forAttribute = require('./forAttribute');
3 |
--------------------------------------------------------------------------------
/packages/structure/src/validation/validations/array.js:
--------------------------------------------------------------------------------
1 | const joi = require('@hapi/joi');
2 | const { mapToJoi } = require('./utils');
3 |
4 | const joiMappings = [
5 | ['minLength', 'min', true],
6 | ['maxLength', 'max', true],
7 | ['exactLength', 'length', true],
8 | ['unique', 'unique'],
9 | ];
10 |
11 | module.exports = function arrayValidation(attributeDefinition) {
12 | let joiSchema = joi.array().items(attributeDefinition.itemTypeDefinition.validation);
13 |
14 | const { sparse } = attributeDefinition.options;
15 |
16 | const canBeSparse = sparse === undefined || sparse;
17 |
18 | joiSchema = joiSchema.sparse(canBeSparse);
19 |
20 | joiSchema = mapToJoi(attributeDefinition, {
21 | initial: joiSchema,
22 | mappings: joiMappings,
23 | });
24 |
25 | return joiSchema;
26 | };
27 |
--------------------------------------------------------------------------------
/packages/structure/src/validation/validations/boolean.js:
--------------------------------------------------------------------------------
1 | const joi = require('@hapi/joi');
2 | const { mapToJoi, equalOption } = require('./utils');
3 |
4 | module.exports = {
5 | type: Boolean,
6 | joiMappings: [],
7 | createJoiSchema(attributeDefinition) {
8 | let joiSchema = equalOption(attributeDefinition, { initial: joi.boolean() });
9 |
10 | return mapToJoi(attributeDefinition, {
11 | initial: joiSchema,
12 | mappings: this.joiMappings,
13 | });
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/packages/structure/src/validation/validations/date.js:
--------------------------------------------------------------------------------
1 | const joi = require('@hapi/joi');
2 | const { mapToJoi, mapToJoiWithReference, equalOption } = require('./utils');
3 |
4 | module.exports = {
5 | type: Date,
6 | joiMappings: [],
7 | valueOrRefOptions: [['min', 'min'], ['max', 'max']],
8 | createJoiSchema(attributeDefinition) {
9 | let joiSchema = mapToJoiWithReference(attributeDefinition, {
10 | initial: joi.date(),
11 | mappings: this.valueOrRefOptions,
12 | });
13 |
14 | joiSchema = equalOption(attributeDefinition, { initial: joiSchema });
15 |
16 | return mapToJoi(attributeDefinition, {
17 | initial: joiSchema,
18 | mappings: this.joiMappings,
19 | });
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/packages/structure/src/validation/validations/nested.js:
--------------------------------------------------------------------------------
1 | const joi = require('@hapi/joi');
2 | const { SCHEMA } = require('../../symbols');
3 | const { requiredOption } = require('./utils');
4 |
5 | exports.forType = function nestedValidationForType(attributeDefinition) {
6 | if (attributeDefinition.hasDynamicType) {
7 | return validationToDynamicType(attributeDefinition);
8 | }
9 |
10 | const typeSchema = attributeDefinition.resolveType()[SCHEMA];
11 | let joiSchema = getNestedValidations(typeSchema);
12 |
13 | joiSchema = requiredOption(attributeDefinition, {
14 | initial: joiSchema,
15 | });
16 |
17 | return joiSchema;
18 | };
19 |
20 | function validationToDynamicType(attributeDefinition) {
21 | let joiSchema = joi.link(`#${attributeDefinition.options.type}`);
22 |
23 | joiSchema = requiredOption(attributeDefinition, {
24 | initial: joiSchema,
25 | });
26 |
27 | return joiSchema;
28 | }
29 |
30 | function getNestedValidations(typeSchema) {
31 | let joiSchema = joi.object();
32 |
33 | if (typeSchema) {
34 | const nestedValidations = typeSchema.attributeDefinitions.reduce(
35 | (validations, attributeDefinition) => ({
36 | ...validations,
37 | [attributeDefinition.name]: attributeDefinition.validation,
38 | }),
39 | {}
40 | );
41 |
42 | joiSchema = joiSchema.keys(nestedValidations);
43 | }
44 |
45 | return joiSchema;
46 | }
47 |
48 | const resolveDynamicLinks = function resolveDynamicLinks({ schema, joiValidation }) {
49 | return schema.attributeDefinitions.reduce((joiValidation, attributeDefinition) => {
50 | if (!attributeDefinition.hasDynamicType) {
51 | return joiValidation;
52 | }
53 |
54 | const type = attributeDefinition.resolveType();
55 | const nestedSchema = type[SCHEMA];
56 |
57 | // warning: uses Joi internals
58 | // https://github.com/hapijs/joi/blob/v16.1.8/lib/types/any.js#L72 ⤵
59 | // https://github.com/hapijs/joi/blob/v16.1.8/lib/base.js#L699 ⤵
60 | // https://github.com/hapijs/joi/blob/v16.1.8/lib/modify.js#L149
61 | if (!nestedSchema || joiValidation._ids._get(nestedSchema.identifier)) {
62 | return joiValidation;
63 | }
64 |
65 | const attributeValidation = nestedSchema.validation;
66 |
67 | const sharedValidation = joiValidation.shared(attributeValidation.joiValidation);
68 |
69 | return resolveDynamicLinks({
70 | schema: nestedSchema,
71 | joiValidation: sharedValidation,
72 | });
73 | }, joiValidation);
74 | };
75 |
76 | exports.resolveDynamicLinks = resolveDynamicLinks;
77 |
--------------------------------------------------------------------------------
/packages/structure/src/validation/validations/number.js:
--------------------------------------------------------------------------------
1 | const joi = require('@hapi/joi');
2 | const { mapToJoi, mapToJoiWithReference, equalOption } = require('./utils');
3 |
4 | module.exports = {
5 | type: Number,
6 | joiMappings: [
7 | ['integer', 'integer'],
8 | ['precision', 'precision', true],
9 | ['multiple', 'multiple', true],
10 | ['positive', 'positive', true],
11 | ['negative', 'negative', true],
12 | ],
13 | valueOrRefOptions: [['min', 'min'], ['greater', 'greater'], ['max', 'max'], ['less', 'less']],
14 | createJoiSchema(attributeDefinition) {
15 | let joiSchema = mapToJoiWithReference(attributeDefinition, {
16 | initial: joi.number(),
17 | mappings: this.valueOrRefOptions,
18 | });
19 |
20 | joiSchema = equalOption(attributeDefinition, { initial: joiSchema });
21 |
22 | return mapToJoi(attributeDefinition, {
23 | initial: joiSchema,
24 | mappings: this.joiMappings,
25 | });
26 | },
27 | };
28 |
--------------------------------------------------------------------------------
/packages/structure/src/validation/validations/string.js:
--------------------------------------------------------------------------------
1 | const joi = require('@hapi/joi');
2 | const { isPlainObject } = require('lodash');
3 | const { mapToJoi, equalOption } = require('./utils');
4 |
5 | module.exports = {
6 | type: String,
7 | joiMappings: [
8 | ['minLength', 'min', true],
9 | ['maxLength', 'max', true],
10 | ['exactLength', 'length', true],
11 | ['regex', 'regex', true],
12 | ['alphanumeric', 'alphanum'],
13 | ['lowerCase', 'lowercase'],
14 | ['upperCase', 'uppercase'],
15 | ['email', 'email'],
16 | ['guid', 'guid', isPlainObject],
17 | ],
18 | createJoiSchema(attributeDefinition) {
19 | let joiSchema = joi.string();
20 |
21 | if (attributeDefinition.options.empty) {
22 | joiSchema = joiSchema.allow('');
23 | }
24 |
25 | joiSchema = equalOption(attributeDefinition, { initial: joiSchema });
26 |
27 | return mapToJoi(attributeDefinition, {
28 | initial: joiSchema,
29 | mappings: this.joiMappings,
30 | });
31 | },
32 | };
33 |
--------------------------------------------------------------------------------
/packages/structure/src/validation/validations/utils.js:
--------------------------------------------------------------------------------
1 | const joi = require('@hapi/joi');
2 | const { isPlainObject, isFunction } = require('lodash');
3 |
4 | exports.mapToJoi = function mapToJoi(attributeDefinition, { initial, mappings }) {
5 | let joiSchema = mappings.reduce((joiSchema, [optionName, joiMethod, passValueToJoi]) => {
6 | const optionValue = attributeDefinition.options[optionName];
7 |
8 | if (optionValue === undefined) {
9 | return joiSchema;
10 | }
11 |
12 | if (shouldPassValueToJoi(passValueToJoi, optionValue)) {
13 | return joiSchema[joiMethod](optionValue);
14 | }
15 |
16 | return joiSchema[joiMethod]();
17 | }, initial);
18 |
19 | joiSchema = requiredOption(attributeDefinition, { initial: joiSchema });
20 |
21 | return joiSchema;
22 | };
23 |
24 | function shouldPassValueToJoi(passValueToJoi, optionValue) {
25 | return passValueToJoi && (!isFunction(passValueToJoi) || passValueToJoi(optionValue));
26 | }
27 |
28 | function mapValueOrReference(valueOrReference) {
29 | if (isPlainObject(valueOrReference)) {
30 | return joi.ref(valueOrReference.attr);
31 | }
32 |
33 | return valueOrReference;
34 | }
35 |
36 | exports.mapToJoiWithReference = function mapToJoiWithReference(
37 | attributeDefinition,
38 | { initial, mappings }
39 | ) {
40 | return mappings.reduce((joiSchema, [optionName, joiMethod]) => {
41 | let optionValue = attributeDefinition.options[optionName];
42 |
43 | if (optionValue === undefined) {
44 | return joiSchema;
45 | }
46 |
47 | optionValue = mapValueOrReference(optionValue);
48 |
49 | return joiSchema[joiMethod](optionValue);
50 | }, initial);
51 | };
52 |
53 | exports.equalOption = function equalOption(attributeDefinition, { initial }) {
54 | let possibilities = attributeDefinition.options.equal;
55 |
56 | if (possibilities === undefined) {
57 | return initial;
58 | }
59 |
60 | if (!Array.isArray(possibilities)) {
61 | possibilities = [possibilities];
62 | }
63 |
64 | possibilities = possibilities.map(mapValueOrReference);
65 |
66 | return initial.equal(...possibilities);
67 | };
68 |
69 | function requiredOption(attributeDefinition, { initial }) {
70 | if (attributeDefinition.options.nullable) {
71 | initial = initial.allow(null);
72 | }
73 |
74 | if (attributeDefinition.options.required) {
75 | initial = initial.required();
76 | }
77 |
78 | return initial;
79 | }
80 |
81 | exports.requiredOption = requiredOption;
82 |
--------------------------------------------------------------------------------
/packages/structure/test/fixtures/BooksCollection.js:
--------------------------------------------------------------------------------
1 | module.exports = class BooksCollection extends Array {};
2 |
--------------------------------------------------------------------------------
/packages/structure/test/fixtures/BrokenCircularBook.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../../src');
2 |
3 | const Book = attributes(
4 | {
5 | name: String,
6 | owner: 'User',
7 | },
8 | {
9 | dynamics: {},
10 | }
11 | )(class Book {});
12 |
13 | /* istanbul ignore next */
14 | module.exports = Book;
15 |
--------------------------------------------------------------------------------
/packages/structure/test/fixtures/CircularBook.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../../src');
2 | const UserModule = require('./CircularUser');
3 |
4 | const Book = attributes(
5 | {
6 | name: String,
7 | owner: 'User',
8 | nextBook: 'Book',
9 | pages: {
10 | type: Number,
11 | },
12 | },
13 | {
14 | dynamics: {
15 | User: () => UserModule.User,
16 | Book: () => Book,
17 | },
18 | }
19 | )(class Book {});
20 |
21 | exports.Book = Book;
22 |
--------------------------------------------------------------------------------
/packages/structure/test/fixtures/CircularBookCustomIdentifier.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../../src');
2 | const UserModule = require('./CircularUserCustomIdentifier');
3 |
4 | const Book = attributes(
5 | {
6 | name: String,
7 | owner: 'UserEntity',
8 | nextBook: 'BookEntity',
9 | pages: {
10 | type: Number,
11 | },
12 | },
13 | {
14 | identifier: 'BookEntity',
15 | dynamics: {
16 | UserEntity: () => UserModule.User,
17 | BookEntity: () => Book,
18 | },
19 | }
20 | )(class Book {});
21 |
22 | exports.Book = Book;
23 |
--------------------------------------------------------------------------------
/packages/structure/test/fixtures/CircularUser.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../../src');
2 | const BookModule = require('./CircularBook');
3 | const BooksCollection = require('./BooksCollection');
4 |
5 | const User = attributes(
6 | {
7 | name: String,
8 | friends: {
9 | type: Array,
10 | itemType: 'User',
11 | },
12 | favoriteBook: {
13 | type: 'Book',
14 | required: true,
15 | nullable: true,
16 | },
17 | books: {
18 | type: 'BooksCollection',
19 | itemType: String,
20 | },
21 | nextBook: {
22 | type: 'Book',
23 | },
24 | },
25 | {
26 | dynamics: {
27 | User: () => User,
28 | Book: () => BookModule.Book,
29 | BooksCollection: () => BooksCollection,
30 | },
31 | }
32 | )(class User {});
33 |
34 | exports.User = User;
35 |
--------------------------------------------------------------------------------
/packages/structure/test/fixtures/CircularUserCustomIdentifier.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../../src');
2 | const BookModule = require('./CircularBookCustomIdentifier');
3 |
4 | const User = attributes(
5 | {
6 | name: String,
7 | friends: {
8 | type: Array,
9 | itemType: 'UserEntity',
10 | },
11 | favoriteBook: {
12 | type: 'BookEntity',
13 | required: true,
14 | nullable: true,
15 | },
16 | },
17 | {
18 | identifier: 'UserEntity',
19 | dynamics: {
20 | UserEntity: () => User,
21 | BookEntity: () => BookModule.Book,
22 | },
23 | }
24 | )(class User {});
25 |
26 | exports.User = User;
27 |
--------------------------------------------------------------------------------
/packages/structure/test/jest.browser.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | runner: '@jest-runner/electron',
3 | testEnvironment: '@jest-runner/electron/environment',
4 | setupFilesAfterEnv: ['/support/setup.js'],
5 | moduleNameMapper: {
6 | src$: '/../distTest/structure.js',
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/packages/structure/test/jest.node.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | setupFilesAfterEnv: ['/support/setup.js'],
3 | };
4 |
--------------------------------------------------------------------------------
/packages/structure/test/support/setup.js:
--------------------------------------------------------------------------------
1 | const jestStructure = require('jest-structure');
2 |
3 | expect.extend(jestStructure);
4 |
--------------------------------------------------------------------------------
/packages/structure/test/unit/__snapshots__/instanceAndUpdate.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`instantiating a structure custom setters and getters when tries to set an attribute that does not exist fails and throws an error 1`] = `"NOPE is not an attribute of this structure"`;
4 |
--------------------------------------------------------------------------------
/packages/structure/test/unit/coercion/array.spec.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../../../src');
2 |
3 | describe('type coercion', () => {
4 | describe('Array', () => {
5 | let Seat;
6 | let User;
7 |
8 | beforeEach(() => {
9 | User = attributes({
10 | books: {
11 | type: Array,
12 | itemType: String,
13 | },
14 | })(class User {});
15 | });
16 |
17 | it('does not coerces undefined', () => {
18 | const user = new User({
19 | books: undefined,
20 | });
21 |
22 | expect(user.books).toBeUndefined();
23 | });
24 |
25 | describe('when raw value is already an array', () => {
26 | it('coerces items', () => {
27 | const user = new User({
28 | books: ['The Lord of The Rings', 1984, true],
29 | });
30 |
31 | expect(user.books).toEqual(['The Lord of The Rings', '1984', 'true']);
32 | });
33 |
34 | it('does not coerce items that are of the expected type', () => {
35 | const book = new String('A Game of Thrones');
36 |
37 | const user = new User({
38 | books: [book],
39 | });
40 |
41 | expect(user.books).toEqual([new String('A Game of Thrones')]);
42 | expect(user.books[0]).toBe(book);
43 | });
44 | });
45 |
46 | describe('when raw value is a string', () => {
47 | it('uses each character as an item', () => {
48 | const user = new User({
49 | books: 'ABC',
50 | });
51 |
52 | expect(user.books).toEqual(['A', 'B', 'C']);
53 | });
54 |
55 | it('coerces empty string to empty array', () => {
56 | const user = new User({
57 | books: '',
58 | });
59 |
60 | expect(user.books).toEqual([]);
61 | });
62 |
63 | it('does nested coercing when expected item type is not String', () => {
64 | const Library = attributes({
65 | bookIds: {
66 | type: Array,
67 | itemType: Number,
68 | },
69 | })(class Library {});
70 |
71 | const library = new Library({
72 | bookIds: '123',
73 | });
74 |
75 | expect(library.bookIds).toEqual([1, 2, 3]);
76 | });
77 | });
78 |
79 | describe('when raw value is an array-like', () => {
80 | it('loops using #length property', () => {
81 | const user = new User({
82 | books: { 0: 'Stonehenge', 1: 1984, length: 2 },
83 | });
84 |
85 | expect(user.books).toEqual(['Stonehenge', '1984']);
86 | });
87 | });
88 |
89 | describe('when raw value implements Symbol.iterator', () => {
90 | it('converts to array then uses each index', () => {
91 | const books = {
92 | *[Symbol.iterator]() {
93 | for (let i = 0; i < 3; i++) {
94 | yield i;
95 | }
96 | },
97 | };
98 |
99 | const user = new User({ books });
100 |
101 | expect(user.books).toEqual(['0', '1', '2']);
102 | });
103 | });
104 |
105 | describe('when raw value is a not iterable', () => {
106 | describe('when it is not null', () => {
107 | it('throws an error', () => {
108 | expect(() => {
109 | new User({
110 | books: 123,
111 | });
112 | }).toThrow(/^Value must be iterable or array-like\.$/);
113 | });
114 | });
115 |
116 | describe('when it is null', () => {
117 | describe('when array is nullable', () => {
118 | let User;
119 |
120 | beforeEach(() => {
121 | User = attributes({
122 | books: {
123 | type: Array,
124 | itemType: String,
125 | nullable: true,
126 | },
127 | })(class User {});
128 | });
129 |
130 | it('keeps as null', () => {
131 | const user = new User({
132 | books: null,
133 | });
134 |
135 | expect(user.books).toBeNull();
136 | });
137 | });
138 |
139 | describe('when array is not nullable', () => {
140 | let User;
141 |
142 | beforeEach(() => {
143 | User = attributes({
144 | books: {
145 | type: Array,
146 | itemType: String,
147 | nullable: false,
148 | },
149 | })(class User {});
150 | });
151 |
152 | it('throws an error', () => {
153 | expect(() => {
154 | new User({
155 | books: null,
156 | });
157 | }).toThrow(/^Value must be iterable or array-like\.$/);
158 | });
159 | });
160 | });
161 | });
162 |
163 | describe('when raw value is a single numeric array', () => {
164 | beforeEach(() => {
165 | Seat = attributes({
166 | seats: {
167 | type: Array,
168 | itemType: Number,
169 | },
170 | })(class Seat {});
171 | });
172 |
173 | it('return the correct array', () => {
174 | const seat = new Seat({
175 | seats: [1],
176 | });
177 |
178 | expect(seat.seats).toEqual([1]);
179 | });
180 | });
181 |
182 | describe('Array from dynamic type', () => {
183 | let CircularUser;
184 | let BooksCollection;
185 |
186 | beforeEach(() => {
187 | CircularUser = require('../../fixtures/CircularUser').User;
188 | BooksCollection = require('../../fixtures/BooksCollection');
189 | });
190 |
191 | it('coerces collection', () => {
192 | const user = new CircularUser({
193 | books: ['Dragons of Ether', 'The Dark Tower'],
194 | });
195 |
196 | expect(user.books).toBeInstanceOf(BooksCollection);
197 | });
198 |
199 | it('coerces items', () => {
200 | const user = new CircularUser({
201 | books: ['The Lord of The Rings', 1984, true],
202 | });
203 |
204 | expect(user.books).toEqual(['The Lord of The Rings', '1984', 'true']);
205 | });
206 |
207 | it('does not coerce items that are of the expected type', () => {
208 | const book = new String('A Game of Thrones');
209 |
210 | const user = new CircularUser({
211 | books: [book],
212 | });
213 |
214 | expect(user.books).toEqual([new String('A Game of Thrones')]);
215 | expect(user.books[0]).toBe(book);
216 | });
217 | });
218 | });
219 | });
220 |
--------------------------------------------------------------------------------
/packages/structure/test/unit/coercion/arraySubclass.spec.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../../../src');
2 |
3 | describe('type coercion', () => {
4 | describe('Array subclass', () => {
5 | let Collection;
6 | let User;
7 |
8 | beforeEach(() => {
9 | Collection = class Collection extends Array {};
10 |
11 | User = attributes({
12 | books: {
13 | type: Collection,
14 | itemType: String,
15 | },
16 | })(class User {});
17 | });
18 |
19 | it('does not coerces undefined', () => {
20 | const user = new User({
21 | books: undefined,
22 | });
23 |
24 | expect(user.books).toBeUndefined();
25 | });
26 |
27 | describe('when raw value is already an array', () => {
28 | it('coerces items', () => {
29 | const user = new User({
30 | books: ['The Lord of The Rings', 1984, true],
31 | });
32 |
33 | expect(user.books).toEqual(['The Lord of The Rings', '1984', 'true']);
34 | });
35 |
36 | it('coerces value to instance of array subclass', () => {
37 | const user = new User({
38 | books: ['The Lord of The Rings', 1984, true],
39 | });
40 |
41 | expect(user.books).toBeInstanceOf(Collection);
42 | });
43 |
44 | it('does not coerce items that are of the expected type', () => {
45 | const book = new String('A Game of Thrones');
46 |
47 | const user = new User({
48 | books: [book],
49 | });
50 |
51 | expect(user.books).toEqual([new String('A Game of Thrones')]);
52 | expect(user.books[0]).toBe(book);
53 | });
54 | });
55 |
56 | describe('when raw value is a string', () => {
57 | it('uses each character as an item', () => {
58 | const user = new User({
59 | books: 'ABC',
60 | });
61 |
62 | expect(user.books).toEqual(['A', 'B', 'C']);
63 | });
64 |
65 | it('coerces empty string to empty array', () => {
66 | const user = new User({
67 | books: '',
68 | });
69 |
70 | expect(user.books).toEqual([]);
71 | });
72 |
73 | it('does nested coercing when expected item type is not String', () => {
74 | const Library = attributes({
75 | bookIds: {
76 | type: Array,
77 | itemType: Number,
78 | },
79 | })(class Library {});
80 |
81 | const library = new Library({
82 | bookIds: '123',
83 | });
84 |
85 | expect(library.bookIds).toEqual([1, 2, 3]);
86 | });
87 |
88 | it('coerces value to instance of array subclass', () => {
89 | const user = new User({
90 | books: 'ABC',
91 | });
92 |
93 | expect(user.books).toBeInstanceOf(Collection);
94 | });
95 | });
96 |
97 | describe('when raw value is an array-like', () => {
98 | it('loops using #length property', () => {
99 | const user = new User({
100 | books: { 0: 'Stonehenge', 1: 1984, length: 2 },
101 | });
102 |
103 | expect(user.books).toEqual(['Stonehenge', '1984']);
104 | });
105 |
106 | it('coerces value to instance of array subclass', () => {
107 | const user = new User({
108 | books: { 0: 'Stonehenge', 1: 1984, length: 2 },
109 | });
110 |
111 | expect(user.books).toBeInstanceOf(Collection);
112 | });
113 | });
114 |
115 | describe('when raw value implements Symbol.iterator', () => {
116 | const books = {
117 | *[Symbol.iterator]() {
118 | for (let i = 0; i < 3; i++) {
119 | yield i;
120 | }
121 | },
122 | };
123 |
124 | it('converts to array then uses each index', () => {
125 | const user = new User({ books });
126 |
127 | expect(user.books).toEqual(['0', '1', '2']);
128 | });
129 |
130 | it('coerces value to instance of array subclass', () => {
131 | const user = new User({ books });
132 |
133 | expect(user.books).toBeInstanceOf(Collection);
134 | });
135 | });
136 |
137 | describe('when raw value is a not iterable', () => {
138 | it('throws an error', () => {
139 | expect(() => {
140 | new User({
141 | books: 123,
142 | });
143 | }).toThrow(/^Value must be iterable or array-like\.$/);
144 | });
145 | });
146 | });
147 | });
148 |
--------------------------------------------------------------------------------
/packages/structure/test/unit/coercion/boolean.spec.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../../../src');
2 |
3 | describe('type coercion', () => {
4 | describe('Boolean', () => {
5 | let User;
6 |
7 | beforeEach(() => {
8 | User = attributes({
9 | isAdmin: Boolean,
10 | hasAccepted: {
11 | type: Boolean,
12 | nullable: true,
13 | },
14 | })(class User {});
15 | });
16 |
17 | it('does not coerces undefined', () => {
18 | const user = new User({
19 | isAdmin: undefined,
20 | });
21 |
22 | expect(user.isAdmin).toBeUndefined();
23 | });
24 |
25 | it('does not coerces null when nullable', () => {
26 | const user = new User({
27 | hasAccepted: null,
28 | });
29 |
30 | expect(user.hasAccepted).toBeNull();
31 | });
32 |
33 | it('coerces string to boolean', () => {
34 | const user = new User({
35 | isAdmin: '10',
36 | });
37 |
38 | expect(user.isAdmin).toBe(true);
39 | });
40 |
41 | it('coerces empty string to false', () => {
42 | const user = new User({
43 | isAdmin: '',
44 | });
45 |
46 | expect(user.isAdmin).toBe(false);
47 | });
48 |
49 | it('coerces null to false', () => {
50 | const user = new User({
51 | isAdmin: null,
52 | });
53 |
54 | expect(user.isAdmin).toBe(false);
55 | });
56 |
57 | it('coerces positive number to true', () => {
58 | const user = new User({
59 | isAdmin: 1,
60 | });
61 |
62 | expect(user.isAdmin).toBe(true);
63 | });
64 |
65 | it('coerces negative number to true', () => {
66 | const user = new User({
67 | isAdmin: -1,
68 | });
69 |
70 | expect(user.isAdmin).toBe(true);
71 | });
72 |
73 | it('coerces zero to false', () => {
74 | const user = new User({
75 | isAdmin: 0,
76 | });
77 |
78 | expect(user.isAdmin).toBe(false);
79 | });
80 |
81 | it('coerces date to true', () => {
82 | const date = new Date();
83 |
84 | const user = new User({
85 | isAdmin: date,
86 | });
87 |
88 | expect(user.isAdmin).toBe(true);
89 | });
90 |
91 | it('coerces object to true', () => {
92 | const user = new User({
93 | isAdmin: {},
94 | });
95 |
96 | expect(user.isAdmin).toBe(true);
97 | });
98 | });
99 | });
100 |
--------------------------------------------------------------------------------
/packages/structure/test/unit/coercion/coercion.spec.js:
--------------------------------------------------------------------------------
1 | const Coercion = require('../../../src/coercion/coercion');
2 | const CoercionNumber = require('../../../src/coercion/coercions/number');
3 | const CoercionDate = require('../../../src/coercion/coercions/date');
4 |
5 | describe('Coercion', () => {
6 | describe('.create', () => {
7 | let value, coercion, attributeDefinition;
8 |
9 | describe('when value is undefined', () => {
10 | beforeEach(() => {
11 | value = undefined;
12 | coercion = null;
13 | attributeDefinition = null;
14 | });
15 |
16 | it('returns undefined', () => {
17 | const executionResponse = Coercion.create(coercion, attributeDefinition).coerce(value);
18 |
19 | expect(executionResponse).toBeUndefined();
20 | });
21 | });
22 |
23 | describe('when value is null', () => {
24 | beforeEach(() => (value = null));
25 |
26 | describe('and attribute is nullable', () => {
27 | beforeEach(() => {
28 | coercion = CoercionNumber;
29 | attributeDefinition = { options: { nullable: true } };
30 | });
31 |
32 | it('returns null', () => {
33 | const executionResponse = Coercion.create(coercion, attributeDefinition).coerce(value);
34 |
35 | expect(executionResponse).toBeNull();
36 | });
37 | });
38 |
39 | describe('and attribute is not nullable', () => {
40 | beforeEach(() => {
41 | coercion = CoercionNumber;
42 | attributeDefinition = { options: { nullable: false } };
43 | });
44 |
45 | it('returns default value present on Coercion object', () => {
46 | const executionResponse = Coercion.create(coercion, attributeDefinition).coerce(value);
47 |
48 | expect(executionResponse).toBe(0);
49 | });
50 | });
51 |
52 | describe('and default attribute is a dynamic value', () => {
53 | beforeEach(() => {
54 | coercion = CoercionDate;
55 | attributeDefinition = { options: { nullable: false } };
56 | });
57 |
58 | it('returns default value present on Coercion object', () => {
59 | const executionResponse = Coercion.create(coercion, attributeDefinition).coerce(value);
60 |
61 | expect(executionResponse).toEqual(new Date('1970-01-01T00:00:00Z'));
62 | });
63 |
64 | it('creates a new object instance for default value', () => {
65 | const executionResponse = Coercion.create(coercion, attributeDefinition).coerce(value);
66 |
67 | expect(executionResponse).not.toBe(coercion.nullValue());
68 | });
69 | });
70 | });
71 |
72 | describe('when value is already coerced to correct type', () => {
73 | let spy;
74 |
75 | beforeEach(() => {
76 | value = 42;
77 | coercion = CoercionNumber;
78 | attributeDefinition = null;
79 |
80 | spy = jest.spyOn(CoercionNumber, 'coerce');
81 | });
82 |
83 | afterEach(() => spy.mockRestore());
84 |
85 | it('returns default value present on Coercion object', () => {
86 | const executionResponse = Coercion.create(coercion, attributeDefinition).coerce(value);
87 |
88 | expect(executionResponse).toBe(42);
89 | });
90 |
91 | it('does not invoke #coerce function', () => {
92 | Coercion.create(value, coercion, attributeDefinition);
93 |
94 | expect(spy).not.toHaveBeenCalled();
95 | });
96 | });
97 |
98 | describe('when value is not coerced to correct type', () => {
99 | beforeEach(() => {
100 | value = '1008';
101 | coercion = CoercionNumber;
102 | attributeDefinition = null;
103 | });
104 |
105 | it('returns default value present on Coercion object', () => {
106 | const executionResponse = Coercion.create(coercion, attributeDefinition).coerce(value);
107 |
108 | expect(executionResponse).toBe(1008);
109 | });
110 | });
111 | });
112 | });
113 |
--------------------------------------------------------------------------------
/packages/structure/test/unit/coercion/date.spec.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../../../src');
2 |
3 | describe('type coercion', () => {
4 | describe('Date', () => {
5 | let User;
6 |
7 | beforeEach(() => {
8 | User = attributes({
9 | birth: Date,
10 | death: {
11 | type: Date,
12 | nullable: true,
13 | },
14 | })(class User {});
15 | });
16 |
17 | it('does not coerce if value is already a date', () => {
18 | const birth = new Date();
19 |
20 | const user = new User({ birth });
21 |
22 | expect(user.birth).not.toEqual(new Date(birth.toString()));
23 | expect(user.birth).toBe(birth);
24 | });
25 |
26 | it('does not coerces undefined', () => {
27 | const user = new User({
28 | birth: undefined,
29 | });
30 |
31 | expect(user.birth).toBeUndefined();
32 | });
33 |
34 | it('does not coerces null when nullable', () => {
35 | const user = new User({
36 | death: null,
37 | });
38 |
39 | expect(user.death).toBeNull();
40 | });
41 |
42 | it('coerces string to date', () => {
43 | const user = new User({
44 | birth: 'Feb 3, 1892',
45 | });
46 |
47 | expect(user.birth).toEqual(new Date('Feb 3, 1892'));
48 | });
49 |
50 | it('coerces null to first date on Unix time', () => {
51 | const user = new User({
52 | birth: null,
53 | });
54 |
55 | expect(user.birth).toEqual(new Date('1970-01-01T00:00:00Z'));
56 | });
57 | });
58 | });
59 |
--------------------------------------------------------------------------------
/packages/structure/test/unit/coercion/number.spec.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../../../src');
2 |
3 | describe('type coercion', () => {
4 | describe('Number', () => {
5 | let User;
6 |
7 | beforeEach(() => {
8 | User = attributes({
9 | age: Number,
10 | earnings: {
11 | type: Number,
12 | nullable: true,
13 | },
14 | })(class User {});
15 | });
16 |
17 | it('does not coerce if value is already a number', () => {
18 | const age = new Number(42);
19 |
20 | const user = new User({ age });
21 |
22 | expect(user.age).not.toBe(42);
23 | expect(user.age).not.toBe(new Number(42));
24 | expect(user.age).toBe(age);
25 | });
26 |
27 | it('does not coerces undefined', () => {
28 | const user = new User({
29 | age: undefined,
30 | });
31 |
32 | expect(user.age).toBeUndefined();
33 | });
34 |
35 | it('does not coerce null when nullable', () => {
36 | const user = new User({
37 | earnings: null,
38 | });
39 |
40 | expect(user.earnings).toBeNull();
41 | });
42 |
43 | it('coerces string to number', () => {
44 | const user = new User({
45 | age: '10',
46 | });
47 |
48 | expect(user.age).toBe(10);
49 | });
50 |
51 | it('coerces null to zero', () => {
52 | const user = new User({
53 | age: null,
54 | });
55 |
56 | expect(user.age).toBe(0);
57 | });
58 |
59 | it('coerces true to one', () => {
60 | const user = new User({
61 | age: true,
62 | });
63 |
64 | expect(user.age).toBe(1);
65 | });
66 |
67 | it('coerces false to zero', () => {
68 | const user = new User({
69 | age: false,
70 | });
71 |
72 | expect(user.age).toBe(0);
73 | });
74 |
75 | it('coerces date to number', () => {
76 | const date = new Date();
77 |
78 | const user = new User({
79 | age: date,
80 | });
81 |
82 | expect(user.age).toBe(date.valueOf());
83 | });
84 |
85 | describe('coercing an object to number', () => {
86 | describe('when the object does not implement #valueOf()', () => {
87 | it('coerces object to NaN', () => {
88 | const objectWithoutValueOf = { data: 42 };
89 |
90 | const user = new User({
91 | age: objectWithoutValueOf,
92 | });
93 |
94 | expect(Number.isNaN(user.age)).toBe(true);
95 | });
96 | });
97 |
98 | describe('when the object implements #valueOf()', () => {
99 | it('coerces object to value returned by #valueOf()', () => {
100 | const objectWithValueOf = {
101 | data: '42',
102 | valueOf() {
103 | return this.data;
104 | },
105 | };
106 |
107 | const user = new User({
108 | age: objectWithValueOf,
109 | });
110 |
111 | expect(user.age).toBe(42);
112 | });
113 | });
114 | });
115 | });
116 | });
117 |
--------------------------------------------------------------------------------
/packages/structure/test/unit/coercion/pojo.spec.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../../../src');
2 |
3 | describe('type coercion', () => {
4 | describe('POJO class', () => {
5 | let User;
6 | let Location;
7 |
8 | beforeEach(() => {
9 | Location = class Location {
10 | constructor({ x, y }) {
11 | this.x = x;
12 | this.y = y;
13 | }
14 | };
15 |
16 | User = attributes({
17 | location: Location,
18 | })(class User {});
19 | });
20 |
21 | it('does not coerce if raw value is an instance of class', () => {
22 | const location = new Location({ x: 1, y: 2 });
23 |
24 | const user = new User({ location });
25 |
26 | expect(user.location).toBe(location);
27 | });
28 |
29 | it('instantiates class with raw value', () => {
30 | const user = new User({
31 | location: { x: 1, y: 2 },
32 | });
33 |
34 | expect(user.location).toBeInstanceOf(Location);
35 | expect(user.location.x).toBe(1);
36 | expect(user.location.y).toBe(2);
37 | });
38 |
39 | it('does not coerce undefined', () => {
40 | const user = new User({
41 | location: undefined,
42 | });
43 |
44 | expect(user.location).toBeUndefined();
45 | });
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/packages/structure/test/unit/coercion/string.spec.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../../../src');
2 |
3 | describe('type coercion', () => {
4 | describe('String', () => {
5 | let User;
6 |
7 | beforeEach(() => {
8 | User = attributes({
9 | name: String,
10 | fatherName: {
11 | type: String,
12 | nullable: true,
13 | },
14 | })(class User {});
15 | });
16 |
17 | describe('when not nullable', () => {
18 | it('does not coerce if value is already a string', () => {
19 | const name = new String('Some name');
20 |
21 | const user = new User({ name });
22 |
23 | expect(user.name).not.toBe('Some name');
24 | expect(user.name).not.toBe(new String('Some name'));
25 | expect(user.name).toBe(name);
26 | });
27 |
28 | it('does not coerces undefined', () => {
29 | const user = new User({
30 | name: undefined,
31 | });
32 |
33 | expect(user.name).toBeUndefined();
34 | });
35 |
36 | it('coerces integer to string', () => {
37 | const user = new User({
38 | name: 10,
39 | });
40 |
41 | expect(user.name).toEqual('10');
42 | });
43 |
44 | it('coerces float to string', () => {
45 | const user = new User({
46 | name: 10.42,
47 | });
48 |
49 | expect(user.name).toEqual('10.42');
50 | });
51 |
52 | it('coerces null to empty string', () => {
53 | const user = new User({
54 | name: null,
55 | });
56 |
57 | expect(user.name).toBe('');
58 | });
59 |
60 | it('coerces boolean to string', () => {
61 | const user = new User({
62 | name: false,
63 | });
64 |
65 | expect(user.name).toEqual('false');
66 | });
67 |
68 | it('coerces date to string', () => {
69 | const date = new Date();
70 |
71 | const user = new User({
72 | name: date,
73 | });
74 |
75 | expect(user.name).toEqual(date.toString());
76 | });
77 | });
78 |
79 | describe('when nullable', () => {
80 | it('does not coerce if value is already a string', () => {
81 | const fatherName = new String('Some name');
82 |
83 | const user = new User({ fatherName });
84 |
85 | expect(user.fatherName).not.toBe('Some name');
86 | expect(user.fatherName).not.toBe(new String('Some name'));
87 | expect(user.fatherName).toBe(fatherName);
88 | });
89 |
90 | it('does not coerces undefined', () => {
91 | const user = new User({
92 | fatherName: undefined,
93 | });
94 |
95 | expect(user.fatherName).toBeUndefined();
96 | });
97 |
98 | it('does not coerces null', () => {
99 | const user = new User({
100 | fatherName: null,
101 | });
102 |
103 | expect(user.fatherName).toBeNull();
104 | });
105 |
106 | it('coerces integer to string', () => {
107 | const user = new User({
108 | fatherName: 10,
109 | });
110 |
111 | expect(user.fatherName).toEqual('10');
112 | });
113 |
114 | it('coerces float to string', () => {
115 | const user = new User({
116 | fatherName: 10.42,
117 | });
118 |
119 | expect(user.fatherName).toEqual('10.42');
120 | });
121 |
122 | it('coerces boolean to string', () => {
123 | const user = new User({
124 | fatherName: false,
125 | });
126 |
127 | expect(user.fatherName).toEqual('false');
128 | });
129 |
130 | it('coerces date to string', () => {
131 | const date = new Date();
132 |
133 | const user = new User({
134 | fatherName: date,
135 | });
136 |
137 | expect(user.fatherName).toEqual(date.toString());
138 | });
139 | });
140 |
141 | describe('coercing an object to string', () => {
142 | describe('when the object does not implement #toString()', () => {
143 | it('coerces object to object tag string', () => {
144 | const objectWithoutToString = { data: 42 };
145 |
146 | const user = new User({
147 | name: objectWithoutToString,
148 | });
149 |
150 | expect(user.name).toEqual('[object Object]');
151 | });
152 | });
153 |
154 | describe('when the object implements #toString()', () => {
155 | it('coerces object to value returned from #toString()', () => {
156 | const objectWithToString = {
157 | data: 42,
158 | toString() {
159 | return this.data;
160 | },
161 | };
162 |
163 | const user = new User({
164 | name: objectWithToString,
165 | });
166 |
167 | expect(user.name).toEqual('42');
168 | });
169 | });
170 | });
171 | });
172 | });
173 |
--------------------------------------------------------------------------------
/packages/structure/test/unit/coercion/structure.spec.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../../../src');
2 |
3 | describe('type coercion', () => {
4 | describe('Structure class', () => {
5 | let Location;
6 | let User;
7 |
8 | beforeEach(() => {
9 | Location = attributes({
10 | x: Number,
11 | y: Number,
12 | })(class Location {});
13 |
14 | User = attributes({
15 | location: Location,
16 | destination: {
17 | type: Location,
18 | nullable: true,
19 | },
20 | })(class User {});
21 | });
22 |
23 | describe('when raw value is an instance of class', () => {
24 | let location, user;
25 |
26 | beforeEach(() => {
27 | location = new Location({ x: 1, y: 2 });
28 | user = new User({ location });
29 | });
30 |
31 | it('does not coerce', () => {
32 | expect(user.location).toBe(location);
33 | });
34 | });
35 |
36 | describe('when attributes is already in correct type', () => {
37 | let user;
38 |
39 | beforeEach(() => {
40 | user = new User({
41 | location: { x: 1, y: 2 },
42 | });
43 | });
44 |
45 | it('does not coerce', () => {
46 | expect(user.location).toBeInstanceOf(Location);
47 | expect(user.location.x).toBe(1);
48 | expect(user.location.y).toBe(2);
49 | });
50 | });
51 |
52 | describe('when attributes in a different type', () => {
53 | let user;
54 |
55 | beforeEach(() => {
56 | user = new User({
57 | location: { x: '1', y: '2' },
58 | });
59 | });
60 |
61 | it('coerces to correct type', () => {
62 | expect(user.location).toBeInstanceOf(Location);
63 | expect(user.location.x).toBe(1);
64 | expect(user.location.y).toBe(2);
65 | });
66 | });
67 |
68 | describe('when value is undefined', () => {
69 | let user;
70 |
71 | beforeEach(() => (user = new User({ location: undefined })));
72 |
73 | it('does not coerce', () => {
74 | expect(user.location).toBeUndefined();
75 | });
76 | });
77 |
78 | describe('when value is null', () => {
79 | let user;
80 |
81 | describe('and attribute is nullable', () => {
82 | beforeEach(() => (user = new User({ destination: null })));
83 |
84 | it('assigns null', () => {
85 | expect(user.destination).toBeNull();
86 | });
87 | });
88 |
89 | describe('and attribute is not nullable', () => {
90 | beforeEach(() => (user = new User({ location: null })));
91 |
92 | it('assigns undefined', () => {
93 | expect(user.location).toBeUndefined();
94 | });
95 | });
96 | });
97 | });
98 |
99 | describe('Structure class with dynamic attribute types', () => {
100 | let CircularUser;
101 | let CircularBook;
102 |
103 | beforeEach(() => {
104 | CircularUser = require('../../fixtures/CircularUser').User;
105 | CircularBook = require('../../fixtures/CircularBook').Book;
106 | });
107 |
108 | describe('when there are not allowed nullable attributes', () => {
109 | let userOne, userTwo;
110 |
111 | beforeEach(() => {
112 | userOne = new CircularUser({
113 | name: 'Circular user one',
114 | friends: [],
115 | favoriteBook: {
116 | name: 'The Silmarillion',
117 | owner: {},
118 | },
119 | nextBook: null,
120 | });
121 |
122 | userTwo = new CircularUser({
123 | name: 'Circular user two',
124 | friends: [userOne],
125 | nextBook: null,
126 | });
127 | });
128 |
129 | it('creates instance properly', () => {
130 | expect(userOne).toBeInstanceOf(CircularUser);
131 | expect(userOne.favoriteBook).toBeInstanceOf(CircularBook);
132 | expect(userOne.favoriteBook.owner).toBeInstanceOf(CircularUser);
133 | expect(userOne.nextBook).toBeUndefined();
134 |
135 | expect(userTwo).toBeInstanceOf(CircularUser);
136 | expect(userTwo.friends[0]).toBeInstanceOf(CircularUser);
137 | expect(userTwo.nextBook).toBeUndefined();
138 | });
139 |
140 | it('coerces when updating the value', () => {
141 | const user = new CircularUser({
142 | favoriteBook: {
143 | name: 'The Silmarillion',
144 | owner: {},
145 | },
146 | });
147 |
148 | user.favoriteBook = {
149 | name: 'The World of Ice & Fire',
150 | owner: { name: 'New name' },
151 | };
152 |
153 | expect(user.favoriteBook).toBeInstanceOf(CircularBook);
154 | expect(user.favoriteBook.name).toBe('The World of Ice & Fire');
155 | expect(user.favoriteBook.owner).toBeInstanceOf(CircularUser);
156 | expect(user.favoriteBook.owner.name).toBe('New name');
157 | });
158 | });
159 |
160 | describe('when there are allowed nullable attributes', () => {
161 | let userOne, userTwo;
162 |
163 | beforeEach(() => {
164 | userOne = new CircularUser({ friends: [], favoriteBook: null });
165 | userTwo = new CircularUser({ friends: [userOne], favoriteBook: null });
166 | });
167 |
168 | it('creates instance properly', () => {
169 | expect(userOne).toBeInstanceOf(CircularUser);
170 | expect(userOne.favoriteBook).toBeNull();
171 |
172 | expect(userTwo).toBeInstanceOf(CircularUser);
173 | expect(userTwo.favoriteBook).toBeNull();
174 | });
175 | });
176 | });
177 | });
178 |
--------------------------------------------------------------------------------
/packages/structure/test/unit/coercion/typeCoercion.spec.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../../../src');
2 |
3 | describe('type coercion', () => {
4 | let User;
5 |
6 | beforeEach(() => {
7 | User = attributes({
8 | name: String,
9 | })(class User {});
10 | });
11 |
12 | it('coerces when assigning value', () => {
13 | const user = new User();
14 |
15 | user.name = 42;
16 |
17 | expect(user.name).toBe('42');
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/packages/structure/test/unit/creatingStructureClass.spec.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../../src');
2 |
3 | describe('creating a structure class', () => {
4 | describe('structure class is passed as the second parameter', () => {
5 | describe('when structure class has a name', () => {
6 | it('throws with a message with structure class name', () => {
7 | expect(() => {
8 | attributes({}, class User {});
9 | }).toThrow(/^You passed the structure class.*\(User\)`\./);
10 | });
11 | });
12 |
13 | describe('when structure class is anonymous', () => {
14 | it('throws with a message with generic structure name', () => {
15 | // It's like this because Babel gives the name _class
16 | // to anonymous classes and do function auto-naming,
17 | // breaking browser tests
18 | const anonymousClass = (() => function() {})();
19 |
20 | expect(() => {
21 | attributes({}, anonymousClass);
22 | }).toThrow(/^You passed the structure class.*\(StructureClass\)`\./);
23 | });
24 | });
25 | });
26 |
27 | describe('using class static methods and properties', () => {
28 | let User;
29 |
30 | beforeEach(() => {
31 | class RawUser {
32 | static staticMethod() {
33 | return 'I am on a static method';
34 | }
35 | }
36 |
37 | RawUser.staticProperty = 'I am a static property';
38 |
39 | User = attributes({
40 | name: String,
41 | })(RawUser);
42 | });
43 |
44 | it('has access to static methods and properties', () => {
45 | expect(User.staticMethod()).toBe('I am on a static method');
46 | expect(User.staticProperty).toBe('I am a static property');
47 | });
48 | });
49 |
50 | describe('using default values for attributes', () => {
51 | describe('when the provided default value is a function', () => {
52 | let User;
53 |
54 | beforeEach(() => {
55 | User = attributes({
56 | age: { type: Number, default: () => 18 },
57 | })(class User {});
58 | });
59 |
60 | it('defines the attribute with the default value executing the function', () => {
61 | const user = new User();
62 |
63 | expect(user.age).toBe(18);
64 | });
65 | });
66 |
67 | describe('when the function default value uses another class attribute', () => {
68 | let User;
69 |
70 | beforeEach(() => {
71 | User = attributes({
72 | name: String,
73 | surname: String,
74 | fullname: {
75 | type: String,
76 | default: (self) => `${self.name} ${self.surname}`,
77 | },
78 | })(class User {});
79 | });
80 |
81 | it('defines the attribute with the default value executing the function', () => {
82 | const user = new User({ name: 'Jack', surname: 'Sparrow' });
83 |
84 | expect(user.fullname).toBe('Jack Sparrow');
85 | });
86 | });
87 |
88 | describe('when the provided default value is a property', () => {
89 | let User;
90 |
91 | beforeEach(() => {
92 | User = attributes({
93 | age: { type: Number, default: 18 },
94 | })(class User {});
95 | });
96 |
97 | it('defines the attribute with the default value of the property', () => {
98 | const user = new User();
99 |
100 | expect(user.age).toBe(18);
101 | });
102 | });
103 | });
104 |
105 | describe('when type is not valid', () => {
106 | describe('when shorthand notation is not a constructor nor a dynamic type name', () => {
107 | it('throws an error', () => {
108 | expect(() => {
109 | attributes({
110 | name: true,
111 | })(class User {});
112 | }).toThrow(/^Attribute type must be a constructor or the name of a dynamic type: name\.$/);
113 | });
114 | });
115 |
116 | describe('when complete notation is not a constructor nor a dynamic type name', () => {
117 | it('throws an error', () => {
118 | expect(() => {
119 | attributes({
120 | name: { type: true },
121 | })(class User {});
122 | }).toThrow(/^Attribute type must be a constructor or the name of a dynamic type: name\.$/);
123 | });
124 | });
125 | });
126 |
127 | describe('when using dynamic attribute types', () => {
128 | it('allows to use dynamic values', () => {
129 | expect(() => {
130 | require('../fixtures/CircularUser');
131 | require('../fixtures/CircularBook');
132 | }).not.toThrow();
133 | });
134 |
135 | describe('when using custom identifiers', () => {
136 | it('allows to use dynamic types', () => {
137 | expect(() => {
138 | require('../fixtures/CircularUserCustomIdentifier');
139 | require('../fixtures/CircularBookCustomIdentifier');
140 | }).not.toThrow();
141 | });
142 | });
143 |
144 | describe('when some dynamic type is missing value', () => {
145 | it('breaks', () => {
146 | expect(() => {
147 | require('../fixtures/BrokenCircularBook');
148 | }).toThrow('Missing dynamic type for attribute: owner');
149 | });
150 | });
151 | });
152 | });
153 |
--------------------------------------------------------------------------------
/packages/structure/test/unit/featureSwitches/coercion.spec.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../../../src');
2 |
3 | describe('coercion feature switch', () => {
4 | describe('when using for the whole structure', () => {
5 | describe('explicitly enabled', () => {
6 | let User;
7 |
8 | beforeEach(() => {
9 | User = attributes(
10 | {
11 | name: String,
12 | },
13 | {
14 | coercion: true,
15 | }
16 | )(class User {});
17 | });
18 |
19 | it('coerces attribute', () => {
20 | const user = new User({ name: 42 });
21 |
22 | expect(user.name).toEqual('42');
23 | });
24 | });
25 |
26 | describe('enabled by default', () => {
27 | let User;
28 |
29 | beforeEach(() => {
30 | User = attributes({
31 | name: String,
32 | })(class User {});
33 | });
34 |
35 | it('coerces attribute', () => {
36 | const user = new User({ name: 42 });
37 |
38 | expect(user.name).toEqual('42');
39 | });
40 | });
41 |
42 | describe('disabled', () => {
43 | let User;
44 |
45 | beforeEach(() => {
46 | User = attributes(
47 | {
48 | name: String,
49 | },
50 | {
51 | coercion: false,
52 | }
53 | )(class User {});
54 | });
55 |
56 | it('does not coerce attribute', () => {
57 | const user = new User({ name: 42 });
58 |
59 | expect(user.name).toEqual(42);
60 | });
61 |
62 | it('fails validation because of wrong type', () => {
63 | const user = new User({ name: 42 });
64 |
65 | expect(user).toHaveInvalidAttribute(['name'], ['"name" must be a string']);
66 | });
67 | });
68 | });
69 |
70 | describe('when using for a single attribute', () => {
71 | describe('enabled', () => {
72 | let User;
73 |
74 | beforeEach(() => {
75 | User = attributes({
76 | name: { type: String, coercion: true },
77 | age: Number,
78 | })(class User {});
79 | });
80 |
81 | it('coerces attribute', () => {
82 | const user = new User({ name: 42 });
83 |
84 | expect(user.name).toEqual('42');
85 | });
86 | });
87 |
88 | describe('disabled', () => {
89 | let User;
90 |
91 | beforeEach(() => {
92 | User = attributes({
93 | name: { type: String, coercion: false },
94 | age: Number,
95 | })(class User {});
96 | });
97 |
98 | it('does not coerce attribute', () => {
99 | const user = new User({ name: 42 });
100 |
101 | expect(user.name).toEqual(42);
102 | });
103 |
104 | it('fails validation because of wrong type', () => {
105 | const user = new User({ name: 42 });
106 |
107 | expect(user).toHaveInvalidAttribute(['name'], ['"name" must be a string']);
108 | });
109 | });
110 |
111 | describe('overrides the schema', () => {
112 | describe('schema: disabled, attribute: enabled', () => {
113 | let User;
114 |
115 | beforeEach(() => {
116 | User = attributes(
117 | {
118 | name: { type: String, coercion: true },
119 | age: Number,
120 | },
121 | {
122 | coercion: false,
123 | }
124 | )(class User {});
125 | });
126 |
127 | it('coerces the attribute but not the others', () => {
128 | const user = new User({ name: 42, age: '1' });
129 |
130 | expect(user.name).toEqual('42');
131 | expect(user.age).toEqual('1');
132 | });
133 |
134 | it('fails validation because of wrong type of other attributes', () => {
135 | const user = new User({ name: 42, age: '1' });
136 |
137 | expect(user).toHaveInvalidAttribute(['age'], ['"age" must be a number']);
138 | });
139 | });
140 |
141 | describe('schema: enabled, attribute: disabled', () => {
142 | let User;
143 |
144 | beforeEach(() => {
145 | User = attributes(
146 | {
147 | name: { type: String, coercion: false },
148 | age: Number,
149 | },
150 | {
151 | coercion: true,
152 | }
153 | )(class User {});
154 | });
155 |
156 | it('does not coerce the attribute but coerces the others', () => {
157 | const user = new User({ name: 42, age: '1' });
158 |
159 | expect(user.name).toEqual(42);
160 | expect(user.age).toEqual(1);
161 | });
162 |
163 | it('fails validation because of wrong type', () => {
164 | const user = new User({ name: 42, age: '1' });
165 |
166 | expect(user).toHaveInvalidAttribute(['name'], ['"name" must be a string']);
167 | });
168 | });
169 | });
170 | });
171 | });
172 |
--------------------------------------------------------------------------------
/packages/structure/test/unit/serialization/array.spec.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../../../src');
2 |
3 | describe('serialization', () => {
4 | describe('Array', () => {
5 | let Book;
6 | let User;
7 |
8 | beforeEach(() => {
9 | Book = attributes({
10 | name: String,
11 | })(class Book {});
12 |
13 | User = attributes({
14 | name: String,
15 | books: {
16 | type: Array,
17 | itemType: Book,
18 | },
19 | })(class User {});
20 | });
21 |
22 | describe('when all data is present', () => {
23 | it('include all data defined on schema', () => {
24 | const user = new User({
25 | name: 'Something',
26 | books: [new Book({ name: 'The Hobbit' })],
27 | });
28 |
29 | expect(user.toJSON()).toEqual({
30 | name: 'Something',
31 | books: [{ name: 'The Hobbit' }],
32 | });
33 | });
34 | });
35 |
36 | describe('when some item is undefined', () => {
37 | it('does not set a key for missing attribute', () => {
38 | const user = new User({
39 | name: 'Some name',
40 | books: [
41 | new Book({ name: 'The Silmarillion' }),
42 | undefined,
43 | new Book({ name: 'The Lord of the Rings' }),
44 | ],
45 | });
46 |
47 | const serializedUser = user.toJSON();
48 |
49 | expect(serializedUser).toEqual({
50 | name: 'Some name',
51 | books: [{ name: 'The Silmarillion' }, undefined, { name: 'The Lord of the Rings' }],
52 | });
53 | });
54 | });
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/packages/structure/test/unit/serialization/jsonStringifyCompatibility.spec.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../../../src');
2 |
3 | describe('JSON.stringify compatibility', () => {
4 | describe('when the structure is serialized with JSON.stringify', () => {
5 | it('calls .toJSON() method', () => {
6 | const Location = attributes({
7 | x: Number,
8 | y: Number,
9 | })(class Location {});
10 |
11 | const User = attributes({
12 | name: String,
13 | location: Location,
14 | })(class User {});
15 |
16 | const user = new User({
17 | name: 'Some name',
18 | location: new Location({
19 | x: 1,
20 | y: 2,
21 | }),
22 | });
23 |
24 | expect(JSON.parse(JSON.stringify(user))).toEqual(user.toJSON());
25 | });
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/packages/structure/test/unit/serialization/nestedStructure.spec.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../../../src');
2 |
3 | describe('serialization', () => {
4 | describe('Nested structure', () => {
5 | let Location;
6 | let User;
7 |
8 | beforeEach(() => {
9 | Location = attributes({
10 | longitude: Number,
11 | latitude: Number,
12 | })(class Location {});
13 |
14 | User = attributes({
15 | name: String,
16 | location: Location,
17 | })(class User {});
18 | });
19 |
20 | describe('when all data is present', () => {
21 | it('include all data defined on schema', () => {
22 | const location = new Location({
23 | longitude: 123,
24 | latitude: 321,
25 | });
26 |
27 | const user = new User({
28 | name: 'Something',
29 | location,
30 | });
31 |
32 | expect(user.toJSON()).toEqual({
33 | name: 'Something',
34 | location: {
35 | longitude: 123,
36 | latitude: 321,
37 | },
38 | });
39 | });
40 | });
41 |
42 | describe('when nested structure is missing', () => {
43 | it('does not set a key for missing structure', () => {
44 | const user = new User({
45 | name: 'Some name',
46 | });
47 |
48 | const serializedUser = user.toJSON();
49 |
50 | expect(serializedUser).toEqual({
51 | name: 'Some name',
52 | });
53 | });
54 | });
55 |
56 | describe('when some attribute on nested structure is missing', () => {
57 | it('does not set a key for missing nested attribute', () => {
58 | const location = new Location({
59 | longitude: 123,
60 | });
61 |
62 | const user = new User({
63 | name: 'Name',
64 | location,
65 | });
66 |
67 | const serializedUser = user.toJSON();
68 |
69 | expect(serializedUser).toEqual({
70 | name: 'Name',
71 | location: {
72 | longitude: 123,
73 | },
74 | });
75 | });
76 | });
77 | });
78 |
79 | describe('Nested structure with dynamic attribute types', () => {
80 | let CircularUser;
81 | let CircularBook;
82 |
83 | beforeEach(() => {
84 | CircularUser = require('../../fixtures/CircularUser').User;
85 | CircularBook = require('../../fixtures/CircularBook').Book;
86 | });
87 |
88 | describe('when all data is present', () => {
89 | it('include all data defined on schema', () => {
90 | const user = new CircularUser({
91 | name: 'Something',
92 | friends: [
93 | new CircularUser({
94 | name: 'Friend 1',
95 | favoriteBook: new CircularBook({ name: 'Book 1' }),
96 | }),
97 | new CircularUser({
98 | name: 'Friend 2',
99 | favoriteBook: new CircularBook({ name: 'Book 2' }),
100 | }),
101 | ],
102 | favoriteBook: new CircularBook({ name: 'The Book' }),
103 | });
104 |
105 | expect(user.toJSON()).toEqual({
106 | name: 'Something',
107 | friends: [
108 | {
109 | name: 'Friend 1',
110 | favoriteBook: { name: 'Book 1' },
111 | },
112 | {
113 | name: 'Friend 2',
114 | favoriteBook: { name: 'Book 2' },
115 | },
116 | ],
117 | favoriteBook: { name: 'The Book' },
118 | });
119 | });
120 | });
121 |
122 | describe('when nested structure is missing', () => {
123 | it('does not set a key for missing structure', () => {
124 | const user = new CircularUser({
125 | name: 'Something',
126 | });
127 |
128 | expect(user.toJSON()).toEqual({
129 | name: 'Something',
130 | });
131 | });
132 | });
133 |
134 | describe('when some attribute on nested structure is missing', () => {
135 | it('does not set a key for missing nested attribute', () => {
136 | const user = new CircularUser({
137 | name: 'Something',
138 | favoriteBook: new CircularBook({}),
139 | });
140 |
141 | expect(user.toJSON()).toEqual({
142 | name: 'Something',
143 | favoriteBook: {},
144 | });
145 | });
146 | });
147 | });
148 | });
149 |
--------------------------------------------------------------------------------
/packages/structure/test/unit/serialization/structure.spec.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../../../src');
2 |
3 | describe('serialization', () => {
4 | describe('Structure', () => {
5 | let User;
6 |
7 | beforeEach(() => {
8 | User = attributes({
9 | name: String,
10 | age: Number,
11 | })(class User {});
12 | });
13 |
14 | describe('when all data is present', () => {
15 | it('include all data defined on schema', () => {
16 | const user = new User({
17 | name: 'Something',
18 | age: 42,
19 | });
20 |
21 | expect(user.toJSON()).toEqual({
22 | name: 'Something',
23 | age: 42,
24 | });
25 | });
26 | });
27 |
28 | describe('when some attribute is missing', () => {
29 | it('does not set a key for missing attribute', () => {
30 | const user = new User({
31 | name: 'Some name',
32 | age: undefined,
33 | });
34 |
35 | const serializedUser = user.toJSON();
36 |
37 | expect(serializedUser).toEqual({
38 | name: 'Some name',
39 | });
40 | });
41 | });
42 |
43 | describe("when attribute's value is null", () => {
44 | let City;
45 |
46 | describe('and is not nullable', () => {
47 | beforeEach(() => {
48 | City = attributes({ name: String })(class City {});
49 | });
50 |
51 | it('serializes with default value', () => {
52 | const city = new City({
53 | name: null,
54 | });
55 |
56 | const serializedCity = city.toJSON();
57 |
58 | expect(serializedCity).toEqual({ name: '' });
59 | });
60 | });
61 |
62 | describe('and is nullable', () => {
63 | beforeEach(() => {
64 | City = attributes({
65 | name: {
66 | type: String,
67 | nullable: true,
68 | },
69 | })(class City {});
70 | });
71 |
72 | it('serializes null attributes', () => {
73 | const city = new City({ name: null });
74 |
75 | const serializedCity = city.toJSON();
76 |
77 | expect(serializedCity).toEqual({ name: null });
78 | });
79 | });
80 |
81 | describe('and is a nullable relationship', () => {
82 | let Country;
83 | let City;
84 |
85 | beforeEach(() => {
86 | Country = attributes({ name: String })(class Country {});
87 |
88 | City = attributes({
89 | country: {
90 | type: Country,
91 | nullable: true,
92 | },
93 | })(class City {});
94 | });
95 |
96 | it('serializes null attributes', () => {
97 | const city = new City({ country: null });
98 |
99 | const serializedCity = city.toJSON();
100 |
101 | expect(serializedCity.country).toBeNull();
102 | });
103 | });
104 | });
105 | });
106 | });
107 |
--------------------------------------------------------------------------------
/packages/structure/test/unit/validation/boolean.spec.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../../../src');
2 |
3 | describe('validation', () => {
4 | describe('Boolean', () => {
5 | describe('no validation', () => {
6 | let User;
7 |
8 | beforeEach(() => {
9 | User = attributes({
10 | isAdmin: {
11 | type: Boolean,
12 | },
13 | hasAccepted: {
14 | type: Boolean,
15 | nullable: true,
16 | },
17 | })(class User {});
18 | });
19 |
20 | describe('when value is present', () => {
21 | it('is valid', () => {
22 | const user = new User({
23 | isAdmin: true,
24 | });
25 |
26 | expect(user).toBeValidStructure();
27 | });
28 | });
29 |
30 | describe('when value is not present', () => {
31 | it('is valid with undefined', () => {
32 | const user = new User({
33 | isAdmin: undefined,
34 | });
35 |
36 | expect(user).toBeValidStructure();
37 | });
38 |
39 | it('is valid with null when nullable', () => {
40 | const user = new User({
41 | hasAccepted: null,
42 | });
43 |
44 | expect(user).toBeValidStructure();
45 | });
46 | });
47 | });
48 |
49 | describe('required', () => {
50 | let User;
51 |
52 | describe('when value is present', () => {
53 | beforeEach(() => {
54 | User = attributes({
55 | isAdmin: {
56 | type: Boolean,
57 | required: true,
58 | },
59 | })(class User {});
60 | });
61 |
62 | it('is valid', () => {
63 | const user = new User({
64 | isAdmin: true,
65 | });
66 |
67 | expect(user).toBeValidStructure();
68 | });
69 | });
70 |
71 | describe('when value is not present', () => {
72 | beforeEach(() => {
73 | User = attributes({
74 | isAdmin: {
75 | type: Boolean,
76 | required: true,
77 | },
78 | })(class User {});
79 | });
80 |
81 | it('is not valid and has errors set', () => {
82 | const user = new User({
83 | isAdmin: undefined,
84 | });
85 |
86 | expect(user).toHaveInvalidAttribute(['isAdmin'], ['"isAdmin" is required']);
87 | });
88 | });
89 |
90 | describe('when value is null', () => {
91 | describe('and attribute is nullable', () => {
92 | beforeEach(() => {
93 | User = attributes({
94 | isAdmin: {
95 | type: Boolean,
96 | required: true,
97 | nullable: true,
98 | },
99 | })(class User {});
100 | });
101 |
102 | it('is valid', () => {
103 | const user = new User({ isAdmin: null });
104 |
105 | expect(user).toBeValidStructure();
106 | });
107 | });
108 |
109 | describe('and attribute is not nullable', () => {
110 | beforeEach(() => {
111 | User = attributes({
112 | isAdmin: {
113 | type: Boolean,
114 | required: true,
115 | nullable: false,
116 | },
117 | })(class User {});
118 | });
119 |
120 | it('is not valid and has errors set', () => {
121 | const user = new User({ isAdmin: null });
122 |
123 | expect(user).toHaveInvalidAttribute(['isAdmin'], ['"isAdmin" is required']);
124 | });
125 | });
126 | });
127 | });
128 |
129 | describe('not required', () => {
130 | let User;
131 |
132 | beforeEach(() => {
133 | User = attributes({
134 | isAdmin: {
135 | type: Boolean,
136 | required: false,
137 | },
138 | })(class User {});
139 | });
140 |
141 | describe('when value is not present', () => {
142 | it('is valid', () => {
143 | const user = new User();
144 |
145 | expect(user).toBeValidStructure();
146 | });
147 | });
148 | });
149 |
150 | describe('equal', () => {
151 | let User;
152 |
153 | beforeEach(() => {
154 | User = attributes({
155 | isAdmin: {
156 | type: Boolean,
157 | equal: true,
158 | },
159 | })(class User {});
160 | });
161 |
162 | describe('when value is equal', () => {
163 | it('is valid', () => {
164 | const user = new User({
165 | isAdmin: true,
166 | });
167 |
168 | expect(user).toBeValidStructure();
169 | });
170 | });
171 |
172 | describe('when value is different', () => {
173 | it('is not valid and has errors set', () => {
174 | const user = new User({
175 | isAdmin: false,
176 | });
177 |
178 | expect(user).toHaveInvalidAttribute(['isAdmin'], ['"isAdmin" must be [true]']);
179 | });
180 | });
181 | });
182 | });
183 | });
184 |
--------------------------------------------------------------------------------
/packages/structure/test/unit/validation/nestedPojo.spec.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../../../src');
2 |
3 | describe('validation', () => {
4 | describe('Nested with POJO class', () => {
5 | describe('no validation', () => {
6 | let Location;
7 | let User;
8 |
9 | beforeEach(() => {
10 | Location = class Location {};
11 |
12 | User = attributes({
13 | lastLocation: {
14 | type: Location,
15 | },
16 | nextLocation: {
17 | type: Location,
18 | nullable: true,
19 | },
20 | })(class User {});
21 | });
22 |
23 | describe('when value is present', () => {
24 | it('is valid', () => {
25 | const user = new User({
26 | lastLocation: new Location(),
27 | });
28 |
29 | expect(user).toBeValidStructure();
30 | });
31 | });
32 |
33 | describe('when value is not present', () => {
34 | it('is valid with undefined', () => {
35 | const user = new User({
36 | lastLocation: undefined,
37 | });
38 |
39 | expect(user).toBeValidStructure();
40 | });
41 |
42 | it('is valid with null when nullable', () => {
43 | const user = new User({
44 | nextLocation: null,
45 | });
46 |
47 | expect(user).toBeValidStructure();
48 | });
49 | });
50 | });
51 |
52 | describe('required', () => {
53 | let Location;
54 | let User;
55 |
56 | beforeEach(() => (Location = class Location {}));
57 |
58 | describe('when value is present', () => {
59 | beforeEach(() => {
60 | User = attributes({
61 | lastLocation: {
62 | type: Location,
63 | required: true,
64 | },
65 | })(class User {});
66 | });
67 |
68 | it('is valid', () => {
69 | const user = new User({
70 | lastLocation: new Location(),
71 | });
72 |
73 | expect(user).toBeValidStructure();
74 | });
75 | });
76 |
77 | describe('when value is not present', () => {
78 | beforeEach(() => {
79 | User = attributes({
80 | lastLocation: {
81 | type: Location,
82 | required: true,
83 | },
84 | })(class User {});
85 | });
86 |
87 | it('is not valid and has errors set', () => {
88 | const user = new User({
89 | lastLocation: undefined,
90 | });
91 |
92 | expect(user).toHaveInvalidAttribute(['lastLocation'], ['"lastLocation" is required']);
93 | });
94 | });
95 |
96 | describe('when value is null', () => {
97 | describe('and attribute is nullable', () => {
98 | beforeEach(() => {
99 | User = attributes({
100 | lastLocation: {
101 | type: Location,
102 | required: true,
103 | nullable: true,
104 | },
105 | })(class User {});
106 | });
107 |
108 | it('is valid', () => {
109 | const user = new User({ lastLocation: null });
110 |
111 | expect(user).toBeValidStructure();
112 | });
113 | });
114 |
115 | describe('and attribute is not nullable', () => {
116 | beforeEach(() => {
117 | User = attributes({
118 | lastLocation: {
119 | type: Location,
120 | required: true,
121 | nullable: false,
122 | },
123 | })(class User {});
124 | });
125 |
126 | it('is not valid and has errors set', () => {
127 | const user = new User({ lastLocation: null });
128 |
129 | expect(user).toHaveInvalidAttribute(['lastLocation'], ['"lastLocation" is required']);
130 | });
131 | });
132 | });
133 | });
134 |
135 | describe('not required', () => {
136 | let Location;
137 | let User;
138 |
139 | beforeEach(() => {
140 | Location = class Location {};
141 |
142 | User = attributes({
143 | lastLocation: {
144 | type: Location,
145 | required: false,
146 | },
147 | })(class User {});
148 | });
149 |
150 | describe('when value is not present', () => {
151 | it('is valid', () => {
152 | const user = new User();
153 |
154 | expect(user).toBeValidStructure();
155 | });
156 | });
157 | });
158 | });
159 | });
160 |
--------------------------------------------------------------------------------
/packages/structure/test/unit/validation/structureSubclass.spec.js:
--------------------------------------------------------------------------------
1 | const { attributes } = require('../../../src');
2 |
3 | describe('validation', () => {
4 | describe('structure subclass', () => {
5 | let Admin;
6 | let User;
7 |
8 | beforeEach(() => {
9 | User = attributes({
10 | name: {
11 | type: String,
12 | required: true,
13 | },
14 | })(class User {});
15 |
16 | Admin = attributes({
17 | level: {
18 | type: Number,
19 | required: true,
20 | },
21 | })(class Admin extends User {});
22 | });
23 |
24 | describe('with invalid superclass schema', () => {
25 | it('is invalid', () => {
26 | const admin = new Admin({
27 | level: 3,
28 | });
29 |
30 | expect(admin).toHaveInvalidAttribute(['name'], ['"name" is required']);
31 | });
32 | });
33 |
34 | describe('with invalid subclass schema', () => {
35 | it('is invalid', () => {
36 | const admin = new Admin({
37 | name: 'The admin',
38 | });
39 |
40 | expect(admin).toHaveInvalidAttribute(['level'], ['"level" is required']);
41 | });
42 | });
43 |
44 | describe('with valid superclass and subclass schema', () => {
45 | it('is valid', () => {
46 | const admin = new Admin({
47 | name: 'The admin',
48 | level: 3,
49 | });
50 |
51 | expect(admin).toBeValidStructure();
52 | });
53 | });
54 |
55 | describe('with nullable attributes on superclass', () => {
56 | let Vehicle;
57 | let Car;
58 |
59 | describe('when nullable is true', () => {
60 | beforeEach(() => {
61 | Vehicle = attributes({
62 | name: {
63 | type: String,
64 | required: true,
65 | nullable: true,
66 | },
67 | })(class Vehicle {});
68 |
69 | Car = attributes({
70 | gearbox: String,
71 | })(class Car extends Vehicle {});
72 | });
73 |
74 | it('is valid', () => {
75 | const car = new Car({ name: null });
76 |
77 | expect(car).toBeValidStructure();
78 | });
79 | });
80 |
81 | describe('when nullable is false', () => {
82 | beforeEach(() => {
83 | Vehicle = attributes({
84 | name: {
85 | type: String,
86 | required: true,
87 | nullable: false,
88 | },
89 | })(class Vehicle {});
90 |
91 | Car = attributes({
92 | gearbox: String,
93 | })(class Car extends Vehicle {});
94 | });
95 |
96 | it('is not valid and has errors set', () => {
97 | const car = new Car({ name: null });
98 |
99 | expect(car).toHaveInvalidAttribute(['name'], ['"name" is required']);
100 | });
101 | });
102 | });
103 | });
104 | });
105 |
--------------------------------------------------------------------------------
/packages/structure/test/webpack.pretest.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpackConfig = require('../webpack.config');
3 |
4 | Object.assign(webpackConfig, {
5 | mode: 'development',
6 | devtool: 'inline-source-map',
7 | output: {
8 | ...webpackConfig.output,
9 | path: path.join(__dirname, '..', 'distTest'),
10 | },
11 | });
12 |
13 | module.exports = webpackConfig;
14 |
--------------------------------------------------------------------------------
/packages/structure/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | mode: 'production',
5 | entry: [path.join(__dirname, 'src/index.js')],
6 | output: {
7 | filename: './structure.js',
8 | library: 'Structure',
9 | libraryTarget: 'umd',
10 | umdNamedDefine: true,
11 | },
12 | externals: {
13 | '@hapi/joi': {
14 | root: 'joi',
15 | commonjs: '@hapi/joi',
16 | commonjs2: '@hapi/joi',
17 | amd: 'joi',
18 | },
19 | lodash: {
20 | root: '_',
21 | commonjs: 'lodash',
22 | commonjs2: 'lodash',
23 | amd: 'lodash',
24 | },
25 | },
26 | module: {
27 | rules: [
28 | {
29 | test: /\.js$/,
30 | exclude: /node_modules/,
31 | loader: 'babel-loader',
32 | options: {
33 | presets: ['@babel/preset-env'],
34 | plugins: [
35 | ['@babel/plugin-proposal-object-rest-spread', { loose: true, useBuiltIns: true }],
36 | ],
37 | },
38 | },
39 | ],
40 | },
41 | };
42 |
--------------------------------------------------------------------------------
/structure.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talyssonoc/structure/210dc05e3204b2d974fbd1401742b4e12de0a9f0/structure.jpg
--------------------------------------------------------------------------------