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

vzakharchenko / forge-sql-orm / 17839768936

18 Sep 2025 07:50PM UTC coverage: 87.204% (-0.2%) from 87.43%
17839768936

push

github

vzakharchenko
fix timestamp issue

463 of 559 branches covered (82.83%)

Branch coverage included in aggregate %.

2045 of 2317 relevant lines covered (88.26%)

14.86 hits per line

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

85.51
/src/utils/sqlUtils.ts
1
import { AnyColumn, Column, isTable, SQL, sql, StringChunk } from "drizzle-orm";
1✔
2
import { AnyMySqlTable, MySqlCustomColumn } from "drizzle-orm/mysql-core/index";
3
import { DateTime } from "luxon";
1✔
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";
1✔
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 or Date
46
 * @param format - The format to use for parsing
47
 * @returns Date object
48
 */
49

50
export const parseDateTime = (value: string | Date, format: string): Date => {
1✔
51
  let result: Date;
13✔
52
  if (value instanceof Date) {
13✔
53
    result = value;
1✔
54
  } else {
13✔
55
    // 1. Try to parse using the provided format (strict mode)
56
    const dt = DateTime.fromFormat(value, format);
12✔
57
    if (dt.isValid) {
12✔
58
      result = dt.toJSDate();
2✔
59
    } else {
12✔
60
      // 2. Try to parse as SQL string
61
      const sqlDt = DateTime.fromSQL(value);
10✔
62
      if (sqlDt.isValid) {
10✔
63
        result = sqlDt.toJSDate();
4✔
64
      } else {
10✔
65
        // 3. Try to parse as RFC2822 string
66
        const isoDt = DateTime.fromRFC2822(value);
6✔
67
        if (isoDt.isValid) {
6!
68
          result = isoDt.toJSDate();
×
69
        } else {
6✔
70
          // 4. Fallback: use native Date constructor
71
          result = new Date(value);
6✔
72
        }
6✔
73
      }
6✔
74
    }
10✔
75
  }
12✔
76
  // 4. Ensure the result is a valid Date object
77
  if (isNaN(result.getTime())) {
13✔
78
    result = new Date(value);
4✔
79
  }
4✔
80
  return result;
13✔
81
};
13✔
82

83
/**
84
 * Helper function to validate and format a date-like value using Luxon DateTime.
85
 * @param value - Date object, ISO/RFC2822/SQL/HTTP string, or timestamp (number|string).
86
 * @param format - DateTime format string (Luxon format tokens).
87
 * @returns Formatted date string.
88
 * @throws Error if value cannot be parsed as a valid date.
89
 */
90
export function formatDateTime(
1✔
91
  value: Date | string | number,
13✔
92
  format: string,
13✔
93
  isTimeStamp: boolean,
13✔
94
): string {
13✔
95
  let dt: DateTime | null = null;
13✔
96

97
  if (value instanceof Date) {
13✔
98
    dt = DateTime.fromJSDate(value);
13✔
99
  } else if (typeof value === "string") {
13!
100
    for (const parser of [
×
101
      DateTime.fromISO,
×
102
      DateTime.fromRFC2822,
×
103
      DateTime.fromSQL,
×
104
      DateTime.fromHTTP,
×
105
    ]) {
×
106
      dt = parser(value);
×
107
      if (dt.isValid) break;
×
108
    }
×
109
    if (!dt?.isValid) {
×
110
      const parsed = Number(value);
×
111
      if (!isNaN(parsed)) {
×
112
        dt = DateTime.fromMillis(parsed);
×
113
      }
×
114
    }
×
115
  } else if (typeof value === "number") {
×
116
    dt = DateTime.fromMillis(value);
×
117
  } else {
×
118
    throw new Error("Unsupported type");
×
119
  }
×
120

121
  if (!dt?.isValid) {
13!
122
    throw new Error("Invalid Date");
×
123
  }
×
124
  const minDate = DateTime.fromSeconds(1);
13✔
125
  const maxDate = DateTime.fromMillis(2147483647 * 1000); // 2038-01-19 03:14:07.999 UTC
13✔
126

127
  if (isTimeStamp) {
13✔
128
    if (dt < minDate) {
2!
129
      throw new Error(
×
130
        "Atlassian Forge does not support zero or negative timestamps. Allowed range: from '1970-01-01 00:00:01.000000' to '2038-01-19 03:14:07.999999'.",
×
131
      );
×
132
    }
×
133
    if (dt > maxDate) {
2!
134
      throw new Error(
×
135
        "Atlassian Forge does not support timestamps beyond 2038-01-19 03:14:07.999999. Please use a smaller date within the supported range.",
×
136
      );
×
137
    }
×
138
  }
2✔
139

140
  return dt.toFormat(format);
13✔
141
}
13✔
142

