@ductape/sdk 0.4.4 → 0.4.6

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 (48) hide show
  1. package/dist/context/execution-context.d.ts +6 -0
  2. package/dist/context/execution-context.js +1 -1
  3. package/dist/context/execution-context.js.map +1 -1
  4. package/dist/database/adapters/dynamodb.adapter.d.ts +2 -0
  5. package/dist/database/adapters/dynamodb.adapter.js +18 -1
  6. package/dist/database/adapters/dynamodb.adapter.js.map +1 -1
  7. package/dist/database/adapters/mongodb.adapter.d.ts +4 -0
  8. package/dist/database/adapters/mongodb.adapter.js +58 -14
  9. package/dist/database/adapters/mongodb.adapter.js.map +1 -1
  10. package/dist/database/adapters/mysql.adapter.d.ts +2 -0
  11. package/dist/database/adapters/mysql.adapter.js +90 -15
  12. package/dist/database/adapters/mysql.adapter.js.map +1 -1
  13. package/dist/database/adapters/postgresql.adapter.d.ts +2 -0
  14. package/dist/database/adapters/postgresql.adapter.js +24 -4
  15. package/dist/database/adapters/postgresql.adapter.js.map +1 -1
  16. package/dist/database/atomic-batch.d.ts +58 -0
  17. package/dist/database/atomic-batch.js +183 -0
  18. package/dist/database/atomic-batch.js.map +1 -0
  19. package/dist/database/atomic-pipeline.d.ts +27 -0
  20. package/dist/database/atomic-pipeline.js +118 -0
  21. package/dist/database/atomic-pipeline.js.map +1 -0
  22. package/dist/database/databases.service.d.ts +9 -0
  23. package/dist/database/databases.service.js +234 -3
  24. package/dist/database/databases.service.js.map +1 -1
  25. package/dist/database/dynamodb-atomic-batch.d.ts +8 -0
  26. package/dist/database/dynamodb-atomic-batch.js +153 -0
  27. package/dist/database/dynamodb-atomic-batch.js.map +1 -0
  28. package/dist/database/index.d.ts +2 -0
  29. package/dist/database/index.js +4 -2
  30. package/dist/database/index.js.map +1 -1
  31. package/dist/database/sql-context.d.ts +17 -0
  32. package/dist/database/sql-context.js +51 -0
  33. package/dist/database/sql-context.js.map +1 -0
  34. package/dist/database/transactions/transaction-manager.d.ts +5 -1
  35. package/dist/database/transactions/transaction-manager.js +13 -2
  36. package/dist/database/transactions/transaction-manager.js.map +1 -1
  37. package/dist/database/types/index.d.ts +1 -0
  38. package/dist/database/types/pipeline.interface.d.ts +19 -0
  39. package/dist/database/types/pipeline.interface.js +22 -0
  40. package/dist/database/types/pipeline.interface.js.map +1 -0
  41. package/dist/index.d.ts +9 -1
  42. package/dist/index.js +17 -2
  43. package/dist/index.js.map +1 -1
  44. package/dist/logs/logs.service.js +10 -0
  45. package/dist/logs/logs.service.js.map +1 -1
  46. package/dist/processor/services/processor.service.d.ts +2 -2
  47. package/dist/tsconfig.tsbuildinfo +1 -1
  48. package/package.json +1 -1
@@ -41,17 +41,38 @@ var __importStar = (this && this.__importStar) || (function () {
41
41
  return result;
42
42
  };
43
43
  })();
44
+ var __rest = (this && this.__rest) || function (s, e) {
45
+ var t = {};
46
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
47
+ t[p] = s[p];
48
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
49
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
50
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
51
+ t[p[i]] = s[p[i]];
52
+ }
53
+ return t;
54
+ };
44
55
  var __importDefault = (this && this.__importDefault) || function (mod) {
45
56
  return (mod && mod.__esModule) ? mod : { "default": mod };
46
57
  };
47
58
  Object.defineProperty(exports, "__esModule", { value: true });
48
59
  exports.databaseService = exports.DatabaseConnection = exports.DatabaseService = void 0;
49
60
  const enums_1 = require("./types/enums");
