@memberjunction/core 5.4.1 → 5.6.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 (67) hide show
  1. package/dist/generic/applicationInfo.d.ts.map +1 -1
  2. package/dist/generic/applicationInfo.js +2 -1
  3. package/dist/generic/applicationInfo.js.map +1 -1
  4. package/dist/generic/baseEngine.d.ts.map +1 -1
  5. package/dist/generic/baseEngine.js +0 -13
  6. package/dist/generic/baseEngine.js.map +1 -1
  7. package/dist/generic/baseEntity.js +2 -2
  8. package/dist/generic/baseEntity.js.map +1 -1
  9. package/dist/generic/compositeKey.d.ts.map +1 -1
  10. package/dist/generic/compositeKey.js +12 -1
  11. package/dist/generic/compositeKey.js.map +1 -1
  12. package/dist/generic/databaseProviderBase.d.ts +653 -1
  13. package/dist/generic/databaseProviderBase.d.ts.map +1 -1
  14. package/dist/generic/databaseProviderBase.js +1463 -0
  15. package/dist/generic/databaseProviderBase.js.map +1 -1
  16. package/dist/generic/entityInfo.d.ts +2 -1
  17. package/dist/generic/entityInfo.d.ts.map +1 -1
  18. package/dist/generic/entityInfo.js +15 -10
  19. package/dist/generic/entityInfo.js.map +1 -1
  20. package/dist/generic/interfaces.d.ts +5 -1
  21. package/dist/generic/interfaces.d.ts.map +1 -1
  22. package/dist/generic/interfaces.js +2 -0
  23. package/dist/generic/interfaces.js.map +1 -1
  24. package/dist/generic/localCacheManager.d.ts.map +1 -1
  25. package/dist/generic/localCacheManager.js +4 -2
  26. package/dist/generic/localCacheManager.js.map +1 -1
  27. package/dist/generic/metadata.js +5 -5
  28. package/dist/generic/metadata.js.map +1 -1
  29. package/dist/generic/platformSQL.d.ts +25 -0
  30. package/dist/generic/platformSQL.d.ts.map +1 -0
  31. package/dist/generic/platformSQL.js +7 -0
  32. package/dist/generic/platformSQL.js.map +1 -0
  33. package/dist/generic/platformVariants.d.ts +68 -0
  34. package/dist/generic/platformVariants.d.ts.map +1 -0
  35. package/dist/generic/platformVariants.js +34 -0
  36. package/dist/generic/platformVariants.js.map +1 -0
  37. package/dist/generic/providerBase.d.ts +34 -1
  38. package/dist/generic/providerBase.d.ts.map +1 -1
  39. package/dist/generic/providerBase.js +76 -10
  40. package/dist/generic/providerBase.js.map +1 -1
  41. package/dist/generic/queryInfo.d.ts +97 -0
  42. package/dist/generic/queryInfo.d.ts.map +1 -1
  43. package/dist/generic/queryInfo.js +154 -13
  44. package/dist/generic/queryInfo.js.map +1 -1
  45. package/dist/generic/runQuerySQLFilterImplementations.d.ts +22 -2
  46. package/dist/generic/runQuerySQLFilterImplementations.d.ts.map +1 -1
  47. package/dist/generic/runQuerySQLFilterImplementations.js +74 -3
  48. package/dist/generic/runQuerySQLFilterImplementations.js.map +1 -1
  49. package/dist/generic/securityInfo.d.ts +18 -0
  50. package/dist/generic/securityInfo.d.ts.map +1 -1
  51. package/dist/generic/securityInfo.js +30 -3
  52. package/dist/generic/securityInfo.js.map +1 -1
  53. package/dist/generic/util.d.ts.map +1 -1
  54. package/dist/generic/util.js +42 -3
  55. package/dist/generic/util.js.map +1 -1
  56. package/dist/index.d.ts +2 -0
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +2 -0
  59. package/dist/index.js.map +1 -1
  60. package/dist/views/runView.d.ts +16 -2
  61. package/dist/views/runView.d.ts.map +1 -1
  62. package/dist/views/runView.js +21 -4
  63. package/dist/views/runView.js.map +1 -1
  64. package/dist/views/viewInfo.d.ts.map +1 -1
  65. package/dist/views/viewInfo.js +2 -1
  66. package/dist/views/viewInfo.js.map +1 -1
  67. package/package.json +2 -2
@@ -1,10 +1,52 @@
1
1
  import { ProviderBase } from "./providerBase.js";
2
2
  import { UserInfo } from "./securityInfo.js";
