• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

vzakharchenko / forge-sql-orm / 14385988882

10 Apr 2025 04:53PM UTC coverage: 79.009% (+2.5%) from 76.514%
14385988882

Pull #41

github

web-flow
Merge 4ed548675 into 0fa3fd4a5
Pull Request #41: 2.0.19

223 of 278 branches covered (80.22%)

Branch coverage included in aggregate %.

183 of 212 new or added lines in 7 files covered. (86.32%)

2 existing lines in 2 files now uncovered.

925 of 1175 relevant lines covered (78.72%)

19.13 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.03
/src/utils/sqlUtils.ts
1
import moment from "moment";
2✔
2
import { AnyColumn, Column, isTable, SQL, sql, StringChunk } from "drizzle-orm";
2✔
3
import { AnyMySqlTable, MySqlCustomColumn } from "drizzle-orm/mysql-core/index";
4
import { PrimaryKeyBuilder } from "drizzle-orm/mysql-core/primary-keys";
5
import { AnyIndexBuilder } from "drizzle-orm/mysql-core/indexes";
6
import { CheckBuilder } from "drizzle-orm/mysql-core/checks";
7
import { ForeignKeyBuilder } from "drizzle-orm/mysql-core/foreign-keys";
8
import { UniqueConstraintBuilder } from "drizzle-orm/mysql-core/unique-constraint";
9
import type { SelectedFields } from "drizzle-orm/mysql-core/query-builders/select.types";
10
import { MySqlTable } from "drizzle-orm/mysql-core";
11
import { isSQLWrapper } from "drizzle-orm/sql/sql";
2✔
12

13
/**
14
 * Interface representing table metadata information
15
 */
16
export interface MetadataInfo {
17
  /** The name of the table */
18
  tableName: string;
19
  /** Record of column names and their corresponding column definitions */
20
  columns: Record<string, AnyColumn>;
21
  /** Array of index builders */
22
  indexes: AnyIndexBuilder[];
23
  /** Array of check constraint builders */
24
  checks: CheckBuilder[];
25
  /** Array of foreign key builders */
26
  foreignKeys: ForeignKeyBuilder[];
27
  /** Array of primary key builders */
28
  primaryKeys: PrimaryKeyBuilder[];
29
  /** Array of unique constraint builders */
30
  uniqueConstraints: UniqueConstraintBuilder[];
31
  /** Array of all extra builders */
32
  extras: any[];
33
}
34

35
/**
36
 * Interface for config builder data
37
 */
38
interface ConfigBuilderData {
39
  value?: any;
40
  [key: string]: any;
41
}
42

43
/**
44
 * Parses a date string into a Date object using the specified format
45
 * @param value - The date string to parse
46
 * @param format - The format to use for parsing
47
 * @returns Date object
48
 */
49
export const parseDateTime = (value: string, format: string): Date => {
2✔
50
  let result: Date;
18✔
51
  const m = moment(value, format, true);
18✔
52
  if (!m.isValid()) {
18✔
53
    const momentDate = moment(value);
8✔
54
    if (momentDate.isValid()) {
8✔
55
      result = momentDate.toDate();
8✔
56
    } else {
8!
57
      result = new Date(value);
×
58
    }
×
59
  } else {
18✔
60
    result = m.toDate();
10✔
61
  }
10✔
62
  if (isNaN(result.getTime())) {
18!
63
    result = new Date(value);
×
64
  }
×
65
  return result;
18✔
66
};
18✔
67

68
/**
69
 * Gets primary keys from the schema.
70
 * @template T - The type of the table schema
71
 * @param {T} table - The table schema
72
 * @returns {[string, AnyColumn][]} Array of primary key name and column pairs
73
 */
