@svadmin/drizzle 0.11.16 → 0.12.6

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.11.16",
3
+ "version": "0.12.6",
4
4
  "description": "Drizzle ORM DataProvider — SQLite, PostgreSQL, MySQL, Cloudflare D1 via refine-sqlx",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -20,14 +20,19 @@
20
20
  }
21
21
  },
22
22
  "peerDependencies": {
23
- "@svadmin/core": "^0.50.0",
23
+ "@svadmin/core": "^0.52.0",
24
+ "drizzle-orm": "^1.0.0-rc.1",
24
25
  "refine-sqlx": "^0.10.3"
25
26
  },
27
+ "dependencies": {
28
+ "@sinclair/typebox": "^0.34.52",
29
+ "@svadmin/refine-adapter": "^0.11.6"
30
+ },
26
31
  "license": "MIT",
27
32
  "author": "zuohuadong",
28
33
  "repository": {
29
34
  "type": "git",
30
- "url": "https://github.com/vibeunion/svadmin.git",
35
+ "url": "git+https://github.com/vibeunion/svadmin.git",
31
36
  "directory": "packages/drizzle"
32
37
  },
33
38
  "homepage": "https://github.com/vibeunion/svadmin#readme",
@@ -35,7 +40,11 @@
35
40
  "url": "https://github.com/vibeunion/svadmin/issues"
36
41
  },
37
42
  "devDependencies": {
43
+ "@cloudflare/workers-types": "^5.20260908.1",
44
+ "@types/better-sqlite3": "^9.6.0",
38
45
  "drizzle-orm": "^1.0.0-rc.1",
46
+ "mysql2": "^3.24.4",
47
+ "postgres": "^3.4.9",
39
48
  "refine-sqlx": "^0.10.3"
40
49
  },
41
50
  "peerDependenciesMeta": {
@@ -0,0 +1,45 @@
1
+ import { Type, type Static } from '@sinclair/typebox';
2
+ import { createExactSchemaValidator } from '@svadmin/core/schema';
3
+
4
+ const columnSchema = Type.Object({
5
+ name: Type.String({ minLength: 1 }),
6
+ dataType: Type.String({ minLength: 1 }),
7
+ columnType: Type.String({ minLength: 1 }),
8
+ notNull: Type.Boolean(),
9
+ hasDefault: Type.Boolean(),
10
+ primary: Type.Boolean(),
11
+ enumValues: Type.Optional(Type.Array(Type.String())),
12
+ });
13
+ const columnValidator = createExactSchemaValidator(columnSchema);
14
+
15
+ export type ColumnMetadata = Static<typeof columnSchema>;
16
+
17
+ function isObject(value: unknown): value is Record<PropertyKey, unknown> {
18
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
19
+ }
20
+
21
+ /** Project only consumed metadata; Drizzle columns contain circular table links. */
22
+ export function decodeColumnMetadata(value: unknown): ColumnMetadata {
23
+ if (!isObject(value)) throw new TypeError('Invalid Drizzle column metadata');
24
+ const candidate = {
25
+ name: value['name'],
26
+ dataType: value['dataType'],
27
+ columnType: value['columnType'],
28
+ notNull: value['notNull'],
29
+ hasDefault: value['hasDefault'],
30
+ primary: value['primary'],
31
+ ...(value['enumValues'] === undefined ? {} : { enumValues: value['enumValues'] }),
32
+ };
33
+ if (!columnValidator.Check(candidate)) {
34
+ throw new TypeError('Invalid Drizzle column metadata');
35
+ }
36
+ return {
37
+ ...candidate,
38
+ ...(candidate.enumValues === undefined ? {} : { enumValues: [...candidate.enumValues] }),
39
+ };
40
+ }
41
+
42
+ export function decodeColumns(value: unknown): [string, ColumnMetadata][] {
43
+ if (!isObject(value)) throw new TypeError('Invalid Drizzle table columns');
44
+ return Object.entries(value).map(([key, column]) => [key, decodeColumnMetadata(column)]);
45
+ }
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  import type { DataProvider } from '@svadmin/core';
19
+ import { createRefineAdapter } from '@svadmin/refine-adapter';
19
20
  import type { RefineSQLConfig } from 'refine-sqlx';
20
21
  import { createRefineSQL } from 'refine-sqlx';
21
22
 
@@ -25,8 +26,8 @@ export type { RefineSQLConfig } from 'refine-sqlx';
25
26
  * Create a svadmin DataProvider backed by Drizzle ORM.
26
27
  *
27
28
  * Wraps refine-sqlx's `createRefineSQL()` and returns a `@svadmin/core` DataProvider.
28
- * Both type systems are structurally compatible (duck typing), so the wrapper is
29
- * a simple type-level bridge with no runtime overhead.
29
+ * The shared adapter validates method availability and every response envelope
30
+ * before database results enter the application's typed data layer.
30
31
  *
31
32
  * @param config - refine-sqlx configuration (connection, schema, features, etc.)
32
33
  * @returns A @svadmin/core DataProvider
@@ -54,18 +55,6 @@ export type { RefineSQLConfig } from 'refine-sqlx';
54
55
  export async function createDrizzleDataProvider<
55
56
  TSchema extends Record<string, unknown>,
56
57
  >(config: RefineSQLConfig<TSchema>): Promise<DataProvider> {
57
- const provider = await createRefineSQL(config);
58
-
59
- // refine-sqlx's DataProvider is structurally compatible with @svadmin/core's DataProvider:
60
- // - Same method names: getList, getOne, getMany, create, update, deleteOne, etc.
61
- // - Same parameter shapes: { resource, pagination, sorters, filters, meta, ... }
62
- // - Same return shapes: { data, total } / { data }
63
- //
64
- // The only nominal difference is type naming (GetListResponse vs GetListResult, etc.)
65
- // which TypeScript's structural type system bridges automatically.
66
- //
67
- // The `custom()` method has a minor signature superset (refine-sqlx supports
68
- // 'head' and 'options' HTTP methods that svadmin doesn't define), which is
69
- // a non-breaking superset — safe to cast.
70
- return provider as unknown as DataProvider;
58
+ const provider: unknown = await createRefineSQL(config);
59
+ return createRefineAdapter(provider);
71
60
  }
@@ -15,24 +15,9 @@
15
15
  */
16
16
 
17
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
- }
18
+ import { getTableColumns } from 'drizzle-orm/utils';
19
+ import { isTable, type Table } from 'drizzle-orm/table';
20
+ import { decodeColumns, type ColumnMetadata } from './column-metadata';
36
21
 
