@svadmin/drizzle 0.5.14 → 0.7.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.
- package/package.json +1 -1
- package/src/index.ts +3 -0
- package/src/infer-fields.ts +171 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
export { createDrizzleDataProvider } from './data-provider';
|
|
7
7
|
export type { RefineSQLConfig } from './data-provider';
|
|
8
8
|
|
|
9
|
+
export { inferFieldsFromDrizzle } from './infer-fields';
|
|
10
|
+
export type { InferFieldsOptions } from './infer-fields';
|
|
11
|
+
|
|
9
12
|
// Re-export commonly used refine-sqlx utilities for convenience
|
|
10
13
|
export type {
|
|
11
14
|
SecurityConfig,
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @svadmin/drizzle — Drizzle Schema → FieldDefinition auto-inference
|
|
3
|
+
*
|
|
4
|
+
* Automatically generates svadmin FieldDefinition arrays from Drizzle ORM table schemas,
|
|
5
|
+
* reducing boilerplate when defining resources.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { inferFieldsFromDrizzle } from '@svadmin/drizzle';
|
|
10
|
+
* import { users } from './schema';
|
|
11
|
+
*
|
|
12
|
+
* const fields = inferFieldsFromDrizzle(users);
|
|
13
|
+
* // → [{ key: 'id', label: 'Id', type: 'number', ... }, { key: 'name', label: 'Name', type: 'text', ... }]
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { FieldDefinition } from '@svadmin/core';
|
|
18
|
+
|
|
19
|
+
/** Drizzle column shape (duck-typed to avoid version coupling) */
|
|
20
|
+
interface DrizzleColumnLike {
|
|
21
|
+
dataType?: string;
|
|
22
|
+
columnType?: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
notNull?: boolean;
|
|
25
|
+
hasDefault?: boolean;
|
|
26
|
+
default?: unknown;
|
|
27
|
+
primary?: boolean;
|
|
28
|
+
enumValues?: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Drizzle table shape (duck-typed — works with any Drizzle table object) */
|
|
32
|
+
interface DrizzleTableLike {
|
|
33
|
+
[Symbol.iterator]?: never;
|
|
34
|
+
_?: { columns?: Record<string, DrizzleColumnLike> };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Options for field inference */
|
|
38
|
+
export interface InferFieldsOptions {
|
|
39
|
+
/** Override labels per column key */
|
|
40
|
+
labels?: Record<string, string>;
|
|
41
|
+
/** Columns to exclude from output */
|
|
42
|
+
exclude?: string[];
|
|
43
|
+
/** Columns to mark as non-editable */
|
|
44
|
+
readOnly?: string[];
|
|
45
|
+
/** Columns to hide from list view */
|
|
46
|
+
hideFromList?: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Map a Drizzle column type string to a svadmin FieldDefinition type.
|
|
51
|
+
*/
|
|
52
|
+
function mapColumnType(col: DrizzleColumnLike): FieldDefinition['type'] {
|
|
53
|
+
const dt = (col.dataType ?? col.columnType ?? '').toLowerCase();
|
|
54
|
+
|
|
55
|
+
// Integer / serial / numeric types
|
|
56
|
+
if (dt.includes('int') || dt.includes('serial') || dt.includes('numeric') || dt.includes('real') || dt.includes('float') || dt.includes('double') || dt.includes('decimal')) {
|
|
57
|
+
return 'number';
|
|
58
|
+
}
|
|
59
|
+
// Boolean
|
|
60
|
+
if (dt.includes('bool')) return 'boolean';
|
|
61
|
+
// Date / time types
|
|
62
|
+
if (dt.includes('date') || dt.includes('time') || dt.includes('timestamp')) return 'date';
|
|
63
|
+
// JSON
|
|
64
|
+
if (dt.includes('json')) return 'text'; // JSON fields render as text by default
|
|
65
|
+
// Enum (Drizzle pgEnum / mysqlEnum)
|
|
66
|
+
if (col.enumValues && col.enumValues.length > 0) return 'select';
|
|
67
|
+
// Text / varchar / char
|
|
68
|
+
if (dt.includes('text') || dt.includes('varchar') || dt.includes('char') || dt.includes('string')) {
|
|
69
|
+
return 'text';
|
|
70
|
+
}
|
|
71
|
+
// UUID
|
|
72
|
+
if (dt.includes('uuid')) return 'text';
|
|
73
|
+
// Blob / bytea — not renderable, skip
|
|
74
|
+
if (dt.includes('blob') || dt.includes('bytea')) return 'text';
|
|
75
|
+
|
|
76
|
+
return 'text'; // fallback
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Format a column key into a human-readable label.
|
|
81
|
+
* `created_at` → `Created At`, `userId` → `User Id`
|
|
82
|
+
*/
|
|
83
|
+
function humanizeKey(key: string): string {
|
|
84
|
+
return key
|
|
85
|
+
.replace(/_/g, ' ')
|
|
86
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
87
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Infer FieldDefinition[] from a Drizzle ORM table schema.
|
|
92
|
+
*
|
|
93
|
+
* Works with any Drizzle table (SQLite, PostgreSQL, MySQL) that exposes
|
|
94
|
+
* the internal `_` metadata with column definitions.
|
|
95
|
+
*/
|
|
96
|
+
export function inferFieldsFromDrizzle(
|
|
97
|
+
table: DrizzleTableLike,
|
|
98
|
+
options: InferFieldsOptions = {},
|
|
99
|
+
): FieldDefinition[] {
|
|
100
|
+
const columns = (table as Record<string, unknown>)?._
|
|
101
|
+
? ((table as Record<string, unknown>)._ as Record<string, unknown>)?.columns as Record<string, DrizzleColumnLike> | undefined
|
|
102
|
+
: undefined;
|
|
103
|
+
|
|
104
|
+
if (!columns) {
|
|
105
|
+
// Fallback: try to iterate over table keys that look like columns
|
|
106
|
+
const result: FieldDefinition[] = [];
|
|
107
|
+
for (const [key, val] of Object.entries(table as Record<string, unknown>)) {
|
|
108
|
+
if (key.startsWith('_') || typeof val === 'function') continue;
|
|
109
|
+
if (options.exclude?.includes(key)) continue;
|
|
110
|
+
|
|
111
|
+
const col = val as DrizzleColumnLike;
|
|
112
|
+
if (!col || typeof col !== 'object') continue;
|
|
113
|
+
|
|
114
|
+
// Check if it looks like a Drizzle column (has dataType or columnType)
|
|
115
|
+
if (!col.dataType && !col.columnType) continue;
|
|
116
|
+
|
|
117
|
+
result.push(buildField(key, col, options));
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const fields: FieldDefinition[] = [];
|
|
123
|
+
for (const [key, col] of Object.entries(columns)) {
|
|
124
|
+
if (options.exclude?.includes(key)) continue;
|
|
125
|
+
fields.push(buildField(key, col, options));
|
|
126
|
+
}
|
|
127
|
+
return fields;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildField(
|
|
131
|
+
key: string,
|
|
132
|
+
col: DrizzleColumnLike,
|
|
133
|
+
options: InferFieldsOptions,
|
|
134
|
+
): FieldDefinition {
|
|
135
|
+
const type = mapColumnType(col);
|
|
136
|
+
const field: FieldDefinition = {
|
|
137
|
+
key: col.name ?? key,
|
|
138
|
+
label: options.labels?.[key] ?? humanizeKey(key),
|
|
139
|
+
type,
|
|
140
|
+
required: col.notNull === true && !col.hasDefault,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Primary key fields should be read-only and hidden from create forms
|
|
144
|
+
if (col.primary) {
|
|
145
|
+
field.showInForm = false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Read-only overrides
|
|
149
|
+
if (options.readOnly?.includes(key)) {
|
|
150
|
+
field.showInForm = false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Hide from list view
|
|
154
|
+
if (options.hideFromList?.includes(key)) {
|
|
155
|
+
field.showInList = false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Enum → select options
|
|
159
|
+
if (col.enumValues && col.enumValues.length > 0) {
|
|
160
|
+
field.type = 'select';
|
|
161
|
+
field.options = col.enumValues.map(v => ({ label: humanizeKey(v), value: v }));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Common timestamp patterns → mark as non-editable
|
|
165
|
+
const lowerKey = key.toLowerCase();
|
|
166
|
+
if (lowerKey === 'createdat' || lowerKey === 'created_at' || lowerKey === 'updatedat' || lowerKey === 'updated_at') {
|
|
167
|
+
field.showInForm = false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return field;
|
|
171
|
+
}
|