74
export function getPrimaryKeys<T extends AnyMySqlTable>(table: T): [string, AnyColumn][] {
2✔
75
  const { columns, primaryKeys } = getTableMetadata(table);
16✔
76

77
  // First try to find primary keys in columns
78
  const columnPrimaryKeys = Object.entries(columns).filter(([, column]) => column.primary) as [
16✔
79
    string,
80
    AnyColumn,
81
  ][];
82

83
  if (columnPrimaryKeys.length > 0) {
16✔
84
    return columnPrimaryKeys;
12✔
85
  }
12✔
86

87
  // If no primary keys found in columns, check primary key builders
88
  if (Array.isArray(primaryKeys) && primaryKeys.length > 0) {
16✔
89
    // Collect all primary key columns from all primary key builders
90
    const primaryKeyColumns = new Set<[string, AnyColumn]>();
4✔
91

92
    primaryKeys.forEach((primaryKeyBuilder) => {
4✔
93
      // Get primary key columns from each builder
94
      Object.entries(columns)
4✔
95
        .filter(([, column]) => {
4✔
96
          // @ts-ignore - PrimaryKeyBuilder has internal columns property
97
          return primaryKeyBuilder.columns.includes(column);
8✔
98
        })
4✔
99
        .forEach(([name, column]) => {
4✔
100
          primaryKeyColumns.add([name, column]);
4✔
101
        });
4✔
102
    });
4✔
103

104
    return Array.from(primaryKeyColumns);
4✔
105
  }
4!
106

107
  return [];
×
108
}
×
109

110
/**
111
 * Processes foreign keys from both foreignKeysSymbol and extraSymbol
112
 * @param table - The table schema
113
 * @param foreignKeysSymbol - Symbol for foreign keys
114
 * @param extraSymbol - Symbol for extra configuration
115
 * @returns Array of foreign key builders
116
 */
117
function processForeignKeys(
52✔
118
  table: AnyMySqlTable,
52✔
119
  foreignKeysSymbol: symbol | undefined,
52✔
120
  extraSymbol: symbol | undefined,
52✔
121
): ForeignKeyBuilder[] {
52✔
122
  const foreignKeys: ForeignKeyBuilder[] = [];
52✔
123

124
  // Process foreign keys from foreignKeysSymbol
125
  if (foreignKeysSymbol) {
52✔
126
    // @ts-ignore
127
    const fkArray: any[] = table[foreignKeysSymbol];
52✔
128
    if (fkArray) {
52✔
129
      fkArray.forEach((fk) => {
52✔
130
        if (fk.reference) {
×
131
          const item = fk.reference(fk);
×
132
          foreignKeys.push(item);
×
133
        }
×
134
      });
52✔
135
    }
52✔
136
  }
52✔
137

138
  // Process foreign keys from extraSymbol
139
  if (extraSymbol) {
52✔
140
    // @ts-ignore
141
    const extraConfigBuilder = table[extraSymbol];
52✔
142
    if (extraConfigBuilder && typeof extraConfigBuilder === "function") {
52✔
143
      const configBuilderData = extraConfigBuilder(table);
20✔
144
      if (configBuilderData) {
20✔
145
        const configBuilders = Array.isArray(configBuilderData)
20✔
146
          ? configBuilderData
20!
147
          : Object.values(configBuilderData).map(
×
NEW
148
              (item) => (item as ConfigBuilderData).value ?? item,
×
149
            );
×
150

151
        configBuilders.forEach((builder) => {
20✔
152
          if (!builder?.constructor) return;
20!
153

154
          const builderName = builder.constructor.name.toLowerCase();
20✔
155
          if (builderName.includes("foreignkeybuilder")) {
20!
156
            foreignKeys.push(builder);
×
157
          }
×
158
        });
20✔
159
      }
20✔
160
    }
20✔
161
  }
52✔
162

163
  return foreignKeys;
52✔
164
}
52✔
165

166
/**
167
 * Extracts table metadata from the schema.
168
 * @param {AnyMySqlTable} table - The table schema
169
 * @returns {MetadataInfo} Object containing table metadata
170
 */
