@memberjunction/codegen-lib 6.1.0-edge.3 → 6.1.0-edge.4

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 (65) hide show
  1. package/README.md +50 -21
  2. package/dist/Config/config.d.ts +118 -1
  3. package/dist/Config/config.d.ts.map +1 -1
  4. package/dist/Config/config.js +202 -8
  5. package/dist/Config/config.js.map +1 -1
  6. package/dist/Database/__tests__/exclude-schema-permissions.test.d.ts +2 -0
  7. package/dist/Database/__tests__/exclude-schema-permissions.test.d.ts.map +1 -0
  8. package/dist/Database/__tests__/exclude-schema-permissions.test.js +32 -0
  9. package/dist/Database/__tests__/exclude-schema-permissions.test.js.map +1 -0
  10. package/dist/Database/codeGenDatabaseProvider.d.ts +77 -2
  11. package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
  12. package/dist/Database/codeGenDatabaseProvider.js +98 -0
  13. package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
  14. package/dist/Database/heal-schema-params.d.ts +33 -0
  15. package/dist/Database/heal-schema-params.d.ts.map +1 -0
  16. package/dist/Database/heal-schema-params.js +51 -0
  17. package/dist/Database/heal-schema-params.js.map +1 -0
  18. package/dist/Database/manage-metadata.d.ts +9 -0
  19. package/dist/Database/manage-metadata.d.ts.map +1 -1
  20. package/dist/Database/manage-metadata.js +66 -51
  21. package/dist/Database/manage-metadata.js.map +1 -1
  22. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +25 -35
  23. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -1
  24. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +97 -51
  25. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -1
  26. package/dist/Database/providers/postgresql/__tests__/SoftPrimaryKeyIndex.test.d.ts +2 -0
  27. package/dist/Database/providers/postgresql/__tests__/SoftPrimaryKeyIndex.test.d.ts.map +1 -0
  28. package/dist/Database/providers/postgresql/__tests__/SoftPrimaryKeyIndex.test.js +110 -0
  29. package/dist/Database/providers/postgresql/__tests__/SoftPrimaryKeyIndex.test.js.map +1 -0
  30. package/dist/Database/providers/postgresql/metadataSupportObjects.js +78 -10
  31. package/dist/Database/providers/postgresql/metadataSupportObjects.js.map +1 -1
  32. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +12 -0
  33. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -1
  34. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +28 -0
  35. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -1
  36. package/dist/Database/providers/sqlserver/__tests__/SoftPrimaryKeyIndex.test.d.ts +2 -0
  37. package/dist/Database/providers/sqlserver/__tests__/SoftPrimaryKeyIndex.test.d.ts.map +1 -0
  38. package/dist/Database/providers/sqlserver/__tests__/SoftPrimaryKeyIndex.test.js +195 -0
  39. package/dist/Database/providers/sqlserver/__tests__/SoftPrimaryKeyIndex.test.js.map +1 -0
  40. package/dist/Database/schema-filters.d.ts +23 -0
  41. package/dist/Database/schema-filters.d.ts.map +1 -0
  42. package/dist/Database/schema-filters.js +42 -0
  43. package/dist/Database/schema-filters.js.map +1 -0
  44. package/dist/Database/schema-scope.d.ts.map +1 -1
  45. package/dist/Database/schema-scope.js +4 -0
  46. package/dist/Database/schema-scope.js.map +1 -1
  47. package/dist/Database/sql_codegen.d.ts +18 -0
  48. package/dist/Database/sql_codegen.d.ts.map +1 -1
  49. package/dist/Database/sql_codegen.js +92 -41
  50. package/dist/Database/sql_codegen.js.map +1 -1
  51. package/dist/EntityNameScanner/EntityNameScanner.d.ts.map +1 -1
  52. package/dist/EntityNameScanner/EntityNameScanner.js +33 -2
  53. package/dist/EntityNameScanner/EntityNameScanner.js.map +1 -1
  54. package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts +8 -4
  55. package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts.map +1 -1
  56. package/dist/Manifest/GenerateClassRegistrationsManifest.js +63 -20
  57. package/dist/Manifest/GenerateClassRegistrationsManifest.js.map +1 -1
  58. package/dist/Misc/entity_subclasses_codegen.d.ts +22 -5
  59. package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
  60. package/dist/Misc/entity_subclasses_codegen.js +96 -30
  61. package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
  62. package/dist/Misc/system_integrity.d.ts.map +1 -1
  63. package/dist/Misc/system_integrity.js +5 -0
  64. package/dist/Misc/system_integrity.js.map +1 -1
  65. package/package.json +29 -29
