@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.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
  /**
@@ -7212,6 +7215,17 @@ var BaseRepository = class {
7212
7215
  __name(this, "BaseRepository");
7213
7216
  }
7214
7217
  defaultConfig;
7218
+ /**
7219
+ * Get the table name for this repository
7220
+ *
7221
+ * Useful for transaction operations where you need the table name
7222
+ * to execute raw queries within a transaction context.
7223
+ *
7224
+ * @returns The table name this repository operates on
7225
+ */
7226
+ getTableName() {
7227
+ return this.tableName;
7228
+ }
7215
7229
  /**
7216
7230
  * Merges default repository config with per-operation config
7217
7231
  * Per-operation config takes precedence over default config
@@ -10442,6 +10456,10 @@ __name(exports.DataValidationPipe, "DataValidationPipe");
10442
10456
  exports.DataValidationPipe = __decorateClass([
10443
10457
  common.Injectable()
10444
10458
  ], exports.DataValidationPipe);
10459
+ var DESCRIPTION_MAX_LENGTH = 60;
10460
+ var FALLBACK_DESCRIPTION_LENGTH = 50;
10461
+ var PROGRESS_LOG_INTERVAL = 10;
10462
+ var ERROR_MESSAGE_MAX_LENGTH = 300;
10445
10463
  var MigrationManager = class {
10446
10464
  static {
10447
10465
  __name(this, "MigrationManager");
@@ -10551,6 +10569,122 @@ var MigrationManager = class {
10551
10569
  }
10552
10570
  return { upSQL: sql2.trim(), downSQL: null };
10553
10571
  }
10572
+ /**
10573
+ * Process dollar-quoted string delimiters ($$ or $tag$)
10574
+ * Returns updated state for tracking if we're inside a dollar block
10575
+ */
10576
+ processDollarDelimiters(line, inDollarBlock, dollarTag) {
10577
+ const dollarMatch = line.match(/\$([a-zA-Z_]*)\$/g);
10578
+ if (!dollarMatch) return { inDollarBlock, dollarTag };
10579
+ let currentInBlock = inDollarBlock;
10580
+ let currentTag = dollarTag;
10581
+ for (const match of dollarMatch) {
10582
+ if (!currentInBlock) {
10583
+ currentInBlock = true;
10584
+ currentTag = match;
10585
+ } else if (match === currentTag) {
10586
+ currentInBlock = false;
10587
+ currentTag = "";
10588
+ }
10589
+ }
10590
+ return { inDollarBlock: currentInBlock, dollarTag: currentTag };
10591
+ }
10592
+ /**
10593
+ * Filter out comment-only statements
10594
+ */
10595
+ isNonCommentStatement(statement) {
10596
+ const withoutComments = statement.replace(/--.*$/gm, "").trim();
10597
+ return withoutComments.length > 0;
10598
+ }
10599
+ /**
10600
+ * Split SQL into individual statements for better error reporting
10601
+ * Handles $$ delimited blocks (functions, triggers) correctly
10602
+ */
10603
+ splitSqlStatements(sql2) {
10604
+ const statements = [];
10605
+ let current = "";
10606
+ let inDollarBlock = false;
10607
+ let dollarTag = "";
10608
+ for (const line of sql2.split("\n")) {
10609
+ const trimmedLine = line.trim();
10610
+ const isEmptyOrComment = trimmedLine === "" || trimmedLine.startsWith("--");
10611
+ current += line + "\n";
10612
+ if (isEmptyOrComment) continue;
10613
+ const dollarState = this.processDollarDelimiters(
10614
+ line,
10615
+ inDollarBlock,
10616
+ dollarTag
10617
+ );
10618
+ inDollarBlock = dollarState.inDollarBlock;
10619
+ dollarTag = dollarState.dollarTag;
10620
+ const isEndOfStatement = !inDollarBlock && trimmedLine.endsWith(";");
10621
+ if (isEndOfStatement && current.trim()) {
10622
+ statements.push(current.trim());
10623
+ current = "";
10624
+ }
10625
+ }
10626
+ if (current.trim()) {
10627
+ statements.push(current.trim());
10628
+ }
10629
+ return statements.filter((s) => this.isNonCommentStatement(s));
10630
+ }
10631
+ /**
10632
+ * Extract a short description from a SQL statement for logging
10633
+ */
10634
+ getStatementDescription(statement) {
10635
+ const firstLine = statement.split("\n").find((l) => l.trim() && !l.trim().startsWith("--"))?.trim() ?? "";
10636
+ const patterns = [
10637
+ /^(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,
10638
+ /^(ALTER\s+TABLE)\s+([^\s]+)/i,
10639
+ /^(DROP\s+(?:TABLE|INDEX|TYPE|FUNCTION|TRIGGER|EXTENSION|SCHEMA|VIEW|POLICY))\s+(?:IF\s+EXISTS\s+)?([^\s(;]+)/i,
10640
+ /^(INSERT\s+INTO)\s+([^\s(]+)/i,
10641
+ /^(COMMENT\s+ON\s+(?:TABLE|COLUMN|INDEX|FUNCTION|TYPE))\s+([^\s]+)/i,
10642
+ /^(GRANT|REVOKE)\s+.+\s+ON\s+([^\s]+)/i
10643
+ ];
10644
+ for (const pattern of patterns) {
10645
+ const match = firstLine.match(pattern);
10646
+ if (match) {
10647
+ return `${match[1]} ${match[2]}`.slice(0, DESCRIPTION_MAX_LENGTH);
10648
+ }
10649
+ }
10650
+ const truncated = firstLine.slice(0, FALLBACK_DESCRIPTION_LENGTH);
10651
+ const suffix = firstLine.length > FALLBACK_DESCRIPTION_LENGTH ? "..." : "";
10652
+ return truncated + suffix;
10653
+ }
10654
+ /**
10655
+ * Execute SQL statements individually with better error reporting
10656
+ */
10657
+ async executeSqlStatements(adapter, sql2, migrationVersion) {
10658
+ const statements = this.splitSqlStatements(sql2);
10659
+ const total = statements.length;
10660
+ console.log(` → ${total} statements to execute`);
10661
+ for (let i = 0; i < statements.length; i++) {
10662
+ const statement = statements[i];
10663
+ const description = this.getStatementDescription(statement);
10664
+ try {
10665
+ await adapter.query(statement);
10666
+ const isInterval = (i + 1) % PROGRESS_LOG_INTERVAL === 0;
10667
+ const isLast = i === total - 1;
10668
+ const isSignificant = Boolean(
10669
+ description.match(/^(CREATE TABLE|CREATE FUNCTION|CREATE TRIGGER)/i)
10670
+ );
10671
+ if (isInterval || isLast || isSignificant) {
10672
+ console.log(` ✓ [${i + 1}/${total}] ${description}`);
10673
+ }
10674
+ } catch (error) {
10675
+ console.log(` ✗ [${i + 1}/${total}] ${description}`);
10676
+ const rawMessage = error.message;
10677
+ const errorMessage = rawMessage.replace(/^SQL Error:\s*/i, "").replace(/^Failed to execute query:.*?-\s*/i, "").slice(0, ERROR_MESSAGE_MAX_LENGTH);
10678
+ throw new errors.DatabaseError(
10679
+ `Migration ${migrationVersion} failed at statement ${i + 1}/${total}:
10680
+ Statement: ${description}
10681
+ Error: ${errorMessage}`,
10682
+ errors$1.DATABASE_ERROR_CODES.QUERY_FAILED,
10683
+ { cause: error }
10684
+ );
10685
+ }
10686
+ }
10687
+ }
10554
10688
  /**
10555
10689
  * Load SQL migration from file
10556
10690
  */
@@ -10562,12 +10696,20 @@ var MigrationManager = class {
10562
10696
  name: migrationFile.name,
10563
10697
  up: /* @__PURE__ */ __name(async (adapter) => {
10564
10698
  if (typeof adapter.query === "function") {
10565
- await adapter.query(upSQL);
10699
+ await this.executeSqlStatements(
10700
+ adapter,
10701
+ upSQL,
10702
+ migrationFile.version
10703
+ );
10566
10704
  }
10567
10705
  }, "up"),
10568
10706
  down: /* @__PURE__ */ __name(async (adapter) => {
10569
10707
  if (downSQL && typeof adapter.query === "function") {
10570
- await adapter.query(downSQL);
10708
+ await this.executeSqlStatements(
10709
+ adapter,
10710
+ downSQL,
10711
+ migrationFile.version
10712
+ );
10571
10713
  } else {
10572
10714
  console.warn(
10573
10715
  `[Migrations] No DOWN migration for ${migrationFile.version}`
@@ -10674,6 +10816,7 @@ var MigrationManager = class {
10674
10816
  /**
10675
10817
  * Run all pending migrations
10676
10818
  */
10819
+ /* eslint-disable max-depth, complexity */
10677
10820
  async up(targetVersion) {
10678
10821
  try {
10679
10822
  await this.initialize();
@@ -10694,9 +10837,15 @@ var MigrationManager = class {
10694
10837
  const migration = await this.loadMigration(migrationFile);
10695
10838
  const startTime = Date.now();
10696
10839
  if (typeof this.adapter.transaction === "function") {
10697
- await this.adapter.transaction(async () => {
10840
+ const txResult = await this.adapter.transaction(async () => {
10698
10841
  await migration.up(this.adapter);
10699
10842
  });
10843
+ if (!txResult.success) {
10844
+ throw txResult.error ?? new errors.DatabaseError(
10845
+ `Migration ${migration.version} failed`,
10846
+ errors$1.DATABASE_ERROR_CODES.QUERY_FAILED
10847
+ );
10848
+ }
10700
10849
  } else {
10701
10850
  await migration.up(this.adapter);
10702
10851
  }
@@ -10751,9 +10900,15 @@ var MigrationManager = class {
10751
10900
  const migration = await this.loadMigration(migrationFile);
10752
10901
  const startTime = Date.now();
10753
10902
  if (typeof this.adapter.transaction === "function") {
10754
- await this.adapter.transaction(async () => {
10903
+ const txResult = await this.adapter.transaction(async () => {
10755
10904
  await migration.down(this.adapter);
10756
10905
  });
10906
+ if (!txResult.success) {
10907
+ throw txResult.error ?? new errors.DatabaseError(
10908
+ `Rollback ${appliedMigration.version} failed`,
10909
+ errors$1.DATABASE_ERROR_CODES.QUERY_FAILED
10910
+ );
10911
+ }
10757
10912
  } else {
10758
10913
  await migration.down(this.adapter);
10759
10914
  }
@@ -10812,6 +10967,10 @@ var MigrationManager = class {
10812
10967
  }
10813
10968
  }
10814
10969
  };
10970
+ var DESCRIPTION_MAX_LENGTH2 = 60;
10971
+ var FALLBACK_DESCRIPTION_LENGTH2 = 50;
10972
+ var PROGRESS_LOG_INTERVAL2 = 10;
10973
+ var ERROR_MESSAGE_MAX_LENGTH2 = 300;
10815
10974
  var SeedManager = class {
10816
10975
  static {
10817
10976
  __name(this, "SeedManager");
@@ -10880,7 +11039,7 @@ var SeedManager = class {
10880
11039
  const files = fs__namespace.readdirSync(this.seedsPath);
10881
11040
  const seeds = [];
10882
11041
  for (const file of files) {
10883
- const match = file.match(/^(\d+)_(.+)\.(ts|js)$/);
11042
+ const match = file.match(/^(\d+)_(.+)\.(ts|js|sql)$/);
10884
11043
  if (match) {
10885
11044
  const [, order, name] = match;
10886
11045
  seeds.push({
@@ -10893,9 +11052,141 @@ var SeedManager = class {
10893
11052
  return seeds.sort((a, b) => a.order - b.order);
10894
11053
  }
10895
11054
  /**
10896
- * Load seed from file
11055
+ * Process dollar-quoted string delimiters ($$ or $tag$)
11056
+ */
11057
+ processDollarDelimiters(line, inDollarBlock, dollarTag) {
11058
+ const dollarMatch = line.match(/\$([a-zA-Z_]*)\$/g);
11059
+ if (!dollarMatch) return { inDollarBlock, dollarTag };
11060
+ let currentInBlock = inDollarBlock;
11061
+ let currentTag = dollarTag;
11062
+ for (const match of dollarMatch) {
11063
+ if (!currentInBlock) {
11064
+ currentInBlock = true;
11065
+ currentTag = match;
11066
+ } else if (match === currentTag) {
11067
+ currentInBlock = false;
11068
+ currentTag = "";
11069
+ }
11070
+ }
11071
+ return { inDollarBlock: currentInBlock, dollarTag: currentTag };
11072
+ }
11073
+ /**
11074
+ * Filter out comment-only statements
11075
+ */
11076
+ isNonCommentStatement(statement) {
11077
+ const withoutComments = statement.replace(/--.*$/gm, "").trim();
11078
+ return withoutComments.length > 0;
11079
+ }
11080
+ /**
11081
+ * Split SQL into individual statements for better error reporting
11082
+ */
11083
+ splitSqlStatements(sql2) {
11084
+ const statements = [];
11085
+ let current = "";
11086
+ let inDollarBlock = false;
11087
+ let dollarTag = "";
11088
+ for (const line of sql2.split("\n")) {
11089
+ const trimmedLine = line.trim();
11090
+ const isEmptyOrComment = trimmedLine === "" || trimmedLine.startsWith("--");
11091
+ current += line + "\n";
11092
+ if (isEmptyOrComment) continue;
11093
+ const dollarState = this.processDollarDelimiters(
11094
+ line,
11095
+ inDollarBlock,
11096
+ dollarTag
11097
+ );
11098
+ inDollarBlock = dollarState.inDollarBlock;
11099
+ dollarTag = dollarState.dollarTag;
11100
+ if (!inDollarBlock && trimmedLine.endsWith(";") && current.trim()) {
11101
+ statements.push(current.trim());
11102
+ current = "";
11103
+ }
11104
+ }
11105
+ if (current.trim()) {
11106
+ statements.push(current.trim());
11107
+ }
11108
+ return statements.filter((s) => this.isNonCommentStatement(s));
11109
+ }
11110
+ /**
11111
+ * Extract a short description from a SQL statement for logging
11112
+ */
11113
+ getStatementDescription(statement) {
11114
+ const firstLine = statement.split("\n").find((l) => l.trim() && !l.trim().startsWith("--"))?.trim() ?? "";
11115
+ const patterns = [
11116
+ /^(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,
11117
+ /^(ALTER\s+TABLE)\s+([^\s]+)/i,
11118
+ /^(DROP\s+(?:TABLE|INDEX|TYPE|FUNCTION|TRIGGER|EXTENSION|SCHEMA|VIEW|POLICY))\s+(?:IF\s+EXISTS\s+)?([^\s(;]+)/i,
11119
+ /^(INSERT\s+INTO)\s+([^\s(]+)/i,
11120
+ /^(COMMENT\s+ON\s+(?:TABLE|COLUMN|INDEX|FUNCTION|TYPE))\s+([^\s]+)/i,
11121
+ /^(GRANT|REVOKE)\s+.+\s+ON\s+([^\s]+)/i
11122
+ ];
11123
+ for (const pattern of patterns) {
11124
+ const match = firstLine.match(pattern);
11125
+ if (match) {
11126
+ return `${match[1]} ${match[2]}`.slice(0, DESCRIPTION_MAX_LENGTH2);
11127
+ }
11128
+ }
11129
+ const truncated = firstLine.slice(0, FALLBACK_DESCRIPTION_LENGTH2);
11130
+ const suffix = firstLine.length > FALLBACK_DESCRIPTION_LENGTH2 ? "..." : "";
11131
+ return truncated + suffix;
11132
+ }
11133
+ /**
11134
+ * Execute SQL statements individually with better error reporting
11135
+ */
11136
+ async executeSqlStatements(sql2, seedName) {
11137
+ const statements = this.splitSqlStatements(sql2);
11138
+ const total = statements.length;
11139
+ console.log(` → ${total} statements to execute`);
11140
+ for (let i = 0; i < statements.length; i++) {
11141
+ const statement = statements[i];
11142
+ const description = this.getStatementDescription(statement);
11143
+ try {
11144
+ await this.adapter.query(statement);
11145
+ const isInterval = (i + 1) % PROGRESS_LOG_INTERVAL2 === 0;
11146
+ const isLast = i === total - 1;
11147
+ const isSignificant = Boolean(description.match(/^(INSERT INTO)/i));
11148
+ if (isInterval || isLast || isSignificant) {
11149
+ console.log(` ✓ [${i + 1}/${total}] ${description}`);
11150
+ }
11151
+ } catch (error) {
11152
+ console.log(` ✗ [${i + 1}/${total}] ${description}`);
11153
+ const rawMessage = error.message;
11154
+ const errorMessage = rawMessage.replace(/^SQL Error:\s*/i, "").replace(/^Failed to execute query:.*?-\s*/i, "").slice(0, ERROR_MESSAGE_MAX_LENGTH2);
11155
+ throw new errors.DatabaseError(
11156
+ `Seed "${seedName}" failed at statement ${i + 1}/${total}:
11157
+ Statement: ${description}
11158
+ Error: ${errorMessage}`,
11159
+ errors$1.DATABASE_ERROR_CODES.QUERY_FAILED,
11160
+ { cause: error }
11161
+ );
11162
+ }
11163
+ }
11164
+ }
11165
+ /**
11166
+ * Load SQL seed from file
10897
11167
  */
11168
+ loadSqlSeed(seedFile) {
11169
+ const sql2 = fs__namespace.readFileSync(seedFile.filePath, "utf-8");
11170
+ return {
11171
+ name: seedFile.name,
11172
+ run: /* @__PURE__ */ __name(async () => {
11173
+ if (typeof this.adapter.query === "function") {
11174
+ await this.executeSqlStatements(sql2, seedFile.name);
11175
+ }
11176
+ }, "run"),
11177
+ // SQL seeds don't have cleanup by default
11178
+ cleanup: void 0
11179
+ };
11180
+ }
11181
+ /**
11182
+ * Load seed from file (supports .ts, .js, and .sql)
11183
+ */
11184
+ // eslint-disable-next-line complexity
10898
11185
  async loadSeed(seedFile) {
11186
+ const ext = path__namespace.extname(seedFile.filePath);
11187
+ if (ext === ".sql") {
11188
+ return this.loadSqlSeed(seedFile);
11189
+ }
10899
11190
  const importPath = seedFile.filePath.startsWith("/") ? seedFile.filePath : new URL(`file:///${seedFile.filePath.replace(/\\/g, "/")}`).href;
10900
11191
  const seedModule = await import(importPath);
10901
11192
  return {
@@ -10949,9 +11240,15 @@ var SeedManager = class {
10949
11240
  */
10950
11241
  async executeSeed(seed) {
10951
11242
  if (typeof this.adapter.transaction === "function") {
10952
- await this.adapter.transaction(async () => {
11243
+ const txResult = await this.adapter.transaction(async () => {
10953
11244
  await seed.run(this.adapter);
10954
11245
  });
11246
+ if (!txResult.success) {
11247
+ throw txResult.error ?? new errors.DatabaseError(
11248
+ `Seed ${seed.name} failed`,
11249
+ errors$1.DATABASE_ERROR_CODES.QUERY_FAILED
11250
+ );
11251
+ }
10955
11252
  } else {
10956
11253
  await seed.run(this.adapter);
10957
11254
  }
@@ -11008,9 +11305,15 @@ var SeedManager = class {
11008
11305
  async executeCleanup(seed) {
11009
11306
  if (!seed.cleanup) return;
11010
11307
  if (typeof this.adapter.transaction === "function") {
11011
- await this.adapter.transaction(async () => {
11308
+ const txResult = await this.adapter.transaction(async () => {
11012
11309
  await seed.cleanup(this.adapter);
11013
11310
  });
11311
+ if (!txResult.success) {
11312
+ throw txResult.error ?? new errors.DatabaseError(
11313
+ `Seed cleanup for ${seed.name} failed`,
11314
+ errors$1.DATABASE_ERROR_CODES.QUERY_FAILED
11315
+ );
11316
+ }
11014
11317
  } else {
11015
11318
  await seed.cleanup(this.adapter);
11016
11319
  }