@modern-admin/adapter-drizzle 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.
- package/dist/converters.d.ts +16 -0
- package/dist/converters.d.ts.map +1 -0
- package/dist/converters.js +225 -0
- package/dist/converters.js.map +1 -0
- package/dist/database.d.ts +10 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +56 -0
- package/dist/database.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/property.d.ts +20 -0
- package/dist/property.d.ts.map +1 -0
- package/dist/property.js +112 -0
- package/dist/property.js.map +1 -0
- package/dist/resource.d.ts +41 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/resource.js +444 -0
- package/dist/resource.js.map +1 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +48 -0
- package/src/converters.ts +254 -0
- package/src/database.ts +65 -0
- package/src/index.ts +31 -0
- package/src/property.ts +125 -0
- package/src/resource.ts +527 -0
- package/src/types.ts +90 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { and, arrayContains, arrayOverlaps, asc, desc, eq, gt, gte, ilike, inArray, isNotNull, isNull, like, lt, lte, ne, not, or } from 'drizzle-orm'
|
|
2
|
+
import type { Filter, FilterElement, FilterOperator, FilterValue, FindOptions } from '@modern-admin/core'
|
|
3
|
+
import type { DrizzleProperty } from './property.js'
|
|
4
|
+
import type { DrizzleColumn, DrizzleTable } from './types.js'
|
|
5
|
+
|
|
6
|
+
const isRangeValue = (
|
|
7
|
+
value: FilterValue,
|
|
8
|
+
): value is { from?: string; to?: string } =>
|
|
9
|
+
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
10
|
+
|
|
11
|
+
const coerceScalar = (
|
|
12
|
+
value: FilterValue,
|
|
13
|
+
property: DrizzleProperty | null,
|
|
14
|
+
): unknown => {
|
|
15
|
+
if (value == null || typeof value === 'boolean') return value
|
|
16
|
+
if (Array.isArray(value)) return value.map((v) => coerceScalar(v as FilterValue, property))
|
|
17
|
+
if (typeof value === 'number') return value
|
|
18
|
+
if (typeof value !== 'string') return value
|
|
19
|
+
if (!property) return value
|
|
20
|
+
switch (property.type()) {
|
|
21
|
+
case 'number':
|
|
22
|
+
case 'currency': {
|
|
23
|
+
const n = Number(value)
|
|
24
|
+
return Number.isFinite(n) ? n : value
|
|
25
|
+
}
|
|
26
|
+
case 'float': {
|
|
27
|
+
const n = parseFloat(value)
|
|
28
|
+
return Number.isFinite(n) ? n : value
|
|
29
|
+
}
|
|
30
|
+
case 'boolean':
|
|
31
|
+
return value === 'true' || value === '1'
|
|
32
|
+
case 'date':
|
|
33
|
+
case 'datetime': {
|
|
34
|
+
const d = new Date(value)
|
|
35
|
+
return Number.isNaN(d.getTime()) ? value : d
|
|
36
|
+
}
|
|
37
|
+
default:
|
|
38
|
+
return value
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Return the case-insensitive `like` function appropriate for the dialect. */
|
|
43
|
+
const ciLike = (column: DrizzleColumn) =>
|
|
44
|
+
(column as DrizzleColumn).columnType?.startsWith('Pg') ? ilike : like
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Wrap a value in `%...%` for a LIKE/ILIKE contains match.
|
|
48
|
+
* The `%` characters within the user value are escaped so they match literally.
|
|
49
|
+
*/
|
|
50
|
+
const likeContains = (v: string) => `%${v}%`
|
|
51
|
+
const likeStartsWith = (v: string) => `${v}%`
|
|
52
|
+
const likeEndsWith = (v: string) => `%${v}`
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build a drizzle condition for a single filter element.
|
|
56
|
+
* When an explicit `operator` is set, it takes precedence over legacy implicit
|
|
57
|
+
* behaviour. All string comparisons use `ilike` (Postgres) / `like` (others)
|
|
58
|
+
* for case-insensitive matching.
|
|
59
|
+
*/
|
|
60
|
+
const elementToCondition = (
|
|
61
|
+
element: FilterElement,
|
|
62
|
+
table: DrizzleTable,
|
|
63
|
+
): unknown => {
|
|
64
|
+
const property = element.property as DrizzleProperty | null
|
|
65
|
+
if (!property) return null
|
|
66
|
+
const column = table[element.path] as DrizzleColumn | undefined
|
|
67
|
+
if (!column) return null
|
|
68
|
+
const { value, operator } = element
|
|
69
|
+
|
|
70
|
+
// ── Explicit operator ────────────────────────────────────────────────
|
|
71
|
+
if (operator) {
|
|
72
|
+
return buildOperatorCondition(operator, value, property, column)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Legacy implicit behaviour (backward compat) ──────────────────────
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
const list = value.map((v) => coerceScalar(v as FilterValue, property)) as unknown[]
|
|
78
|
+
if (!list.length) return null
|
|
79
|
+
if (property.isArray()) return arrayOverlaps(column as never, list as never)
|
|
80
|
+
return inArray(column as never, list as never)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (isRangeValue(value)) {
|
|
84
|
+
const conds: unknown[] = []
|
|
85
|
+
if (value.from !== undefined && value.from !== '') {
|
|
86
|
+
conds.push(gte(column as never, coerceScalar(value.from, property) as never))
|
|
87
|
+
}
|
|
88
|
+
if (value.to !== undefined && value.to !== '') {
|
|
89
|
+
conds.push(lte(column as never, coerceScalar(value.to, property) as never))
|
|
90
|
+
}
|
|
91
|
+
if (!conds.length) return null
|
|
92
|
+
return conds.length === 1 ? conds[0] : and(...(conds as never[]))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const coerced = coerceScalar(value, property)
|
|
96
|
+
if (property.isArray()) {
|
|
97
|
+
return arrayContains(column as never, [coerced] as never)
|
|
98
|
+
}
|
|
99
|
+
if (property.type() === 'string' && typeof coerced === 'string') {
|
|
100
|
+
const op = ciLike(column)
|
|
101
|
+
return op(column as never, likeContains(coerced) as never)
|
|
102
|
+
}
|
|
103
|
+
return eq(column as never, coerced as never)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Translate an explicit FilterOperator to a drizzle SQL condition.
|
|
108
|
+
* String comparisons use `ilike` (PG) / `like` (others).
|
|
109
|
+
*/
|
|
110
|
+
const buildOperatorCondition = (
|
|
111
|
+
operator: FilterOperator,
|
|
112
|
+
value: FilterValue,
|
|
113
|
+
property: DrizzleProperty,
|
|
114
|
+
column: DrizzleColumn,
|
|
115
|
+
): unknown => {
|
|
116
|
+
const isString = property.type() === 'string'
|
|
117
|
+
|
|
118
|
+
switch (operator) {
|
|
119
|
+
case 'eq': {
|
|
120
|
+
const coerced = coerceScalar(value, property)
|
|
121
|
+
if (isString && typeof coerced === 'string') {
|
|
122
|
+
const op = ciLike(column)
|
|
123
|
+
// Exact match via ILIKE without wildcards (case-insensitive equals)
|
|
124
|
+
return op(column as never, coerced as never)
|
|
125
|
+
}
|
|
126
|
+
return eq(column as never, coerced as never)
|
|
127
|
+
}
|
|
128
|
+
case 'neq': {
|
|
129
|
+
const coerced = coerceScalar(value, property)
|
|
130
|
+
if (isString && typeof coerced === 'string') {
|
|
131
|
+
const op = ciLike(column)
|
|
132
|
+
return not(op(column as never, coerced as never))
|
|
133
|
+
}
|
|
134
|
+
return ne(column as never, coerced as never)
|
|
135
|
+
}
|
|
136
|
+
case 'co': {
|
|
137
|
+
const coerced = coerceScalar(value, property)
|
|
138
|
+
if (typeof coerced === 'string') {
|
|
139
|
+
const op = ciLike(column)
|
|
140
|
+
return op(column as never, likeContains(coerced) as never)
|
|
141
|
+
}
|
|
142
|
+
return eq(column as never, coerced as never)
|
|
143
|
+
}
|
|
144
|
+
case 'nco': {
|
|
145
|
+
const coerced = coerceScalar(value, property)
|
|
146
|
+
if (typeof coerced === 'string') {
|
|
147
|
+
const op = ciLike(column)
|
|
148
|
+
return not(op(column as never, likeContains(coerced) as never))
|
|
149
|
+
}
|
|
150
|
+
return ne(column as never, coerced as never)
|
|
151
|
+
}
|
|
152
|
+
case 'sw': {
|
|
153
|
+
const coerced = coerceScalar(value, property)
|
|
154
|
+
if (typeof coerced === 'string') {
|
|
155
|
+
const op = ciLike(column)
|
|
156
|
+
return op(column as never, likeStartsWith(coerced) as never)
|
|
157
|
+
}
|
|
158
|
+
return eq(column as never, coerced as never)
|
|
159
|
+
}
|
|
160
|
+
case 'ew': {
|
|
161
|
+
const coerced = coerceScalar(value, property)
|
|
162
|
+
if (typeof coerced === 'string') {
|
|
163
|
+
const op = ciLike(column)
|
|
164
|
+
return op(column as never, likeEndsWith(coerced) as never)
|
|
165
|
+
}
|
|
166
|
+
return eq(column as never, coerced as never)
|
|
167
|
+
}
|
|
168
|
+
case 'empty':
|
|
169
|
+
return or(
|
|
170
|
+
isNull(column as never),
|
|
171
|
+
eq(column as never, '' as never),
|
|
172
|
+
)
|
|
173
|
+
case 'nempty':
|
|
174
|
+
return and(
|
|
175
|
+
isNotNull(column as never),
|
|
176
|
+
ne(column as never, '' as never),
|
|
177
|
+
)
|
|
178
|
+
case 'in': {
|
|
179
|
+
if (Array.isArray(value)) {
|
|
180
|
+
const list = value.map((v) => coerceScalar(v as FilterValue, property)) as unknown[]
|
|
181
|
+
if (!list.length) return null
|
|
182
|
+
if (property.isArray()) return arrayOverlaps(column as never, list as never)
|
|
183
|
+
return inArray(column as never, list as never)
|
|
184
|
+
}
|
|
185
|
+
const coerced = coerceScalar(value, property)
|
|
186
|
+
return eq(column as never, coerced as never)
|
|
187
|
+
}
|
|
188
|
+
case 'gt': {
|
|
189
|
+
const coerced = coerceScalar(value, property)
|
|
190
|
+
return gt(column as never, coerced as never)
|
|
191
|
+
}
|
|
192
|
+
case 'lt': {
|
|
193
|
+
const coerced = coerceScalar(value, property)
|
|
194
|
+
return lt(column as never, coerced as never)
|
|
195
|
+
}
|
|
196
|
+
case 'between': {
|
|
197
|
+
const str = typeof value === 'string' ? value : ''
|
|
198
|
+
const comma = str.indexOf(',')
|
|
199
|
+
const fromStr = comma >= 0 ? str.slice(0, comma) : str
|
|
200
|
+
const toStr = comma >= 0 ? str.slice(comma + 1) : ''
|
|
201
|
+
const conds: unknown[] = []
|
|
202
|
+
if (fromStr) conds.push(gte(column as never, coerceScalar(fromStr, property) as never))
|
|
203
|
+
if (toStr) conds.push(lte(column as never, coerceScalar(toStr, property) as never))
|
|
204
|
+
if (!conds.length) return null
|
|
205
|
+
return conds.length === 1 ? conds[0] : and(...(conds as never[]))
|
|
206
|
+
}
|
|
207
|
+
default:
|
|
208
|
+
return null
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Convert a core Filter into a drizzle `where` SQL condition. Returns
|
|
214
|
+
* `undefined` when no usable filter was provided so the caller can omit
|
|
215
|
+
* `.where()` entirely.
|
|
216
|
+
*/
|
|
217
|
+
export const filterToWhere = (
|
|
218
|
+
filter: Filter,
|
|
219
|
+
table: DrizzleTable,
|
|
220
|
+
): unknown => {
|
|
221
|
+
const conditions: unknown[] = []
|
|
222
|
+
filter.reduce<null>((_, element) => {
|
|
223
|
+
const cond = elementToCondition(element, table)
|
|
224
|
+
if (cond != null) conditions.push(cond)
|
|
225
|
+
return null
|
|
226
|
+
}, null)
|
|
227
|
+
if (conditions.length === 0) return undefined
|
|
228
|
+
if (conditions.length === 1) return conditions[0]
|
|
229
|
+
return and(...(conditions as never[]))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface DrizzleFindShape {
|
|
233
|
+
where: unknown
|
|
234
|
+
limit?: number
|
|
235
|
+
offset?: number
|
|
236
|
+
orderBy?: unknown
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export const findOptionsToDrizzle = (
|
|
240
|
+
options: FindOptions,
|
|
241
|
+
table: DrizzleTable,
|
|
242
|
+
): Pick<DrizzleFindShape, 'limit' | 'offset' | 'orderBy'> => {
|
|
243
|
+
const out: Pick<DrizzleFindShape, 'limit' | 'offset' | 'orderBy'> = {}
|
|
244
|
+
if (options.limit != null) out.limit = options.limit
|
|
245
|
+
if (options.offset != null) out.offset = options.offset
|
|
246
|
+
const sortBy = options.sort?.sortBy
|
|
247
|
+
if (sortBy) {
|
|
248
|
+
const column = table[sortBy] as DrizzleColumn | undefined
|
|
249
|
+
if (column) {
|
|
250
|
+
out.orderBy = (options.sort?.direction === 'desc' ? desc : asc)(column as never)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return out
|
|
254
|
+
}
|
package/src/database.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { BaseDatabase } from '@modern-admin/core'
|
|
2
|
+
import { DrizzleResource } from './resource.js'
|
|
3
|
+
import type { DrizzleDatabaseConfig, DrizzleTable } from './types.js'
|
|
4
|
+
|
|
5
|
+
const isDrizzleDatabaseConfig = (db: unknown): db is DrizzleDatabaseConfig =>
|
|
6
|
+
typeof db === 'object' &&
|
|
7
|
+
db !== null &&
|
|
8
|
+
'client' in db &&
|
|
9
|
+
'schema' in db &&
|
|
10
|
+
typeof (db as { schema?: object }).schema === 'object'
|
|
11
|
+
|
|
12
|
+
const looksLikeTable = (value: unknown): value is DrizzleTable => {
|
|
13
|
+
if (!value || typeof value !== 'object') return false
|
|
14
|
+
// Drizzle tables expose at least one column with `name` + `dataType`.
|
|
15
|
+
for (const key of Object.keys(value as Record<string, unknown>)) {
|
|
16
|
+
if (key === '_') continue
|
|
17
|
+
const col = (value as Record<string, unknown>)[key]
|
|
18
|
+
if (
|
|
19
|
+
col &&
|
|
20
|
+
typeof col === 'object' &&
|
|
21
|
+
typeof (col as { name?: unknown }).name === 'string' &&
|
|
22
|
+
typeof (col as { dataType?: unknown }).dataType === 'string'
|
|
23
|
+
) {
|
|
24
|
+
return true
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class DrizzleDatabase extends BaseDatabase {
|
|
31
|
+
public readonly config: DrizzleDatabaseConfig
|
|
32
|
+
|
|
33
|
+
constructor(config: unknown) {
|
|
34
|
+
super(config)
|
|
35
|
+
if (!isDrizzleDatabaseConfig(config)) {
|
|
36
|
+
throw new Error('DrizzleDatabase requires { client, schema } config')
|
|
37
|
+
}
|
|
38
|
+
this.config = config
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static override isAdapterFor(db: unknown): boolean {
|
|
42
|
+
return isDrizzleDatabaseConfig(db)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override resources(): DrizzleResource[] {
|
|
46
|
+
const { client, schema, resources: overrides, dialect } = this.config
|
|
47
|
+
const out: DrizzleResource[] = []
|
|
48
|
+
for (const tableKey of Object.keys(schema)) {
|
|
49
|
+
const table = schema[tableKey]
|
|
50
|
+
if (!looksLikeTable(table)) continue
|
|
51
|
+
const cfg = overrides?.[tableKey] ?? {}
|
|
52
|
+
out.push(
|
|
53
|
+
new DrizzleResource({
|
|
54
|
+
client,
|
|
55
|
+
schema,
|
|
56
|
+
table,
|
|
57
|
+
tableKey,
|
|
58
|
+
dialect: dialect ?? 'pg',
|
|
59
|
+
...cfg,
|
|
60
|
+
}),
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
return out
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// @modern-admin/adapter-drizzle — Drizzle ORM adapter for @modern-admin/core.
|
|
2
|
+
//
|
|
3
|
+
// Usage:
|
|
4
|
+
// import { drizzle } from 'drizzle-orm/node-postgres'
|
|
5
|
+
// import * as schema from './schema'
|
|
6
|
+
// import { DrizzleDatabase, DrizzleResource } from '@modern-admin/adapter-drizzle'
|
|
7
|
+
//
|
|
8
|
+
// const client = drizzle(pool, { schema })
|
|
9
|
+
// const admin = new ModernAdmin({
|
|
10
|
+
// databases: [{ client, schema }],
|
|
11
|
+
// adapters: [{ Database: DrizzleDatabase, Resource: DrizzleResource }],
|
|
12
|
+
// })
|
|
13
|
+
|
|
14
|
+
export { DrizzleDatabase } from './database.js'
|
|
15
|
+
export { DrizzleResource } from './resource.js'
|
|
16
|
+
export { DrizzleProperty, extractForeignKeys, findPrimaryColumn } from './property.js'
|
|
17
|
+
export { filterToWhere, findOptionsToDrizzle } from './converters.js'
|
|
18
|
+
export type {
|
|
19
|
+
DrizzleClientLike,
|
|
20
|
+
DrizzleColumn,
|
|
21
|
+
DrizzleDatabaseConfig,
|
|
22
|
+
DrizzleDeleteBuilder,
|
|
23
|
+
DrizzleDialect,
|
|
24
|
+
DrizzleInsertBuilder,
|
|
25
|
+
DrizzleQueryBuilder,
|
|
26
|
+
DrizzleResourceConfig,
|
|
27
|
+
DrizzleSchema,
|
|
28
|
+
DrizzleSelectBuilder,
|
|
29
|
+
DrizzleTable,
|
|
30
|
+
DrizzleUpdateBuilder,
|
|
31
|
+
} from './types.js'
|
package/src/property.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { BaseProperty, type PropertyType } from '@modern-admin/core'
|
|
2
|
+
import type { DrizzleColumn, DrizzleTable } from './types.js'
|
|
3
|
+
|
|
4
|
+
const DATA_TYPE_TO_PROPERTY: Readonly<Record<string, PropertyType>> = {
|
|
5
|
+
string: 'string',
|
|
6
|
+
number: 'number',
|
|
7
|
+
bigint: 'number',
|
|
8
|
+
boolean: 'boolean',
|
|
9
|
+
date: 'datetime',
|
|
10
|
+
json: 'json',
|
|
11
|
+
buffer: 'string',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const isUuidColumn = (name: string, columnType?: string): boolean => {
|
|
15
|
+
if (columnType && /uuid/i.test(columnType)) return true
|
|
16
|
+
return /(^id$|Id$|_id$|uuid)/i.test(name)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Property descriptor backed by a Drizzle column. Maps drizzle's runtime
|
|
21
|
+
* dataType / columnType tags onto the core PropertyType taxonomy.
|
|
22
|
+
*/
|
|
23
|
+
export class DrizzleProperty extends BaseProperty {
|
|
24
|
+
public readonly column: DrizzleColumn
|
|
25
|
+
|
|
26
|
+
constructor(column: DrizzleColumn, reference: string | null = null, position = 1) {
|
|
27
|
+
const isEnum = (column.enumValues?.length ?? 0) > 0
|
|
28
|
+
const isArray = column.dataType === 'array'
|
|
29
|
+
const type = DrizzleProperty.resolveType(column, isEnum, reference !== null)
|
|
30
|
+
super({
|
|
31
|
+
path: column.name,
|
|
32
|
+
type,
|
|
33
|
+
isId: column.primary === true,
|
|
34
|
+
isArray,
|
|
35
|
+
isSortable: type !== 'json' && type !== 'mixed' && !isArray,
|
|
36
|
+
isRequired:
|
|
37
|
+
column.notNull === true && column.hasDefault !== true && column.primary !== true,
|
|
38
|
+
position,
|
|
39
|
+
reference,
|
|
40
|
+
availableValues: isEnum ? Array.from(column.enumValues!) : null,
|
|
41
|
+
})
|
|
42
|
+
this.column = column
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private static resolveType(
|
|
46
|
+
column: DrizzleColumn,
|
|
47
|
+
isEnum: boolean,
|
|
48
|
+
isReference: boolean,
|
|
49
|
+
): PropertyType {
|
|
50
|
+
if (isReference) return 'reference'
|
|
51
|
+
if (isEnum) return 'enum'
|
|
52
|
+
// For PgArray columns, the *element* type lives on `baseColumn`. Surface
|
|
53
|
+
// that as the property type so adapters/UI render the inner kind (e.g.
|
|
54
|
+
// `text[]` shows as `string`).
|
|
55
|
+
const dataType =
|
|
56
|
+
column.dataType === 'array' && column.baseColumn?.dataType
|
|
57
|
+
? column.baseColumn.dataType
|
|
58
|
+
: column.dataType
|
|
59
|
+
const mapped = DATA_TYPE_TO_PROPERTY[dataType]
|
|
60
|
+
if (mapped) {
|
|
61
|
+
if (mapped === 'string' && column.primary && isUuidColumn(column.name, column.columnType)) {
|
|
62
|
+
return 'uuid'
|
|
63
|
+
}
|
|
64
|
+
return mapped
|
|
65
|
+
}
|
|
66
|
+
return 'mixed'
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Find the primary-key column in a drizzle table. Returns null when absent. */
|
|
71
|
+
export const findPrimaryColumn = (table: DrizzleTable): DrizzleColumn | null => {
|
|
72
|
+
for (const key of Object.keys(table)) {
|
|
73
|
+
if (key === '_') continue
|
|
74
|
+
const col = table[key] as DrizzleColumn | undefined
|
|
75
|
+
if (col && col.primary === true) return col
|
|
76
|
+
}
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const FK_SYMBOL_DESC = /InlineForeignKeys$/
|
|
81
|
+
const TABLE_NAME_SYMBOL_DESC = /BaseName$/
|
|
82
|
+
|
|
83
|
+
interface DrizzleFKShape {
|
|
84
|
+
reference: () => {
|
|
85
|
+
columns: Array<{ name: string }>
|
|
86
|
+
foreignTable: object
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const tableBaseName = (table: object): string | null => {
|
|
91
|
+
for (const sym of Object.getOwnPropertySymbols(table)) {
|
|
92
|
+
if (TABLE_NAME_SYMBOL_DESC.test(sym.description ?? '')) {
|
|
93
|
+
const v = (table as Record<symbol, unknown>)[sym]
|
|
94
|
+
if (typeof v === 'string') return v
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Walk drizzle's hidden inline-FK symbol on a table and return a map of
|
|
102
|
+
* local column name → foreign table base name. Returns an empty map when
|
|
103
|
+
* the table has no FKs or when the structure differs (driver-specific).
|
|
104
|
+
*/
|
|
105
|
+
export const extractForeignKeys = (table: DrizzleTable): Record<string, string> => {
|
|
106
|
+
const out: Record<string, string> = {}
|
|
107
|
+
for (const sym of Object.getOwnPropertySymbols(table)) {
|
|
108
|
+
if (!FK_SYMBOL_DESC.test(sym.description ?? '')) continue
|
|
109
|
+
const fks = (table as Record<symbol, unknown>)[sym]
|
|
110
|
+
if (!Array.isArray(fks)) continue
|
|
111
|
+
for (const fk of fks as DrizzleFKShape[]) {
|
|
112
|
+
try {
|
|
113
|
+
const ref = fk.reference()
|
|
114
|
+
const targetName = tableBaseName(ref.foreignTable)
|
|
115
|
+
if (!targetName) continue
|
|
116
|
+
for (const c of ref.columns) {
|
|
117
|
+
out[c.name] = targetName
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// ignore malformed FKs — keep adapter resilient across drivers.
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return out
|
|
125
|
+
}
|