@@ -4,7 +4,8 @@ import * as fs from 'fs';
4
4
  import path from 'path';
5
5
  import { SQLUtilityBase } from './sql.js';
6
6
  import { CodeGenDatabaseProvider } from './codeGenDatabaseProvider.js';
7
- import { autoIndexForeignKeys, configInfo, customSqlScripts, mjCoreSchema } from '../Config/config.js';
7
+ import { autoIndexForeignKeys, autoIndexSoftPrimaryKeys, configInfo, customSqlScripts, mjCoreSchema } from '../Config/config.js';
8
+ import { entityInCustomBaseViewRefreshScope, shouldEmitCascadeForRelatedEntity } from './schema-filters.js';
8
9
  import { ManageMetadataBase } from './manage-metadata.js';
9
10
  import { UserCache } from '@memberjunction/generic-database-provider';
10
11
  import { combineFiles, logIf } from '../Misc/util.js';
@@ -16,6 +17,15 @@ export const SPType = {
16
17
  Update: 'Update',
17
18
  Delete: 'Delete',
18
19
  };
20
+ /**
21
+ * Entities whose SchemaName is not in excludeSchemas (case-insensitive).
22
+ * This is the CodeGen run's in-scope set: SQL objects and GRANTs are
23
+ * generated and executed only for these. excludeSchemas means "not this run".
24
+ */
25
+ export function entitiesNotInExcludedSchemas(entities, excludeSchemas) {
26
+ const exclude = new Set(excludeSchemas.map((s) => s.toLowerCase()));
27
+ return entities.filter((e) => !exclude.has(e.SchemaName.toLowerCase()));
28
+ }
19
29
  /**
20
30
  * This class is responsible for generating database level objects like views, stored procedures, permissions, etc. You can sub-class this class to create your own SQL generation logic or implement support for other
21
31
  * databases. The base class implements support for SQL Server. In future versions of MJ, we will break out an abstract base class that has the skeleton of the logic and then the SQL Server version will be a sub-class
@@ -108,9 +118,7 @@ export class SQLCodeGenBase {
108
118
  // Entities are already sorted by name in PostProcessEntityMetadata (see providerBase.ts)
109
119
  const baselineEntities = entities.filter(e => e.IncludeInAPI);
110
120
  // OPTIMIZATION: Use a Set for O(1) lookups instead of O(n) array finds to improve performance
111
- const excludeSchemasSet = new Set(configInfo.excludeSchemas.map(s => s.toLowerCase()));
112
- const includedEntities = baselineEntities.filter(e => !excludeSchemasSet.has(e.SchemaName.toLowerCase())); //only include entities that are NOT in the excludeSchemas list
113
- const excludedEntities = baselineEntities.filter(e => excludeSchemasSet.has(e.SchemaName.toLowerCase())); //only include entities that ARE in the excludeSchemas list in this array
121
+ const includedEntities = entitiesNotInExcludedSchemas(baselineEntities, configInfo.excludeSchemas);
114
122
  // Initialize temp batch files for each schema
115
123
  // These will be populated as SQL is generated and will be used for actual execution
116
124
  const schemas = Array.from(new Set(baselineEntities.map(e => e.SchemaName))).sort();
@@ -140,17 +148,17 @@ export class SQLCodeGenBase {
140
148
  .filter(e => e !== undefined);
141
149
  // Track per-batch success without short-circuiting. Historically,
142
150
  // any single batch failure here returned `false` immediately, which
143
- // skipped the cascade-regen and excluded-perms batches and left
144
- // their entities with no SQL emitted at all. The post-run CRUD
145
- // validator (in runCodeGen.ts) is the authoritative ship-gate now,
146
- // so we want every batch to attempt its work and surface the
147
- // complete picture of what's missing — partial generation is
148
- // strictly more useful than nothing.
151
+ // skipped the cascade-regen batch and left those entities with no
152
+ // SQL emitted at all. The post-run CRUD validator (in runCodeGen.ts)
153
+ // is the authoritative ship-gate now, so we want every in-scope
154
+ // batch to attempt its work and surface the complete picture of
155
+ // what's missing — partial generation is strictly more useful than
156
+ // nothing.
149
157
  let entityGenSuccess = true;
150
158
  // Build a UNION will-regenerate set across every batch we're about
151
- // to launch (main + cascade-regen + excluded-perms). The PG view
152
- // fallback's restoreDependents path uses this to skip restoring
153
- // captured dependents that codegen will recreate later in the run.
159
+ // to launch (main + cascade-regen). The PG view fallback's
160
+ // restoreDependents path uses this to skip restoring captured
161
+ // dependents that codegen will recreate later in the run.
154
162
  // Without the union, batch 1's fallback only sees batch 1's
155
163
  // entities — so a dependent like vwAIAgentRuns (which lives in
156
164
  // batch 2 cascade-regen) is "missing from willRegenerate", the
@@ -162,7 +170,6 @@ export class SQLCodeGenBase {
162
170
  const allBatchEntities = [
163
171
  ...entitiesWithoutCascadeRegeneration,
164
172
  ...entitiesForCascadeRegeneration,
165
- ...excludedEntities,
166
173
  ];
167
174
  const globalWillRegenerate = new Set(allBatchEntities
168
175
  .filter(e => !!e.BaseView)
@@ -196,7 +203,7 @@ export class SQLCodeGenBase {
196
203
  willRegenerate: globalWillRegenerate
197
204
  }); // enable sql logging for NEW entities....
198
205
  if (!genResult.Success) {
199
- logError('Main entity SQL generation batch had failures — continuing with cascade-regen and excluded-perms batches; validator will report any missing routines.');
206
+ logError('Main entity SQL generation batch had failures — continuing with cascade-regen; validator will report any missing routines.');
200
207
  entityGenSuccess = false;
201
208
  }
202
209
  // Generate SQL for cascade delete regenerations in dependency order (sequentially)
@@ -219,23 +226,19 @@ export class SQLCodeGenBase {
219
226
  }
220
227
  genResult.Files.push(...cascadeGenResult.Files);
221
228
  }
222
- // STEP 2(c) - for the excludedEntities, while we don't want to generate SQL, we do want to generate the permissions files for them
223
- updateSpinner(`Generating permissions for ${excludedEntities.length} excluded entities...`);
224
- const genResult2 = await this.generateAndExecuteEntitySQLToSeparateFiles({
225
- pool,
226
- entities: excludedEntities,
227
- directory,
228
- onlyPermissions: true,
229
- skipExecution: true, // skip execution because we execute it all in a giant batch below
230
- batchSize: perEntityBatchSize,
231
- writeFiles: true,
232
- enableSQLLoggingForNewOrModifiedEntities: false, /*don't log this stuff, it is just permissions for excluded entities*/
233
- willRegenerate: globalWillRegenerate
234
- });
235
- if (!genResult2.Success) {
236
- logError('Excluded-entities permissions batch had failures — continuing; validator will report any missing routines.');
237
- entityGenSuccess = false;
238
- }
229
+ // STEP 2(c) used to generate+apply GRANT files for excludeSchemas entities
230
+ // ("we don't want to generate SQL, we do want to generate the permissions").
231
+ // That is wrong once excludeSchemas means "not this CodeGen run":
232
+ //
233
+ // - Open Apps exclude sibling schemas (__mj_BizAppsCommon, …) and __mj.
234
+ // GRANT SELECT ON [__mj_BizAppsCommon].[vwContactMethods] then runs from
235
+ // the orders (or other) repo, pollutes SQL Scripts/generated/<sibling>/,
236
+ // and fails the whole run (exit 1) when that object is missing or lives
237
+ // in a schema the executing login does not resolve.
238
+ // - Core (__mj) permissions already ship in MJ migrations. Refreshing them
239
+ // from an app CodeGen is not this app's job.
240
+ //
241
+ // Permissions are applied in STEP 4 for includedEntities only.
239
242
  if (entityGenSuccess) {
240
243
  succeedSpinner(`Entity generation completed (${(new Date().getTime() - step2StartTime.getTime()) / 1000}s)`);
241
244
  }
