@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.cjs CHANGED
@@ -3621,6 +3621,7 @@ var SupabaseAdapter = class {
3621
3621
  return ops[operator]();
3622
3622
  }
3623
3623
  };
3624
+ var SQL_ERROR_TRUNCATE_LENGTH = 500;
3624
3625
  var SQLAdapter = class {
3625
3626
  static {
3626
3627
  __name(this, "SQLAdapter");
@@ -3631,6 +3632,7 @@ var SQLAdapter = class {
3631
3632
  idColumnMap = /* @__PURE__ */ new Map();
3632
3633
  configIdColumns;
3633
3634
  defaultSchema;
3635
+ showSqlInErrors;
3634
3636
  /**
3635
3637
  * Creates a new SQLAdapter instance.
3636
3638
  * @param {SQLAdapterConfig} config - Configuration for the SQL adapter.
@@ -3644,6 +3646,7 @@ var SQLAdapter = class {
3644
3646
  constructor(config) {
3645
3647
  this.config = config;
3646
3648
  this.defaultSchema = config.schema ?? "public";
3649
+ this.showSqlInErrors = config.showSqlInErrors ?? true;
3647
3650
  this.pool = new pg.Pool({
3648
3651
  connectionString: config.connectionString,
3649
3652
  ...config.pool
@@ -3762,16 +3765,16 @@ var SQLAdapter = class {
3762
3765
  const result = await this.pool.query(sql2, params);
3763
3766
  return result.rows;
3764
3767
  } catch (error) {
3765
- throw new errors.DatabaseError(
3766
- `Failed to execute query: ${sql2} - ${error.message}`,
3767
- errors$1.DATABASE_ERROR_CODES.QUERY_FAILED,
3768
- {
3769
- context: {
3770
- source: "SQLAdapter.query"
3771
- },
3772
- cause: error
3773
- }
3774
- );
3768
+ const truncatedSql = sql2.slice(0, SQL_ERROR_TRUNCATE_LENGTH);
3769
+ const sqlSuffix = sql2.length > SQL_ERROR_TRUNCATE_LENGTH ? "..." : "";
3770
+ const errorMessage = this.showSqlInErrors ? `SQL Error: ${error.message}
3771
+ Query: ${truncatedSql}${sqlSuffix}` : `SQL Error: ${error.message}`;
3772
+ throw new errors.DatabaseError(errorMessage, errors$1.DATABASE_ERROR_CODES.QUERY_FAILED, {
3773
+ context: {
3774
+ source: "SQLAdapter.query"
3775
+ },
3776
+ cause: error
3777
+ });
3775
3778
  }
3776
3779
  }
3777
3780
  /**
@@ -10442,6 +10445,10 @@ __name(exports.DataValidationPipe, "DataValidationPipe");
10442
10445
  exports.DataValidationPipe = __decorateClass([
10443
10446
  common.Injectable()
10444
10447
  ], exports.DataValidationPipe);
10448
+ var DESCRIPTION_MAX_LENGTH = 60;
10449
+ var FALLBACK_DESCRIPTION_LENGTH = 50;
10450
+ var PROGRESS_LOG_INTERVAL = 10;
10451
+ var ERROR_MESSAGE_MAX_LENGTH = 300;
10445
10452
  var MigrationManager = class {
10446
10453
  static {
10447
10454
  __name(this, "MigrationManager");
@@ -10551,6 +10558,116 @@ var MigrationManager = class {
10551
10558
  }
10552
10559
  return { upSQL: sql2.trim(), downSQL: null };
10553
10560
  }
10561
+ /**
10562
+ * Process dollar-quoted string delimiters ($$ or $tag$)
10563
+ * Returns updated state for tracking if we're inside a dollar block
10564
+ */
10565
+ processDollarDelimiters(line, inDollarBlock, dollarTag) {
10566
+ const dollarMatch = line.match(/\$([a-zA-Z_]*)\$/g);
10567
+ if (!dollarMatch) return { inDollarBlock, dollarTag };
10568
+ let currentInBlock = inDollarBlock;
10569
+ let currentTag = dollarTag;
10570
+ for (const match of dollarMatch) {
10571
+ if (!currentInBlock) {
10572
+ currentInBlock = true;
10573
+ currentTag = match;
10574
+ } else if (match === currentTag) {
10575
+ currentInBlock = false;
10576
+ currentTag = "";
10577
+ }
10578
+ }
10579
+ return { inDollarBlock: currentInBlock, dollarTag: currentTag };
10580
+ }
10581
+ /**
10582
+ * Filter out comment-only statements
10583
+ */
10584
+ isNonCommentStatement(statement) {
10585
+ const withoutComments = statement.replace(/--.*$/gm, "").trim();
10586
+ return withoutComments.length > 0;
10587
+ }
10588
+ /**
10589
+ * Split SQL into individual statements for better error reporting
10590
+ * Handles $$ delimited blocks (functions, triggers) correctly
10591
+ */
10592
+ splitSqlStatements(sql2) {
10593
+ const statements = [];
10594
+ let current = "";
10595
+ let inDollarBlock = false;
10596
+ let dollarTag = "";
10597
+ for (const line of sql2.split("\n")) {
10598
+ const trimmedLine = line.trim();
10599
+ const isEmptyOrComment = trimmedLine === "" || trimmedLine.startsWith("--");
10600
+ current += line + "\n";
10601
+ if (isEmptyOrComment) continue;
10602
+ const dollarState = this.processDollarDelimiters(line, inDollarBlock, dollarTag);
10603
+ inDollarBlock = dollarState.inDollarBlock;
10604
+ dollarTag = dollarState.dollarTag;
10605
+ const isEndOfStatement = !inDollarBlock && trimmedLine.endsWith(";");
10606
+ if (isEndOfStatement && current.trim()) {
10607
+ statements.push(current.trim());
10608
+ current = "";
10609
+ }
10610
+ }
10611
+ if (current.trim()) {
10612
+ statements.push(current.trim());
10613
+ }
10614
+ return statements.filter((s) => this.isNonCommentStatement(s));
10615
+ }
10616
+ /**
10617
+ * Extract a short description from a SQL statement for logging
10618
+ */
10619
+ getStatementDescription(statement) {
10620
+ const firstLine = statement.split("\n").find((l) => l.trim() && !l.trim().startsWith("--"))?.trim() ?? "";
10621
+ const patterns = [
10622
+ /^(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,
10623
+ /^(ALTER\s+TABLE)\s+([^\s]+)/i,
10624
+ /^(DROP\s+(?:TABLE|INDEX|TYPE|FUNCTION|TRIGGER|EXTENSION|SCHEMA|VIEW|POLICY))\s+(?:IF\s+EXISTS\s+)?([^\s(;]+)/i,
10625
+ /^(INSERT\s+INTO)\s+([^\s(]+)/i,
10626
+ /^(COMMENT\s+ON\s+(?:TABLE|COLUMN|INDEX|FUNCTION|TYPE))\s+([^\s]+)/i,
10627
+ /^(GRANT|REVOKE)\s+.+\s+ON\s+([^\s]+)/i
10628
+ ];
10629
+ for (const pattern of patterns) {
10630
+ const match = firstLine.match(pattern);
10631
+ if (match) {
10632
+ return `${match[1]} ${match[2]}`.slice(0, DESCRIPTION_MAX_LENGTH);
10633
+ }
10634
+ }
10635
+ const truncated = firstLine.slice(0, FALLBACK_DESCRIPTION_LENGTH);
10636
+ const suffix = firstLine.length > FALLBACK_DESCRIPTION_LENGTH ? "..." : "";
10637
+ return truncated + suffix;
10638
+ }
10639
+ /**
10640
+ * Execute SQL statements individually with better error reporting
10641
+ */
10642
+ async executeSqlStatements(adapter, sql2, migrationVersion) {
10643
+ const statements = this.splitSqlStatements(sql2);
10644
+ const total = statements.length;
10645
+ console.log(` → ${total} statements to execute`);
10646
+ for (let i = 0; i < statements.length; i++) {
10647
+ const statement = statements[i];
10648
+ const description = this.getStatementDescription(statement);
10649
+ try {
10650
+ await adapter.query(statement);
10651
+ const isInterval = (i + 1) % PROGRESS_LOG_INTERVAL === 0;
10652
+ const isLast = i === total - 1;
10653
+ const isSignificant = Boolean(description.match(/^(CREATE TABLE|CREATE FUNCTION|CREATE TRIGGER)/i));
10654
+ if (isInterval || isLast || isSignificant) {
10655
+ console.log(` ✓ [${i + 1}/${total}] ${description}`);
10656
+ }
10657
+ } catch (error) {
10658
+ console.log(` ✗ [${i + 1}/${total}] ${description}`);
10659
+ const rawMessage = error.message;
10660
+ const errorMessage = rawMessage.replace(/^SQL Error:\s*/i, "").replace(/^Failed to execute query:.*?-\s*/i, "").slice(0, ERROR_MESSAGE_MAX_LENGTH);
10661
+ throw new errors.DatabaseError(
10662
+ `Migration ${migrationVersion} failed at statement ${i + 1}/${total}:
10663
+ Statement: ${description}
10664
+ Error: ${errorMessage}`,
10665
+ errors$1.DATABASE_ERROR_CODES.QUERY_FAILED,
10666
+ { cause: error }
10667
+ );
10668
+ }
10669
+ }
10670
+ }
10554
10671
  /**
10555
10672
  * Load SQL migration from file
10556
10673
  */
@@ -10562,12 +10679,20 @@ var MigrationManager = class {
10562
10679
  name: migrationFile.name,
10563
10680
  up: /* @__PURE__ */ __name(async (adapter) => {
10564
10681
  if (typeof adapter.query === "function") {
10565
- await adapter.query(upSQL);
10682
+ await this.executeSqlStatements(
10683
+ adapter,
10684
+ upSQL,
10685
+ migrationFile.version
10686
+ );
10566
10687
  }
10567
10688
  }, "up"),
10568
10689
  down: /* @__PURE__ */ __name(async (adapter) => {
10569
10690
  if (downSQL && typeof adapter.query === "function") {
10570
- await adapter.query(downSQL);
10691
+ await this.executeSqlStatements(
10692
+ adapter,
10693
+ downSQL,
10694
+ migrationFile.version
10695
+ );
10571
10696
  } else {
10572
10697
  console.warn(
10573
10698
  `[Migrations] No DOWN migration for ${migrationFile.version}`
@@ -10674,6 +10799,7 @@ var MigrationManager = class {
10674
10799
  /**
10675
10800
  * Run all pending migrations
10676
10801
  */
10802
+ /* eslint-disable max-depth, complexity */
10677
10803
  async up(targetVersion) {
10678
10804
  try {
10679
10805
  await this.initialize();
@@ -10694,9 +10820,15 @@ var MigrationManager = class {
10694
10820
  const migration = await this.loadMigration(migrationFile);
10695
10821
  const startTime = Date.now();
10696
10822
  if (typeof this.adapter.transaction === "function") {
10697
- await this.adapter.transaction(async () => {
10823
+ const txResult = await this.adapter.transaction(async () => {
10698
10824
  await migration.up(this.adapter);
10699
10825
  });
10826
+ if (!txResult.success) {
10827
+ throw txResult.error ?? new errors.DatabaseError(
10828
+ `Migration ${migration.version} failed`,
10829
+ errors$1.DATABASE_ERROR_CODES.QUERY_FAILED
10830
+ );
10831
+ }
10700
10832
  } else {
10701
10833
  await migration.up(this.adapter);
10702
10834
  }
@@ -10751,9 +10883,15 @@ var MigrationManager = class {
10751
10883
  const migration = await this.loadMigration(migrationFile);
10752
10884
  const startTime = Date.now();
10753
10885
  if (typeof this.adapter.transaction === "function") {
10754
- await this.adapter.transaction(async () => {
10886
+ const txResult = await this.adapter.transaction(async () => {
10755
10887
  await migration.down(this.adapter);
10756
10888
  });
10889
+ if (!txResult.success) {
10890
+ throw txResult.error ?? new errors.DatabaseError(
10891
+ `Rollback ${appliedMigration.version} failed`,
10892
+ errors$1.DATABASE_ERROR_CODES.QUERY_FAILED
10893
+ );
10894
+ }
10757
10895
  } else {
10758
10896
  await migration.down(this.adapter);
10759
10897
  }
@@ -10812,6 +10950,10 @@ var MigrationManager = class {
10812
10950
  }
10813
10951
  }
10814
10952
  };
10953
+ var DESCRIPTION_MAX_LENGTH2 = 60;
10954
+ var FALLBACK_DESCRIPTION_LENGTH2 = 50;
10955
+ var PROGRESS_LOG_INTERVAL2 = 10;
10956
+ var ERROR_MESSAGE_MAX_LENGTH2 = 300;
10815
10957
  var SeedManager = class {
10816
10958
  static {
10817
10959
  __name(this, "SeedManager");
@@ -10880,7 +11022,7 @@ var SeedManager = class {
10880
11022
  const files = fs__namespace.readdirSync(this.seedsPath);
10881
11023
  const seeds = [];
10882
11024
  for (const file of files) {
10883
- const match = file.match(/^(\d+)_(.+)\.(ts|js)$/);
11025
+ const match = file.match(/^(\d+)_(.+)\.(ts|js|sql)$/);
10884
11026
  if (match) {
10885
11027
  const [, order, name] = match;
10886
11028
  seeds.push({
@@ -10893,9 +11035,137 @@ var SeedManager = class {
10893
11035
  return seeds.sort((a, b) => a.order - b.order);
10894
11036
  }
10895
11037
  /**
10896
- * Load seed from file
11038
+ * Process dollar-quoted string delimiters ($$ or $tag$)
11039
+ */
11040
+ processDollarDelimiters(line, inDollarBlock, dollarTag) {
11041
+ const dollarMatch = line.match(/\$([a-zA-Z_]*)\$/g);
11042
+ if (!dollarMatch) return { inDollarBlock, dollarTag };
11043
+ let currentInBlock = inDollarBlock;
11044
+ let currentTag = dollarTag;
11045
+ for (const match of dollarMatch) {
11046
+ if (!currentInBlock) {
11047
+ currentInBlock = true;
11048
+ currentTag = match;
11049
+ } else if (match === currentTag) {
11050
+ currentInBlock = false;
11051
+ currentTag = "";
11052
+ }
11053
+ }
11054
+ return { inDollarBlock: currentInBlock, dollarTag: currentTag };
11055
+ }
11056
+ /**
11057
+ * Filter out comment-only statements
11058
+ */
11059
+ isNonCommentStatement(statement) {
11060
+ const withoutComments = statement.replace(/--.*$/gm, "").trim();
11061
+ return withoutComments.length > 0;
11062
+ }
11063
+ /**
11064
+ * Split SQL into individual statements for better error reporting
11065
+ */
11066
+ splitSqlStatements(sql2) {
11067
+ const statements = [];
11068
+ let current = "";
11069
+ let inDollarBlock = false;
11070
+ let dollarTag = "";
11071
+ for (const line of sql2.split("\n")) {
11072
+ const trimmedLine = line.trim();
11073
+ const isEmptyOrComment = trimmedLine === "" || trimmedLine.startsWith("--");
11074
+ current += line + "\n";
11075
+ if (isEmptyOrComment) continue;
11076
+ const dollarState = this.processDollarDelimiters(line, inDollarBlock, dollarTag);
11077
+ inDollarBlock = dollarState.inDollarBlock;
11078
+ dollarTag = dollarState.dollarTag;
11079
+ if (!inDollarBlock && trimmedLine.endsWith(";") && current.trim()) {
11080
+ statements.push(current.trim());
11081
+ current = "";
11082
+ }
11083
+ }
11084
+ if (current.trim()) {
11085
+ statements.push(current.trim());
11086
+ }
11087
+ return statements.filter((s) => this.isNonCommentStatement(s));
11088
+ }
11089
+ /**
11090
+ * Extract a short description from a SQL statement for logging
11091
+ */
11092
+ getStatementDescription(statement) {
11093
+ const firstLine = statement.split("\n").find((l) => l.trim() && !l.trim().startsWith("--"))?.trim() ?? "";
11094
+ const patterns = [
11095
+ /^(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,
11096
+ /^(ALTER\s+TABLE)\s+([^\s]+)/i,
11097
+ /^(DROP\s+(?:TABLE|INDEX|TYPE|FUNCTION|TRIGGER|EXTENSION|SCHEMA|VIEW|POLICY))\s+(?:IF\s+EXISTS\s+)?([^\s(;]+)/i,
11098
+ /^(INSERT\s+INTO)\s+([^\s(]+)/i,
11099
+ /^(COMMENT\s+ON\s+(?:TABLE|COLUMN|INDEX|FUNCTION|TYPE))\s+([^\s]+)/i,
11100
+ /^(GRANT|REVOKE)\s+.+\s+ON\s+([^\s]+)/i
11101
+ ];
11102
+ for (const pattern of patterns) {
11103
+ const match = firstLine.match(pattern);
11104
+ if (match) {
11105
+ return `${match[1]} ${match[2]}`.slice(0, DESCRIPTION_MAX_LENGTH2);
11106
+ }
11107
+ }
11108
+ const truncated = firstLine.slice(0, FALLBACK_DESCRIPTION_LENGTH2);
11109
+ const suffix = firstLine.length > FALLBACK_DESCRIPTION_LENGTH2 ? "..." : "";
11110
+ return truncated + suffix;
11111
+ }
11112
+ /**
11113
+ * Execute SQL statements individually with better error reporting
11114
+ */
11115
+ async executeSqlStatements(sql2, seedName) {
11116
+ const statements = this.splitSqlStatements(sql2);
11117
+ const total = statements.length;
11118
+ console.log(` → ${total} statements to execute`);
11119
+ for (let i = 0; i < statements.length; i++) {
11120
+ const statement = statements[i];
11121
+ const description = this.getStatementDescription(statement);
11122
+ try {
11123
+ await this.adapter.query(statement);
11124
+ const isInterval = (i + 1) % PROGRESS_LOG_INTERVAL2 === 0;
11125
+ const isLast = i === total - 1;
11126
+ const isSignificant = Boolean(description.match(/^(INSERT INTO)/i));
11127
+ if (isInterval || isLast || isSignificant) {
11128
+ console.log(` ✓ [${i + 1}/${total}] ${description}`);
11129
+ }
11130
+ } catch (error) {
11131
+ console.log(` ✗ [${i + 1}/${total}] ${description}`);
11132
+ const rawMessage = error.message;
11133
+ const errorMessage = rawMessage.replace(/^SQL Error:\s*/i, "").replace(/^Failed to execute query:.*?-\s*/i, "").slice(0, ERROR_MESSAGE_MAX_LENGTH2);
11134
+ throw new errors.DatabaseError(
11135
+ `Seed "${seedName}" failed at statement ${i + 1}/${total}:
11136
+ Statement: ${description}
11137
+ Error: ${errorMessage}`,
11138
+ errors$1.DATABASE_ERROR_CODES.QUERY_FAILED,
11139
+ { cause: error }
11140
+ );
11141
+ }
11142
+ }
11143
+ }
11144
+ /**
11145
+ * Load SQL seed from file
11146
+ */
11147
+ loadSqlSeed(seedFile) {
11148
+ const sql2 = fs__namespace.readFileSync(seedFile.filePath, "utf-8");
11149
+ return {
11150
+ name: seedFile.name,
11151
+ run: /* @__PURE__ */ __name(async () => {
11152
+ if (typeof this.adapter.query === "function") {
11153
+ await this.executeSqlStatements(sql2, seedFile.name);
11154
+ }
11155
+ }, "run"),
11156
+ // SQL seeds don't have cleanup by default
11157
+ cleanup: void 0
11158
+ };
11159
+ }
11160
+ /**
11161
+ * Load seed from file (supports .ts, .js, and .sql)
10897
11162
  */
11163
+ // eslint-disable-next-line complexity
10898
11164
  async loadSeed(seedFile) {
11165
+ const ext = path__namespace.extname(seedFile.filePath);
11166
+ if (ext === ".sql") {
11167
+ return this.loadSqlSeed(seedFile);
11168
+ }
10899
11169
  const importPath = seedFile.filePath.startsWith("/") ? seedFile.filePath : new URL(`file:///${seedFile.filePath.replace(/\\/g, "/")}`).href;
10900
11170
  const seedModule = await import(importPath);
10901
11171
  return {
@@ -10949,9 +11219,15 @@ var SeedManager = class {
10949
11219
  */
10950
11220
  async executeSeed(seed) {
10951
11221
  if (typeof this.adapter.transaction === "function") {
10952
- await this.adapter.transaction(async () => {
11222
+ const txResult = await this.adapter.transaction(async () => {
10953
11223
  await seed.run(this.adapter);
10954
11224
  });
11225
+ if (!txResult.success) {
11226
+ throw txResult.error ?? new errors.DatabaseError(
11227
+ `Seed ${seed.name} failed`,
11228
+ errors$1.DATABASE_ERROR_CODES.QUERY_FAILED
11229
+ );
11230
+ }
10955
11231
  } else {
10956
11232
  await seed.run(this.adapter);
10957
11233
  }
@@ -11008,9 +11284,15 @@ var SeedManager = class {
11008
11284
  async executeCleanup(seed) {
11009
11285
  if (!seed.cleanup) return;
11010
11286
  if (typeof this.adapter.transaction === "function") {
11011
- await this.adapter.transaction(async () => {
11287
+ const txResult = await this.adapter.transaction(async () => {
11012
11288
  await seed.cleanup(this.adapter);
11013
11289
  });
11290
+ if (!txResult.success) {
11291
+ throw txResult.error ?? new errors.DatabaseError(
11292
+ `Seed cleanup for ${seed.name} failed`,
11293
+ errors$1.DATABASE_ERROR_CODES.QUERY_FAILED
11294
+ );
11295
+ }
11014
11296
  } else {
11015
11297
  await seed.cleanup(this.adapter);
11016
11298
  }