3
+ import { EntityDependency, EntityInfo, RecordChange, RecordDependency, RecordMergeRequest, RecordMergeResult } from "./entityInfo.js";
4
+ import { BaseEntity, BaseEntityResult } from "./baseEntity.js";
5
+ import { EntitySaveOptions, EntityDeleteOptions, EntityMergeOptions, PotentialDuplicateRequest, PotentialDuplicateResponse } from "./interfaces.js";
6
+ import { CompositeKey } from "./compositeKey.js";
7
+ import { AggregateResult, EntityRecordNameInput, EntityRecordNameResult, RunReportResult } from "./interfaces.js";
8
+ import { RunReportParams } from "./runReport.js";
9
+ export { DatabasePlatform, PlatformSQL, IsPlatformSQL } from "./platformSQL.js";
10
+ import { DatabasePlatform } from "./platformSQL.js";
11
+ /**
12
+ * Represents a single field change in the DiffObjects comparison result
13
+ */
14
+ export type FieldChange = {
15
+ field: string;
16
+ oldValue: unknown;
17
+ newValue: unknown;
18
+ };
19
+ /**
20
+ * Result of save SQL generation. Subclasses populate this from their dialect-specific
21
+ * SQL generators (stored procedure calls, function calls, etc.).
22
+ */
23
+ export interface SaveSQLResult {
24
+ /** The complete SQL to execute (may include record-change tracking, temp tables, etc.) */
25
+ fullSQL: string;
26
+ /** Simpler SQL without record-change wrapping, used for logging/migration replay */
27
+ simpleSQL?: string;
28
+ /** Parameterized query values (e.g. [$1,$2] for PG). Null/undefined for inline SQL. */
29
+ parameters?: unknown[] | null;
30
+ /** Provider-specific extra data (e.g. overlapping-change data for ISA propagation) */
31
+ extraData?: Record<string, unknown>;
32
+ }
33
+ /**
34
+ * Result of delete SQL generation.
35
+ */
36
+ export interface DeleteSQLResult {
37
+ fullSQL: string;
38
+ simpleSQL?: string;
39
+ parameters?: unknown[] | null;
40
+ }
3
41
  /**
4
42
  * This class is a generic server-side provider class to abstract database operations
5
43
  * on any database system and therefore be usable by server-side components that need to
6
44
  * do database operations but do not want close coupling with a specific database provider
7
45
  * like @see @memberjunction/sqlserver-dataprovider
46
+ *
47
+ * It contains DB-agnostic business logic (record change tracking, favorites, ISA hierarchy,
48
+ * record dependencies, diffing, etc.) that is shared across all database providers.
49
+ * Subclasses implement abstract methods for DB-specific SQL dialect generation.
8
50
  */
