sqlstack 3.3.0 → 3.4.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.
Files changed (64) hide show
  1. package/dist/cjs/adapters/sqlite.d.ts.map +1 -1
  2. package/dist/cjs/adapters/sqlite.js +4 -1
  3. package/dist/cjs/adapters/sqlite.js.map +1 -1
  4. package/dist/cjs/index.d.ts +1 -1
  5. package/dist/cjs/index.d.ts.map +1 -1
  6. package/dist/cjs/index.js +2 -1
  7. package/dist/cjs/index.js.map +1 -1
  8. package/dist/cjs/stack.d.ts +2 -0
  9. package/dist/cjs/stack.d.ts.map +1 -1
  10. package/dist/cjs/stack.js +40 -27
  11. package/dist/cjs/stack.js.map +1 -1
  12. package/dist/cjs/testing/builder.d.ts.map +1 -1
  13. package/dist/cjs/testing/builder.js +14 -5
  14. package/dist/cjs/testing/builder.js.map +1 -1
  15. package/dist/cjs/testing/fixture-provenance.d.ts +3 -0
  16. package/dist/cjs/testing/fixture-provenance.d.ts.map +1 -0
  17. package/dist/cjs/testing/fixture-provenance.js +21 -0
  18. package/dist/cjs/testing/fixture-provenance.js.map +1 -0
  19. package/dist/cjs/testing/index.d.ts +2 -0
  20. package/dist/cjs/testing/index.d.ts.map +1 -1
  21. package/dist/cjs/testing/index.js +5 -1
  22. package/dist/cjs/testing/index.js.map +1 -1
  23. package/dist/cjs/testing/postgres/database.d.ts +9 -1
  24. package/dist/cjs/testing/postgres/database.d.ts.map +1 -1
  25. package/dist/cjs/testing/postgres/database.js +17 -13
  26. package/dist/cjs/testing/postgres/database.js.map +1 -1
  27. package/dist/cjs/testing/resource.d.ts +76 -0
  28. package/dist/cjs/testing/resource.d.ts.map +1 -0
  29. package/dist/cjs/testing/resource.js +117 -0
  30. package/dist/cjs/testing/resource.js.map +1 -0
  31. package/dist/cjs/testing/sqlite/builder.d.ts.map +1 -1
  32. package/dist/cjs/testing/sqlite/builder.js +10 -2
  33. package/dist/cjs/testing/sqlite/builder.js.map +1 -1
  34. package/dist/cjs/testing/sqlite/database.d.ts +11 -1
  35. package/dist/cjs/testing/sqlite/database.d.ts.map +1 -1
  36. package/dist/cjs/testing/sqlite/database.js +32 -18
  37. package/dist/cjs/testing/sqlite/database.js.map +1 -1
  38. package/dist/cjs/transactions.d.ts +6 -0
  39. package/dist/cjs/transactions.d.ts.map +1 -1
  40. package/dist/cjs/transactions.js +9 -0
  41. package/dist/cjs/transactions.js.map +1 -1
  42. package/dist/esm/adapters/sqlite.js +4 -1
  43. package/dist/esm/adapters/sqlite.js.map +1 -1
  44. package/dist/esm/index.js +1 -1
  45. package/dist/esm/index.js.map +1 -1
  46. package/dist/esm/stack.js +40 -27
  47. package/dist/esm/stack.js.map +1 -1
  48. package/dist/esm/testing/builder.js +14 -5
  49. package/dist/esm/testing/builder.js.map +1 -1
  50. package/dist/esm/testing/fixture-provenance.js +15 -0
  51. package/dist/esm/testing/fixture-provenance.js.map +1 -0
  52. package/dist/esm/testing/index.js +1 -0
  53. package/dist/esm/testing/index.js.map +1 -1
  54. package/dist/esm/testing/postgres/database.js +17 -13
  55. package/dist/esm/testing/postgres/database.js.map +1 -1
  56. package/dist/esm/testing/resource.js +106 -0
  57. package/dist/esm/testing/resource.js.map +1 -0
  58. package/dist/esm/testing/sqlite/builder.js +10 -2
  59. package/dist/esm/testing/sqlite/builder.js.map +1 -1
  60. package/dist/esm/testing/sqlite/database.js +32 -18
  61. package/dist/esm/testing/sqlite/database.js.map +1 -1
  62. package/dist/esm/transactions.js +8 -0
  63. package/dist/esm/transactions.js.map +1 -1
  64. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"database.js","sourceRoot":"","sources":["../../../../src/testing/postgres/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,uBAAuB,EAAqB,MAAM,UAAU,CAAC;AAgB9E;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,KAAoB,EACpB,UAA6B,EAAE;IAE/B,MAAM,EAAE,GAAG,MAAM,MAAM,EAAE,CAAC;IAC1B,MAAM,IAAI,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC;IACzC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,EAAE,gBAAgB,EAAE,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;IAC/D,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,OAAO;QACL,GAAG,EAAE,KAAK,CAAC,GAAG;QACd,KAAK,EAAE,IAAI;QACX,KAAK,CAAC,KAAK,CAAC,GAAW,EAAE,SAA6B,EAAE;YACtD,IAAI,QAAQ,EAAE,CAAC;gBACb,MAAM,IAAI,uBAAuB,CAC/B,kBAAkB,KAAK,CAAC,IAAI,uDAAuD,CACpF,CAAC;YACJ,CAAC;YACD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,MAAmB,CAAC,CAAC;YACvD,OAAO,GAAG,CAAC,IAAI,CAAC;QAClB,CAAC;QACD,KAAK,CAAC,OAAO;YACX,IAAI,QAAQ;gBAAE,OAAO;YACrB,QAAQ,GAAG,IAAI,CAAC;YAChB,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC;YACnB,CAAC;YAAC,MAAM,CAAC;gBACP,gCAAgC;YAClC,CAAC;YACD,MAAM,YAAY,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC1C,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import { dropDatabase } from './allocator';\nimport { loadPg, TestPostgresServerError, TestServerOptions } from './server';\n\n/** The built test database handle. */\nexport interface TestPostgresDatabase {\n /** Normal PostgreSQL connection string identifying only this database. */\n readonly url: string;\n query(sql: string, params?: readonly unknown[]): Promise<unknown>;\n /** Close helper connections and drop the owned database. Idempotent. */\n dispose(): Promise<void>;\n}\n\nexport interface OwnedDatabase {\n readonly name: string;\n readonly url: string;\n}\n\n/**\n * Connect a helper pool to an allocated database and produce the handle.\n * The handle owns exactly this database and drops it on dispose.\n */\nexport async function createDatabaseHandle(\n owned: OwnedDatabase,\n options: TestServerOptions = {}\n): Promise<TestPostgresDatabase & { _pool: any }> {\n const pg = await loadPg();\n const Pool = pg.Pool ?? pg.default?.Pool;\n const pool = new Pool({ connectionString: owned.url, max: 4 });\n let disposed = false;\n return {\n url: owned.url,\n _pool: pool,\n async query(sql: string, params: readonly unknown[] = []) {\n if (disposed) {\n throw new TestPostgresServerError(\n `Test database \"${owned.name}\" has been disposed; no further queries are possible.`\n );\n }\n const res = await pool.query(sql, params as unknown[]);\n return res.rows;\n },\n async dispose() {\n if (disposed) return;\n disposed = true;\n try {\n await pool.end();\n } catch {\n /* pool may already be closed */\n }\n await dropDatabase(owned.name, options);\n },\n };\n}\n"]}
