@plyaz/db 0.1.1 → 0.3.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/index.mjs CHANGED
@@ -3597,6 +3597,7 @@ var SupabaseAdapter = class {
3597
3597
  return ops[operator]();
3598
3598
  }
3599
3599
  };
3600
+ var SQL_ERROR_TRUNCATE_LENGTH = 500;
3600
3601
  var SQLAdapter = class {
3601
3602
  static {
3602
3603
  __name(this, "SQLAdapter");
@@ -3607,6 +3608,7 @@ var SQLAdapter = class {
3607
3608
  idColumnMap = /* @__PURE__ */ new Map();
3608
3609
  configIdColumns;
3609
3610
  defaultSchema;
3611
+ showSqlInErrors;
3610
3612
  /**
3611
3613
  * Creates a new SQLAdapter instance.
3612
3614
  * @param {SQLAdapterConfig} config - Configuration for the SQL adapter.
@@ -3620,6 +3622,7 @@ var SQLAdapter = class {
3620
3622
  constructor(config) {
3621
3623
  this.config = config;
3622
3624
  this.defaultSchema = config.schema ?? "public";
3625
+ this.showSqlInErrors = config.showSqlInErrors ?? true;
3623
3626
  this.pool = new Pool({
3624
3627
  connectionString: config.connectionString,
3625
3628
  ...config.pool
@@ -3738,16 +3741,16 @@ var SQLAdapter = class {
3738
3741
  const result = await this.pool.query(sql2, params);
3739
3742
  return result.rows;
3740
3743
  } catch (error) {
3741
- throw new DatabaseError(
3742
- `Failed to execute query: ${sql2} - ${error.message}`,
3743
- DATABASE_ERROR_CODES.QUERY_FAILED,
3744
- {
3745
- context: {
3746
- source: "SQLAdapter.query"
3747
- },
3748
- cause: error
3749
- }
3750
- );
3744
+ const truncatedSql = sql2.slice(0, SQL_ERROR_TRUNCATE_LENGTH);
3745
+ const sqlSuffix = sql2.length > SQL_ERROR_TRUNCATE_LENGTH ? "..." : "";
3746
+ const errorMessage = this.showSqlInErrors ? `SQL Error: ${error.message}
3747
+ Query: ${truncatedSql}${sqlSuffix}` : `SQL Error: ${error.message}`;
3748
+ throw new DatabaseError(errorMessage, DATABASE_ERROR_CODES.QUERY_FAILED, {
3749
+ context: {
3750
+ source: "SQLAdapter.query"
3751
+ },
3752
+ cause: error
3753
+ });
3751
3754
  }
3752
3755
  }
3753
3756
  /**
@@ -7188,6 +7191,17 @@ var BaseRepository = class {
7188
7191
  __name(this, "BaseRepository");
7189
7192
  }
7190
7193
  defaultConfig;
7194
+ /**
7195
+ * Get the table name for this repository
7196
+ *
7197
+ * Useful for transaction operations where you need the table name
7198
+ * to execute raw queries within a transaction context.
7199
+ *
7200
+ * @returns The table name this repository operates on
7201
+ */
7202
+ getTableName() {
7203
+ return this.tableName;
7204
+ }
7191
7205
  /**
7192
7206
  * Merges default repository config with per-operation config
7193
7207
  * Per-operation config takes precedence over default config
@@ -10418,6 +10432,10 @@ __name(DataValidationPipe, "DataValidationPipe");
10418
10432
  DataValidationPipe = __decorateClass([
10419
10433
  Injectable()
10420
10434
  ], DataValidationPipe);
10435
+ var DESCRIPTION_MAX_LENGTH = 60;
10436
+ var FALLBACK_DESCRIPTION_LENGTH = 50;
10437
+ var PROGRESS_LOG_INTERVAL = 10;
10438
+ var ERROR_MESSAGE_MAX_LENGTH = 300;
10421
10439
  var MigrationManager = class {
10422
10440
  static {
10423
10441
  __name(this, "MigrationManager");
@@ -10527,6 +10545,122 @@ var MigrationManager = class {
10527
10545
  }
10528
10546
  return { upSQL: sql2.trim(), downSQL: null };
10529
10547
  }
10548
+ /**
10549
+ * Process dollar-quoted string delimiters ($$ or $tag$)
10550
+ * Returns updated state for tracking if we're inside a dollar block
10551
+ */
10552
+ processDollarDelimiters(line, inDollarBlock, dollarTag) {
10553
+ const dollarMatch = line.match(/\$([a-zA-Z_]*)\$/g);
10554
+ if (!dollarMatch) return { inDollarBlock, dollarTag };
10555
+ let currentInBlock = inDollarBlock;
10556
+ let currentTag = dollarTag;
10557
+ for (const match of dollarMatch) {
10558
+ if (!currentInBlock) {
10559
+ currentInBlock = true;
10560
+ currentTag = match;
10561
+ } else if (match === currentTag) {
10562
+ currentInBlock = false;
10563
+ currentTag = "";
10564
+ }
10565
+ }
10566
+ return { inDollarBlock: currentInBlock, dollarTag: currentTag };
10567
+ }
10568
+ /**
10569
+ * Filter out comment-only statements
10570
+ */
10571
+ isNonCommentStatement(statement) {
10572
+ const withoutComments = statement.replace(/--.*$/gm, "").trim();
10573
+ return withoutComments.length > 0;
10574
+ }
10575
+ /**
10576
+ * Split SQL into individual statements for better error reporting
10577
+ * Handles $$ delimited blocks (functions, triggers) correctly
10578
+ */
10579
+ splitSqlStatements(sql2) {
10580
+ const statements = [];
10581
+ let current = "";
10582
+ let inDollarBlock = false;
10583
+ let dollarTag = "";
10584
+ for (const line of sql2.split("\n")) {
10585
+ const trimmedLine = line.trim();
10586
+ const isEmptyOrComment = trimmedLine === "" || trimmedLine.startsWith("--");
10587
+ current += line + "\n";
10588
+ if (isEmptyOrComment) continue;
10589
+ const dollarState = this.processDollarDelimiters(
10590
+ line,
10591
+ inDollarBlock,
10592
+ dollarTag
10593
+ );
10594
+ inDollarBlock = dollarState.inDollarBlock;
10595
+ dollarTag = dollarState.dollarTag;
10596
+ const isEndOfStatement = !inDollarBlock && trimmedLine.endsWith(";");
10597
+ if (isEndOfStatement && current.trim()) {
10598
+ statements.push(current.trim());
10599
+ current = "";
10600
+ }
10601
+ }
10602
+ if (current.trim()) {
10603
+ statements.push(current.trim());
10604
+ }
10605
+ return statements.filter((s) => this.isNonCommentStatement(s));
10606
+ }
10607
+ /**
10608
+ * Extract a short description from a SQL statement for logging
10609
+ */
10610
+ getStatementDescription(statement) {
10611
+ const firstLine = statement.split("\n").find((l) => l.trim() && !l.trim().startsWith("--"))?.trim() ?? "";
10612
+ const patterns = [
10613
+ /^(CREATE\s+(?:OR\s+REPLACE\s+)?(?:TABLE|INDEX|UNIQUE\s+INDEX|TYPE|FUNCTION|TRIGGER|EXTENSION|SCHEMA|VIEW|POLICY))\s+(?:IF\s+NOT\s+EXISTS\s+)?([^\s(]+)/i,
10614
+ /^(ALTER\s+TABLE)\s+([^\s]+)/i,
10615
+ /^(DROP\s+(?:TABLE|INDEX|TYPE|FUNCTION|TRIGGER|EXTENSION|SCHEMA|VIEW|POLICY))\s+(?:IF\s+EXISTS\s+)?([^\s(;]+)/i,
10616
+ /^(INSERT\s+INTO)\s+([^\s(]+)/i,
10617
+ /^(COMMENT\s+ON\s+(?:TABLE|COLUMN|INDEX|FUNCTION|TYPE))\s+([^\s]+)/i,
10618
+ /^(GRANT|REVOKE)\s+.+\s+ON\s+([^\s]+)/i
10619
+ ];
10620
+ for (const pattern of patterns) {
10621
+ const match = firstLine.match(pattern);
10622
+ if (match) {
10623
+ return `${match[1]} ${match[2]}`.slice(0, DESCRIPTION_MAX_LENGTH);
10624
+ }
10625
+ }
10626
+ const truncated = firstLine.slice(0, FALLBACK_DESCRIPTION_LENGTH);
10627
+ const suffix = firstLine.length > FALLBACK_DESCRIPTION_LENGTH ? "..." : "";
10628
+ return truncated + suffix;
10629
+ }
10630
+ /**
10631
+ * Execute SQL statements individually with better error reporting
10632
+ */
10633
+ async executeSqlStatements(adapter, sql2, migrationVersion) {
10634
+ const statements = this.splitSqlStatements(sql2);
10635
+ const total = statements.length;
10636
+ console.log(` → ${total} statements to execute`);
10637
+ for (let i = 0; i < statements.length; i++) {
10638
+ const statement = statements[i];
10639
+ const description = this.getStatementDescription(statement);
10640
+ try {
10641
+ await adapter.query(statement);
10642
+ const isInterval = (i + 1) % PROGRESS_LOG_INTERVAL === 0;
10643
+ const isLast = i === total - 1;
10644
+ const isSignificant = Boolean(
10645
+ description.match(/^(CREATE TABLE|CREATE FUNCTION|CREATE TRIGGER)/i)
10646
+ );
10647
+ if (isInterval || isLast || isSignificant) {
10648
+ console.log(` ✓ [${i + 1}/${total}] ${description}`);
10649
+ }
10650
+ } catch (error) {
10651
+ console.log(` ✗ [${i + 1}/${total}] ${description}`);
10652
+ const rawMessage = error.message;
10653
+ const errorMessage = rawMessage.replace(/^SQL Error:\s*/i, "").replace(/^Failed to execute query:.*?-\s*/i, "").slice(0, ERROR_MESSAGE_MAX_LENGTH);
10654
+ throw new DatabaseError(
10655
+ `Migration ${migrationVersion} failed at statement ${i + 1}/${total}:
10656
+ Statement: ${description}
10657
+ Error: ${errorMessage}`,
10658
+ DATABASE_ERROR_CODES.QUERY_FAILED,
10659
+ { cause: error }
10660
+ );
10661
+ }
10662
+ }
10663
+ }
10530
10664
  /**
10531
10665
  * Load SQL migration from file
10532
10666
  */
@@ -10538,12 +10672,20 @@ var MigrationManager = class {
10538
10672
  name: migrationFile.name,
10539
10673
  up: /* @__PURE__ */ __name(async (adapter) => {
10540
10674
  if (typeof adapter.query === "function") {
10541
- await adapter.query(upSQL);
10675
+ await this.executeSqlStatements(
10676
+ adapter,
10677
+ upSQL,
10678
+ migrationFile.version
10679
+ );
10542
10680
  }
10543
10681
  }, "up"),
10544
10682
  down: /* @__PURE__ */ __name(async (adapter) => {
10545
10683
  if (downSQL && typeof adapter.query === "function") {
10546
- await adapter.query(downSQL);
10684
+ await this.executeSqlStatements(
10685
+ adapter,
10686
+ downSQL,
10687
+ migrationFile.version
10688
+ );
10547
10689
  } else {
10548
10690
  console.warn(
10549
10691
  `[Migrations] No DOWN migration for ${migrationFile.version}`
@@ -10650,6 +10792,7 @@ var MigrationManager = class {
10650
10792
  /**
10651
10793
  * Run all pending migrations
10652
10794
  */
10795
+ /* eslint-disable max-depth, complexity */
10653
10796
  async up(targetVersion) {
10654
10797
  try {
10655
10798
  await this.initialize();
@@ -10670,9 +10813,15 @@ var MigrationManager = class {
10670
10813
  const migration = await this.loadMigration(migrationFile);
10671
10814
  const startTime = Date.now();
10672
10815
  if (typeof this.adapter.transaction === "function") {
10673
- await this.adapter.transaction(async () => {
10816
+ const txResult = await this.adapter.transaction(async () => {
10674
10817
  await migration.up(this.adapter);
10675
10818
  });
10819
+ if (!txResult.success) {
10820
+ throw txResult.error ?? new DatabaseError(
10821
+ `Migration ${migration.version} failed`,
10822
+ DATABASE_ERROR_CODES.QUERY_FAILED
10823
+ );
10824
+ }
10676
10825
  } else {
10677
10826
  await migration.up(this.adapter);
10678
10827
  }
@@ -10727,9 +10876,15 @@ var MigrationManager = class {
10727
10876
  const migration = await this.loadMigration(migrationFile);
10728
10877
  const startTime = Date.now();
10729
10878
  if (typeof this.adapter.transaction === "function") {
10730
- await this.adapter.transaction(async () => {
10879
+ const txResult = await this.adapter.transaction(async () => {
10731
10880
  await migration.down(this.adapter);
10732
10881
  });
10882
+ if (!txResult.success) {
10883
+ throw txResult.error ?? new DatabaseError(
10884
+ `Rollback ${appliedMigration.version} failed`,
10885
+ DATABASE_ERROR_CODES.QUERY_FAILED
10886
+ );
10887
+ }
10733
10888
  } else {
10734
10889
  await migration.down(this.adapter);
10735
10890
  }
@@ -10788,6 +10943,10 @@ var MigrationManager = class {
10788
10943
  }
10789
10944
  }
10790
10945
  };
10946
+ var DESCRIPTION_MAX_LENGTH2 = 60;
10947
+ var FALLBACK_DESCRIPTION_LENGTH2 = 50;
10948
+ var PROGRESS_LOG_INTERVAL2 = 10;
10949
+ var ERROR_MESSAGE_MAX_LENGTH2 = 300;
10791
10950
  var SeedManager = class {
10792
10951
  static {
10793
10952
  __name(this, "SeedManager");
@@ -10856,7 +11015,7 @@ var SeedManager = class {
10856
11015
  const files = fs.readdirSync(this.seedsPath);
10857
11016
  const seeds = [];
10858
11017
  for (const file of files) {
10859
- const match = file.match(/^(\d+)_(.+)\.(ts|js)$/);
11018
+ const match = file.match(/^(\d+)_(.+)\.(ts|js|sql)$/);
10860
11019
  if (match) {
10861
11020
  const [, order, name] = match;
10862
11021
  seeds.push({
@@ -10869,9 +11028,141 @@ var SeedManager = class {
10869
11028
  return seeds.sort((a, b) => a.order - b.order);
10870
11029
  }
10871
11030
  /**
10872
- * Load seed from file
11031
+ * Process dollar-quoted string delimiters ($$ or $tag$)
11032
+ */
11033
+ processDollarDelimiters(line, inDollarBlock, dollarTag) {
11034
+ const dollarMatch = line.match(/\$([a-zA-Z_]*)\$/g);
11035
+ if (!dollarMatch) return { inDollarBlock, dollarTag };
11036
+ let currentInBlock = inDollarBlock;
11037
+ let currentTag = dollarTag;
11038
+ for (const match of dollarMatch) {
11039
+ if (!currentInBlock) {
11040
+ currentInBlock = true;
11041
+ currentTag = match;
11042
+ } else if (match === currentTag) {
11043
+ currentInBlock = false;
11044
+ currentTag = "";
11045
+ }
11046
+ }
11047
+ return { inDollarBlock: currentInBlock, dollarTag: currentTag };
11048
+ }
11049
+ /**
11050
+ * Filter out comment-only statements
11051
+ */
11052
+ isNonCommentStatement(statement) {
11053
+ const withoutComments = statement.replace(/--.*$/gm, "").trim();
11054
+ return withoutComments.length > 0;
11055
+ }
11056
+ /**
11057
+ * Split SQL into individual statements for better error reporting
11058
+ */
11059
+ splitSqlStatements(sql2) {
11060
+ const statements = [];
11061
+ let current = "";
11062
+ let inDollarBlock = false;
11063
+ let dollarTag = "";
11064
+ for (const line of sql2.split("\n")) {
11065
+ const trimmedLine = line.trim();
11066
+ const isEmptyOrComment = trimmedLine === "" || trimmedLine.startsWith("--");
11067
+ current += line + "\n";
11068
+ if (isEmptyOrComment) continue;
11069
+ const dollarState = this.processDollarDelimiters(
11070
+ line,
11071
+ inDollarBlock,
11072
+ dollarTag
11073
+ );
11074
+ inDollarBlock = dollarState.inDollarBlock;
11075
+ dollarTag = dollarState.dollarTag;
11076
+ if (!inDollarBlock && trimmedLine.endsWith(";") && current.trim()) {
11077
+ statements.push(current.trim());
11078
+ current = "";
11079
+ }
11080
+ }
11081
+ if (current.trim()) {
11082
+ statements.push(current.trim());
11083
+ }
11084
+ return statements.filter((s) => this.isNonCommentStatement(s));
11085
+ }
11086
+ /**
11087
+ * Extract a short description from a SQL statement for logging
11088
+ */
11089
+ getStatementDescription(statement) {
11090
+ const firstLine = statement.split("\n").find((l) => l.trim() && !l.trim().startsWith("--"))?.trim() ?? "";
11091
+ const patterns = [
11092
+ /^(CREATE\s+(?:OR\s+REPLACE\s+)?(?:TABLE|INDEX|UNIQUE\s+INDEX|TYPE|FUNCTION|TRIGGER|EXTENSION|SCHEMA|VIEW|POLICY))\s+(?:IF\s+NOT\s+EXISTS\s+)?([^\s(]+)/i,
11093
+ /^(ALTER\s+TABLE)\s+([^\s]+)/i,
11094
+ /^(DROP\s+(?:TABLE|INDEX|TYPE|FUNCTION|TRIGGER|EXTENSION|SCHEMA|VIEW|POLICY))\s+(?:IF\s+EXISTS\s+)?([^\s(;]+)/i,
11095
+ /^(INSERT\s+INTO)\s+([^\s(]+)/i,
11096
+ /^(COMMENT\s+ON\s+(?:TABLE|COLUMN|INDEX|FUNCTION|TYPE))\s+([^\s]+)/i,
11097
+ /^(GRANT|REVOKE)\s+.+\s+ON\s+([^\s]+)/i
11098
+ ];
11099
+ for (const pattern of patterns) {
11100
+ const match = firstLine.match(pattern);
11101
+ if (match) {
11102
+ return `${match[1]} ${match[2]}`.slice(0, DESCRIPTION_MAX_LENGTH2);
11103
+ }
11104
+ }
11105
+ const truncated = firstLine.slice(0, FALLBACK_DESCRIPTION_LENGTH2);
11106
+ const suffix = firstLine.length > FALLBACK_DESCRIPTION_LENGTH2 ? "..." : "";
11107
+ return truncated + suffix;
11108
+ }
11109
+ /**
11110
+ * Execute SQL statements individually with better error reporting
11111
+ */
11112
+ async executeSqlStatements(sql2, seedName) {
11113
+ const statements = this.splitSqlStatements(sql2);
11114
+ const total = statements.length;
11115
+ console.log(` → ${total} statements to execute`);
11116
+ for (let i = 0; i < statements.length; i++) {
11117
+ const statement = statements[i];
11118
+ const description = this.getStatementDescription(statement);
11119
+ try {
11120
+ await this.adapter.query(statement);
11121
+ const isInterval = (i + 1) % PROGRESS_LOG_INTERVAL2 === 0;
11122
+ const isLast = i === total - 1;
11123
+ const isSignificant = Boolean(description.match(/^(INSERT INTO)/i));
11124
+ if (isInterval || isLast || isSignificant) {
11125
+ console.log(` ✓ [${i + 1}/${total}] ${description}`);
11126
+ }
11127
+ } catch (error) {
11128
+ console.log(` ✗ [${i + 1}/${total}] ${description}`);
11129
+ const rawMessage = error.message;
11130
+ const errorMessage = rawMessage.replace(/^SQL Error:\s*/i, "").replace(/^Failed to execute query:.*?-\s*/i, "").slice(0, ERROR_MESSAGE_MAX_LENGTH2);
11131
+ throw new DatabaseError(
11132
+ `Seed "${seedName}" failed at statement ${i + 1}/${total}:
11133
+ Statement: ${description}
11134
+ Error: ${errorMessage}`,
11135
+ DATABASE_ERROR_CODES.QUERY_FAILED,
11136
+ { cause: error }
11137
+ );
11138
+ }
11139
+ }
11140
+ }
11141
+ /**
11142
+ * Load SQL seed from file
10873
11143
  */
11144
+ loadSqlSeed(seedFile) {
11145
+ const sql2 = fs.readFileSync(seedFile.filePath, "utf-8");
11146
+ return {
11147
+ name: seedFile.name,
11148
+ run: /* @__PURE__ */ __name(async () => {
11149
+ if (typeof this.adapter.query === "function") {
11150
+ await this.executeSqlStatements(sql2, seedFile.name);
11151
+ }
11152
+ }, "run"),
11153
+ // SQL seeds don't have cleanup by default
11154
+ cleanup: void 0
11155
+ };
11156
+ }
11157
+ /**
11158
+ * Load seed from file (supports .ts, .js, and .sql)
11159
+ */
11160
+ // eslint-disable-next-line complexity
10874
11161
  async loadSeed(seedFile) {
11162
+ const ext = path.extname(seedFile.filePath);
11163
+ if (ext === ".sql") {
11164
+ return this.loadSqlSeed(seedFile);
11165
+ }
10875
11166
  const importPath = seedFile.filePath.startsWith("/") ? seedFile.filePath : new URL(`file:///${seedFile.filePath.replace(/\\/g, "/")}`).href;
10876
11167
  const seedModule = await import(importPath);
10877
11168
  return {
@@ -10925,9 +11216,15 @@ var SeedManager = class {
10925
11216
  */
10926
11217
  async executeSeed(seed) {
10927
11218
  if (typeof this.adapter.transaction === "function") {
10928
- await this.adapter.transaction(async () => {
11219
+ const txResult = await this.adapter.transaction(async () => {
10929
11220
  await seed.run(this.adapter);
10930
11221
  });
11222
+ if (!txResult.success) {
11223
+ throw txResult.error ?? new DatabaseError(
11224
+ `Seed ${seed.name} failed`,
11225
+ DATABASE_ERROR_CODES.QUERY_FAILED
11226
+ );
11227
+ }
10931
11228
  } else {
10932
11229
  await seed.run(this.adapter);
10933
11230
  }
@@ -10984,9 +11281,15 @@ var SeedManager = class {
10984
11281
  async executeCleanup(seed) {
10985
11282
  if (!seed.cleanup) return;
10986
11283
  if (typeof this.adapter.transaction === "function") {
10987
- await this.adapter.transaction(async () => {
11284
+ const txResult = await this.adapter.transaction(async () => {
10988
11285
  await seed.cleanup(this.adapter);
10989
11286
  });
11287
+ if (!txResult.success) {
11288
+ throw txResult.error ?? new DatabaseError(
11289
+ `Seed cleanup for ${seed.name} failed`,
11290
+ DATABASE_ERROR_CODES.QUERY_FAILED
11291
+ );
11292
+ }
10990
11293
  } else {
10991
11294
  await seed.cleanup(this.adapter);
10992
11295
  }