@plyaz/db 0.1.1 → 0.2.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
  /**
@@ -10418,6 +10421,10 @@ __name(DataValidationPipe, "DataValidationPipe");
10418
10421
  DataValidationPipe = __decorateClass([
10419
10422
  Injectable()
10420
10423
  ], DataValidationPipe);
10424
+ var DESCRIPTION_MAX_LENGTH = 60;
10425
+ var FALLBACK_DESCRIPTION_LENGTH = 50;
10426
+ var PROGRESS_LOG_INTERVAL = 10;
10427
+ var ERROR_MESSAGE_MAX_LENGTH = 300;
10421
10428
  var MigrationManager = class {
10422
10429
  static {
10423
10430
  __name(this, "MigrationManager");
@@ -10527,6 +10534,116 @@ var MigrationManager = class {
10527
10534
  }
10528
10535
  return { upSQL: sql2.trim(), downSQL: null };
10529
10536
  }
10537
+ /**
10538
+ * Process dollar-quoted string delimiters ($$ or $tag$)
10539
+ * Returns updated state for tracking if we're inside a dollar block
10540
+ */
10541
+ processDollarDelimiters(line, inDollarBlock, dollarTag) {
10542
+ const dollarMatch = line.match(/\$([a-zA-Z_]*)\$/g);
10543
+ if (!dollarMatch) return { inDollarBlock, dollarTag };
10544
+ let currentInBlock = inDollarBlock;
10545
+ let currentTag = dollarTag;
10546
+ for (const match of dollarMatch) {
10547
+ if (!currentInBlock) {
10548
+ currentInBlock = true;
10549
+ currentTag = match;
10550
+ } else if (match === currentTag) {
10551
+ currentInBlock = false;
10552
+ currentTag = "";
10553
+ }
10554
+ }
10555
+ return { inDollarBlock: currentInBlock, dollarTag: currentTag };
10556
+ }
10557
+ /**
10558
+ * Filter out comment-only statements
10559
+ */
10560
+ isNonCommentStatement(statement) {
10561
+ const withoutComments = statement.replace(/--.*$/gm, "").trim();
10562
+ return withoutComments.length > 0;
10563
+ }
10564
+ /**
10565
+ * Split SQL into individual statements for better error reporting
10566
+ * Handles $$ delimited blocks (functions, triggers) correctly
10567
+ */
10568
+ splitSqlStatements(sql2) {
10569
+ const statements = [];
10570
+ let current = "";
10571
+ let inDollarBlock = false;
10572
+ let dollarTag = "";
10573
+ for (const line of sql2.split("\n")) {
10574
+ const trimmedLine = line.trim();
10575
+ const isEmptyOrComment = trimmedLine === "" || trimmedLine.startsWith("--");
10576
+ current += line + "\n";
10577
+ if (isEmptyOrComment) continue;
10578
+ const dollarState = this.processDollarDelimiters(line, inDollarBlock, dollarTag);
10579
+ inDollarBlock = dollarState.inDollarBlock;
10580
+ dollarTag = dollarState.dollarTag;
10581
+ const isEndOfStatement = !inDollarBlock && trimmedLine.endsWith(";");
10582
+ if (isEndOfStatement && current.trim()) {
10583
+ statements.push(current.trim());
10584
+ current = "";
10585
+ }
10586
+ }
10587
+ if (current.trim()) {
10588
+ statements.push(current.trim());
10589
+ }
10590
+ return statements.filter((s) => this.isNonCommentStatement(s));
10591
+ }
10592
+ /**
10593
+ * Extract a short description from a SQL statement for logging
10594
+ */
10595
+ getStatementDescription(statement) {
10596
+ const firstLine = statement.split("\n").find((l) => l.trim() && !l.trim().startsWith("--"))?.trim() ?? "";
10597
+ const patterns = [
10598
+ /^(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,
10599
+ /^(ALTER\s+TABLE)\s+([^\s]+)/i,
10600
+ /^(DROP\s+(?:TABLE|INDEX|TYPE|FUNCTION|TRIGGER|EXTENSION|SCHEMA|VIEW|POLICY))\s+(?:IF\s+EXISTS\s+)?([^\s(;]+)/i,
10601
+ /^(INSERT\s+INTO)\s+([^\s(]+)/i,
10602
+ /^(COMMENT\s+ON\s+(?:TABLE|COLUMN|INDEX|FUNCTION|TYPE))\s+([^\s]+)/i,
10603
+ /^(GRANT|REVOKE)\s+.+\s+ON\s+([^\s]+)/i
10604
+ ];
10605
+ for (const pattern of patterns) {
10606
+ const match = firstLine.match(pattern);
10607
+ if (match) {
10608
+ return `${match[1]} ${match[2]}`.slice(0, DESCRIPTION_MAX_LENGTH);
10609
+ }
10610
+ }
10611
+ const truncated = firstLine.slice(0, FALLBACK_DESCRIPTION_LENGTH);
10612
+ const suffix = firstLine.length > FALLBACK_DESCRIPTION_LENGTH ? "..." : "";
10613
+ return truncated + suffix;
10614
+ }
10615
+ /**
10616
+ * Execute SQL statements individually with better error reporting
10617
+ */
10618
+ async executeSqlStatements(adapter, sql2, migrationVersion) {
10619
+ const statements = this.splitSqlStatements(sql2);
10620
+ const total = statements.length;
10621
+ console.log(` → ${total} statements to execute`);
10622
+ for (let i = 0; i < statements.length; i++) {
10623
+ const statement = statements[i];
10624
+ const description = this.getStatementDescription(statement);
10625
+ try {
10626
+ await adapter.query(statement);
10627
+ const isInterval = (i + 1) % PROGRESS_LOG_INTERVAL === 0;
10628
+ const isLast = i === total - 1;
10629
+ const isSignificant = Boolean(description.match(/^(CREATE TABLE|CREATE FUNCTION|CREATE TRIGGER)/i));
10630
+ if (isInterval || isLast || isSignificant) {
10631
+ console.log(` ✓ [${i + 1}/${total}] ${description}`);
10632
+ }
10633
+ } catch (error) {
10634
+ console.log(` ✗ [${i + 1}/${total}] ${description}`);
10635
+ const rawMessage = error.message;
10636
+ const errorMessage = rawMessage.replace(/^SQL Error:\s*/i, "").replace(/^Failed to execute query:.*?-\s*/i, "").slice(0, ERROR_MESSAGE_MAX_LENGTH);
10637
+ throw new DatabaseError(
10638
+ `Migration ${migrationVersion} failed at statement ${i + 1}/${total}:
10639
+ Statement: ${description}
10640
+ Error: ${errorMessage}`,
10641
+ DATABASE_ERROR_CODES.QUERY_FAILED,
10642
+ { cause: error }
10643
+ );
10644
+ }
10645
+ }
10646
+ }
10530
10647
  /**
10531
10648
  * Load SQL migration from file
10532
10649
  */
@@ -10538,12 +10655,20 @@ var MigrationManager = class {
10538
10655
  name: migrationFile.name,
10539
10656
  up: /* @__PURE__ */ __name(async (adapter) => {
10540
10657
  if (typeof adapter.query === "function") {
10541
- await adapter.query(upSQL);
10658
+ await this.executeSqlStatements(
10659
+ adapter,
10660
+ upSQL,
10661
+ migrationFile.version
10662
+ );
10542
10663
  }
10543
10664
  }, "up"),
10544
10665
  down: /* @__PURE__ */ __name(async (adapter) => {
10545
10666
  if (downSQL && typeof adapter.query === "function") {
10546
- await adapter.query(downSQL);
10667
+ await this.executeSqlStatements(
10668
+ adapter,
10669
+ downSQL,
10670
+ migrationFile.version
10671
+ );
10547
10672
  } else {
10548
10673
  console.warn(
10549
10674
  `[Migrations] No DOWN migration for ${migrationFile.version}`
@@ -10650,6 +10775,7 @@ var MigrationManager = class {
10650
10775
  /**
10651
10776
  * Run all pending migrations
10652
10777
  */
10778
+ /* eslint-disable max-depth, complexity */
10653
10779
  async up(targetVersion) {
10654
10780
  try {
10655
10781
  await this.initialize();
@@ -10670,9 +10796,15 @@ var MigrationManager = class {
10670
10796
  const migration = await this.loadMigration(migrationFile);
10671
10797
  const startTime = Date.now();
10672
10798
  if (typeof this.adapter.transaction === "function") {
10673
- await this.adapter.transaction(async () => {
10799
+ const txResult = await this.adapter.transaction(async () => {
10674
10800
  await migration.up(this.adapter);
10675
10801
  });
10802
+ if (!txResult.success) {
10803
+ throw txResult.error ?? new DatabaseError(
10804
+ `Migration ${migration.version} failed`,
10805
+ DATABASE_ERROR_CODES.QUERY_FAILED
10806
+ );
10807
+ }
10676
10808
  } else {
10677
10809
  await migration.up(this.adapter);
10678
10810
  }
@@ -10727,9 +10859,15 @@ var MigrationManager = class {
10727
10859
  const migration = await this.loadMigration(migrationFile);
10728
10860
  const startTime = Date.now();
10729
10861
  if (typeof this.adapter.transaction === "function") {
10730
- await this.adapter.transaction(async () => {
10862
+ const txResult = await this.adapter.transaction(async () => {
10731
10863
  await migration.down(this.adapter);
10732
10864
  });
10865
+ if (!txResult.success) {
10866
+ throw txResult.error ?? new DatabaseError(
10867
+ `Rollback ${appliedMigration.version} failed`,
10868
+ DATABASE_ERROR_CODES.QUERY_FAILED
10869
+ );
10870
+ }
10733
10871
  } else {
10734
10872
  await migration.down(this.adapter);
10735
10873
  }
@@ -10788,6 +10926,10 @@ var MigrationManager = class {
10788
10926
  }
10789
10927
  }
10790
10928
  };
10929
+ var DESCRIPTION_MAX_LENGTH2 = 60;
10930
+ var FALLBACK_DESCRIPTION_LENGTH2 = 50;
10931
+ var PROGRESS_LOG_INTERVAL2 = 10;
10932
+ var ERROR_MESSAGE_MAX_LENGTH2 = 300;
10791
10933
  var SeedManager = class {
10792
10934
  static {
10793
10935
  __name(this, "SeedManager");
@@ -10856,7 +10998,7 @@ var SeedManager = class {
10856
10998
  const files = fs.readdirSync(this.seedsPath);
10857
10999
  const seeds = [];
10858
11000
  for (const file of files) {
10859
- const match = file.match(/^(\d+)_(.+)\.(ts|js)$/);
11001
+ const match = file.match(/^(\d+)_(.+)\.(ts|js|sql)$/);
10860
11002
  if (match) {
10861
11003
  const [, order, name] = match;
10862
11004
  seeds.push({
@@ -10869,9 +11011,137 @@ var SeedManager = class {
10869
11011
  return seeds.sort((a, b) => a.order - b.order);
10870
11012
  }
10871
11013
  /**
10872
- * Load seed from file
11014
+ * Process dollar-quoted string delimiters ($$ or $tag$)
11015
+ */
11016
+ processDollarDelimiters(line, inDollarBlock, dollarTag) {
11017
+ const dollarMatch = line.match(/\$([a-zA-Z_]*)\$/g);
11018
+ if (!dollarMatch) return { inDollarBlock, dollarTag };
11019
+ let currentInBlock = inDollarBlock;
11020
+ let currentTag = dollarTag;
11021
+ for (const match of dollarMatch) {
11022
+ if (!currentInBlock) {
11023
+ currentInBlock = true;
11024
+ currentTag = match;
11025
+ } else if (match === currentTag) {
11026
+ currentInBlock = false;
11027
+ currentTag = "";
11028
+ }
11029
+ }
11030
+ return { inDollarBlock: currentInBlock, dollarTag: currentTag };
11031
+ }
11032
+ /**
11033
+ * Filter out comment-only statements
11034
+ */
11035
+ isNonCommentStatement(statement) {
11036
+ const withoutComments = statement.replace(/--.*$/gm, "").trim();
11037
+ return withoutComments.length > 0;
11038
+ }
11039
+ /**
11040
+ * Split SQL into individual statements for better error reporting
11041
+ */
11042
+ splitSqlStatements(sql2) {
11043
+ const statements = [];
11044
+ let current = "";
11045
+ let inDollarBlock = false;
11046
+ let dollarTag = "";
11047
+ for (const line of sql2.split("\n")) {
11048
+ const trimmedLine = line.trim();
11049
+ const isEmptyOrComment = trimmedLine === "" || trimmedLine.startsWith("--");
11050
+ current += line + "\n";
11051
+ if (isEmptyOrComment) continue;
11052
+ const dollarState = this.processDollarDelimiters(line, inDollarBlock, dollarTag);
11053
+ inDollarBlock = dollarState.inDollarBlock;
11054
+ dollarTag = dollarState.dollarTag;
11055
+ if (!inDollarBlock && trimmedLine.endsWith(";") && current.trim()) {
11056
+ statements.push(current.trim());
11057
+ current = "";
11058
+ }
11059
+ }
11060
+ if (current.trim()) {
11061
+ statements.push(current.trim());
11062
+ }
11063
+ return statements.filter((s) => this.isNonCommentStatement(s));
11064
+ }
11065
+ /**
11066
+ * Extract a short description from a SQL statement for logging
11067
+ */
11068
+ getStatementDescription(statement) {
11069
+ const firstLine = statement.split("\n").find((l) => l.trim() && !l.trim().startsWith("--"))?.trim() ?? "";
11070
+ const patterns = [
11071
+ /^(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,
11072
+ /^(ALTER\s+TABLE)\s+([^\s]+)/i,
11073
+ /^(DROP\s+(?:TABLE|INDEX|TYPE|FUNCTION|TRIGGER|EXTENSION|SCHEMA|VIEW|POLICY))\s+(?:IF\s+EXISTS\s+)?([^\s(;]+)/i,
11074
+ /^(INSERT\s+INTO)\s+([^\s(]+)/i,
11075
+ /^(COMMENT\s+ON\s+(?:TABLE|COLUMN|INDEX|FUNCTION|TYPE))\s+([^\s]+)/i,
11076
+ /^(GRANT|REVOKE)\s+.+\s+ON\s+([^\s]+)/i
11077
+ ];
11078
+ for (const pattern of patterns) {
11079
+ const match = firstLine.match(pattern);
11080
+ if (match) {
11081
+ return `${match[1]} ${match[2]}`.slice(0, DESCRIPTION_MAX_LENGTH2);
11082
+ }
11083
+ }
11084
+ const truncated = firstLine.slice(0, FALLBACK_DESCRIPTION_LENGTH2);
11085
+ const suffix = firstLine.length > FALLBACK_DESCRIPTION_LENGTH2 ? "..." : "";
11086
+ return truncated + suffix;
11087
+ }
11088
+ /**
11089
+ * Execute SQL statements individually with better error reporting
11090
+ */
11091
+ async executeSqlStatements(sql2, seedName) {
11092
+ const statements = this.splitSqlStatements(sql2);
11093
+ const total = statements.length;
11094
+ console.log(` → ${total} statements to execute`);
11095
+ for (let i = 0; i < statements.length; i++) {
11096
+ const statement = statements[i];
11097
+ const description = this.getStatementDescription(statement);
11098
+ try {
11099
+ await this.adapter.query(statement);
11100
+ const isInterval = (i + 1) % PROGRESS_LOG_INTERVAL2 === 0;
11101
+ const isLast = i === total - 1;
11102
+ const isSignificant = Boolean(description.match(/^(INSERT INTO)/i));
11103
+ if (isInterval || isLast || isSignificant) {
11104
+ console.log(` ✓ [${i + 1}/${total}] ${description}`);
11105
+ }
11106
+ } catch (error) {
11107
+ console.log(` ✗ [${i + 1}/${total}] ${description}`);
11108
+ const rawMessage = error.message;
11109
+ const errorMessage = rawMessage.replace(/^SQL Error:\s*/i, "").replace(/^Failed to execute query:.*?-\s*/i, "").slice(0, ERROR_MESSAGE_MAX_LENGTH2);
11110
+ throw new DatabaseError(
11111
+ `Seed "${seedName}" failed at statement ${i + 1}/${total}:
11112
+ Statement: ${description}
11113
+ Error: ${errorMessage}`,
11114
+ DATABASE_ERROR_CODES.QUERY_FAILED,
11115
+ { cause: error }
11116
+ );
11117
+ }
11118
+ }
11119
+ }
11120
+ /**
11121
+ * Load SQL seed from file
11122
+ */
11123
+ loadSqlSeed(seedFile) {
11124
+ const sql2 = fs.readFileSync(seedFile.filePath, "utf-8");
11125
+ return {
11126
+ name: seedFile.name,
11127
+ run: /* @__PURE__ */ __name(async () => {
11128
+ if (typeof this.adapter.query === "function") {
11129
+ await this.executeSqlStatements(sql2, seedFile.name);
11130
+ }
11131
+ }, "run"),
11132
+ // SQL seeds don't have cleanup by default
11133
+ cleanup: void 0
11134
+ };
11135
+ }
11136
+ /**
11137
+ * Load seed from file (supports .ts, .js, and .sql)
10873
11138
  */
11139
+ // eslint-disable-next-line complexity
10874
11140
  async loadSeed(seedFile) {
11141
+ const ext = path.extname(seedFile.filePath);
11142
+ if (ext === ".sql") {
11143
+ return this.loadSqlSeed(seedFile);
11144
+ }
10875
11145
  const importPath = seedFile.filePath.startsWith("/") ? seedFile.filePath : new URL(`file:///${seedFile.filePath.replace(/\\/g, "/")}`).href;
10876
11146
  const seedModule = await import(importPath);
10877
11147
  return {
@@ -10925,9 +11195,15 @@ var SeedManager = class {
10925
11195
  */
10926
11196
  async executeSeed(seed) {
10927
11197
  if (typeof this.adapter.transaction === "function") {
10928
- await this.adapter.transaction(async () => {
11198
+ const txResult = await this.adapter.transaction(async () => {
10929
11199
  await seed.run(this.adapter);
10930
11200
  });
11201
+ if (!txResult.success) {
11202
+ throw txResult.error ?? new DatabaseError(
11203
+ `Seed ${seed.name} failed`,
11204
+ DATABASE_ERROR_CODES.QUERY_FAILED
11205
+ );
11206
+ }
10931
11207
  } else {
10932
11208
  await seed.run(this.adapter);
10933
11209
  }
@@ -10984,9 +11260,15 @@ var SeedManager = class {
10984
11260
  async executeCleanup(seed) {
10985
11261
  if (!seed.cleanup) return;
10986
11262
  if (typeof this.adapter.transaction === "function") {
10987
- await this.adapter.transaction(async () => {
11263
+ const txResult = await this.adapter.transaction(async () => {
10988
11264
  await seed.cleanup(this.adapter);
10989
11265
  });
11266
+ if (!txResult.success) {
11267
+ throw txResult.error ?? new DatabaseError(
11268
+ `Seed cleanup for ${seed.name} failed`,
11269
+ DATABASE_ERROR_CODES.QUERY_FAILED
11270
+ );
11271
+ }
10990
11272
  } else {
10991
11273
  await seed.cleanup(this.adapter);
10992
11274
  }