@spfn/core 0.3.0-beta.3 → 0.3.0-beta.31
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 +304 -12
- package/dist/app-config/index.d.ts +39 -1
- package/dist/app-config/index.js.map +1 -1
- package/dist/authz/index.js +47 -391
- 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 +157 -16
- package/dist/codegen/index.js +772 -166
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.js +4 -4
- package/dist/config/index.js.map +1 -1
- package/dist/contract/index.d.ts +39 -16
- package/dist/contract/index.js +147 -23
- package/dist/contract/index.js.map +1 -1
- package/dist/db/index.d.ts +230 -35
- package/dist/db/index.js +316 -160
- package/dist/db/index.js.map +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-CIZlYJlL.d.ts +157 -0
- package/dist/errors/index.d.ts +18 -1
- package/dist/errors/index.js +47 -391
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +5 -5
- package/dist/event/index.js +202 -3
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +18 -4
- package/dist/event/sse/client.js +94 -18
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +18 -5
- package/dist/event/sse/index.js +452 -114
- package/dist/event/sse/index.js.map +1 -1
- 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.js +52 -397
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +75 -14
- package/dist/nextjs/index.js +200 -84
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +46 -8
- package/dist/nextjs/server.js +15 -1
- package/dist/nextjs/server.js.map +1 -1
- package/dist/ops/index.d.ts +61 -42
- package/dist/ops/index.js +648 -97
- package/dist/ops/index.js.map +1 -1
- package/dist/registry-C2KQCh0m.d.ts +473 -0
- package/dist/route/index.d.ts +5 -2
- package/dist/route/index.js +107 -28
- package/dist/route/index.js.map +1 -1
- package/dist/route/types.d.ts +7 -1
- package/dist/{router-Cy7rAfmj.d.ts → route-builder-DRW-MSCJ.d.ts} +51 -126
- package/dist/router-C57y-vz1.d.ts +184 -0
- package/dist/router-CX5uVjmU.d.ts +406 -0
- package/dist/server/index.d.ts +130 -7
- package/dist/server/index.js +1174 -164
- package/dist/server/index.js.map +1 -1
- package/dist/state/client.d.ts +206 -0
- package/dist/state/client.js +678 -0
- package/dist/state/client.js.map +1 -0
- package/dist/state/index.d.ts +443 -0
- package/dist/state/index.js +644 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/react.d.ts +49 -0
- package/dist/state/react.js +50 -0
- package/dist/state/react.js.map +1 -0
- package/dist/surface/index.d.ts +115 -0
- package/dist/surface/index.js +1013 -0
- package/dist/surface/index.js.map +1 -0
- package/dist/token-manager-Ckjt2-qq.d.ts +122 -0
- package/dist/{types-CfYVhIQ9.d.ts → types-5Rl11_Nb.d.ts} +43 -4
- package/dist/types-CKH0D3Dy.d.ts +441 -0
- package/dist/{types-ZQODsBft.d.ts → types-CurMab2Q.d.ts} +52 -2
- package/dist/{types-2AbaW4Ie.d.ts → types-DncqXfWN.d.ts} +4 -3
- package/docs/file-upload.md +195 -333
- package/package.json +41 -11
- package/src/cache/README.md +330 -0
- package/src/codegen/README.md +721 -0
- package/src/config/README.md +333 -0
- package/src/contract/README.md +442 -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 +845 -0
- package/src/env/README.md +787 -0
- package/src/errors/README.md +445 -0
- package/src/event/README.md +994 -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 +756 -0
- package/src/route/README.md +763 -0
- package/src/security/README.md +100 -0
- package/src/server/README.md +704 -0
- package/src/state/README.md +806 -0
- package/dist/token-manager-BT5EnUAR.d.ts +0 -278
- 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,16 @@ function runWithTransaction(tx, txId, callback) {
|
|
|
1763
1790
|
} else {
|
|
1764
1791
|
txLogger.debug("Root transaction context set", { txId, level: newLevel });
|
|
1765
1792
|
}
|
|
1766
|
-
const afterCommitCallbacks = existingContext ? existingContext.afterCommitCallbacks : [];
|
|
1767
1793
|
return asyncContext.run(
|
|
1768
|
-
{
|
|
1794
|
+
{
|
|
1795
|
+
tx,
|
|
1796
|
+
txId,
|
|
1797
|
+
level: newLevel,
|
|
1798
|
+
beforeCommitCallbacks: [],
|
|
1799
|
+
afterCommitCallbacks: [],
|
|
1800
|
+
afterRollbackCallbacks: [],
|
|
1801
|
+
nestedFrames: createNestedFrameGate()
|
|
1802
|
+
},
|
|
1769
1803
|
callback
|
|
1770
1804
|
);
|
|
1771
1805
|
}
|
|
@@ -1781,14 +1815,79 @@ function onAfterCommit(callback) {
|
|
|
1781
1815
|
}
|
|
1782
1816
|
context.afterCommitCallbacks.push(callback);
|
|
1783
1817
|
}
|
|
1818
|
+
function onBeforeCommit(callback) {
|
|
1819
|
+
const context = getTransactionContext();
|
|
1820
|
+
if (!context) {
|
|
1821
|
+
txLogger.warn(
|
|
1822
|
+
"beforeCommit callback ran immediately (no transaction): a throw cannot abort anything"
|
|
1823
|
+
);
|
|
1824
|
+
Promise.resolve().then(callback).catch((err) => {
|
|
1825
|
+
txLogger.error("beforeCommit callback failed (no transaction)", {
|
|
1826
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1827
|
+
});
|
|
1828
|
+
});
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
context.beforeCommitCallbacks.push(callback);
|
|
1832
|
+
}
|
|
1833
|
+
function onAfterRollback(callback) {
|
|
1834
|
+
const context = getTransactionContext();
|
|
1835
|
+
if (!context) {
|
|
1836
|
+
txLogger.warn("afterRollback callback ignored (no transaction)");
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
context.afterRollbackCallbacks.push(callback);
|
|
1840
|
+
}
|
|
1784
1841
|
var MAX_TIMEOUT_MS = 2147483647;
|
|
1785
1842
|
var txLogger2 = logger.child("@spfn/core:transaction");
|
|
1843
|
+
function logSafely(emit) {
|
|
1844
|
+
try {
|
|
1845
|
+
emit();
|
|
1846
|
+
} catch {
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
async function runBeforeCommitCallbacks(callbacks) {
|
|
1850
|
+
for (const cb of [...callbacks]) {
|
|
1851
|
+
await cb();
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
async function runAfterRollbackCallbacks(callbacks, txId, context, enableLogging) {
|
|
1855
|
+
if (enableLogging) {
|
|
1856
|
+
logSafely(() => txLogger2.debug("Executing afterRollback callbacks", {
|
|
1857
|
+
txId,
|
|
1858
|
+
context,
|
|
1859
|
+
count: callbacks.length
|
|
1860
|
+
}));
|
|
1861
|
+
}
|
|
1862
|
+
for (const cb of callbacks) {
|
|
1863
|
+
await Promise.resolve().then(cb).catch((err) => logSafely(() => {
|
|
1864
|
+
txLogger2.error("afterRollback callback failed", {
|
|
1865
|
+
txId,
|
|
1866
|
+
context,
|
|
1867
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1868
|
+
});
|
|
1869
|
+
}));
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
function promoteFrameHooks(frame, parent) {
|
|
1873
|
+
parent.beforeCommitCallbacks.push(...frame.beforeCommitCallbacks);
|
|
1874
|
+
parent.afterCommitCallbacks.push(...frame.afterCommitCallbacks);
|
|
1875
|
+
parent.afterRollbackCallbacks.push(...frame.afterRollbackCallbacks);
|
|
1876
|
+
}
|
|
1877
|
+
async function discardFrameHooks(frame, parent, error, txId, context, enableLogging) {
|
|
1878
|
+
await asyncContext.run(
|
|
1879
|
+
{ ...parent, nestedFrames: createNestedFrameGate() },
|
|
1880
|
+
() => runAfterRollbackCallbacks(frame.afterRollbackCallbacks, txId, context, enableLogging)
|
|
1881
|
+
).catch(() => void 0);
|
|
1882
|
+
throw error;
|
|
1883
|
+
}
|
|
1786
1884
|
async function runInTransaction(callback, options = {}) {
|
|
1787
1885
|
const defaultTimeout = env.TRANSACTION_TIMEOUT;
|
|
1788
1886
|
const {
|
|
1789
1887
|
slowThreshold = 1e3,
|
|
1790
1888
|
enableLogging = true,
|
|
1791
|
-
context = "transaction"
|
|
1889
|
+
context = "transaction",
|
|
1890
|
+
requiresNew = false
|
|
1792
1891
|
} = options;
|
|
1793
1892
|
const timeout = options.timeout ?? defaultTimeout;
|
|
1794
1893
|
const idleTimeout = options.idleTimeout ?? env.TRANSACTION_IDLE_TIMEOUT;
|
|
@@ -1866,24 +1965,50 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1866
1965
|
}
|
|
1867
1966
|
throw error;
|
|
1868
1967
|
}
|
|
1869
|
-
const
|
|
1870
|
-
const isNested =
|
|
1871
|
-
if (isNested && timeout > 0 && enableLogging) {
|
|
1968
|
+
const savepointOwner = requiresNew ? null : getTransactionContext();
|
|
1969
|
+
const isNested = savepointOwner !== null;
|
|
1970
|
+
if (isNested && options.timeout !== void 0 && options.timeout > 0 && enableLogging) {
|
|
1872
1971
|
txLogger2.warn("Timeout ignored in nested transaction", {
|
|
1873
1972
|
txId,
|
|
1874
1973
|
context,
|
|
1875
|
-
outerTxId:
|
|
1974
|
+
outerTxId: savepointOwner.txId,
|
|
1876
1975
|
requestedTimeout: `${timeout}ms`,
|
|
1877
|
-
reason: "SET LOCAL
|
|
1976
|
+
reason: "the SAVEPOINT runs under the outer transaction's statement_timeout; SET LOCAL here would re-scope the whole outer transaction"
|
|
1878
1977
|
});
|
|
1879
1978
|
}
|
|
1880
1979
|
if (enableLogging) {
|
|
1881
1980
|
txLogger2.debug("Transaction started", { txId, context });
|
|
1882
1981
|
}
|
|
1883
1982
|
const startTime = Date.now();
|
|
1983
|
+
let beforeCommitCallbacks = [];
|
|
1884
1984
|
let afterCommitCallbacks = [];
|
|
1985
|
+
let afterRollbackCallbacks = [];
|
|
1986
|
+
const openTransaction = (body) => {
|
|
1987
|
+
if (savepointOwner) {
|
|
1988
|
+
return savepointOwner.nestedFrames.run(
|
|
1989
|
+
() => savepointOwner.tx.transaction(body).then(
|
|
1990
|
+
(result) => {
|
|
1991
|
+
promoteFrameHooks({ beforeCommitCallbacks, afterCommitCallbacks, afterRollbackCallbacks }, savepointOwner);
|
|
1992
|
+
return result;
|
|
1993
|
+
},
|
|
1994
|
+
(error) => discardFrameHooks(
|
|
1995
|
+
{ afterRollbackCallbacks },
|
|
1996
|
+
savepointOwner,
|
|
1997
|
+
error,
|
|
1998
|
+
txId,
|
|
1999
|
+
context,
|
|
2000
|
+
enableLogging
|
|
2001
|
+
)
|
|
2002
|
+
)
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
if (requiresNew) {
|
|
2006
|
+
return asyncContext.exit(() => writeDb.transaction(body));
|
|
2007
|
+
}
|
|
2008
|
+
return writeDb.transaction(body);
|
|
2009
|
+
};
|
|
1885
2010
|
try {
|
|
1886
|
-
const result = await
|
|
2011
|
+
const result = await openTransaction(async (tx) => {
|
|
1887
2012
|
const transaction = tx;
|
|
1888
2013
|
if (timeout > 0 && !isNested) {
|
|
1889
2014
|
await transaction.execute(sql.raw(`SET LOCAL statement_timeout = ${timeout}`));
|
|
@@ -1892,81 +2017,115 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1892
2017
|
await transaction.execute(sql.raw(`SET LOCAL idle_in_transaction_session_timeout = ${idleTimeout}`));
|
|
1893
2018
|
}
|
|
1894
2019
|
return await runWithTransaction(transaction, txId, async () => {
|
|
2020
|
+
const ctx = getTransactionContext();
|
|
2021
|
+
if (ctx) {
|
|
2022
|
+
beforeCommitCallbacks = ctx.beforeCommitCallbacks;
|
|
2023
|
+
afterCommitCallbacks = ctx.afterCommitCallbacks;
|
|
2024
|
+
afterRollbackCallbacks = ctx.afterRollbackCallbacks;
|
|
2025
|
+
}
|
|
1895
2026
|
const innerResult = await callback(transaction);
|
|
1896
|
-
if (!isNested) {
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
2027
|
+
if (!isNested && beforeCommitCallbacks.length > 0) {
|
|
2028
|
+
if (enableLogging) {
|
|
2029
|
+
logSafely(() => txLogger2.debug("Executing beforeCommit callbacks", {
|
|
2030
|
+
txId,
|
|
2031
|
+
context,
|
|
2032
|
+
count: beforeCommitCallbacks.length
|
|
2033
|
+
}));
|
|
1900
2034
|
}
|
|
2035
|
+
await runBeforeCommitCallbacks(beforeCommitCallbacks);
|
|
1901
2036
|
}
|
|
1902
2037
|
return innerResult;
|
|
1903
2038
|
});
|
|
1904
2039
|
});
|
|
1905
2040
|
const duration = Date.now() - startTime;
|
|
1906
2041
|
if (enableLogging) {
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
2042
|
+
logSafely(() => {
|
|
2043
|
+
if (duration >= slowThreshold) {
|
|
2044
|
+
txLogger2.warn("Slow transaction committed", {
|
|
2045
|
+
txId,
|
|
2046
|
+
context,
|
|
2047
|
+
duration: `${duration}ms`,
|
|
2048
|
+
threshold: `${slowThreshold}ms`,
|
|
2049
|
+
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."
|
|
2050
|
+
});
|
|
2051
|
+
} else {
|
|
2052
|
+
txLogger2.debug("Transaction committed", {
|
|
2053
|
+
txId,
|
|
2054
|
+
context,
|
|
2055
|
+
duration: `${duration}ms`
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
});
|
|
1922
2059
|
}
|
|
1923
2060
|
if (!isNested && afterCommitCallbacks.length > 0) {
|
|
1924
2061
|
if (enableLogging) {
|
|
1925
|
-
txLogger2.debug("Executing afterCommit callbacks", {
|
|
2062
|
+
logSafely(() => txLogger2.debug("Executing afterCommit callbacks", {
|
|
1926
2063
|
txId,
|
|
1927
2064
|
context,
|
|
1928
2065
|
count: afterCommitCallbacks.length
|
|
1929
|
-
});
|
|
2066
|
+
}));
|
|
1930
2067
|
}
|
|
1931
2068
|
for (const cb of afterCommitCallbacks) {
|
|
1932
|
-
Promise.resolve().then(cb).catch((err) => {
|
|
2069
|
+
Promise.resolve().then(cb).catch((err) => logSafely(() => {
|
|
1933
2070
|
txLogger2.error("afterCommit callback failed", {
|
|
1934
2071
|
txId,
|
|
1935
2072
|
context,
|
|
1936
2073
|
error: err instanceof Error ? err.message : String(err)
|
|
1937
2074
|
});
|
|
1938
|
-
});
|
|
2075
|
+
}));
|
|
1939
2076
|
}
|
|
1940
2077
|
}
|
|
1941
2078
|
return result;
|
|
1942
2079
|
} catch (error) {
|
|
1943
2080
|
const duration = Date.now() - startTime;
|
|
1944
2081
|
if (enableLogging) {
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
2082
|
+
logSafely(() => {
|
|
2083
|
+
if (duration >= slowThreshold) {
|
|
2084
|
+
txLogger2.warn("Slow transaction rolled back", {
|
|
2085
|
+
txId,
|
|
2086
|
+
context,
|
|
2087
|
+
duration: `${duration}ms`,
|
|
2088
|
+
threshold: `${slowThreshold}ms`,
|
|
2089
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2090
|
+
errorType: error instanceof Error ? error.name : "Unknown",
|
|
2091
|
+
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."
|
|
2092
|
+
});
|
|
2093
|
+
} else {
|
|
2094
|
+
txLogger2.error("Transaction rolled back", {
|
|
2095
|
+
txId,
|
|
2096
|
+
context,
|
|
2097
|
+
duration: `${duration}ms`,
|
|
2098
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2099
|
+
errorType: error instanceof Error ? error.name : "Unknown"
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
2104
|
+
if (!isNested && afterRollbackCallbacks.length > 0) {
|
|
2105
|
+
await runAfterRollbackCallbacks(afterRollbackCallbacks, txId, context, enableLogging).catch(() => void 0);
|
|
1964
2106
|
}
|
|
1965
2107
|
throw error;
|
|
1966
2108
|
}
|
|
1967
2109
|
}
|
|
1968
2110
|
|
|
1969
2111
|
// src/db/transaction/middleware.ts
|
|
2112
|
+
var SQLSTATE_PATTERN = /^[0-9A-Z]{5}$/;
|
|
2113
|
+
function isDriverOriginError(error) {
|
|
2114
|
+
if (!error || typeof error !== "object") {
|
|
2115
|
+
return false;
|
|
2116
|
+
}
|
|
2117
|
+
const candidate = error;
|
|
2118
|
+
if (typeof candidate.code !== "string") {
|
|
2119
|
+
return false;
|
|
2120
|
+
}
|
|
2121
|
+
if (POSTGRES_JS_CONNECTION_CODES.has(candidate.code)) {
|
|
2122
|
+
return true;
|
|
2123
|
+
}
|
|
2124
|
+
if (!SQLSTATE_PATTERN.test(candidate.code)) {
|
|
2125
|
+
return false;
|
|
2126
|
+
}
|
|
2127
|
+
return typeof candidate.severity === "string" || typeof candidate.severity_local === "string";
|
|
2128
|
+
}
|
|
1970
2129
|
function Transactional(options = {}) {
|
|
1971
2130
|
return createMiddleware(async (c, next) => {
|
|
1972
2131
|
const route = `${c.req.method} ${c.req.path}`;
|
|
@@ -1986,13 +2145,10 @@ function Transactional(options = {}) {
|
|
|
1986
2145
|
);
|
|
1987
2146
|
} catch (error) {
|
|
1988
2147
|
reportDatabaseError(error);
|
|
1989
|
-
if (error instanceof
|
|
1990
|
-
throw error;
|
|
1991
|
-
}
|
|
1992
|
-
if (error instanceof TransactionError) {
|
|
2148
|
+
if (error instanceof SerializableError) {
|
|
1993
2149
|
throw error;
|
|
1994
2150
|
}
|
|
1995
|
-
if (error
|
|
2151
|
+
if (isDriverOriginError(error)) {
|
|
1996
2152
|
throw fromPostgresError(error);
|
|
1997
2153
|
}
|
|
1998
2154
|
throw error;
|
|
@@ -2531,6 +2687,6 @@ var BaseRepository = class {
|
|
|
2531
2687
|
}
|
|
2532
2688
|
};
|
|
2533
2689
|
|
|
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 };
|
|
2690
|
+
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
2691
|
//# sourceMappingURL=index.js.map
|
|
2536
2692
|
//# sourceMappingURL=index.js.map
|