143
/**
144
 * Gets primary keys from the schema.
145
 * @template T - The type of the table schema
146
 * @param {T} table - The table schema
147
 * @returns {[string, AnyColumn][]} Array of primary key name and column pairs
148
 */
149
export function getPrimaryKeys<T extends AnyMySqlTable>(table: T): [string, AnyColumn][] {
1✔
150
  const { columns, primaryKeys } = getTableMetadata(table);
14✔
151

152
  // First try to find primary keys in columns
153
  const columnPrimaryKeys = Object.entries(columns).filter(([, column]) => column.primary) as [
14✔
154
    string,
155
    AnyColumn,
156
  ][];
157

158
  if (columnPrimaryKeys.length > 0) {
14✔
159
    return columnPrimaryKeys;
10✔
160
  }
10✔
161

162
  // If no primary keys found in columns, check primary key builders
163
  if (Array.isArray(primaryKeys) && primaryKeys.length > 0) {
14✔
164
    // Collect all primary key columns from all primary key builders
165
    const primaryKeyColumns = new Set<[string, AnyColumn]>();
4✔
166

167
    primaryKeys.forEach((primaryKeyBuilder) => {
4✔
168
      // Get primary key columns from each builder
169
      Object.entries(columns)
4✔
170
        .filter(([, column]) => {
4✔
171
          // @ts-ignore - PrimaryKeyBuilder has internal columns property
172
          return primaryKeyBuilder.columns.includes(column);
8✔
173
        })
4✔
174
        .forEach(([name, column]) => {
4✔
175
          primaryKeyColumns.add([name, column]);
4✔
176
        });
4✔
177
    });
4✔
178

179
    return Array.from(primaryKeyColumns);
4✔
180
  }
4!
181

182
  return [];
×
183
}
×
184

185
/**
186
 * Processes foreign keys from both foreignKeysSymbol and extraSymbol
187
 * @param table - The table schema
188
 * @param foreignKeysSymbol - Symbol for foreign keys
189
 * @param extraSymbol - Symbol for extra configuration
190
 * @returns Array of foreign key builders
191
 */
192
function processForeignKeys(
46✔
193
  table: AnyMySqlTable,
46✔
194
  foreignKeysSymbol: symbol | undefined,
46✔
195
  extraSymbol: symbol | undefined,
46✔
196
): ForeignKeyBuilder[] {
46✔
197
  const foreignKeys: ForeignKeyBuilder[] = [];
46✔
198

199
  // Process foreign keys from foreignKeysSymbol
200
  if (foreignKeysSymbol) {
46✔
201
    // @ts-ignore
202
    const fkArray: any[] = table[foreignKeysSymbol];
46✔
203
    if (fkArray) {
46✔
204
      fkArray.forEach((fk) => {
46✔
205
        if (fk.reference) {
×
206
          const item = fk.reference(fk);
×
207
          foreignKeys.push(item);
×
208
        }
×
209
      });
46✔
210
    }
46✔
211
  }
46✔
212

213
  // Process foreign keys from extraSymbol
214
  if (extraSymbol) {
46✔
215
    // @ts-ignore
216
    const extraConfigBuilder = table[extraSymbol];
46✔
217
    if (extraConfigBuilder && typeof extraConfigBuilder === "function") {
46✔
218
      const configBuilderData = extraConfigBuilder(table);
21✔
219
      if (configBuilderData) {
21✔
220
        const configBuilders = Array.isArray(configBuilderData)
21✔
221
          ? configBuilderData
21!
222
          : Object.values(configBuilderData).map(
×
223
              (item) => (item as ConfigBuilderData).value ?? item,
×
224
            );
×
225

226
        configBuilders.forEach((builder) => {
21✔
227
          if (!builder?.constructor) return;
21!
228

229
          const builderName = builder.constructor.name.toLowerCase();
21✔
230
          if (builderName.includes("foreignkeybuilder")) {
21!
231
            foreignKeys.push(builder);
×
232
          }
×
233
        });
21✔
234
      }
21✔
235
    }
21✔
236
  }
46✔
237

238
  return foreignKeys;
46✔
239
}
46✔
240

241
/**
242
 * Extracts table metadata from the schema.
243
 * @param {AnyMySqlTable} table - The table schema
244
 * @returns {MetadataInfo} Object containing table metadata
245
 */
