@stonecrop/utilities 0.11.0 → 0.11.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stonecrop/utilities",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "author": {
@@ -1,135 +0,0 @@
1
- /**
2
- * Naming Convention Utilities
3
- * Converts between SQL snake_case and JavaScript camelCase naming conventions
4
- * @packageDocumentation
5
- */
6
- /**
7
- * Converts snake_case to camelCase
8
- * @param snakeCase - Snake case string
9
- * @returns Camel case string
10
- * @public
11
- * @example
12
- * ```typescript
13
- * snakeToCamel('user_email') // 'userEmail'
14
- * snakeToCamel('created_at') // 'createdAt'
15
- * snakeToCamel('user_id') // 'userId'
16
- * ```
17
- */
18
- export function snakeToCamel(snakeCase) {
19
- return snakeCase.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
20
- }
21
- /**
22
- * Converts camelCase to snake_case
23
- * @param camelCase - Camel case string
24
- * @returns Snake case string
25
- * @public
26
- * @example
27
- * ```typescript
28
- * camelToSnake('userEmail') // 'user_email'
29
- * camelToSnake('createdAt') // 'created_at'
30
- * camelToSnake('userId') // 'user_id'
31
- * ```
32
- */
33
- export function camelToSnake(camelCase) {
34
- return camelCase.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
35
- }
36
- /**
37
- * Converts snake_case to Title Case label
38
- * @param snakeCase - Snake case string
39
- * @returns Title case label
40
- * @public
41
- * @example
42
- * ```typescript
43
- * snakeToLabel('user_email') // 'User Email'
44
- * snakeToLabel('first_name') // 'First Name'
45
- * snakeToLabel('created_at') // 'Created At'
46
- * ```
47
- */
48
- export function snakeToLabel(snakeCase) {
49
- return snakeCase
50
- .split('_')
51
- .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
52
- .join(' ');
53
- }
54
- /**
55
- * Converts camelCase to Title Case label
56
- * @param camelCase - Camel case string
57
- * @returns Title case label
58
- * @public
59
- * @example
60
- * ```typescript
61
- * camelToLabel('userEmail') // 'User Email'
62
- * camelToLabel('firstName') // 'First Name'
63
- * camelToLabel('createdAt') // 'Created At'
64
- * ```
65
- */
66
- export function camelToLabel(camelCase) {
67
- // Insert space before capital letters and capitalize first letter
68
- const withSpaces = camelCase.replace(/([A-Z])/g, ' $1').trim();
69
- return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1);
70
- }
71
- /**
72
- * Converts SQL column name to Stonecrop field naming convention
73
- * Handles special cases like ID suffixes
74
- * @param sqlName - SQL column name (snake_case)
75
- * @returns Field name and label
76
- * @public
77
- * @example
78
- * ```typescript
79
- * convertSQLName('user_email')
80
- * // { fieldname: 'userEmail', label: 'User Email', originalName: 'user_email' }
81
- *
82
- * convertSQLName('user_id')
83
- * // { fieldname: 'userId', label: 'User', originalName: 'user_id' }
84
- *
85
- * convertSQLName('created_at')
86
- * // { fieldname: 'createdAt', label: 'Created At', originalName: 'created_at' }
87
- * ```
88
- */
89
- export function convertSQLName(sqlName) {
90
- const fieldname = snakeToCamel(sqlName);
91
- // Generate label - special handling for _id suffix
92
- let label;
93
- if (sqlName.endsWith('_id')) {
94
- // Remove _id suffix for label: user_id -> User
95
- const withoutId = sqlName.slice(0, -3);
96
- label = snakeToLabel(withoutId);
97
- }
98
- else {
99
- label = snakeToLabel(sqlName);
100
- }
101
- return {
102
- fieldname,
103
- label,
104
- originalName: sqlName,
105
- };
106
- }
107
- /**
108
- * Batch converts multiple SQL column names
109
- * @param sqlNames - Array of SQL column names
110
- * @returns Array of name conversions
111
- * @public
112
- */
113
- export function convertSQLNames(sqlNames) {
114
- return sqlNames.map(convertSQLName);
115
- }
116
- /**
117
- * Creates a bidirectional mapping between SQL and Stonecrop names
118
- * @param sqlNames - Array of SQL column names
119
- * @returns Mapping object with both directions
120
- * @public
121
- */
122
- export function createNameMapping(sqlNames) {
123
- const conversions = convertSQLNames(sqlNames);
124
- const sqlToFieldname = new Map();
125
- const fieldnameToSQL = new Map();
126
- for (const conversion of conversions) {
127
- sqlToFieldname.set(conversion.originalName, conversion.fieldname);
128
- fieldnameToSQL.set(conversion.fieldname, conversion.originalName);
129
- }
130
- return {
131
- sqlToFieldname,
132
- fieldnameToSQL,
133
- conversions,
134
- };
135
- }
@@ -1,260 +0,0 @@
1
- /**
2
- * SQL Schema Introspection Utilities
3
- * Parses PostgreSQL DDL and converts to Stonecrop schema format
4
- * @packageDocumentation
5
- */
6
- /**
7
- * Maps PostgreSQL data types to Stonecrop field types
8
- * @param sqlType - PostgreSQL data type
9
- * @param enumValues - Enum values if type is ENUM
10
- * @returns Corresponding Stonecrop field type
11
- * @public
12
- */
13
- export function mapSQLTypeToFieldType(sqlType, enumValues) {
14
- const normalizedType = sqlType.toLowerCase().trim();
15
- // Handle parameterized types (e.g., varchar(255) -> varchar)
16
- const baseType = normalizedType.split('(')[0].trim();
17
- // Enum types become Select fields
18
- if (enumValues && enumValues.length > 0) {
19
- return 'Select';
20
- }
21
- // Type mapping based on PostgreSQL types
22
- const typeMap = {
23
- // Text types
24
- varchar: 'Data',
25
- character: 'Data',
26
- char: 'Data',
27
- text: 'Text',
28
- // Numeric types
29
- integer: 'Int',
30
- int: 'Int',
31
- int2: 'Int',
32
- int4: 'Int',
33
- int8: 'Int',
34
- smallint: 'Int',
35
- bigint: 'Int',
36
- serial: 'Int',
37
- bigserial: 'Int',
38
- // Float types
39
- numeric: 'Float',
40
- decimal: 'Float',
41
- real: 'Float',
42
- float: 'Float',
43
- float4: 'Float',
44
- float8: 'Float',
45
- double: 'Float',
46
- money: 'Currency',
47
- // Boolean
48
- boolean: 'Check',
49
- bool: 'Check',
50
- // Date/Time types
51
- timestamp: 'Datetime',
52
- timestamptz: 'Datetime',
53
- date: 'Date',
54
- time: 'Time',
55
- timetz: 'Time',
56
- // JSON types (use Code for now)
57
- json: 'Code',
58
- jsonb: 'Code',
59
- xml: 'Code',
60
- // Array types (use Text for serialization)
61
- array: 'Text',
62
- };
63
- return typeMap[baseType] || 'Data';
64
- }
65
- /**
66
- * Parses a PostgreSQL CREATE TABLE statement
67
- * @param ddl - SQL DDL string
68
- * @returns Array of parsed tables
69
- * @public
70
- */
71
- export function parseDDL(ddl) {
72
- const tables = [];
73
- // Split by CREATE TABLE statements
74
- const createTableRegex = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:(\w+)\.)?(\w+)\s*\(([\s\S]*?)\);/gi;
75
- let match;
76
- while ((match = createTableRegex.exec(ddl)) !== null) {
77
- const schema = match[1];
78
- const tableName = match[2];
79
- const columnsDef = match[3];
80
- const table = {
81
- name: tableName,
82
- columns: [],
83
- schema: schema || 'public',
84
- };
85
- // Parse columns and constraints
86
- const lines = columnsDef.split(',').map(l => l.trim());
87
- const primaryKeys = [];
88
- const foreignKeys = new Map();
89
- const uniqueColumns = new Set();
90
- for (const line of lines) {
91
- // Skip empty lines
92
- if (!line)
93
- continue;
94
- // Handle PRIMARY KEY constraint
95
- if (/PRIMARY\s+KEY/i.test(line)) {
96
- const pkMatch = line.match(/PRIMARY\s+KEY\s*\(([^)]+)\)/i);
97
- if (pkMatch) {
98
- const cols = pkMatch[1].split(',').map(c => c.trim().replace(/"/g, ''));
99
- primaryKeys.push(...cols);
100
- }
101
- continue;
102
- }
103
- // Handle FOREIGN KEY constraint
104
- if (/FOREIGN\s+KEY/i.test(line)) {
105
- const fkMatch = line.match(/FOREIGN\s+KEY\s*\(([^)]+)\)\s*REFERENCES\s+(\w+)\s*\(([^)]+)\)/i);
106
- if (fkMatch) {
107
- const localCol = fkMatch[1].trim().replace(/"/g, '');
108
- const refTable = fkMatch[2].trim();
109
- const refCol = fkMatch[3].trim().replace(/"/g, '');
110
- foreignKeys.set(localCol, { table: refTable, column: refCol });
111
- }
112
- continue;
113
- }
114
- // Handle UNIQUE constraint
115
- if (/UNIQUE/i.test(line) && /UNIQUE\s*\(/i.test(line)) {
116
- const uniqueMatch = line.match(/UNIQUE\s*\(([^)]+)\)/i);
117
- if (uniqueMatch) {
118
- const cols = uniqueMatch[1].split(',').map(c => c.trim().replace(/"/g, ''));
119
- cols.forEach(c => uniqueColumns.add(c));
120
- }
121
- continue;
122
- }
123
- // Parse column definition
124
- const columnMatch = line.match(/^(\w+)\s+([A-Za-z0-9_()]+)(.*)$/i);
125
- if (columnMatch) {
126
- const columnName = columnMatch[1];
127
- const dataType = columnMatch[2];
128
- const modifiers = columnMatch[3] || '';
129
- const column = {
130
- name: columnName,
131
- sqlType: dataType,
132
- notNull: /NOT\s+NULL/i.test(modifiers),
133
- primaryKey: false,
134
- unique: /UNIQUE/i.test(modifiers),
135
- };
136
- // Extract enum values
137
- if (/^ENUM/i.test(dataType)) {
138
- const enumMatch = dataType.match(/ENUM\s*\(([^)]+)\)/i);
139
- if (enumMatch) {
140
- column.enumValues = enumMatch[1].split(',').map(v => v.trim().replace(/^'|'$/g, ''));
141
- }
142
- }
143
- // Extract default value
144
- const defaultMatch = modifiers.match(/DEFAULT\s+([^,\s]+)/i);
145
- if (defaultMatch) {
146
- column.defaultValue = defaultMatch[1];
147
- }
148
- // Extract CHECK constraint
149
- const checkMatch = modifiers.match(/CHECK\s*\(([^)]+)\)/i);
150
- if (checkMatch) {
151
- column.checkConstraint = checkMatch[1];
152
- }
153
- // Check for inline REFERENCES (foreign key)
154
- const refMatch = modifiers.match(/REFERENCES\s+(\w+)\s*\(([^)]+)\)/i);
155
- if (refMatch) {
156
- column.foreignKey = {
157
- table: refMatch[1],
158
- column: refMatch[2].trim().replace(/"/g, ''),
159
- };
160
- }
161
- table.columns.push(column);
162
- }
163
- }
164
- // Apply primary keys
165
- primaryKeys.forEach(pk => {
166
- const col = table.columns.find(c => c.name === pk);
167
- if (col) {
168
- col.primaryKey = true;
169
- col.notNull = true; // PKs are always NOT NULL
170
- }
171
- });
172
- // Apply foreign keys
173
- foreignKeys.forEach((fk, colName) => {
174
- const col = table.columns.find(c => c.name === colName);
175
- if (col) {
176
- col.foreignKey = fk;
177
- }
178
- });
179
- // Apply unique constraints
180
- uniqueColumns.forEach(colName => {
181
- const col = table.columns.find(c => c.name === colName);
182
- if (col) {
183
- col.unique = true;
184
- }
185
- });
186
- tables.push(table);
187
- }
188
- return tables;
189
- }
190
- /**
191
- * Converts SQL table to Stonecrop schema format
192
- * @param table - Parsed SQL table
193
- * @param namingConverter - Function to convert field names
194
- * @returns Conversion result with schema and relationships
195
- * @public
196
- */
197
- export function convertTableToSchema(table, namingConverter) {
198
- const result = {
199
- doctype: table.name,
200
- schema: [],
201
- relationships: [],
202
- };
203
- for (const column of table.columns) {
204
- const { fieldname, label } = namingConverter
205
- ? namingConverter(column.name)
206
- : { fieldname: column.name, label: column.name };
207
- const fieldtype = column.foreignKey ? 'Link' : mapSQLTypeToFieldType(column.sqlType, column.enumValues);
208
- const field = {
209
- fieldname,
210
- label,
211
- fieldtype,
212
- };
213
- // Add required flag for NOT NULL columns (except PKs which are readonly)
214
- if (column.notNull && !column.primaryKey) {
215
- field.required = true;
216
- }
217
- // Make primary keys readonly
218
- if (column.primaryKey) {
219
- field.readonly = true;
220
- }
221
- // Handle foreign keys (Link fields)
222
- if (column.foreignKey) {
223
- field.options = column.foreignKey.table;
224
- result.relationships.push({
225
- fieldname,
226
- targetDoctype: column.foreignKey.table,
227
- targetField: column.foreignKey.column,
228
- });
229
- }
230
- // Handle enum values (Select fields)
231
- if (column.enumValues && column.enumValues.length > 0) {
232
- field.options = column.enumValues.join('\n');
233
- }
234
- // Handle default values
235
- if (column.defaultValue) {
236
- // Clean up default value (remove quotes, casts, etc.)
237
- let defaultVal = column.defaultValue.replace(/^'|'$/g, '');
238
- if (fieldtype === 'Int' || fieldtype === 'Float') {
239
- defaultVal = parseFloat(defaultVal);
240
- }
241
- else if (fieldtype === 'Check') {
242
- defaultVal = defaultVal === 'true' || defaultVal === 't';
243
- }
244
- field.default = defaultVal;
245
- }
246
- result.schema.push(field);
247
- }
248
- return result;
249
- }
250
- /**
251
- * Introspects SQL DDL and converts all tables to Stonecrop schemas
252
- * @param ddl - PostgreSQL DDL string
253
- * @param namingConverter - Optional function to convert field names
254
- * @returns Array of conversion results
255
- * @public
256
- */
257
- export function introspectSQL(ddl, namingConverter) {
258
- const tables = parseDDL(ddl);
259
- return tables.map(table => convertTableToSchema(table, namingConverter));
260
- }