@svadmin/drizzle 0.6.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/drizzle",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Drizzle ORM DataProvider — SQLite, PostgreSQL, MySQL, Cloudflare D1 via refine-sqlx",
5
5
  "type": "module",
6
6
  "sideEffects": false,
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
+ }