37
22
  /** Options for field inference */
38
23
  export interface InferFieldsOptions {
@@ -49,11 +34,13 @@ export interface InferFieldsOptions {
49
34
  /**
50
35
  * Map a Drizzle column type string to a svadmin FieldDefinition type.
51
36
  */
52
- function mapColumnType(col: DrizzleColumnLike): FieldDefinition['type'] {
53
- const dt = (col.dataType ?? col.columnType ?? '').toLowerCase();
37
+ function mapColumnType(col: ColumnMetadata): FieldDefinition['type'] {
38
+ const dt = `${col.dataType} ${col.columnType}`.toLowerCase();
54
39
 
55
40
  // 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')) {
41
+ if (col.dataType.includes('bool')) return 'boolean';
42
+ if (col.dataType.includes('date')) return 'date';
43
+ if (dt.includes('int') || dt.includes('serial') || dt.includes('numeric') || dt.includes('real') || dt.includes('float') || dt.includes('double') || dt.includes('decimal') || dt.includes('number')) {
57
44
  return 'number';
58
45
  }
59
46
  // Boolean
@@ -90,37 +77,18 @@ function humanizeKey(key: string): string {
90
77
  /**
91
78
  * Infer FieldDefinition[] from a Drizzle ORM table schema.
92
79
  *
93
- * Works with any Drizzle table (SQLite, PostgreSQL, MySQL) that exposes
94
- * the internal `_` metadata with column definitions.
80
+ * Uses Drizzle's runtime column API; `_` is type-only metadata, not a table
81
+ * property. Malformed column metadata is rejected before deriving UI fields.
95
82
  */
96
83
  export function inferFieldsFromDrizzle(
97
- table: DrizzleTableLike,
84
+ table: Table,
98
85
  options: InferFieldsOptions = {},
99
86
  ): 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
- }
87
+ if (!isTable(table)) throw new TypeError('Expected a Drizzle table');
88
+ const columns: unknown = getTableColumns(table);
121
89
 
122
90
  const fields: FieldDefinition[] = [];
123
- for (const [key, col] of Object.entries(columns)) {
91
+ for (const [key, col] of decodeColumns(columns)) {
124
92
  if (options.exclude?.includes(key)) continue;
125
93
  fields.push(buildField(key, col, options));
126
94
  }
@@ -129,12 +97,12 @@ export function inferFieldsFromDrizzle(
129
97
 
130
98
  function buildField(
131
99
  key: string,
132
- col: DrizzleColumnLike,
100
+ col: ColumnMetadata,
133
101
  options: InferFieldsOptions,
134
102
  ): FieldDefinition {
135
103
  const type = mapColumnType(col);
136
104
  const field: FieldDefinition = {
137
- key: col.name ?? key,
105
+ key,
138
106
  label: options.labels?.[key] ?? humanizeKey(key),
139
107
  type,
140
108
  required: col.notNull === true && !col.hasDefault,