velocious 1.0.499 → 1.0.500

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "velocious": "build/bin/velocious.js"
4
4
  },
5
5
  "name": "velocious",
6
- "version": "1.0.499",
6
+ "version": "1.0.500",
7
7
  "main": "build/index.js",
8
8
  "types": "build/index.d.ts",
9
9
  "files": [
@@ -57,7 +57,7 @@
57
57
  "diggerize": "^1.0.5",
58
58
  "ejs": "^6.0.1",
59
59
  "env-sense": "^1.0.2",
60
- "epic-locks": "^1.0.7",
60
+ "epic-locks": "^1.0.8",
61
61
  "escape-string-regexp": "^1.0.5",
62
62
  "eventemitter3": "^5.0.1",
63
63
  "gettext-universal": "^1.0.23",
@@ -148,12 +148,89 @@ export default class VelociousDatabaseDriversMssql extends Base{
148
148
  return digg(rows, 0, "db_name")
149
149
  }
150
150
 
151
+ /**
152
+ * Disables every foreign key constraint (bulk `NOCHECK`).
153
+ * @returns {Promise<void>} - Resolves when foreign keys are disabled.
154
+ */
151
155
  async disableForeignKeys() {
152
- await this.query("EXEC sp_MSforeachtable \"ALTER TABLE ? NOCHECK CONSTRAINT all\"")
156
+ await this._execConstraintToggle("EXEC sp_MSforeachtable \"ALTER TABLE ? NOCHECK CONSTRAINT all\"", "disableForeignKeys")
153
157
  }
154
158
 
159
+ /**
160
+ * Re-enables and re-validates every foreign key constraint (`WITH CHECK`).
161
+ * @returns {Promise<void>} - Resolves when foreign keys are enabled.
162
+ */
155
163
  async enableForeignKeys() {
156
- await this.query("EXEC sp_MSforeachtable @command1=\"print '?'\", @command2=\"ALTER TABLE ? WITH CHECK CHECK CONSTRAINT all\"")
164
+ await this._execConstraintToggle("EXEC sp_MSforeachtable @command1=\"print '?'\", @command2=\"ALTER TABLE ? WITH CHECK CHECK CONSTRAINT all\"", "enableForeignKeys")
165
+ }
166
+
167
+ /**
168
+ * Runs a bulk constraint-toggle statement. `ALTER TABLE ... NOCHECK/CHECK
169
+ * CONSTRAINT` needs a schema-modification lock on every table, so if the
170
+ * request times out it is almost always blocked by another session that is
171
+ * still holding a lock (a leaked/uncommitted connection). On a timeout,
172
+ * capture which sessions were blocking so the real culprit is named instead
173
+ * of leaving a bare "Request failed to complete in 15000ms".
174
+ * @param {string} sql - Constraint-toggle SQL.
175
+ * @param {string} label - Operation label for the error.
176
+ * @returns {Promise<void>} - Resolves when the toggle completes.
177
+ */
178
+ async _execConstraintToggle(sql, label) {
179
+ try {
180
+ await this.query(sql)
181
+ } catch (error) {
182
+ if (error instanceof Error && /Timeout: Request failed to complete/i.test(error.message)) {
183
+ const snapshot = await this._captureBlockingSessionsForDebug().catch(
184
+ (diagError) => `(blocking diagnostics query failed: ${diagError instanceof Error ? diagError.message : diagError})`
185
+ )
186
+
187
+ throw new Error(`${error.message}\n\n[${label} blocked] other user sessions with open transactions or active requests:\n${snapshot}`, {cause: error})
188
+ }
189
+
190
+ throw error
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Snapshots the sessions that could be blocking a constraint toggle in THIS
196
+ * database — every session other than this one that holds a lock in `DB_ID()`
197
+ * or is running a request against it — with its last statement, wait state,
198
+ * and blocking session, enough to identify a connection that leaked a lock.
199
+ * Scoped to the current database so a multi-database server does not leak
200
+ * unrelated sessions' SQL into the error and bury the real blocker.
201
+ * @returns {Promise<string>} - JSON snapshot, or a "(none)" marker.
202
+ */
203
+ async _captureBlockingSessionsForDebug() {
204
+ const rows = await this.query(`
205
+ WITH lock_sessions AS (
206
+ SELECT DISTINCT request_session_id AS session_id
207
+ FROM sys.dm_tran_locks
208
+ WHERE resource_database_id = DB_ID()
209
+ )
210
+ SELECT
211
+ s.session_id AS sessionId,
212
+ s.status AS sessionStatus,
213
+ s.login_time AS loginTime,
214
+ s.last_request_start_time AS lastRequestStart,
215
+ s.last_request_end_time AS lastRequestEnd,
216
+ s.open_transaction_count AS openTransactionCount,
217
+ r.status AS requestStatus,
218
+ r.command AS command,
219
+ r.wait_type AS waitType,
220
+ r.wait_time AS waitTimeMs,
221
+ r.blocking_session_id AS blockingSessionId,
222
+ CAST(ib.event_info AS NVARCHAR(MAX)) AS lastSql
223
+ FROM sys.dm_exec_sessions s
224
+ LEFT JOIN sys.dm_exec_requests r ON r.session_id = s.session_id
225
+ OUTER APPLY sys.dm_exec_input_buffer(s.session_id, NULL) ib
226
+ WHERE s.is_user_process = 1
227
+ AND s.session_id <> @@SPID
228
+ AND (s.session_id IN (SELECT session_id FROM lock_sessions) OR r.database_id = DB_ID())
229
+ `)
230
+
231
+ if (rows.length === 0) return "(none — no other session held a lock or ran a request in this database when queried)"
232
+
233
+ return JSON.stringify(rows, null, 2)
157
234
  }
158
235
 
159
236
  /**
@@ -94,6 +94,33 @@ export default class DataCopier {
94
94
  return sourceRowsByTableName
95
95
  }
96
96
 
97
+ /**
98
+ * Deletes one tenant's rows from the target database, children before parents, with
99
+ * foreign keys disabled inside a single transaction. This is `copy` without the reinsert:
100
+ * the same plan traversal selects the tenant's current target rows and `deleteTargetRows`
101
+ * removes them children-first so the ordering never trips a foreign key. Multi-tenant apps
102
+ * use it to purge a tenant — for example clearing the tenant's master copy in the
103
+ * global/default database on teardown, so foreign keys stop referencing the tenant's
104
+ * about-to-be-removed root row.
105
+ *
106
+ * Returns the deleted rows keyed by table name so the caller can perform any app-specific
107
+ * post-delete work (record-location cleanup, auditing) without that policy leaking into the
108
+ * framework.
109
+ * @param {string} keyValue - Tenant key whose rows should be removed from the target.
110
+ * @returns {Promise<Map<string, Record<string, unknown>[]>>} - The deleted rows by table name.
111
+ */
112
+ async deleteTenantRows(keyValue) {
113
+ const rowsByTableName = await this.loadRows(this.targetDb, keyValue)
114
+
115
+ await this.targetDb.withDisabledForeignKeys(async () => {
116
+ await this.targetDb.transaction(async () => {
117
+ await this.deleteTargetRows(rowsByTableName)
118
+ })
119
+ })
120
+
121
+ return rowsByTableName
122
+ }
123
+
97
124
  /**
98
125
  * Loads the rows for `keyValue` for every table in the plan from `db`, resolving
99
126
  * parent-scoped tables from the ids already selected for their parent table. Used for
@@ -167,11 +167,10 @@ export default class DbGenerateFrontendModels extends BaseCommand {
167
167
  }
168
168
 
169
169
  for (const [frontendModelsDir, generatedFiles] of generatedFilesByDirectory) {
170
- const indexContent = this.buildIndexFileContent(generatedFiles)
171
-
172
- await fs.writeFile(`${frontendModelsDir}/index.js`, indexContent)
173
-
174
- console.log("create src/frontend-models/index.js")
170
+ // The index.js barrel is no longer generated — nothing imports it (models are
171
+ // imported by file path, and setup.js performs the registration side-effects).
172
+ // Remove any stale one left from an older generator.
173
+ await fs.rm(`${frontendModelsDir}/index.js`, {force: true})
175
174
 
176
175
  const setupContent = this.buildSetupFileContent(generatedFiles)
177
176
 
@@ -632,21 +631,6 @@ export default class DbGenerateFrontendModels extends BaseCommand {
632
631
  return fileContent
633
632
  }
634
633
 
635
- /**
636
- * Runs build index file content.
637
- * @param {Array<{className: string, fileName: string}>} generatedFiles - Generated model files.
638
- * @returns {string} - Index file content that imports and re-exports all models.
639
- */
640
- buildIndexFileContent(generatedFiles) {
641
- let content = generatedFileBanner(FRONTEND_MODELS_REGENERATE_COMMAND)
642
-
643
- for (const {className, fileName} of generatedFiles) {
644
- content += `export {default as ${className}} from "./${fileName}"\n`
645
- }
646
-
647
- return content
648
- }
649
-
650
634
  /**
651
635
  * Runs build setup file content.
652
636
  * @param {Array<{className: string, fileName: string}>} generatedFiles - Generated model files.