@@ -244,15 +247,15 @@ export class SQLCodeGenBase {
244
247
  }
245
248
  // STEP 2(d) now that we've generated the SQL, let's create a combined file in each schema sub-directory for convenience for a DBA
246
249
  startSpinner('Creating combined SQL files...');
247
- const allEntityFiles = this.createCombinedEntitySQLFiles(directory, baselineEntities);
248
- succeedSpinner(`Created combined SQL files for ${allEntityFiles.length} schemas`);
250
+ const allEntityFiles = this.createCombinedEntitySQLFiles(directory, includedEntities);
251
+ succeedSpinner(`Created combined SQL files for ${allEntityFiles.length} file(s)`);
249
252
  // STEP 2(e) ---- FINALLY, we execute SQL in proper dependency order
250
253
  // Use temp batch files (which maintain CodeGen log order) if available, otherwise fall back to combined files
251
254
  startSpinner('Executing entity SQL files...');
252
255
  const step2eStartTime = new Date();
253
256
  let executionSuccess = false;
254
257
  if (useProviderPhasedExecution) {
255
- // Per-entity phased execution already ran during steps 2(b)/(c).
258
+ // Per-entity phased execution already ran during step 2(b).
256
259
  // Skip the bulk file re-execution — replaying the same SQL here
257
260
  // would re-trigger 42P16 without the phased recovery wrapper and
258
261
  // undo the work the phased executor just did.
@@ -363,10 +366,13 @@ export class SQLCodeGenBase {
363
366
  succeedSpinner('No valid entities to regenerate');
364
367
  }
365
368
  }