1
+ {"version":3,"file":"database.js","sourceRoot":"","sources":["../../../../src/testing/postgres/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,uBAAuB,EAAqB,MAAM,UAAU,CAAC;AAC9E,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAwBhG;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,KAAoB,EACpB,UAA6B,EAAE;IAE/B,MAAM,EAAE,GAAG,MAAM,MAAM,EAAE,CAAC;IAC1B,MAAM,IAAI,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC;IACzC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,EAAE,gBAAgB,EAAE,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;IAC/D,MAAM,UAAU,GAAG,cAAc,CAAC,UAAU,CAAC,CAAC;IAC9C,IAAI,QAAmC,CAAC;IAExC,MAAM,MAAM,GAA0C;QACpD,MAAM,EAAE,UAAU;QAClB,UAAU;QACV,GAAG,EAAE,KAAK,CAAC,GAAG;QACd,KAAK,EAAE,IAAI;QACX,KAAK,CAAC,KAAK,CAAC,GAAW,EAAE,SAA6B,EAAE;YACtD,IAAI,QAAQ,EAAE,CAAC;gBACb,MAAM,IAAI,uBAAuB,CAC/B,kBAAkB,KAAK,CAAC,IAAI,uDAAuD,CACpF,CAAC;YACJ,CAAC;YACD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,MAAmB,CAAC,CAAC;YACvD,OAAO,GAAG,CAAC,IAAI,CAAC;QAClB,CAAC;QACD,OAAO;YACL,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,cAAc,CAAC,MAAM,CAAC,CAAC;gBACvB,QAAQ,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,kBAAkB,KAAK,CAAC,IAAI,GAAG,EAAE;oBACvF,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;oBAChB,GAAG,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC;iBACxC,CAAC,CAAC,CAAC;YACN,CAAC;YACD,OAAO,QAAQ,CAAC;QAClB,CAAC;KACF,CAAC;IACF,gBAAgB,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC,CAAC;IAC7D,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["import { dropDatabase } from './allocator';\nimport { loadPg, TestPostgresServerError, TestServerOptions } from './server';\nimport { mintResourceId, registerResource, retireResource, runCleanupSteps } from '../resource';\n\n/** The built test database handle. */\nexport interface TestPostgresDatabase {\n /** Engine discriminator for SQLStack-minted disposable test databases. */\n readonly engine: 'postgres';\n /** Fresh per-allocation identifier; never reused within a process. */\n readonly resourceId: string;\n /** Normal PostgreSQL connection string identifying only this database. */\n readonly url: string;\n query(sql: string, params?: readonly unknown[]): Promise<unknown>;\n /**\n * Close helper connections and drop the owned database. Idempotent:\n * concurrent and repeated calls share one outcome. Every cleanup step is\n * attempted; failures are aggregated into a `TestDatabaseDisposeError`.\n */\n dispose(): Promise<void>;\n}\n\nexport interface OwnedDatabase {\n readonly name: string;\n readonly url: string;\n}\n\n/**\n * Connect a helper pool to an allocated database and produce the handle.\n * The handle owns exactly this database and drops it on dispose.\n */\nexport async function createDatabaseHandle(\n owned: OwnedDatabase,\n options: TestServerOptions = {}\n): Promise<TestPostgresDatabase & { _pool: any }> {\n const pg = await loadPg();\n const Pool = pg.Pool ?? pg.default?.Pool;\n const pool = new Pool({ connectionString: owned.url, max: 4 });\n const resourceId = mintResourceId('postgres');\n let disposal: Promise<void> | undefined;\n\n const handle: TestPostgresDatabase & { _pool: any } = {\n engine: 'postgres',\n resourceId,\n url: owned.url,\n _pool: pool,\n async query(sql: string, params: readonly unknown[] = []) {\n if (disposal) {\n throw new TestPostgresServerError(\n `Test database \"${owned.name}\" has been disposed; no further queries are possible.`\n );\n }\n const res = await pool.query(sql, params as unknown[]);\n return res.rows;\n },\n dispose() {\n if (!disposal) {\n retireResource(handle);\n disposal = Promise.resolve().then(() => runCleanupSteps(`Test database \"${owned.name}\"`, [\n () => pool.end(),\n () => dropDatabase(owned.name, options),\n ]));\n }\n return disposal;\n },\n };\n registerResource(handle, { engine: 'postgres', resourceId });\n return handle;\n}\n"]}
@@ -0,0 +1,106 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { SqlStackError } from '../core/errors.js';
3
+ /** Handles whose owned resources are currently allocated (not yet disposed). */
4
+ const active = new WeakSet();
5
+ /** Provenance recorded at mint time, kept for the handle's whole lifetime. */
6
+ const provenance = new WeakMap();
7
+ let counter = 0;
8
+ /** Fresh, process-unique identifier for one allocation. */
9
+ export function mintResourceId(engine) {
10
+ counter += 1;
11
+ return `${engine}-${randomBytes(6).toString('hex')}-${counter}`;
12
+ }
13
+ /**
14
+ * Register a freshly allocated handle as an active SQLStack resource.
15
+ * Only fixture allocation code paths call this; it is never exported from
16
+ * `sqlstack/testing`.
17
+ */
18
+ export function registerResource(handle, record) {
19
+ const locator = handle;
20
+ provenance.set(handle, Object.freeze({ ...record, url: locator.url, db: locator.db,
21
+ conn: locator.conn, path: locator.path, query: locator.query, dispose: locator.dispose }));
22
+ active.add(handle);
23
+ }
24
+ /**
25
+ * Optional shared testing resource-scope ABI. SQLStack remains independently
26
+ * usable; it never creates a scope or imports a test runner.
27
+ * Called within the builder's rollback boundary before returning a fixture.
28
+ */
29
+ export function adoptFixtureResource(handle) {
30
+ const channel = globalThis[Symbol.for('@noego/testing.resource-scope.v1')];
31
+ if (channel) {
32
+ if (typeof channel.getStore !== 'function')
33
+ throw new Error('Incompatible NoEgo resource-scope v1 channel');
34
+ channel.getStore()?.own(handle, 'sqlstack fixture');
35
+ }
36
+ return handle;
37
+ }
38
+ /** Mark a handle's owned resources as released. Verification fails from now on. */
39
+ export function retireResource(handle) {
40
+ active.delete(handle);
41
+ }
42
+ /**
43
+ * True only for a handle object that was minted by a SQLStack fixture
44
+ * allocation in this process and has not been disposed. Field checks are a
45
+ * consistency guard on top of identity membership, never a substitute for it.
46
+ */
47
+ export function isDisposableTestDatabase(value) {
48
+ if (value === null || (typeof value !== 'object' && typeof value !== 'function'))
49
+ return false;
50
+ const handle = value;
51
+ if (!active.has(handle))
52
+ return false;
53
+ const record = provenance.get(handle);
54
+ if (!record)
55
+ return false;
56
+ const candidate = handle;
57
+ if (candidate.engine !== record.engine || candidate.resourceId !== record.resourceId)
58
+ return false;
59
+ if (record.engine === 'sqlite' && candidate.storage !== record.storage)
60
+ return false;
61
+ const locator = handle;
62
+ for (const key of ['url', 'db', 'conn', 'path', 'query', 'dispose']) {
63
+ if (locator[key] !== record[key])
64
+ return false;
65
+ }
66
+ return true;
67
+ }
68
+ /** Failed fixture construction plus failed rollback, preserving both causes. */
69
+ export class TestDatabaseBuildError extends SqlStackError {
70
+ constructor(message, cause, cleanup) {
71
+ super(message);
72
+ this.cause = cause;
73
+ this.name = 'TestDatabaseBuildError';
74
+ this.errors = Object.freeze([cause, cleanup]);
75
+ }
76
+ }
77
+ /** Raised by `dispose()` when one or more owned cleanup steps failed. */
78
+ export class TestDatabaseDisposeError extends SqlStackError {
79
+ constructor(message, errors) {
80
+ super(message);
81
+ this.name = 'TestDatabaseDisposeError';
82
+ this.errors = Object.freeze([...errors]);
83
+ }
84
+ }
85
+ /**
86
+ * Run every cleanup step, retaining each failure, then either resolve or
87
+ * reject once with all retained errors.
88
+ */
89
+ export async function runCleanupSteps(label, steps) {
90
+ const errors = [];
91
+ for (const step of steps) {
92
+ try {
93
+ await step();
94
+ }
95
+ catch (err) {
96
+ errors.push(err);
97
+ }
98
+ }
99
+ if (errors.length === 0)
100
+ return;
101
+ const detail = errors
102
+ .map((err, index) => ` [${index + 1}] ${err instanceof Error ? err.message : String(err)}`)
103
+ .join('\n');
104
+ throw new TestDatabaseDisposeError(`${label}: ${errors.length} cleanup step(s) failed while disposing:\n${detail}`, errors);
105
+ }
106
+ //# sourceMappingURL=resource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource.js","sourceRoot":"","sources":["../../../src/testing/resource.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { SqlStackError } from '../core/errors';\nimport type { TestPostgresDatabase } from './postgres/database';\nimport type { TestSqliteDatabase } from './sqlite/database';\n\n/**\n * Internal provenance for SQLStack-minted disposable test databases.\n *\n * Every handle allocated by a SQLStack fixture builder is registered here at\n * allocation time and retired on dispose. Membership is module-private:\n * nothing outside this package can mint, register, or extend a handle's\n * lifetime, and structural copies (spread, `{ ...handle }`, hand-built\n * lookalikes) never validate because verification is by object identity.\n */\n\n/** A disposable test database minted by a SQLStack fixture allocation. */\nexport type DisposableTestDatabase =\n | (TestPostgresDatabase & { readonly engine: 'postgres'; readonly resourceId: string })\n | (TestSqliteDatabase & {\n readonly engine: 'sqlite';\n readonly resourceId: string;\n readonly storage: 'memory' | 'file';\n });\n\nexport type DisposableEngine = DisposableTestDatabase['engine'];\n\ninterface ResourceRecord {\n readonly engine: DisposableEngine;\n readonly resourceId: string;\n readonly storage?: 'memory' | 'file';\n readonly url?: unknown;\n readonly db?: unknown;\n readonly conn?: unknown;\n readonly path?: unknown;\n readonly query?: unknown;\n readonly dispose?: unknown;\n}\n\n/** Handles whose owned resources are currently allocated (not yet disposed). */\nconst active = new WeakSet<object>();\n/** Provenance recorded at mint time, kept for the handle's whole lifetime. */\nconst provenance = new WeakMap<object, ResourceRecord>();\n\nlet counter = 0;\n\n/** Fresh, process-unique identifier for one allocation. */\nexport function mintResourceId(engine: DisposableEngine): string {\n counter += 1;\n return `${engine}-${randomBytes(6).toString('hex')}-${counter}`;\n}\n\n/**\n * Register a freshly allocated handle as an active SQLStack resource.\n * Only fixture allocation code paths call this; it is never exported from\n * `sqlstack/testing`.\n */\nexport function registerResource(handle: object, record: ResourceRecord): void {\n const locator = handle as Record<string, unknown>;\n provenance.set(handle, Object.freeze({ ...record, url: locator.url, db: locator.db,\n conn: locator.conn, path: locator.path, query: locator.query, dispose: locator.dispose }));\n active.add(handle);\n}\n\n/**\n * Optional shared testing resource-scope ABI. SQLStack remains independently\n * usable; it never creates a scope or imports a test runner.\n * Called within the builder's rollback boundary before returning a fixture.\n */\nexport function adoptFixtureResource<T extends { dispose(): unknown }>(handle: T): T {\n const channel = (globalThis as Record<PropertyKey, any>)[Symbol.for('@noego/testing.resource-scope.v1')];\n if (channel) {\n if (typeof channel.getStore !== 'function') throw new Error('Incompatible NoEgo resource-scope v1 channel');\n channel.getStore()?.own(handle, 'sqlstack fixture');\n }\n return handle;\n}\n\n/** Mark a handle's owned resources as released. Verification fails from now on. */\nexport function retireResource(handle: object): void {\n active.delete(handle);\n}\n\n/**\n * True only for a handle object that was minted by a SQLStack fixture\n * allocation in this process and has not been disposed. Field checks are a\n * consistency guard on top of identity membership, never a substitute for it.\n */\nexport function isDisposableTestDatabase(value: unknown): value is DisposableTestDatabase {\n if (value === null || (typeof value !== 'object' && typeof value !== 'function')) return false;\n const handle = value as object;\n if (!active.has(handle)) return false;\n const record = provenance.get(handle);\n if (!record) return false;\n const candidate = handle as { engine?: unknown; resourceId?: unknown; storage?: unknown };\n if (candidate.engine !== record.engine || candidate.resourceId !== record.resourceId) return false;\n if (record.engine === 'sqlite' && candidate.storage !== record.storage) return false;\n const locator = handle as Record<string, unknown>;\n for (const key of ['url', 'db', 'conn', 'path', 'query', 'dispose'] as const) {\n if (locator[key] !== record[key]) return false;\n }\n return true;\n}\n\n/** Failed fixture construction plus failed rollback, preserving both causes. */\nexport class TestDatabaseBuildError extends SqlStackError {\n readonly errors: readonly unknown[];\n constructor(message: string, readonly cause: unknown, cleanup: unknown) {\n super(message);\n this.name = 'TestDatabaseBuildError';\n this.errors = Object.freeze([cause, cleanup]);\n }\n}\n\n/** Raised by `dispose()` when one or more owned cleanup steps failed. */\nexport class TestDatabaseDisposeError extends SqlStackError {\n /** Every cleanup failure, in attempt order. Nothing is swallowed. */\n readonly errors: readonly unknown[];\n\n constructor(message: string, errors: readonly unknown[]) {\n super(message);\n this.name = 'TestDatabaseDisposeError';\n this.errors = Object.freeze([...errors]);\n }\n}\n\n/**\n * Run every cleanup step, retaining each failure, then either resolve or\n * reject once with all retained errors.\n */\nexport async function runCleanupSteps(\n label: string,\n steps: readonly (() => Promise<void> | void)[]\n): Promise<void> {\n const errors: unknown[] = [];\n for (const step of steps) {\n try {\n await step();\n } catch (err) {\n errors.push(err);\n }\n }\n if (errors.length === 0) return;\n const detail = errors\n .map((err, index) => ` [${index + 1}] ${err instanceof Error ? err.message : String(err)}`)\n .join('\\n');\n throw new TestDatabaseDisposeError(\n `${label}: ${errors.length} cleanup step(s) failed while disposing:\\n${detail}`,\n errors\n );\n}\n"]}
@@ -1,4 +1,6 @@
1
1
  import * as fs from 'node:fs';
2
+ import { adoptFixtureResource, TestDatabaseBuildError } from '../resource.js';
3
+ import { assertFixtureSqlFile } from '../fixture-provenance.js';
2
4
  import { generateDdl } from '../schema/ddl.js';
3
5
  import { parseTestSchema } from '../schema/parser.js';
4
6
  import { createDatabaseHandle, TestSqliteError } from './database.js';
@@ -9,6 +11,7 @@ function readRawSql(schema) {
9
11
  return { label: `sql entry #${entry.index} (inline)`, sql: entry.sql };
10
12
  }
11
13
  try {
14
+ assertFixtureSqlFile(entry.resolvedPath);
12
15
  return {
13
16
  label: `sql entry #${entry.index} (${entry.sourcePath})`,
14
17
  sql: fs.readFileSync(entry.resolvedPath, 'utf8'),
@@ -66,10 +69,15 @@ function makeBuilder(state) {
66
69
  }
67
70
  }
68
71
  await insertRows((sql, params) => handle.db.query(sql, params), state.schema, effective);
69
- return handle;
72
+ return adoptFixtureResource(handle);
70
73
  }
71
74
  catch (err) {
72
- await handle.dispose();
75
+ try {
76
+ await handle.dispose();
77
+ }
78
+ catch (cleanup) {
79
+ throw new TestDatabaseBuildError('SQLite fixture build and cleanup failed', err, cleanup);
80
+ }
73
81
  throw err;
74
82
  }
75
83
  },
@@ -1 +1 @@
1
- {"version":3,"file":"builder.js","sourceRoot":"","sources":["../../../../src/testing/sqlite/builder.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport { generateDdl } from '../schema/ddl';\nimport { parseTestSchema } from '../schema/parser';\nimport type { NormalizedTestSchema, ParseTestSchemaOptions } from '../schema/types';\nimport { createDatabaseHandle, TestSqliteDatabase, TestSqliteError } from './database';\nimport { composeRows, DataOperation, insertRows, Row, toOperations } from './loadData';\n\nexport interface TestSqliteOptions extends ParseTestSchemaOptions {\n /**\n * Storage for the test database. Defaults to `'file'`: an owned temporary\n * file, removed on `dispose()`. Pass `'memory'` to opt back into an isolated\n * `:memory:` database — only worth it for a test that opens many databases\n * and never touches restart behavior.\n */\n mode?: 'memory' | 'file';\n /** Legacy alias for `mode`. `false` selects `:memory:`. */\n fileBacked?: boolean;\n /** Legacy alias for `mode`. `false` selects `:memory:`. */\n tempFile?: boolean;\n}\n\nexport interface TestSqliteBuilder {\n readonly schema: NormalizedTestSchema;\n readonly ops: readonly DataOperation[];\n data(rows: Record<string, Row[]>): TestSqliteBuilder;\n build(): Promise<TestSqliteDatabase>;\n}\n\ninterface BuilderState {\n readonly schema: NormalizedTestSchema;\n readonly ops: readonly DataOperation[];\n readonly options: Readonly<TestSqliteOptions>;\n}\n\nfunction readRawSql(schema: NormalizedTestSchema): { label: string; sql: string }[] {\n return schema.sql.map((entry) => {\n if (entry.kind === 'inline') {\n return { label: `sql entry #${entry.index} (inline)`, sql: entry.sql };\n }\n try {\n return {\n label: `sql entry #${entry.index} (${entry.sourcePath})`,\n sql: fs.readFileSync(entry.resolvedPath, 'utf8'),\n };\n } catch (err) {\n throw new TestSqliteError(\n `Cannot read raw SQL file for sql entry #${entry.index}: ${entry.resolvedPath} ` +\n `(authored as \"${entry.sourcePath}\" in ${schema.source}): ${(err as Error).message}`\n );\n }\n });\n}\n\nasync function executeRaw(handle: TestSqliteDatabase, sql: string): Promise<void> {\n const conn = handle.conn as { exec?: (sql: string) => unknown };\n if (typeof conn?.exec === 'function') {\n await conn.exec(sql);\n } else {\n await handle.db.query(sql, []);\n }\n}\n\nfunction makeBuilder(state: BuilderState): TestSqliteBuilder {\n return {\n schema: state.schema,\n ops: state.ops,\n data(rows: Record<string, Row[]>): TestSqliteBuilder {\n return makeBuilder({\n ...state,\n ops: [...state.ops, ...toOperations(state.schema, rows)],\n });\n },\n async build(): Promise<TestSqliteDatabase> {\n const rawSql = readRawSql(state.schema);\n const ddl = generateDdl(state.schema);\n const effective = composeRows(state.schema, state.ops);\n const fileBacked = state.options.mode\n ? state.options.mode === 'file'\n : state.options.fileBacked !== false && state.options.tempFile !== false;\n const handle = await createDatabaseHandle(fileBacked);\n try {\n for (const statement of ddl) {\n try {\n await handle.db.query(statement, []);\n } catch (err) {\n throw new TestSqliteError(\n `Failed to apply structured DDL from ${state.schema.source}: ${(err as Error).message}\\n` +\n `Statement:\\n${statement}`\n );\n }\n }\n for (const entry of rawSql) {\n try {\n await executeRaw(handle, entry.sql);\n } catch (err) {\n throw new TestSqliteError(\n `Failed to apply raw ${entry.label} from ${state.schema.source}: ${(err as Error).message}`\n );\n }\n }\n await insertRows((sql, params) => handle.db.query(sql, params), state.schema, effective);\n return handle;\n } catch (err) {\n await handle.dispose();\n throw err;\n }\n },\n };\n}\n\nexport function testSqlite(\n schema: string | object,\n options: TestSqliteOptions = {}\n): TestSqliteBuilder {\n const normalized = isNormalizedSchema(schema)\n ? schema\n : parseTestSchema(schema as string | object, options);\n if (normalized.dialect !== 'sqlite') {\n throw new TestSqliteError(\n `testSqlite requires a schema with dialect: sqlite (received: ${normalized.dialect})`\n );\n }\n return makeBuilder({\n schema: normalized,\n ops: [],\n options: Object.freeze({ ...options }),\n });\n}\n\nfunction isNormalizedSchema(input: unknown): input is NormalizedTestSchema {\n return (\n !!input &&\n typeof input === 'object' &&\n Array.isArray((input as NormalizedTestSchema).tables) &&\n Array.isArray((input as NormalizedTestSchema).sql) &&\n typeof (input as NormalizedTestSchema).source === 'string'\n );\n}\n"]}
1
+ {"version":3,"file":"builder.js","sourceRoot":"","sources":["../../../../src/testing/sqlite/builder.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport { adoptFixtureResource, TestDatabaseBuildError } from '../resource';\nimport { assertFixtureSqlFile } from '../fixture-provenance';\nimport { generateDdl } from '../schema/ddl';\nimport { parseTestSchema } from '../schema/parser';\nimport type { NormalizedTestSchema, ParseTestSchemaOptions } from '../schema/types';\nimport { createDatabaseHandle, TestSqliteDatabase, TestSqliteError } from './database';\nimport { composeRows, DataOperation, insertRows, Row, toOperations } from './loadData';\n\nexport interface TestSqliteOptions extends ParseTestSchemaOptions {\n /**\n * Storage for the test database. Defaults to `'file'`: an owned temporary\n * file, removed on `dispose()`. Pass `'memory'` to opt back into an isolated\n * `:memory:` database — only worth it for a test that opens many databases\n * and never touches restart behavior.\n */\n mode?: 'memory' | 'file';\n /** Legacy alias for `mode`. `false` selects `:memory:`. */\n fileBacked?: boolean;\n /** Legacy alias for `mode`. `false` selects `:memory:`. */\n tempFile?: boolean;\n}\n\nexport interface TestSqliteBuilder {\n readonly schema: NormalizedTestSchema;\n readonly ops: readonly DataOperation[];\n data(rows: Record<string, Row[]>): TestSqliteBuilder;\n build(): Promise<TestSqliteDatabase>;\n}\n\ninterface BuilderState {\n readonly schema: NormalizedTestSchema;\n readonly ops: readonly DataOperation[];\n readonly options: Readonly<TestSqliteOptions>;\n}\n\nfunction readRawSql(schema: NormalizedTestSchema): { label: string; sql: string }[] {\n return schema.sql.map((entry) => {\n if (entry.kind === 'inline') {\n return { label: `sql entry #${entry.index} (inline)`, sql: entry.sql };\n }\n try {\n assertFixtureSqlFile(entry.resolvedPath);\n return {\n label: `sql entry #${entry.index} (${entry.sourcePath})`,\n sql: fs.readFileSync(entry.resolvedPath, 'utf8'),\n };\n } catch (err) {\n throw new TestSqliteError(\n `Cannot read raw SQL file for sql entry #${entry.index}: ${entry.resolvedPath} ` +\n `(authored as \"${entry.sourcePath}\" in ${schema.source}): ${(err as Error).message}`\n );\n }\n });\n}\n\nasync function executeRaw(handle: TestSqliteDatabase, sql: string): Promise<void> {\n const conn = handle.conn as { exec?: (sql: string) => unknown };\n if (typeof conn?.exec === 'function') {\n await conn.exec(sql);\n } else {\n await handle.db.query(sql, []);\n }\n}\n\nfunction makeBuilder(state: BuilderState): TestSqliteBuilder {\n return {\n schema: state.schema,\n ops: state.ops,\n data(rows: Record<string, Row[]>): TestSqliteBuilder {\n return makeBuilder({\n ...state,\n ops: [...state.ops, ...toOperations(state.schema, rows)],\n });\n },\n async build(): Promise<TestSqliteDatabase> {\n const rawSql = readRawSql(state.schema);\n const ddl = generateDdl(state.schema);\n const effective = composeRows(state.schema, state.ops);\n const fileBacked = state.options.mode\n ? state.options.mode === 'file'\n : state.options.fileBacked !== false && state.options.tempFile !== false;\n const handle = await createDatabaseHandle(fileBacked);\n try {\n for (const statement of ddl) {\n try {\n await handle.db.query(statement, []);\n } catch (err) {\n throw new TestSqliteError(\n `Failed to apply structured DDL from ${state.schema.source}: ${(err as Error).message}\\n` +\n `Statement:\\n${statement}`\n );\n }\n }\n for (const entry of rawSql) {\n try {\n await executeRaw(handle, entry.sql);\n } catch (err) {\n throw new TestSqliteError(\n `Failed to apply raw ${entry.label} from ${state.schema.source}: ${(err as Error).message}`\n );\n }\n }\n await insertRows((sql, params) => handle.db.query(sql, params), state.schema, effective);\n return adoptFixtureResource(handle);\n } catch (err) {\n try { await handle.dispose(); }\n catch (cleanup) { throw new TestDatabaseBuildError('SQLite fixture build and cleanup failed', err, cleanup); }\n throw err;\n }\n },\n };\n}\n\nexport function testSqlite(\n schema: string | object,\n options: TestSqliteOptions = {}\n): TestSqliteBuilder {\n const normalized = isNormalizedSchema(schema)\n ? schema\n : parseTestSchema(schema as string | object, options);\n if (normalized.dialect !== 'sqlite') {\n throw new TestSqliteError(\n `testSqlite requires a schema with dialect: sqlite (received: ${normalized.dialect})`\n );\n }\n return makeBuilder({\n schema: normalized,\n ops: [],\n options: Object.freeze({ ...options }),\n });\n}\n\nfunction isNormalizedSchema(input: unknown): input is NormalizedTestSchema {\n return (\n !!input &&\n typeof input === 'object' &&\n Array.isArray((input as NormalizedTestSchema).tables) &&\n Array.isArray((input as NormalizedTestSchema).sql) &&\n typeof (input as NormalizedTestSchema).source === 'string'\n );\n}\n"]}
@@ -3,6 +3,7 @@ import * as os from 'node:os';
3
3
  import * as path from 'node:path';
4
4
  import { createSqliteDb } from '../../adapters/sqlite.js';
5
5
  import { SqlStackError } from '../../core/errors.js';
6
+ import { mintResourceId, registerResource, retireResource, runCleanupSteps, TestDatabaseDisposeError } from '../resource.js';
6
7
  export class TestSqliteError extends SqlStackError {
7
8
  constructor(message) {
8
9
  super(message);
@@ -32,6 +33,12 @@ function trackDirectory(directory) {
32
33
  }
33
34
  });
34
35
  }
