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

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 (71) hide show
  1. package/dist/Config/config.d.ts +172 -10
  2. package/dist/Config/config.d.ts.map +1 -1
  3. package/dist/Config/config.js +57 -1
  4. package/dist/Config/config.js.map +1 -1
  5. package/dist/Config/schema-output.d.ts +33 -0
  6. package/dist/Config/schema-output.d.ts.map +1 -0
  7. package/dist/Config/schema-output.js +52 -0
  8. package/dist/Config/schema-output.js.map +1 -0
  9. package/dist/Database/codeGenDatabaseProvider.d.ts +49 -0
  10. package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
  11. package/dist/Database/codeGenDatabaseProvider.js +105 -0
  12. package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
  13. package/dist/Database/exclude-tables.d.ts +26 -0
  14. package/dist/Database/exclude-tables.d.ts.map +1 -0
  15. package/dist/Database/exclude-tables.js +38 -0
  16. package/dist/Database/exclude-tables.js.map +1 -0
  17. package/dist/Database/manage-metadata.d.ts +52 -2
  18. package/dist/Database/manage-metadata.d.ts.map +1 -1
  19. package/dist/Database/manage-metadata.js +95 -14
  20. package/dist/Database/manage-metadata.js.map +1 -1
  21. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +2 -0
  22. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -1
  23. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +10 -0
  24. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -1
  25. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.js +17 -0
  26. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.js.map +1 -1
  27. package/dist/Database/sql_codegen.d.ts +15 -0
  28. package/dist/Database/sql_codegen.d.ts.map +1 -1
  29. package/dist/Database/sql_codegen.js +55 -15
  30. package/dist/Database/sql_codegen.js.map +1 -1
  31. package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts +22 -0
  32. package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts.map +1 -1
  33. package/dist/Manifest/GenerateClassRegistrationsManifest.js +112 -5
  34. package/dist/Manifest/GenerateClassRegistrationsManifest.js.map +1 -1
  35. package/dist/Misc/codegen-reporter.d.ts +3 -0
  36. package/dist/Misc/codegen-reporter.d.ts.map +1 -1
  37. package/dist/Misc/codegen-reporter.js +3 -0
  38. package/dist/Misc/codegen-reporter.js.map +1 -1
  39. package/dist/Misc/emit-stats.d.ts +28 -0
  40. package/dist/Misc/emit-stats.d.ts.map +1 -0
  41. package/dist/Misc/emit-stats.js +43 -0
  42. package/dist/Misc/emit-stats.js.map +1 -0
  43. package/dist/Misc/entity_subclasses_codegen.d.ts +20 -2
  44. package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
  45. package/dist/Misc/entity_subclasses_codegen.js +92 -33
  46. package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
  47. package/dist/Misc/file-write.d.ts +15 -0
  48. package/dist/Misc/file-write.d.ts.map +1 -0
  49. package/dist/Misc/file-write.js +33 -0
  50. package/dist/Misc/file-write.js.map +1 -0
  51. package/dist/Misc/graphql_server_codegen.d.ts +12 -2
  52. package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
  53. package/dist/Misc/graphql_server_codegen.js +61 -12
  54. package/dist/Misc/graphql_server_codegen.js.map +1 -1
  55. package/dist/Misc/runCommand.d.ts +5 -0
  56. package/dist/Misc/runCommand.d.ts.map +1 -1
  57. package/dist/Misc/runCommand.js +43 -17
  58. package/dist/Misc/runCommand.js.map +1 -1
  59. package/dist/Misc/schema-emit.d.ts +123 -0
  60. package/dist/Misc/schema-emit.d.ts.map +1 -0
  61. package/dist/Misc/schema-emit.js +220 -0
  62. package/dist/Misc/schema-emit.js.map +1 -0
  63. package/dist/index.d.ts +6 -0
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +6 -0
  66. package/dist/index.js.map +1 -1
  67. package/dist/runCodeGen.d.ts +20 -2
  68. package/dist/runCodeGen.d.ts.map +1 -1
  69. package/dist/runCodeGen.js +147 -21
  70. package/dist/runCodeGen.js.map +1 -1
  71. package/package.json +29 -29
