millas 0.2.28 → 0.2.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/bin/millas.js +12 -2
  2. package/package.json +2 -1
  3. package/src/cli.js +117 -20
  4. package/src/commands/call.js +1 -1
  5. package/src/commands/createsuperuser.js +137 -182
  6. package/src/commands/key.js +61 -83
  7. package/src/commands/lang.js +423 -515
  8. package/src/commands/make.js +88 -62
  9. package/src/commands/migrate.js +200 -279
  10. package/src/commands/new.js +55 -50
  11. package/src/commands/route.js +78 -80
  12. package/src/commands/schedule.js +52 -150
  13. package/src/commands/serve.js +158 -191
  14. package/src/console/AppCommand.js +106 -0
  15. package/src/console/BaseCommand.js +726 -0
  16. package/src/console/CommandContext.js +66 -0
  17. package/src/console/CommandRegistry.js +88 -0
  18. package/src/console/Style.js +123 -0
  19. package/src/console/index.js +12 -3
  20. package/src/container/AppInitializer.js +10 -0
  21. package/src/facades/DB.js +195 -0
  22. package/src/index.js +2 -1
  23. package/src/scaffold/maker.js +102 -42
  24. package/src/schematics/Collection.js +28 -0
  25. package/src/schematics/SchematicEngine.js +122 -0
  26. package/src/schematics/Template.js +99 -0
  27. package/src/schematics/index.js +7 -0
  28. package/src/templates/command/default.template.js +14 -0
  29. package/src/templates/command/schema.json +19 -0
  30. package/src/templates/controller/default.template.js +10 -0
  31. package/src/templates/controller/resource.template.js +59 -0
  32. package/src/templates/controller/schema.json +30 -0
  33. package/src/templates/job/default.template.js +11 -0
  34. package/src/templates/job/schema.json +19 -0
  35. package/src/templates/middleware/default.template.js +11 -0
  36. package/src/templates/middleware/schema.json +19 -0
  37. package/src/templates/migration/default.template.js +14 -0
  38. package/src/templates/migration/schema.json +19 -0
  39. package/src/templates/model/default.template.js +14 -0
  40. package/src/templates/model/migration.template.js +17 -0
  41. package/src/templates/model/schema.json +30 -0
  42. package/src/templates/service/default.template.js +12 -0
  43. package/src/templates/service/schema.json +19 -0
  44. package/src/templates/shape/default.template.js +11 -0
  45. package/src/templates/shape/schema.json +19 -0
  46. package/src/validation/BaseValidator.js +3 -0
  47. package/src/validation/types.js +3 -3
