From 86bad000ce2b3e1db1778b9745842bd06ce0b913 Mon Sep 17 00:00:00 2001 From: Lawrence Date: Fri, 27 Sep 2024 17:04:54 +0100 Subject: [PATCH 1/4] chore: add this.setMinifiedName and remove Type suffix --- src/type/Model.js | 28 ++++- src/type/Type.js | 12 ++- src/type/Type.test.js | 6 +- src/type/complex/ArrayType.js | 6 +- src/type/complex/CustomType.js | 16 ++- src/type/complex/CustomType.test.js | 14 +-- src/type/resolved/ResolvedType.test.js | 20 ++-- src/type/resolved/SlugType.js | 4 + src/type/simple/BooleanType.js | 14 ++- src/type/simple/DateType.js | 24 +++-- src/type/simple/NumberType.js | 14 ++- src/type/simple/StringType.js | 14 ++- test/acceptance/Persist.test.js | 12 +-- test/fixtures/Models.js | 135 ++++++++++++++----------- 14 files changed, 192 insertions(+), 127 deletions(-) diff --git a/src/type/Model.js b/src/type/Model.js index da3daec..bdec9e9 100644 --- a/src/type/Model.js +++ b/src/type/Model.js @@ -73,11 +73,11 @@ class Model { (key, value) => { if (!simple) { if (this.constructor[key]) { - if (this.constructor[key].name.endsWith('DateType')) { + if (this.constructor[key].name.endsWith('Date')) { return new Date(value); } - if (this.constructor[key].name.endsWith('ArrayOf(Date)Type')) { + if (this.constructor[key].name.endsWith('ArrayOf(Date)')) { return value.map(d => new Date(d)); } } @@ -211,12 +211,12 @@ class Model { for (const [name, value] of Object.entries(data)) { if (this[name]?._resolved) continue; - if (this[name].name.endsWith('DateType')) { + if (this[name].name.endsWith('Date')) { model[name] = new Date(value); continue; } - if (this[name].name.endsWith('ArrayOf(Date)Type')) { + if (this[name].name.endsWith('ArrayOf(Date)')) { model[name] = data[name].map(d => new Date(d)); continue; } @@ -259,6 +259,26 @@ class Model { return false; } } + + /** + * Set the name of the Model class + * + * Use this when your model might be minified to retain consistent class names. + * + * @param {string} name + * @static + * + * @example + * export default class TestModel { + * static { + * this.string = Persist.Type.String; + * Object.defineProperty(this, 'name', {value: 'TestModel'}); + * } + * } + */ + static setMinifiedName(name) { + Object.defineProperty(this, 'name', {value: name}); + } } export default Model; diff --git a/src/type/Type.js b/src/type/Type.js index 6e82301..a0ab702 100644 --- a/src/type/Type.js +++ b/src/type/Type.js @@ -38,12 +38,12 @@ class Type { static _schema = undefined; /** - * Converts the class name to a string, removing the "Type" suffix. + * Converts the class name to a string * - * @returns {string} The name of the type without the "Type" suffix. + * @returns {string} The name of the type. */ static toString() { - return this.name?.replace(/Type$/, ''); + return this.name; } /** @@ -58,10 +58,14 @@ class Type { } // Define the class name as "Required" - Object.defineProperty(Required, 'name', {value: `Required${this.toString()}Type`}); + Object.defineProperty(Required, 'name', {value: `Required${this.toString()}`}); return Required; } + + static { + Object.defineProperty(this, 'name', {value: 'Type'}); + } } export default Type; diff --git a/src/type/Type.test.js b/src/type/Type.test.js index 9c957fa..4dee886 100644 --- a/src/type/Type.test.js +++ b/src/type/Type.test.js @@ -1,8 +1,8 @@ import Type from './Type.js'; import test from 'ava'; -test('Type has no type', t => { - t.is(Type.toString(), ''); +test('Type is of type Type', t => { + t.is(Type.toString(), 'Type'); }); test('Type is not required', t => { @@ -22,7 +22,7 @@ test('Type is not a resolved type', t => { }); test('RequiredType is of Required type', t => { - t.is(Type.required.toString(), 'Required'); + t.is(Type.required.toString(), 'RequiredType'); }); test('RequiredType is required', t => { diff --git a/src/type/complex/ArrayType.js b/src/type/complex/ArrayType.js index 9ac482c..e96ee4f 100644 --- a/src/type/complex/ArrayType.js +++ b/src/type/complex/ArrayType.js @@ -66,17 +66,17 @@ class ArrayType { * @returns {string} The string representation of the required array type. */ static toString() { - return `RequiredArrayOf(${type})`; + return `RequiredArrayOf(${type.toString()})`; } } - Object.defineProperty(Required, 'name', {value: `Required${this.toString()}Type`}); + Object.defineProperty(Required, 'name', {value: `Required${this.toString()}`}); return Required; } } - Object.defineProperty(ArrayOf, 'name', {value: `${ArrayOf.toString()}Type`}); + Object.defineProperty(ArrayOf, 'name', {value: ArrayOf.toString()}); return ArrayOf; } diff --git a/src/type/complex/CustomType.js b/src/type/complex/CustomType.js index d075fbe..ace41c1 100644 --- a/src/type/complex/CustomType.js +++ b/src/type/complex/CustomType.js @@ -35,15 +35,23 @@ class CustomType { * Represents a custom type defined by a JSON schema. */ class Custom extends Type { - /** @type {string} The data type, which is 'object' */ - static _type = 'object'; + static { + /** @type {string} The data type, which is 'object' */ + this._type = 'object'; - /** @type {Object} The JSON schema that defines the structure and validation rules */ - static _schema = schema; + /** @type {Object} The JSON schema that defines the structure and validation rules */ + this._schema = schema; + + Object.defineProperty(this, 'name', {value: 'Custom'}); + } } return Custom; } + + static { + Object.defineProperty(this, 'name', {value: 'Custom'}); + } } export default CustomType; diff --git a/src/type/complex/CustomType.test.js b/src/type/complex/CustomType.test.js index e4018f5..7333978 100644 --- a/src/type/complex/CustomType.test.js +++ b/src/type/complex/CustomType.test.js @@ -62,14 +62,10 @@ const invalidSchema = { }; test('CustomType.of(invalidSchema) throws an invalid schema error', t => { - const error = t.throws(() => { + t.throws(() => { CustomType.of(invalidSchema); - }, {instanceOf: Error}); - - t.is( - error.message, - 'schema is invalid: data/properties/string/type must be equal to one of the allowed ' + - 'values, data/properties/string/type must be array, data/properties/string/type must ' + - 'match a schema in anyOf', - ); + }, { + instanceOf: Error, + message: 'schema is invalid: data/properties/string/type must be equal to one of the allowed values, data/properties/string/type must be array, data/properties/string/type must match a schema in anyOf', + }); }); diff --git a/src/type/resolved/ResolvedType.test.js b/src/type/resolved/ResolvedType.test.js index e0a579d..3cfd0e9 100644 --- a/src/type/resolved/ResolvedType.test.js +++ b/src/type/resolved/ResolvedType.test.js @@ -6,7 +6,7 @@ class UnimplementedResolvedType extends ResolvedType { } test('UnimplementedResolvedType is of type UnimplementedResolved', t => { - t.is(UnimplementedResolvedType.toString(), 'UnimplementedResolved'); + t.is(UnimplementedResolvedType.toString(), 'UnimplementedResolvedType'); }); test('UnimplementedResolvedType.of(name) is of type UnimplementedResolvedOf', t => { @@ -30,17 +30,19 @@ test('UnimplementedResolvedType is a resolved type', t => { }); test('UnimplementedResolvedType raises a not implemented error on resolving', t => { - const error = t.throws(() => { + t.throws(() => { UnimplementedResolvedType.resolve({}); - }, {instanceOf: Error}); - - t.is(error.message, 'UnimplementedResolvedType does not implement resolve(model)'); + }, { + instanceOf: Error, + message: 'UnimplementedResolvedType does not implement resolve(model)', + }); }); test('UnimplementedResolvedType.of(propertyName) raises a not implemented error on resolving', t => { - const error = t.throws(() => { + t.throws(() => { UnimplementedResolvedType.of('name').resolve({}); - }, {instanceOf: Error}); - - t.is(error.message, 'ResolvedTypeOf does not implement resolve(model)'); + }, { + instanceOf: Error, + message: 'ResolvedTypeOf does not implement resolve(model)', + }); }); diff --git a/src/type/resolved/SlugType.js b/src/type/resolved/SlugType.js index e31418f..0ee192f 100644 --- a/src/type/resolved/SlugType.js +++ b/src/type/resolved/SlugType.js @@ -59,6 +59,10 @@ class SlugType extends ResolvedType { return SlugOf; } + + static { + Object.defineProperty(this, 'name', {value: 'Slug'}); + } } export default SlugType; diff --git a/src/type/simple/BooleanType.js b/src/type/simple/BooleanType.js index 5c838b6..80b0a04 100644 --- a/src/type/simple/BooleanType.js +++ b/src/type/simple/BooleanType.js @@ -10,11 +10,15 @@ import SimpleType from './SimpleType.js'; * @extends SimpleType */ class BooleanType extends SimpleType { - /** - * @static - * @property {string} _type - The type identifier for BooleanType, set to `'boolean'`. - */ - static _type = 'boolean'; + static { + /** + * @static + * @property {string} _type - The type identifier for BooleanType, set to `'boolean'`. + */ + this._type = 'boolean'; + + Object.defineProperty(this, 'name', {value: 'Boolean'}); + } } export default BooleanType; diff --git a/src/type/simple/DateType.js b/src/type/simple/DateType.js index 7e02afd..768aed3 100644 --- a/src/type/simple/DateType.js +++ b/src/type/simple/DateType.js @@ -10,17 +10,21 @@ import SimpleType from './SimpleType.js'; * @extends SimpleType */ class DateType extends SimpleType { - /** - * @static - * @property {string} _type - The type identifier for DateType, set to `'string'`. - */ - static _type = 'string'; + static { + /** + * @static + * @property {string} _type - The type identifier for DateType, set to `'string'`. + */ + this._type = 'string'; - /** - * @static - * @property {string} _format - The format for DateType, set to `'iso-date-time'`. - */ - static _format = 'iso-date-time'; + /** + * @static + * @property {string} _format - The format for DateType, set to `'iso-date-time'`. + */ + this._format = 'iso-date-time'; + + Object.defineProperty(this, 'name', {value: 'Date'}); + } /** * Checks if the given value is a valid date. diff --git a/src/type/simple/NumberType.js b/src/type/simple/NumberType.js index c5b6558..787ada8 100644 --- a/src/type/simple/NumberType.js +++ b/src/type/simple/NumberType.js @@ -10,11 +10,15 @@ import SimpleType from './SimpleType.js'; * @extends SimpleType */ class NumberType extends SimpleType { - /** - * @static - * @property {string} _type - The type identifier for NumberType, set to `'number'`. - */ - static _type = 'number'; + static { + /** + * @static + * @property {string} _type - The type identifier for NumberType, set to `'number'`. + */ + this._type = 'number'; + + Object.defineProperty(this, 'name', {value: 'Number'}); + } } export default NumberType; diff --git a/src/type/simple/StringType.js b/src/type/simple/StringType.js index 97e385f..8f132a1 100644 --- a/src/type/simple/StringType.js +++ b/src/type/simple/StringType.js @@ -10,11 +10,15 @@ import SimpleType from './SimpleType.js'; * @extends SimpleType */ class StringType extends SimpleType { - /** - * @static - * @property {string} _type - The type identifier for the string type. - */ - static _type = 'string'; + static { + /** + * @static + * @property {string} _type - The type identifier for the string type. + */ + this._type = 'string'; + + Object.defineProperty(this, 'name', {value: 'String'}); + } } export default StringType; diff --git a/test/acceptance/Persist.test.js b/test/acceptance/Persist.test.js index 6dbc6a5..f9a77ea 100644 --- a/test/acceptance/Persist.test.js +++ b/test/acceptance/Persist.test.js @@ -2,27 +2,27 @@ import Persist from '@acodeninja/persist'; import test from 'ava'; test('Persist contains the String Type', t => { - t.is(Persist.Type.String.name, 'StringType'); + t.is(Persist.Type.String.name, 'String'); }); test('Persist contains the Number Type', t => { - t.is(Persist.Type.Number.name, 'NumberType'); + t.is(Persist.Type.Number.name, 'Number'); }); test('Persist contains the Boolean Type', t => { - t.is(Persist.Type.Boolean.name, 'BooleanType'); + t.is(Persist.Type.Boolean.name, 'Boolean'); }); test('Persist contains the Date Type', t => { - t.is(Persist.Type.Date.name, 'DateType'); + t.is(Persist.Type.Date.name, 'Date'); }); test('Persist contains the Resolved Slug Type', t => { - t.is(Persist.Type.Resolved.Slug.name, 'SlugType'); + t.is(Persist.Type.Resolved.Slug.name, 'Slug'); }); test('Persist contains the Complex Custom Type', t => { - t.is(Persist.Type.Custom.name, 'CustomType'); + t.is(Persist.Type.Custom.name, 'Custom'); }); test('Persist contains the Model Type', t => { diff --git a/test/fixtures/Models.js b/test/fixtures/Models.js index 59eba18..332417b 100644 --- a/test/fixtures/Models.js +++ b/test/fixtures/Models.js @@ -9,8 +9,11 @@ import Type from '../../src/type/index.js'; * @property {Type.Boolean} boolean - A boolean type property. */ export class LinkedModel extends Type.Model { - static string = Type.String; - static boolean = Type.Boolean; + static { + this.setMinifiedName('LinkedModel'); + this.string = Type.String; + this.boolean = Type.Boolean; + } } /** @@ -21,7 +24,10 @@ export class LinkedModel extends Type.Model { * @property {Type.String} string - A string type property. */ export class LinkedManyModel extends Type.Model { - static string = Type.String; + static { + this.setMinifiedName('LinkedManyModel'); + this.string = Type.String; + } } /** @@ -32,7 +38,10 @@ export class LinkedManyModel extends Type.Model { * @property {MainModel} linked - A circular reference to the `MainModel`. */ export class CircularModel extends Type.Model { - static linked = () => MainModel; + static { + this.setMinifiedName('CircularModel'); + this.linked = () => MainModel; + } } /** @@ -43,7 +52,10 @@ export class CircularModel extends Type.Model { * @property {MainModel[]} linked - An array of circular references to the `MainModel`. */ export class CircularManyModel extends Type.Model { - static linked = () => Type.Array.of(MainModel); + static { + this.setMinifiedName('CircularManyModel'); + this.linked = () => Type.Array.of(MainModel); + } } /** @@ -85,61 +97,64 @@ export class CircularManyModel extends Type.Model { * @method searchProperties Returns the list of properties used in search. */ export class MainModel extends Type.Model { - static custom = Type.Custom.of({ - type: 'object', - additionalProperties: false, - properties: {test: {type: 'string'}}, - required: ['test'], - }); - static string = Type.String; - static stringSlug = Type.Resolved.Slug.of('string'); - static requiredString = Type.String.required; - static requiredStringSlug = Type.Resolved.Slug.of('requiredString'); - static number = Type.Number; - static requiredNumber = Type.Number.required; - static boolean = Type.Boolean; - static requiredBoolean = Type.Boolean.required; - static date = Type.Date; - static requiredDate = Type.Date.required; - static emptyArrayOfStrings = Type.Array.of(Type.String); - static emptyArrayOfNumbers = Type.Array.of(Type.Number); - static emptyArrayOfBooleans = Type.Array.of(Type.Boolean); - static emptyArrayOfDates = Type.Array.of(Type.Date); - static emptyArrayOfModels = Type.Array.of(LinkedManyModel); - static arrayOfString = Type.Array.of(Type.String); - static arrayOfNumber = Type.Array.of(Type.Number); - static arrayOfBoolean = Type.Array.of(Type.Boolean); - static arrayOfDate = Type.Array.of(Type.Date); - static requiredArrayOfString = Type.Array.of(Type.String).required; - static requiredArrayOfNumber = Type.Array.of(Type.Number).required; - static requiredArrayOfBoolean = Type.Array.of(Type.Boolean).required; - static requiredArrayOfDate = Type.Array.of(Type.Date).required; - static circular = CircularModel; - static circularMany = Type.Array.of(CircularManyModel); - static linked = () => LinkedModel; - static requiredLinked = LinkedModel.required; - static linkedMany = () => Type.Array.of(LinkedManyModel); + static { + this.setMinifiedName('MainModel'); + this.custom = Type.Custom.of({ + type: 'object', + additionalProperties: false, + properties: {test: {type: 'string'}}, + required: ['test'], + }); + this.string = Type.String; + this.stringSlug = Type.Resolved.Slug.of('string'); + this.requiredString = Type.String.required; + this.requiredStringSlug = Type.Resolved.Slug.of('requiredString'); + this.number = Type.Number; + this.requiredNumber = Type.Number.required; + this.boolean = Type.Boolean; + this.requiredBoolean = Type.Boolean.required; + this.date = Type.Date; + this.requiredDate = Type.Date.required; + this.emptyArrayOfStrings = Type.Array.of(Type.String); + this.emptyArrayOfNumbers = Type.Array.of(Type.Number); + this.emptyArrayOfBooleans = Type.Array.of(Type.Boolean); + this.emptyArrayOfDates = Type.Array.of(Type.Date); + this.emptyArrayOfModels = Type.Array.of(LinkedManyModel); + this.arrayOfString = Type.Array.of(Type.String); + this.arrayOfNumber = Type.Array.of(Type.Number); + this.arrayOfBoolean = Type.Array.of(Type.Boolean); + this.arrayOfDate = Type.Array.of(Type.Date); + this.requiredArrayOfString = Type.Array.of(Type.String).required; + this.requiredArrayOfNumber = Type.Array.of(Type.Number).required; + this.requiredArrayOfBoolean = Type.Array.of(Type.Boolean).required; + this.requiredArrayOfDate = Type.Array.of(Type.Date).required; + this.circular = CircularModel; + this.circularMany = Type.Array.of(CircularManyModel); + this.linked = LinkedModel; + this.requiredLinked = () => LinkedModel.required; + this.linkedMany = Type.Array.of(LinkedManyModel); - /** - * Returns the list of properties to be indexed. - * - * @returns {string[]} An array of property names to be indexed. - */ - static indexedProperties = () => [ - 'string', - 'boolean', - 'number', - 'arrayOfString', - 'stringSlug', - 'linked.string', - 'linked.boolean', - 'linkedMany.[*].string', - ]; + /** + * Returns the list of properties to be indexed. + * + * @returns {string[]} An array of property names to be indexed. + */ + this.indexedProperties = () => [ + 'string', + 'boolean', + 'number', + 'arrayOfString', + 'stringSlug', + 'linked.string', + 'linked.boolean', + 'linkedMany.[*].string', + ]; - /** - * Returns the list of properties used in search. - * - * @returns {string[]} An array of property names used for search. - */ - static searchProperties = () => ['string', 'stringSlug', 'linked.string', 'linkedMany.[*].string']; + /** + * Returns the list of properties used in search. + * + * @returns {string[]} An array of property names used for search. + */ + this.searchProperties = () => ['string', 'stringSlug', 'linked.string', 'linkedMany.[*].string']; + } } From 293c6a092acf759ff7de7456f1f4634322468f2b Mon Sep 17 00:00:00 2001 From: Lawrence Date: Sat, 28 Sep 2024 10:01:17 +0100 Subject: [PATCH 2/4] fix: allow developers to indicate the class name for minification --- ava.config.js | 6 + eslint.config.js | 8 +- src/engine/S3Engine.test.js | 4 +- src/type/resolved/SlugType.js | 2 + test/acceptance/minified.test.js | 73 ++ test/fixtures/minified/.gitignore | 2 + test/fixtures/minified/main.js | 3 + test/fixtures/minified/package-lock.json | 1229 ++++++++++++++++++++++ test/fixtures/minified/package.json | 17 + test/fixtures/minified/webpack.config.js | 17 + 10 files changed, 1358 insertions(+), 3 deletions(-) create mode 100644 test/acceptance/minified.test.js create mode 100644 test/fixtures/minified/.gitignore create mode 100644 test/fixtures/minified/main.js create mode 100644 test/fixtures/minified/package-lock.json create mode 100644 test/fixtures/minified/package.json create mode 100644 test/fixtures/minified/webpack.config.js diff --git a/ava.config.js b/ava.config.js index 1b29b23..a53c488 100644 --- a/ava.config.js +++ b/ava.config.js @@ -5,4 +5,10 @@ export default { 'exports/**/*.test.js', 'test/acceptance/**/*.test.js', ], + watchMode: { + ignoreChanges: [ + 'coverage', + 'test/fixtures/minified/*', + ], + }, }; diff --git a/eslint.config.js b/eslint.config.js index 70b3aeb..1c541ea 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,7 +2,13 @@ import globals from 'globals'; import pluginJs from '@eslint/js'; export default [ - {ignores: ['coverage/', 'node_modules/']}, + { + ignores: [ + 'coverage/', + 'node_modules/', + 'test/fixtures/minified/main.bundle.*', + ], + }, {languageOptions: {globals: globals.node}}, pluginJs.configs.recommended, { diff --git a/src/engine/S3Engine.test.js b/src/engine/S3Engine.test.js index f9f04ce..d05c649 100644 --- a/src/engine/S3Engine.test.js +++ b/src/engine/S3Engine.test.js @@ -736,7 +736,7 @@ test('S3Engine.find(MainModel, {string: "test"}) when a matching model does not bucket: 'test-bucket', prefix: 'test', client, - }).find(MainModel, {string: 'String'}); + }).find(MainModel, {string: 'test'}); assertions.calledWith(t, client.send, new GetObjectCommand({ Key: 'test/MainModel/_index.json', @@ -753,7 +753,7 @@ test('S3Engine.find(MainModel, {string: "test"}) when no index exists', async t bucket: 'test-bucket', prefix: 'test', client, - }).find(MainModel, {string: 'String'}); + }).find(MainModel, {string: 'test'}); t.deepEqual(models, []); }); diff --git a/src/type/resolved/SlugType.js b/src/type/resolved/SlugType.js index 0ee192f..0bca0d5 100644 --- a/src/type/resolved/SlugType.js +++ b/src/type/resolved/SlugType.js @@ -57,6 +57,8 @@ class SlugType extends ResolvedType { } } + Object.defineProperty(SlugOf, 'name', {value: `SlugOf(${property})`}); + return SlugOf; } diff --git a/test/acceptance/minified.test.js b/test/acceptance/minified.test.js new file mode 100644 index 0000000..75cce82 --- /dev/null +++ b/test/acceptance/minified.test.js @@ -0,0 +1,73 @@ +import {dirname, resolve} from 'node:path'; +import {fileURLToPath} from 'node:url'; +import {spawn} from 'node:child_process'; +import test from 'ava'; + +const run = (command, ...args) => + new Promise((done, failed) => { + const execution = spawn(command, args, { + cwd: resolve(dirname(fileURLToPath(import.meta.url)), '../fixtures/minified'), + env: process.env, + shell: true, + }); + + const output = []; + + execution.stdout.on('data', (data) => { + output.push(`stdout: ${data}`); + }); + + execution.stderr.on('data', (data) => { + output.push(`stderr: ${data}`); + }); + + execution.on('close', (_) => { + done(); + }); + + execution.on('error', (error) => { + console.error(output.join('\n')); + failed(error); + }); + }); + +test('model and type names are not mangled when minified', async t => { + t.timeout(30 * 1000); + + await run('npm', 'i'); + await run('npm', 'run', 'webpack'); + + const {getModel} = await import('../fixtures/minified/main.bundle.js'); + const model = getModel(); + + t.is(model.name, 'MainModel'); + + t.is(model.custom.name, 'Custom'); + + t.is(model.string.name, 'String'); + t.is(model.stringSlug.name, 'SlugOf(string)'); + t.is(model.requiredString.name, 'RequiredString'); + t.is(model.arrayOfString.name, 'ArrayOf(String)'); + t.is(model.requiredArrayOfString.name, 'RequiredArrayOf(String)'); + + t.is(model.boolean.name, 'Boolean'); + t.is(model.requiredBoolean.name, 'RequiredBoolean'); + t.is(model.arrayOfBoolean.name, 'ArrayOf(Boolean)'); + t.is(model.requiredArrayOfBoolean.name, 'RequiredArrayOf(Boolean)'); + + t.is(model.number.name, 'Number'); + t.is(model.requiredNumber.name, 'RequiredNumber'); + t.is(model.arrayOfNumber.name, 'ArrayOf(Number)'); + t.is(model.requiredArrayOfNumber.name, 'RequiredArrayOf(Number)'); + + t.is(model.date.name, 'Date'); + t.is(model.requiredDate.name, 'RequiredDate'); + t.is(model.arrayOfDate.name, 'ArrayOf(Date)'); + t.is(model.requiredArrayOfDate.name, 'RequiredArrayOf(Date)'); + + t.is(model.linked.name, 'LinkedModel'); + t.is(model.linkedMany.name, 'ArrayOf(LinkedManyModel)'); + + t.is(model.circular.name, 'CircularModel'); + t.is(model.circularMany.name, 'ArrayOf(CircularManyModel)'); +}); diff --git a/test/fixtures/minified/.gitignore b/test/fixtures/minified/.gitignore new file mode 100644 index 0000000..1c4d96d --- /dev/null +++ b/test/fixtures/minified/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +main.bundle.* diff --git a/test/fixtures/minified/main.js b/test/fixtures/minified/main.js new file mode 100644 index 0000000..6f5e94d --- /dev/null +++ b/test/fixtures/minified/main.js @@ -0,0 +1,3 @@ +import {MainModel} from '../Models.js'; + +export const getModel = () => MainModel; diff --git a/test/fixtures/minified/package-lock.json b/test/fixtures/minified/package-lock.json new file mode 100644 index 0000000..f65bfd1 --- /dev/null +++ b/test/fixtures/minified/package-lock.json @@ -0,0 +1,1229 @@ +{ + "name": "minified-test", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "minified-test", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "webpack": "^5.95.0", + "webpack-cli": "^5.1.4" + } + }, + "../../../package.json": { + "extraneous": true + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.7.3", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.12.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.12.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.12.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.12.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.12.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.12.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.12.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.12.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.12.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.12.1", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/browserslist": { + "version": "4.24.0", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001664", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.29", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.17.1", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.14.0", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.5.4", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/kind-of": { + "version": "6.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.18", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.0", + "dev": true, + "license": "ISC" + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.34.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.95.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.7.1", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "dev": true, + "license": "MIT" + } + } +} diff --git a/test/fixtures/minified/package.json b/test/fixtures/minified/package.json new file mode 100644 index 0000000..f05c0c9 --- /dev/null +++ b/test/fixtures/minified/package.json @@ -0,0 +1,17 @@ +{ + "name": "minified-test", + "version": "1.0.0", + "main": "main.js", + "devDependencies": { + "webpack": "^5.95.0", + "webpack-cli": "^5.1.4" + }, + "scripts": { + "webpack": "webpack" + }, + "type": "module", + "keywords": [], + "author": "", + "license": "ISC", + "description": "" +} diff --git a/test/fixtures/minified/webpack.config.js b/test/fixtures/minified/webpack.config.js new file mode 100644 index 0000000..b14a7c6 --- /dev/null +++ b/test/fixtures/minified/webpack.config.js @@ -0,0 +1,17 @@ +import {dirname, resolve} from 'node:path'; +import {fileURLToPath} from 'node:url'; + +export default { + entry: './main.js', + mode: 'production', + experiments: { + outputModule: true, + }, + output: { + filename: 'main.bundle.js', + path: resolve(dirname(fileURLToPath(import.meta.url))), + library: { + type: 'module', + }, + }, +}; From 9a271c25b5df98c17175965bbbd3beaeda956b93 Mon Sep 17 00:00:00 2001 From: Lawrence Date: Mon, 30 Sep 2024 08:47:01 +0100 Subject: [PATCH 3/4] docs: separate and expand existing docs --- README.md | 256 +++------------------------------ docs/code-quirks.md | 71 +++++++++ docs/model-property-types.md | 173 ++++++++++++++++++++++ docs/models-as-properties.md | 159 ++++++++++++++++++++ docs/search-queries.md | 47 ++++++ docs/storage-engines.md | 61 ++++++++ docs/structured-queries.md | 107 ++++++++++++++ docs/transactions.md | 23 +++ test/fixtures/minified/main.js | 5 + 9 files changed, 667 insertions(+), 235 deletions(-) create mode 100644 docs/code-quirks.md create mode 100644 docs/model-property-types.md create mode 100644 docs/models-as-properties.md create mode 100644 docs/search-queries.md create mode 100644 docs/storage-engines.md create mode 100644 docs/structured-queries.md create mode 100644 docs/transactions.md diff --git a/README.md b/README.md index dcb54db..330769c 100644 --- a/README.md +++ b/README.md @@ -1,243 +1,29 @@ # @acodeninja/persist -A JSON based data modelling and persistence library with alternate storage mechanisms. +A JSON based data modelling and persistence library with alternate storage mechanisms, designed with static site generation in mind. -## Models +![NPM Version](https://img.shields.io/npm/v/%40acodeninja%2Fpersist) +![NPM Unpacked Size](https://img.shields.io/npm/unpacked-size/%40acodeninja%2Fpersist) +![GitHub top language](https://img.shields.io/github/languages/top/acodeninja/persist) +![NPM Downloads](https://img.shields.io/npm/dw/%40acodeninja%2Fpersist) -The `Model` and `Type` classes allow creating representations of data objects +[![DeepSource](https://app.deepsource.com/gh/acodeninja/persist.svg/?label=active+issues&show_trend=true&token=Vd8_PJuRwwoq4_uBJ0_ymc06)](https://app.deepsource.com/gh/acodeninja/persist/) +[![DeepSource](https://app.deepsource.com/gh/acodeninja/persist.svg/?label=code+coverage&show_trend=true&token=Vd8_PJuRwwoq4_uBJ0_ymc06)](https://app.deepsource.com/gh/acodeninja/persist/) -### Defining Models +## Features -##### A model using all available basic types +- Data modelling with relationships +- Data validation +- Data querying +- Fuzzy search +- Storage with: S3, HTTP and Filesystem -```javascript -import Persist from "@acodeninja/persist"; +## Find out more -export class SimpleModel extends Persist.Type.Model { - static boolean = Persist.Type.Boolean; - static string = Persist.Type.String; - static number = Persist.Type.Number; - static date = Persist.Type.Date; -} -``` - -##### A simple model using required types - -```javascript -import Persist from "@acodeninja/persist"; - -export class SimpleModel extends Persist.Type.Model { - static requiredBoolean = Persist.Type.Boolean.required; - static requiredString = Persist.Type.String.required; - static requiredNumber = Persist.Type.Number.required; - static requiredDate = Persist.Type.Date.required; -} -``` - -##### A simple model using arrays of basic types - -```javascript -import Persist from "@acodeninja/persist"; - -export class SimpleModel extends Persist.Type.Model { - static arrayOfBooleans = Persist.Type.Array.of(Type.Boolean); - static arrayOfStrings = Persist.Type.Array.of(Type.String); - static arrayOfNumbers = Persist.Type.Array.of(Type.Number); - static arrayOfDates = Persist.Type.Array.of(Type.Date); - static requiredArrayOfBooleans = Persist.Type.Array.of(Type.Boolean).required; - static requiredArrayOfStrings = Persist.Type.Array.of(Type.String).required; - static requiredArrayOfNumbers = Persist.Type.Array.of(Type.Number).required; - static requiredArrayOfDates = Persist.Type.Array.of(Type.Date).required; -} -``` - -
- Complex relationships are also supported - -#### One-to-One Relationships - -##### A one-to-one relationship - -```javascript -import Persist from "@acodeninja/persist"; - -export class ModelB extends Persist.Type.Model { -} - -export class ModelA extends Persist.Type.Model { - static linked = ModelB; -} -``` - -##### A circular one-to-one relationship - -```javascript -import Persist from "@acodeninja/persist"; - -export class ModelA extends Persist.Type.Model { - static linked = () => ModelB; -} - -export class ModelB extends Persist.Type.Model { - static linked = ModelA; -} -``` - -#### One-to-Many Relationships - -##### A one-to-many relationship - -```javascript -import Persist from "@acodeninja/persist"; - -export class ModelB extends Persist.Type.Model { -} - -export class ModelA extends Persist.Type.Model { - static linked = Persist.Type.Array.of(ModelB); -} -``` - -##### A circular one-to-many relationship - -```javascript -import Persist from "@acodeninja/persist"; - -export class ModelA extends Persist.Type.Model { - static linked = () => Type.Array.of(ModelB); -} - -export class ModelB extends Persist.Type.Model { - static linked = ModelA; -} -``` - -#### Many-to-Many Relationships - -##### A many-to-many relationship - -```javascript -import Persist from "@acodeninja/persist"; - -export class ModelA extends Persist.Type.Model { - static linked = Persist.Type.Array.of(ModelB); -} - -export class ModelB extends Persist.Type.Model { - static linked = Persist.Type.Array.of(ModelA); -} -``` -
- -## Find and Search - -Models may expose a `searchProperties()` and `indexProperties()` static method to indicate which -fields should be indexed for storage engine `find()` and `search()` methods. - -Use `find()` for a low usage exact string match on any indexed attribute of a model. - -Use `search()` for a medium usage fuzzy string match on any search indexed attribute of a model. - -```javascript -import Persist from "@acodeninja/persist"; -import FileEngine from "@acodeninja/persist/engine/file"; - -export class Tag extends Persist.Type.Model { - static tag = Persist.Type.String.required; - static description = Persist.Type.String; - static searchProperties = () => ['tag', 'description']; - static indexProperties = () => ['tag']; -} - -const tag = new Tag({tag: 'documentation', description: 'How to use the persist library'}); - -await FileEngine.find(Tag, {tag: 'documentation'}); -// [Tag {tag: 'documentation', description: 'How to use the persist library'}] - -await FileEngine.search(Tag, 'how to'); -// [Tag {tag: 'documentation', description: 'How to use the persist library'}] -``` - -## Storage - -### Filesystem Storage Engine - -To store models using the local file system, use the `File` storage engine. - -```javascript -import Persist from "@acodeninja/persist"; -import FileEngine from "@acodeninja/persist/engine/file"; - -Persist.addEngine('local', FileEngine, { - path: '/app/storage', -}); - -export class Tag extends Persist.Type.Model { - static tag = Persist.Type.String.required; -} - -await Persist.getEngine('local', FileEngine).put(new Tag({tag: 'documentation'})); -``` - -### HTTP Storage Engine - -To store models using an S3 Bucket, use the `S3` storage engine. - -```javascript -import Persist from "@acodeninja/persist"; -import HTTPEngine from "@acodeninja/persist/engine/http"; - -Persist.addEngine('remote', HTTPEngine, { - host: 'https://api.example.com', -}); - -export class Tag extends Persist.Type.Model { - static tag = Persist.Type.String.required; -} - -await Persist.getEngine('remote', HTTPEngine).put(new Tag({tag: 'documentation'})); -``` - -### S3 Storage Engine - -To store models using an S3 Bucket, use the `S3` storage engine. - -```javascript -import Persist from "@acodeninja/persist"; -import S3Engine from "@acodeninja/persist/engine/s3"; - -Persist.addEngine('remote', S3Engine, { - bucket: 'test-bucket', - client: new S3Client(), -}); - -export class Tag extends Persist.Type.Model { - static tag = Persist.Type.String.required; -} - -await Persist.getEngine('remote', S3Engine).put(new Tag({tag: 'documentation'})); -``` - -## Transactions - -Create transactions to automatically roll back on failure to update. - -```javascript -import Persist from "@acodeninja/persist"; -import S3Engine from "@acodeninja/persist/engine/s3"; - -Persist.addEngine('remote', S3Engine, { - bucket: 'test-bucket', - client: new S3Client(), - transactions: true, -}); - -export class Tag extends Persist.Type.Model { - static tag = Persist.Type.String.required; -} - -const transaction = Persist.getEngine('remote', S3Engine).start(); - -await transaction.put(new Tag({tag: 'documentation'})); -await transaction.commit(); -``` +- [Model Property Types](./docs/model-property-types.md) +- [Models as Properties](./docs/models-as-properties.md) +- [Structured Queries](./docs/structured-queries.md) +- [Search Queries](./docs/search-queries.md) +- [Storage Engines](./docs/storage-engines.md) +- [Transactions](./docs/transactions.md) +- [Quirks](./docs/code-quirks.md) diff --git a/docs/code-quirks.md b/docs/code-quirks.md new file mode 100644 index 0000000..b7da85e --- /dev/null +++ b/docs/code-quirks.md @@ -0,0 +1,71 @@ +# Code Quirks + +When using Persist in a minified or bundled codebase, it's important to be aware of two key quirks: handling class names during minification and managing reference errors when working with model relationships. + +## Class Names and Minification + +When you bundle or minify JavaScript code for production, class names are often altered, which can cause issues. Specifically, models may lose their original class names, which we rely on for storing data in the correct namespace. + +To avoid this problem, you have two options: + +1. Disable class name mangling in your minifier. +2. Use `this.setMinifiedName(name)` to manually specify the model's name. + +```javascript +import Persist from "@acodeninja/persist"; + +export class Person extends Persist.Type.Model { + static { + this.setMinifiedName('Person'); + this.name = Persist.Type.String.required; + } +} +``` + +If you don't set the minified name, the wrong namespace may be used when saving models, leading to unexpected behavior. + +## Reference Errors + +When defining relationships between models, especially circular relationships (e.g., `Person` references `Address`, and `Address` references `Person`), the order of declarations matters. If the models are referenced before they are initialized, you'll encounter `ReferenceError` messages, like: + +```console +ReferenceError: Cannot access 'Address' before initialization +``` + +To avoid these errors, always define model relationships using arrow functions. For example: + +```javascript +import Persist from "@acodeninja/persist"; + +export class Person extends Persist.Type.Model { + static { + this.address = () => Address; + } +} + +export class Address extends Persist.Type.Model { + static { + this.person = () => Person; + this.address = Persist.Type.String.required; + this.postcode = Persist.Type.String.required; + } +} +``` + +By doing this, you ensure that model references are evaluated lazily, after all models have been initialized, preventing `ReferenceError` issues. + +## Using `HTTP` Engine in Browser + +When implementing thee `HTTP` engine for code that runs in the web browser, you must pass `fetch` into the engine configuration and bind it to the `window` object. + +```javascript +import Persist from "@acodeninja/persist"; +import HTTPEngine from "@acodeninja/persist/engine/http"; + +Persist.addEngine('remote', HTTPEngine, { + host: 'https://api.example.com', + fetch: fetch.bind(window), +}); +``` + +This will ensure that `fetch` can access the window context which is required for it to function. diff --git a/docs/model-property-types.md b/docs/model-property-types.md new file mode 100644 index 0000000..74d9e51 --- /dev/null +++ b/docs/model-property-types.md @@ -0,0 +1,173 @@ +# Model Property Types + +Persist uses a type definition for the properties of each model, this allows for validation and type coercion when saving and retrieving data. + +Model properties can be assigned a `Type`, or another `Model`. For more information on see [Models as Properties](./models-as-properties.md). + +## Defining Model Properties + +Properties can be defined on a model by setting static properties to the value of a type on the class that describes the model. + +```javascript +import Persist from '@acodeninja/persist'; + +class Person extends Persist.Type.Model { + static { + this.firstName = Persist.Type.String; + this.lastName = Persist.Type.String; + } +} +``` + +## Simple Types + +### `Persist.Type.String` + +Use the `String` type for model properties that should store a [string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String). The `String` type also supports the `.required` modifier to ensure that when the model is persisted a value must exist for it. + +```javascript +import Persist from '@acodeninja/persist'; + +class Person extends Persist.Type.Model { + static { + this.firstName = Persist.Type.String; + this.lastName = Persist.Type.String.required; + } +} +``` + +### `Persist.Type.Boolean` + +Use the `Boolean` type for model properties that should store a [boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean). The `Boolean` type also supports the `.required` modifier to ensure that when the model is persisted a value must exist for it. + +```javascript +import Persist from '@acodeninja/persist'; + +class Person extends Persist.Type.Model { + static { + this.markettingEmailsActive = Persist.Type.Boolean; + this.accountActive = Persist.Type.Boolean.required; + } +} +``` + +### `Persist.Type.Number` + +Use the `Number` type for model properties that should store a [number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number). The `Number` type also supports the `.required` modifier to ensure that when the model is persisted a value must exist for it. + +```javascript +import Persist from '@acodeninja/persist'; + +class Person extends Persist.Type.Model { + static { + this.loginToken = Persist.Type.Number; + this.accountId = Persist.Type.Number.required; + } +} +``` + +### `Persist.Type.Date` + +Use the `Date` type for model properties that should store a [date](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date). The `Date` type also supports the `.required` modifier to ensure that when the model is persisted a value must exist for it. + +```javascript +import Persist from '@acodeninja/persist'; + +class Person extends Persist.Type.Model { + static { + this.lastLogin = Persist.Type.Date; + this.createdAt = Persist.Type.Date.required; + } +} +``` + +## Complex Types + +### `Persist.Type.Array.of(type)` + +Use the `Array` type for model properties that should store an array of another type or model. The `Array` type also supports the `.required` modifier to ensure that when the model is persisted a value must exist for it. + +```javascript +import Persist from '@acodeninja/persist'; + +class Person extends Persist.Type.Model { + static { + this.failedLoginAttempts = Persist.Type.Array.of(Persist.Type.Date); + this.fullName = Persist.Type.Array.of(Persist.Type.String).required; + } +} +``` + +### `Persist.Type.Custom.of(schema)` + +Use the `Custom` type for model properties that should store a custom [json-schema draft-07](https://json-schema.org/draft-07/json-schema-hypermedia) object. You can also use any formats defined by the [`avj-formats`](https://ajv.js.org/packages/ajv-formats.html) library. The `Custom` type also supports the `.required` modifier to ensure that when the model is persisted a value must exist for it. + +```javascript +import Persist from '@acodeninja/persist'; + +class Person extends Persist.Type.Model { + static { + this.address = Persist.Type.Custom.of({ + type: 'object', + additionalProperties: false, + required: ['line1', 'city', 'postcode'], + properties: { + line1: {type: 'string'}, + line2: {type: 'string'}, + city: {type: 'string'}, + postcode: { + type: 'string', + pattern: "^[A-Z]+[0-9]+\s[A-Z]+[0-9]+$", + }, + }, + }).required; + } +} +``` + +## Resolved Types + +Resolved types are different from other types in that they do not directly store data themselves, rather they perform an action on another property of the model. + +### `Persist.Type.Resolved.Slug.of(property)` + +Use the `Slug` type for model properties that should have a slug version of another properties value. The `Custom` type also supports the `.required` modifier to ensure that when the model is persisted a value must exist for it. + +```javascript +import Persist from '@acodeninja/persist'; + +class Page extends Persist.Type.Model { + static { + this.title = Persist.Type.String; + this.slug = Persist.Type.Resolved.Slug.of('title'); + } +} + +const page = new Page({title: 'A really important article!'}); +const {slug} = page.toData(); + +console.log(slug); // a-really-important-article +``` + +## Modifiers + +Models and most types support a modifier, this will alter the validation and persistence process based on the type of modifier used. + +### `.required` + +Most types support the `.required` modifier, which will alter validation to enforce the presence of the property when saving data. + +```javascript +class RequiredStringModel extends Persist.Type.Model { + static { + this.requiredString = Type.String.required; + this.requiredNumber = Type.Number.required; + this.requiredBoolean = Type.Boolean.required; + this.requiredDate = Type.Date.required; + this.requiredArrayOfString = Type.Array.of(Type.String).required; + this.requiredArrayOfNumber = Type.Array.of(Type.Number).required; + this.requiredArrayOfBoolean = Type.Array.of(Type.Boolean).required; + this.requiredArrayOfDate = Type.Array.of(Type.Date).required; + } +} +``` diff --git a/docs/models-as-properties.md b/docs/models-as-properties.md new file mode 100644 index 0000000..88f0359 --- /dev/null +++ b/docs/models-as-properties.md @@ -0,0 +1,159 @@ +# Models as Properties + +In addition to assigning basic types to model properties, you can assign entire models as properties. This allows for the creation of complex relationships between models. For information on using basic types for properties, refer to [model property types](./model-property-types.md). + +We’ll explore different types of relationships between models using examples of `Person` and `Address` models, evolving the definition step by step. + +```javascript +import Persist from "@acodeninja/persist"; + +export class Person extends Persist.Type.Model { + static { + this.name = Persist.Type.String.required; + } +} + +export class Address extends Persist.Type.Model { + static { + this.address = Persist.Type.String.required; + this.postcode = Persist.Type.String.required; + } +} +``` + +## One-to-One Relationships + +To define a **one-to-one** relationship between two models, set a static property in one model as a function that returns the other model. This ensures that the models can be defined in any order, avoiding issues with initialization. + +```javascript +import Persist from "@acodeninja/persist"; + +export class Person extends Persist.Type.Model { + static { + this.name = Persist.Type.String.required; + this.address = () => Address; + } +} + +export class Address extends Persist.Type.Model { + static { + this.address = Persist.Type.String.required; + this.postcode = Persist.Type.String.required; + } +} +``` + +> [!IMPORTANT] +> **Why Use an Arrow Function?** +> +> The arrow function allows the model to reference another model that may not have been defined yet. Without it, you might encounter an error like `ReferenceError: Cannot access 'Address' before initialization`. + +### Circular One-to-One Relationships + +You can extend the previous example by allowing both models to reference each other. This is useful for circular relationships, where querying one model (e.g., `Address`) should also allow access to the related model (e.g., `Person`). + +```javascript +import Persist from "@acodeninja/persist"; + +export class Person extends Persist.Type.Model { + static { + this.name = Persist.Type.String.required; + this.address = () => Address; + } +} + +export class Address extends Persist.Type.Model { + static { + this.person = () => Person; + this.address = Persist.Type.String.required; + this.postcode = Persist.Type.String.required; + } +} +``` + +## One-to-Many Relationships + +To model a **one-to-many** relationship, use `Persist.Type.Array` to store an array of related models. For instance, if a `Person` can have multiple addresses, this is how it would be defined: + +```javascript +import Persist from "@acodeninja/persist"; + +export class Person extends Persist.Type.Model { + static { + this.name = Persist.Type.String.required; + this.addresses = () => Persist.Type.Array.of(Address); + } +} + +export class Address extends Persist.Type.Model { + static { + this.person = () => Person; + this.address = Persist.Type.String.required; + this.postcode = Persist.Type.String.required; + } +} +``` + +This structure allows for querying both the Person and their multiple Address records, while maintaining the ability to retrieve the related person from any address. + +## Many-to-Many Relationships + +In some cases, you may want to model a many-to-many relationship. For example, if multiple people can live at the same address, this type of relationship is ideal. + +```javascript +import Persist from "@acodeninja/persist"; + +export class Person extends Persist.Type.Model { + static { + this.name = Persist.Type.String.required; + this.addresses = () => Persist.Type.Array.of(Address); + } +} + +export class Address extends Persist.Type.Model { + static { + this.people = () => Persist.Type.Array.of(Person); + this.address = Persist.Type.String.required; + this.postcode = Persist.Type.String.required; + } +} +``` + +This allows both `Person` and `Address` models to reference each other as arrays, establishing a many-to-many relationship. + +## Combining Relationships + +In more complex scenarios, you may want to capture additional information about the relationship itself. For example, when tracking when a person moved to a particular address, you can create an intermediary model (e.g., `Abode`) to store this information. + +```javascript +import Persist from "@acodeninja/persist"; + +export class Person extends Persist.Type.Model { + static { + this.name = Persist.Type.String.required; + this.addresses = () => Persist.Type.Array.of(Abode); + } +} + +export class Abode extends Persist.Type.Model { + static { + this.moveInDate = Persist.Type.Date.required; + this.address = () => Address; + this.person = () => Person; + } +} + +export class Address extends Persist.Type.Model { + static { + this.people = () => Persist.Type.Array.of(Person); + this.address = Persist.Type.String.required; + this.postcode = Persist.Type.String.required; + } +} +``` + +In this setup: + +- A `Person` can have multiple `Abode` entries (i.e., where they lived and when they moved in). +- Each `Abode` links a `Person` to an `Address`, while also recording the move-in date. +- An `Address` can still reference multiple people, making this a flexible and more complex relationship model. diff --git a/docs/search-queries.md b/docs/search-queries.md new file mode 100644 index 0000000..f602d13 --- /dev/null +++ b/docs/search-queries.md @@ -0,0 +1,47 @@ +# Search Queries + +In addition to [structured queries](./structured-queries.md), persist also supports fuzzy search across fields indexed for search. + +## Indexing Data for Search + +To set index properties on a model for search, define the static function `searchProperties` as an arrow function that returns an array of fields that should be indexed for search. + +Let's consider the following models: + +```javascript +import Persist from "@acodeninja/persist"; + +export class Person extends Persist.Type.Model { + static { + this.name = Persist.Type.String.required; + this.address = () => Address; + this.searchProperties = () => ['name', 'address.address']; + } +} + +export class Address extends Persist.Type.Model { + static { + this.address = Persist.Type.String.required; + this.postcode = Persist.Type.String.required; + this.searchProperties = () => ['address', 'postcode']; + } +} +``` + +Every time a `Person` model is put to a storage engine, the person's name and address are saved to the search index and can be queried. + +## Searching + +To search for any `Person` who lives on station road, the following search query can be run: + +```javascript +import Persist from "@acodeninja/persist"; +import Person from "./Person"; +import FileEngine from "@acodeninja/persist/engine/file" + +FileEngine + .configure(configuration) + .search(Person, 'station road'); +``` + +This will find all matches for people who live at any address that includes `station road`. diff --git a/docs/storage-engines.md b/docs/storage-engines.md new file mode 100644 index 0000000..cf12dfd --- /dev/null +++ b/docs/storage-engines.md @@ -0,0 +1,61 @@ +# Storage Engines + +Persist makes several storage engines available for use with the library + +## Filesystem Storage Engine + +To store models using the local file system, use the `File` storage engine. + +```javascript +import Persist from "@acodeninja/persist"; +import FileEngine from "@acodeninja/persist/engine/file"; + +Persist.addEngine('local', FileEngine, { + path: '/app/storage', +}); + +export class Tag extends Persist.Type.Model { + static tag = Persist.Type.String.required; +} + +await Persist.getEngine('local', FileEngine).put(new Tag({tag: 'documentation'})); +``` + +## HTTP Storage Engine + +To store models using an HTTP server, use the `HTTP` storage engine. When using the `HTTP` engine in the browser, refer to [code quirks](./code-quirks.md#using-http-engine-in-browser). + +```javascript +import Persist from "@acodeninja/persist"; +import HTTPEngine from "@acodeninja/persist/engine/http"; + +Persist.addEngine('remote', HTTPEngine, { + host: 'https://api.example.com', +}); + +export class Tag extends Persist.Type.Model { + static tag = Persist.Type.String.required; +} + +await Persist.getEngine('remote', HTTPEngine).put(new Tag({tag: 'documentation'})); +``` + +## S3 Storage Engine + +To store models using an S3 Bucket, use the `S3` storage engine. To use the `S3` engine you must also add the `@aws-sdk/client-s3` dependency to your `package.json` file. + +```javascript +import Persist from "@acodeninja/persist"; +import S3Engine from "@acodeninja/persist/engine/s3"; + +Persist.addEngine('remote', S3Engine, { + bucket: 'test-bucket', + client: new S3Client(), +}); + +export class Tag extends Persist.Type.Model { + static tag = Persist.Type.String.required; +} + +await Persist.getEngine('remote', S3Engine).put(new Tag({tag: 'documentation'})); +``` diff --git a/docs/structured-queries.md b/docs/structured-queries.md new file mode 100644 index 0000000..1fda5b1 --- /dev/null +++ b/docs/structured-queries.md @@ -0,0 +1,107 @@ +# Structured Queries + +Use structured queries when you need to filter a collection of models using a series of exact and partial matching conditions. + +## Indexing Data + +To set index properties on a model, define the static function `indexProperties` as an arrow function that returns an array of fields that should be indexed for querying. + +Let's consider the following models: + +```javascript +import Persist from "@acodeninja/persist"; + +export class Person extends Persist.Type.Model { + static { + this.name = Persist.Type.String.required; + this.address = () => Address; + this.indexProperties = () => ['name', 'address.postcode']; + } +} + +export class Address extends Persist.Type.Model { + static { + this.address = Persist.Type.String.required; + this.postcode = Persist.Type.String.required; + this.indexProperties = () => ['postcode']; + } +} +``` + +Every time a `Person` model is put to a storage engine, the person's name and address postcode are saved to the index and can be queried. + +> [!NOTE] +> All fields included in the model index will be stored in the same file so be careful not to index fields that contain a lot of data. + +## Querying Exact Matches + +To query for a `Person` called `Joe Bloggs` an exact query can be written: + +```javascript +import Persist from "@acodeninja/persist"; +import Person from "./Person"; +import FileEngine from "@acodeninja/persist/engine/file" + +FileEngine + .configure(configuration) + .find(Person, { + name: {$is: 'Joe Bloggs'}, + }); +``` + +## Querying Partial Matches + +To query for a `Person` with name `Joe` a contains query can be written: + +```javascript +import Persist from "@acodeninja/persist"; +import Person from "./Person"; +import FileEngine from "@acodeninja/persist/engine/file" + +FileEngine + .configure(configuration) + .find(Person, { + name: {$contains: 'Joe'}, + }); +``` + +## Querying Combination Matches + +To query for a `Person` who lives at `SW1 1AA` a combination of contains and exact queries can be written: + +```javascript +import Persist from "@acodeninja/persist"; +import Person from "./Person"; +import FileEngine from "@acodeninja/persist/engine/file" + +FileEngine + .configure(configuration) + .find(Person, { + address: { + $contains: { + postcode: {$is: 'SW1 1AA'}, + }, + }, + }); +``` + +## Multiple Queries + +To query for anyone called `Joe Bloggs` who lives in the `SW1` postcode area, we can combine queries: + +```javascript +import Persist from "@acodeninja/persist"; +import Person from "./Person"; +import FileEngine from "@acodeninja/persist/engine/file" + +FileEngine + .configure(configuration) + .find(Person, { + name: {$is: 'Joe Bloggs'}, + address: { + $contains: { + postcode: {$contains: 'SW1'}, + }, + }, + }); +``` diff --git a/docs/transactions.md b/docs/transactions.md new file mode 100644 index 0000000..431440d --- /dev/null +++ b/docs/transactions.md @@ -0,0 +1,23 @@ +## Transactions + +Create transactions to automatically roll back on failure. + +```javascript +import Persist from "@acodeninja/persist"; +import S3Engine from "@acodeninja/persist/engine/s3"; + +Persist.addEngine('remote', S3Engine, { + bucket: 'test-bucket', + client: new S3Client(), + transactions: true, +}); + +export class Tag extends Persist.Type.Model { + static tag = Persist.Type.String.required; +} + +const transaction = Persist.getEngine('remote', S3Engine).start(); + +await transaction.put(new Tag({tag: 'documentation'})); +await transaction.commit(); +``` diff --git a/test/fixtures/minified/main.js b/test/fixtures/minified/main.js index 6f5e94d..29f8d00 100644 --- a/test/fixtures/minified/main.js +++ b/test/fixtures/minified/main.js @@ -1,3 +1,8 @@ import {MainModel} from '../Models.js'; +/** + * Returns the main model class + * + * @return {MainModel} + */ export const getModel = () => MainModel; From cf0f459cecc06b316221add68b77d6b789e159e6 Mon Sep 17 00:00:00 2001 From: Lawrence Date: Mon, 30 Sep 2024 08:56:02 +0100 Subject: [PATCH 4/4] ci: upload code coverage to deepsource --- .github/workflows/quality-checks.yml | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml index 9cc3db1..85f8ff6 100644 --- a/.github/workflows/quality-checks.yml +++ b/.github/workflows/quality-checks.yml @@ -24,23 +24,13 @@ jobs: - uses: actions/checkout@v4 - uses: asdf-vm/actions/install@v3 - run: make init - - run: make test/coverage - - dry-run-release: - name: 📋 dry run release - runs-on: ubuntu-latest - permissions: - contents: write - issues: write - pull-requests: write - id-token: write - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: asdf-vm/actions/install@v3 - - run: npm clean-install - - run: npx semantic-release --dry-run + - run: make test/coverage/report + - run: curl https://deepsource.io/cli | sh + - name: deepsource report env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + DEEPSOURCE_DSN: ${{ secrets.DEEPSOURCE_DSN }} + run: | + ./bin/deepsource report \ + --analyzer test-coverage \ + --key javascript \ + --value-file "$(realpath ./coverage/lcov.info)"