246
export function getTableMetadata(table: AnyMySqlTable): MetadataInfo {
1✔
247
  const symbols = Object.getOwnPropertySymbols(table);
46✔
248
  const nameSymbol = symbols.find((s) => s.toString().includes("Name"));
46✔
249
  const columnsSymbol = symbols.find((s) => s.toString().includes("Columns"));
46✔
250
  const foreignKeysSymbol = symbols.find((s) => s.toString().includes("ForeignKeys)"));
46✔
251
  const extraSymbol = symbols.find((s) => s.toString().includes("ExtraConfigBuilder"));
46✔
252

253
  // Initialize builders arrays
254
  const builders = {
46✔
255
    indexes: [] as AnyIndexBuilder[],
46✔
256
    checks: [] as CheckBuilder[],
46✔
257
    foreignKeys: [] as ForeignKeyBuilder[],
46✔
258
    primaryKeys: [] as PrimaryKeyBuilder[],
46✔
259
    uniqueConstraints: [] as UniqueConstraintBuilder[],
46✔
260
    extras: [] as any[],
46✔
261
  };
46✔
262

263
  // Process foreign keys
264
  builders.foreignKeys = processForeignKeys(table, foreignKeysSymbol, extraSymbol);
46✔
265

266
  // Process extra configuration if available
267
  if (extraSymbol) {
46✔
268
    // @ts-ignore
269
    const extraConfigBuilder = table[extraSymbol];
46✔
270
    if (extraConfigBuilder && typeof extraConfigBuilder === "function") {
46✔
271
      const configBuilderData = extraConfigBuilder(table);
21✔
272
      if (configBuilderData) {
21✔
273
        // Convert configBuilderData to array if it's an object
274
        const configBuilders = Array.isArray(configBuilderData)
21✔
275
          ? configBuilderData
21!
276
          : Object.values(configBuilderData).map(
×
277
              (item) => (item as ConfigBuilderData).value ?? item,
×
278
            );
×
279

280
        // Process each builder
281
        configBuilders.forEach((builder) => {
21✔
282
          if (!builder?.constructor) return;
21!
283

284
          const builderName = builder.constructor.name.toLowerCase();
21✔
285

286
          // Map builder types to their corresponding arrays
287
          const builderMap = {
21✔
288
            indexbuilder: builders.indexes,
21✔
289
            checkbuilder: builders.checks,
21✔
290
            primarykeybuilder: builders.primaryKeys,
21✔
291
            uniqueconstraintbuilder: builders.uniqueConstraints,
21✔
292
          };
21✔
293

294
          // Add builder to appropriate array if it matches any type
295
          for (const [type, array] of Object.entries(builderMap)) {
21✔
296
            if (builderName.includes(type)) {
63✔
297
              array.push(builder);
21✔
298
              break;
21✔
299
            }
21✔
300
          }
63✔
301

302
          // Always add to extras array
303
          builders.extras.push(builder);
21✔
304
        });
21✔
305
      }
21✔
306
    }
21✔
307
  }
46✔
308

309
  return {
46✔
310
    tableName: nameSymbol ? (table as any)[nameSymbol] : "",
46!
311
    columns: columnsSymbol ? ((table as any)[columnsSymbol] as Record<string, AnyColumn>) : {},
46!
312
    ...builders,
46✔
313
  };
46✔
314
}
46✔
315

316
/**
317
 * Generates SQL statements for dropping tables and/or their sequences.
318
 *
319
 * @param tables - List of table names to generate DROP statements for.
320
 * @param options - Configuration object:
321
 *   - sequence: whether to drop associated sequences (default: true)
322
 *   - table: whether to drop tables themselves (default: true)
323
 * @returns Array of SQL statements for dropping the specified objects
324
 */
325
export function generateDropTableStatements(
1✔
326
  tables: string[],
4✔
327
  options?: { sequence: boolean; table: boolean },
4✔
328
): string[] {
4✔
329
  const dropStatements: string[] = [];
4✔
330
  const validOptions = options ?? { sequence: true, table: true };
4✔
331
  if (!validOptions.sequence && !validOptions.table) {
4!
332
    console.warn('No drop operations requested: both "table" and "sequence" options are false');
×
333
    return [];
×
334
  }
×
335
  tables.forEach((tableName) => {
4✔
336
    if (validOptions.table) {
6✔
337
      dropStatements.push(`DROP TABLE IF EXISTS \`${tableName}\`;`);
6✔
338
    }
6✔
339
    if (validOptions.sequence) {
6✔
340
      dropStatements.push(`DROP SEQUENCE IF EXISTS \`${tableName}\`;`);
6✔
341
    }
6✔
342
  });
4✔
343

344
  return dropStatements;
4✔
345
}
4✔
346