@@ -283,6 +283,22 @@ export class ManageMetadataBase {
283
283
  static get modifiedEntityList() {
284
284
  return this._modifiedEntityList;
285
285
  }
286
+ static { this._deletedEntitySchemaList = []; }
287
+ /**
288
+ * Schemas that lost an entity during this run.
289
+ *
290
+ * Deletion cannot be reported the way new/modified entities are. Those lists carry entity
291
+ * NAMES, which downstream code resolves to a schema by looking the entity up in live
292
+ * metadata — and a deleted entity is, by then, no longer there to look up. So the schema is
293
+ * captured here at deletion time instead.
294
+ *
295
+ * Without this the schema is never marked dirty, its per-schema file is never rebuilt, and
296
+ * the dead class keeps a live `@RegisterClass` registration on disk. The old monolith could
297
+ * not drift this way: it was rewritten in full every run.
298
+ */
299
+ static get deletedEntitySchemaList() {
300
+ return this._deletedEntitySchemaList;
301
+ }
286
302
  static { this._entitiesRequiringViewRegen = []; }
287
303
  /**
288
304
  * Entities that had late-phase changes requiring their base views to be
@@ -3260,6 +3276,11 @@ export class ManageMetadataBase {
3260
3276
  const sqlDelete = this.dbProvider.callRoutineSQL(mj_core_schema(), 'spDeleteEntityWithCoreDependencies', [`'${e.ID}'`], ['EntityID'], true);
3261
3277
  await this.LogSQLAndExecute(pool, sqlDelete, `SQL text to remove entity ${e.Name}`);
3262
3278
  logStatus(` > Removed metadata for table ${e.SchemaName}.${e.BaseTable}`);
3279
+ // Recorded only after the delete succeeds, so a failed removal (logged below for
3280
+ // an admin to handle) does not mark a schema dirty for an entity still present.
3281
+ if (e.SchemaName && e.SchemaName.trim().length > 0) {
3282
+ ManageMetadataBase._deletedEntitySchemaList.push(e.SchemaName.trim());
3283
+ }
3263
3284
  // next up we need to remove the spCreate, spDelete, spUpdate, BaseView, and FullTextSearchFunction, if provided.
3264
3285
  // We only remoe these artifcacts when they are generated which is info we have in the BaseViewGenerated, spCreateGenerated, etc. fields
3265
3286
  await this.checkDropSQLObject(pool, e.BaseViewGenerated, 'view', e.SchemaName, e.BaseView);
@@ -3897,14 +3918,20 @@ export class ManageMetadataBase {
3897
3918
  if (ag.featureEnabled('EntityDescriptions')) {
3898
3919
  // we have the feature enabled, so let's loop through the new entities and generate descriptions for them
3899
3920
  for (let e of ManageMetadataBase.newEntityList) {
3900
- const dataResult = await this.runQuery(pool, `SELECT * FROM ${this.qs(mj_core_schema(), 'vwEntities')} WHERE Name = '${e}'`);
3921
+ // Every literal in this function is doubled-quote escaped. The description below is
3922
+ // FREE TEXT FROM AN LLM — prose about business entities contains apostrophes as a
3923
+ // matter of course, and one unescaped quote aborts the whole CodeGen run with
3924
+ // "Unclosed quotation mark" (SQL Server) at the very end of an otherwise-complete
3925
+ // pass. '' doubling is correct on both supported dialects.
3926
+ const esc = (s) => s.replace(/'/g, "''");
3927
+ const dataResult = await this.runQuery(pool, `SELECT * FROM ${this.qs(mj_core_schema(), 'vwEntities')} WHERE Name = '${esc(e)}'`);
3901
3928
  const data = dataResult.recordset;
3902
3929
  const fieldsResult = await this.runQuery(pool, `SELECT * FROM ${this.qs(mj_core_schema(), 'vwEntityFields')} WHERE EntityID='${data[0].ID}'`);
3903
3930
  const fields = fieldsResult.recordset;
3904
3931
  // Use new API to generate entity description
3905
3932
  const result = await ag.generateEntityDescription(e, data[0].BaseTable, fields.map((f) => ({ Name: f.Name, Type: f.Type, IsNullable: f.AllowsNull, Description: f.Description })), currentUser);
3906
3933
  if (result?.entityDescription && result.entityDescription.length > 0) {
3907
- const sSQL = `UPDATE ${this.qs(mj_core_schema(), 'Entity')} SET Description = '${result.entityDescription}' WHERE Name = '${e}'`;
3934
+ const sSQL = `UPDATE ${this.qs(mj_core_schema(), 'Entity')} SET Description = '${esc(result.entityDescription)}' WHERE Name = '${esc(e)}'`;
3908
3935
  await this.LogSQLAndExecute(pool, sSQL, `SQL text to update entity description for entity ${e}`);
3909
3936
  }
3910
3937
  else {
@@ -4119,14 +4146,28 @@ export class ManageMetadataBase {
4119
4146
  * following INSERT can use the real BaseView column_id without colliding on
4120
4147
  * UQ_EntityField_EntityID_Sequence. The +100000 band is unique-safe; the
4121
4148
  * subsequent spUpdateExistingEntityFieldsFromSchema rewrite brings every row
4122
- * (parked and new) back to live catalog order. Skip rows already parked so a
4123
- * second pass in the same run does not add 100000 twice.
4149
+ * (parked and new) back to live catalog order.
4150
+ *
4151
+ * Parks ONLY when nothing on the entity is parked yet, which is what makes a second
4152
+ * emission for the same entity in the same run a no-op. `Sequence < 100000` alone does
4153
+ * not achieve that: after the first park the rows below the band are precisely the ones
4154
+ * the first pass just INSERTED at their catalog ordinals, so a second park lifts THOSE
4155
+ * into the band — onto the row the first park moved from the same ordinal, and the
4156
+ * migration dies on UQ_EntityField_EntityID_Sequence with a duplicate at 100000+ordinal.
4157
+ * Reachable whenever an entity gains fields in both CodeGen passes: pass 1 for the real
4158
+ * columns, pass 2 for the denormalized name column a new foreign key introduces.
4124
4159
  */
4125
4160
  parkEntityFieldSequencesSQL(entityID) {
4126
- return `UPDATE ${this.qs(mj_core_schema(), 'EntityField')}
4161
+ const table = this.qs(mj_core_schema(), 'EntityField');
4162
+ return `UPDATE ${table}
4127
4163
  SET ${this.qi('Sequence')} = ${this.qi('Sequence')} + 100000
4128
4164
  WHERE ${this.qi('EntityID')} = '${entityID}'
4129
- AND ${this.qi('Sequence')} < 100000;`;
4165
+ AND ${this.qi('Sequence')} < 100000
4166
+ AND NOT EXISTS (
4167
+ SELECT 1 FROM ${table}
4168
+ WHERE ${this.qi('EntityID')} = '${entityID}'
4169
+ AND ${this.qi('Sequence')} >= 100000
4170
+ );`;
4130
4171
  }
4131
4172
  parseDefaultValue(sqlDefaultValue) {
4132
4173
  if (sqlDefaultValue === null || sqlDefaultValue === undefined) {
@@ -4998,15 +5039,12 @@ export class ManageMetadataBase {
4998
5039
  // process a single new entity
4999
5040
  let newEntityName = await this.createNewEntityName(newEntity, currentUser);
5000
5041
  const newEntityDisplayName = this.createNewEntityDisplayName(newEntity, newEntityName);
5001
- let suffix = '';
5002
- const existingEntity = md.Entities.find(e => e.Name.toLowerCase() === newEntityName.toLowerCase());
5003
- const existingEntityInNewEntityList = ManageMetadataBase.newEntityList.find(e => e === newEntityName); // check the newly created entity list to make sure we didn't create the new entity name along the way in this RUN of CodeGen as it wouldn't yet be in our metadata above
5004
- if (existingEntity || existingEntityInNewEntityList) {
5005
- // the generated name is already in place, so we need another name
5006
- suffix = '__' + newEntity.SchemaName;
5007
- newEntityName = newEntityName + suffix;
5008
- LogError(` >>>> WARNING: Entity name already exists, so using ${newEntityName} instead. If you did not intend for this, please rename the ${newEntity.SchemaName}.${newEntity.TableName} table in the database.`);
5042
+ const { name: uniqueName, suffix: uniqueSuffix } = this.resolveUniqueEntityName(newEntityName, newEntity.SchemaName, md.Entities.map(e => e.Name));
5043
+ if (uniqueName !== newEntityName) {
5044
+ LogError(` >>>> WARNING: Entity name already exists, so using ${uniqueName} instead. If you did not intend for this, please rename the ${newEntity.SchemaName}.${newEntity.TableName} table in the database.`);
5009
5045
  }
5046
+ newEntityName = uniqueName;
5047
+ const suffix = uniqueSuffix;
5010
5048
  const isNewSchema = await this.isSchemaNew(pool, newEntity.SchemaName);
5011
5049
  const newEntityID = this.createNewUUID();
5012
5050
  const sSQLInsert = this.createNewEntityInsertSQL(newEntityID, newEntityName, newEntity, suffix, newEntityDisplayName);
@@ -5562,6 +5600,49 @@ export class ManageMetadataBase {
5562
5600
  }
5563
5601
  return revoked;
5564
5602
  }
5603
+ /**
5604
+ * Pick an entity name that is actually free, disambiguating with the schema name and, if
5605
+ * that is still taken, a counter.
5606
+ *
5607
+ * Entity names are generated from the table name with trailing discriminators stripped, so
5608
+ * distinct tables routinely generate the SAME name — NetSuite's customlist72, customlist74,
5609
+ * customlist160, customlist436, customlist534 and customlist873 all generate "Custom Lists".
5610
+ *
5611
+ * The previous logic appended the schema suffix ONCE and assumed the result was unique. It
5612
+ * is not: the first table took "Custom Lists", and every table after it appended the same
5613
+ * "__netsuite" and produced the identical "Custom Lists__netsuite". The second was a
5614
+ * duplicate-key failure on UQ_Entity_Name and the rest were never created — a silent loss of
5615
+ * 8 entities on one NetSuite tenant, reported only as repeated identical INSERT errors.
5616
+ *
5617
+ * Comparison is case-INSENSITIVE on both sides, matching UQ_Entity_Name's collation. The old
5618
+ * in-run check used an exact `===` while the metadata check beside it lowercased, so two
5619
+ * names differing only in case read as free and then collided on INSERT.
5620
+ *
5621
+ * @param desiredName the generated name to place
5622
+ * @param schemaName schema, used for the first disambiguation step
5623
+ * @param takenNames every name already spoken for — existing metadata AND names claimed
5624
+ * earlier in this run (which are not in metadata yet)
5625
+ * @returns the free name and the suffix used to reach it ('' when the name was already free)
5626
+ */
5627
+ resolveUniqueEntityName(desiredName, schemaName, takenNames) {
5628
+ const taken = new Set([...takenNames, ...ManageMetadataBase.newEntityList].map(n => n.toLowerCase()));
5629
+ const isTaken = (candidate) => taken.has(candidate.toLowerCase());
5630
+ if (!isTaken(desiredName)) {
5631
+ return { name: desiredName, suffix: '' };
5632
+ }
5633
+ let suffix = '__' + schemaName;
5634
+ let name = desiredName + suffix;
5635
+ // Bounded: thousands of tables collapsing to one name is a naming problem no suffix can
5636
+ // fix, and looping forever would hide it. Let the INSERT fail loudly instead.
5637
+ const maxAttempts = 1000;
5638
+ let attempt = 2;
5639
+ while (isTaken(name) && attempt <= maxAttempts) {
5640
+ suffix = `__${schemaName}_${attempt}`;
5641
+ name = desiredName + suffix;
5642
+ attempt++;
5643
+ }
5644
+ return { name, suffix };
5645
+ }
5565
5646
  createNewEntityInsertSQL(newEntityUUID, newEntityName, newEntity, newEntitySuffix, newEntityDisplayName) {
5566
5647
  const newEntityDefaults = configInfo.newEntityDefaults;
5567
5648
  const newEntityDescriptionEscaped = newEntity.EntityDescription ? `'${newEntity.EntityDescription.replace(/'/g, "''")}'` : null;