61
+ const pipeline_interface_1 = require("./types/pipeline.interface");
62
+ const atomic_pipeline_1 = require("./atomic-pipeline");
63
+ const sql_context_1 = require("./sql-context");
50
64
  const presave_processor_1 = require("./presave/presave-processor");
65
+ const atomic_batch_1 = require("./atomic-batch");
66
+ const dynamodb_adapter_1 = require("./adapters/dynamodb.adapter");
67
+ const transaction_manager_1 = require("./transactions/transaction-manager");
68
+ const mongodb_adapter_1 = require("./adapters/mongodb.adapter");
69
+ const postgresql_adapter_1 = require("./adapters/postgresql.adapter");
70
+ const mysql_adapter_1 = require("./adapters/mysql.adapter");
71
+ const context_1 = require("../context");
51
72
  const adapter_factory_1 = require("./adapters/adapter.factory");
52
73
  const query_builder_1 = require("./operators/query-builder");
53
74
  const aggregation_builder_1 = require("./operators/aggregation-builder");
54
- const transaction_manager_1 = require("./transactions/transaction-manager");
75
+ const transaction_manager_2 = require("./transactions/transaction-manager");
55
76
  const schema_manager_1 = require("./schema/schema-manager");
56
77
  const migration_engine_1 = require("./migrations/migration-engine");
57
78
  const action_manager_1 = require("./actions/action-manager");
@@ -94,6 +115,7 @@ class DatabaseService {
94
115
  this.adapters = new Map();
95
116
  this.connectionContexts = new Map();
96
117
  this.currentContext = null;
118
+ this.transactionScopes = new WeakMap();
97
119
  this.schemaManagers = new Map();
98
120
  this.migrationEngines = new Map();
99
121
  /** Service configuration */
@@ -116,7 +138,7 @@ class DatabaseService {
116
138
  this.config = config || null;
117
139
  this.preInitializedProductBuilder = config === null || config === void 0 ? void 0 : config.preInitializedProductBuilder;
118
140
  this.adapterFactory = new adapter_factory_1.AdapterFactory();
119
- this.transactionManager = new transaction_manager_1.TransactionManager();
141
+ this.transactionManager = new transaction_manager_2.TransactionManager();
120
142
  this.actionManager = new action_manager_1.ActionManager(this);
121
143
  this._privateKey = (config === null || config === void 0 ? void 0 : config.private_key) || '';
122
144
  if ((config === null || config === void 0 ? void 0 : config.default_product) || (config === null || config === void 0 ? void 0 : config.default_env)) {
@@ -1356,6 +1378,9 @@ class DatabaseService {
1356
1378
  */
1357
1379
  async raw(options) {
1358
1380
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
1381
+ // A cached response cannot represent this transaction's snapshot or own writes.
1382
+ if (options.transaction && options.cache)
1383
+ options = Object.assign(Object.assign({}, options), { cache: undefined });
1359
1384
  const process_id = (0, processor_utils_1.generateObjectId)();
1360
1385
  const start = Date.now();
1361
1386
  this.initializeLogService();
@@ -1860,6 +1885,104 @@ class DatabaseService {
1860
1885
  * where: { status: 'completed' },
1861
1886
  * });
1862
1887
  */
1888
+ async atomicPipeline(options) {
1889
+ const { context: _context } = options, batchInput = __rest(options, ["context"]);
1890
+ (0, atomic_batch_1.validateAtomicBatch)(batchInput);
1891
+ // Retain only the opaque session and transaction by identity across awaits.
1892
+ options = Object.assign(Object.assign({}, options), { context: JSON.parse(JSON.stringify(options.context)), operations: JSON.parse(JSON.stringify(options.operations)) });
1893
+ return (0, atomic_pipeline_1.executeAtomicPipeline)(options, {
1894
+ preflight: async (scope) => {
1895
+ try {
1896
+ await this.ensureAdapterFor(scope);
1897
+ const adapter = this.getAdapter(scope);
1898
+ if (!(adapter instanceof mongodb_adapter_1.MongoDBAdapter || adapter instanceof postgresql_adapter_1.PostgreSQLAdapter || adapter instanceof mysql_adapter_1.MySQLAdapter))
1899
+ throw new Error('Adapter cannot provide a transactional context pipeline');
1900
+ if ((adapter instanceof mongodb_adapter_1.MongoDBAdapter) === !!scope.context.sql)
1901
+ throw new Error('Context dialect does not match adapter');
1902
+ if (scope.transaction && (!(scope.transaction instanceof transaction_manager_1.Transaction) || !scope.transaction.belongsTo(adapter)
1903
+ || !scope.transaction.isActive() || this.transactionScopes.get(scope.transaction) !== JSON.stringify([scope.product, scope.env, scope.database])))
1904
+ throw new Error('Transaction scope mismatch');
1905
+ if (adapter instanceof mongodb_adapter_1.MongoDBAdapter)
1906
+ await adapter.assertAtomicBatchSupport([...new Set([scope.context.table, ...scope.operations.map(op => op.table)])]);
1907
+ else if (scope.transaction && scope.transaction.isolationLevel !== enums_1.IsolationLevel.SERIALIZABLE)
1908
+ throw new Error('SQL pipeline requires SERIALIZABLE isolation');
1909
+ }
1910
+ catch (_a) {
1911
+ throw new atomic_batch_1.AtomicBatchError('BATCH_PREFLIGHT_FAILED', 'not_started');
1912
+ }
1913
+ },
1914
+ begin: () => this.beginTransaction({ product: options.product, env: options.env, database: options.database, isolationLevel: enums_1.IsolationLevel.SERIALIZABLE }),
1915
+ read: async (transaction) => {
1916
+ if (!options.context.sql)
1917
+ return this.aggregatePipeline({ product: options.product, env: options.env, database: options.database,
1918
+ session: options.session, transaction, table: options.context.table, pipeline: options.context.pipeline, maxTimeMS: options.context.maxTimeMS });
1919
+ const adapter = this.getAdapter(options);
1920
+ await adapter.assertAtomicBatchSupport([...new Set([options.context.table, ...(options.context.sql.joins || []).map(join => join.table), ...options.operations.map(op => op.table)])], transaction);
1921
+ let context = options.context.sql;
1922
+ if (options.session) {
1923
+ const builder = await this.getProductBuilder(options.product);
1924
+ const key = builder.fetchPrivateKey();
1925
+ if (!key)
1926
+ throw new Error('Cannot verify pipeline session');
1927
+ const { processSessionForExecution } = await Promise.resolve().then(() => __importStar(require('../sessions')));
1928
+ const result = await processSessionForExecution(options.session, key, context, options.env, { fetchSessionSelector: async (tag) => { var _a; return (_a = (await builder.fetchSession(tag))) === null || _a === void 0 ? void 0 : _a.selector; } });
1929
+ if (result.error)
1930
+ throw new Error('Pipeline session validation failed');
1931
+ context = result.input;
1932
+ }
1933
+ const query = (0, sql_context_1.compileSqlContext)(options.context.table, context, adapter instanceof postgresql_adapter_1.PostgreSQLAdapter);
1934
+ return adapter.raw(Object.assign(Object.assign({}, query), { table: options.context.table, transaction }));
1935
+ },
1936
+ write: batch => this.atomicBatch(batch),
1937
+ });
1938
+ }
1939
+ async aggregatePipeline(options) {
1940
+ var _a, _b, _c;
1941
+ for (const key of ['product', 'env', 'database', 'table']) {
1942
+ if (typeof options[key] !== 'string' || !options[key].trim())
1943
+ throw new Error(`aggregatePipeline requires ${key}`);
1944
+ }
1945
+ (0, pipeline_interface_1.validateReadPipeline)(options.pipeline);
1946
+ await this.ensureAdapterFor(options);
1947
+ const adapter = this.getAdapter(options);
1948
+ if (!(adapter instanceof mongodb_adapter_1.MongoDBAdapter))
1949
+ throw new Error('aggregatePipeline requires MongoDB');
1950
+ const transaction = options.transaction;
1951
+ if (transaction && (!(transaction instanceof transaction_manager_1.Transaction) || !transaction.belongsTo(adapter) || !transaction.isActive()
1952
+ || this.transactionScopes.get(transaction) !== JSON.stringify([options.product, options.env, options.database]))) {
1953
+ throw new Error('Pipeline transaction does not belong to this product/environment/database');
1954
+ }
1955
+ let resolved = options;
1956
+ let sessionLogFields = {};
1957
+ if (options.session) {
1958
+ const builder = await this.getProductBuilder(options.product);
1959
+ const privateKey = builder.fetchPrivateKey();
1960
+ if (!privateKey)
1961
+ throw new Error('Cannot verify pipeline session');
1962
+ const { processSessionForExecution } = await Promise.resolve().then(() => __importStar(require('../sessions')));
1963
+ // Never recursively transform the driver session or replace the original opaque token.
1964
+ const { transaction: _transaction } = options, input = __rest(options, ["transaction"]);
1965
+ const result = await processSessionForExecution(options.session, privateKey, input, options.env, {
1966
+ fetchSessionSelector: async (tag) => { var _a; return (_a = (await builder.fetchSession(tag))) === null || _a === void 0 ? void 0 : _a.selector; },
1967
+ });
1968
+ if (result.error)
1969
+ throw new Error('Pipeline session validation failed');
1970
+ resolved = Object.assign(Object.assign({}, options), { pipeline: result.input.pipeline, transaction, session: options.session });
1971
+ sessionLogFields = result.logFields;
1972
+ }
1973
+ this.initializeLogService();
1974
+ const start = Date.now();
1975
+ let successful = false;
1976
+ try {
1977
+ const result = await adapter.aggregatePipeline(resolved);
1978
+ successful = true;
1979
+ return result;
1980
+ }
1981
+ finally {
1982
+ (_a = this.logService) === null || _a === void 0 ? void 0 : _a.add(Object.assign(Object.assign({ product_tag: options.product, workspace_id: ((_b = this.config) === null || _b === void 0 ? void 0 : _b.workspace_id) || '', env: options.env, app_env: options.env, process_id: (0, processor_utils_1.generateObjectId)(), type: logs_types_1.LogEventTypes.DATABASE, parent_tag: options.database, child_tag: 'aggregatePipeline' }, sessionLogFields), { start, end: Date.now(), message: `Database pipeline - ${successful ? 'success' : 'failed'}`, status: successful ? logs_types_1.LogEventStatus.SUCCESS : logs_types_1.LogEventStatus.FAIL, successful_execution: successful, failed_execution: !successful, data: { table: options.table, stages: options.pipeline.length } }));
1983
+ void ((_c = this.logService) === null || _c === void 0 ? void 0 : _c.publish().catch(() => { }));
1984
+ }
1985
+ }
1863
1986
  async aggregate(options) {
1864
1987
  var _a, _b, _c, _d, _e, _f, _g, _h;
1865
1988
  const process_id = (0, processor_utils_1.generateObjectId)();
@@ -1973,7 +2096,109 @@ class DatabaseService {
1973
2096
  async beginTransaction(options) {
1974
2097
  await this.ensureAdapterFor(options);
1975
2098
  const adapter = this.getAdapter(options);
1976
- return this.transactionManager.beginTransaction(adapter, options);
2099
+ const transaction = await this.transactionManager.beginTransaction(adapter, options);
2100
+ this.transactionScopes.set(transaction, JSON.stringify([options.product, options.env, options.database]));
2101
+ return transaction;
2102
+ }
2103
+ /** Bounded ACID batch. Unsupported adapters and configured triggers fail closed. */
2104
+ async atomicBatch(options) {
2105
+ var _a;
2106
+ const logs = [];
2107
+ const batch = { id: (0, processor_utils_1.generateObjectId)(), logs, system: !!options.system };
2108
+ let outcome = 'not_started';
2109
+ try {
2110
+ const result = await (0, context_1.runWithExecutionContext)((0, context_1.createExecutionContext)({ database_batch: batch }), () => (0, atomic_batch_1.executeAtomicBatch)({
2111
+ preflight: async (scope) => {
2112
+ var _a;
2113
+ await this.ensureAdapterFor(scope);
2114
+ if (((_a = this.currentContext) === null || _a === void 0 ? void 0 : _a.product) && this.currentContext.product !== scope.product) {
2115
+ throw new Error('Atomic batch product must match the active database context');
2116
+ }
2117
+ if (scope.session && !(await this.getProductBuilder(scope.product)).fetchPrivateKey()) {
2118
+ throw new Error('Atomic batch cannot verify the provided user session');
2119
+ }
2120
+ const adapter = this.getAdapter(scope);
2121
+ if (!(adapter instanceof mongodb_adapter_1.MongoDBAdapter || adapter instanceof postgresql_adapter_1.PostgreSQLAdapter || adapter instanceof mysql_adapter_1.MySQLAdapter || adapter instanceof dynamodb_adapter_1.DynamoDBAdapter)) {
2122
+ throw new Error('This adapter cannot provide the required multi-table ACID contract; Cassandra logged batches are not a substitute');
2123
+ }
2124
+ if (scope.transaction && (!(scope.transaction instanceof transaction_manager_1.Transaction) || !scope.transaction.belongsTo(adapter) || !scope.transaction.isActive()
2125
+ || this.transactionScopes.get(scope.transaction) !== JSON.stringify([scope.product, scope.env, scope.database]))) {
2126
+ throw new Error('Atomic batch transaction does not belong to the active database connection');
2127
+ }
2128
+ if (scope.transaction && (adapter instanceof postgresql_adapter_1.PostgreSQLAdapter || adapter instanceof mysql_adapter_1.MySQLAdapter)
2129
+ && scope.transaction.isolationLevel !== enums_1.IsolationLevel.SERIALIZABLE) {
2130
+ throw new Error('Borrowed SQL batch transactions must use SERIALIZABLE isolation');
2131
+ }
2132
+ const triggers = await this.fetchTriggers(scope.product, scope.database);
2133
+ if (triggers.length)
2134
+ throw new Error('Atomic batch does not yet support configured triggers; use a transaction-local outbox');
2135
+ if (adapter instanceof mongodb_adapter_1.MongoDBAdapter)
2136
+ await adapter.assertAtomicBatchSupport([...new Set(scope.operations.map(op => op.table))]);
2137
+ },
2138
+ executeNative: async (scope) => {
2139
+ var _a, _b, _c, _d;
2140
+ const adapter = this.getAdapter(scope);
2141
+ if (!(adapter instanceof dynamodb_adapter_1.DynamoDBAdapter))
2142
+ return undefined;
2143
+ let resolved = scope;
2144
+ let sessionLogFields = {};
2145
+ if (scope.session) {
2146
+ const builder = await this.getProductBuilder(scope.product);
2147
+ const { processSessionForExecution } = await Promise.resolve().then(() => __importStar(require('../sessions')));
2148
+ const result = await processSessionForExecution(scope.session, builder.fetchPrivateKey(), scope, scope.env, {
2149
+ fetchSessionSelector: async (tag) => { var _a; return (_a = (await builder.fetchSession(tag))) === null || _a === void 0 ? void 0 : _a.selector; },
2150
+ });
2151
+ if (result.error)
2152
+ throw new atomic_batch_1.AtomicBatchError('BATCH_VALIDATION', 'not_started');
2153
+ resolved = Object.assign(Object.assign({}, scope), { operations: result.input.operations });
2154
+ sessionLogFields = result.logFields;
2155
+ }
2156
+ this.initializeLogService();
2157
+ const start = Date.now();
2158
+ try {
2159
+ const result = await adapter.atomicBatch(resolved);
2160
+ (_a = this.logService) === null || _a === void 0 ? void 0 : _a.add(Object.assign({ product_tag: scope.product, workspace_id: ((_b = this.config) === null || _b === void 0 ? void 0 : _b.workspace_id) || '', env: scope.env, app_env: scope.env, process_id: result.transactionId, type: logs_types_1.LogEventTypes.DATABASE, parent_tag: scope.database, child_tag: 'atomicBatch', start, end: Date.now(), message: 'Database atomic batch - success', status: logs_types_1.LogEventStatus.SUCCESS, successful_execution: true, data: { operations: scope.operations.map(op => ({ name: op.name, operation: op.operation, table: op.table })) } }, sessionLogFields));
2161
+ return result;
2162
+ }
2163
+ catch (error) {
2164
+ (_c = this.logService) === null || _c === void 0 ? void 0 : _c.add(Object.assign({ product_tag: scope.product, workspace_id: ((_d = this.config) === null || _d === void 0 ? void 0 : _d.workspace_id) || '', env: scope.env, app_env: scope.env, process_id: batch.id, type: logs_types_1.LogEventTypes.DATABASE, parent_tag: scope.database, child_tag: 'atomicBatch', start, end: Date.now(), message: 'Database atomic batch - failed', status: logs_types_1.LogEventStatus.FAIL, failed_execution: true, data: { outcome: error instanceof atomic_batch_1.AtomicBatchError ? error.outcome : 'not_started' } }, sessionLogFields));
2165
+ throw error;
2166
+ }
2167
+ },
2168
+ validateTransaction: async (scope, transaction) => {
2169
+ const adapter = this.getAdapter(scope);
2170
+ if (adapter instanceof postgresql_adapter_1.PostgreSQLAdapter || adapter instanceof mysql_adapter_1.MySQLAdapter) {
2171
+ await adapter.assertAtomicBatchSupport([...new Set(scope.operations.map(op => op.table))], transaction);
2172
+ }
2173
+ },
2174
+ begin: (scope) => this.beginTransaction({ product: scope.product, env: scope.env, database: scope.database, isolationLevel: enums_1.IsolationLevel.SERIALIZABLE }),
2175
+ execute: async (op, scope, transaction) => {
2176
+ const input = { product: scope.product, env: scope.env, database: scope.database, session: scope.session,
2177
+ table: op.table, transaction, returning: op.returning };
2178
+ switch (op.operation) {
2179
+ case 'insert': return this.insert(Object.assign(Object.assign({}, input), { data: op.data }));
2180
+ case 'update': return this.update(Object.assign(Object.assign({}, input), { where: op.where, data: op.data }));
2181
+ case 'delete': return this.delete(Object.assign(Object.assign({}, input), { where: op.where }));
2182
+ case 'upsert': return this.upsert(Object.assign(Object.assign({}, input), { data: op.data, conflictKeys: op.conflictKeys }));
2183
+ }
2184
+ },
2185
+ }, options));
2186
+ outcome = result.status;
2187
+ return result;
2188
+ }
2189
+ catch (error) {
2190
+ outcome = error instanceof atomic_batch_1.AtomicBatchError ? error.outcome : 'not_started';
2191
+ throw error;
2192
+ }
2193
+ finally {
2194
+ for (const emit of logs) {
2195
+ try {
2196
+ emit(outcome);
2197
+ }
2198
+ catch ( /* Telemetry cannot change transaction outcome. */_b) { /* Telemetry cannot change transaction outcome. */ }
2199
+ }
2200
+ void ((_a = this.logService) === null || _a === void 0 ? void 0 : _a.publish().catch(() => { }));
2201
+ }
1977
2202
  }
1978
2203
  // ==================== SCHEMA OPERATIONS ====================
1979
2204
  /**
@@ -2839,6 +3064,9 @@ class DatabaseService {
2839
3064
  return await fn(adapter);
2840
3065
  }
2841
3066
  catch (error) {
3067
+ // An uncertain transactional write must never be replayed on a fresh connection.
3068
+ if (options.transaction)
3069
+ throw error;
2842
3070
  if (!this.isConnectionError(error))
2843
3071
  throw error;
2844
3072
  await this.connect({
@@ -3467,6 +3695,9 @@ class DatabaseConnection {
3467
3695
  aggregate(options) {
3468
3696
  return this.service.aggregate(Object.assign(Object.assign({}, options), { database: this._database, env: this._env }));
3469
3697
  }
3698
+ aggregatePipeline(options) {
3699
+ return this.service.aggregatePipeline(Object.assign(Object.assign({}, options), { product: this._product, database: this._database, env: this._env }));
3700
+ }
3470
3701
  groupBy(options) {
3471
3702
  return this.service.groupBy(Object.assign(Object.assign({}, options), { database: this._database, env: this._env }));
3472
3703
  }