@aruvili/core 0.1.0

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.
@@ -0,0 +1,61 @@
1
+ export type FieldType = 'Select' | 'Text' | 'Small Text' | 'Long Text' | 'Int' | 'Float' | 'Check' | 'Date' | 'Datetime' | 'Link' | 'Currency' | 'Table' | 'Table MultiSelect';
2
+ export interface DocField {
3
+ fieldname: string;
4
+ label: string;
5
+ fieldtype: FieldType;
6
+ required?: boolean;
7
+ unique?: boolean;
8
+ options?: string;
9
+ default?: any;
10
+ searchable?: boolean;
11
+ read_only?: boolean;
12
+ hidden?: boolean;
13
+ max_length?: number;
14
+ min_value?: number;
15
+ max_value?: number;
16
+ }
17
+ export interface PermissionRule {
18
+ role: string;
19
+ create: boolean;
20
+ read: boolean;
21
+ update: boolean;
22
+ delete: boolean;
23
+ submit?: boolean;
24
+ cancel?: boolean;
25
+ }
26
+ export type NamingRule = 'Autoincrement' | 'UUID' | 'Expression' | 'NamingSeries';
27
+ export interface WorkflowTransition {
28
+ state: string;
29
+ action: string;
30
+ next_state: string;
31
+ allowed_roles: string[];
32
+ }
33
+ export interface DocTypeWorkflow {
34
+ fieldname: string;
35
+ initial_state: string;
36
+ transitions: WorkflowTransition[];
37
+ }
38
+ export interface DocTypeDefinition {
39
+ name: string;
40
+ istable?: boolean;
41
+ is_submittable?: boolean;
42
+ naming_rule?: NamingRule;
43
+ naming_series?: string;
44
+ title_field?: string;
45
+ track_changes?: boolean;
46
+ max_attachments?: number;
47
+ fields: DocField[];
48
+ permissions: PermissionRule[];
49
+ workflow?: DocTypeWorkflow;
50
+ }
51
+ export interface DatabaseColumnMeta {
52
+ columnName: string;
53
+ dataType: string;
54
+ isNullable: boolean;
55
+ characterMaximumLength: number | null;
56
+ }
57
+ /**
58
+ * Reserved SQL keywords that cannot be used as table or column identifiers.
59
+ */
60
+ export declare const SQL_RESERVED_WORDS: Set<string>;
61
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GACjB,QAAQ,GACR,MAAM,GACN,YAAY,GACZ,WAAW,GACX,KAAK,GACL,OAAO,GACP,OAAO,GACP,MAAM,GACN,UAAU,GACV,MAAM,GACN,UAAU,GACV,OAAO,GACP,mBAAmB,CAAC;AAExB,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,SAAS,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,MAAM,UAAU,GAAG,eAAe,GAAG,MAAM,GAAG,YAAY,GAAG,cAAc,CAAC;AAElF,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,kBAAkB,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,WAAW,CAAC,EAAE,UAAU,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,QAAQ,EAAE,CAAC;IACnB,WAAW,EAAE,cAAc,EAAE,CAAC;IAC9B,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAC;CACvC;AAED;;GAEG;AACH,eAAO,MAAM,kBAAkB,aAW7B,CAAC"}
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SQL_RESERVED_WORDS = void 0;
4
+ /**
5
+ * Reserved SQL keywords that cannot be used as table or column identifiers.
6
+ */
7
+ exports.SQL_RESERVED_WORDS = new Set([
8
+ 'select', 'insert', 'update', 'delete', 'drop', 'alter', 'create',
9
+ 'table', 'index', 'from', 'where', 'join', 'order', 'group', 'having',
10
+ 'limit', 'offset', 'union', 'all', 'and', 'or', 'not', 'null', 'true',
11
+ 'false', 'primary', 'key', 'foreign', 'references', 'constraint',
12
+ 'default', 'check', 'unique', 'cascade', 'set', 'into', 'values',
13
+ 'returning', 'exists', 'between', 'like', 'in', 'is', 'as', 'on',
14
+ 'begin', 'commit', 'rollback', 'grant', 'revoke', 'user', 'role',
15
+ 'schema', 'database', 'trigger', 'function', 'procedure', 'view',
16
+ 'sequence', 'serial', 'bigserial', 'text', 'integer', 'boolean',
17
+ 'varchar', 'timestamp', 'date', 'numeric', 'uuid'
18
+ ]);
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":";;;AA6EA;;GAEG;AACU,QAAA,kBAAkB,GAAG,IAAI,GAAG,CAAC;IACxC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ;IACjE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ;IACrE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM;IACrE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY;IAChE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ;IAChE,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI;IAChE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM;IAChE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM;IAChE,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS;IAC/D,SAAS,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;CAClD,CAAC,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { DocTypeDefinition } from './types/index.js';
2
+ /**
3
+ * Validates a DocTypeDefinition schema structure for database safety and logical compliance.
4
+ * Enterprise-grade: checks SQL injection vectors, PostgreSQL limits, and structural integrity.
5
+ */
6
+ export declare function validateDocType(definition: DocTypeDefinition): {
7
+ valid: boolean;
8
+ errors: string[];
9
+ };
10
+ //# sourceMappingURL=validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAsB,MAAM,kBAAkB,CAAC;AAsBzE;;;GAGG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,iBAAiB,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CA+InG"}
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateDocType = validateDocType;
4
+ const index_js_1 = require("./types/index.js");
5
+ const RESERVED_FIELD_NAMES = new Set([
6
+ 'name',
7
+ 'uuid',
8
+ 'created_at',
9
+ 'updated_at',
10
+ 'created_by',
11
+ 'modified_by',
12
+ 'docstatus',
13
+ 'parent',
14
+ 'parenttype',
15
+ 'parentfield',
16
+ 'idx'
17
+ ]);
18
+ const VALID_FIELDNAME_REGEX = /^[a-z][a-z0-9_]*$/;
19
+ const VALID_DOCTYPE_NAME_REGEX = /^[A-Za-z][A-Za-z0-9 _-]*$/;
20
+ const MAX_DOCTYPE_NAME_LENGTH = 100;
21
+ const MAX_FIELD_COUNT = 200;
22
+ const MAX_FIELDNAME_LENGTH = 63; // PostgreSQL identifier limit
23
+ /**
24
+ * Validates a DocTypeDefinition schema structure for database safety and logical compliance.
25
+ * Enterprise-grade: checks SQL injection vectors, PostgreSQL limits, and structural integrity.
26
+ */
27
+ function validateDocType(definition) {
28
+ const errors = [];
29
+ // === DocType Name Validations ===
30
+ if (!definition.name || definition.name.trim() === '') {
31
+ errors.push('DocType name is required.');
32
+ }
33
+ else {
34
+ if (definition.name.length > MAX_DOCTYPE_NAME_LENGTH) {
35
+ errors.push(`DocType name '${definition.name}' exceeds maximum length of ${MAX_DOCTYPE_NAME_LENGTH} characters.`);
36
+ }
37
+ if (!VALID_DOCTYPE_NAME_REGEX.test(definition.name)) {
38
+ errors.push(`DocType name '${definition.name}' contains invalid characters. Allowed: letters, digits, spaces, hyphens, underscores.`);
39
+ }
40
+ // Block SQL reserved words as DocType names (prevents injection via table names)
41
+ if (index_js_1.SQL_RESERVED_WORDS.has(definition.name.toLowerCase().trim())) {
42
+ errors.push(`DocType name '${definition.name}' is a reserved SQL keyword and cannot be used.`);
43
+ }
44
+ }
45
+ // === Fields Array ===
46
+ if (!definition.fields || !Array.isArray(definition.fields)) {
47
+ errors.push('DocType must contain an array of fields.');
48
+ return { valid: false, errors };
49
+ }
50
+ if (definition.fields.length > MAX_FIELD_COUNT) {
51
+ errors.push(`DocType '${definition.name}' exceeds maximum of ${MAX_FIELD_COUNT} fields.`);
52
+ }
53
+ const seenFields = new Set();
54
+ for (const field of definition.fields) {
55
+ const { fieldname, fieldtype } = field;
56
+ // --- Fieldname presence ---
57
+ if (!fieldname || fieldname.trim() === '') {
58
+ errors.push(`Fieldname is missing for a field in ${definition.name}.`);
59
+ continue;
60
+ }
61
+ // --- Fieldname format ---
62
+ if (!VALID_FIELDNAME_REGEX.test(fieldname)) {
63
+ errors.push(`Fieldname '${fieldname}' in ${definition.name} is invalid. It must be snake_case, start with a lowercase letter, and contain only lowercase letters, digits, and underscores.`);
64
+ }
65
+ // --- Fieldname length (PostgreSQL limit) ---
66
+ if (fieldname.length > MAX_FIELDNAME_LENGTH) {
67
+ errors.push(`Fieldname '${fieldname}' exceeds PostgreSQL identifier limit of ${MAX_FIELDNAME_LENGTH} characters.`);
68
+ }
69
+ // --- Reserved names ---
70
+ if (RESERVED_FIELD_NAMES.has(fieldname)) {
71
+ errors.push(`Fieldname '${fieldname}' in ${definition.name} is reserved by the framework database architecture.`);
72
+ }
73
+ // --- SQL reserved words as column names ---
74
+ if (index_js_1.SQL_RESERVED_WORDS.has(fieldname.toLowerCase())) {
75
+ errors.push(`Fieldname '${fieldname}' in ${definition.name} is a reserved SQL keyword and cannot be used as a column name.`);
76
+ }
77
+ // --- Duplicates ---
78
+ if (seenFields.has(fieldname)) {
79
+ errors.push(`Duplicate fieldname '${fieldname}' declared in ${definition.name}.`);
80
+ }
81
+ seenFields.add(fieldname);
82
+ // --- Fieldtype presence ---
83
+ if (!fieldtype) {
84
+ errors.push(`Field '${fieldname}' is missing fieldtype in ${definition.name}.`);
85
+ }
86
+ // === Specific field validations ===
87
+ if (fieldtype === 'Select') {
88
+ if (!field.options || field.options.trim() === '') {
89
+ errors.push(`Select field '${fieldname}' in ${definition.name} requires options (comma-separated list).`);
90
+ }
91
+ }
92
+ if (fieldtype === 'Link' || fieldtype === 'Table') {
93
+ if (!field.options || field.options.trim() === '') {
94
+ errors.push(`Relationship field '${fieldname}' of type '${fieldtype}' in ${definition.name} requires target option.`);
95
+ }
96
+ }
97
+ // --- Numeric constraints validation ---
98
+ if (field.min_value !== undefined && field.max_value !== undefined) {
99
+ if (field.min_value > field.max_value) {
100
+ errors.push(`Field '${fieldname}' in ${definition.name}: min_value (${field.min_value}) cannot exceed max_value (${field.max_value}).`);
101
+ }
102
+ }
103
+ // --- max_length validation ---
104
+ if (field.max_length !== undefined) {
105
+ if (field.max_length <= 0 || field.max_length > 10485760) {
106
+ errors.push(`Field '${fieldname}' in ${definition.name}: max_length must be between 1 and 10485760.`);
107
+ }
108
+ }
109
+ }
110
+ // === Validate naming configuration ===
111
+ if (!definition.istable) {
112
+ const rule = definition.naming_rule || 'UUID';
113
+ if (rule === 'NamingSeries' && (!definition.naming_series || definition.naming_series.trim() === '')) {
114
+ errors.push(`DocType '${definition.name}' requires naming_series configuration if NamingSeries rule is selected.`);
115
+ }
116
+ }
117
+ // === Validate permissions structure ===
118
+ if (!definition.istable && (!definition.permissions || definition.permissions.length === 0)) {
119
+ errors.push(`DocType '${definition.name}' must define at least one permission rule (non-child tables require explicit access control).`);
120
+ }
121
+ if (definition.permissions) {
122
+ for (const perm of definition.permissions) {
123
+ if (!perm.role || perm.role.trim() === '') {
124
+ errors.push(`A permission rule in ${definition.name} is missing a role name.`);
125
+ }
126
+ }
127
+ }
128
+ // Validate submittable config
129
+ if (definition.is_submittable && definition.istable) {
130
+ errors.push(`DocType '${definition.name}' cannot be both a child table (istable) and submittable.`);
131
+ }
132
+ // Validate title_field references an existing field
133
+ if (definition.title_field) {
134
+ const fieldExists = definition.fields.some(f => f.fieldname === definition.title_field);
135
+ if (!fieldExists) {
136
+ errors.push(`title_field '${definition.title_field}' in ${definition.name} does not reference an existing field.`);
137
+ }
138
+ }
139
+ return {
140
+ valid: errors.length === 0,
141
+ errors
142
+ };
143
+ }
144
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@aruvili/core",
3
+ "version": "0.1.0",
4
+ "description": "Framework-agnostic DocType and DDL core engine",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "bin": {
8
+ "aruvili-meta": "./dist/cli.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "test": "bun test"
13
+ },
14
+ "dependencies": {
15
+ "zod": "^3.23.8"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^20.0.0",
19
+ "typescript": "~6.0.2"
20
+ }
21
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { validateDocType } from './validation.js';
5
+ import { generateCreateTableDDL } from './ddl.js';
6
+
7
+ const [, , command, ...args] = process.argv;
8
+
9
+ if (!command || command === 'help') {
10
+ printHelp();
11
+ process.exit(0);
12
+ }
13
+
14
+ switch (command) {
15
+ case 'init':
16
+ handleInit(args[0]);
17
+ break;
18
+ case 'validate':
19
+ handleValidate(args[0]);
20
+ break;
21
+ case 'ddl':
22
+ handleDdl(args[0]);
23
+ break;
24
+ default:
25
+ console.error(`Unknown command: ${command}`);
26
+ printHelp();
27
+ process.exit(1);
28
+ }
29
+
30
+ function printHelp() {
31
+ console.log(`
32
+ Aruvili Meta-Framework CLI
33
+ Usage: aruvili-meta <command> [args]
34
+
35
+ Commands:
36
+ init <name> Scaffold a new DocType definition template
37
+ validate <path> Validate a DocType JSON schema file
38
+ ddl <path> Generate PostgreSQL CREATE TABLE DDL from JSON schema
39
+ help Print this usage information
40
+ `);
41
+ }
42
+
43
+ function handleInit(name: string) {
44
+ if (!name) {
45
+ console.error('Error: DocType name is required.');
46
+ process.exit(1);
47
+ }
48
+ const filename = `${name.toLowerCase().replace(/[^a-z0-9_]/g, '_')}.json`;
49
+ const template = {
50
+ name,
51
+ is_submittable: false,
52
+ istable: false,
53
+ fields: [
54
+ { fieldname: 'title', label: 'Title', fieldtype: 'Text', required: true }
55
+ ],
56
+ permissions: [
57
+ { role: 'System Manager', read: true, write: true, create: true, delete: true }
58
+ ]
59
+ };
60
+
61
+ fs.writeFileSync(path.resolve(process.cwd(), filename), JSON.stringify(template, null, 2));
62
+ console.log(`Scaffolded template DocType schema: ${filename}`);
63
+ }
64
+
65
+ function handleValidate(filePath: string) {
66
+ if (!filePath) {
67
+ console.error('Error: Path to DocType JSON file is required.');
68
+ process.exit(1);
69
+ }
70
+ try {
71
+ const data = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), filePath), 'utf-8'));
72
+ const result = validateDocType(data);
73
+ if (result.valid) {
74
+ console.log(`✅ DocType '${data.name}' is valid.`);
75
+ } else {
76
+ console.error(`❌ Validation errors found in ${filePath}:`);
77
+ result.errors.forEach(err => console.error(` - ${err}`));
78
+ process.exit(1);
79
+ }
80
+ } catch (err: any) {
81
+ console.error(`Error: Failed to validate. ${err.message}`);
82
+ process.exit(1);
83
+ }
84
+ }
85
+
86
+ function handleDdl(filePath: string) {
87
+ if (!filePath) {
88
+ console.error('Error: Path to DocType JSON file is required.');
89
+ process.exit(1);
90
+ }
91
+ try {
92
+ const data = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), filePath), 'utf-8'));
93
+ const result = validateDocType(data);
94
+ if (!result.valid) {
95
+ console.error(`❌ Cannot generate DDL. DocType schema has validation errors:`);
96
+ result.errors.forEach(err => console.error(` - ${err}`));
97
+ process.exit(1);
98
+ }
99
+ const ddl = generateCreateTableDDL(data);
100
+ console.log(ddl.join('\n'));
101
+ } catch (err: any) {
102
+ console.error(`Error: Failed to generate DDL. ${err.message}`);
103
+ process.exit(1);
104
+ }
105
+ }
@@ -0,0 +1,236 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { DocTypeDefinition } from './types/index.js';
3
+ import { validateDocType } from './validation.js';
4
+ import { generateCreateTableDDL, generateAlterTableDDL, getTableName } from './ddl.js';
5
+
6
+ describe('DocType validation', () => {
7
+ it('should validate a correct DocType definition', () => {
8
+ const doc: DocTypeDefinition = {
9
+ name: 'Task',
10
+ fields: [
11
+ { fieldname: 'title', label: 'Title', fieldtype: 'Text', required: true },
12
+ { fieldname: 'status', label: 'Status', fieldtype: 'Select', options: 'Open,Closed', default: 'Open' }
13
+ ],
14
+ permissions: [{ role: 'System Manager', create: true, read: true, update: true, delete: true }]
15
+ };
16
+ const result = validateDocType(doc);
17
+ expect(result.valid).toBe(true);
18
+ expect(result.errors.length).toBe(0);
19
+ });
20
+
21
+ it('should block reserved field names', () => {
22
+ const doc: DocTypeDefinition = {
23
+ name: 'Task',
24
+ fields: [{ fieldname: 'docstatus', label: 'Doc Status', fieldtype: 'Int' }],
25
+ permissions: [{ role: 'System Manager', create: true, read: true, update: true, delete: true }]
26
+ };
27
+ const result = validateDocType(doc);
28
+ expect(result.valid).toBe(false);
29
+ expect(result.errors.join('')).toContain('reserved');
30
+ });
31
+
32
+ it('should block uuid as a field name', () => {
33
+ const doc: DocTypeDefinition = {
34
+ name: 'Task',
35
+ fields: [{ fieldname: 'uuid', label: 'UUID', fieldtype: 'Text' }],
36
+ permissions: [{ role: 'System Manager', create: true, read: true, update: true, delete: true }]
37
+ };
38
+ const result = validateDocType(doc);
39
+ expect(result.valid).toBe(false);
40
+ expect(result.errors.join('')).toContain('reserved');
41
+ });
42
+
43
+ it('should validate fieldname naming conventions', () => {
44
+ const doc: DocTypeDefinition = {
45
+ name: 'Task',
46
+ fields: [{ fieldname: 'Title Field', label: 'Title', fieldtype: 'Text' }],
47
+ permissions: [{ role: 'System Manager', create: true, read: true, update: true, delete: true }]
48
+ };
49
+ const result = validateDocType(doc);
50
+ expect(result.valid).toBe(false);
51
+ expect(result.errors.join('')).toContain('is invalid');
52
+ });
53
+
54
+ it('should block Select type fields without options', () => {
55
+ const doc: DocTypeDefinition = {
56
+ name: 'Task',
57
+ fields: [{ fieldname: 'status', label: 'Status', fieldtype: 'Select' }],
58
+ permissions: [{ role: 'System Manager', create: true, read: true, update: true, delete: true }]
59
+ };
60
+ const result = validateDocType(doc);
61
+ expect(result.valid).toBe(false);
62
+ expect(result.errors.join('')).toContain('requires options');
63
+ });
64
+
65
+ it('should block SQL reserved words as DocType names', () => {
66
+ const doc: DocTypeDefinition = {
67
+ name: 'Select',
68
+ fields: [{ fieldname: 'title', label: 'Title', fieldtype: 'Text' }],
69
+ permissions: [{ role: 'System Manager', create: true, read: true, update: true, delete: true }]
70
+ };
71
+ const result = validateDocType(doc);
72
+ expect(result.valid).toBe(false);
73
+ expect(result.errors.join('')).toContain('reserved SQL keyword');
74
+ });
75
+
76
+ it('should block SQL reserved words as field names', () => {
77
+ const doc: DocTypeDefinition = {
78
+ name: 'Task',
79
+ fields: [{ fieldname: 'select', label: 'Select', fieldtype: 'Text' }],
80
+ permissions: [{ role: 'System Manager', create: true, read: true, update: true, delete: true }]
81
+ };
82
+ const result = validateDocType(doc);
83
+ expect(result.valid).toBe(false);
84
+ });
85
+
86
+ it('should require permissions on non-child DocTypes', () => {
87
+ const doc: DocTypeDefinition = {
88
+ name: 'Task',
89
+ fields: [{ fieldname: 'title', label: 'Title', fieldtype: 'Text' }],
90
+ permissions: []
91
+ };
92
+ const result = validateDocType(doc);
93
+ expect(result.valid).toBe(false);
94
+ expect(result.errors.join('')).toContain('at least one permission');
95
+ });
96
+
97
+ it('should allow child tables without permissions', () => {
98
+ const doc: DocTypeDefinition = {
99
+ name: 'Task Item',
100
+ istable: true,
101
+ fields: [{ fieldname: 'item_name', label: 'Item', fieldtype: 'Text' }],
102
+ permissions: []
103
+ };
104
+ const result = validateDocType(doc);
105
+ expect(result.valid).toBe(true);
106
+ });
107
+
108
+ it('should block submittable child tables', () => {
109
+ const doc: DocTypeDefinition = {
110
+ name: 'Bad Child',
111
+ istable: true,
112
+ is_submittable: true,
113
+ fields: [{ fieldname: 'item_name', label: 'Item', fieldtype: 'Text' }],
114
+ permissions: []
115
+ };
116
+ const result = validateDocType(doc);
117
+ expect(result.valid).toBe(false);
118
+ expect(result.errors.join('')).toContain('cannot be both');
119
+ });
120
+
121
+ it('should validate title_field references existing field', () => {
122
+ const doc: DocTypeDefinition = {
123
+ name: 'Task',
124
+ title_field: 'nonexistent',
125
+ fields: [{ fieldname: 'title', label: 'Title', fieldtype: 'Text' }],
126
+ permissions: [{ role: 'System Manager', create: true, read: true, update: true, delete: true }]
127
+ };
128
+ const result = validateDocType(doc);
129
+ expect(result.valid).toBe(false);
130
+ expect(result.errors.join('')).toContain('does not reference');
131
+ });
132
+ });
133
+
134
+ describe('SQL DDL Code Generation', () => {
135
+ it('should generate a valid CREATE TABLE with uuid column', () => {
136
+ const doc: DocTypeDefinition = {
137
+ name: 'Task',
138
+ fields: [
139
+ { fieldname: 'title', label: 'Title', fieldtype: 'Text', required: true },
140
+ { fieldname: 'description', label: 'Description', fieldtype: 'Long Text' },
141
+ { fieldname: 'project', label: 'Project', fieldtype: 'Link', options: 'Project' }
142
+ ],
143
+ permissions: [{ role: 'System Manager', create: true, read: true, update: true, delete: true }]
144
+ };
145
+ const statements = generateCreateTableDDL(doc);
146
+ const sql = statements.join('\n');
147
+
148
+ expect(getTableName(doc.name)).toBe('dt_task');
149
+ expect(sql).toContain('CREATE TABLE dt_task');
150
+ expect(sql).toContain('name VARCHAR(255) PRIMARY KEY');
151
+ expect(sql).toContain('uuid UUID DEFAULT gen_random_uuid() UNIQUE NOT NULL');
152
+ expect(sql).toContain('title VARCHAR(255) NOT NULL');
153
+ expect(sql).toContain('description TEXT');
154
+ expect(sql).toContain('project VARCHAR(255)');
155
+ // Verify automatic indexes
156
+ expect(sql).toContain('idx_dt_task_docstatus');
157
+ expect(sql).toContain('idx_dt_task_created_at');
158
+ expect(sql).toContain('idx_dt_task_project');
159
+ });
160
+
161
+ it('should generate child table columns', () => {
162
+ const doc: DocTypeDefinition = {
163
+ name: 'Invoice Item',
164
+ istable: true,
165
+ fields: [{ fieldname: 'item_code', label: 'Item Code', fieldtype: 'Text', required: true }],
166
+ permissions: []
167
+ };
168
+ const statements = generateCreateTableDDL(doc);
169
+ const sql = statements.join('\n');
170
+
171
+ expect(sql).toContain('uuid UUID DEFAULT gen_random_uuid() UNIQUE NOT NULL');
172
+ expect(sql).toContain('parent VARCHAR(255) NOT NULL');
173
+ expect(sql).toContain('parenttype VARCHAR(255) NOT NULL');
174
+ expect(sql).toContain('parentfield VARCHAR(255) NOT NULL');
175
+ expect(sql).toContain('idx INTEGER NOT NULL');
176
+ expect(sql).toContain('idx_dt_invoice_item_parent');
177
+ });
178
+
179
+ it('should calculate additive migrations with uuid backfill', () => {
180
+ const doc: DocTypeDefinition = {
181
+ name: 'Task',
182
+ fields: [
183
+ { fieldname: 'title', label: 'Title', fieldtype: 'Text' },
184
+ { fieldname: 'priority', label: 'Priority', fieldtype: 'Select', options: 'Low,Medium,High', default: 'Medium', searchable: true }
185
+ ],
186
+ permissions: [{ role: 'System Manager', create: true, read: true, update: true, delete: true }]
187
+ };
188
+
189
+ // uuid missing from existing DB
190
+ const existing = [
191
+ { columnName: 'name', dataType: 'varchar', isNullable: false, characterMaximumLength: 255 },
192
+ { columnName: 'title', dataType: 'varchar', isNullable: true, characterMaximumLength: 255 }
193
+ ];
194
+
195
+ const statements = generateAlterTableDDL(doc, existing);
196
+ expect(statements.length).toBe(3); // uuid + priority column + priority index
197
+ expect(statements[0]).toContain('ADD COLUMN uuid UUID');
198
+
199
+ // uuid already exists
200
+ const existingWithUuid = [
201
+ { columnName: 'uuid', dataType: 'uuid', isNullable: false, characterMaximumLength: null },
202
+ { columnName: 'name', dataType: 'varchar', isNullable: false, characterMaximumLength: 255 },
203
+ { columnName: 'title', dataType: 'varchar', isNullable: true, characterMaximumLength: 255 }
204
+ ];
205
+
206
+ const statements2 = generateAlterTableDDL(doc, existingWithUuid);
207
+ expect(statements2.length).toBe(2);
208
+ expect(statements2[0]).toContain('ADD COLUMN priority');
209
+ });
210
+
211
+ it('should generate workflow column and index DDL when workflow is configured', () => {
212
+ const doc: DocTypeDefinition = {
213
+ name: 'Task',
214
+ fields: [
215
+ { fieldname: 'title', label: 'Title', fieldtype: 'Text', required: true }
216
+ ],
217
+ permissions: [{ role: 'System Manager', create: true, read: true, update: true, delete: true }],
218
+ workflow: {
219
+ fieldname: 'workflow_state',
220
+ initial_state: 'Draft',
221
+ transitions: [
222
+ { state: 'Draft', action: 'Submit', next_state: 'Approved', allowed_roles: ['System Manager'] }
223
+ ]
224
+ }
225
+ };
226
+
227
+ const ddl = generateCreateTableDDL(doc);
228
+ const tableStmt = ddl.find(s => s.startsWith('CREATE TABLE'));
229
+ expect(tableStmt).toBeDefined();
230
+ expect(tableStmt).toContain('workflow_state VARCHAR(255) DEFAULT \'Draft\'');
231
+
232
+ const indexStmt = ddl.find(s => s.includes('idx_dt_task_workflow_state'));
233
+ expect(indexStmt).toBeDefined();
234
+ });
235
+ });
236
+