171
export function getTableMetadata(table: AnyMySqlTable): MetadataInfo {
2✔
172
  const symbols = Object.getOwnPropertySymbols(table);
52✔
173
  const nameSymbol = symbols.find((s) => s.toString().includes("Name"));
52✔
174
  const columnsSymbol = symbols.find((s) => s.toString().includes("Columns"));
52✔
175
  const foreignKeysSymbol = symbols.find((s) => s.toString().includes("ForeignKeys)"));
52✔
176
  const extraSymbol = symbols.find((s) => s.toString().includes("ExtraConfigBuilder"));
52✔
177

178
  // Initialize builders arrays
179
  const builders = {
52✔
180
    indexes: [] as AnyIndexBuilder[],
52✔
181
    checks: [] as CheckBuilder[],
52✔
182
    foreignKeys: [] as ForeignKeyBuilder[],
52✔
183
    primaryKeys: [] as PrimaryKeyBuilder[],
52✔
184
    uniqueConstraints: [] as UniqueConstraintBuilder[],
52✔
185
    extras: [] as any[],
52✔
186
  };
52✔
187

188
  // Process foreign keys
189
  builders.foreignKeys = processForeignKeys(table, foreignKeysSymbol, extraSymbol);
52✔
190

191
  // Process extra configuration if available
192
  if (extraSymbol) {
52✔
193
    // @ts-ignore
194
    const extraConfigBuilder = table[extraSymbol];
52✔
195
    if (extraConfigBuilder && typeof extraConfigBuilder === "function") {
52✔
196
      const configBuilderData = extraConfigBuilder(table);
20✔
197
      if (configBuilderData) {
20✔
198
        // Convert configBuilderData to array if it's an object
199
        const configBuilders = Array.isArray(configBuilderData)
20✔
200
          ? configBuilderData
20!
201
          : Object.values(configBuilderData).map(
×
NEW
202
              (item) => (item as ConfigBuilderData).value ?? item,
×
203
            );
×
204

205
        // Process each builder
206
        configBuilders.forEach((builder) => {
20✔
207
          if (!builder?.constructor) return;
20!
208

209
          const builderName = builder.constructor.name.toLowerCase();
20✔
210

211
          // Map builder types to their corresponding arrays
212
          const builderMap = {
20✔
213
            indexbuilder: builders.indexes,
20✔
214
            checkbuilder: builders.checks,
20✔
215
            primarykeybuilder: builders.primaryKeys,
20✔
216
            uniqueconstraintbuilder: builders.uniqueConstraints,
20✔
217
          };
20✔
218

219
          // Add builder to appropriate array if it matches any type
220
          for (const [type, array] of Object.entries(builderMap)) {
20✔
221
            if (builderName.includes(type)) {
60✔
222
              array.push(builder);
20✔
223
              break;
20✔
224
            }
20✔
225
          }
60✔
226

227
          // Always add to extras array
228
          builders.extras.push(builder);
20✔
229
        });
20✔
230
      }
20✔
231
    }
20✔
232
  }
52✔
233

234
  return {
52✔
235
    tableName: nameSymbol ? (table as any)[nameSymbol] : "",
52!
236
    columns: columnsSymbol ? ((table as any)[columnsSymbol] as Record<string, AnyColumn>) : {},
52!
237
    ...builders,
52✔
238
  };
52✔
239
}
52✔
240

241
/**
242
 * Generates SQL statements to drop tables
243
 * @param tables - Array of table names
244
 * @returns Array of SQL statements for dropping tables
245
 */
246
export function generateDropTableStatements(tables: string[]): string[] {
2✔
247
  const dropStatements: string[] = [];
×
248

NEW
249
  tables.forEach((tableName) => {
×
NEW
250
    dropStatements.push(`DROP TABLE IF EXISTS \`${tableName}\`;`);
×
UNCOV
251
  });
×
252

253
  // Add statement to clear migrations table
254
  dropStatements.push(`DELETE FROM __migrations;`);
×
255

256
  return dropStatements;
×
257
}
×
258

259
type AliasColumnMap = Record<string, AnyColumn>;
260