347
type AliasColumnMap = Record<string, AnyColumn>;
348

349
function mapSelectTableToAlias(
10✔
350
  table: MySqlTable,
10✔
351
  uniqPrefix: string,
10✔
352
  aliasMap: AliasColumnMap,
10✔
353
): any {
10✔
354
  const { columns, tableName } = getTableMetadata(table);
10✔
355
  const selectionsTableFields: Record<string, unknown> = {};
10✔
356
  Object.keys(columns).forEach((name) => {
10✔
357
    const column = columns[name] as AnyColumn;
39✔
358
    const uniqName = `a_${uniqPrefix}_${tableName}_${column.name}`.toLowerCase();
39✔
359
    const fieldAlias = sql.raw(uniqName);
39✔
360
    selectionsTableFields[name] = sql`${column} as \`${fieldAlias}\``;
39✔
361
    aliasMap[uniqName] = column;
39✔
362
  });
10✔
363
  return selectionsTableFields;
10✔
364
}
10✔
365

366
function isDrizzleColumn(column: any): boolean {
79✔
367
  return column && typeof column === "object" && "table" in column;
79✔
368
}
79✔
369

370
export function mapSelectAllFieldsToAlias(
1✔
371
  selections: any,
89✔
372
  name: string,
89✔
373
  uniqName: string,
89✔
374
  fields: any,
89✔
375
  aliasMap: AliasColumnMap,
89✔
376
): any {
89✔
377
  if (isTable(fields)) {
89✔
378
    selections[name] = mapSelectTableToAlias(fields as MySqlTable, uniqName, aliasMap);
10✔
379
  } else if (isDrizzleColumn(fields)) {
89✔
380
    const column = fields as Column;
58✔
381
    const uniqAliasName = `a_${uniqName}_${column.name}`.toLowerCase();
58✔
382
    let aliasName = sql.raw(uniqAliasName);
58✔
383
    selections[name] = sql`${column} as \`${aliasName}\``;
58✔
384
    aliasMap[uniqAliasName] = column;
58✔
385
  } else if (isSQLWrapper(fields)) {
79✔
386
    selections[name] = fields;
10✔
387
  } else {
21✔
388
    const innerSelections: any = {};
11✔
389
    Object.entries(fields).forEach(([iname, ifields]) => {
11✔
390
      mapSelectAllFieldsToAlias(innerSelections, iname, `${uniqName}_${iname}`, ifields, aliasMap);
22✔
391
    });
11✔
392
    selections[name] = innerSelections;
11✔
393
  }
11✔
394
  return selections;
89✔
395
}
89✔
396
export function mapSelectFieldsWithAlias<TSelection extends SelectedFields>(
1✔
397
  fields: TSelection,
29✔
398
): { selections: TSelection; aliasMap: AliasColumnMap } {
29✔
399
  if (!fields) {
29!
400
    throw new Error("fields is empty");
×
401
  }
×
402
  const aliasMap: AliasColumnMap = {};
29✔
403
  const selections: any = {};
29✔
404
  Object.entries(fields).forEach(([name, fields]) => {
29✔
405
    mapSelectAllFieldsToAlias(selections, name, name, fields, aliasMap);
67✔
406
  });
29✔
407
  return { selections, aliasMap };
29✔
408
}
29✔
409

410
function getAliasFromDrizzleAlias(value: unknown): string | undefined {
200✔
411
  const isSQL =
200✔
412
    value !== null && typeof value === "object" && isSQLWrapper(value) && "queryChunks" in value;
200✔
413
  if (isSQL) {
200✔
414
    const sql = value as SQL;
158✔
415
    const queryChunks = sql.queryChunks;
158✔
416
    if (queryChunks.length > 3) {
158✔
417
      const aliasNameChunk = queryChunks[queryChunks.length - 2];
140✔
418
      if (isSQLWrapper(aliasNameChunk) && "queryChunks" in aliasNameChunk) {
140✔
419
        const aliasNameChunkSql = aliasNameChunk as SQL;
140✔
420
        if (aliasNameChunkSql.queryChunks?.length === 1 && aliasNameChunkSql.queryChunks[0]) {
140✔
421
          const queryChunksStringChunc = aliasNameChunkSql.queryChunks[0];
140✔
422
          if ("value" in queryChunksStringChunc) {
140✔
423
            const values = (queryChunksStringChunc as StringChunk).value;
140✔
424
            if (values && values.length === 1) {
140✔
425
              return values[0];
140✔
426
            }
140✔
427
          }
140✔
428
        }
140✔
429
      }
140✔
430
    }
140✔
431
  }
158✔
432
  return undefined;
60✔
433
}
60✔
434