@@ -0,0 +1,28 @@
1
+ const SchematicEngine = require('./SchematicEngine');
2
+
3
+ class Collection {
4
+ constructor(name, templatesDir) {
5
+ this.name = name;
6
+ this.engine = new SchematicEngine(templatesDir);
7
+ this.schematics = new Map();
8
+ }
9
+
10
+ add(name, schematicName) {
11
+ this.schematics.set(name, schematicName);
12
+ return this;
13
+ }
14
+
15
+ async generate(name, data, options = {}) {
16
+ const schematicName = this.schematics.get(name);
17
+ if (!schematicName) {
18
+ throw new Error(`Schematic '${name}' not found in collection '${this.name}'`);
19
+ }
20
+ return await this.engine.generate(schematicName, data, options);
21
+ }
22
+
23
+ list() {
24
+ return Array.from(this.schematics.keys());
25
+ }
26
+ }
27
+
28
+ module.exports = Collection;
@@ -0,0 +1,122 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const Template = require('./Template');
4
+
5
+ class SchematicEngine {
6
+ constructor(templatesDir) {
7
+ this.templatesDir = templatesDir;
8
+ }
9
+
10
+ load(name) {
11
+ const dir = path.join(this.templatesDir, name);
12
+ const schemaPath = path.join(dir, 'schema.json');
13
+
14
+ if (!fs.existsSync(schemaPath)) {
15
+ throw new Error(`Schematic not found: ${name}`);
16
+ }
17
+
18
+ const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
19
+ return { schema, dir };
20
+ }
21
+
22
+ async generate(name, data, options = {}) {
23
+ const { schema, dir } = this.load(name);
24
+
25
+ this.#validate(schema, data, options);
26
+
27
+ const results = [];
28
+ const allData = { ...data, ...options };
29
+
30
+ for (const fileConfig of schema.files) {
31
+ // Skip conditional files if condition not met
32
+ if (fileConfig.condition && !options[fileConfig.condition]) {
33
+ continue;
34
+ }
35
+
36
+ const templateFile = this.#selectTemplate(schema, fileConfig, options);
37
+ const templatePath = path.join(dir, templateFile);
38
+
39
+ if (!fs.existsSync(templatePath)) {
40
+ throw new Error(`Template file not found: ${templateFile}`);
41
+ }
42
+
43
+ const templateContent = require(templatePath);
44
+ const template = new Template(templateContent, { helpers: schema.helpers || {} });
45
+ const rendered = template.render(allData);
46
+
47
+ const outputPath = this.#resolveOutputPath(fileConfig.output, allData);
48
+
49
+ if (fs.existsSync(outputPath) && !options.force) {
50
+ throw new Error(`File already exists: ${outputPath}. Use --force to overwrite.`);
51
+ }
52
+
53
+ this.#ensureDir(path.dirname(outputPath));
54
+ fs.writeFileSync(outputPath, rendered);
55
+
56
+ results.push({ path: outputPath, content: rendered });
57
+ }
58
+
59
+ return results.length === 1 ? results[0] : results;
60
+ }
61
+
62
+ #validate(schema, data, options) {
63
+ for (const arg of schema.arguments || []) {
64
+ const value = data[arg.name];
65
+
66
+ if (arg.required && !value) {
67
+ throw new Error(`Missing required argument: ${arg.name}`);
68
+ }
69
+
70
+ if (value && arg.type) {
71
+ const actualType = typeof value;
72
+ if (actualType !== arg.type) {
73
+ throw new Error(`Argument '${arg.name}' must be of type ${arg.type}, got ${actualType}`);
74
+ }
75
+ }
76
+
77
+ if (value && arg.enum && !arg.enum.includes(value)) {
78
+ throw new Error(`Argument '${arg.name}' must be one of: ${arg.enum.join(', ')}`);
79
+ }
80
+ }
81
+
82
+ for (const opt of schema.options || []) {
83
+ const value = options[opt.name];
84
+
85
+ if (value !== undefined && opt.type) {
86
+ const actualType = typeof value;
87
+ if (actualType !== opt.type) {
88
+ throw new Error(`Option '${opt.name}' must be of type ${opt.type}, got ${actualType}`);
89
+ }
90
+ }
91
+
92
+ if (value && opt.enum && !opt.enum.includes(value)) {
93
+ throw new Error(`Option '${opt.name}' must be one of: ${opt.enum.join(', ')}`);
94
+ }
95
+ }
96
+ }
97
+
98
+ #selectTemplate(schema, fileConfig, options) {
99
+ for (const [key, value] of Object.entries(options)) {
100
+ if (value === true) {
101
+ const variant = `${key}.template.js`;
102
+ if (fs.existsSync(path.join(this.templatesDir, schema.name, variant))) {
103
+ return variant;
104
+ }
105
+ }
106
+ }
107
+ return fileConfig.template;
108
+ }
109
+
110
+ #resolveOutputPath(pattern, allData) {
111
+ const template = new Template(pattern);
112
+ return path.join(process.cwd(), template.render(allData));
113
+ }
114
+
115
+ #ensureDir(dir) {
116
+ if (!fs.existsSync(dir)) {
117
+ fs.mkdirSync(dir, { recursive: true });
118
+ }
119
+ }
120
+ }
121
+
122
+ module.exports = SchematicEngine;
@@ -0,0 +1,99 @@
1
+ class Template {
2
+ constructor(content, options = {}) {
3
+ this.content = content;
4
+ this.helpers = options.helpers || {};
5
+ }
6
+
7
+ render(data) {
8
+ let result = this.content;
9
+
10
+ // Handle conditionals: {{#if key}}...{{else}}...{{/if}}
11
+ result = this.#processConditionals(result, data);
12
+
13
+ // Handle loops: {{#each key}}...{{/each}}
14
+ result = this.#processLoops(result, data);
15
+
16
+ // Handle variables: {{ name }}, {{ name | filter | filter2 }}
17
+ result = result.replace(/\{\{\s*([^}#/|]+)(\s*\|\s*([^}]+))?\s*\}\}/g,
18
+ (match, key, _, filters) => {
19
+ let value = data[key.trim()];
20
+ if (value === undefined || value === null) return '';
21
+ if (filters) value = this.#applyFilters(value, filters.trim());
22
+ return value;
23
+ }
24
+ );
25
+
26
+ return result;
27
+ }
28
+
29
+ #processConditionals(content, data) {
30
+ // Handle {{#if !key}} negation
31
+ content = content.replace(/\{\{#if\s+!(\w+)\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/if\}\}/g,
32
+ (match, key, trueBranch, falseBranch = '') => {
33
+ return !data[key] ? trueBranch : falseBranch;
34
+ }
35
+ );
36
+
37
+ // Handle {{#if key}}
38
+ content = content.replace(/\{\{#if\s+(\w+)\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/if\}\}/g,
39
+ (match, key, trueBranch, falseBranch = '') => {
40
+ return data[key] ? trueBranch : falseBranch;
41
+ }
42
+ );
43
+
44
+ return content;
45
+ }
46
+
47
+ #processLoops(content, data) {
48
+ return content.replace(/\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g,
49
+ (match, key, template) => {
50
+ const items = data[key];
51
+ if (!Array.isArray(items)) return '';
52
+ return items.map(item => {
53
+ const itemTemplate = new Template(template);
54
+ return itemTemplate.render(typeof item === 'object' ? item : { item });
55
+ }).join('');
56
+ }
57
+ );
58
+ }
59
+
60
+ #applyFilters(value, filtersStr) {
61
+ const filterNames = filtersStr.split('|').map(f => f.trim());
62
+ return filterNames.reduce((val, filterName) => this.#applyFilter(val, filterName), value);
63
+ }
64
+
65
+ #applyFilter(value, filter) {
66
+ if (value === undefined || value === null) return value;
67
+
68
+ const filters = {
69
+ pascalCase: str => String(str).replace(/(?:^|[-_])(\w)/g, (_, c) => c.toUpperCase()).replace(/[-_]/g, ''),
70
+ camelCase: str => {
71
+ const pascal = String(str).replace(/(?:^|[-_])(\w)/g, (_, c) => c.toUpperCase()).replace(/[-_]/g, '');
72
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
73
+ },
74
+ snakeCase: str => String(str).replace(/[A-Z]/g, (c, i) => (i ? '_' : '') + c.toLowerCase()).replace(/[-\s]/g, '_'),
75
+ kebabCase: str => String(str).replace(/[A-Z]/g, (c, i) => (i ? '-' : '') + c.toLowerCase()).replace(/[_\s]/g, '-'),
76
+ plural: str => {
77
+ const s = String(str);
78
+ if (s.endsWith('s')) return s;
79
+ if (s.endsWith('y')) return s.slice(0, -1) + 'ies';
80
+ return s + 's';
81
+ },
82
+ singular: str => {
83
+ const s = String(str);
84
+ if (s.endsWith('ies')) return s.slice(0, -3) + 'y';
85
+ if (s.endsWith('s')) return s.slice(0, -1);
86
+ return s;
87
+ },
88
+ lower: str => String(str).toLowerCase(),
89
+ upper: str => String(str).toUpperCase(),
90
+ };
91
+
92
+ const customFilter = this.helpers[filter];
93
+ if (customFilter) return customFilter(value);
94
+
95
+ return filters[filter]?.(value) ?? value;
96
+ }
97
+ }
98
+
99
+ module.exports = Template;
@@ -0,0 +1,7 @@
1
+ const Template = require('./Template');
2
+ const SchematicEngine = require('./SchematicEngine');
3
+
4
+ module.exports = {
5
+ Template,
6
+ SchematicEngine,
7
+ };
@@ -0,0 +1,14 @@
1
+ module.exports = `const { BaseCommand } = require('millas/src/console');
2
+
3
+ class {{ name | pascalCase }}Command extends BaseCommand {
4
+ static signature = '{{ name | kebabCase }}';
5
+ static description = '{{ name | pascalCase }} command description';
6
+
7
+ async run(args, opts) {
8
+ this.info('Running {{ name | pascalCase }}Command');
9
+ // Command logic here
10
+ }
11
+ }
12
+
13
+ module.exports = {{ name | pascalCase }}Command;
14
+ `;
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "command",
3
+ "description": "Generate a new CLI command",
4
+ "arguments": [
5
+ {
6
+ "name": "name",
7
+ "type": "string",
8
+ "required": true,
9
+ "description": "Command name"
10
+ }
11
+ ],
12
+ "options": [],
13
+ "files": [
14
+ {
15
+ "template": "default.template.js",
16
+ "output": "app/commands/{{ name | pascalCase }}Command.js"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,10 @@
1
+ module.exports = `const { Controller } = require('millas/src');
2
+
3
+ class {{ name | pascalCase }}Controller extends Controller {
4
+ async handle(req, res) {
5
+ res.json({ message: 'Hello from {{ name | pascalCase }}Controller' });
6
+ }
7
+ }
8
+
9
+ module.exports = {{ name | pascalCase }}Controller;
10
+ `;
@@ -0,0 +1,59 @@
1
+ module.exports = `const { Controller } = require('millas/src');
2
+ {{#if model}}
3
+ const {{ model | pascalCase }} = require('../models/{{ model | pascalCase }}');
4
+ {{/if}}
5
+
6
+ class {{ name | pascalCase }}Controller extends Controller {
7
+ async index(req, res) {
8
+ {{#if model}}
9
+ const items = await {{ model | pascalCase }}.all();
10
+ res.json(items);
11
+ {{else}}
12
+ res.json({ message: 'List all {{ name | plural }}' });
13
+ {{/if}}
14
+ }
15
+
16
+ async show(req, res) {
17
+ {{#if model}}
18
+ const item = await {{ model | pascalCase }}.find(req.params.id);
19
+ if (!item) return res.status(404).json({ error: '{{ model | pascalCase }} not found' });
20
+ res.json(item);
21
+ {{else}}
22
+ res.json({ message: 'Show {{ name }} with id: ' + req.params.id });
23
+ {{/if}}
24
+ }
25
+
26
+ async store(req, res) {
27
+ {{#if model}}
28
+ const item = await {{ model | pascalCase }}.create(req.body);
29
+ res.status(201).json(item);
30
+ {{else}}
31
+ res.status(201).json({ message: 'Create new {{ name }}', data: req.body });
32
+ {{/if}}
33
+ }
34
+
35
+ async update(req, res) {
36
+ {{#if model}}
37
+ const item = await {{ model | pascalCase }}.find(req.params.id);
38
+ if (!item) return res.status(404).json({ error: '{{ model | pascalCase }} not found' });
39
+ await item.update(req.body);
40
+ res.json(item);
41
+ {{else}}
42
+ res.json({ message: 'Update {{ name }} with id: ' + req.params.id, data: req.body });
43
+ {{/if}}
44
+ }
45
+
46
+ async destroy(req, res) {
47
+ {{#if model}}
48
+ const item = await {{ model | pascalCase }}.find(req.params.id);
49
+ if (!item) return res.status(404).json({ error: '{{ model | pascalCase }} not found' });
50
+ await item.delete();
51
+ res.status(204).send();
52
+ {{else}}
53
+ res.status(204).send();
54
+ {{/if}}
55
+ }
56
+ }
57
+
58
+ module.exports = {{ name | pascalCase }}Controller;
59
+ `;
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "controller",
3
+ "description": "Generate a new controller",
4
+ "arguments": [
5
+ {
6
+ "name": "name",
7
+ "type": "string",
8
+ "required": true,
9
+ "description": "Controller name"
10
+ }
11
+ ],
12
+ "options": [
13
+ {
14
+ "name": "resource",
15
+ "type": "boolean",
16
+ "description": "Generate resource controller with CRUD methods"
17
+ },
18
+ {
19
+ "name": "model",
20
+ "type": "string",
21
+ "description": "Associated model name"
22
+ }
23
+ ],
24
+ "files": [
25
+ {
26
+ "template": "default.template.js",
27
+ "output": "app/controllers/{{ name | pascalCase }}Controller.js"
28
+ }
29
+ ]
30
+ }
@@ -0,0 +1,11 @@
1
+ module.exports = `const { Job } = require('millas/src');
2
+
3
+ class {{ name | pascalCase }}Job extends Job {
4
+ async handle(data) {
5
+ // Job logic here
6
+ this.logger.info('Processing {{ name | pascalCase }}Job', data);
7
+ }
8
+ }
9
+
10
+ module.exports = {{ name | pascalCase }}Job;
11
+ `;
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "job",
3
+ "description": "Generate a new job",
4
+ "arguments": [
5
+ {
6
+ "name": "name",
7
+ "type": "string",
8
+ "required": true,
9
+ "description": "Job name"
10
+ }
11
+ ],
12
+ "options": [],
13
+ "files": [
14
+ {
15
+ "template": "default.template.js",
16
+ "output": "app/jobs/{{ name | pascalCase }}Job.js"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,11 @@
1
+ module.exports = `const { Middleware } = require('millas/src');
2
+
3
+ class {{ name | pascalCase }}Middleware extends Middleware {
4
+ async handle(req, res, next) {
5
+ // Add your middleware logic here
6
+ next();
7
+ }
8
+ }
9
+
10
+ module.exports = {{ name | pascalCase }}Middleware;
11
+ `;
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "middleware",
3
+ "description": "Generate a new middleware",
4
+ "arguments": [
5
+ {
6
+ "name": "name",
7
+ "type": "string",
8
+ "required": true,
9
+ "description": "Middleware name"
10
+ }
11
+ ],
12
+ "options": [],
13
+ "files": [
14
+ {
15
+ "template": "default.template.js",
16
+ "output": "app/middleware/{{ name | pascalCase }}Middleware.js"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,14 @@
1
+ module.exports = `const { Migration } = require('millas/src');
2
+
3
+ class {{ name | pascalCase }} extends Migration {
4
+ async up(schema) {
5
+ // Define schema changes here
6
+ }
7
+
8
+ async down(schema) {
9
+ // Reverse schema changes here
10
+ }
11
+ }
12
+
13
+ module.exports = {{ name | pascalCase }};
14
+ `;
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "migration",
3
+ "description": "Generate a new migration",
4
+ "arguments": [
5
+ {
6
+ "name": "name",
7
+ "type": "string",
8
+ "required": true,
9
+ "description": "Migration name"
10
+ }
11
+ ],
12
+ "options": [],
13
+ "files": [
14
+ {
15
+ "template": "default.template.js",
16
+ "output": "database/migrations/{{ timestamp }}_{{ name | snakeCase }}.js"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,14 @@
1
+ module.exports = `const { Model, fields } = require('millas/src');
2
+
3
+ class {{ name | pascalCase }} extends Model {
4
+ static table = '{{ name | snakeCase | plural }}';
5
+
6
+ static fields = {
7
+ id: fields.id(),
8
+ created_at: fields.timestamp(),
9
+ updated_at: fields.timestamp(),
10
+ };
11
+ }
12
+
13
+ module.exports = {{ name | pascalCase }};
14
+ `;
@@ -0,0 +1,17 @@
1
+ module.exports = `const { Migration } = require('millas/src');
2
+
3
+ class Create{{ name | pascalCase | plural }}Table extends Migration {
4
+ async up(schema) {
5
+ await schema.createTable('{{ name | snakeCase | plural }}', (table) => {
6
+ table.id();
7
+ table.timestamps();
8
+ });
9
+ }
10
+
11
+ async down(schema) {
12
+ await schema.dropTable('{{ name | snakeCase | plural }}');
13
+ }
14
+ }
15
+
16
+ module.exports = Create{{ name | pascalCase | plural }}Table;
17
+ `;
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "model",
3
+ "description": "Generate a new model",
4
+ "arguments": [
5
+ {
6
+ "name": "name",
7
+ "type": "string",
8
+ "required": true,
9
+ "description": "Model name"
10
+ }
11
+ ],
12
+ "options": [
13
+ {
14
+ "name": "migration",
15
+ "type": "boolean",
16
+ "description": "Generate migration file"
17
+ }
18
+ ],
19
+ "files": [
20
+ {
21
+ "template": "default.template.js",
22
+ "output": "app/models/{{ name | pascalCase }}.js"
23
+ },
24
+ {
25
+ "template": "migration.template.js",
26
+ "output": "database/migrations/{{ timestamp }}_create_{{ name | snakeCase | plural }}_table.js",
27
+ "condition": "migration"
28
+ }
29
+ ]
30
+ }
@@ -0,0 +1,12 @@
1
+ module.exports = `class {{ name | pascalCase }}Service {
2
+ constructor() {
3
+ // Initialize service
4
+ }
5
+
6
+ async execute() {
7
+ // Service logic here
8
+ }
9
+ }
10
+
11
+ module.exports = {{ name | pascalCase }}Service;
12
+ `;
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "service",
3
+ "description": "Generate a new service",
4
+ "arguments": [
5
+ {
6
+ "name": "name",
7
+ "type": "string",
8
+ "required": true,
9
+ "description": "Service name"
10
+ }
11
+ ],
12
+ "options": [],
13
+ "files": [
14
+ {
15
+ "template": "default.template.js",
16
+ "output": "app/services/{{ name | pascalCase }}Service.js"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,11 @@
1
+ module.exports = `const { Shape } = require('millas/src');
2
+
3
+ class {{ name | pascalCase }}Shape extends Shape {
4
+ static rules = {
5
+ // Define validation rules here
6
+ // Example: name: v => v.string().required(),
7
+ };
8
+ }
9
+
10
+ module.exports = {{ name | pascalCase }}Shape;
11
+ `;
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "shape",
3
+ "description": "Generate a new validation shape",
4
+ "arguments": [
5
+ {
6
+ "name": "name",
7
+ "type": "string",
8
+ "required": true,
9
+ "description": "Shape name"
10
+ }
11
+ ],
12
+ "options": [],
13
+ "files": [
14
+ {
15
+ "template": "default.template.js",
16
+ "output": "app/shapes/{{ name | pascalCase }}Shape.js"
17
+ }
18
+ ]
19
+ }
@@ -14,6 +14,7 @@ class BaseValidator {
14
14
  * @param {string} [typeError] — message shown when the value fails the base type check
15
15
  */
16
16
  constructor(typeError) {
17
+ this._type = undefined;
17
18
  this._typeError = typeError || null;
18
19
  this._required = false;
19
20
  this._requiredMsg = null;
@@ -162,6 +163,8 @@ class BaseValidator {
162
163
  */
163
164
  async run(value, key, allData = {}) {
164
165
  const label = this._fieldLabel(key);
166
+ if (this._type === undefined || this._type === null)
167
+ throw new Error(`[Millas] Validator for "${key}" has no known type.`);
165
168
 
166
169
  // ── Apply default ───────────────────────────────────────────────────────
167
170
  if (this._isEmpty(value) && this._defaultValue !== undefined) {
@@ -39,8 +39,8 @@ const { BaseValidator, _titleCase } = require('./BaseValidator');
39
39
  // ── StringValidator ────────────────────────────────────────────────────────────
40
40
 
41
41
  class StringValidator extends BaseValidator {
42
- constructor() {
43
- super('Must be a string');
42
+ constructor(message) {
43
+ super(message || 'Must be a string');
44
44
  this._type = 'string';
45
45
  this._minLen = null;
46
46
  this._maxLen = null;
@@ -479,7 +479,7 @@ function _parseSize(str) {
479
479
 
480
480
  // ── Factory functions ─────────────────────────────────────────────────────────
481
481
 
482
- const string = () => new StringValidator();
482
+ const string = (message) => new StringValidator(message);
483
483
  const email = () => new EmailValidator();
484
484
  const number = () => new NumberValidator();
485
485
  const boolean = () => new BooleanValidator();