366
- // STEP 4- Apply permissions, executing all .permissions files
369
+ // STEP 4- Apply permissions for THIS run's schemas only. Excluded-schema
370
+ // GRANT files are not generated (see STEP 2(c) note) and must not be
371
+ // executed: a missing sibling object would fail CodeGen with
372
+ // "Cannot find the object 'vw…'" even though this app's work succeeded.
367
373
  startSpinner('Applying permissions...');
368
374
  const step4StartTime = new Date();
369
- if (!await this.applyPermissions(pool, directory, baselineEntities)) {
375
+ if (!await this.applyPermissions(pool, directory, includedEntities)) {
370
376
  failSpinner('Failed to apply permissions');
371
377
  overallSuccess = false;
372
378
  }
@@ -925,7 +931,8 @@ export class SQLCodeGenBase {
925
931
  return md.Entities.filter(e => modifiedOrNewNames.includes(e.Name) &&
926
932
  !e.BaseViewGenerated &&
927
933
  e.IncludeInAPI &&
928
- !e.VirtualEntity);
934
+ !e.VirtualEntity &&
935
+ entityInCustomBaseViewRefreshScope(e.SchemaName, configInfo.excludeSchemas ?? [], configInfo.includeSchemas));
929
936
  }
930
937
  /**
931
938
  * Builds a combined SQL string of sp_refreshview statements for the given entities.
@@ -951,7 +958,14 @@ export class SQLCodeGenBase {
951
958
  return outer; // fully custom — one view, and it is a standing prerequisite
952
959
  }
953
960
  const inner = this._dbProvider.generateViewRefreshSQL(e.SchemaName, e.GeneratedViewName);
954
- return inner + '\n' + this.guardOnApplicationOwnedView(e, outer);
961
+ const rebind = this._dbProvider.generateLayeredOuterRebindSQL(e);
962
+ const outerSql = outer.trim().length > 0
963
+ ? this.guardOnApplicationOwnedView(e, outer)
964
+ : '';
965
+ const rebindSql = rebind.trim().length > 0
966
+ ? this.guardOnApplicationOwnedView(e, rebind.trim())
967
+ : '';
968
+ return [inner, outerSql, rebindSql].filter((s) => s && s.trim().length > 0).join('\n');
955
969
  })
956
970
  .join('\n');
957
971
  }
@@ -981,9 +995,21 @@ export class SQLCodeGenBase {
981
995
  let baseViewChanged = false;
982
996
  // Indexes for Fkeys for the table (skip for virtual entities — views can't have indexes)
983
997
  if (!options.onlyPermissions && !options.entity.VirtualEntity) {
984
- const shouldGenerateIndexes = autoIndexForeignKeys() || (configInfo.forceRegeneration?.enabled && configInfo.forceRegeneration?.indexes);
998
+ const forceIndexes = !!(configInfo.forceRegeneration?.enabled && configInfo.forceRegeneration?.indexes);
999
+ const shouldGenerateIndexes = autoIndexForeignKeys() || forceIndexes;
985
1000
  const indexSQL = shouldGenerateIndexes ? this.generateIndexesForForeignKeys(options.pool, options.entity) : ''; // generate indexes if auto-indexing is on OR force regeneration is enabled
986
- const s = this.generateSingleEntitySQLFileHeader(options.entity, 'Index for Foreign Keys') + indexSQL;
1001
+ // The soft-PK index rides in the same generated file (and therefore the same
1002
+ // migration log), but on its own setting — see generateIndexForSoftPrimaryKey.
1003
+ // Appended rather than prepended so the file's existing content stays
1004
+ // byte-identical for every entity that has no soft PK, which is nearly all of
1005
+ // them: writeFileIfChanged compares content, and reordering would rewrite the
1006
+ // whole tree for no functional gain.
1007
+ const softPKSQL = (autoIndexSoftPrimaryKeys() || forceIndexes)
1008
+ ? this.generateIndexForSoftPrimaryKey(options.entity)
1009
+ : '';
1010
+ const s = this.generateSingleEntitySQLFileHeader(options.entity, 'Index for Foreign Keys')
1011
+ + indexSQL
1012
+ + (softPKSQL ? (indexSQL ? '\n\n' : '') + softPKSQL : '');
987
1013
  if (options.writeFiles) {
988
1014
  const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('index', options.entity.SchemaName, options.entity.BaseTable, false, true));
989
1015
  this.writeFileIfChanged(filePath, s);
@@ -1324,6 +1350,19 @@ export class SQLCodeGenBase {
1324
1350
  const indexStatements = this._dbProvider.generateForeignKeyIndexes(entity);
1325
1351
  return indexStatements.join('\n\n');
1326
1352
  }
1353
+ /**
1354
+ * Generates the composite index covering the entity's SOFT primary key. Empty for every
1355
+ * entity with a real `PRIMARY KEY` constraint, which is nearly all of them.
1356
+ *
1357
+ * Kept separate from {@link generateIndexesForForeignKeys} and gated by its own setting: a
1358
+ * soft-PK index serves MJ's own per-record existence check on the create path, which is a
1359
+ * different concern from indexing foreign keys for joins and filters, and an operator's
1360
+ * opinion about one is not an opinion about the other.
1361
+ */
1362
+ generateIndexForSoftPrimaryKey(entity) {
1363
+ const indexStatements = this._dbProvider.generateSoftPrimaryKeyIndex(entity);
1364
+ return indexStatements.join('\n\n');
1365
+ }
1327
1366
  /**
1328
1367
  * Detects self-referential foreign keys in an entity (e.g., ParentTaskID pointing back to Task table)
1329
1368
  * Returns array of field info objects representing recursive relationships
@@ -1597,6 +1636,12 @@ export class SQLCodeGenBase {
1597
1636
  const refreshSQL = this._dbProvider.generateViewRefreshSQL(entity.SchemaName, entity.BaseView);
1598
1637
  sOutput += this.guardOnApplicationOwnedView(entity, refreshSQL) + separator;
1599
1638
  }
1639
+ if (entity.HasLayeredBaseView && !entity.VirtualEntity) {
1640
+ const rebindSQL = this._dbProvider.generateLayeredOuterRebindSQL(entity);
1641
+ if (rebindSQL.trim().length > 0) {
1642
+ sOutput += this.guardOnApplicationOwnedView(entity, rebindSQL.trim()) + separator;
1643
+ }
1644
+ }
1600
1645
  return sOutput + permissionsHeader + this.guardOnApplicationOwnedView(entity, permissionsBody) + separator;
1601
1646
  }
1602
1647
  /**
@@ -1949,6 +1994,9 @@ export class SQLCodeGenBase {
1949
1994
  const md = new Metadata(); // global-provider-ok: codegen runs offline against a single provider
1950
1995
  // Find all fields in other entities that are foreign keys to this entity
1951
1996
  for (const e of md.Entities) {
1997
+ if (!shouldEmitCascadeForRelatedEntity(entity.SchemaName, e.SchemaName, configInfo.allowCrossSchemaCascadeDeletes === true)) {
1998
+ continue;
1999
+ }
1952
2000
  for (const ef of e.Fields) {
1953
2001
  if (UUIDsEqual(ef.RelatedEntityID, entity.ID) && ef.IsVirtual === false) {
1954
2002
  const cascadeSql = await this.generateSingleCascadeOperation(entity, e, ef, pool);
@@ -2059,6 +2107,9 @@ export class SQLCodeGenBase {
2059
2107
  const md = new Metadata(); // global-provider-ok: codegen runs offline against a single provider
2060
2108
  // Find all fields in other entities that are foreign keys to this entity
2061
2109
  for (const e of md.Entities) {
2110
+ if (!shouldEmitCascadeForRelatedEntity(entity.SchemaName, e.SchemaName, configInfo.allowCrossSchemaCascadeDeletes === true)) {
2111
+ continue;
2112
+ }
2062
2113
  for (const ef of e.Fields) {
2063
2114
  if (UUIDsEqual(ef.RelatedEntityID, entity.ID) && ef.IsVirtual === false) {
2064
2115
  // Skip self-referential foreign keys (e.g., ParentID pointing to same entity)