435
function transformValue(
140✔
436
  value: unknown,
140✔
437
  alias: string,
140✔
438
  aliasMap: Record<string, AnyColumn>,
140✔
439
): unknown {
140✔
440
  const column = aliasMap[alias];
140✔
441
  if (!column) return value;
140!
442

443
  let customColumn = column as MySqlCustomColumn<any>;
140✔
444
  // @ts-ignore
445
  const fromDriver = customColumn?.mapFrom;
140✔
446
  if (fromDriver && value !== null && value !== undefined) {
140✔
447
    return fromDriver(value);
26✔
448
  }
26✔
449
  return value;
114✔
450
}
114✔
451

452
function transformObject(
71✔
453
  obj: Record<string, unknown>,
71✔
454
  selections: Record<string, unknown>,
71✔
455
  aliasMap: Record<string, AnyColumn>,
71✔
456
): Record<string, unknown> {
71✔
457
  const result: Record<string, unknown> = {};
71✔
458

459
  for (const [key, value] of Object.entries(obj)) {
71✔
460
    const selection = selections[key];
200✔
461
    const alias = getAliasFromDrizzleAlias(selection);
200✔
462
    if (alias && aliasMap[alias]) {
200✔
463
      result[key] = transformValue(value, alias, aliasMap);
140✔
464
    } else if (selection && typeof selection === "object" && !isSQLWrapper(selection)) {
200✔
465
      result[key] = transformObject(
42✔
466
        value as Record<string, unknown>,
42✔
467
        selection as Record<string, unknown>,
42✔
468
        aliasMap,
42✔
469
      );
42✔
470
    } else {
60✔
471
      result[key] = value;
18✔
472
    }
18✔
473
  }
200✔
474

475
  return result;
71✔
476
}
71✔
477

478
export function applyFromDriverTransform<T, TSelection>(
1✔
479
  rows: T[],
21✔
480
  selections: TSelection,
21✔
481
  aliasMap: Record<string, AnyColumn>,
21✔
482
): T[] {
21✔
483
  return rows.map((row) => {
21✔
484
    const transformed = transformObject(
29✔
485
      row as Record<string, unknown>,
29✔
486
      selections as Record<string, unknown>,
29✔
487
      aliasMap,
29✔
488
    ) as Record<string, unknown>;
29✔
489

490
    return processNullBranches(transformed) as unknown as T;
29✔
491
  });
21✔
492
}
21✔
493

494
function processNullBranches(obj: Record<string, unknown>): Record<string, unknown> | null {
79✔
495
  if (obj === null || typeof obj !== "object") {
79!
496
    return obj;
×
497
  }
×
498

499
  // Skip built-in objects like Date, Array, etc.
500
  if (obj.constructor && obj.constructor.name !== "Object") {
79✔
501
    return obj;
8✔
502
  }
8✔
503

504
  const result: Record<string, unknown> = {};
71✔
505
  let allNull = true;
71✔
506

507
  for (const [key, value] of Object.entries(obj)) {
79✔
508
    if (value === null || value === undefined) {
200✔
509
      result[key] = null;
5✔
510
      continue;
5✔
511
    }
5✔
512

513
    if (typeof value === "object") {
200✔
514
      const processed = processNullBranches(value as Record<string, unknown>);
50✔
515
      result[key] = processed;
50✔
516
      if (processed !== null) {
50✔
517
        allNull = false;
49✔
518
      }
49✔
519
    } else {
196✔
520
      result[key] = value;
145✔
521
      allNull = false;
145✔
522
    }
145✔
523
  }
200✔
524

525
  return allNull ? null : result;
79✔
526
}
79✔
527

528
export function formatLimitOffset(limitOrOffset: number): number {
1✔
529
  if (typeof limitOrOffset !== "number" || isNaN(limitOrOffset)) {
5✔
530
    throw new Error("limitOrOffset must be a valid number");
2✔
531
  }
2✔
532
  return sql.raw(`${limitOrOffset}`) as unknown as number;
3✔
533
}
3✔
534

535
export function nextVal(sequenceName: string): number {
1✔
536
  return sql.raw(`NEXTVAL(${sequenceName})`) as unknown as number;
3✔
537
}
3✔
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