9
51
  export declare abstract class DatabaseProviderBase extends ProviderBase {
10
52
  /**
@@ -16,7 +58,7 @@ export declare abstract class DatabaseProviderBase extends ProviderBase {
16
58
  * @param T - The type of the result set
17
59
  * @returns A promise that resolves to an array of results of type T
18
60
  */
19
- abstract ExecuteSQL<T>(query: string, parameters?: any[], options?: ExecuteSQLOptions, contextUser?: UserInfo): Promise<Array<T>>;
61
+ abstract ExecuteSQL<T>(query: string, parameters?: unknown[], options?: ExecuteSQLOptions, contextUser?: UserInfo): Promise<Array<T>>;
20
62
  /**
21
63
  * Begins a transaction for the current database connection.
22
64
  */
@@ -29,6 +71,616 @@ export declare abstract class DatabaseProviderBase extends ProviderBase {
29
71
  * Rolls back the current transaction.
30
72
  */
31
73
  abstract RollbackTransaction(): Promise<void>;
74
+ /**
75
+ * Returns the database platform key for this provider.
76
+ * Override in subclasses. Defaults to 'sqlserver' for backward compatibility.
77
+ * Inherited from ProviderBase; redeclared here for DatabaseProviderBase consumers.
78
+ */
79
+ get PlatformKey(): DatabasePlatform;
80
+ /**
81
+ * Gets the MemberJunction core schema name (e.g. '__mj').
82
+ * Subclasses should override if they have a different way to resolve this.
83
+ * Defaults to the value from ConfigData.
84
+ */
85
+ get MJCoreSchemaName(): string;
86
+ /**************************************************************************/
87
+ /**************************************************************************/
88
+ /**
89
+ * Quotes a database identifier (table, column, view name) using the provider's
90
+ * dialect convention. SQL Server uses [brackets], PostgreSQL uses "double quotes".
91
+ * @param name The identifier to quote
92
+ */
93
+ abstract QuoteIdentifier(name: string): string;
94
+ /**
95
+ * Quotes a schema-qualified object name (e.g. schema.viewName) using the provider's
96
+ * dialect convention. SQL Server uses [schema].[view], PostgreSQL uses "schema"."view".
97
+ * @param schemaName The schema name
98
+ * @param objectName The object name (table, view, etc.)
99
+ */
100
+ abstract QuoteSchemaAndView(schemaName: string, objectName: string): string;
101
+ /**
102
+ * Builds a UNION ALL query that checks each child entity's base table for a record
103
+ * with the given primary key. Used by FindISAChildEntity/FindISAChildEntities.
104
+ * @param childEntities The child entities to search
105
+ * @param recordPKValue The primary key value to find
106
+ */
107
+ protected abstract BuildChildDiscoverySQL(childEntities: EntityInfo[], recordPKValue: string): string;
108
+ /**
109
+ * Builds SQL for hard-link (foreign key) dependency queries.
110
+ * Returns a UNION ALL query across all dependent entities.
111
+ * @param entityDependencies The entity-level dependency metadata
112
+ * @param compositeKey The primary key of the record being checked
113
+ */
114
+ protected abstract BuildHardLinkDependencySQL(entityDependencies: EntityDependency[], compositeKey: CompositeKey): string;
115
+ /**
116
+ * Builds SQL for soft-link dependency queries (entities using EntityIDFieldName pattern).
117
+ * Returns a UNION ALL query across all soft-linked entities.
118
+ * @param entityName The entity name being checked for dependencies
119
+ * @param compositeKey The primary key of the record
120
+ */
121
+ protected abstract BuildSoftLinkDependencySQL(entityName: string, compositeKey: CompositeKey): string;
122
+ /**
123
+ * Generates the SQL (and optional parameters) for a Save (Create or Update) operation.
124
+ * Each provider produces its own dialect: SQL Server generates T-SQL EXEC statements,
125
+ * PostgreSQL generates SELECT FROM function(...) calls, etc.
126
+ *
127
+ * @param entity The entity being saved
128
+ * @param isNew True for INSERT / Create, false for UPDATE
129
+ * @param user The acting user (needed for encryption, audit columns, etc.)
130
+ */
131
+ protected abstract GenerateSaveSQL(entity: BaseEntity, isNew: boolean, user: UserInfo): Promise<SaveSQLResult>;
132
+ /**
133
+ * Generates the SQL (and optional parameters) for a Delete operation.
134
+ */
135
+ protected abstract GenerateDeleteSQL(entity: BaseEntity, user: UserInfo): DeleteSQLResult;
136
+ /**************************************************************************/
137
+ /**************************************************************************/
138
+ /**************************************************************************/
139
+ /**************************************************************************/
140
+ /**
141
+ * Regex pattern matching known database UUID/ID generation functions for this provider's platform.
142
+ * SQL Server should match NEWID, NEWSEQUENTIALID.
143
+ * PostgreSQL should match gen_random_uuid, uuid_generate_v4.
144
+ * Case-insensitive, should match the full string with optional whitespace and parens.
145
+ */
146
+ protected abstract get UUIDFunctionPattern(): RegExp;
147
+ /**
148
+ * Regex pattern matching known database default-value functions (non-UUID) for this provider's platform.
149
+ * SQL Server should match GETDATE, GETUTCDATE, SYSDATETIME, etc.
150
+ * PostgreSQL should match NOW, CURRENT_TIMESTAMP, clock_timestamp, etc.
151
+ * Case-insensitive, should match the full string with optional whitespace and parens.
152
+ */
153
+ protected abstract get DBDefaultFunctionPattern(): RegExp;
154
+ /**
155
+ * Combined regex covering UUID functions from ALL platforms.
156
+ * Used by the static convenience method for code that doesn't have a provider instance.
157
+ */
158
+ private static readonly _allPlatformUUIDPattern;
159
+ /**
160
+ * Combined regex covering default-value functions from ALL platforms.
161
+ * Used by the static convenience method for code that doesn't have a provider instance.
162
+ */
163
+ private static readonly _allPlatformDefaultPattern;
164
+ /**
165
+ * Checks whether a string value looks like a database UUID generation function
166
+ * for this provider's platform.
167
+ *
168
+ * @param value The string value to check
169
+ * @returns true if the value matches a known UUID generation function pattern
170
+ */
171
+ IsUUIDGenerationFunction(value: string): boolean;
172
+ /**
173
+ * Checks whether a string value looks like a known database default-value function
174
+ * that is NOT a UUID generator for this provider's platform.
175
+ *
176
+ * @param value The string value to check
177
+ * @returns true if the value matches a known non-UUID database function pattern
178
+ */
179
+ IsNonUUIDDatabaseFunction(value: string): boolean;
180
+ /**
181
+ * Static convenience: checks all platforms' UUID generation functions.
182
+ * Prefer the instance method when you have a provider reference.
183
+ */
184
+ static IsUUIDGenerationFunctionAllPlatforms(value: string): boolean;
185
+ /**
186
+ * Static convenience: checks all platforms' default-value functions.
187
+ * Prefer the instance method when you have a provider reference.
188
+ */
189
+ static IsNonUUIDDatabaseFunctionAllPlatforms(value: string): boolean;
190
+ /**
191
+ * Generates a new UUID suitable for use as a primary key or unique identifier.
192
+ * Uses uuidv4() from @memberjunction/global. Subclasses may override to provide
193
+ * platform-specific ID generation if needed.
194
+ *
195
+ * @returns A new UUID string
196
+ */
197
+ GenerateNewID(): string;
198
+ /**************************************************************************/
199
+ /**************************************************************************/
200
+ /**************************************************************************/
201
+ /**************************************************************************/
202
+ /**
203
+ * Creates a changes object by comparing two JavaScript objects, identifying fields that have different values.
204
+ * Each property in the returned object represents a changed field, with the field name as the key.
205
+ *
206
+ * @param oldData - The original data object to compare from
207
+ * @param newData - The new data object to compare to
208
+ * @param entityInfo - Entity metadata used to validate fields and determine comparison logic
209
+ * @param quoteToEscape - The quote character to escape in string values (typically "'")
210
+ * @returns A Record mapping field names to FieldChange objects, or null if either input is null/undefined.
211
+ * Only includes fields that have actually changed and are not read-only.
212
+ */
213
+ DiffObjects(oldData: Record<string, unknown>, newData: Record<string, unknown>, entityInfo: EntityInfo, quoteToEscape: string): Record<string, FieldChange> | null;
214
+ /**
215
+ * Determines whether a specific field value has changed between old and new data.
216
+ */
217
+ private isFieldDifferent;
218
+ /**
219
+ * Escapes a value for use in diff output, handling strings and nested objects.
220
+ */
221
+ private escapeValueForDiff;
222
+ /**
223
+ * Converts a diff/changes object into a human-readable description of what changed.
224
+ * @param changesObject The output of DiffObjects()
225
+ * @param maxValueLength Maximum length for displayed values before truncation
226
+ * @param cutOffText Text to append when values are truncated
227
+ */
228
+ CreateUserDescriptionOfChanges(changesObject: Record<string, FieldChange>, maxValueLength?: number, cutOffText?: string): string;
229
+ /**
230
+ * Truncates a string value to a maximum length, appending trailing characters if truncated.
231
+ */
232
+ protected TrimString(value: unknown, maxLength: number, trailingChars: string): unknown;
233
+ /**
234
+ * Recursively escapes the specified quote character in all string properties of an object or array.
235
+ * Essential for preparing data to be embedded in SQL strings.
236
+ *
237
+ * @param obj - The object, array, or primitive value to process
238
+ * @param quoteToEscape - The quote character to escape (typically single quote "'")
239
+ * @returns A new object/array with all string values having quotes properly escaped
240
+ */
241
+ protected EscapeQuotesInProperties(obj: unknown, quoteToEscape: string): unknown;
242
+ /**
243
+ * Transforms a transaction result row into a list of field/value pairs.
244
+ */
245
+ protected MapTransactionResultToNewValues(transactionResult: Record<string, unknown>): {
246
+ FieldName: string;
247
+ Value: unknown;
248
+ }[];
249
+ /**************************************************************************/
250
+ /**************************************************************************/
251
+ /**************************************************************************/
252
+ /**************************************************************************/
253
+ /**
254
+ * Discovers which IS-A child entity, if any, has a record with the given primary key.
255
+ * Executes a single UNION ALL query across all child entity tables for maximum efficiency.
256
+ *
257
+ * @param entityInfo The parent entity whose children to search
258
+ * @param recordPKValue The primary key value to find in child tables
259
+ * @param contextUser Optional context user for audit/permission purposes
260
+ * @returns The child entity name if found, or null if no child record exists
261
+ */
262
+ FindISAChildEntity(entityInfo: EntityInfo, recordPKValue: string, contextUser?: UserInfo): Promise<{
263
+ ChildEntityName: string;
264
+ } | null>;
265
+ /**
266
+ * Discovers ALL IS-A child entities that have records with the given primary key.
267
+ * Used for overlapping subtype parents (AllowMultipleSubtypes = true) where multiple
268
+ * children can coexist.
269
+ *
270
+ * @param entityInfo The parent entity whose children to search
271
+ * @param recordPKValue The primary key value to find in child tables
272
+ * @param contextUser Optional context user for audit/permission purposes
273
+ * @returns Array of child entity names found (empty if none)
274
+ */
275
+ FindISAChildEntities(entityInfo: EntityInfo, recordPKValue: string, contextUser?: UserInfo): Promise<{
276
+ ChildEntityName: string;
277
+ }[]>;
278
+ /**
279
+ * Checks whether a given entity matches the target name, or is an ancestor
280
+ * of the target (i.e., the target is somewhere in its descendant sub-tree).
281
+ * Used to identify and skip the active branch during sibling propagation.
282
+ */
283
+ protected IsEntityOrAncestorOf(entityInfo: EntityInfo, targetName: string): boolean;
284
+ /**
285
+ * Recursively enumerates an entity's entire sub-tree from metadata.
286
+ * No DB queries — uses EntityInfo.ChildEntities which is populated from metadata.
287
+ */
288
+ protected GetFullSubTree(entityInfo: EntityInfo): EntityInfo[];
289
+ /**************************************************************************/
290
+ /**************************************************************************/
291
+ /**************************************************************************/
292
+ /**************************************************************************/
293
+ /**
294
+ * Retrieves the change history for a specific record.
295
+ * Uses the vwRecordChanges view which exists in both SQL Server and PostgreSQL.
296
+ *
297
+ * @param entityName The entity name
298
+ * @param compositeKey The record's composite primary key
299
+ * @param contextUser Optional context user
300
+ */
301
+ GetRecordChanges(entityName: string, compositeKey: CompositeKey, contextUser?: UserInfo): Promise<RecordChange[]>;
302
+ /**************************************************************************/
303
+ /**************************************************************************/
304
+ /**************************************************************************/
305
+ /**************************************************************************/
306
+ /**
307
+ * Checks if a record is marked as a favorite for a given user.
308
+ */
309
+ GetRecordFavoriteStatus(userId: string, entityName: string, compositeKey: CompositeKey, contextUser?: UserInfo): Promise<boolean>;
310
+ /**
311
+ * Gets the favorite record ID if the record is a favorite for the given user, null otherwise.
312
+ */
313
+ GetRecordFavoriteID(userId: string, entityName: string, compositeKey: CompositeKey, contextUser?: UserInfo): Promise<string | null>;
314
+ /**
315
+ * Creates or deletes a user favorite record for the specified entity record.
316
+ * Uses GetEntityObject and BaseEntity CRUD methods (no entity-specific type imports needed).
317
+ */
318
+ SetRecordFavoriteStatus(userId: string, entityName: string, compositeKey: CompositeKey, isFavorite: boolean, contextUser: UserInfo): Promise<void>;
319
+ /**************************************************************************/
320
+ /**************************************************************************/
321
+ /**************************************************************************/
322
+ /**************************************************************************/
323
+ /**
324
+ * Returns a list of record-level dependencies — records in other entities linked to
325
+ * the specified entity/record via foreign keys (hard links) or EntityIDFieldName
326
+ * soft links. Uses abstract SQL builders for dialect-specific query generation.
327
+ *
328
+ * @param entityName The entity name to check
329
+ * @param compositeKey The primary key(s) of the record
330
+ * @param contextUser Optional context user
331
+ */
332
+ GetRecordDependencies(entityName: string, compositeKey: CompositeKey, contextUser?: UserInfo): Promise<RecordDependency[]>;
333
+ /**
334
+ * Parses raw SQL results from dependency queries into RecordDependency objects.
335
+ */
336
+ private parseRecordDependencyResults;
337
+ /**************************************************************************/
338
+ /**************************************************************************/
339
+ /**************************************************************************/
340
+ /**************************************************************************/
341
+ /**
342
+ * Called during Save before any SQL is executed to run validation-type entity actions.
343
+ * Return a non-empty string to abort the save with that message; return null to proceed.
344
+ * SQL Server overrides this to delegate to HandleEntityActions('validate', ...).
345
+ */
346
+ protected OnValidateBeforeSave(_entity: BaseEntity, _user: UserInfo): Promise<string | null>;
347
+ /**
348
+ * Called before the save SQL is executed.
349
+ * SQL Server overrides this to fire before-save entity actions and AI actions.
350
+ */
351
+ protected OnBeforeSaveExecute(_entity: BaseEntity, _user: UserInfo, _options: EntitySaveOptions): Promise<void>;
352
+ /**
353
+ * Called after a successful save (both direct and transaction-callback paths).
354
+ * Intentionally synchronous (fire-and-forget) — SQL Server overrides to dispatch
355
+ * after-save entity actions and AI actions without awaiting.
356
+ */
357
+ protected OnAfterSaveExecute(_entity: BaseEntity, _user: UserInfo, _options: EntitySaveOptions): void;
358
+ /**
359
+ * Called before the delete SQL is executed.
360
+ * SQL Server overrides to fire before-delete entity actions and AI actions.
361
+ */
362
+ protected OnBeforeDeleteExecute(_entity: BaseEntity, _user: UserInfo, _options: EntityDeleteOptions): Promise<void>;
363
+ /**
364
+ * Called after a successful delete.
365
+ * Intentionally synchronous — see OnAfterSaveExecute.
366
+ */
367
+ protected OnAfterDeleteExecute(_entity: BaseEntity, _user: UserInfo, _options: EntityDeleteOptions): void;
368
+ /**
369
+ * Post-processes rows returned by a save/load SQL operation.
370
+ * SQL Server overrides to handle datetimeoffset conversion and field decryption.
371
+ * Default: returns rows unchanged.
372
+ */
373
+ protected PostProcessRows(rows: Record<string, unknown>[], _entityInfo: EntityInfo, _user: UserInfo): Promise<Record<string, unknown>[]>;
374
+ /**
375
+ * Called after a direct (non-transaction) save succeeds, before returning.
376
+ * SQL Server overrides to propagate record-change entries to IS-A sibling branches.
377
+ */
378
+ protected OnSaveCompleted(_entity: BaseEntity, _saveSQLResult: SaveSQLResult, _user: UserInfo, _options: EntitySaveOptions): Promise<void>;
379
+ /**
380
+ * Called before starting a save/delete SQL operation to pause background metadata refresh.
381
+ * SQL Server overrides to set _bAllowRefresh = false.
382
+ */
383
+ protected OnSuspendRefresh(): void;
384
+ /**
385
+ * Called after a save/delete SQL operation completes (success or failure) to resume refresh.
386
+ */
387
+ protected OnResumeRefresh(): void;
388
+ /**
389
+ * Returns provider-specific extra data to attach to a TransactionItem.
390
+ * SQL Server overrides to include { dataSource: this._pool }.
391
+ */
392
+ protected GetTransactionExtraData(_entity: BaseEntity): Record<string, unknown>;
393
+ /**
394
+ * Builds the ExecuteSQLOptions for a Save operation.
395
+ * SQL Server overrides to add connectionSource for IS-A shared transactions.
396
+ */
397
+ protected BuildSaveExecuteOptions(entity: BaseEntity, sqlDetails: SaveSQLResult): ExecuteSQLOptions;
398
+ /**
399
+ * Builds the ExecuteSQLOptions for a Delete operation.
400
+ */
401
+ protected BuildDeleteExecuteOptions(entity: BaseEntity, sqlDetails: DeleteSQLResult): ExecuteSQLOptions;
402
+ /**
403
+ * Validates the result of a delete SQL execution by checking that the returned
404
+ * primary keys match the entity being deleted.
405
+ * SQL Server overrides to handle the multi-result-set case (CASCADE deletes).
406
+ */
407
+ protected ValidateDeleteResult(entity: BaseEntity, rawResult: Record<string, unknown>[], entityResult: BaseEntityResult): boolean;
408
+ /**************************************************************************/
409
+ /**************************************************************************/
410
+ /**************************************************************************/
411
+ /**************************************************************************/
412
+ /**
413
+ * Validates a user-provided SQL clause (WHERE, ORDER BY, etc.) to prevent SQL injection.
414
+ * Checks for forbidden keywords (INSERT, UPDATE, DELETE, EXEC, DROP, UNION, CAST, etc.)
415
+ * and dangerous patterns (comments, semicolons, xp_ prefix).
416
+ * String literals are stripped before validation to avoid false positives.
417
+ *
418
+ * @param clause The SQL clause to validate
419
+ * @returns true if the clause is safe, false if it contains forbidden patterns
420
+ */
421
+ protected ValidateUserProvidedSQLClause(clause: string): boolean;
422
+ /**
423
+ * Checks that the given user has read permissions on the specified entity.
424
+ * Throws if the user lacks CanRead permission.
425
+ *
426
+ * @param entityName The entity to check permissions for
427
+ * @param contextUser The user whose permissions to check
428
+ * @throws Error if contextUser is null, entity is not found, or user lacks read permission
429
+ */
430
+ protected CheckUserReadPermissions(entityName: string, contextUser: UserInfo): void;
431
+ /**
432
+ * Builds and validates an aggregate SQL query from the provided aggregate expressions.
433
+ * Uses SQLExpressionValidator from @memberjunction/global for injection prevention.
434
+ * Uses QuoteIdentifier/QuoteSchemaAndView for dialect-neutral SQL generation.
435
+ *
436
+ * @param aggregates Array of aggregate expressions to validate and build
437
+ * @param entityInfo Entity metadata for field reference validation
438
+ * @param schemaName Schema name for the entity
439
+ * @param baseView Base view name for the entity
440
+ * @param whereSQL WHERE clause to apply (without the WHERE keyword)
441
+ * @returns Object with aggregateSQL string and any validation errors
442
+ */
443
+ protected BuildAggregateSQL(aggregates: {
444
+ expression: string;
445
+ alias?: string;
446
+ }[], entityInfo: EntityInfo, schemaName: string, baseView: string, whereSQL: string): {
447
+ aggregateSQL: string | null;
448
+ validationErrors: AggregateResult[];
449
+ };
450
+ /**
451
+ * Executes an aggregate query and maps results back to the original expressions.
452
+ *
453
+ * @param aggregateSQL The SQL query to execute (from BuildAggregateSQL)
454
+ * @param aggregates Original aggregate expression definitions
455
+ * @param validationErrors Any validation errors from BuildAggregateSQL
456
+ * @param contextUser User context for query execution
457
+ * @returns Array of AggregateResult objects with execution time
458
+ */
459
+ protected ExecuteAggregateQuery(aggregateSQL: string | null, aggregates: {
460
+ expression: string;
461
+ alias?: string;
462
+ }[], validationErrors: AggregateResult[], contextUser?: UserInfo): Promise<{
463
+ results: AggregateResult[];
464
+ executionTime: number;
465
+ }>;
466
+ /**
467
+ * Builds the SQL to retrieve the "name" field value for a specific entity record.
468
+ * Uses QuoteIdentifier/QuoteSchemaAndView for dialect-neutral SQL generation.
469
+ *
470
+ * @param entityName The entity name
471
+ * @param compositeKey The record's primary key
472
+ * @returns The SQL query string, or null if the entity has no name field
473
+ */
474
+ protected BuildEntityRecordNameSQL(entityName: string, compositeKey: CompositeKey): string | null;
475
+ /**
476
+ * Retrieves the display name for a single entity record.
477
+ * Uses BuildEntityRecordNameSQL for dialect-neutral SQL generation.
478
+ */
479
+ protected InternalGetEntityRecordName(entityName: string, compositeKey: CompositeKey, contextUser?: UserInfo): Promise<string>;
480
+ /**
481
+ * Retrieves display names for multiple entity records.
482
+ */
483
+ protected InternalGetEntityRecordNames(info: EntityRecordNameInput[], contextUser?: UserInfo): Promise<EntityRecordNameResult[]>;
484
+ /**************************************************************************/
485
+ /**************************************************************************/
486
+ /**************************************************************************/
487
+ /**************************************************************************/
488
+ /**
489
+ * Saves an entity record — the full orchestration flow shared by all DB providers.
490
+ *
491
+ * 1. Permission & dirty-state checks
492
+ * 2. Validation via OnValidateBeforeSave hook
493
+ * 3. Before-save actions via OnBeforeSaveExecute hook
494
+ * 4. SQL generation via GenerateSaveSQL (abstract, provider-specific)
495
+ * 5. Execute via TransactionGroup or directly
496
+ * 6. After-save actions via OnAfterSaveExecute hook
497
+ * 7. Post-save cleanup via OnSaveCompleted hook (ISA propagation, etc.)
498
+ */
499
+ Save(entity: BaseEntity, user: UserInfo, options: EntitySaveOptions): Promise<{}>;
500
+ /**
501
+ * Deletes an entity record — the full orchestration flow shared by all DB providers.
502
+ *
503
+ * 1. Permission checks & replay handling
504
+ * 2. SQL generation via GenerateDeleteSQL (abstract, provider-specific)
505
+ * 3. Before-delete actions via OnBeforeDeleteExecute hook
506
+ * 4. Execute via TransactionGroup or directly
507
+ * 5. Validate delete result (PK match check)
508
+ * 6. After-delete actions via OnAfterDeleteExecute hook
509
+ */
510
+ Delete(entity: BaseEntity, options: EntityDeleteOptions, user: UserInfo): Promise<boolean>;
511
+ /**************************************************************************/
512
+ /**************************************************************************/
513
+ /**************************************************************************/
514
+ /**************************************************************************/
515
+ /**
516
+ * Creates an audit log record in the MJ: Audit Logs entity.
517
+ * Uses BaseEntity with .Set() calls (no typed entity subclass imports needed - can't use those from MJCore anyway).
518
+ * Callers typically fire-and-forget.
519
+ *
520
+ * @param user The user performing the action
521
+ * @param authorizationName Optional authorization name to look up
522
+ * @param auditLogTypeName The audit log type name (must exist in metadata)
523
+ * @param status 'Success' or 'Failed'
524
+ * @param details Optional details (JSON string, description, etc.)
525
+ * @param entityId The entity ID being audited
526
+ * @param recordId Optional record ID being audited
527
+ * @param auditLogDescription Optional description for the audit log
528
+ * @param saveOptions Save options to pass to the entity Save() call
529
+ * @returns The saved audit log BaseEntity, or null on error
530
+ */
531
+ CreateAuditLogRecord(user: UserInfo, authorizationName: string | null, auditLogTypeName: string, status: string, details: string | null, entityId: string, recordId: string | null, auditLogDescription: string | null, saveOptions: EntitySaveOptions | null): Promise<BaseEntity | null>;
532
+ /**************************************************************************/
533
+ /**************************************************************************/
534
+ /**************************************************************************/
535
+ /**************************************************************************/
536
+ /**
537
+ * Handles entity actions (non-AI) for save, delete, or validate operations.
538
+ * Override in subclasses that have access to EntityActionEngineServer.
539
+ * Default: no-op, returns empty array.
540
+ *
541
+ * @param entity The entity being saved/deleted/validated
542
+ * @param baseType The operation type
543
+ * @param before True for before-hooks, false for after-hooks
544
+ * @param user The acting user
545
+ * @returns Array of action results (empty by default)
546
+ */
547
+ protected HandleEntityActions(_entity: BaseEntity, _baseType: 'save' | 'delete' | 'validate', _before: boolean, _user: UserInfo): Promise<{
548
+ Success: boolean;
549
+ Message?: string;
550
+ }[]>;
551
+ /**
552
+ * Handles AI-specific entity actions for save or delete operations.
553
+ * Override in subclasses that have access to AIEngine.
554
+ * Default: no-op.
555
+ */
556
+ protected HandleEntityAIActions(_entity: BaseEntity, _baseType: 'save' | 'delete', _before: boolean, _user: UserInfo): Promise<void>;
557
+ /**
558
+ * Returns AI actions configured for the given entity and timing.
559
+ * Override in subclasses that have access to AIEngine.
560
+ * Default: returns empty array.
561
+ */
562
+ protected GetEntityAIActions(_entityInfo: EntityInfo, _before: boolean): {
563
+ ID: string;
564
+ EntityID: string;
565
+ TriggerEvent: string;
566
+ AIActionID: string;
567
+ AIModelID: string;
568
+ }[];
569
+ /**************************************************************************/
570
+ /**************************************************************************/
571
+ /**************************************************************************/
572
+ /**************************************************************************/
573
+ /**
574
+ * Returns the stored procedure / function name for a Create or Update operation.
575
+ * Pure metadata lookup — no SQL execution needed.
576
+ * SQL Server uses spCreate/spUpdate naming, PostgreSQL uses the same pattern.
577
+ *
578
+ * @param entity The entity being saved
579
+ * @param bNewRecord True for Create, false for Update
580
+ * @returns The SP/function name
581
+ */
582
+ GetCreateUpdateSPName(entity: BaseEntity, bNewRecord: boolean): string;
583
+ /**************************************************************************/
584
+ /**************************************************************************/
585
+ /**************************************************************************/
586
+ /**************************************************************************/
587
+ /**
588
+ * Logs a record change entry by diffing old/new data and executing
589
+ * provider-specific SQL to insert the record change.
590
+ * Concrete orchestration; SQL generation is delegated to BuildRecordChangeSQL.
591
+ *
592
+ * @param newData The new record data (null for deletes)
593
+ * @param oldData The old record data (null for creates)
594
+ * @param entityName The entity name
595
+ * @param recordID The record ID (CompositeKey string)
596
+ * @param entityInfo The entity metadata
597
+ * @param type The change type
598
+ * @param user The acting user
599
+ */
600
+ protected LogRecordChange(newData: Record<string, unknown> | null, oldData: Record<string, unknown> | null, entityName: string, recordID: string, entityInfo: EntityInfo, type: 'Create' | 'Update' | 'Delete', user: UserInfo): Promise<unknown[] | undefined>;
601
+ /**
602
+ * Builds the SQL (and optional parameters) for inserting a record change entry.
603
+ * Each provider generates its own dialect: SQL Server uses EXEC spCreateRecordChange_Internal,
604
+ * PostgreSQL uses INSERT INTO "RecordChange" with parameterized values.
605
+ *
606
+ * Returns null if there are no changes to log.
607
+ */
608
+ protected abstract BuildRecordChangeSQL(newData: Record<string, unknown> | null, oldData: Record<string, unknown> | null, entityName: string, recordID: string, entityInfo: EntityInfo, type: 'Create' | 'Update' | 'Delete', user: UserInfo): {
609
+ sql: string;
610
+ parameters?: unknown[];
611
+ } | null;
612
+ /**
613
+ * Propagates record change entries to sibling branches of an IS-A hierarchy.
614
+ * Called after saving an entity with AllowMultipleSubtypes (overlapping subtypes).
615
+ * Collects SQL from BuildSiblingRecordChangeSQL for each sibling and executes as a batch.
616
+ *
617
+ * @param parentInfo The parent entity info
618
+ * @param changeData The changes JSON and description
619
+ * @param pkValue The primary key value
620
+ * @param userId The acting user ID
621
+ * @param activeChildEntityName The child entity that initiated the save (to skip)
622
+ * @param extraExecOptions Optional provider-specific execution options (e.g. connectionSource for SQL Server transactions)
623
+ */
624
+ protected PropagateRecordChangesToSiblings(parentInfo: EntityInfo, changeData: {
625
+ changesJSON: string;
626
+ changesDescription: string;
627
+ }, pkValue: string, userId: string, activeChildEntityName: string | undefined, extraExecOptions?: Record<string, unknown>): Promise<void>;
628
+ /**
629
+ * Builds the SQL for a single sibling entity's record change entry in the propagation batch.
630
+ * SQL Server uses FOR JSON PATH + spCreateRecordChange_Internal.
631
+ * PostgreSQL uses json_build_object + INSERT INTO "RecordChange".
632
+ */
633
+ protected abstract BuildSiblingRecordChangeSQL(varName: string, entityInfo: EntityInfo, safeChangesJSON: string, safeChangesDesc: string, safePKValue: string, safeUserId: string): string;
634
+ /**************************************************************************/
635
+ /**************************************************************************/
636
+ /**************************************************************************/
637
+ /**************************************************************************/
638
+ /**
639
+ * Initiates duplicate detection for a list of records.
640
+ * Uses BaseEntity to create a Duplicate Run record.
641
+ * Subclasses may override to provide additional functionality.
642
+ *
643
+ * @param params The duplicate detection request parameters
644
+ * @param contextUser The acting user
645
+ * @returns A response indicating the duplicate detection status
646
+ */
647
+ GetRecordDuplicates(params: PotentialDuplicateRequest, contextUser?: UserInfo): Promise<PotentialDuplicateResponse>;
648
+ /**
649
+ * Merges multiple records into a single surviving record.
650
+ * Full orchestration: transaction, field map update, dependency re-pointing,
651
+ * deletion, and merge logging.
652
+ *
653
+ * @param request The merge request with surviving record and records to merge
654
+ * @param contextUser The acting user
655
+ * @param _options Optional merge options
656
+ * @returns The merge result
657
+ */
658
+ MergeRecords(request: RecordMergeRequest, contextUser?: UserInfo, _options?: EntityMergeOptions): Promise<RecordMergeResult>;
659
+ /**
660
+ * Creates the initial merge log record at the start of a merge operation.
661
+ * Uses BaseEntity with .Set() calls (no typed entity subclass imports available in MJCore).
662
+ */
663
+ protected StartMergeLogging(request: RecordMergeRequest, result: RecordMergeResult, contextUser?: UserInfo): Promise<BaseEntity>;
664
+ /**
665
+ * Finalizes merge logging by updating the log record with completion status
666
+ * and creating deletion detail records.
667
+ * Uses BaseEntity with .Set() calls (no typed entity subclass imports).
668
+ */
669
+ protected CompleteMergeLogging(recordMergeLog: BaseEntity, result: RecordMergeResult, contextUser?: UserInfo): Promise<void>;
670
+ /**************************************************************************/
671
+ /**************************************************************************/
672
+ /**************************************************************************/
673
+ /**************************************************************************/
674
+ /**
675
+ * Runs a report by looking up its SQL definition from vwReports and executing it.
676
+ * Both SQL Server and PostgreSQL share this logic — the only dialect difference
677
+ * is identifier quoting, handled by QuoteIdentifier/QuoteSchemaAndView.
678
+ *
679
+ * @param params Report parameters including ReportID
680
+ * @param contextUser Optional context user for permission/audit purposes
681
+ * @deprecated Reports are no longer supported and will eventually be removed. Interactive Components and Artifacts are replacements
682
+ */
683
+ RunReport(params: RunReportParams, contextUser?: UserInfo): Promise<RunReportResult>;
32
684
  }
33
685
  /**
34
686
  * Configuration options for SQL execution with logging support