261
function mapSelectTableToAlias(
12✔
262
  table: MySqlTable,
12✔
263
  uniqPrefix: string,
12✔
264
  aliasMap: AliasColumnMap,
12✔
265
): any {
12✔
266
  const { columns, tableName } = getTableMetadata(table);
12✔
267
  const selectionsTableFields: Record<string, unknown> = {};
12✔
268
  Object.keys(columns).forEach((name) => {
12✔
269
    const column = columns[name] as AnyColumn;
46✔
270
    const uniqName = `a_${uniqPrefix}_${tableName}_${column.name}`.toLowerCase();
46✔
271
    const fieldAlias = sql.raw(uniqName);
46✔
272
    selectionsTableFields[name] = sql`${column} as \`${fieldAlias}\``;
46✔
273
    aliasMap[uniqName] = column;
46✔
274
  });
12✔
275
  return selectionsTableFields;
12✔
276
}
12✔
277

278
function isDrizzleColumn(column: any): boolean {
52✔
279
  return column && typeof column === "object" && "table" in column;
52✔
280
}
52✔
281

282
export function mapSelectAllFieldsToAlias(
2✔
283
  selections: any,
64✔
284
  name: string,
64✔
285
  uniqName: string,
64✔
286
  fields: any,
64✔
287
  aliasMap: AliasColumnMap,
64✔
288
): any {
64✔
289
  if (isTable(fields)) {
64✔
290
    selections[name] = mapSelectTableToAlias(fields as MySqlTable, uniqName, aliasMap);
12✔
291
  } else if (isDrizzleColumn(fields)) {
64✔
292
    const column = fields as Column;
28✔
293
    const uniqAliasName = `a_${uniqName}_${column.name}`.toLowerCase();
28✔
294
    let aliasName = sql.raw(uniqAliasName);
28✔
295
    selections[name] = sql`${column} as \`${aliasName}\``;
28✔
296
    aliasMap[uniqAliasName] = column;
28✔
297
  } else if (isSQLWrapper(fields)) {
52✔
298
    selections[name] = fields;
10✔
299
  } else {
24✔
300
    const innerSelections: any = {};
14✔
301
    Object.entries(fields).forEach(([iname, ifields]) => {
14✔
302
      mapSelectAllFieldsToAlias(innerSelections, iname, `${uniqName}_${iname}`, ifields, aliasMap);
28✔
303
    });
14✔
304
    selections[name] = innerSelections;
14✔
305
  }
14✔
306
  return selections;
64✔
307
}
64✔
308
export function mapSelectFieldsWithAlias<TSelection extends SelectedFields>(
2✔
309
  fields: TSelection,
12✔
310
): { selections: TSelection; aliasMap: AliasColumnMap } {
12✔
311
  if (!fields) {
12!
312
    throw new Error("fields is empty");
×
313
  }
×
314
  const aliasMap: AliasColumnMap = {};
12✔
315
  const selections: any = {};
12✔
316
  Object.entries(fields).forEach(([name, fields]) => {
12✔
317
    mapSelectAllFieldsToAlias(selections, name, name, fields, aliasMap);
36✔
318
  });
12✔
319
  return { selections, aliasMap };
12✔
320
}
12✔
321

322
function getAliasFromDrizzleAlias(value: unknown): string | undefined {
220✔
323
  const isSQL =
220✔
324
    value !== null && typeof value === "object" && isSQLWrapper(value) && "queryChunks" in value;
220✔
325
  if (isSQL) {
220✔
326
    const sql = value as SQL;
168✔
327
    const queryChunks = sql.queryChunks;
168✔
328
    if (queryChunks.length > 3) {
168✔
329
      const aliasNameChunk = queryChunks[queryChunks.length - 2];
148✔
330
      if (isSQLWrapper(aliasNameChunk) && "queryChunks" in aliasNameChunk) {
148✔
331
        const aliasNameChunkSql = aliasNameChunk as SQL;
148✔
332
        if (aliasNameChunkSql.queryChunks?.length === 1 && aliasNameChunkSql.queryChunks[0]) {
148✔
333
          const queryChunksStringChunc = aliasNameChunkSql.queryChunks[0];
148✔
334
          if ("value" in queryChunksStringChunc) {
148✔
335
            const values = (queryChunksStringChunc as StringChunk).value;
148✔
336
            if (values && values.length === 1) {
148✔
337
              return values[0];
148✔
338
            }
148✔
339
          }
148✔
340
        }
148✔
341
      }
148✔
342
    }
148✔
343
  }
168✔
344
  return undefined;
72✔
345
}
72✔
346

