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/README.md +1 -1
- package/build/database/drivers/mssql/index.js +79 -2
- package/build/database/tenants/data-copier.js +27 -0
- package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +4 -20
- package/build/src/database/drivers/mssql/index.d.ts +22 -0
- package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mssql/index.js +74 -3
- package/build/src/database/tenants/data-copier.d.ts +16 -0
- package/build/src/database/tenants/data-copier.d.ts.map +1 -1
- package/build/src/database/tenants/data-copier.js +25 -1
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +0 -9
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +5 -16
- package/package.json +2 -2
- package/src/database/drivers/mssql/index.js +79 -2
- package/src/database/tenants/data-copier.js +27 -0
- package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +4 -20
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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.
|