36
+ /** Remove exactly the SQLStack-owned temp directory; never a caller path. */
37
+ function removeOwnedDirectory(directory) {
38
+ fs.rmSync(directory, { recursive: true, force: true });
39
+ // Keep failed paths in the owned inventory for the exit cleanup retry.
40
+ abandonedDirectories.delete(directory);
41
+ }
35
42
  export async function createDatabaseHandle(fileBacked) {
36
43
  let directory;
37
44
  let filename = ':memory:';
@@ -46,8 +53,12 @@ export async function createDatabaseHandle(fileBacked) {
46
53
  }
47
54
  catch (err) {
48
55
  if (directory) {
49
- fs.rmSync(directory, { recursive: true, force: true });
50
- abandonedDirectories.delete(directory);
56
+ try {
57
+ removeOwnedDirectory(directory);
58
+ }
59
+ catch (cleanup) {
60
+ throw Object.assign(new TestDatabaseDisposeError('SQLite test allocation failed and owned cleanup failed', [err, cleanup]), { cause: err });
61
+ }
51
62
  }
52
63
  const message = err.message;
53
64
  if (message.includes('No SQLite driver available')) {
@@ -55,31 +66,34 @@ export async function createDatabaseHandle(fileBacked) {
55
66
  }
56
67
  throw new TestSqliteError(`Failed to create SQLite test database: ${message}`);
57
68
  }
58
- let disposed = false;
59
- return {
69
+ const storage = fileBacked ? 'file' : 'memory';
70
+ const resourceId = mintResourceId('sqlite');
71
+ const ownedDirectory = directory;
72
+ let disposal;
73
+ const handle = {
74
+ engine: 'sqlite',
75
+ resourceId,
76
+ storage,
60
77
  db,
61
78
  conn: db.conn,
62
79
  path: fileBacked ? filename : undefined,
63
80
  async query(sql, params = []) {
64
- if (disposed)
81
+ if (disposal)
65
82
  throw new TestSqliteError('Test SQLite database has been disposed.');
66
83
  return db.query(sql, [...params]);
67
84
  },
68
- async dispose() {
69
- if (disposed)
70
- return;
71
- disposed = true;
72
- try {
73
- await db.close();
74
- }
75
- catch {
76
- /* connection may already be closed */
77
- }
78
- if (directory) {
79
- fs.rmSync(directory, { recursive: true, force: true });
80
- abandonedDirectories.delete(directory);
85
+ dispose() {
86
+ if (!disposal) {
87
+ retireResource(handle);
88
+ const steps = [() => db.close()];
89
+ if (ownedDirectory)
90
+ steps.push(() => removeOwnedDirectory(ownedDirectory));
91
+ disposal = Promise.resolve().then(() => runCleanupSteps(`Test SQLite database (${storage})`, steps));
81
92
  }
93
+ return disposal;
82
94
  },
83
95
  };
96
+ registerResource(handle, { engine: 'sqlite', resourceId, storage });
97
+ return handle;
84
98
  }
85
99
  //# sourceMappingURL=database.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"database.js","sourceRoot":"","sources":["../../../../src/testing/sqlite/database.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport { createSqliteDb } from '../../adapters/sqlite';\nimport { SqlStackError } from '../../core/errors';\nimport type { Database } from '../../registry';\n\nexport class TestSqliteError extends SqlStackError {\n constructor(message: string) {\n super(message);\n this.name = 'TestSqliteError';\n }\n}\n\nexport interface TestSqliteDatabase {\n /** The live sqlstack SQLite adapter, suitable for direct injection. */\n readonly db: Database;\n /** The underlying better-sqlite3 (or sqlite3) connection. */\n readonly conn: unknown;\n /** Absolute database path in file mode; undefined for :memory:. */\n readonly path?: string;\n query(sql: string, params?: readonly unknown[]): Promise<unknown>;\n /** Close the connection and remove owned temporary files. Idempotent. */\n dispose(): Promise<void>;\n}\n\n/**\n * Temp directories for file-backed handles that were never disposed. Tests are\n * expected to call `dispose()`, but a failed assertion can skip it; this keeps\n * an abandoned directory from outliving the worker process.\n */\nconst abandonedDirectories = new Set<string>();\nlet exitCleanupRegistered = false;\n\nfunction trackDirectory(directory: string): void {\n abandonedDirectories.add(directory);\n if (exitCleanupRegistered) return;\n exitCleanupRegistered = true;\n process.once('exit', () => {\n for (const dir of abandonedDirectories) {\n try {\n fs.rmSync(dir, { recursive: true, force: true });\n } catch {\n /* best-effort cleanup */\n }\n }\n });\n}\n\nexport async function createDatabaseHandle(\n fileBacked: boolean\n): Promise<TestSqliteDatabase> {\n let directory: string | undefined;\n let filename = ':memory:';\n if (fileBacked) {\n directory = fs.mkdtempSync(path.join(os.tmpdir(), 'sqlstack-test-'));\n filename = path.join(directory, 'database.sqlite');\n trackDirectory(directory);\n }\n\n let db: Database;\n try {\n db = await createSqliteDb(filename);\n } catch (err) {\n if (directory) {\n fs.rmSync(directory, { recursive: true, force: true });\n abandonedDirectories.delete(directory);\n }\n const message = (err as Error).message;\n if (message.includes('No SQLite driver available')) {\n throw new TestSqliteError('testSqlite requires the optional peer \"better-sqlite3\".');\n }\n throw new TestSqliteError(`Failed to create SQLite test database: ${message}`);\n }\n\n let disposed = false;\n return {\n db,\n conn: db.conn,\n path: fileBacked ? filename : undefined,\n async query(sql: string, params: readonly unknown[] = []) {\n if (disposed) throw new TestSqliteError('Test SQLite database has been disposed.');\n return db.query(sql, [...params]);\n },\n async dispose() {\n if (disposed) return;\n disposed = true;\n try {\n await db.close();\n } catch {\n /* connection may already be closed */\n }\n if (directory) {\n fs.rmSync(directory, { recursive: true, force: true });\n abandonedDirectories.delete(directory);\n }\n },\n };\n}\n"]}
1
+ {"version":3,"file":"database.js","sourceRoot":"","sources":["../../../../src/testing/sqlite/database.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport { createSqliteDb } from '../../adapters/sqlite';\nimport { SqlStackError } from '../../core/errors';\nimport type { Database } from '../../registry';\nimport { mintResourceId, registerResource, retireResource, runCleanupSteps, TestDatabaseDisposeError } from '../resource';\n\nexport class TestSqliteError extends SqlStackError {\n constructor(message: string) {\n super(message);\n this.name = 'TestSqliteError';\n }\n}\n\nexport interface TestSqliteDatabase {\n /** Engine discriminator for SQLStack-minted disposable test databases. */\n readonly engine: 'sqlite';\n /** Fresh per-allocation identifier; never reused within a process. */\n readonly resourceId: string;\n /** Explicit storage: an owned temporary file, or an isolated `:memory:` db. */\n readonly storage: 'memory' | 'file';\n /** The live sqlstack SQLite adapter, suitable for direct injection. */\n readonly db: Database;\n /** The underlying better-sqlite3 (or sqlite3) connection. */\n readonly conn: unknown;\n /** Absolute database path in file mode; undefined for :memory:. */\n readonly path?: string;\n query(sql: string, params?: readonly unknown[]): Promise<unknown>;\n /**\n * Close the connection and remove owned temporary files. Idempotent:\n * concurrent and repeated calls share one outcome. Every cleanup step is\n * attempted; failures are aggregated into a `TestDatabaseDisposeError`.\n */\n dispose(): Promise<void>;\n}\n\n/**\n * Temp directories for file-backed handles that were never disposed. Tests are\n * expected to call `dispose()`, but a failed assertion can skip it; this keeps\n * an abandoned directory from outliving the worker process.\n */\nconst abandonedDirectories = new Set<string>();\nlet exitCleanupRegistered = false;\n\nfunction trackDirectory(directory: string): void {\n abandonedDirectories.add(directory);\n if (exitCleanupRegistered) return;\n exitCleanupRegistered = true;\n process.once('exit', () => {\n for (const dir of abandonedDirectories) {\n try {\n fs.rmSync(dir, { recursive: true, force: true });\n } catch {\n /* best-effort cleanup */\n }\n }\n });\n}\n\n/** Remove exactly the SQLStack-owned temp directory; never a caller path. */\nfunction removeOwnedDirectory(directory: string): void {\n fs.rmSync(directory, { recursive: true, force: true });\n // Keep failed paths in the owned inventory for the exit cleanup retry.\n abandonedDirectories.delete(directory);\n}\n\nexport async function createDatabaseHandle(\n fileBacked: boolean\n): Promise<TestSqliteDatabase> {\n let directory: string | undefined;\n let filename = ':memory:';\n if (fileBacked) {\n directory = fs.mkdtempSync(path.join(os.tmpdir(), 'sqlstack-test-'));\n filename = path.join(directory, 'database.sqlite');\n trackDirectory(directory);\n }\n\n let db: Database;\n try {\n db = await createSqliteDb(filename);\n } catch (err) {\n if (directory) {\n try { removeOwnedDirectory(directory); }\n catch (cleanup) {\n throw Object.assign(new TestDatabaseDisposeError('SQLite test allocation failed and owned cleanup failed', [err, cleanup]), { cause: err });\n }\n }\n const message = (err as Error).message;\n if (message.includes('No SQLite driver available')) {\n throw new TestSqliteError('testSqlite requires the optional peer \"better-sqlite3\".');\n }\n throw new TestSqliteError(`Failed to create SQLite test database: ${message}`);\n }\n\n const storage: 'memory' | 'file' = fileBacked ? 'file' : 'memory';\n const resourceId = mintResourceId('sqlite');\n const ownedDirectory = directory;\n let disposal: Promise<void> | undefined;\n\n const handle: TestSqliteDatabase = {\n engine: 'sqlite',\n resourceId,\n storage,\n db,\n conn: db.conn,\n path: fileBacked ? filename : undefined,\n async query(sql: string, params: readonly unknown[] = []) {\n if (disposal) throw new TestSqliteError('Test SQLite database has been disposed.');\n return db.query(sql, [...params]);\n },\n dispose() {\n if (!disposal) {\n retireResource(handle);\n const steps: (() => Promise<void> | void)[] = [() => db.close()];\n if (ownedDirectory) steps.push(() => removeOwnedDirectory(ownedDirectory));\n disposal = Promise.resolve().then(() => runCleanupSteps(`Test SQLite database (${storage})`, steps));\n }\n return disposal;\n },\n };\n registerResource(handle, { engine: 'sqlite', resourceId, storage });\n return handle;\n}\n"]}
@@ -230,6 +230,14 @@ export function currentTransaction(db) {
230
230
  return undefined;
231
231
  return new TransactionHandle(entry, state, false);
232
232
  }
233
+ /**
234
+ * Whether this async call carries any transaction records. This observation
235
+ * never resolves a database, opens a connection, or consults legacy defaults.
236
+ * It does not choose an owner: use currentTransactionFor(entry) for that.
237
+ */
238
+ export function hasTransactionContext() {
239
+ return (getStore()?.txByDb.size ?? 0) > 0;
240
+ }
233
241
  /** Active transaction handle for a concrete database entry, if any. */
234
242
  export function currentTransactionFor(entry) {
235
243
  const store = getStore();
@@ -1 +1 @@
1
- {"version":3,"file":"transactions.js","sourceRoot":"","sources":["../../src/transactions.ts"],"names":[],"mappings":"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","sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport type { Database, QueryResult } from './registry';\nimport { createPgDb, createMysqlDb } from './adapters';\nimport { RollbackTransactionError, SqlStackError } from './core/errors';\nimport type { DatabaseEntry } from './stack';\nimport { resolveExecution, legacyEntry } from './runtime';\n\nexport type TransactionOptions = {\n db?: string;\n lazy?: boolean;\n nested?: 'join';\n};\n\n// Keyed by concrete DatabaseEntry identity (owning stack + registration), so\n// two roots transacting on the same name (\"primary\") can never collide\n// (spec 16 §20, spec 17 §14).\ntype TxStore = {\n txByDb: Map<DatabaseEntry, TxState>;\n};\n\nconst als = new AsyncLocalStorage<TxStore>();\n\nfunction getStore(): TxStore | undefined {\n return als.getStore();\n}\n\nfunction getOrCreateStore(): { store: TxStore; isNew: boolean } {\n const existing = getStore();\n if (existing) return { store: existing, isNew: false };\n return { store: { txByDb: new Map() }, isNew: true };\n}\n\nfunction isRollbackTransactionError(err: unknown): err is RollbackTransactionError {\n return err instanceof RollbackTransactionError;\n}\n\nfunction unwrapRollbackCause(err: RollbackTransactionError): unknown {\n return typeof err.cause === 'undefined' ? err : err.cause;\n}\n\nexport class TransactionHandle {\n readonly entry: DatabaseEntry;\n #state: TxState;\n #owned: boolean;\n #finished = false;\n\n constructor(entry: DatabaseEntry, state: TxState, owned: boolean) {\n this.entry = entry;\n this.#state = state;\n this.#owned = owned;\n }\n\n get dbName(): string {\n return this.entry.name;\n }\n\n async ensureStarted() {\n await this.#state.ensureStarted();\n }\n\n rollbackOnly(errorToThrow?: unknown) {\n this.#state.rollbackOnly = true;\n if (typeof errorToThrow !== 'undefined') {\n this.#state.rollbackError = errorToThrow;\n }\n }\n\n async commit() {\n if (!this.#owned) return;\n await this.#state.commitIfStarted();\n }\n\n async rollback() {\n if (!this.#owned) return;\n await this.#state.rollbackIfStarted();\n }\n\n async release() {\n if (!this.#owned) return;\n await this.#state.release();\n }\n\n async finishOnSuccess() {\n if (!this.#owned || this.#finished) return;\n this.#finished = true;\n await this.#state.finishOnSuccess();\n }\n\n async finishOnError(err: unknown) {\n if (!this.#owned || this.#finished) return;\n this.#finished = true;\n await this.#state.finishOnError(err);\n }\n}\n\nclass TxState {\n readonly entry: DatabaseEntry;\n readonly db: Database;\n readonly options: Required<Pick<TransactionOptions, 'lazy'>> & Pick<TransactionOptions, 'nested'>;\n depth = 1;\n started = false;\n completed = false;\n rollbackOnly = false;\n rollbackError: unknown | undefined;\n\n private txDb: Database | undefined;\n private releaseFn: (() => Promise<void>) | undefined;\n\n constructor(entry: DatabaseEntry, options: TransactionOptions | undefined) {\n this.entry = entry;\n this.db = entry.db;\n this.options = {\n lazy: options?.lazy ?? true,\n nested: options?.nested ?? 'join',\n };\n }\n\n get activeDb(): Database {\n return this.txDb ?? this.db;\n }\n\n async ensureStarted() {\n if (this.started) return;\n\n const dialect = this.db.dialect ?? 'sqlite';\n if (dialect === 'postgres') {\n const pool: any = this.db.conn;\n if (!pool || typeof pool.connect !== 'function') {\n throw new SqlStackError('Postgres transactions require a pg Pool with .connect() as Database.conn');\n }\n const client = await pool.connect();\n this.txDb = createPgDb(client);\n this.releaseFn = async () => {\n if (typeof client.release === 'function') client.release();\n };\n await this.txDb.query('BEGIN', []);\n this.started = true;\n return;\n }\n\n if (dialect === 'mysql') {\n const pool: any = this.db.conn;\n if (!pool || typeof pool.getConnection !== 'function') {\n throw new SqlStackError('MySQL transactions require a mysql2 Pool with .getConnection() as Database.conn');\n }\n const conn = await pool.getConnection();\n this.txDb = createMysqlDb(conn);\n this.releaseFn = async () => {\n if (typeof conn.release === 'function') conn.release();\n };\n await this.txDb.query('START TRANSACTION', []);\n this.started = true;\n return;\n }\n\n // sqlite: existing connection is already bound\n await this.db.query('BEGIN', []);\n this.started = true;\n }\n\n async query(sql: string, params: unknown[]): Promise<QueryResult> {\n await this.ensureStarted();\n return await this.activeDb.query(sql, params);\n }\n\n async commitIfStarted() {\n if (this.completed) return;\n if (!this.started) {\n this.completed = true;\n return;\n }\n await this.activeDb.query('COMMIT', []);\n this.completed = true;\n }\n\n async rollbackIfStarted() {\n if (this.completed) return;\n if (!this.started) {\n this.completed = true;\n return;\n }\n await this.activeDb.query('ROLLBACK', []);\n this.completed = true;\n }\n\n async release() {\n try {\n if (this.releaseFn) await this.releaseFn();\n } finally {\n this.txDb = undefined;\n this.releaseFn = undefined;\n }\n }\n\n async finishOnSuccess() {\n try {\n if (this.rollbackOnly) {\n await this.rollbackIfStarted();\n if (typeof this.rollbackError !== 'undefined') throw this.rollbackError;\n throw new RollbackTransactionError();\n }\n await this.commitIfStarted();\n } finally {\n await this.release();\n }\n }\n\n async finishOnError(err: unknown) {\n try {\n await this.rollbackIfStarted();\n } finally {\n await this.release();\n }\n if (isRollbackTransactionError(err)) {\n throw unwrapRollbackCause(err);\n }\n throw err;\n }\n}\n\nfunction enterTx(entry: DatabaseEntry, options?: TransactionOptions): { handle: TransactionHandle; store: TxStore; isNewStore: boolean } {\n const { store, isNew } = getOrCreateStore();\n const existing = store.txByDb.get(entry);\n if (existing) {\n existing.depth++;\n return { handle: new TransactionHandle(entry, existing, false), store, isNewStore: isNew };\n }\n const state = new TxState(entry, options);\n store.txByDb.set(entry, state);\n return { handle: new TransactionHandle(entry, state, true), store, isNewStore: isNew };\n}\n\nasync function exitTx(entry: DatabaseEntry, store: TxStore) {\n const state = store.txByDb.get(entry);\n if (!state) return;\n state.depth = Math.max(0, state.depth - 1);\n if (state.depth === 0) {\n store.txByDb.delete(entry);\n }\n}\n\n/**\n * @deprecated Synchronous lookup against the legacy global registry only.\n * Under an active IoC root, use `currentTransactionFor(entry)` with an entry\n * from the root's SqlStack (`stack.getEntry(name)`).\n */\nexport function currentTransaction(db?: string): TransactionHandle | undefined {\n const store = getStore();\n if (!store) return undefined;\n const entry = legacyEntry(db);\n const state = store.txByDb.get(entry);\n if (!state) return undefined;\n return new TransactionHandle(entry, state, false);\n}\n\n/** Active transaction handle for a concrete database entry, if any. */\nexport function currentTransactionFor(entry: DatabaseEntry): TransactionHandle | undefined {\n const store = getStore();\n const state = store?.txByDb.get(entry);\n if (!state) return undefined;\n return new TransactionHandle(entry, state, false);\n}\n\nexport async function withTransaction<T>(fn: () => Promise<T>, options?: TransactionOptions): Promise<T> {\n const { entry } = await resolveExecution(options?.db);\n const { handle, store, isNewStore } = enterTx(entry, options);\n\n const run = async () => {\n try {\n const out = await fn();\n await handle.finishOnSuccess();\n return out;\n } catch (err) {\n await handle.finishOnError(err);\n throw err;\n } finally {\n await exitTx(entry, store);\n }\n };\n\n return isNewStore ? await als.run(store, run) : await run();\n}\n\nexport type TransactionDecorator = {\n (target: any, prop: string | symbol, desc: PropertyDescriptor): void;\n (options?: TransactionOptions): (target: any, prop: string | symbol, desc: PropertyDescriptor) => void;\n};\n\nexport const transaction: TransactionDecorator = function (...args: any[]) {\n // @transaction\n if (args.length === 3 && typeof args[2]?.value === 'function') {\n const [target, prop, desc] = args as [any, string | symbol, PropertyDescriptor];\n wrapMethod(target, prop, desc, undefined);\n return;\n }\n // @transaction() or @transaction({ ... })\n const options: TransactionOptions | undefined = args.length > 0 ? args[0] : undefined;\n return function (target: any, prop: string | symbol, desc: PropertyDescriptor) {\n wrapMethod(target, prop, desc, options);\n };\n} as any;\n\nfunction wrapMethod(_target: any, _prop: string | symbol, desc: PropertyDescriptor, options?: TransactionOptions) {\n const original = desc.value;\n desc.value = async function (...callArgs: any[]) {\n return await withTransaction(() => original.apply(this, callArgs), options);\n };\n}\n\nexport type ActiveTransaction = {\n query(sql: string, params: unknown[]): Promise<QueryResult>;\n};\n\nexport function getActiveTransaction(entry: DatabaseEntry): ActiveTransaction | undefined {\n const store = getStore();\n return store?.txByDb.get(entry);\n}\n"]}
1
+ {"version":3,"file":"transactions.js","sourceRoot":"","sources":["../../src/transactions.ts"],"names":[],"mappings":"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","sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport type { Database, QueryResult } from './registry';\nimport { createPgDb, createMysqlDb } from './adapters';\nimport { RollbackTransactionError, SqlStackError } from './core/errors';\nimport type { DatabaseEntry } from './stack';\nimport { resolveExecution, legacyEntry } from './runtime';\n\nexport type TransactionOptions = {\n db?: string;\n lazy?: boolean;\n nested?: 'join';\n};\n\n// Keyed by concrete DatabaseEntry identity (owning stack + registration), so\n// two roots transacting on the same name (\"primary\") can never collide\n// (spec 16 §20, spec 17 §14).\ntype TxStore = {\n txByDb: Map<DatabaseEntry, TxState>;\n};\n\nconst als = new AsyncLocalStorage<TxStore>();\n\nfunction getStore(): TxStore | undefined {\n return als.getStore();\n}\n\nfunction getOrCreateStore(): { store: TxStore; isNew: boolean } {\n const existing = getStore();\n if (existing) return { store: existing, isNew: false };\n return { store: { txByDb: new Map() }, isNew: true };\n}\n\nfunction isRollbackTransactionError(err: unknown): err is RollbackTransactionError {\n return err instanceof RollbackTransactionError;\n}\n\nfunction unwrapRollbackCause(err: RollbackTransactionError): unknown {\n return typeof err.cause === 'undefined' ? err : err.cause;\n}\n\nexport class TransactionHandle {\n readonly entry: DatabaseEntry;\n #state: TxState;\n #owned: boolean;\n #finished = false;\n\n constructor(entry: DatabaseEntry, state: TxState, owned: boolean) {\n this.entry = entry;\n this.#state = state;\n this.#owned = owned;\n }\n\n get dbName(): string {\n return this.entry.name;\n }\n\n async ensureStarted() {\n await this.#state.ensureStarted();\n }\n\n rollbackOnly(errorToThrow?: unknown) {\n this.#state.rollbackOnly = true;\n if (typeof errorToThrow !== 'undefined') {\n this.#state.rollbackError = errorToThrow;\n }\n }\n\n async commit() {\n if (!this.#owned) return;\n await this.#state.commitIfStarted();\n }\n\n async rollback() {\n if (!this.#owned) return;\n await this.#state.rollbackIfStarted();\n }\n\n async release() {\n if (!this.#owned) return;\n await this.#state.release();\n }\n\n async finishOnSuccess() {\n if (!this.#owned || this.#finished) return;\n this.#finished = true;\n await this.#state.finishOnSuccess();\n }\n\n async finishOnError(err: unknown) {\n if (!this.#owned || this.#finished) return;\n this.#finished = true;\n await this.#state.finishOnError(err);\n }\n}\n\nclass TxState {\n readonly entry: DatabaseEntry;\n readonly db: Database;\n readonly options: Required<Pick<TransactionOptions, 'lazy'>> & Pick<TransactionOptions, 'nested'>;\n depth = 1;\n started = false;\n completed = false;\n rollbackOnly = false;\n rollbackError: unknown | undefined;\n\n private txDb: Database | undefined;\n private releaseFn: (() => Promise<void>) | undefined;\n\n constructor(entry: DatabaseEntry, options: TransactionOptions | undefined) {\n this.entry = entry;\n this.db = entry.db;\n this.options = {\n lazy: options?.lazy ?? true,\n nested: options?.nested ?? 'join',\n };\n }\n\n get activeDb(): Database {\n return this.txDb ?? this.db;\n }\n\n async ensureStarted() {\n if (this.started) return;\n\n const dialect = this.db.dialect ?? 'sqlite';\n if (dialect === 'postgres') {\n const pool: any = this.db.conn;\n if (!pool || typeof pool.connect !== 'function') {\n throw new SqlStackError('Postgres transactions require a pg Pool with .connect() as Database.conn');\n }\n const client = await pool.connect();\n this.txDb = createPgDb(client);\n this.releaseFn = async () => {\n if (typeof client.release === 'function') client.release();\n };\n await this.txDb.query('BEGIN', []);\n this.started = true;\n return;\n }\n\n if (dialect === 'mysql') {\n const pool: any = this.db.conn;\n if (!pool || typeof pool.getConnection !== 'function') {\n throw new SqlStackError('MySQL transactions require a mysql2 Pool with .getConnection() as Database.conn');\n }\n const conn = await pool.getConnection();\n this.txDb = createMysqlDb(conn);\n this.releaseFn = async () => {\n if (typeof conn.release === 'function') conn.release();\n };\n await this.txDb.query('START TRANSACTION', []);\n this.started = true;\n return;\n }\n\n // sqlite: existing connection is already bound\n await this.db.query('BEGIN', []);\n this.started = true;\n }\n\n async query(sql: string, params: unknown[]): Promise<QueryResult> {\n await this.ensureStarted();\n return await this.activeDb.query(sql, params);\n }\n\n async commitIfStarted() {\n if (this.completed) return;\n if (!this.started) {\n this.completed = true;\n return;\n }\n await this.activeDb.query('COMMIT', []);\n this.completed = true;\n }\n\n async rollbackIfStarted() {\n if (this.completed) return;\n if (!this.started) {\n this.completed = true;\n return;\n }\n await this.activeDb.query('ROLLBACK', []);\n this.completed = true;\n }\n\n async release() {\n try {\n if (this.releaseFn) await this.releaseFn();\n } finally {\n this.txDb = undefined;\n this.releaseFn = undefined;\n }\n }\n\n async finishOnSuccess() {\n try {\n if (this.rollbackOnly) {\n await this.rollbackIfStarted();\n if (typeof this.rollbackError !== 'undefined') throw this.rollbackError;\n throw new RollbackTransactionError();\n }\n await this.commitIfStarted();\n } finally {\n await this.release();\n }\n }\n\n async finishOnError(err: unknown) {\n try {\n await this.rollbackIfStarted();\n } finally {\n await this.release();\n }\n if (isRollbackTransactionError(err)) {\n throw unwrapRollbackCause(err);\n }\n throw err;\n }\n}\n\nfunction enterTx(entry: DatabaseEntry, options?: TransactionOptions): { handle: TransactionHandle; store: TxStore; isNewStore: boolean } {\n const { store, isNew } = getOrCreateStore();\n const existing = store.txByDb.get(entry);\n if (existing) {\n existing.depth++;\n return { handle: new TransactionHandle(entry, existing, false), store, isNewStore: isNew };\n }\n const state = new TxState(entry, options);\n store.txByDb.set(entry, state);\n return { handle: new TransactionHandle(entry, state, true), store, isNewStore: isNew };\n}\n\nasync function exitTx(entry: DatabaseEntry, store: TxStore) {\n const state = store.txByDb.get(entry);\n if (!state) return;\n state.depth = Math.max(0, state.depth - 1);\n if (state.depth === 0) {\n store.txByDb.delete(entry);\n }\n}\n\n/**\n * @deprecated Synchronous lookup against the legacy global registry only.\n * Under an active IoC root, use `currentTransactionFor(entry)` with an entry\n * from the root's SqlStack (`stack.getEntry(name)`).\n */\nexport function currentTransaction(db?: string): TransactionHandle | undefined {\n const store = getStore();\n if (!store) return undefined;\n const entry = legacyEntry(db);\n const state = store.txByDb.get(entry);\n if (!state) return undefined;\n return new TransactionHandle(entry, state, false);\n}\n\n/**\n * Whether this async call carries any transaction records. This observation\n * never resolves a database, opens a connection, or consults legacy defaults.\n * It does not choose an owner: use currentTransactionFor(entry) for that.\n */\nexport function hasTransactionContext(): boolean {\n return (getStore()?.txByDb.size ?? 0) > 0;\n}\n\n/** Active transaction handle for a concrete database entry, if any. */\nexport function currentTransactionFor(entry: DatabaseEntry): TransactionHandle | undefined {\n const store = getStore();\n const state = store?.txByDb.get(entry);\n if (!state) return undefined;\n return new TransactionHandle(entry, state, false);\n}\n\nexport async function withTransaction<T>(fn: () => Promise<T>, options?: TransactionOptions): Promise<T> {\n const { entry } = await resolveExecution(options?.db);\n const { handle, store, isNewStore } = enterTx(entry, options);\n\n const run = async () => {\n try {\n const out = await fn();\n await handle.finishOnSuccess();\n return out;\n } catch (err) {\n await handle.finishOnError(err);\n throw err;\n } finally {\n await exitTx(entry, store);\n }\n };\n\n return isNewStore ? await als.run(store, run) : await run();\n}\n\nexport type TransactionDecorator = {\n (target: any, prop: string | symbol, desc: PropertyDescriptor): void;\n (options?: TransactionOptions): (target: any, prop: string | symbol, desc: PropertyDescriptor) => void;\n};\n\nexport const transaction: TransactionDecorator = function (...args: any[]) {\n // @transaction\n if (args.length === 3 && typeof args[2]?.value === 'function') {\n const [target, prop, desc] = args as [any, string | symbol, PropertyDescriptor];\n wrapMethod(target, prop, desc, undefined);\n return;\n }\n // @transaction() or @transaction({ ... })\n const options: TransactionOptions | undefined = args.length > 0 ? args[0] : undefined;\n return function (target: any, prop: string | symbol, desc: PropertyDescriptor) {\n wrapMethod(target, prop, desc, options);\n };\n} as any;\n\nfunction wrapMethod(_target: any, _prop: string | symbol, desc: PropertyDescriptor, options?: TransactionOptions) {\n const original = desc.value;\n desc.value = async function (...callArgs: any[]) {\n return await withTransaction(() => original.apply(this, callArgs), options);\n };\n}\n\nexport type ActiveTransaction = {\n query(sql: string, params: unknown[]): Promise<QueryResult>;\n};\n\nexport function getActiveTransaction(entry: DatabaseEntry): ActiveTransaction | undefined {\n const store = getStore();\n return store?.txByDb.get(entry);\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlstack",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "author": "",