347
function transformValue(
148✔
348
  value: unknown,
148✔
349
  alias: string,
148✔
350
  aliasMap: Record<string, AnyColumn>,
148✔
351
): unknown {
148✔
352
  const column = aliasMap[alias];
148✔
353
  if (!column) return value;
148!
354

355
  let customColumn = column as MySqlCustomColumn<any>;
148✔
356
  // @ts-ignore
357
  const fromDriver = customColumn?.mapFrom;
148✔
358
  if (fromDriver && value !== null && value !== undefined) {
148✔
359
    return fromDriver(value);
28✔
360
  }
28✔
361
  return value;
120✔
362
}
120✔
363

364
function transformObject(
76✔
365
  obj: Record<string, unknown>,
76✔
366
  selections: Record<string, unknown>,
76✔
367
  aliasMap: Record<string, AnyColumn>,
76✔
368
): Record<string, unknown> {
76✔
369
  const result: Record<string, unknown> = {};
76✔
370

371
  for (const [key, value] of Object.entries(obj)) {
76✔
372
    const selection = selections[key];
220✔
373
    const alias = getAliasFromDrizzleAlias(selection);
220✔
374
    if (alias && aliasMap[alias]) {
220✔
375
      result[key] = transformValue(value, alias, aliasMap);
148✔
376
    } else if (selection && typeof selection === "object" && !isSQLWrapper(selection)) {
220✔
377
      result[key] = transformObject(
52✔
378
        value as Record<string, unknown>,
52✔
379
        selection as Record<string, unknown>,
52✔
380
        aliasMap,
52✔
381
      );
52✔
382
    } else {
72✔
383
      result[key] = value;
20✔
384
    }
20✔
385
  }
220✔
386

387
  return result;
76✔
388
}
76✔
389

390
export function applyFromDriverTransform<T, TSelection>(
2✔
391
  rows: T[],
12✔
392
  selections: TSelection,
12✔
393
  aliasMap: Record<string, AnyColumn>,
12✔
394
): T[] {
12✔
395
  return rows.map((row) => {
12✔
396
    const transformed = transformObject(
24✔
397
      row as Record<string, unknown>,
24✔
398
      selections as Record<string, unknown>,
24✔
399
      aliasMap,
24✔
400
    ) as Record<string, unknown>;
24✔
401

402
    return processNullBranches(transformed) as unknown as T;
24✔
403
  });
12✔
404
}
12✔
405

406
function processNullBranches(obj: Record<string, unknown>): Record<string, unknown> | null {
84✔
407
  if (obj === null || typeof obj !== "object") {
84!
408
    return obj;
×
409
  }
×
410

411
  // Skip built-in objects like Date, Array, etc.
412
  if (obj.constructor && obj.constructor.name !== "Object") {
84✔
413
    return obj;
8✔
414
  }
8✔
415

416
  const result: Record<string, unknown> = {};
76✔
417
  let allNull = true;
76✔
418

419
  for (const [key, value] of Object.entries(obj)) {
84✔
420
    if (value === null || value === undefined) {
220✔
421
      result[key] = null;
10✔
422
      continue;
10✔
423
    }
10✔
424

425
    if (typeof value === "object") {
220✔
426
      const processed = processNullBranches(value as Record<string, unknown>);
60✔
427
      result[key] = processed;
60✔
428
      if (processed !== null) {
60✔
429
        allNull = false;
58✔
430
      }
58✔
431
    } else {
220✔
432
      result[key] = value;
150✔
433
      allNull = false;
150✔
434
    }
150✔
435
  }
220✔
436

437
  return allNull ? null : result;
84✔
438
}
84✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc