@memberjunction/sqlserver-dataprovider 5.4.0 → 5.5.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.
@@ -15,20 +15,12 @@
15
15
  * In practice - this FILE will NOT exist in the entities library, we need to move to its own separate project
16
16
  * so it is only included by the consumer of the entities library if they want to use it.
17
17
  **************************************************************************************************************/
18
- import { BaseEntity, IEntityDataProvider, IMetadataProvider, RunViewResult, AggregateResult, EntityInfo, EntityFieldInfo, ApplicationInfo, RunViewParams, ProviderType, UserInfo, RecordChange, ILocalStorageProvider, IFileSystemProvider, AuditLogTypeInfo, AuthorizationInfo, TransactionGroupBase, EntitySaveOptions, RunReportParams, DatasetItemFilterType, DatasetResultType, DatasetStatusResultType, EntityRecordNameInput, EntityRecordNameResult, IRunReportProvider, RunReportResult, RecordDependency, RecordMergeRequest, RecordMergeResult, EntityDependency, RunQueryResult, RunQueryParams, PotentialDuplicateRequest, PotentialDuplicateResponse, CompositeKey, EntityDeleteOptions, EntityMergeOptions, DatasetItemResultType, DatabaseProviderBase, QueryInfo, RunViewWithCacheCheckParams, RunViewsWithCacheCheckResponse, RunViewWithCacheCheckResult, RunQueryWithCacheCheckParams, RunQueriesWithCacheCheckResponse, RunQueryWithCacheCheckResult } from '@memberjunction/core';
19
- import { MJAuditLogEntity, MJEntityAIActionEntity, MJQueryEntity, MJRecordMergeLogEntity, MJUserViewEntityExtended } from '@memberjunction/core-entities';
18
+ import { BaseEntity, IEntityDataProvider, IMetadataProvider, EntityInfo, EntityFieldInfo, ProviderType, UserInfo, ILocalStorageProvider, IFileSystemProvider, TransactionGroupBase, EntitySaveOptions, IRunReportProvider, EntityDependency, RunQueryResult, RunQueryParams, CompositeKey, BaseEntityResult, SaveSQLResult, DeleteSQLResult, QueryInfo, RunViewWithCacheCheckParams, RunQueryWithCacheCheckParams } from '@memberjunction/core';
19
+ import { EntityAIActionParams } from '@memberjunction/aiengine';
20
+ import { GenericDatabaseProvider, ExecuteSQLBatchOptions } from '@memberjunction/generic-database-provider';
20
21
  import sql from 'mssql';
21
22
  import { Observable } from 'rxjs';
22
- import { ExecuteSQLOptions, ExecuteSQLBatchOptions, SQLServerProviderConfigData, SqlLoggingOptions, SqlLoggingSession } from './types.js';
23
- import { ActionResult } from '@memberjunction/actions-base';
24
- /**
25
- * Represents a single field change in the DiffObjects comparison result
26
- */
27
- export type FieldChange = {
28
- field: string;
29
- oldValue: any;
30
- newValue: any;
31
- };
23
+ import { ExecuteSQLOptions, SQLServerProviderConfigData } from './types.js';
32
24
  /**
33
25
  * SQL Server implementation of the MemberJunction data provider interfaces.
34
26
  *
@@ -48,7 +40,15 @@ export type FieldChange = {
48
40
  * await provider.Config();
49
41
  * ```
50
42
  */
51
- export declare class SQLServerDataProvider extends DatabaseProviderBase implements IEntityDataProvider, IMetadataProvider, IRunReportProvider {
43
+ export declare class SQLServerDataProvider extends GenericDatabaseProvider implements IEntityDataProvider, IMetadataProvider, IRunReportProvider {
44
+ /**************************************************************************/
45
+ /**************************************************************************/
46
+ QuoteIdentifier(name: string): string;
47
+ QuoteSchemaAndView(schemaName: string, objectName: string): string;
48
+ private static readonly _sqlServerUUIDPattern;
49
+ private static readonly _sqlServerDefaultPattern;
50
+ protected get UUIDFunctionPattern(): RegExp;
51
+ protected get DBDefaultFunctionPattern(): RegExp;
52
52
  private _pool;
53
53
  private _transaction;
54
54
  private _transactionDepth;
@@ -61,8 +61,6 @@ export declare class SQLServerDataProvider extends DatabaseProviderBase implemen
61
61
  private _recordDupeDetector;
62
62
  private _needsDatetimeOffsetAdjustment;
63
63
  private _datetimeOffsetTestComplete;
64
- private static _sqlLoggingSessionsKey;
65
- private get _sqlLoggingSessions();
66
64
  private _sqlQueue$;
67
65
  private _queueSubscription;
68
66
  private _transactionState$;
@@ -135,105 +133,12 @@ export declare class SQLServerDataProvider extends DatabaseProviderBase implemen
135
133
  * Gets the MemberJunction core schema name (defaults to __mj if not configured)
136
134
  */
137
135
  get MJCoreSchemaName(): string;
138
- /**************************************************************************/
139
- /**************************************************************************/
140
- /**
141
- * Creates a new SQL logging session that will capture all SQL operations to a file.
142
- * Returns a disposable session object that must be disposed to stop logging.
143
- *
144
- * @param filePath - Full path to the file where SQL statements will be logged
145
- * @param options - Optional configuration for the logging session
146
- * @returns Promise<SqlLoggingSession> - Disposable session object
147
- *
148
- * @example
149
- * ```typescript
150
- * // Basic usage
151
- * const session = await provider.CreateSqlLogger('./logs/metadata-sync.sql');
152
- * try {
153
- * // Perform operations that will be logged
154
- * await provider.ExecuteSQL('INSERT INTO ...');
155
- * } finally {
156
- * await session.dispose(); // Stop logging
157
- * }
158
- *
159
- * // With migration formatting
160
- * const session = await provider.CreateSqlLogger('./migrations/changes.sql', {
161
- * formatAsMigration: true,
162
- * description: 'MetadataSync push operation'
163
- * });
164
- * ```
165
- */
166
- CreateSqlLogger(filePath: string, options?: SqlLoggingOptions): Promise<SqlLoggingSession>;
167
136
  GetCurrentUser(): Promise<UserInfo>;
168
- /**
169
- * Gets information about all active SQL logging sessions.
170
- * Useful for monitoring and debugging.
171
- *
172
- * @returns Array of session information objects
173
- */
174
- GetActiveSqlLoggingSessions(): Array<{
175
- id: string;
176
- filePath: string;
177
- startTime: Date;
178
- statementCount: number;
179
- options: SqlLoggingOptions;
180
- }>;
181
- /**
182
- * Gets a specific SQL logging session by its ID.
183
- * Returns the session if found, or undefined if not found.
184
- *
185
- * @param sessionId - The unique identifier of the session to retrieve
186
- * @returns The SqlLoggingSession if found, undefined otherwise
187
- */
188
- GetSqlLoggingSessionById(sessionId: string): SqlLoggingSession | undefined;
189
- /**
190
- * Disposes all active SQL logging sessions.
191
- * Useful for cleanup on provider shutdown.
192
- */
193
- DisposeAllSqlLoggingSessions(): Promise<void>;
194
137
  /**
195
138
  * Dispose of this provider instance and clean up resources.
196
139
  * This should be called when the provider is no longer needed.
197
140
  */
198
141
  Dispose(): Promise<void>;
199
- /**
200
- * Internal method to log SQL statement to all active logging sessions.
201
- * This is called automatically by ExecuteSQL methods.
202
- *
203
- * @param query - The SQL query being executed
204
- * @param parameters - Parameters for the query
205
- * @param description - Optional description for this operation
206
- * @param ignoreLogging - If true, this statement will not be logged
207
- * @param isMutation - Whether this is a data mutation operation
208
- * @param simpleSQLFallback - Optional simple SQL to use for loggers with logRecordChangeMetadata=false
209
- */
210
- private _logSqlStatement;
211
- /**
212
- * Static method to log SQL statements from external sources like transaction groups
213
- *
214
- * @param query - The SQL query being executed
215
- * @param parameters - Parameters for the query
216
- * @param description - Optional description for this operation
217
- * @param isMutation - Whether this is a data mutation operation
218
- * @param simpleSQLFallback - Optional simple SQL to use for loggers with logRecordChangeMetadata=false
219
- */
220
- static LogSQLStatement(query: string, parameters?: any, description?: string, isMutation?: boolean, simpleSQLFallback?: string, contextUser?: UserInfo): Promise<void>;
221
- /**************************************************************************/
222
- /**************************************************************************/
223
- /**************************************************************************/
224
- /**************************************************************************/
225
- RunReport(params: RunReportParams, contextUser?: UserInfo): Promise<RunReportResult>;
226
- /**************************************************************************/
227
- /**************************************************************************/
228
- /**
229
- * Resolves a hierarchical category path (e.g., "/MJ/AI/Agents/") to a CategoryID.
230
- * The path is split by "/" and each segment is matched case-insensitively against
231
- * category names, walking down the hierarchy from root to leaf.
232
- *
233
- * @param categoryPath The hierarchical category path (e.g., "/MJ/AI/Agents/")
234
- * @returns The CategoryID if the path exists, null otherwise
235
- */
236
- private resolveCategoryPath;
237
142
  /**
238
143
  * Finds a query by ID or by Name+Category combination.
239
144
  * Supports both direct CategoryID lookup and hierarchical CategoryPath path resolution.
@@ -246,15 +151,6 @@ export declare class SQLServerDataProvider extends DatabaseProviderBase implemen
246
151
  * @returns The found QueryInfo or null if not found
247
152
  */
248
153
  protected findQuery(QueryID: string, QueryName: string, CategoryID: string, CategoryPath: string, refreshMetadataIfNotFound?: boolean): Promise<QueryInfo | null>;
249
- /**
250
- * Looks up a query from QueryEngine's auto-refreshed cache by ID, name, and optional category filters.
251
- */
252
- protected findQueryInEngine(QueryID: string, QueryName: string, CategoryID: string, CategoryPath: string): MJQueryEntity | null;
253
- /**
254
- * Creates a fresh QueryInfo from a MJQueryEntity and patches the ProviderBase in-memory cache.
255
- * This avoids stale data without requiring a full metadata reload.
256
- */
257
- protected refreshQueryInfoFromEntity(entity: MJQueryEntity): QueryInfo;
258
154
  /**************************************************************************/
259
155
  /**************************************************************************/
260
156
  protected InternalRunQuery(params: RunQueryParams, contextUser?: UserInfo): Promise<RunQueryResult>;
@@ -308,19 +204,6 @@ export declare class SQLServerDataProvider extends DatabaseProviderBase implemen
308
204
  * @returns Array of query results
309
205
  */
310
206
  protected InternalRunQueries(params: RunQueryParams[], contextUser?: UserInfo): Promise<RunQueryResult[]>;
311
- /**
312
- * RunQueriesWithCacheCheck - Smart cache validation for batch RunQueries.
313
- * For each query request, if cacheStatus is provided, uses the Query's CacheValidationSQL
314
- * to check if the cached data is still current by comparing MAX(__mj_UpdatedAt) and COUNT(*)
315
- * with client's values. Returns 'current' if cache is valid (no data), or 'stale' with fresh data.
316
- *
317
- * Queries without CacheValidationSQL configured will return 'no_validation' status with full data.
318
- */
319
- RunQueriesWithCacheCheck<T = unknown>(params: RunQueryWithCacheCheckParams[], contextUser?: UserInfo): Promise<RunQueriesWithCacheCheckResponse<T>>;
320
- /**
321
- * Resolves QueryInfo from RunQueryParams (by ID or Name+CategoryPath).
322
- */
323
- protected resolveQueryInfo(params: RunQueryParams): QueryInfo | undefined;
324
207
  /**
325
208
  * Executes a batched cache status check for multiple queries using their CacheValidationSQL.
326
209
  */
@@ -334,34 +217,13 @@ export declare class SQLServerDataProvider extends DatabaseProviderBase implemen
334
217
  rowCount?: number;
335
218
  errorMessage?: string;
336
219
  }>>;
337
- /**
338
- * Runs the full query and returns results with cache metadata.
339
- */
340
- protected runFullQueryAndReturnForQuery<T = unknown>(params: RunQueryParams, queryIndex: number, status: 'stale' | 'no_validation', contextUser?: UserInfo, queryId?: string): Promise<RunQueryWithCacheCheckResult<T>>;
341
220
  /**************************************************************************/
342
221
  /**************************************************************************/
343
- /**
344
- * This method will check to see if the where clause for the view provided has any templating within it, and if it does
345
- * will replace the templating with the appropriate run-time values. This is done recursively with depth-first traversal
346
- * so that if there are nested templates, they will be replaced as well. We also maintain a stack to ensure that any
347
- * possible circular references are caught and an error is thrown if that is the case.
348
- * @param viewEntity
349
- * @param user
350
- */
351
- protected RenderViewWhereClause(viewEntity: MJUserViewEntityExtended, user: UserInfo, stack?: string[]): Promise<string>;
352
222
  /**************************************************************************/
353
223
  /**************************************************************************/
354
- protected InternalRunView<T = any>(params: RunViewParams, contextUser?: UserInfo): Promise<RunViewResult<T>>;
355
- protected InternalRunViews<T = any>(params: RunViewParams[], contextUser?: UserInfo): Promise<RunViewResult<T>[]>;
356
- /**
357
- * RunViewsWithCacheCheck - Smart cache validation for batch RunViews.
358
- * For each view request, if cacheStatus is provided, first checks if the cache is current
359
- * by comparing MAX(__mj_UpdatedAt) and COUNT(*) with client's values.
360
- * Returns 'current' if cache is valid (no data), or 'stale' with fresh data if cache is outdated.
361
- *
362
- * Optimized to batch all cache status checks into a single SQL call with multiple result sets.
363
- */
364
- RunViewsWithCacheCheck<T = unknown>(params: RunViewWithCacheCheckParams[], contextUser?: UserInfo): Promise<RunViewsWithCacheCheckResponse<T>>;
224
+ protected BuildTopClause(maxRows: number): string;
225
+ protected BuildPaginationSQL(maxRows: number, startRow: number): string;
226
+ protected BuildParameterPlaceholder(index: number): string;
365
227
  /**
366
228
  * Executes a batched cache status check for multiple views in a single SQL call.
367
229
  * Uses multiple result sets to return status for each view efficiently.
@@ -377,124 +239,15 @@ export declare class SQLServerDataProvider extends DatabaseProviderBase implemen
377
239
  rowCount?: number;
378
240
  errorMessage?: string;
379
241
  }>>;
380
- /**
381
- * Builds the WHERE clause for cache status check, using same logic as InternalRunView.
382
- */
383
- protected buildWhereClauseForCacheCheck(params: RunViewParams, entityInfo: EntityInfo, user: UserInfo): Promise<string>;
384
- /**
385
- * Compares client cache status with server status to determine if cache is current.
386
- */
387
- protected isCacheCurrent(clientStatus: {
388
- maxUpdatedAt: string;
389
- rowCount: number;
390
- }, serverStatus: {
391
- maxUpdatedAt?: string;
392
- rowCount?: number;
393
- }): boolean;
394
- /**
395
- * Runs the full query and returns results with cache metadata.
396
- */
397
- protected runFullQueryAndReturn<T = unknown>(params: RunViewParams, viewIndex: number, contextUser?: UserInfo): Promise<RunViewWithCacheCheckResult<T>>;
398
- /**
399
- * Extracts the maximum __mj_UpdatedAt value from a result set.
400
- * @param results - Array of result objects that may contain __mj_UpdatedAt
401
- * @returns ISO string of the max timestamp, or current time if none found
402
- */
403
- protected extractMaxUpdatedAt(results: unknown[]): string;
404
- /**
405
- * Gets the IDs of records that have been deleted since a given timestamp.
406
- * Uses the RecordChange table which tracks all deletions for entities with TrackRecordChanges enabled.
407
- * @param entityID - The entity ID to check deletions for
408
- * @param sinceTimestamp - ISO timestamp to check deletions since
409
- * @param contextUser - Optional user context for permissions
410
- * @returns Array of record IDs (in CompositeKey concatenated string format)
411
- */
412
- protected getDeletedRecordIDsSince(entityID: string, sinceTimestamp: string, contextUser?: UserInfo): Promise<string[]>;
413
- /**
414
- * Gets rows that have been created or updated since a given timestamp.
415
- * @param params - RunView parameters (used for entity, filter, etc.)
416
- * @param entityInfo - Entity metadata
417
- * @param sinceTimestamp - ISO timestamp to check updates since
418
- * @param whereSQL - Pre-built WHERE clause from the original query
419
- * @param contextUser - Optional user context for permissions
420
- * @returns Array of updated/created rows
421
- */
422
- protected getUpdatedRowsSince<T = unknown>(params: RunViewParams, entityInfo: EntityInfo, sinceTimestamp: string, whereSQL: string, contextUser?: UserInfo): Promise<T[]>;
423
- /**
424
- * Runs a differential query and returns only changes since the client's cached state.
425
- * This includes updated/created rows and deleted record IDs.
426
- *
427
- * Validates that the differential can be safely applied by checking for "hidden" deletes
428
- * (rows deleted outside of MJ's RecordChanges tracking, e.g., direct SQL deletes).
429
- * If hidden deletes are detected, falls back to a full query with 'stale' status.
430
- *
431
- * @param params - RunView parameters
432
- * @param entityInfo - Entity metadata
433
- * @param clientMaxUpdatedAt - Client's cached maxUpdatedAt timestamp
434
- * @param clientRowCount - Client's cached row count
435
- * @param serverStatus - Current server status (for new row count)
436
- * @param whereSQL - Pre-built WHERE clause
437
- * @param viewIndex - Index for correlation in batch operations
438
- * @param contextUser - Optional user context
439
- * @returns RunViewWithCacheCheckResult with differential data, or falls back to full query if unsafe
440
- */
441
- protected runDifferentialQueryAndReturn<T = unknown>(params: RunViewParams, entityInfo: EntityInfo, clientMaxUpdatedAt: string, clientRowCount: number, serverStatus: {
442
- maxUpdatedAt?: string;
443
- rowCount?: number;
444
- }, whereSQL: string, viewIndex: number, contextUser?: UserInfo): Promise<RunViewWithCacheCheckResult<T>>;
445
- protected validateUserProvidedSQLClause(clause: string): boolean;
446
- /**
447
- * Validates and builds an aggregate SQL query from the provided aggregate expressions.
448
- * Uses the SQLExpressionValidator to ensure expressions are safe from SQL injection.
449
- *
450
- * @param aggregates - Array of aggregate expressions to validate and build
451
- * @param entityInfo - Entity metadata for field reference validation
452
- * @param schemaName - Schema name for the table
453
- * @param baseView - Base view name for the table
454
- * @param whereSQL - WHERE clause to apply (without the WHERE keyword)
455
- * @returns Object with aggregateSQL string and any validation errors
456
- */
457
- protected buildAggregateSQL(aggregates: {
458
- expression: string;
459
- alias?: string;
460
- }[], entityInfo: EntityInfo, schemaName: string, baseView: string, whereSQL: string): {
461
- aggregateSQL: string | null;
462
- validationErrors: AggregateResult[];
463
- };
464
- /**
465
- * Executes the aggregate query and maps results back to the original expressions.
466
- *
467
- * @param aggregateSQL - The aggregate SQL query to execute
468
- * @param aggregates - Original aggregate expressions (for result mapping)
469
- * @param validationErrors - Any validation errors from buildAggregateSQL
470
- * @param contextUser - User context for query execution
471
- * @returns Array of AggregateResult objects
472
- */
473
- protected executeAggregateQuery(aggregateSQL: string | null, aggregates: {
474
- expression: string;
475
- alias?: string;
476
- }[], validationErrors: AggregateResult[], contextUser?: UserInfo): Promise<{
477
- results: AggregateResult[];
478
- executionTime: number;
479
- }>;
480
- protected getRunTimeViewFieldString(params: RunViewParams, viewEntity: MJUserViewEntityExtended): string;
481
- protected getRunTimeViewFieldArray(params: RunViewParams, viewEntity: MJUserViewEntityExtended): EntityFieldInfo[];
482
242
  protected executeSQLForUserViewRunLogging(viewId: number, entityBaseView: string, whereSQL: string, orderBySQL: string, user: UserInfo): Promise<{
483
243
  executeViewSQL: string;
484
244
  runID: string;
485
245
  }>;
486
- protected createViewUserSearchSQL(entityInfo: EntityInfo, userSearchString: string): string;
487
- CreateAuditLogRecord(user: UserInfo, authorizationName: string | null, auditLogTypeName: string, status: string, details: string | null, entityId: string, recordId: any | null, auditLogDescription: string | null, saveOptions: EntitySaveOptions): Promise<MJAuditLogEntity>;
488
- protected CheckUserReadPermissions(entityName: string, contextUser: UserInfo): void;
489
246
  /**************************************************************************/
490
247
  /**************************************************************************/
491
248
  /**************************************************************************/
492
249
  /**************************************************************************/
493
250
  get ProviderType(): ProviderType;
494
- GetRecordFavoriteStatus(userId: string, entityName: string, CompositeKey: CompositeKey, contextUser?: UserInfo): Promise<boolean>;
495
- GetRecordFavoriteID(userId: string, entityName: string, CompositeKey: CompositeKey, contextUser?: UserInfo): Promise<string | null>;
496
- SetRecordFavoriteStatus(userId: string, entityName: string, CompositeKey: CompositeKey, isFavorite: boolean, contextUser: UserInfo): Promise<void>;
497
- GetRecordChanges(entityName: string, compositeKey: CompositeKey, contextUser?: UserInfo): Promise<RecordChange[]>;
498
251
  /**
499
252
  * This function will generate SQL statements for all of the possible soft links that are not traditional foreign keys but exist in entities
500
253
  * where there is a column that has the EntityIDFieldName set to a column name (not null). We need to get a list of all such soft link fields across ALL entities
@@ -502,22 +255,9 @@ export declare class SQLServerDataProvider extends DatabaseProviderBase implemen
502
255
  * @param entityName
503
256
  * @param compositeKey
504
257
  */
505
- protected GetSoftLinkDependencySQL(entityName: string, compositeKey: CompositeKey): string;
506
- protected GetHardLinkDependencySQL(entityDependencies: EntityDependency[], compositeKey: CompositeKey): string;
507
- /**
508
- * Returns a list of dependencies - records that are linked to the specified Entity/RecordID combination. A dependency is as defined by the relationships in the database. The MemberJunction metadata that is used
509
- * for this simply reflects the foreign key relationships that exist in the database. The CodeGen tool is what detects all of the relationships and generates the metadata that is used by MemberJunction. The metadata in question
510
- * is within the EntityField table and specifically the RelatedEntity and RelatedEntityField columns. In turn, this method uses that metadata and queries the database to determine the dependencies. To get the list of entity dependencies
511
- * you can use the utility method GetEntityDependencies(), which doesn't check for dependencies on a specific record, but rather gets the metadata in one shot that can be used for dependency checking.
512
- * @param entityName the name of the entity to check
513
- * @param KeyValuePairs the primary key(s) to check - only send multiple if you have an entity with a composite primary key
514
- */
515
- GetRecordDependencies(entityName: string, compositeKey: CompositeKey, contextUser?: UserInfo): Promise<RecordDependency[]>;
258
+ protected BuildSoftLinkDependencySQL(entityName: string, compositeKey: CompositeKey): string;
259
+ protected BuildHardLinkDependencySQL(entityDependencies: EntityDependency[], compositeKey: CompositeKey): string;
516
260
  protected GetRecordDependencyLinkSQL(dep: EntityDependency, entity: EntityInfo, relatedEntity: EntityInfo, CompositeKey: CompositeKey): string;
517
- GetRecordDuplicates(params: PotentialDuplicateRequest, contextUser?: UserInfo): Promise<PotentialDuplicateResponse>;
518
- MergeRecords(request: RecordMergeRequest, contextUser?: UserInfo, options?: EntityMergeOptions): Promise<RecordMergeResult>;
519
- protected StartMergeLogging(request: RecordMergeRequest, result: RecordMergeResult, contextUser: UserInfo): Promise<MJRecordMergeLogEntity>;
520
- protected CompleteMergeLogging(recordMergeLog: MJRecordMergeLogEntity, result: RecordMergeResult, contextUser?: UserInfo): Promise<void>;
521
261
  /**
522
262
  * Generates the SQL Statement that will Save a record to the database.
523
263
  *
@@ -546,47 +286,11 @@ export declare class SQLServerDataProvider extends DatabaseProviderBase implemen
546
286
  */
547
287
  private GetSaveSQLWithDetails;
548
288
  /**
549
- * Gets AI actions configured for an entity based on trigger timing
550
- *
551
- * @param entityInfo - The entity to get AI actions for
552
- * @param before - True to get before-save actions, false for after-save
553
- * @returns Array of AI action entities
554
- * @internal
555
- */
556
- protected GetEntityAIActions(entityInfo: EntityInfo, before: boolean): MJEntityAIActionEntity[];
557
- /**
558
- * Handles entity actions (non-AI) for save, delete, or validate operations
559
- *
560
- * @param entity - The entity being operated on
561
- * @param baseType - The type of operation
562
- * @param before - Whether this is before or after the operation
563
- * @param user - The user performing the operation
564
- * @returns Array of action results
565
- * @internal
566
- */
567
- protected HandleEntityActions(entity: BaseEntity, baseType: 'save' | 'delete' | 'validate', before: boolean, user: UserInfo): Promise<ActionResult[]>;
568
- /**
569
- * Handles Entity AI Actions. Parameters are setup for a future support of delete actions, but currently that isn't supported so the baseType parameter
570
- * isn't fully functional. If you pass in delete, the function will just exit for now, and in the future calling code will start working when we support
571
- * Delete as a trigger event for Entity AI Actions...
572
- * @param entity
573
- * @param baseType
574
- * @param before
575
- * @param user
576
- */
577
- protected HandleEntityAIActions(entity: BaseEntity, baseType: 'save' | 'delete', before: boolean, user: UserInfo): Promise<void>;
578
- Save(entity: BaseEntity, user: UserInfo, options: EntitySaveOptions): Promise<{}>;
579
- protected MapTransactionResultToNewValues(transactionResult: Record<string, any>): {
580
- FieldName: string;
581
- Value: any;
582
- }[];
583
- /**
584
- * Returns the stored procedure name to use for the given entity based on if it is a new record or an existing record.
585
- * @param entity
586
- * @param bNewRecord
587
- * @returns
289
+ * Override to defer AI action tasks when a transaction is active.
290
+ * When inside a transaction, tasks are queued to _deferredTasks and
291
+ * processed after transaction commit (see processDeferredTasks).
588
292
  */
589
- GetCreateUpdateSPName(entity: BaseEntity, bNewRecord: boolean): string;
293
+ protected EnqueueAfterSaveAIAction(params: EntityAIActionParams, user: UserInfo): void;
590
294
  private getAllEntityColumnsSQL;
591
295
  /**
592
296
  * Generates the stored procedure parameters for a save operation.
@@ -624,85 +328,14 @@ export declare class SQLServerDataProvider extends DatabaseProviderBase implemen
624
328
  */
625
329
  protected packageSPParam(paramValue: any, quoteString: string, unicodePrefix: string): string;
626
330
  protected GetLogRecordChangeSQL(newData: any, oldData: any, entityName: string, recordID: any, entityInfo: EntityInfo, type: 'Create' | 'Update' | 'Delete', user: UserInfo, wrapRecordIdInQuotes: boolean): string;
627
- protected LogRecordChange(newData: any, oldData: any, entityName: string, recordID: any, entityInfo: EntityInfo, type: 'Create' | 'Update' | 'Delete', user: UserInfo): Promise<any>;
628
- /**
629
- * This method will create a human-readable string that describes the changes object that was created using the DiffObjects() method
630
- * @param changesObject JavaScript object that has properties for each changed field that in turn have field, oldValue and newValue as sub-properties
631
- * @param maxValueLength If not specified, default value of 200 characters applies where any values after the maxValueLength is cut off. The actual values are stored in the ChangesJSON and FullRecordJSON in the RecordChange table, this is only for the human-display
632
- * @param cutOffText If specified, and if maxValueLength applies to any of the values being included in the description, this cutOffText param will be appended to the end of the cut off string to indicate to the human reader that the value is partial.
633
- * @returns
634
- */
635
- CreateUserDescriptionOfChanges(changesObject: any, maxValueLength?: number, cutOffText?: string): string;
636
- protected trimString(value: any, maxLength: number, trailingChars: string): any;
637
331
  /**
638
- * Recursively escapes quotes in all string properties of an object or array.
639
- * This method traverses through nested objects and arrays, escaping the specified
640
- * quote character in all string values to prevent SQL injection and syntax errors.
641
- *
642
- * @param obj - The object, array, or primitive value to process
643
- * @param quoteToEscape - The quote character to escape (typically single quote "'")
644
- * @returns A new object/array with all string values having quotes properly escaped.
645
- * Non-string values are preserved as-is.
646
- *
647
- * @example
648
- * // Escaping single quotes in a nested object
649
- * const input = {
650
- * name: "John's Company",
651
- * details: {
652
- * description: "It's the best",
653
- * tags: ["Won't fail", "Can't stop"]
654
- * }
655
- * };
656
- * const escaped = this.escapeQuotesInProperties(input, "'");
657
- * // Result: {
658
- * // name: "John''s Company",
659
- * // details: {
660
- * // description: "It''s the best",
661
- * // tags: ["Won''t fail", "Can''t stop"]
662
- * // }
663
- * // }
664
- *
665
- * @remarks
666
- * This method is essential for preparing data to be embedded in SQL strings.
667
- * It handles:
668
- * - Nested objects of any depth
669
- * - Arrays (including arrays of objects)
670
- * - Mixed-type objects with strings, numbers, booleans, null values
671
- * - Circular references are NOT handled and will cause stack overflow
672
- */
673
- protected escapeQuotesInProperties(obj: any, quoteToEscape: string): any;
674
- /**
675
- * Creates a changes object by comparing two javascript objects, identifying fields that have different values.
676
- * Each property in the returned object represents a changed field, with the field name as the key.
677
- *
678
- * @param oldData - The original data object to compare from
679
- * @param newData - The new data object to compare to
680
- * @param entityInfo - Entity metadata used to validate fields and determine comparison logic
681
- * @param quoteToEscape - The quote character to escape in string values (typically "'")
682
- * @returns A Record mapping field names to FieldChange objects containing the field name, old value, and new value.
683
- * Returns null if either oldData or newData is null/undefined.
684
- * Only includes fields that have actually changed and are not read-only.
685
- *
686
- * @remarks
687
- * - Read-only fields are never considered changed
688
- * - null and undefined are treated as equivalent
689
- * - Date fields are compared by timestamp
690
- * - String and object values have quotes properly escaped for SQL
691
- * - Objects/arrays are recursively escaped using escapeQuotesInProperties
692
- *
693
- * @example
694
- * ```typescript
695
- * const changes = provider.DiffObjects(
696
- * { name: "John's Co", revenue: 1000 },
697
- * { name: "John's Co", revenue: 2000 },
698
- * entityInfo,
699
- * "'"
700
- * );
701
- * // Returns: { revenue: { field: "revenue", oldValue: 1000, newValue: 2000 } }
702
- * ```
332
+ * Implements the abstract BuildRecordChangeSQL from DatabaseProviderBase.
333
+ * Delegates to GetLogRecordChangeSQL for T-SQL generation.
703
334
  */
704
- DiffObjects(oldData: any, newData: any, entityInfo: EntityInfo, quoteToEscape: string): Record<string, FieldChange> | null;
705
- Load(entity: BaseEntity, CompositeKey: CompositeKey, EntityRelationshipsToLoad: string[], user: UserInfo): Promise<{}>;
335
+ protected BuildRecordChangeSQL(newData: Record<string, unknown> | null, oldData: Record<string, unknown> | null, entityName: string, recordID: string, entityInfo: EntityInfo, type: 'Create' | 'Update' | 'Delete', user: UserInfo): {
336
+ sql: string;
337
+ parameters?: unknown[];
338
+ } | null;
706
339
  /**
707
340
  * Generates the SQL statement for deleting an entity record
708
341
  *
@@ -717,54 +350,41 @@ export declare class SQLServerDataProvider extends DatabaseProviderBase implemen
717
350
  * @returns Object with fullSQL and simpleSQL properties
718
351
  */
719
352
  private GetDeleteSQLWithDetails;
720
- Delete(entity: BaseEntity, options: EntityDeleteOptions, user: UserInfo): Promise<boolean>;
353
+ /**************************************************************************/
354
+ /**************************************************************************/
355
+ protected GenerateSaveSQL(entity: BaseEntity, isNew: boolean, user: UserInfo): Promise<SaveSQLResult>;
356
+ protected GenerateDeleteSQL(entity: BaseEntity, user: UserInfo): DeleteSQLResult;
357
+ protected OnSaveCompleted(entity: BaseEntity, saveSQLResult: SaveSQLResult, user: UserInfo, options: EntitySaveOptions): Promise<void>;
358
+ protected OnSuspendRefresh(): void;
359
+ protected OnResumeRefresh(): void;
360
+ protected GetTransactionExtraData(_entity: BaseEntity): Record<string, unknown>;
361
+ protected BuildSaveExecuteOptions(entity: BaseEntity, sqlDetails: SaveSQLResult): ExecuteSQLOptions;
362
+ protected BuildDeleteExecuteOptions(entity: BaseEntity, sqlDetails: DeleteSQLResult): ExecuteSQLOptions;
363
+ protected ValidateDeleteResult(entity: BaseEntity, rawResult: Record<string, unknown>[], entityResult: BaseEntityResult): boolean;
364
+ /**************************************************************************/
365
+ /**************************************************************************/
721
366
  /**************************************************************************/
722
367
  /**************************************************************************/
723
368
  /**************************************************************************/
724
369
  /**************************************************************************/
725
- GetDatasetByName(datasetName: string, itemFilters?: DatasetItemFilterType[], contextUser?: UserInfo, providerToUse?: IMetadataProvider): Promise<DatasetResultType>;
726
- /**
727
- * Constructs the SQL query for a dataset item.
728
- * @param item - The dataset item metadata
729
- * @param itemFilters - Optional filters to apply
730
- * @param datasetName - Name of the dataset (for error logging)
731
- * @returns The SQL query string, or null if columns are invalid
732
- */
733
- protected GetDatasetItemSQL(item: any, itemFilters: any, datasetName: string): string | null;
734
- protected GetDatasetItem(item: any, itemFilters: any, datasetName: any, contextUser: UserInfo): Promise<DatasetItemResultType>;
735
370
  /**
736
- * Gets column info for a dataset item, which might be * for all columns or if a Columns field was provided in the DatasetItem table,
737
- * attempts to use those columns assuming they are valid.
738
- * @param item
739
- * @param datasetName
740
- * @returns
371
+ * Public backward-compatible wrapper that delegates to PostProcessRows (inherited from GenericDP).
372
+ * Used by SQLServerTransactionGroup which needs a public entry point for row processing.
373
+ *
374
+ * PostProcessRows (GenericDP) handles: AdjustDatetimeFields → encryption decryption.
741
375
  */
742
- protected GetColumnsForDatasetItem(item: any, datasetName: string): string;
743
- GetDatasetStatusByName(datasetName: string, itemFilters?: DatasetItemFilterType[], contextUser?: UserInfo, providerToUse?: IMetadataProvider): Promise<DatasetStatusResultType>;
744
- protected GetApplicationMetadata(contextUser: UserInfo): Promise<ApplicationInfo[]>;
745
- protected GetAuditLogTypeMetadata(contextUser: UserInfo): Promise<AuditLogTypeInfo[]>;
746
- protected GetUserMetadata(contextUser: UserInfo): Promise<UserInfo[]>;
747
- protected GetAuthorizationMetadata(contextUser: UserInfo): Promise<AuthorizationInfo[]>;
376
+ ProcessEntityRows(rows: Record<string, unknown>[], entityInfo: EntityInfo, contextUser?: UserInfo): Promise<Record<string, unknown>[]>;
748
377
  /**
749
- * Processes entity rows returned from SQL Server to handle:
750
- * 1. Timezone conversions for datetime fields
751
- * 2. Field-level decryption for encrypted fields
752
- *
753
- * This method specifically handles the conversion of datetime2 fields (which SQL Server returns without timezone info)
754
- * to proper UTC dates, preventing JavaScript from incorrectly interpreting them as local time.
755
- *
756
- * For encrypted fields, this method decrypts values at the data provider level.
757
- * API-level filtering (AllowDecryptInAPI/SendEncryptedValue) is handled by the GraphQL layer.
378
+ * SQL Server-specific datetime field adjustments.
758
379
  *
759
- * @param rows The raw result rows from SQL Server
760
- * @param entityInfo The entity metadata to determine field types
761
- * @param contextUser Optional user context for decryption operations
762
- * @returns The processed rows with corrected datetime values and decrypted fields
380
+ * SQL Server's `datetime2` and `datetime` types store values without timezone info.
381
+ * The mssql driver creates Date objects using local timezone interpretation, which is
382
+ * incorrect when the server stores UTC. This method adjusts dates back to UTC.
763
383
  *
764
- * @security Encrypted fields are decrypted here for internal use.
765
- * The API layer handles response filtering based on AllowDecryptInAPI settings.
384
+ * For `datetimeoffset`, the driver sometimes mishandles timezone info (detected via
385
+ * `NeedsDatetimeOffsetAdjustment()`), requiring similar correction.
766
386
  */
767
- ProcessEntityRows(rows: any[], entityInfo: EntityInfo, contextUser?: UserInfo): Promise<any[]>;
387
+ protected AdjustDatetimeFields(rows: Record<string, unknown>[], datetimeFields: EntityFieldInfo[], entityInfo: EntityInfo): Promise<Record<string, unknown>[]>;
768
388
  /**
769
389
  * Static method for executing SQL with proper handling of connections and logging.
770
390
  * This is the single point where all SQL execution happens in the entire class.
@@ -898,38 +518,12 @@ export declare class SQLServerDataProvider extends DatabaseProviderBase implemen
898
518
  * @param txn The sql.Transaction object returned from BeginISATransaction()
899
519
  */
900
520
  RollbackISATransaction(txn: unknown): Promise<void>;
901
- /**
902
- * Discovers which IS-A child entity, if any, has a record with the given primary key.
903
- * Executes a single UNION ALL query across all child entity tables for maximum efficiency.
904
- * Each branch of the UNION is a PK lookup on a clustered index — effectively instant.
905
- *
906
- * @param entityInfo The parent entity whose children to search
907
- * @param recordPKValue The primary key value to find in child tables
908
- * @param contextUser Optional context user for audit/permission purposes
909
- * @returns The child entity name if found, or null if no child record exists
910
- */
911
- FindISAChildEntity(entityInfo: EntityInfo, recordPKValue: string, contextUser?: UserInfo): Promise<{
912
- ChildEntityName: string;
913
- } | null>;
914
- /**
915
- * Discovers ALL IS-A child entities that have records with the given primary key.
916
- * Used for overlapping subtype parents (AllowMultipleSubtypes = true) where multiple
917
- * children can coexist. Same UNION ALL query as FindISAChildEntity, but returns all matches.
918
- *
919
- * @param entityInfo The parent entity whose children to search
920
- * @param recordPKValue The primary key value to find in child tables
921
- * @param contextUser Optional context user for audit/permission purposes
922
- * @returns Array of child entity names found (empty if none)
923
- */
924
- FindISAChildEntities(entityInfo: EntityInfo, recordPKValue: string, contextUser?: UserInfo): Promise<{
925
- ChildEntityName: string;
926
- }[]>;
927
521
  /**
928
522
  * Builds a UNION ALL query that checks each child entity's base table for a record
929
523
  * with the given primary key. Returns the first match (disjoint subtypes guarantee
930
524
  * at most one result) unless used with overlapping subtypes.
931
525
  */
932
- private buildChildDiscoverySQL;
526
+ protected BuildChildDiscoverySQL(childEntities: EntityInfo[], recordPKValue: string): string;
933
527
  /**************************************************************************
934
528
  * IS-A Overlapping Subtype — Record Change Propagation
935
529
  *
@@ -951,24 +545,12 @@ export declare class SQLServerDataProvider extends DatabaseProviderBase implemen
951
545
  * Undefined when saving the parent directly — all children get propagated to.
952
546
  * @param transaction The active IS-A transaction, or undefined for standalone saves
953
547
  */
954
- private PropagateRecordChangesToSiblings;
955
- /**
956
- * Checks whether a given entity matches the target name, or is an ancestor
957
- * of the target (i.e., the target is somewhere in its descendant sub-tree).
958
- * Used to identify and skip the active branch during sibling propagation.
959
- */
960
- private isEntityOrAncestorOf;
961
- /**
962
- * Recursively enumerates an entity's entire sub-tree from metadata.
963
- * No DB queries — uses EntityInfo.ChildEntities which is populated from metadata.
964
- */
965
- private getFullSubTree;
966
548
  /**
967
- * Generates a single block of SQL for one sibling entity in the Record Change
549
+ * Generates a single block of T-SQL for one sibling entity in the Record Change
968
550
  * propagation batch. Uses SELECT...FOR JSON to get the full record, then
969
- * conditionally inserts a Record Change entry if the record exists.
551
+ * conditionally inserts a Record Change entry via spCreateRecordChange_Internal.
970
552
  */
971
- private buildSiblingRecordChangeSQL;
553
+ protected BuildSiblingRecordChangeSQL(varName: string, entityInfo: EntityInfo, safeChangesJSON: string, safeChangesDesc: string, safePKValue: string, safeUserId: string): string;
972
554
  BeginTransaction(): Promise<void>;
973
555
  CommitTransaction(): Promise<void>;
974
556
  RollbackTransaction(): Promise<void>;
@@ -986,9 +568,6 @@ export declare class SQLServerDataProvider extends DatabaseProviderBase implemen
986
568
  private processDeferredTasks;
987
569
  get LocalStorageProvider(): ILocalStorageProvider;
988
570
  get FileSystemProvider(): IFileSystemProvider;
989
- protected InternalGetEntityRecordNames(info: EntityRecordNameInput[], contextUser?: UserInfo): Promise<EntityRecordNameResult[]>;
990
- protected InternalGetEntityRecordName(entityName: string, CompositeKey: CompositeKey, contextUser?: UserInfo): Promise<string>;
991
- protected GetEntityRecordNameSQL(entityName: string, CompositeKey: CompositeKey): string;
992
571
  CreateTransactionGroup(): Promise<TransactionGroupBase>;
993
572
  /**************************************************************************/
994
573
  /**************************************************************************/