@spfn/core 0.3.0-beta.2 → 0.3.0-beta.21
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 +343 -16
- package/dist/app-config/index.d.ts +134 -0
- package/dist/app-config/index.js +58 -0
- package/dist/app-config/index.js.map +1 -0
- package/dist/authz/index.js +1 -381
- package/dist/authz/index.js.map +1 -1
- package/dist/{boss-D16fO2oG.d.ts → boss-BiDCbbAQ.d.ts} +60 -7
- package/dist/codegen/index.d.ts +93 -15
- package/dist/codegen/index.js +626 -165
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +0 -63
- package/dist/config/index.js +15 -15
- package/dist/config/index.js.map +1 -1
- package/dist/contract/index.d.ts +40 -17
- package/dist/contract/index.js +147 -23
- package/dist/contract/index.js.map +1 -1
- package/dist/db/index.d.ts +201 -34
- package/dist/db/index.js +294 -158
- package/dist/db/index.js.map +1 -1
- package/dist/{define-middleware-DfDP39Nq.d.ts → define-middleware-CVKgqo8S.d.ts} +1 -1
- package/dist/env/index.d.ts +6 -442
- package/dist/env/index.js +13 -16
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.js +24 -1
- package/dist/env/loader.js.map +1 -1
- package/dist/error-TsUIjp_x.d.ts +157 -0
- package/dist/errors/index.js +1 -381
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +5 -5
- package/dist/event/index.js +32 -1
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +18 -4
- package/dist/event/sse/client.js +38 -2
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +5 -4
- package/dist/event/ws/client.d.ts +3 -2
- package/dist/event/ws/index.d.ts +4 -3
- package/dist/job/index.d.ts +48 -3
- package/dist/job/index.js +131 -52
- package/dist/job/index.js.map +1 -1
- package/dist/logger/index.js +0 -12
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.d.ts +6 -2
- package/dist/middleware/index.js +7 -388
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +88 -8
- package/dist/nextjs/index.js +138 -62
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +45 -7
- package/dist/nextjs/server.js +14 -0
- package/dist/nextjs/server.js.map +1 -1
- package/dist/ops/index.d.ts +62 -43
- package/dist/ops/index.js +638 -97
- package/dist/ops/index.js.map +1 -1
- package/dist/registry-C2KQCh0m.d.ts +473 -0
- package/dist/route/index.d.ts +6 -3
- package/dist/route/index.js +97 -28
- package/dist/route/index.js.map +1 -1
- package/dist/route/types.d.ts +7 -1
- package/dist/{router-Qbssr11H.d.ts → route-builder-DRW-MSCJ.d.ts} +52 -127
- package/dist/router-4YoIy5fN.d.ts +172 -0
- package/dist/{token-manager-BT5EnUAR.d.ts → router-CysBmgv3.d.ts} +81 -127
- package/dist/server/index.d.ts +185 -14
- package/dist/server/index.js +883 -78
- package/dist/server/index.js.map +1 -1
- package/dist/surface/index.d.ts +115 -0
- package/dist/surface/index.js +1003 -0
- package/dist/surface/index.js.map +1 -0
- package/dist/token-manager-Ckjt2-qq.d.ts +122 -0
- package/dist/{types-D1c57Ko-.d.ts → types-5Rl11_Nb.d.ts} +43 -4
- package/dist/{types-ZQODsBft.d.ts → types-B8zAxFsE.d.ts} +2 -1
- package/dist/types-CxhPH4s_.d.ts +375 -0
- package/dist/{types-2AbaW4Ie.d.ts → types-W4u_S0lF.d.ts} +4 -3
- package/docs/file-upload.md +195 -333
- package/package.json +20 -9
- package/src/cache/README.md +330 -0
- package/src/codegen/README.md +634 -0
- package/src/config/README.md +333 -0
- package/src/contract/README.md +440 -0
- package/src/db/README.md +589 -0
- package/src/db/manager/README.md +500 -0
- package/src/db/schema/README.md +354 -0
- package/src/db/transaction/README.md +822 -0
- package/src/env/README.md +787 -0
- package/src/errors/README.md +429 -0
- package/src/event/README.md +789 -0
- package/src/job/README.md +613 -0
- package/src/logger/README.md +321 -0
- package/src/middleware/README.md +634 -0
- package/src/nextjs/README.md +688 -0
- package/src/route/README.md +763 -0
- package/src/security/README.md +100 -0
- package/src/server/README.md +704 -0
- package/dist/types-9oszaJqp.d.ts +0 -250
package/dist/db/index.js
CHANGED
|
@@ -3,10 +3,11 @@ import { env } from '@spfn/core/config';
|
|
|
3
3
|
import { logger } from '@spfn/core/logger';
|
|
4
4
|
import net from 'net';
|
|
5
5
|
import postgres from 'postgres';
|
|
6
|
-
import { QueryError, ConnectionError, DeadlockError, TransactionError, ConstraintViolationError, DuplicateEntryError,
|
|
6
|
+
import { QueryError, ConnectionError, DeadlockError, TransactionError, ConstraintViolationError, DuplicateEntryError, SerializableError } from '@spfn/core/errors';
|
|
7
7
|
import { parseNumber, parseBoolean } from '@spfn/core/env';
|
|
8
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
9
|
-
import { join,
|
|
8
|
+
import { existsSync, readdirSync, readFileSync, statSync, realpathSync, lstatSync } from 'fs';
|
|
9
|
+
import { join, relative } from 'path';
|
|
10
|
+
import mm from 'micromatch';
|
|
10
11
|
import { sql, count as count$1, lt, gt, and, desc, asc, eq } from 'drizzle-orm';
|
|
11
12
|
import { bigserial, timestamp, bigint, uuid as uuid$1, text, jsonb, pgSchema } from 'drizzle-orm/pg-core';
|
|
12
13
|
import { createHash, randomUUID } from 'crypto';
|
|
@@ -1213,7 +1214,10 @@ var BARREL_FILE_PATTERNS = [
|
|
|
1213
1214
|
"/config.js",
|
|
1214
1215
|
"/config.mjs"
|
|
1215
1216
|
];
|
|
1216
|
-
var
|
|
1217
|
+
var DEFAULT_ENTITY_GLOB = "./src/server/entities/**/*.ts";
|
|
1218
|
+
var SUPPORTED_EXTENSIONS = [".ts", ".mts", ".cts", ".tsx", ".js", ".mjs", ".cjs", ".jsx"];
|
|
1219
|
+
var DECLARATION_PATTERN = /\.d\.[mc]?ts$/;
|
|
1220
|
+
var SKIPPED_DIRECTORIES = /^(\.|node_modules$)/;
|
|
1217
1221
|
function isBarrelFile(filePath) {
|
|
1218
1222
|
const posixPath = toPosixPath(filePath);
|
|
1219
1223
|
return BARREL_FILE_PATTERNS.some((pattern) => posixPath.endsWith(pattern));
|
|
@@ -1223,90 +1227,100 @@ function isAbsolutePath(path) {
|
|
|
1223
1227
|
return !!path.match(/^[A-Za-z]:[\/\\]/);
|
|
1224
1228
|
}
|
|
1225
1229
|
function hasSupportedExtension(filePath) {
|
|
1226
|
-
if (
|
|
1230
|
+
if (DECLARATION_PATTERN.test(filePath)) return false;
|
|
1227
1231
|
return SUPPORTED_EXTENSIONS.some((ext) => filePath.endsWith(ext));
|
|
1228
1232
|
}
|
|
1229
1233
|
function filterBarrelFiles(files) {
|
|
1230
1234
|
return files.filter((file) => !isBarrelFile(file));
|
|
1231
1235
|
}
|
|
1232
|
-
function
|
|
1233
|
-
const files = [];
|
|
1234
|
-
if (!existsSync(dir)) return files;
|
|
1236
|
+
function safeStat(path) {
|
|
1235
1237
|
try {
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
try {
|
|
1240
|
-
const stat = statSync(fullPath);
|
|
1241
|
-
if (stat.isDirectory()) {
|
|
1242
|
-
files.push(...scanDirectoryRecursive(fullPath, extension));
|
|
1243
|
-
} else if (stat.isFile()) {
|
|
1244
|
-
if ((!extension || fullPath.endsWith(extension)) && hasSupportedExtension(fullPath)) {
|
|
1245
|
-
files.push(fullPath);
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
} catch (error) {
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
} catch (error) {
|
|
1238
|
+
return statSync(path);
|
|
1239
|
+
} catch {
|
|
1240
|
+
return void 0;
|
|
1252
1241
|
}
|
|
1253
|
-
return files;
|
|
1254
1242
|
}
|
|
1255
|
-
function
|
|
1256
|
-
const files = [];
|
|
1257
|
-
if (!existsSync(dir)) return files;
|
|
1243
|
+
function safeReaddir(dir) {
|
|
1258
1244
|
try {
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
try {
|
|
1263
|
-
const stat = statSync(fullPath);
|
|
1264
|
-
if (stat.isFile()) {
|
|
1265
|
-
if ((filePattern === "*" || filePattern.startsWith("*.") && entry.endsWith(filePattern.slice(1))) && hasSupportedExtension(fullPath)) {
|
|
1266
|
-
files.push(fullPath);
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
} catch (error) {
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
} catch (error) {
|
|
1245
|
+
return readdirSync(dir);
|
|
1246
|
+
} catch {
|
|
1247
|
+
return [];
|
|
1273
1248
|
}
|
|
1274
|
-
return files;
|
|
1275
1249
|
}
|
|
1276
|
-
function
|
|
1277
|
-
const
|
|
1278
|
-
|
|
1279
|
-
const
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
}
|
|
1250
|
+
function walkDirectory(dir, maxDepth, ancestors = /* @__PURE__ */ new Set()) {
|
|
1251
|
+
const walk = { files: [], directories: [] };
|
|
1252
|
+
if (maxDepth < 1 || !safeStat(dir)?.isDirectory()) return walk;
|
|
1253
|
+
const realDir = realpathSafe(dir);
|
|
1254
|
+
if (ancestors.has(realDir)) return walk;
|
|
1255
|
+
const chain = new Set(ancestors).add(realDir);
|
|
1256
|
+
for (const entry of safeReaddir(dir)) {
|
|
1257
|
+
const path = join(dir, entry);
|
|
1258
|
+
const stat = safeStat(path);
|
|
1259
|
+
if (stat?.isFile() && hasSupportedExtension(path)) {
|
|
1260
|
+
walk.files.push(path);
|
|
1261
|
+
} else if (stat?.isDirectory() && !SKIPPED_DIRECTORIES.test(entry)) {
|
|
1262
|
+
walk.directories.push(path);
|
|
1263
|
+
const isLink = isSymlink(path);
|
|
1264
|
+
const child = walkDirectory(path, isLink ? Math.min(maxDepth - 1, 1) : maxDepth - 1, chain);
|
|
1265
|
+
for (const file of child.files) walk.files.push(file);
|
|
1266
|
+
for (const sub of child.directories) walk.directories.push(sub);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
return walk;
|
|
1270
|
+
}
|
|
1271
|
+
function isSymlink(path) {
|
|
1272
|
+
try {
|
|
1273
|
+
return lstatSync(path).isSymbolicLink();
|
|
1274
|
+
} catch {
|
|
1275
|
+
return false;
|
|
1294
1276
|
}
|
|
1295
|
-
return Array.from(schemas);
|
|
1296
1277
|
}
|
|
1297
|
-
function
|
|
1298
|
-
|
|
1299
|
-
return
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1278
|
+
function realpathSafe(path) {
|
|
1279
|
+
try {
|
|
1280
|
+
return realpathSync(path);
|
|
1281
|
+
} catch {
|
|
1282
|
+
return path;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
function scanDirectory(dir, maxDepth) {
|
|
1286
|
+
return walkDirectory(dir, maxDepth).files;
|
|
1287
|
+
}
|
|
1288
|
+
function toRelativePattern(entry, cwd) {
|
|
1289
|
+
return toPosixPath(isAbsolutePath(entry) ? relative(cwd, entry) : entry).replace(/^\.\//, "").replace(/[()]/g, "\\$&");
|
|
1290
|
+
}
|
|
1291
|
+
function expandGlob(pattern, cwd) {
|
|
1292
|
+
const scan = mm.scan(pattern);
|
|
1293
|
+
const depth = scan.isGlobstar || scan.glob.includes("**") ? Infinity : scan.glob.split("/").length;
|
|
1294
|
+
const base = join(cwd, scan.base.replace(/\\([()])/g, "$1"));
|
|
1295
|
+
const matches = mm.matcher(pattern);
|
|
1296
|
+
const matched = (path) => matches(toPosixPath(relative(cwd, path)));
|
|
1297
|
+
const walk = walkDirectory(base, depth);
|
|
1298
|
+
return Array.from(/* @__PURE__ */ new Set([...walk.files.filter(matched), ...walk.directories.filter(matched).flatMap((dir) => scanDirectory(dir, 1))]));
|
|
1299
|
+
}
|
|
1300
|
+
function expandSchemaEntry(entry, cwd) {
|
|
1301
|
+
const pattern = toRelativePattern(entry, cwd);
|
|
1302
|
+
if (mm.scan(pattern).isGlob) {
|
|
1303
|
+
return expandGlob(pattern, cwd);
|
|
1304
|
+
}
|
|
1305
|
+
const path = isAbsolutePath(entry) ? entry : join(cwd, entry);
|
|
1306
|
+
const stat = safeStat(path);
|
|
1307
|
+
if (!stat) return [];
|
|
1308
|
+
return stat.isDirectory() ? scanDirectory(path, 1) : [path];
|
|
1309
|
+
}
|
|
1310
|
+
function selectDefaultSchema(cwd) {
|
|
1311
|
+
const registry = env.DRIZZLE_SCHEMA_PATH;
|
|
1312
|
+
const registryFile = isAbsolutePath(registry) ? registry : join(cwd, registry);
|
|
1313
|
+
const registryExists = safeStat(registryFile)?.isFile() === true;
|
|
1314
|
+
const scanned = filterBarrelFiles(expandSchemaEntry(DEFAULT_ENTITY_GLOB, cwd));
|
|
1315
|
+
if (scanned.length === 0 && registryExists) {
|
|
1316
|
+
return { schemas: [registry], source: `entity registry ${registry}` };
|
|
1306
1317
|
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1318
|
+
return {
|
|
1319
|
+
schemas: [DEFAULT_ENTITY_GLOB],
|
|
1320
|
+
source: `${DEFAULT_ENTITY_GLOB} scan`,
|
|
1321
|
+
files: scanned,
|
|
1322
|
+
registry: registryExists ? registry : void 0
|
|
1323
|
+
};
|
|
1310
1324
|
}
|
|
1311
1325
|
function discoverPackageSchemas(cwd) {
|
|
1312
1326
|
const schemas = [];
|
|
@@ -1335,9 +1349,7 @@ function discoverPackageSchemas(cwd) {
|
|
|
1335
1349
|
const packageSchemas = Array.isArray(pkgJson.spfn.schemas) ? pkgJson.spfn.schemas : [pkgJson.spfn.schemas];
|
|
1336
1350
|
for (const schema of packageSchemas) {
|
|
1337
1351
|
const absolutePath = join(pkgPath, schema);
|
|
1338
|
-
|
|
1339
|
-
const schemaFiles = filterBarrelFiles(expandedFiles);
|
|
1340
|
-
schemas.push(...schemaFiles);
|
|
1352
|
+
schemas.push(...filterBarrelFiles(expandSchemaEntry(absolutePath, pkgPath)));
|
|
1341
1353
|
}
|
|
1342
1354
|
}
|
|
1343
1355
|
} catch (error) {
|
|
@@ -1396,6 +1408,8 @@ function getDrizzleConfig(options = {}) {
|
|
|
1396
1408
|
const schema2 = filteredSchemas.length === 1 ? filteredSchemas[0] : filteredSchemas;
|
|
1397
1409
|
return {
|
|
1398
1410
|
schema: schema2,
|
|
1411
|
+
schemaSource: `package ${options.packageFilter}`,
|
|
1412
|
+
schemaRegistry: void 0,
|
|
1399
1413
|
out,
|
|
1400
1414
|
dialect,
|
|
1401
1415
|
dbCredentials: getDbCredentials(dialect, databaseUrl),
|
|
@@ -1404,32 +1418,18 @@ function getDrizzleConfig(options = {}) {
|
|
|
1404
1418
|
}
|
|
1405
1419
|
};
|
|
1406
1420
|
}
|
|
1407
|
-
const userSchema = options.schema ?? "./src/server/entities/**/*.ts";
|
|
1408
|
-
const userSchemas = Array.isArray(userSchema) ? userSchema : [userSchema];
|
|
1409
|
-
const packageSchemas = options.disablePackageDiscovery ? [] : discoverPackageSchemas(options.cwd ?? process.cwd());
|
|
1410
|
-
let allSchemas = [...userSchemas, ...packageSchemas];
|
|
1411
1421
|
const cwd = options.cwd ?? process.cwd();
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
const filtered = filterBarrelFiles(expanded);
|
|
1418
|
-
expandedFiles.push(...filtered);
|
|
1419
|
-
}
|
|
1420
|
-
allSchemas = expandedFiles;
|
|
1421
|
-
}
|
|
1422
|
+
const selection = options.schema ? { schemas: Array.isArray(options.schema) ? options.schema : [options.schema], source: "schema option" } : selectDefaultSchema(cwd);
|
|
1423
|
+
const schemaSource = selection.source;
|
|
1424
|
+
const schemaRegistry = selection.registry;
|
|
1425
|
+
const packageSchemas = options.disablePackageDiscovery ? [] : discoverPackageSchemas(cwd);
|
|
1426
|
+
const allSchemas = options.expandGlobs ? [...selection.files ?? selection.schemas.flatMap((entry) => expandSchemaEntry(entry, cwd)), ...packageSchemas] : [...selection.schemas, ...packageSchemas];
|
|
1422
1427
|
const schema = allSchemas.length === 1 ? allSchemas[0] : allSchemas;
|
|
1423
|
-
|
|
1424
|
-
if (dialect === "postgresql") {
|
|
1425
|
-
if (options.schemaFilter) {
|
|
1426
|
-
schemaFilter = options.schemaFilter;
|
|
1427
|
-
} else if (options.autoDetectSchemas && expandedFiles.length > 0) {
|
|
1428
|
-
schemaFilter = detectSchemasFromFiles(expandedFiles);
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1428
|
+
const schemaFilter = dialect === "postgresql" ? options.schemaFilter : void 0;
|
|
1431
1429
|
return {
|
|
1432
1430
|
schema,
|
|
1431
|
+
schemaSource,
|
|
1432
|
+
schemaRegistry,
|
|
1433
1433
|
out,
|
|
1434
1434
|
dialect,
|
|
1435
1435
|
dbCredentials: getDbCredentials(dialect, databaseUrl),
|
|
@@ -1451,9 +1451,7 @@ function getDbCredentials(dialect, url) {
|
|
|
1451
1451
|
throw new Error(`Unsupported dialect: ${dialect}`);
|
|
1452
1452
|
}
|
|
1453
1453
|
}
|
|
1454
|
-
function
|
|
1455
|
-
const config = getDrizzleConfig(options);
|
|
1456
|
-
const cwd = options.cwd ?? process.cwd();
|
|
1454
|
+
function renderDrizzleConfig(config, cwd = process.cwd()) {
|
|
1457
1455
|
const normalizeSchemaPath = (schemaPath) => {
|
|
1458
1456
|
if (isAbsolutePath(schemaPath)) {
|
|
1459
1457
|
return schemaPath;
|
|
@@ -1477,6 +1475,9 @@ export default defineConfig({
|
|
|
1477
1475
|
});
|
|
1478
1476
|
`;
|
|
1479
1477
|
}
|
|
1478
|
+
function generateDrizzleConfigFile(options = {}) {
|
|
1479
|
+
return renderDrizzleConfig(getDrizzleConfig(options), options.cwd ?? process.cwd());
|
|
1480
|
+
}
|
|
1480
1481
|
function id() {
|
|
1481
1482
|
return bigserial("id", { mode: "number" }).primaryKey();
|
|
1482
1483
|
}
|
|
@@ -1743,6 +1744,32 @@ function pendingMigrationsSummary(targets) {
|
|
|
1743
1744
|
return `${pending} pending migration(s) in ${names}`;
|
|
1744
1745
|
}
|
|
1745
1746
|
var txLogger = logger.child("@spfn/core:transaction");
|
|
1747
|
+
var concurrentNestingReported = false;
|
|
1748
|
+
function reportConcurrentNesting() {
|
|
1749
|
+
if (concurrentNestingReported) {
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
concurrentNestingReported = true;
|
|
1753
|
+
txLogger.warn("Concurrent nested transactions are serialized", {
|
|
1754
|
+
reason: "sibling SAVEPOINTs share the outer transaction's connection, where one sibling's ROLLBACK TO would discard the other's writes",
|
|
1755
|
+
hint: "await nested calls one at a time, or pass requiresNew: true to give a branch its own connection. A nested call whose callback awaits a sibling started after it deadlocks."
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
function createNestedFrameGate() {
|
|
1759
|
+
let tail = Promise.resolve();
|
|
1760
|
+
let pending = 0;
|
|
1761
|
+
return {
|
|
1762
|
+
run(frame) {
|
|
1763
|
+
if (pending > 0) {
|
|
1764
|
+
reportConcurrentNesting();
|
|
1765
|
+
}
|
|
1766
|
+
pending++;
|
|
1767
|
+
const result = tail.then(frame);
|
|
1768
|
+
tail = result.then(() => void pending--, () => void pending--);
|
|
1769
|
+
return result;
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1746
1773
|
var asyncContext = new AsyncLocalStorage();
|
|
1747
1774
|
function getTransactionContext() {
|
|
1748
1775
|
return asyncContext.getStore() ?? null;
|
|
@@ -1755,7 +1782,7 @@ function runWithTransaction(tx, txId, callback) {
|
|
|
1755
1782
|
const existingContext = getTransactionContext();
|
|
1756
1783
|
const newLevel = existingContext ? existingContext.level + 1 : 1;
|
|
1757
1784
|
if (existingContext) {
|
|
1758
|
-
txLogger.
|
|
1785
|
+
txLogger.debug("Nested transaction started (SAVEPOINT)", {
|
|
1759
1786
|
outerTxId: existingContext.txId,
|
|
1760
1787
|
innerTxId: txId,
|
|
1761
1788
|
level: newLevel
|
|
@@ -1763,9 +1790,19 @@ function runWithTransaction(tx, txId, callback) {
|
|
|
1763
1790
|
} else {
|
|
1764
1791
|
txLogger.debug("Root transaction context set", { txId, level: newLevel });
|
|
1765
1792
|
}
|
|
1766
|
-
const
|
|
1793
|
+
const beforeCommitCallbacks = existingContext?.beforeCommitCallbacks ?? [];
|
|
1794
|
+
const afterCommitCallbacks = existingContext?.afterCommitCallbacks ?? [];
|
|
1795
|
+
const afterRollbackCallbacks = existingContext?.afterRollbackCallbacks ?? [];
|
|
1767
1796
|
return asyncContext.run(
|
|
1768
|
-
{
|
|
1797
|
+
{
|
|
1798
|
+
tx,
|
|
1799
|
+
txId,
|
|
1800
|
+
level: newLevel,
|
|
1801
|
+
beforeCommitCallbacks,
|
|
1802
|
+
afterCommitCallbacks,
|
|
1803
|
+
afterRollbackCallbacks,
|
|
1804
|
+
nestedFrames: createNestedFrameGate()
|
|
1805
|
+
},
|
|
1769
1806
|
callback
|
|
1770
1807
|
);
|
|
1771
1808
|
}
|
|
@@ -1781,14 +1818,67 @@ function onAfterCommit(callback) {
|
|
|
1781
1818
|
}
|
|
1782
1819
|
context.afterCommitCallbacks.push(callback);
|
|
1783
1820
|
}
|
|
1821
|
+
function onBeforeCommit(callback) {
|
|
1822
|
+
const context = getTransactionContext();
|
|
1823
|
+
if (!context) {
|
|
1824
|
+
txLogger.warn(
|
|
1825
|
+
"beforeCommit callback ran immediately (no transaction): a throw cannot abort anything"
|
|
1826
|
+
);
|
|
1827
|
+
Promise.resolve().then(callback).catch((err) => {
|
|
1828
|
+
txLogger.error("beforeCommit callback failed (no transaction)", {
|
|
1829
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1830
|
+
});
|
|
1831
|
+
});
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
context.beforeCommitCallbacks.push(callback);
|
|
1835
|
+
}
|
|
1836
|
+
function onAfterRollback(callback) {
|
|
1837
|
+
const context = getTransactionContext();
|
|
1838
|
+
if (!context) {
|
|
1839
|
+
txLogger.warn("afterRollback callback ignored (no transaction)");
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
context.afterRollbackCallbacks.push(callback);
|
|
1843
|
+
}
|
|
1784
1844
|
var MAX_TIMEOUT_MS = 2147483647;
|
|
1785
1845
|
var txLogger2 = logger.child("@spfn/core:transaction");
|
|
1846
|
+
function logSafely(emit) {
|
|
1847
|
+
try {
|
|
1848
|
+
emit();
|
|
1849
|
+
} catch {
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
async function runBeforeCommitCallbacks(callbacks) {
|
|
1853
|
+
for (const cb of [...callbacks]) {
|
|
1854
|
+
await cb();
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
async function runAfterRollbackCallbacks(callbacks, txId, context, enableLogging) {
|
|
1858
|
+
if (enableLogging) {
|
|
1859
|
+
logSafely(() => txLogger2.debug("Executing afterRollback callbacks", {
|
|
1860
|
+
txId,
|
|
1861
|
+
context,
|
|
1862
|
+
count: callbacks.length
|
|
1863
|
+
}));
|
|
1864
|
+
}
|
|
1865
|
+
for (const cb of callbacks) {
|
|
1866
|
+
await Promise.resolve().then(cb).catch((err) => logSafely(() => {
|
|
1867
|
+
txLogger2.error("afterRollback callback failed", {
|
|
1868
|
+
txId,
|
|
1869
|
+
context,
|
|
1870
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1871
|
+
});
|
|
1872
|
+
}));
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1786
1875
|
async function runInTransaction(callback, options = {}) {
|
|
1787
1876
|
const defaultTimeout = env.TRANSACTION_TIMEOUT;
|
|
1788
1877
|
const {
|
|
1789
1878
|
slowThreshold = 1e3,
|
|
1790
1879
|
enableLogging = true,
|
|
1791
|
-
context = "transaction"
|
|
1880
|
+
context = "transaction",
|
|
1881
|
+
requiresNew = false
|
|
1792
1882
|
} = options;
|
|
1793
1883
|
const timeout = options.timeout ?? defaultTimeout;
|
|
1794
1884
|
const idleTimeout = options.idleTimeout ?? env.TRANSACTION_IDLE_TIMEOUT;
|
|
@@ -1866,24 +1956,37 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1866
1956
|
}
|
|
1867
1957
|
throw error;
|
|
1868
1958
|
}
|
|
1869
|
-
const
|
|
1870
|
-
const isNested =
|
|
1871
|
-
if (isNested && timeout > 0 && enableLogging) {
|
|
1959
|
+
const savepointOwner = requiresNew ? null : getTransactionContext();
|
|
1960
|
+
const isNested = savepointOwner !== null;
|
|
1961
|
+
if (isNested && options.timeout !== void 0 && options.timeout > 0 && enableLogging) {
|
|
1872
1962
|
txLogger2.warn("Timeout ignored in nested transaction", {
|
|
1873
1963
|
txId,
|
|
1874
1964
|
context,
|
|
1875
|
-
outerTxId:
|
|
1965
|
+
outerTxId: savepointOwner.txId,
|
|
1876
1966
|
requestedTimeout: `${timeout}ms`,
|
|
1877
|
-
reason: "SET LOCAL
|
|
1967
|
+
reason: "the SAVEPOINT runs under the outer transaction's statement_timeout; SET LOCAL here would re-scope the whole outer transaction"
|
|
1878
1968
|
});
|
|
1879
1969
|
}
|
|
1880
1970
|
if (enableLogging) {
|
|
1881
1971
|
txLogger2.debug("Transaction started", { txId, context });
|
|
1882
1972
|
}
|
|
1883
1973
|
const startTime = Date.now();
|
|
1974
|
+
let beforeCommitCallbacks = [];
|
|
1884
1975
|
let afterCommitCallbacks = [];
|
|
1976
|
+
let afterRollbackCallbacks = [];
|
|
1977
|
+
const openTransaction = (body) => {
|
|
1978
|
+
if (savepointOwner) {
|
|
1979
|
+
return savepointOwner.nestedFrames.run(
|
|
1980
|
+
() => savepointOwner.tx.transaction(body)
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
if (requiresNew) {
|
|
1984
|
+
return asyncContext.exit(() => writeDb.transaction(body));
|
|
1985
|
+
}
|
|
1986
|
+
return writeDb.transaction(body);
|
|
1987
|
+
};
|
|
1885
1988
|
try {
|
|
1886
|
-
const result = await
|
|
1989
|
+
const result = await openTransaction(async (tx) => {
|
|
1887
1990
|
const transaction = tx;
|
|
1888
1991
|
if (timeout > 0 && !isNested) {
|
|
1889
1992
|
await transaction.execute(sql.raw(`SET LOCAL statement_timeout = ${timeout}`));
|
|
@@ -1892,81 +1995,117 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1892
1995
|
await transaction.execute(sql.raw(`SET LOCAL idle_in_transaction_session_timeout = ${idleTimeout}`));
|
|
1893
1996
|
}
|
|
1894
1997
|
return await runWithTransaction(transaction, txId, async () => {
|
|
1895
|
-
const innerResult = await callback(transaction);
|
|
1896
1998
|
if (!isNested) {
|
|
1897
1999
|
const ctx = getTransactionContext();
|
|
1898
2000
|
if (ctx) {
|
|
1899
|
-
|
|
2001
|
+
beforeCommitCallbacks = ctx.beforeCommitCallbacks;
|
|
2002
|
+
afterCommitCallbacks = ctx.afterCommitCallbacks;
|
|
2003
|
+
afterRollbackCallbacks = ctx.afterRollbackCallbacks;
|
|
1900
2004
|
}
|
|
1901
2005
|
}
|
|
2006
|
+
const innerResult = await callback(transaction);
|
|
2007
|
+
if (!isNested && beforeCommitCallbacks.length > 0) {
|
|
2008
|
+
if (enableLogging) {
|
|
2009
|
+
logSafely(() => txLogger2.debug("Executing beforeCommit callbacks", {
|
|
2010
|
+
txId,
|
|
2011
|
+
context,
|
|
2012
|
+
count: beforeCommitCallbacks.length
|
|
2013
|
+
}));
|
|
2014
|
+
}
|
|
2015
|
+
await runBeforeCommitCallbacks(beforeCommitCallbacks);
|
|
2016
|
+
}
|
|
1902
2017
|
return innerResult;
|
|
1903
2018
|
});
|
|
1904
2019
|
});
|
|
1905
2020
|
const duration = Date.now() - startTime;
|
|
1906
2021
|
if (enableLogging) {
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
2022
|
+
logSafely(() => {
|
|
2023
|
+
if (duration >= slowThreshold) {
|
|
2024
|
+
txLogger2.warn("Slow transaction committed", {
|
|
2025
|
+
txId,
|
|
2026
|
+
context,
|
|
2027
|
+
duration: `${duration}ms`,
|
|
2028
|
+
threshold: `${slowThreshold}ms`,
|
|
2029
|
+
hint: "A transaction holds a pooled connection (and row locks) for its whole duration. If this is slow because of non-DB work (external API, etc.) inside the transaction, move that work out \u2014 it starves the connection pool."
|
|
2030
|
+
});
|
|
2031
|
+
} else {
|
|
2032
|
+
txLogger2.debug("Transaction committed", {
|
|
2033
|
+
txId,
|
|
2034
|
+
context,
|
|
2035
|
+
duration: `${duration}ms`
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
2038
|
+
});
|
|
1922
2039
|
}
|
|
1923
2040
|
if (!isNested && afterCommitCallbacks.length > 0) {
|
|
1924
2041
|
if (enableLogging) {
|
|
1925
|
-
txLogger2.debug("Executing afterCommit callbacks", {
|
|
2042
|
+
logSafely(() => txLogger2.debug("Executing afterCommit callbacks", {
|
|
1926
2043
|
txId,
|
|
1927
2044
|
context,
|
|
1928
2045
|
count: afterCommitCallbacks.length
|
|
1929
|
-
});
|
|
2046
|
+
}));
|
|
1930
2047
|
}
|
|
1931
2048
|
for (const cb of afterCommitCallbacks) {
|
|
1932
|
-
Promise.resolve().then(cb).catch((err) => {
|
|
2049
|
+
Promise.resolve().then(cb).catch((err) => logSafely(() => {
|
|
1933
2050
|
txLogger2.error("afterCommit callback failed", {
|
|
1934
2051
|
txId,
|
|
1935
2052
|
context,
|
|
1936
2053
|
error: err instanceof Error ? err.message : String(err)
|
|
1937
2054
|
});
|
|
1938
|
-
});
|
|
2055
|
+
}));
|
|
1939
2056
|
}
|
|
1940
2057
|
}
|
|
1941
2058
|
return result;
|
|
1942
2059
|
} catch (error) {
|
|
1943
2060
|
const duration = Date.now() - startTime;
|
|
1944
2061
|
if (enableLogging) {
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
2062
|
+
logSafely(() => {
|
|
2063
|
+
if (duration >= slowThreshold) {
|
|
2064
|
+
txLogger2.warn("Slow transaction rolled back", {
|
|
2065
|
+
txId,
|
|
2066
|
+
context,
|
|
2067
|
+
duration: `${duration}ms`,
|
|
2068
|
+
threshold: `${slowThreshold}ms`,
|
|
2069
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2070
|
+
errorType: error instanceof Error ? error.name : "Unknown",
|
|
2071
|
+
hint: "If the error is an idle-in-transaction timeout, the transaction held a pooled connection while awaiting non-DB work (external API, etc.). Move that work out of the transaction."
|
|
2072
|
+
});
|
|
2073
|
+
} else {
|
|
2074
|
+
txLogger2.error("Transaction rolled back", {
|
|
2075
|
+
txId,
|
|
2076
|
+
context,
|
|
2077
|
+
duration: `${duration}ms`,
|
|
2078
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2079
|
+
errorType: error instanceof Error ? error.name : "Unknown"
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2084
|
+
if (!isNested && afterRollbackCallbacks.length > 0) {
|
|
2085
|
+
await runAfterRollbackCallbacks(afterRollbackCallbacks, txId, context, enableLogging).catch(() => void 0);
|
|
1964
2086
|
}
|
|
1965
2087
|
throw error;
|
|
1966
2088
|
}
|
|
1967
2089
|
}
|
|
1968
2090
|
|
|
1969
2091
|
// src/db/transaction/middleware.ts
|
|
2092
|
+
var SQLSTATE_PATTERN = /^[0-9A-Z]{5}$/;
|
|
2093
|
+
function isDriverOriginError(error) {
|
|
2094
|
+
if (!error || typeof error !== "object") {
|
|
2095
|
+
return false;
|
|
2096
|
+
}
|
|
2097
|
+
const candidate = error;
|
|
2098
|
+
if (typeof candidate.code !== "string") {
|
|
2099
|
+
return false;
|
|
2100
|
+
}
|
|
2101
|
+
if (POSTGRES_JS_CONNECTION_CODES.has(candidate.code)) {
|
|
2102
|
+
return true;
|
|
2103
|
+
}
|
|
2104
|
+
if (!SQLSTATE_PATTERN.test(candidate.code)) {
|
|
2105
|
+
return false;
|
|
2106
|
+
}
|
|
2107
|
+
return typeof candidate.severity === "string" || typeof candidate.severity_local === "string";
|
|
2108
|
+
}
|
|
1970
2109
|
function Transactional(options = {}) {
|
|
1971
2110
|
return createMiddleware(async (c, next) => {
|
|
1972
2111
|
const route = `${c.req.method} ${c.req.path}`;
|
|
@@ -1986,13 +2125,10 @@ function Transactional(options = {}) {
|
|
|
1986
2125
|
);
|
|
1987
2126
|
} catch (error) {
|
|
1988
2127
|
reportDatabaseError(error);
|
|
1989
|
-
if (error instanceof
|
|
1990
|
-
throw error;
|
|
1991
|
-
}
|
|
1992
|
-
if (error instanceof TransactionError) {
|
|
2128
|
+
if (error instanceof SerializableError) {
|
|
1993
2129
|
throw error;
|
|
1994
2130
|
}
|
|
1995
|
-
if (error
|
|
2131
|
+
if (isDriverOriginError(error)) {
|
|
1996
2132
|
throw fromPostgresError(error);
|
|
1997
2133
|
}
|
|
1998
2134
|
throw error;
|
|
@@ -2531,6 +2667,6 @@ var BaseRepository = class {
|
|
|
2531
2667
|
}
|
|
2532
2668
|
};
|
|
2533
2669
|
|
|
2534
|
-
export { BaseRepository, PROJECT_MIGRATIONS_TABLE, PROJECT_TARGET_NAME, RUN_MIGRATIONS_HINT, RepositoryError, Transactional, auditFields, checkConnection, closeDatabase, collectMigrationStatus, count, countPendingMigrations, create, createDatabaseConnection, createDatabaseFromEnv, createMany, createSchema, deleteMany, deleteOne, detectDialect, discoverFunctionMigrations, enumText, filterPendingEntries, findMany, findOne, forceReconnectDatabase, foreignKey, formatPendingMigrations, fromPostgresError, functionMigrationsTable, generateDrizzleConfigFile, getDatabase, getDatabaseInfo, getDrizzleConfig, getSchemaInfo, getTransaction, getTransactionContext, hasMigrationTargets, id, initDatabase, isConnectionLevelError, migrationTargets, onAfterCommit, optionalForeignKey, packageNameToSchema, pendingMigrationTargets, pendingMigrationsSummary, projectMigrationsDir, publishingFields, readMigrationEntries, reportDatabaseError, resetConnectionErrorCounter, runInTransaction, runWithTransaction, setDatabase, setDatabaseProvider, softDelete, timestamps, typedJsonb, updateMany, updateOne, upsert, utcTimestamp, uuid, verificationTimestamp };
|
|
2670
|
+
export { BaseRepository, PROJECT_MIGRATIONS_TABLE, PROJECT_TARGET_NAME, RUN_MIGRATIONS_HINT, RepositoryError, Transactional, auditFields, checkConnection, closeDatabase, collectMigrationStatus, count, countPendingMigrations, create, createDatabaseConnection, createDatabaseFromEnv, createMany, createSchema, deleteMany, deleteOne, detectDialect, discoverFunctionMigrations, enumText, filterPendingEntries, findMany, findOne, forceReconnectDatabase, foreignKey, formatPendingMigrations, fromPostgresError, functionMigrationsTable, generateDrizzleConfigFile, getDatabase, getDatabaseInfo, getDrizzleConfig, getSchemaInfo, getTransaction, getTransactionContext, hasMigrationTargets, id, initDatabase, isConnectionLevelError, migrationTargets, onAfterCommit, onAfterRollback, onBeforeCommit, optionalForeignKey, packageNameToSchema, pendingMigrationTargets, pendingMigrationsSummary, projectMigrationsDir, publishingFields, readMigrationEntries, renderDrizzleConfig, reportDatabaseError, resetConnectionErrorCounter, runInTransaction, runWithTransaction, setDatabase, setDatabaseProvider, softDelete, timestamps, typedJsonb, updateMany, updateOne, upsert, utcTimestamp, uuid, verificationTimestamp };
|
|
2535
2671
|
//# sourceMappingURL=index.js.map
|
|
2536
2672
|
//# sourceMappingURL=index.js.map
|