@memberjunction/codegen-lib 1.0.6 → 1.0.7

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 (58) hide show
  1. package/dist/advanced_generation.d.ts +3 -0
  2. package/dist/advanced_generation.d.ts.map +1 -1
  3. package/dist/advanced_generation.js +39 -26
  4. package/dist/advanced_generation.js.map +1 -1
  5. package/dist/angular_client_codegen.d.ts +45 -2
  6. package/dist/angular_client_codegen.d.ts.map +1 -1
  7. package/dist/angular_client_codegen.js +423 -399
  8. package/dist/angular_client_codegen.js.map +1 -1
  9. package/dist/createNewUser.d.ts +6 -1
  10. package/dist/createNewUser.d.ts.map +1 -1
  11. package/dist/createNewUser.js +68 -53
  12. package/dist/createNewUser.js.map +1 -1
  13. package/dist/dbSchema.d.ts +10 -3
  14. package/dist/dbSchema.d.ts.map +1 -1
  15. package/dist/dbSchema.js +144 -130
  16. package/dist/dbSchema.js.map +1 -1
  17. package/dist/entity_subclasses_codegen.d.ts +11 -6
  18. package/dist/entity_subclasses_codegen.d.ts.map +1 -1
  19. package/dist/entity_subclasses_codegen.js +144 -131
  20. package/dist/entity_subclasses_codegen.js.map +1 -1
  21. package/dist/graphql_server_codegen.d.ts +28 -5
  22. package/dist/graphql_server_codegen.d.ts.map +1 -1
  23. package/dist/graphql_server_codegen.js +552 -531
  24. package/dist/graphql_server_codegen.js.map +1 -1
  25. package/dist/logging.d.ts +20 -0
  26. package/dist/logging.d.ts.map +1 -1
  27. package/dist/logging.js +61 -26
  28. package/dist/logging.js.map +1 -1
  29. package/dist/manageMetadata.d.ts +128 -7
  30. package/dist/manageMetadata.d.ts.map +1 -1
  31. package/dist/manageMetadata.js +992 -898
  32. package/dist/manageMetadata.js.map +1 -1
  33. package/dist/runCodeGen.d.ts +16 -0
  34. package/dist/runCodeGen.d.ts.map +1 -1
  35. package/dist/runCodeGen.js +185 -140
  36. package/dist/runCodeGen.js.map +1 -1
  37. package/dist/runCommand.d.ts +7 -2
  38. package/dist/runCommand.d.ts.map +1 -1
  39. package/dist/runCommand.js +104 -90
  40. package/dist/runCommand.js.map +1 -1
  41. package/dist/sql.d.ts +21 -16
  42. package/dist/sql.d.ts.map +1 -1
  43. package/dist/sql.js +98 -87
  44. package/dist/sql.js.map +1 -1
  45. package/dist/sql_codegen.d.ts +56 -13
  46. package/dist/sql_codegen.d.ts.map +1 -1
  47. package/dist/sql_codegen.js +836 -795
  48. package/dist/sql_codegen.js.map +1 -1
  49. package/dist/util.js +0 -2
  50. package/dist/util.js.map +1 -1
  51. package/package.json +32 -32
  52. package/readme.md +2 -2
  53. package/dist/graphql_client_codegen.d.ts +0 -4
  54. package/dist/graphql_client_codegen.js +0 -161
  55. package/dist/graphql_client_codegen.js.map +0 -1
  56. package/dist/react_client_codegen.d.ts +0 -4
  57. package/dist/react_client_codegen.js +0 -147
  58. package/dist/react_client_codegen.js.map +0 -1
@@ -1,247 +1,805 @@
1
1
  "use strict";
2
- /* Steps in this process are:
3
- 1) Create New Entity Records from new tables
4
- 2)
5
-
6
- */
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var ManageMetadataBase_1;
7
9
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.updateEntityFieldRelatedEntityNameFieldMap = exports.manageEntityFields = exports.manageManyToManyEntityRelationships = exports.manageOneToManyEntityRelationships = exports.manageEntityRelationships = exports.manageMetadata = exports.newEntityList = void 0;
10
+ exports.ManageMetadataBase = void 0;
9
11
  const config_1 = require("./config");
10
12
  const core_1 = require("@memberjunction/core");
11
13
  const logging_1 = require("./logging");
12
14
  const sql_1 = require("./sql");
13
15
  const advanced_generation_1 = require("./advanced_generation");
14
- exports.newEntityList = [];
15
- async function manageMetadata(ds) {
16
- const md = new core_1.Metadata();
17
- const excludeSchemas = config_1.configInfo.excludeSchemas ? config_1.configInfo.excludeSchemas : [];
18
- let bSuccess = true;
19
- if (!await createNewEntities(ds)) {
20
- (0, logging_1.logError)('Error creating new entities');
21
- bSuccess = false;
22
- }
23
- if (!await updateExistingEntitiesFromSchema(ds, excludeSchemas)) {
24
- (0, logging_1.logError)('Error updating existing entities');
25
- bSuccess = false;
26
- }
27
- if (!await (0, sql_1.recompileAllBaseViews)(ds, excludeSchemas, true)) {
28
- (0, logging_1.logError)('Warning: Non-Fatal error recompiling base views');
29
- // many times the former versions of base views will NOT succesfully recompile, so don't consider that scenario to be a
30
- // failure for this entire function
31
- }
32
- if (!await manageEntityFields(ds, excludeSchemas)) {
33
- (0, logging_1.logError)('Error managing entity fields');
34
- bSuccess = false;
35
- }
36
- if (!await manageEntityRelationships(ds, excludeSchemas, md)) {
37
- (0, logging_1.logError)('Error managing entity relationships');
38
- bSuccess = false;
39
- }
40
- if (exports.newEntityList.length > 0) {
41
- await generateNewEntityDescriptions(ds, md); // don't pass excludeSchemas becuase by definition this is the NEW entities we created
42
- }
43
- // if (! await manageVirtualEntities(ds)) {
44
- // logError('Error managing virtual entities');
45
- // bSuccess = false;
46
- // }
47
- // now - we need to tell our metadata object to refresh itself
48
- await md.Refresh();
49
- return bSuccess;
50
- }
51
- exports.manageMetadata = manageMetadata;
52
- // COMMENTED OUT FOR NOW - FUTURE FUNCTIONALITY...
53
- // ------------------------------------------------
54
- // export async function manageVirtualEntities(ds: DataSource): Promise<boolean> {
55
- // let bSuccess = true;
56
- // // virtual entities are records defined in the entity metadata and do NOT define a distinct base table
57
- // // but they do specify a base view. We DO NOT generate a base view for a virtual entity, we simply use it to figure
58
- // // out the fields that should be in the entity definition and add/update/delete the entity definition to match what's in the view when this runs
59
- // const sql = 'SELECT * FROM vwEntities WHERE VirtualEntity = 1';
60
- // const virtualEntities = await ds.query(sql);
61
- // if (virtualEntities && virtualEntities.length > 0) {
62
- // // we have 1+ virtual entities, now loop through them and process each one
63
- // for (const ve of virtualEntities) {
64
- // if (! await manageSingleVirtualEntity(ds, ve)) {
65
- // logError(`Error managing virtual entity ${ve.Name}`);
66
- // bSuccess = false;
67
- // }
68
- // }
69
- // }
70
- // return bSuccess;
71
- // }
72
- // async function manageSingleVirtualEntity(ds: DataSource, ve: any): Promise<boolean> {
73
- // try {
74
- // // for a given virtual entity, we need to loop through the fields that exist in the current SQL definition for the view
75
- // // and add/update/delete the entity fields to match what's in the view
76
- // let bSuccess = true;
77
- // const sql = `SELECT * FROM vwSQLColumnsAndEntityFields WHERE EntityID = ${ve.ID}`;
78
- // const veFields = await ds.query(sql);
79
- // if (veFields && veFields.length > 0) {
80
- // // we have 1+ fields, now loop through them and process each one
81
- // // first though, remove any fields that are no longer in the view
82
- // const md = new Metadata();
83
- // const entity = md.Entities.find(e => e.Name === ve.Name);
84
- // if (entity) {
85
- // const removeList = [];
86
- // const fieldsToRemove = entity.Fields.filter(f => !veFields.find(vf => vf.FieldName === f.Name));
87
- // for (const f of fieldsToRemove) {
88
- // removeList.push(f.ID);
89
- // }
90
- // const sqlRemove = `DELETE FROM [${mj_core_schema()}].EntityField WHERE ID IN (${removeList.join(',')})`;
91
- // await ds.query(sqlRemove); // this removes the fields that shouldn't be there anymore
92
- // }
93
- // for (const vef of veFields) {
94
- // if (! await manageSingleVirtualEntityField(ds, ve, vef)) {
95
- // logError(`Error managing virtual entity field ${vef.FieldName} for virtual entity ${ve.Name}`);
96
- // bSuccess = false;
97
- // }
98
- // }
99
- // }
100
- // // finally make sure we update the UpdatedAt field for the entity
101
- // const sqlUpdate = `UPDATE [${mj_core_schema()}].Entity SET UpdatedAt=GETDATE() WHERE ID = ${ve.ID}`;
102
- // await ds.query(sqlUpdate);
103
- // return true;
104
- // }
105
- // catch (e) {
106
- // logError(e);
107
- // return false;
108
- // }
109
- // }
110
- // async function manageSingleVirtualEntityField(ds: DataSource, ve: any, veField: any): Promise<boolean> {
111
- // // this function checks to see if the field exists in the entity definition, and if not, adds it
112
- // // if it exist it updates the entity field to match the view's data type and nullability attributes
113
- // // first, get the entity definition
114
- // const md = new Metadata();
115
- // const entity = md.Entities.find(e => e.Name === ve.Name);
116
- // if (entity) {
117
- // const field = entity.Fields.find(f => f.Name === veField.FieldName);
118
- // if (field) {
119
- // // have a match, so the field exists in the entity definition, now check to see if it needs to be updated
120
- // if (field.Type !== veField.Type || field.AllowsNull !== veField.AllowsNull) {
121
- // // the field needs to be updated, so update it
122
- // const sqlUpdate = `UPDATE [${mj_core_schema()}].EntityField SET Type='${veField.Type}', AllowsNull=${veField.AllowsNull ? 1 : 0}, UpdatedAt=GETDATE() WHERE ID = ${field.ID}`;
123
- // await ds.query(sqlUpdate);
124
- // }
125
- // }
126
- // }
127
- // return true;
128
- // }
129
- async function manageEntityRelationships(ds, excludeSchemas, md) {
130
- let bResult = true;
131
- bResult = bResult && await manageManyToManyEntityRelationships(ds, excludeSchemas);
132
- bResult = bResult && await manageOneToManyEntityRelationships(ds, excludeSchemas, md);
133
- return bResult;
134
- }
135
- exports.manageEntityRelationships = manageEntityRelationships;
136
- async function manageOneToManyEntityRelationships(ds, excludeSchemas, md) {
137
- // the way this works is that we look for entities in our catalog and we look for
138
- // foreign keys in those entities. For example, if we saw an entity called Persons and that entity
139
- // had a foreign key linking to an entity called Organizations via a field called OrganizationID, then we would create a relationship
140
- // record in the EntityRelationship table for that relationships. In that example we would create the
141
- // relationship record with the following values:
142
- // EntityID = ID of Organizations entity
143
- // RelatedEntityID = ID of Persons entity
144
- // RelatedEntityJoinField = OrganizationID
145
- // Type = "One To Many"
146
- // BundleInAPI = 1
147
- // DisplayInForm = 1
148
- // DisplayName = Persons (name of the entity)
149
- try {
150
- // STEP 1 - search for all foreign keys in the vwEntityFields view, we use the RelatedEntityID field to determine our FKs
151
- const sSQL = `SELECT *
152
- FROM ${(0, config_1.mj_core_schema)()}.vwEntityFields
153
- WHERE
154
- RelatedEntityID IS NOT NULL AND
155
- IsVirtual = 0 AND
156
- EntityID NOT IN (SELECT ID FROM ${(0, config_1.mj_core_schema)()}.Entity WHERE SchemaName IN (${excludeSchemas.map(s => `'${s}'`).join(',')}))
157
- ORDER BY RelatedEntityID`;
158
- const entityFields = await ds.query(sSQL);
159
- // now loop through all of our fkey fields
160
- for (const f of entityFields) {
161
- // for each field determine if an existing relationship exists, if not, create it
162
- const sSQLRelationship = `SELECT * FROM ${(0, config_1.mj_core_schema)()}.EntityRelationship WHERE EntityID = ${f.RelatedEntityID} AND RelatedEntityID = ${f.EntityID}`;
163
- const relationships = await ds.query(sSQLRelationship);
164
- if (relationships && relationships.length === 0) {
165
- // no relationship exists, so create it
166
- const e = md.Entities.find(e => e.ID === f.EntityID);
167
- const sSQLInsert = `INSERT INTO ${(0, config_1.mj_core_schema)()}.EntityRelationship (EntityID, RelatedEntityID, RelatedEntityJoinField, Type, BundleInAPI, DisplayInForm, DisplayName)
168
- VALUES (${f.RelatedEntityID}, ${f.EntityID}, '${f.Name}', 'One To Many', 1, 1, '${e.Name}')`;
169
- await ds.query(sSQLInsert);
16
+ const global_1 = require("@memberjunction/global");
17
+ /**
18
+ * Base class for managing metadata within the CodeGen system. This class can be sub-classed to extend/override base class functionality. Make sure to use the RegisterClass decorator from the @memberjunction/global package
19
+ * to properly register your subclass with a priority of 1+ to ensure it gets instantiated.
20
+ */
21
+ let ManageMetadataBase = ManageMetadataBase_1 = class ManageMetadataBase {
22
+ static get newEntityList() {
23
+ return this._newEntityList;
24
+ }
25
+ /**
26
+ * Primary function to manage metadata within the CodeGen system. This function will call a series of sub-functions to manage the metadata.
27
+ * @param ds - the DataSource object to use for querying and updating the database
28
+ * @returns
29
+ */
30
+ async manageMetadata(ds) {
31
+ const md = new core_1.Metadata();
32
+ const excludeSchemas = config_1.configInfo.excludeSchemas ? config_1.configInfo.excludeSchemas : [];
33
+ let bSuccess = true;
34
+ if (!await this.createNewEntities(ds)) {
35
+ (0, logging_1.logError)('Error creating new entities');
36
+ bSuccess = false;
37
+ }
38
+ if (!await this.updateExistingEntitiesFromSchema(ds, excludeSchemas)) {
39
+ (0, logging_1.logError)('Error updating existing entities');
40
+ bSuccess = false;
41
+ }
42
+ const sqlUtility = global_1.MJGlobal.Instance.ClassFactory.CreateInstance(sql_1.SQLUtilityBase);
43
+ if (!await sqlUtility.recompileAllBaseViews(ds, excludeSchemas, true)) {
44
+ (0, logging_1.logError)('Warning: Non-Fatal error recompiling base views');
45
+ // many times the former versions of base views will NOT succesfully recompile, so don't consider that scenario to be a
46
+ // failure for this entire function
47
+ }
48
+ if (!await this.manageEntityFields(ds, excludeSchemas)) {
49
+ (0, logging_1.logError)('Error managing entity fields');
50
+ bSuccess = false;
51
+ }
52
+ if (!await this.manageEntityRelationships(ds, excludeSchemas, md)) {
53
+ (0, logging_1.logError)('Error managing entity relationships');
54
+ bSuccess = false;
55
+ }
56
+ if (ManageMetadataBase_1.newEntityList.length > 0) {
57
+ await this.generateNewEntityDescriptions(ds, md); // don't pass excludeSchemas becuase by definition this is the NEW entities we created
58
+ }
59
+ if (!await this.manageVirtualEntities(ds)) {
60
+ (0, logging_1.logError)('Error managing virtual entities');
61
+ bSuccess = false;
62
+ }
63
+ // now - we need to tell our metadata object to refresh itself
64
+ await md.Refresh();
65
+ return bSuccess;
66
+ }
67
+ // VIRTUAL ENTITY FUNCTIONALITY IS COMMENTED OUT FOR NOW - FUTURE FUNCTIONALITY...
68
+ // ------------------------------------------------
69
+ async manageVirtualEntities(ds) {
70
+ // let bSuccess = true;
71
+ // // virtual entities are records defined in the entity metadata and do NOT define a distinct base table
72
+ // // but they do specify a base view. We DO NOT generate a base view for a virtual entity, we simply use it to figure
73
+ // // out the fields that should be in the entity definition and add/update/delete the entity definition to match what's in the view when this runs
74
+ // const sql = 'SELECT * FROM vwEntities WHERE VirtualEntity = 1';
75
+ // const virtualEntities = await ds.query(sql);
76
+ // if (virtualEntities && virtualEntities.length > 0) {
77
+ // // we have 1+ virtual entities, now loop through them and process each one
78
+ // for (const ve of virtualEntities) {
79
+ // if (! await manageSingleVirtualEntity(ds, ve)) {
80
+ // logError(`Error managing virtual entity ${ve.Name}`);
81
+ // bSuccess = false;
82
+ // }
83
+ // }
84
+ // }
85
+ // return bSuccess;
86
+ return true; // temp until the above is implemented
87
+ }
88
+ async manageSingleVirtualEntity(ds, ve) {
89
+ // try {
90
+ // // for a given virtual entity, we need to loop through the fields that exist in the current SQL definition for the view
91
+ // // and add/update/delete the entity fields to match what's in the view
92
+ // let bSuccess = true;
93
+ // const sql = `SELECT * FROM vwSQLColumnsAndEntityFields WHERE EntityID = ${ve.ID}`;
94
+ // const veFields = await ds.query(sql);
95
+ // if (veFields && veFields.length > 0) {
96
+ // // we have 1+ fields, now loop through them and process each one
97
+ // // first though, remove any fields that are no longer in the view
98
+ // const md = new Metadata();
99
+ // const entity = md.Entities.find(e => e.Name === ve.Name);
100
+ // if (entity) {
101
+ // const removeList = [];
102
+ // const fieldsToRemove = entity.Fields.filter(f => !veFields.find(vf => vf.FieldName === f.Name));
103
+ // for (const f of fieldsToRemove) {
104
+ // removeList.push(f.ID);
105
+ // }
106
+ // const sqlRemove = `DELETE FROM [${mj_core_schema()}].EntityField WHERE ID IN (${removeList.join(',')})`;
107
+ // await ds.query(sqlRemove); // this removes the fields that shouldn't be there anymore
108
+ // }
109
+ // for (const vef of veFields) {
110
+ // if (! await manageSingleVirtualEntityField(ds, ve, vef)) {
111
+ // logError(`Error managing virtual entity field ${vef.FieldName} for virtual entity ${ve.Name}`);
112
+ // bSuccess = false;
113
+ // }
114
+ // }
115
+ // }
116
+ // // finally make sure we update the UpdatedAt field for the entity
117
+ // const sqlUpdate = `UPDATE [${mj_core_schema()}].Entity SET UpdatedAt=GETDATE() WHERE ID = ${ve.ID}`;
118
+ // await ds.query(sqlUpdate);
119
+ // return true;
120
+ // }
121
+ // catch (e) {
122
+ // logError(e);
123
+ // return false;
124
+ // }
125
+ return true; // temp until the above is implemented
126
+ }
127
+ async manageSingleVirtualEntityField(ds, ve, veField) {
128
+ // // this protected checks to see if the field exists in the entity definition, and if not, adds it
129
+ // // if it exist it updates the entity field to match the view's data type and nullability attributes
130
+ // // first, get the entity definition
131
+ // const md = new Metadata();
132
+ // const entity = md.Entities.find(e => e.Name === ve.Name);
133
+ // if (entity) {
134
+ // const field = entity.Fields.find(f => f.Name === veField.FieldName);
135
+ // if (field) {
136
+ // // have a match, so the field exists in the entity definition, now check to see if it needs to be updated
137
+ // if (field.Type !== veField.Type || field.AllowsNull !== veField.AllowsNull) {
138
+ // // the field needs to be updated, so update it
139
+ // const sqlUpdate = `UPDATE [${mj_core_schema()}].EntityField SET Type='${veField.Type}', AllowsNull=${veField.AllowsNull ? 1 : 0}, UpdatedAt=GETDATE() WHERE ID = ${field.ID}`;
140
+ // await ds.query(sqlUpdate);
141
+ // }
142
+ // }
143
+ // }
144
+ // return true;
145
+ return true; // temp until the above is implemented
146
+ }
147
+ /**
148
+ * This method creates and updates relationships in the metadata based on foreign key relationships in the database.
149
+ * @param ds
150
+ * @param excludeSchemas - specify any schemas to exclude here and any relationships to/from the specified schemas will be ignored
151
+ * @param md
152
+ * @returns
153
+ */
154
+ async manageEntityRelationships(ds, excludeSchemas, md) {
155
+ let bResult = true;
156
+ bResult = bResult && await this.manageManyToManyEntityRelationships(ds, excludeSchemas);
157
+ bResult = bResult && await this.manageOneToManyEntityRelationships(ds, excludeSchemas, md);
158
+ return bResult;
159
+ }
160
+ /**
161
+ * Manages 1->M relationships between entities in the metadata based on foreign key relationships in the database.
162
+ * @param ds
163
+ * @param excludeSchemas - specify any schemas to exclude here and any relationships to/from the specified schemas will be ignored
164
+ * @param md
165
+ * @returns
166
+ */
167
+ async manageOneToManyEntityRelationships(ds, excludeSchemas, md) {
168
+ // the way this works is that we look for entities in our catalog and we look for
169
+ // foreign keys in those entities. For example, if we saw an entity called Persons and that entity
170
+ // had a foreign key linking to an entity called Organizations via a field called OrganizationID, then we would create a relationship
171
+ // record in the EntityRelationship table for that relationships. In that example we would create the
172
+ // relationship record with the following values:
173
+ // EntityID = ID of Organizations entity
174
+ // RelatedEntityID = ID of Persons entity
175
+ // RelatedEntityJoinField = OrganizationID
176
+ // Type = "One To Many"
177
+ // BundleInAPI = 1
178
+ // DisplayInForm = 1
179
+ // DisplayName = Persons (name of the entity)
180
+ try {
181
+ // STEP 1 - search for all foreign keys in the vwEntityFields view, we use the RelatedEntityID field to determine our FKs
182
+ const sSQL = `SELECT *
183
+ FROM ${(0, config_1.mj_core_schema)()}.vwEntityFields
184
+ WHERE
185
+ RelatedEntityID IS NOT NULL AND
186
+ IsVirtual = 0 AND
187
+ EntityID NOT IN (SELECT ID FROM ${(0, config_1.mj_core_schema)()}.Entity WHERE SchemaName IN (${excludeSchemas.map(s => `'${s}'`).join(',')}))
188
+ ORDER BY RelatedEntityID`;
189
+ const entityFields = await ds.query(sSQL);
190
+ // now loop through all of our fkey fields
191
+ for (const f of entityFields) {
192
+ // for each field determine if an existing relationship exists, if not, create it
193
+ const sSQLRelationship = `SELECT * FROM ${(0, config_1.mj_core_schema)()}.EntityRelationship WHERE EntityID = ${f.RelatedEntityID} AND RelatedEntityID = ${f.EntityID}`;
194
+ const relationships = await ds.query(sSQLRelationship);
195
+ if (relationships && relationships.length === 0) {
196
+ // no relationship exists, so create it
197
+ const e = md.Entities.find(e => e.ID === f.EntityID);
198
+ const sSQLInsert = `INSERT INTO ${(0, config_1.mj_core_schema)()}.EntityRelationship (EntityID, RelatedEntityID, RelatedEntityJoinField, Type, BundleInAPI, DisplayInForm, DisplayName)
199
+ VALUES (${f.RelatedEntityID}, ${f.EntityID}, '${f.Name}', 'One To Many', 1, 1, '${e.Name}')`;
200
+ await ds.query(sSQLInsert);
201
+ }
170
202
  }
203
+ return true;
204
+ }
205
+ catch (e) {
206
+ (0, logging_1.logError)(e);
207
+ return false;
208
+ }
209
+ }
210
+ /**
211
+ * Manages M->M relationships between entities in the metadata based on foreign key relationships in the database.
212
+ * NOT IMPLEMENTED IN CURRENT VERSION IN BASE CLASS. M->M relationships ARE supported fully, but they are not AUTO generated by this
213
+ * method, instead an administrator must manually create these relationships in the metadata.
214
+ * @param ds
215
+ * @param excludeSchemas
216
+ * @returns
217
+ */
218
+ async manageManyToManyEntityRelationships(ds, excludeSchemas) {
219
+ return true; // not implemented for now
220
+ }
221
+ /**
222
+ * Manages the creation, updating and deletion of entity field records in the metadata based on the database schema.
223
+ * @param ds
224
+ * @param excludeSchemas
225
+ * @returns
226
+ */
227
+ async manageEntityFields(ds, excludeSchemas) {
228
+ let bSuccess = true;
229
+ const startTime = new Date();
230
+ if (!await this.deleteUnneededEntityFields(ds, excludeSchemas)) {
231
+ (0, logging_1.logError)('Error deleting unneeded entity fields');
232
+ bSuccess = false;
233
+ }
234
+ (0, logging_1.logStatus)(` Deleted unneeded entity fields in ${(new Date().getTime() - startTime.getTime()) / 1000} seconds`);
235
+ const step2StartTime = new Date();
236
+ if (!await this.updateExistingEntityFieldsFromSchema(ds, excludeSchemas)) {
237
+ (0, logging_1.logError)('Error updating existing entity fields from schema');
238
+ bSuccess = false;
239
+ }
240
+ (0, logging_1.logStatus)(` Updated existing entity fields from schema in ${(new Date().getTime() - step2StartTime.getTime()) / 1000} seconds`);
241
+ const step3StartTime = new Date();
242
+ if (!await this.createNewEntityFieldsFromSchema(ds)) { // has its own internal filtering for exclude schema/table so don't pass in
243
+ (0, logging_1.logError)('Error creating new entity fields from schema');
244
+ bSuccess = false;
245
+ }
246
+ (0, logging_1.logStatus)(` Created new entity fields from schema in ${(new Date().getTime() - step3StartTime.getTime()) / 1000} seconds`);
247
+ const step4StartTime = new Date();
248
+ if (!await this.setDefaultColumnWidthWhereNeeded(ds, excludeSchemas)) {
249
+ (0, logging_1.logError)('Error setting default column width where needed');
250
+ bSuccess = false;
171
251
  }
172
- return true;
173
- }
174
- catch (e) {
175
- (0, logging_1.logError)(e);
176
- return false;
177
- }
178
- }
179
- exports.manageOneToManyEntityRelationships = manageOneToManyEntityRelationships;
180
- async function manageManyToManyEntityRelationships(ds, excludeSchemas) {
181
- return true; // not implemented for now
182
- }
183
- exports.manageManyToManyEntityRelationships = manageManyToManyEntityRelationships;
184
- async function manageEntityFields(ds, excludeSchemas) {
185
- let bSuccess = true;
186
- const startTime = new Date();
187
- if (!await deleteUnneededEntityFields(ds, excludeSchemas)) {
188
- (0, logging_1.logError)('Error deleting unneeded entity fields');
189
- bSuccess = false;
190
- }
191
- (0, logging_1.logStatus)(` Deleted unneeded entity fields in ${(new Date().getTime() - startTime.getTime()) / 1000} seconds`);
192
- const step2StartTime = new Date();
193
- if (!await updateExistingEntityFieldsFromSchema(ds, excludeSchemas)) {
194
- (0, logging_1.logError)('Error updating existing entity fields from schema');
195
- bSuccess = false;
196
- }
197
- (0, logging_1.logStatus)(` Updated existing entity fields from schema in ${(new Date().getTime() - step2StartTime.getTime()) / 1000} seconds`);
198
- const step3StartTime = new Date();
199
- if (!await createNewEntityFieldsFromSchema(ds)) { // has its own internal filtering for exclude schema/table so don't pass in
200
- (0, logging_1.logError)('Error creating new entity fields from schema');
201
- bSuccess = false;
202
- }
203
- (0, logging_1.logStatus)(` Created new entity fields from schema in ${(new Date().getTime() - step3StartTime.getTime()) / 1000} seconds`);
204
- const step4StartTime = new Date();
205
- if (!await setDefaultColumnWidthWhereNeeded(ds, excludeSchemas)) {
206
- (0, logging_1.logError)('Error setting default column width where needed');
207
- bSuccess = false;
208
- }
209
- (0, logging_1.logStatus)(` Set default column width where needed in ${(new Date().getTime() - step4StartTime.getTime()) / 1000} seconds`);
210
- const step5StartTime = new Date();
211
- if (!await updateEntityFieldDisplayNameWhereNull(ds, excludeSchemas)) {
212
- (0, logging_1.logError)('Error updating entity field display name where null');
213
- bSuccess = false;
214
- }
215
- (0, logging_1.logStatus)(` Updated entity field display name where null in ${(new Date().getTime() - step5StartTime.getTime()) / 1000} seconds`);
216
- const step6StartTime = new Date();
217
- if (!await manageEntityFieldValues(ds, excludeSchemas)) {
218
- (0, logging_1.logError)('Error managing entity field values');
219
- bSuccess = false;
220
- }
221
- (0, logging_1.logStatus)(` Managed entity field values in ${(new Date().getTime() - step6StartTime.getTime()) / 1000} seconds`);
222
- (0, logging_1.logStatus)(` Total time to manage entity fields: ${(new Date().getTime() - startTime.getTime()) / 1000} seconds`);
223
- return bSuccess;
224
- }
225
- exports.manageEntityFields = manageEntityFields;
226
- async function generateNewEntityDescriptions(ds, md) {
227
- // for the list of new entities, go through and attempt to generate new entity descriptions
228
- const ag = new advanced_generation_1.AdvancedGeneration();
229
- if (ag.featureEnabled('EntityDescriptions')) {
230
- // we have the feature enabled, so let's loop through the new entities and generate descriptions for them
231
- const llm = ag.LLM;
232
- const prompt = ag.getPrompt('EntityDescriptions');
233
- const systemPrompt = prompt.systemPrompt;
234
- const userMessage = prompt.userMessage + '\n\n';
235
- // now loop through the new entities and generate descriptions for them
236
- for (let e of exports.newEntityList) {
237
- const data = await ds.query(`SELECT * FROM [${(0, config_1.mj_core_schema)()}].vwEntities WHERE Name = '${e}'`);
238
- const fields = await ds.query(`SELECT * FROM [${(0, config_1.mj_core_schema)()}].vwEntityFields WHERE EntityID = ${data[0].ID}`);
239
- const entityUserMessage = userMessage + `Entity Name: ${e},
240
- Base Table: ${data[0].BaseTable},
241
- Schema: ${data[0].SchemaName}.
242
- Fields:
243
- ${fields.map(f => ` ${f.Name}: ${f.Type}`).join('\n')}`;
244
- const result = await llm.ChatCompletion({
252
+ (0, logging_1.logStatus)(` Set default column width where needed in ${(new Date().getTime() - step4StartTime.getTime()) / 1000} seconds`);
253
+ const step5StartTime = new Date();
254
+ if (!await this.updateEntityFieldDisplayNameWhereNull(ds, excludeSchemas)) {
255
+ (0, logging_1.logError)('Error updating entity field display name where null');
256
+ bSuccess = false;
257
+ }
258
+ (0, logging_1.logStatus)(` Updated entity field display name where null in ${(new Date().getTime() - step5StartTime.getTime()) / 1000} seconds`);
259
+ const step6StartTime = new Date();
260
+ if (!await this.manageEntityFieldValues(ds, excludeSchemas)) {
261
+ (0, logging_1.logError)('Error managing entity field values');
262
+ bSuccess = false;
263
+ }
264
+ (0, logging_1.logStatus)(` Managed entity field values in ${(new Date().getTime() - step6StartTime.getTime()) / 1000} seconds`);
265
+ (0, logging_1.logStatus)(` Total time to manage entity fields: ${(new Date().getTime() - startTime.getTime()) / 1000} seconds`);
266
+ return bSuccess;
267
+ }
268
+ /**
269
+ * This method generates descriptions for entities in teh system where there is no existing description. This is an experimental feature and is done using AI. In order for it
270
+ * to be invoked, the EntityDescriptions feature must be enabled in the Advanced Generation configuration.
271
+ * @param ds
272
+ * @param md
273
+ */
274
+ async generateNewEntityDescriptions(ds, md) {
275
+ // for the list of new entities, go through and attempt to generate new entity descriptions
276
+ const ag = new advanced_generation_1.AdvancedGeneration();
277
+ if (ag.featureEnabled('EntityDescriptions')) {
278
+ // we have the feature enabled, so let's loop through the new entities and generate descriptions for them
279
+ const llm = ag.LLM;
280
+ const prompt = ag.getPrompt('EntityDescriptions');
281
+ const systemPrompt = prompt.systemPrompt;
282
+ const userMessage = prompt.userMessage + '\n\n';
283
+ // now loop through the new entities and generate descriptions for them
284
+ for (let e of ManageMetadataBase_1.newEntityList) {
285
+ const data = await ds.query(`SELECT * FROM [${(0, config_1.mj_core_schema)()}].vwEntities WHERE Name = '${e}'`);
286
+ const fields = await ds.query(`SELECT * FROM [${(0, config_1.mj_core_schema)()}].vwEntityFields WHERE EntityID = ${data[0].ID}`);
287
+ const entityUserMessage = userMessage + `Entity Name: ${e},
288
+ Base Table: ${data[0].BaseTable},
289
+ Schema: ${data[0].SchemaName}.
290
+ Fields:
291
+ ${fields.map(f => ` ${f.Name}: ${f.Type}`).join('\n')}`;
292
+ const result = await llm.ChatCompletion({
293
+ model: ag.AIModel,
294
+ messages: [
295
+ {
296
+ role: 'system',
297
+ content: systemPrompt
298
+ },
299
+ {
300
+ role: 'user',
301
+ content: entityUserMessage
302
+ }
303
+ ]
304
+ });
305
+ if (result?.success) {
306
+ const resultText = result?.data.choices[0].message.content;
307
+ try {
308
+ const structuredResult = JSON.parse(resultText);
309
+ if (structuredResult?.entityDescription && structuredResult.entityDescription.length > 0) {
310
+ const ssql = `UPDATE [${(0, config_1.mj_core_schema)()}].Entity SET Description = '${structuredResult.entityDescription}' WHERE Name = '${e}'`;
311
+ await ds.query(ssql);
312
+ }
313
+ else {
314
+ console.warn(' >>> Advanced Generation Error: LLM returned a blank entity description, skipping entity description for entity ' + e);
315
+ }
316
+ }
317
+ catch (e) {
318
+ console.warn(' >>> Advanced Generation Error: LLM returned invalid result, skipping entity description for entity ' + e + '. Result from LLM: ' + resultText, e);
319
+ }
320
+ }
321
+ else {
322
+ console.warn(' >>> Advanced Generation Error: LLM call failed, skipping entity description for entity ' + e);
323
+ }
324
+ }
325
+ }
326
+ }
327
+ /**
328
+ * This method is responsible for generating a Display Name for each field where a display name is not already set. The approach in the base class
329
+ * uses a simple algorithm that looks for case changes in the field name and inserts spaces at those points. It also strips the trailing 'ID' from the field name if it exists.
330
+ * Override this method in a sub-class if you would like to implement a different approach for generating display names.
331
+ * @param ds
332
+ * @param excludeSchemas
333
+ * @returns
334
+ */
335
+ async updateEntityFieldDisplayNameWhereNull(ds, excludeSchemas) {
336
+ try {
337
+ const sql = `SELECT
338
+ ef.ID, ef.Name
339
+ FROM
340
+ [${(0, config_1.mj_core_schema)()}].vwEntityFields ef
341
+ INNER JOIN
342
+ [${(0, config_1.mj_core_schema)()}].vwEntities e
343
+ ON
344
+ ef.EntityID = e.ID
345
+ WHERE
346
+ ef.DisplayName IS NULL AND
347
+ ef.DisplayName <> ef.Name AND
348
+ ef.Name <> \'ID\' AND
349
+ e.SchemaName NOT IN (${excludeSchemas.map(s => `'${s}'`).join(',')})
350
+ `;
351
+ const fields = await ds.query(sql);
352
+ if (fields && fields.length > 0)
353
+ for (const field of fields) {
354
+ const sDisplayName = this.stripTrailingChars(this.convertCamelCaseToHaveSpaces(field.Name), 'ID', true).trim();
355
+ if (sDisplayName.length > 0 && sDisplayName.toLowerCase().trim() !== field.Name.toLowerCase().trim()) {
356
+ const sSQL = `UPDATE [${(0, config_1.mj_core_schema)()}].EntityField SET UpdatedAt=GETDATE(), DisplayName = '${sDisplayName}' WHERE ID = ${field.ID}`;
357
+ await ds.query(sSQL);
358
+ }
359
+ }
360
+ return true;
361
+ }
362
+ catch (e) {
363
+ (0, logging_1.logError)(e);
364
+ return false;
365
+ }
366
+ }
367
+ /**
368
+ * This method updates the DefaultColumnWidth field in the EntityField metadata. The default logic uses a stored procedure called spSetDefaultColumnWidthWhereNeeded
369
+ * which is part of the MJ Core Schema. You can override this method to implement custom logic for setting default column widths. It is NOT recommended to
370
+ * modify the stored procedure in the MJ Core Schema because your changes will be overriden during a future upgrade.
371
+ * @param ds
372
+ * @param excludeSchemas
373
+ * @returns
374
+ */
375
+ async setDefaultColumnWidthWhereNeeded(ds, excludeSchemas) {
376
+ try {
377
+ await ds.query(`EXEC ${(0, config_1.mj_core_schema)()}.spSetDefaultColumnWidthWhereNeeded @ExcludedSchemaNames='${excludeSchemas.join(',')}'`);
378
+ return true;
379
+ }
380
+ catch (e) {
381
+ (0, logging_1.logError)(e);
382
+ return false;
383
+ }
384
+ }
385
+ /**
386
+ * Creates a SQL statement to retrieve all of the pending entity fields that need to be created in the metadata. This method looks for fields that exist in the underlying
387
+ * database but are NOT in the metadata.
388
+ */
389
+ getPendingEntityFieldsSELECTSQL() {
390
+ const sSQL = `WITH NumberedRows AS (
391
+ SELECT
392
+ sf.EntityID,
393
+ sf.Sequence,
394
+ sf.FieldName,
395
+ sf.Description,
396
+ sf.Type,
397
+ sf.Length,
398
+ sf.Precision,
399
+ sf.Scale,
400
+ sf.AllowsNull,
401
+ sf.DefaultValue,
402
+ sf.AutoIncrement,
403
+ IIF(sf.IsVirtual = 1, 0, IIF(sf.FieldName = 'CreatedAt' OR sf.FieldName = 'UpdatedAt' OR sf.FieldName = 'ID', 0, 1)) AllowUpdateAPI,
404
+ sf.IsVirtual,
405
+ re.ID RelatedEntityID,
406
+ fk.referenced_column RelatedEntityFieldName,
407
+ IIF(sf.FieldName = 'Name', 1, 0) IsNameField,
408
+ IsPrimaryKey = CASE
409
+ WHEN pk.ColumnName IS NOT NULL THEN 1
410
+ ELSE 0
411
+ END,
412
+ IsUnique = CASE
413
+ WHEN pk.ColumnName IS NOT NULL THEN 1
414
+ ELSE
415
+ CASE
416
+ WHEN uk.ColumnName IS NOT NULL THEN 1
417
+ ELSE 0
418
+ END
419
+ END,
420
+ ROW_NUMBER() OVER (PARTITION BY sf.EntityID, sf.FieldName ORDER BY (SELECT NULL)) AS rn
421
+ FROM
422
+ [${(0, config_1.mj_core_schema)()}].vwSQLColumnsAndEntityFields sf
423
+ LEFT OUTER JOIN
424
+ [${(0, config_1.mj_core_schema)()}].Entity e
425
+ ON
426
+ sf.EntityID = e.ID
427
+ LEFT OUTER JOIN
428
+ [${(0, config_1.mj_core_schema)()}].vwForeignKeys fk
429
+ ON
430
+ sf.FieldName = fk.[column] AND
431
+ e.BaseTable = fk.[table] AND
432
+ e.SchemaName = fk.[schema_name]
433
+ LEFT OUTER JOIN
434
+ [${(0, config_1.mj_core_schema)()}].Entity re -- Related Entity
435
+ ON
436
+ re.BaseTable = fk.referenced_table AND
437
+ re.SchemaName = fk.[referenced_schema]
438
+ LEFT OUTER JOIN
439
+ [${(0, config_1.mj_core_schema)()}].vwTablePrimaryKeys pk
440
+ ON
441
+ e.BaseTable = pk.TableName AND
442
+ sf.FieldName = pk.ColumnName AND
443
+ e.SchemaName = pk.SchemaName
444
+ LEFT OUTER JOIN
445
+ [${(0, config_1.mj_core_schema)()}].vwTableUniqueKeys uk
446
+ ON
447
+ e.BaseTable = uk.TableName AND
448
+ sf.FieldName = uk.ColumnName AND
449
+ e.SchemaName = uk.SchemaName
450
+ WHERE
451
+ EntityFieldID IS NULL -- only where we have NOT YET CREATED EntityField records\n${this.createExcludeTablesAndSchemasFilter('sf.')}
452
+ )
453
+ SELECT
454
+ *
455
+ FROM
456
+ NumberedRows WHERE rn = 1 -- if someone has two foreign keys with same to/from table and field name this makes sure we only get the field info ONCE
457
+ ORDER BY EntityID, Sequence`;
458
+ return sSQL;
459
+ }
460
+ /**
461
+ * This method builds a SQL Statement that will insert a row into the EntityField table with information about a new field.
462
+ * @param n - the new field
463
+ * @returns
464
+ */
465
+ getPendingEntityFieldINSERTSQL(n) {
466
+ const bDefaultInView = (n.FieldName?.trim().toLowerCase() === 'id' ||
467
+ n.FieldName?.trim().toLowerCase() === 'name' ||
468
+ n.Sequence <= config_1.configInfo.newEntityDefaults?.IncludeFirstNFieldsAsDefaultInView ||
469
+ n.IsNameField ? true : false);
470
+ const escapedDescription = n.Description ? `'${n.Description.replace(/'/g, "''")}'` : 'NULL';
471
+ return `
472
+ INSERT INTO [${(0, config_1.mj_core_schema)()}].EntityField
473
+ (
474
+ EntityID,
475
+ Sequence,
476
+ Name,
477
+ DisplayName,
478
+ Description,
479
+ Type,
480
+ Length,
481
+ Precision,
482
+ Scale,
483
+ AllowsNull,
484
+ DefaultValue,
485
+ AutoIncrement,
486
+ AllowUpdateAPI,
487
+ IsVirtual,
488
+ RelatedEntityID,
489
+ RelatedEntityFieldName,
490
+ IsNameField,
491
+ IncludeInUserSearchAPI,
492
+ IncludeRelatedEntityNameFieldInBaseView,
493
+ DefaultInView,
494
+ IsPrimaryKey,
495
+ IsUnique
496
+ )
497
+ VALUES
498
+ (
499
+ ${n.EntityID},
500
+ ${n.Sequence},
501
+ '${n.FieldName}',
502
+ '${this.convertCamelCaseToHaveSpaces(n.FieldName).trim()}',
503
+ ${escapedDescription},
504
+ '${n.Type}',
505
+ ${n.Length},
506
+ ${n.Precision},
507
+ ${n.Scale},
508
+ ${n.AllowsNull ? 1 : 0},
509
+ '${this.parseDefaultValue(n.DefaultValue)}',
510
+ ${n.AutoIncrement ? 1 : 0},
511
+ ${n.AllowUpdateAPI ? 1 : 0},
512
+ ${n.IsVirtual ? 1 : 0},
513
+ ${n.RelatedEntityID},
514
+ ${n.RelatedEntityFieldName && n.RelatedEntityFieldName.length > 0 ? `'${n.RelatedEntityFieldName}'` : 'NULL'},
515
+ ${n.IsNameField !== null ? n.IsNameField : 0},
516
+ ${n.FieldName === 'ID' || n.IsNameField ? 1 : 0},
517
+ ${n.RelatedEntityID && n.RelatedEntityID > 0 && n.Type.trim().toLowerCase() === 'int' ? 1 : 0},
518
+ ${bDefaultInView ? 1 : 0},
519
+ ${n.IsPrimaryKey},
520
+ ${n.IsUnique}
521
+ )`;
522
+ }
523
+ /**
524
+ * This method takes the stored DEFAULT CONSTRAINT value from the database and parses it to retrieve the actual default value. This is necessary because the default value is
525
+ * sometimes wrapped in parentheses and sometimes wrapped in single quotes. This method removes the wrapping characters and returns the actual default value. Some common raw values
526
+ * that exist in SQL Server include 'getdate()', '(getdate())', 'N''SomeValue''', etc. and this method will remove those wrapping characters to get the actual underlying default value.
527
+ * NOTE: For future versions of MemberJunction where multiple back-end providers could be used, this method will be moved to the Provider architecture so that database-specific versions
528
+ * can be implemented, along with many other aspects of this current codebase.
529
+ * @param sqlDefaultValue
530
+ * @returns
531
+ */
532
+ parseDefaultValue(sqlDefaultValue) {
533
+ let sResult = null;
534
+ if (sqlDefaultValue !== null && sqlDefaultValue !== undefined) {
535
+ if (sqlDefaultValue.startsWith('(') && sqlDefaultValue.endsWith(')'))
536
+ sResult = sqlDefaultValue.substring(1, sqlDefaultValue.length - 1);
537
+ else
538
+ sResult = sqlDefaultValue;
539
+ if (sResult.toUpperCase().startsWith('N\'') && sResult.endsWith('\''))
540
+ sResult = sResult.substring(2, sResult.length - 1);
541
+ if (sResult.startsWith('\'') && sResult.endsWith('\''))
542
+ sResult = sResult.substring(1, sResult.length - 1);
543
+ }
544
+ return sResult;
545
+ }
546
+ async createNewEntityFieldsFromSchema(ds) {
547
+ try {
548
+ const sSQL = this.getPendingEntityFieldsSELECTSQL();
549
+ const newEntityFields = await ds.query(sSQL);
550
+ await ds.transaction(async () => {
551
+ // wrap in a transaction so we get all of it or none of it
552
+ for (let i = 0; i < newEntityFields.length; ++i) {
553
+ const n = newEntityFields[i];
554
+ if (n.EntityID !== null && n.EntityID !== undefined && n.EntityID > 0) {
555
+ // need to check for null entity id = that is because the above query can return candidate Entity Fields but the entities may not have been created if the entities
556
+ // that would have been created violate rules - such as not having an ID column, etc.
557
+ const sSQLInsert = this.getPendingEntityFieldINSERTSQL(n);
558
+ await ds.query(sSQLInsert);
559
+ // if we get here, we're okay, otherwise we have an exception, which we want as it blows up transaction
560
+ }
561
+ }
562
+ });
563
+ return true;
564
+ }
565
+ catch (e) {
566
+ (0, logging_1.logError)(e);
567
+ return false;
568
+ }
569
+ }
570
+ /**
571
+ * This method handles updating entity field related name field maps which is basically the process of finding the related entity field that is the "name" field for the related entity.
572
+ * @param ds
573
+ * @param entityFieldID
574
+ * @param relatedEntityNameFieldMap
575
+ * @returns
576
+ */
577
+ async updateEntityFieldRelatedEntityNameFieldMap(ds, entityFieldID, relatedEntityNameFieldMap) {
578
+ try {
579
+ const sSQL = `EXEC [${(0, config_1.mj_core_schema)()}].spUpdateEntityFieldRelatedEntityNameFieldMap
580
+ @EntityFieldID=${entityFieldID} ,
581
+ @RelatedEntityNameFieldMap='${relatedEntityNameFieldMap}'`;
582
+ await ds.query(sSQL);
583
+ return true;
584
+ }
585
+ catch (e) {
586
+ (0, logging_1.logError)(e);
587
+ return false;
588
+ }
589
+ }
590
+ async updateExistingEntitiesFromSchema(ds, excludeSchemas) {
591
+ try {
592
+ await ds.query(`EXEC [${(0, config_1.mj_core_schema)()}].spUpdateExistingEntitiesFromSchema @ExcludedSchemaNames='${excludeSchemas.join(',')}'`);
593
+ return true;
594
+ }
595
+ catch (e) {
596
+ (0, logging_1.logError)(e);
597
+ return false;
598
+ }
599
+ }
600
+ async updateExistingEntityFieldsFromSchema(ds, excludeSchemas) {
601
+ try {
602
+ await ds.query(`EXEC [${(0, config_1.mj_core_schema)()}].spUpdateExistingEntityFieldsFromSchema @ExcludedSchemaNames='${excludeSchemas.join(',')}'`);
603
+ return true;
604
+ }
605
+ catch (e) {
606
+ (0, logging_1.logError)(e);
607
+ return false;
608
+ }
609
+ }
610
+ async deleteUnneededEntityFields(ds, excludeSchemas) {
611
+ try {
612
+ await ds.query(`EXEC [${(0, config_1.mj_core_schema)()}].spDeleteUnneededEntityFields @ExcludedSchemaNames='${excludeSchemas.join(',')}'`);
613
+ return true;
614
+ }
615
+ catch (e) {
616
+ (0, logging_1.logError)(e);
617
+ return false;
618
+ }
619
+ }
620
+ async manageEntityFieldValues(ds, excludeSchemas) {
621
+ try {
622
+ // here we want to get all of the entity fields that have check constraints attached to them. For each field that has a check constraint, we want to
623
+ // evaluate it to see if it is a simple series of OR statements or not, if it is a simple series of OR statements, we can parse the possible values
624
+ // for the field and sync that up with the EntityFieldValue table. If it is not a simple series of OR statements, we will not be able to parse it and we'll
625
+ // just ignore it.
626
+ const filter = excludeSchemas && excludeSchemas.length > 0 ? ` WHERE SchemaName NOT IN (${excludeSchemas.map(s => `'${s}'`).join(',')})` : '';
627
+ const sSQL = `SELECT * FROM [${(0, config_1.mj_core_schema)()}].vwEntityFieldsWithCheckConstraints${filter}`;
628
+ const result = await ds.query(sSQL);
629
+ // now, for each of the constraints we get back here, loop through and evaluate if they're simple and if they're simple, parse and sync with entity field values for that field
630
+ for (const r of result) {
631
+ if (r.ConstraintDefinition && r.ConstraintDefinition.length > 0) {
632
+ const parsedValues = this.parseCheckConstraintValues(r.ConstraintDefinition, r.ColumnName);
633
+ if (parsedValues) {
634
+ // we have parsed values from the check constraint, so sync them with the entity field values
635
+ await this.syncEntityFieldValues(ds, r.EntityID, r.ColumnName, parsedValues);
636
+ // finally, make sure the ValueListType column within the EntityField table is set to "List" because for check constraints we only allow the values specified in the list.
637
+ await ds.query(`UPDATE [${(0, config_1.mj_core_schema)()}].EntityField SET ValueListType='List' WHERE EntityID=${r.EntityID} AND Name='${r.ColumnName}'`);
638
+ }
639
+ }
640
+ }
641
+ return true;
642
+ }
643
+ catch (e) {
644
+ (0, logging_1.logError)(e);
645
+ return false;
646
+ }
647
+ }
648
+ async syncEntityFieldValues(ds, entityID, entityFieldName, possibleValues) {
649
+ try {
650
+ // first, get a list of all of the existing entity field values for the field already in the database
651
+ const sSQL = `SELECT * FROM [${(0, config_1.mj_core_schema)()}].EntityFieldValue WHERE EntityID=${entityID} AND EntityFieldName = '${entityFieldName}'`;
652
+ const existingValues = await ds.query(sSQL);
653
+ // now, loop through the possible values and add any that are not already in the database
654
+ // Step 1: for any existing value that is NOT in the list of possible Values, delete it
655
+ let numRemoved = 0;
656
+ await ds.transaction(async () => {
657
+ for (const ev of existingValues) {
658
+ if (!possibleValues.find(v => v === ev.Value)) {
659
+ // delete the value from the database
660
+ const sSQLDelete = `DELETE FROM [${(0, config_1.mj_core_schema)()}].EntityFieldValue WHERE ID=${ev.ID}`;
661
+ await ds.query(sSQLDelete);
662
+ numRemoved++;
663
+ }
664
+ }
665
+ // Step 2: for any possible value that is NOT in the list of existing values, add it
666
+ let numAdded = 0;
667
+ for (const v of possibleValues) {
668
+ if (!existingValues.find(ev => ev.Value === v)) {
669
+ // add the value to the database
670
+ const sSQLInsert = `INSERT INTO [${(0, config_1.mj_core_schema)()}].EntityFieldValue
671
+ (EntityID, EntityFieldName, Sequence, Value, Code)
672
+ VALUES
673
+ (${entityID}, '${entityFieldName}', ${1 + possibleValues.indexOf(v)}, '${v}', '${v}')`;
674
+ await ds.query(sSQLInsert);
675
+ numAdded++;
676
+ }
677
+ }
678
+ // Step 3: finally, for the existing values that are in the list of possible values, update the sequence to match the order in the possible values list
679
+ let numUpdated = 0;
680
+ for (const v of possibleValues) {
681
+ const ev = existingValues.find(ev => ev.Value === v);
682
+ if (ev) {
683
+ // update the sequence to match the order in the possible values list
684
+ const sSQLUpdate = `UPDATE [${(0, config_1.mj_core_schema)()}].EntityFieldValue SET Sequence=${1 + possibleValues.indexOf(v)} WHERE ID=${ev.ID}`;
685
+ await ds.query(sSQLUpdate);
686
+ numUpdated++;
687
+ }
688
+ }
689
+ });
690
+ return true;
691
+ }
692
+ catch (e) {
693
+ (0, logging_1.logError)(e);
694
+ return false;
695
+ }
696
+ }
697
+ parseCheckConstraintValues(constraintDefinition, fieldName) {
698
+ // This regex checks for the overall structure including field name and 'OR' sequences
699
+ // an example of a valid constraint definition would be: ([FieldName]='Value1' OR [FieldName]='Value2' OR [FieldName]='Value3')
700
+ // like: ([AutoRunIntervalUnits]='Years' OR [AutoRunIntervalUnits]='Months' OR [AutoRunIntervalUnits]='Weeks' OR [AutoRunIntervalUnits]='Days' OR [AutoRunIntervalUnits]='Hours' OR [AutoRunIntervalUnits]='Minutes')
701
+ // Note: Assuming fieldName does not contain regex special characters; otherwise, it needs to be escaped as well.
702
+ const structureRegex = new RegExp(`^\\(\\[${fieldName}\\]='[^']+'(?: OR \\[${fieldName}\\]='[^']+?')+\\)$`);
703
+ if (!structureRegex.test(constraintDefinition)) {
704
+ console.log('Constraint does not match the simple OR condition pattern or field name does not match.');
705
+ return null;
706
+ }
707
+ // Regular expression to match the values within the single quotes specifically for the field
708
+ const valueRegex = new RegExp(`\\[${fieldName}\\]='([^']+)\'`, 'g');
709
+ let match;
710
+ const possibleValues = [];
711
+ // Use regex to find matches and extract the values
712
+ while ((match = valueRegex.exec(constraintDefinition)) !== null) {
713
+ // This is necessary to avoid infinite loops with zero-width matches
714
+ if (match.index === valueRegex.lastIndex) {
715
+ valueRegex.lastIndex++;
716
+ }
717
+ // The first captured group contains the value
718
+ if (match[1]) {
719
+ possibleValues.push(match[1]);
720
+ }
721
+ }
722
+ return possibleValues;
723
+ }
724
+ createExcludeTablesAndSchemasFilter(fieldPrefix) {
725
+ let sExcludeTables = '';
726
+ let sExcludeSchemas = '';
727
+ if (config_1.configInfo.excludeTables) {
728
+ for (let i = 0; i < config_1.configInfo.excludeTables.length; ++i) {
729
+ const t = config_1.configInfo.excludeTables[i];
730
+ sExcludeTables += (sExcludeTables.length > 0 ? ' AND ' : '') +
731
+ (t.schema.indexOf('%') > -1 ? ` NOT ( ${fieldPrefix}SchemaName LIKE '${t.schema}'` :
732
+ ` NOT ( ${fieldPrefix}SchemaName = '${t.schema}'`);
733
+ sExcludeTables += (t.table.indexOf('%') > -1 ? ` AND ${fieldPrefix}TableName LIKE '${t.table}') ` :
734
+ ` AND ${fieldPrefix}TableName = '${t.table}') `);
735
+ }
736
+ }
737
+ if (config_1.configInfo.excludeSchemas) {
738
+ for (let i = 0; i < config_1.configInfo.excludeSchemas.length; ++i) {
739
+ const s = config_1.configInfo.excludeSchemas[i];
740
+ sExcludeSchemas += (sExcludeSchemas.length > 0 ? ' AND ' : '') +
741
+ (s.indexOf('%') > -1 ? `${fieldPrefix}SchemaName NOT LIKE '${s}'` : `${fieldPrefix}SchemaName <> '${s}'`);
742
+ }
743
+ }
744
+ const sWhere = (sExcludeTables.length > 0 || sExcludeSchemas.length > 0 ? ` AND ` : '') +
745
+ (sExcludeTables.length > 0 ? `(${sExcludeTables})` : '') +
746
+ (sExcludeSchemas.length > 0 ? (sExcludeTables.length > 0 ? ` AND ` : ``) + '(' + sExcludeSchemas + ')' : '');
747
+ return sWhere;
748
+ }
749
+ async createNewEntities(ds) {
750
+ try {
751
+ const sSQL = `SELECT * FROM [${(0, config_1.mj_core_schema)()}].vwSQLTablesAndEntities WHERE EntityID IS NULL ` + this.createExcludeTablesAndSchemasFilter('');
752
+ const newEntities = await ds.query(sSQL);
753
+ if (newEntities && newEntities.length > 0) {
754
+ const md = new core_1.Metadata();
755
+ await ds.transaction(async () => {
756
+ // wrap in a transaction so we get all of it or none of it
757
+ for (let i = 0; i < newEntities.length; ++i) {
758
+ // process each of the new entities
759
+ await this.createNewEntity(ds, newEntities[i], md);
760
+ }
761
+ });
762
+ if (ManageMetadataBase_1.newEntityList.length > 0) {
763
+ // only do this if we actually created new entities
764
+ (0, core_1.LogStatus)(` Done creating entities, refreshing metadata to reflect new entities...`);
765
+ await md.Refresh(); // refresh now since we've added some new entities
766
+ }
767
+ }
768
+ return true; // if we get here, we succeeded
769
+ }
770
+ catch (e) {
771
+ (0, core_1.LogError)(e);
772
+ return false;
773
+ }
774
+ }
775
+ async shouldCreateNewEntity(ds, newEntity) {
776
+ // validate that the new entity meets our criteria for creation
777
+ // criteria:
778
+ // 1) entity has a field that is a primary key
779
+ // validate all of these factors by getting the sql from SQL Server and check the result, if failure, shouldCreate=false and generate validation message, otherwise return empty validation message and true for shouldCreate.
780
+ const query = `EXEC ${core_1.Metadata.Provider.ConfigData.MJCoreSchemaName}.spGetPrimaryKeyForTable @TableName='${newEntity.TableName}', @SchemaName='${newEntity.SchemaName}'`;
781
+ try {
782
+ const result = await ds.query(query);
783
+ if (result.length === 0) {
784
+ return { shouldCreate: false, validationMessage: "No primary key found" };
785
+ }
786
+ return { shouldCreate: true, validationMessage: '' };
787
+ }
788
+ catch (error) {
789
+ const errorMsg = 'Error validating new entity for table:' + newEntity?.TableName;
790
+ console.error(errorMsg, error);
791
+ return { shouldCreate: false, validationMessage: errorMsg };
792
+ }
793
+ }
794
+ async createNewEntityName(newEntity) {
795
+ const ag = new advanced_generation_1.AdvancedGeneration();
796
+ if (ag.featureEnabled('EntityNames')) {
797
+ // get the LLM for this entity
798
+ const chat = ag.LLM;
799
+ const prompt = ag.getPrompt('EntityNames');
800
+ const systemPrompt = ag.fillTemplate(prompt.systemPrompt, newEntity);
801
+ const userMessage = ag.fillTemplate(prompt.userMessage, newEntity);
802
+ const result = await chat.ChatCompletion({
245
803
  model: ag.AIModel,
246
804
  messages: [
247
805
  {
@@ -250,7 +808,7 @@ async function generateNewEntityDescriptions(ds, md) {
250
808
  },
251
809
  {
252
810
  role: 'user',
253
- content: entityUserMessage
811
+ content: userMessage
254
812
  }
255
813
  ]
256
814
  });
@@ -258,695 +816,231 @@ async function generateNewEntityDescriptions(ds, md) {
258
816
  const resultText = result?.data.choices[0].message.content;
259
817
  try {
260
818
  const structuredResult = JSON.parse(resultText);
261
- if (structuredResult?.entityDescription && structuredResult.entityDescription.length > 0) {
262
- const ssql = `UPDATE [${(0, config_1.mj_core_schema)()}].Entity SET Description = '${structuredResult.entityDescription}' WHERE Name = '${e}'`;
263
- await ds.query(ssql);
819
+ if (structuredResult?.entityName) {
820
+ return structuredResult.entityName;
264
821
  }
265
822
  else {
266
- console.warn(' >>> Advanced Generation Error: LLM returned a blank entity description, skipping entity description for entity ' + e);
823
+ console.warn(' >>> Advanced Generation Error: LLM returned a blank entity name, falling back to simple generated entity name');
824
+ return this.simpleNewEntityName(newEntity.TableName);
267
825
  }
268
826
  }
269
827
  catch (e) {
270
- console.warn(' >>> Advanced Generation Error: LLM returned invalid result, skipping entity description for entity ' + e + '. Result from LLM: ' + resultText, e);
828
+ console.warn(' >>> Advanced Generation Error: LLM returned invalid result, falling back to simple generated entity name. Result from LLM: ' + resultText, e);
829
+ return this.simpleNewEntityName(newEntity.TableName);
271
830
  }
272
831
  }
273
832
  else {
274
- console.warn(' >>> Advanced Generation Error: LLM call failed, skipping entity description for entity ' + e);
833
+ console.warn(' >>> Advanced Generation Error: LLM call failed, falling back to simple generated entity name.');
834
+ return this.simpleNewEntityName(newEntity.TableName);
275
835
  }
276
836
  }
277
- }
278
- }
279
- async function updateEntityFieldDisplayNameWhereNull(ds, excludeSchemas) {
280
- try {
281
- const sql = `SELECT
282
- ef.ID, ef.Name
283
- FROM
284
- [${(0, config_1.mj_core_schema)()}].vwEntityFields ef
285
- INNER JOIN
286
- [${(0, config_1.mj_core_schema)()}].vwEntities e
287
- ON
288
- ef.EntityID = e.ID
289
- WHERE
290
- ef.DisplayName IS NULL AND
291
- ef.DisplayName <> ef.Name AND
292
- ef.Name <> \'ID\' AND
293
- e.SchemaName NOT IN (${excludeSchemas.map(s => `'${s}'`).join(',')})
294
- `;
295
- const fields = await ds.query(sql);
296
- if (fields && fields.length > 0)
297
- for (const field of fields) {
298
- const sDisplayName = stripTrailingChars(convertCamelCaseToHaveSpaces(field.Name), 'ID', true).trim();
299
- if (sDisplayName.length > 0 && sDisplayName.toLowerCase().trim() !== field.Name.toLowerCase().trim()) {
300
- const sSQL = `UPDATE [${(0, config_1.mj_core_schema)()}].EntityField SET UpdatedAt=GETDATE(), DisplayName = '${sDisplayName}' WHERE ID = ${field.ID}`;
301
- await ds.query(sSQL);
302
- }
303
- }
304
- return true;
305
- }
306
- catch (e) {
307
- (0, logging_1.logError)(e);
308
- return false;
309
- }
310
- }
311
- async function setDefaultColumnWidthWhereNeeded(ds, excludeSchemas) {
312
- try {
313
- await ds.query(`EXEC ${(0, config_1.mj_core_schema)()}.spSetDefaultColumnWidthWhereNeeded @ExcludedSchemaNames='${excludeSchemas.join(',')}'`);
314
- return true;
315
- }
316
- catch (e) {
317
- (0, logging_1.logError)(e);
318
- return false;
319
- }
320
- }
321
- function getPendingEntityFieldsSELECTSQL() {
322
- const sSQL = `WITH NumberedRows AS (
323
- SELECT
324
- sf.EntityID,
325
- sf.Sequence,
326
- sf.FieldName,
327
- sf.Description,
328
- sf.Type,
329
- sf.Length,
330
- sf.Precision,
331
- sf.Scale,
332
- sf.AllowsNull,
333
- sf.DefaultValue,
334
- sf.AutoIncrement,
335
- IIF(sf.IsVirtual = 1, 0, IIF(sf.FieldName = 'CreatedAt' OR sf.FieldName = 'UpdatedAt' OR sf.FieldName = 'ID', 0, 1)) AllowUpdateAPI,
336
- sf.IsVirtual,
337
- re.ID RelatedEntityID,
338
- fk.referenced_column RelatedEntityFieldName,
339
- IIF(sf.FieldName = 'Name', 1, 0) IsNameField,
340
- IsPrimaryKey = CASE
341
- WHEN pk.ColumnName IS NOT NULL THEN 1
342
- ELSE 0
343
- END,
344
- IsUnique = CASE
345
- WHEN pk.ColumnName IS NOT NULL THEN 1
346
- ELSE
347
- CASE
348
- WHEN uk.ColumnName IS NOT NULL THEN 1
349
- ELSE 0
350
- END
351
- END,
352
- ROW_NUMBER() OVER (PARTITION BY sf.EntityID, sf.FieldName ORDER BY (SELECT NULL)) AS rn
353
- FROM
354
- [${(0, config_1.mj_core_schema)()}].vwSQLColumnsAndEntityFields sf
355
- LEFT OUTER JOIN
356
- [${(0, config_1.mj_core_schema)()}].Entity e
357
- ON
358
- sf.EntityID = e.ID
359
- LEFT OUTER JOIN
360
- [${(0, config_1.mj_core_schema)()}].vwForeignKeys fk
361
- ON
362
- sf.FieldName = fk.[column] AND
363
- e.BaseTable = fk.[table] AND
364
- e.SchemaName = fk.[schema_name]
365
- LEFT OUTER JOIN
366
- [${(0, config_1.mj_core_schema)()}].Entity re -- Related Entity
367
- ON
368
- re.BaseTable = fk.referenced_table AND
369
- re.SchemaName = fk.[referenced_schema]
370
- LEFT OUTER JOIN
371
- [${(0, config_1.mj_core_schema)()}].vwTablePrimaryKeys pk
372
- ON
373
- e.BaseTable = pk.TableName AND
374
- sf.FieldName = pk.ColumnName AND
375
- e.SchemaName = pk.SchemaName
376
- LEFT OUTER JOIN
377
- [${(0, config_1.mj_core_schema)()}].vwTableUniqueKeys uk
378
- ON
379
- e.BaseTable = uk.TableName AND
380
- sf.FieldName = uk.ColumnName AND
381
- e.SchemaName = uk.SchemaName
382
- WHERE
383
- EntityFieldID IS NULL -- only where we have NOT YET CREATED EntityField records\n${createExcludeTablesAndSchemasFilter('sf.')}
384
- )
385
- SELECT
386
- *
387
- FROM
388
- NumberedRows WHERE rn = 1 -- if someone has two foreign keys with same to/from table and field name this makes sure we only get the field info ONCE
389
- ORDER BY EntityID, Sequence`;
390
- return sSQL;
391
- }
392
- function getPendingEntityFieldINSERTSQL(n) {
393
- const bDefaultInView = (n.FieldName?.trim().toLowerCase() === 'id' ||
394
- n.FieldName?.trim().toLowerCase() === 'name' ||
395
- n.Sequence <= config_1.configInfo.newEntityDefaults?.IncludeFirstNFieldsAsDefaultInView ||
396
- n.IsNameField ? true : false);
397
- const escapedDescription = n.Description ? `'${n.Description.replace(/'/g, "''")}'` : 'NULL';
398
- return `
399
- INSERT INTO [${(0, config_1.mj_core_schema)()}].EntityField
400
- (
401
- EntityID,
402
- Sequence,
403
- Name,
404
- DisplayName,
405
- Description,
406
- Type,
407
- Length,
408
- Precision,
409
- Scale,
410
- AllowsNull,
411
- DefaultValue,
412
- AutoIncrement,
413
- AllowUpdateAPI,
414
- IsVirtual,
415
- RelatedEntityID,
416
- RelatedEntityFieldName,
417
- IsNameField,
418
- IncludeInUserSearchAPI,
419
- IncludeRelatedEntityNameFieldInBaseView,
420
- DefaultInView,
421
- IsPrimaryKey,
422
- IsUnique
423
- )
424
- VALUES
425
- (
426
- ${n.EntityID},
427
- ${n.Sequence},
428
- '${n.FieldName}',
429
- '${convertCamelCaseToHaveSpaces(n.FieldName).trim()}',
430
- ${escapedDescription},
431
- '${n.Type}',
432
- ${n.Length},
433
- ${n.Precision},
434
- ${n.Scale},
435
- ${n.AllowsNull ? 1 : 0},
436
- '${parseDefaultValue(n.DefaultValue)}',
437
- ${n.AutoIncrement ? 1 : 0},
438
- ${n.AllowUpdateAPI ? 1 : 0},
439
- ${n.IsVirtual ? 1 : 0},
440
- ${n.RelatedEntityID},
441
- ${n.RelatedEntityFieldName && n.RelatedEntityFieldName.length > 0 ? `'${n.RelatedEntityFieldName}'` : 'NULL'},
442
- ${n.IsNameField !== null ? n.IsNameField : 0},
443
- ${n.FieldName === 'ID' || n.IsNameField ? 1 : 0},
444
- ${n.RelatedEntityID && n.RelatedEntityID > 0 && n.Type.trim().toLowerCase() === 'int' ? 1 : 0},
445
- ${bDefaultInView ? 1 : 0},
446
- ${n.IsPrimaryKey},
447
- ${n.IsUnique}
448
- )`;
449
- }
450
- function parseDefaultValue(sqlDefaultValue) {
451
- let sResult = null;
452
- if (sqlDefaultValue !== null && sqlDefaultValue !== undefined) {
453
- if (sqlDefaultValue.startsWith('(') && sqlDefaultValue.endsWith(')'))
454
- sResult = sqlDefaultValue.substring(1, sqlDefaultValue.length - 1);
455
- else
456
- sResult = sqlDefaultValue;
457
- if (sResult.toUpperCase().startsWith('N\'') && sResult.endsWith('\''))
458
- sResult = sResult.substring(2, sResult.length - 1);
459
- if (sResult.startsWith('\'') && sResult.endsWith('\''))
460
- sResult = sResult.substring(1, sResult.length - 1);
461
- }
462
- return sResult;
463
- }
464
- async function createNewEntityFieldsFromSchema(ds) {
465
- try {
466
- const sSQL = getPendingEntityFieldsSELECTSQL();
467
- const newEntityFields = await ds.query(sSQL);
468
- await ds.transaction(async () => {
469
- // wrap in a transaction so we get all of it or none of it
470
- for (let i = 0; i < newEntityFields.length; ++i) {
471
- const n = newEntityFields[i];
472
- if (n.EntityID !== null && n.EntityID !== undefined && n.EntityID > 0) {
473
- // need to check for null entity id = that is because the above query can return candidate Entity Fields but the entities may not have been created if the entities
474
- // that would have been created violate rules - such as not having an ID column, etc.
475
- const sSQLInsert = getPendingEntityFieldINSERTSQL(n);
476
- await ds.query(sSQLInsert);
477
- // if we get here, we're okay, otherwise we have an exception, which we want as it blows up transaction
478
- }
479
- }
480
- });
481
- return true;
482
- }
483
- catch (e) {
484
- (0, logging_1.logError)(e);
485
- return false;
486
- }
487
- }
488
- async function updateEntityFieldRelatedEntityNameFieldMap(ds, entityFieldID, relatedEntityNameFieldMap) {
489
- try {
490
- const sSQL = `EXEC [${(0, config_1.mj_core_schema)()}].spUpdateEntityFieldRelatedEntityNameFieldMap
491
- @EntityFieldID=${entityFieldID} ,
492
- @RelatedEntityNameFieldMap='${relatedEntityNameFieldMap}'`;
493
- await ds.query(sSQL);
494
- return true;
495
- }
496
- catch (e) {
497
- (0, logging_1.logError)(e);
498
- return false;
499
- }
500
- }
501
- exports.updateEntityFieldRelatedEntityNameFieldMap = updateEntityFieldRelatedEntityNameFieldMap;
502
- async function updateExistingEntitiesFromSchema(ds, excludeSchemas) {
503
- try {
504
- await ds.query(`EXEC [${(0, config_1.mj_core_schema)()}].spUpdateExistingEntitiesFromSchema @ExcludedSchemaNames='${excludeSchemas.join(',')}'`);
505
- return true;
506
- }
507
- catch (e) {
508
- (0, logging_1.logError)(e);
509
- return false;
510
- }
511
- }
512
- async function updateExistingEntityFieldsFromSchema(ds, excludeSchemas) {
513
- try {
514
- await ds.query(`EXEC [${(0, config_1.mj_core_schema)()}].spUpdateExistingEntityFieldsFromSchema @ExcludedSchemaNames='${excludeSchemas.join(',')}'`);
515
- return true;
516
- }
517
- catch (e) {
518
- (0, logging_1.logError)(e);
519
- return false;
520
- }
521
- }
522
- async function deleteUnneededEntityFields(ds, excludeSchemas) {
523
- try {
524
- await ds.query(`EXEC [${(0, config_1.mj_core_schema)()}].spDeleteUnneededEntityFields @ExcludedSchemaNames='${excludeSchemas.join(',')}'`);
525
- return true;
526
- }
527
- catch (e) {
528
- (0, logging_1.logError)(e);
529
- return false;
530
- }
531
- }
532
- async function manageEntityFieldValues(ds, excludeSchemas) {
533
- try {
534
- // here we want to get all of the entity fields that have check constraints attached to them. For each field that has a check constraint, we want to
535
- // evaluate it to see if it is a simple series of OR statements or not, if it is a simple series of OR statements, we can parse the possible values
536
- // for the field and sync that up with the EntityFieldValue table. If it is not a simple series of OR statements, we will not be able to parse it and we'll
537
- // just ignore it.
538
- const filter = excludeSchemas && excludeSchemas.length > 0 ? ` WHERE SchemaName NOT IN (${excludeSchemas.map(s => `'${s}'`).join(',')})` : '';
539
- const sSQL = `SELECT * FROM [${(0, config_1.mj_core_schema)()}].vwEntityFieldsWithCheckConstraints${filter}`;
540
- const result = await ds.query(sSQL);
541
- // now, for each of the constraints we get back here, loop through and evaluate if they're simple and if they're simple, parse and sync with entity field values for that field
542
- for (const r of result) {
543
- if (r.ConstraintDefinition && r.ConstraintDefinition.length > 0) {
544
- const parsedValues = parseCheckConstraintValues(r.ConstraintDefinition, r.ColumnName);
545
- if (parsedValues) {
546
- // we have parsed values from the check constraint, so sync them with the entity field values
547
- await syncEntityFieldValues(ds, r.EntityID, r.ColumnName, parsedValues);
548
- // finally, make sure the ValueListType column within the EntityField table is set to "List" because for check constraints we only allow the values specified in the list.
549
- await ds.query(`UPDATE [${(0, config_1.mj_core_schema)()}].EntityField SET ValueListType='List' WHERE EntityID=${r.EntityID} AND Name='${r.ColumnName}'`);
550
- }
551
- }
837
+ else {
838
+ return this.simpleNewEntityName(newEntity.TableName);
552
839
  }
553
- return true;
554
- }
555
- catch (e) {
556
- (0, logging_1.logError)(e);
557
- return false;
558
- }
559
- }
560
- async function syncEntityFieldValues(ds, entityID, entityFieldName, possibleValues) {
561
- try {
562
- // first, get a list of all of the existing entity field values for the field already in the database
563
- const sSQL = `SELECT * FROM [${(0, config_1.mj_core_schema)()}].EntityFieldValue WHERE EntityID=${entityID} AND EntityFieldName = '${entityFieldName}'`;
564
- const existingValues = await ds.query(sSQL);
565
- // now, loop through the possible values and add any that are not already in the database
566
- // Step 1: for any existing value that is NOT in the list of possible Values, delete it
567
- let numRemoved = 0;
568
- await ds.transaction(async () => {
569
- for (const ev of existingValues) {
570
- if (!possibleValues.find(v => v === ev.Value)) {
571
- // delete the value from the database
572
- const sSQLDelete = `DELETE FROM [${(0, config_1.mj_core_schema)()}].EntityFieldValue WHERE ID=${ev.ID}`;
573
- await ds.query(sSQLDelete);
574
- numRemoved++;
840
+ }
841
+ simpleNewEntityName(tableName) {
842
+ return this.convertCamelCaseToHaveSpaces(this.generatePluralName(tableName));
843
+ }
844
+ async createNewEntity(ds, newEntity, md) {
845
+ try {
846
+ const { shouldCreate, validationMessage } = await this.shouldCreateNewEntity(ds, newEntity);
847
+ if (shouldCreate) {
848
+ // process a single new entity
849
+ let newEntityName = await this.createNewEntityName(newEntity);
850
+ let suffix = '';
851
+ const existingEntity = md.Entities.find(e => e.Name === newEntityName);
852
+ const existingEntityInNewEntityList = ManageMetadataBase_1.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
853
+ if (existingEntity || existingEntityInNewEntityList) {
854
+ // the generated name is already in place, so we need another name
855
+ // use Entity Name __ SchemaName instead of just the table name as basis
856
+ suffix = '__' + newEntity.SchemaName;
857
+ newEntityName = newEntityName + suffix;
858
+ (0, core_1.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.`);
575
859
  }
576
- }
577
- // Step 2: for any possible value that is NOT in the list of existing values, add it
578
- let numAdded = 0;
579
- for (const v of possibleValues) {
580
- if (!existingValues.find(ev => ev.Value === v)) {
581
- // add the value to the database
582
- const sSQLInsert = `INSERT INTO [${(0, config_1.mj_core_schema)()}].EntityFieldValue
583
- (EntityID, EntityFieldName, Sequence, Value, Code)
584
- VALUES
585
- (${entityID}, '${entityFieldName}', ${1 + possibleValues.indexOf(v)}, '${v}', '${v}')`;
860
+ // get the next entity ID
861
+ const params = [newEntity.SchemaName];
862
+ const sSQLNewEntityID = `EXEC [${(0, config_1.mj_core_schema)()}].spGetNextEntityID @SchemaName=@0`;
863
+ const newEntityIDRaw = await ds.query(sSQLNewEntityID, params);
864
+ const newEntityID = newEntityIDRaw && newEntityIDRaw.length > 0 ? newEntityIDRaw[0].NextID : null;
865
+ if (newEntityID && newEntityID > 0) {
866
+ const isNewSchema = await this.isSchemaNew(ds, newEntity.SchemaName);
867
+ const sSQLInsert = this.createNewEntityInsertSQL(newEntityID, newEntityName, newEntity, suffix);
586
868
  await ds.query(sSQLInsert);
587
- numAdded++;
588
- }
589
- }
590
- // Step 3: finally, for the existing values that are in the list of possible values, update the sequence to match the order in the possible values list
591
- let numUpdated = 0;
592
- for (const v of possibleValues) {
593
- const ev = existingValues.find(ev => ev.Value === v);
594
- if (ev) {
595
- // update the sequence to match the order in the possible values list
596
- const sSQLUpdate = `UPDATE [${(0, config_1.mj_core_schema)()}].EntityFieldValue SET Sequence=${1 + possibleValues.indexOf(v)} WHERE ID=${ev.ID}`;
597
- await ds.query(sSQLUpdate);
598
- numUpdated++;
599
- }
600
- }
601
- });
602
- return true;
603
- }
604
- catch (e) {
605
- (0, logging_1.logError)(e);
606
- return false;
607
- }
608
- }
609
- function parseCheckConstraintValues(constraintDefinition, fieldName) {
610
- // This regex checks for the overall structure including field name and 'OR' sequences
611
- // an example of a valid constraint definition would be: ([FieldName]='Value1' OR [FieldName]='Value2' OR [FieldName]='Value3')
612
- // like: ([AutoRunIntervalUnits]='Years' OR [AutoRunIntervalUnits]='Months' OR [AutoRunIntervalUnits]='Weeks' OR [AutoRunIntervalUnits]='Days' OR [AutoRunIntervalUnits]='Hours' OR [AutoRunIntervalUnits]='Minutes')
613
- // Note: Assuming fieldName does not contain regex special characters; otherwise, it needs to be escaped as well.
614
- const structureRegex = new RegExp(`^\\(\\[${fieldName}\\]='[^']+'(?: OR \\[${fieldName}\\]='[^']+?')+\\)$`);
615
- if (!structureRegex.test(constraintDefinition)) {
616
- console.log('Constraint does not match the simple OR condition pattern or field name does not match.');
617
- return null;
618
- }
619
- // Regular expression to match the values within the single quotes specifically for the field
620
- const valueRegex = new RegExp(`\\[${fieldName}\\]='([^']+)\'`, 'g');
621
- let match;
622
- const possibleValues = [];
623
- // Use regex to find matches and extract the values
624
- while ((match = valueRegex.exec(constraintDefinition)) !== null) {
625
- // This is necessary to avoid infinite loops with zero-width matches
626
- if (match.index === valueRegex.lastIndex) {
627
- valueRegex.lastIndex++;
628
- }
629
- // The first captured group contains the value
630
- if (match[1]) {
631
- possibleValues.push(match[1]);
632
- }
633
- }
634
- return possibleValues;
635
- }
636
- function createExcludeTablesAndSchemasFilter(fieldPrefix) {
637
- let sExcludeTables = '';
638
- let sExcludeSchemas = '';
639
- if (config_1.configInfo.excludeTables) {
640
- for (let i = 0; i < config_1.configInfo.excludeTables.length; ++i) {
641
- const t = config_1.configInfo.excludeTables[i];
642
- sExcludeTables += (sExcludeTables.length > 0 ? ' AND ' : '') +
643
- (t.schema.indexOf('%') > -1 ? ` NOT ( ${fieldPrefix}SchemaName LIKE '${t.schema}'` :
644
- ` NOT ( ${fieldPrefix}SchemaName = '${t.schema}'`);
645
- sExcludeTables += (t.table.indexOf('%') > -1 ? ` AND ${fieldPrefix}TableName LIKE '${t.table}') ` :
646
- ` AND ${fieldPrefix}TableName = '${t.table}') `);
647
- }
648
- }
649
- if (config_1.configInfo.excludeSchemas) {
650
- for (let i = 0; i < config_1.configInfo.excludeSchemas.length; ++i) {
651
- const s = config_1.configInfo.excludeSchemas[i];
652
- sExcludeSchemas += (sExcludeSchemas.length > 0 ? ' AND ' : '') +
653
- (s.indexOf('%') > -1 ? `${fieldPrefix}SchemaName NOT LIKE '${s}'` : `${fieldPrefix}SchemaName <> '${s}'`);
654
- }
655
- }
656
- const sWhere = (sExcludeTables.length > 0 || sExcludeSchemas.length > 0 ? ` AND ` : '') +
657
- (sExcludeTables.length > 0 ? `(${sExcludeTables})` : '') +
658
- (sExcludeSchemas.length > 0 ? (sExcludeTables.length > 0 ? ` AND ` : ``) + '(' + sExcludeSchemas + ')' : '');
659
- return sWhere;
660
- }
661
- async function createNewEntities(ds) {
662
- try {
663
- const sSQL = `SELECT * FROM [${(0, config_1.mj_core_schema)()}].vwSQLTablesAndEntities WHERE EntityID IS NULL ` + createExcludeTablesAndSchemasFilter('');
664
- const newEntities = await ds.query(sSQL);
665
- if (newEntities && newEntities.length > 0) {
666
- const md = new core_1.Metadata();
667
- await ds.transaction(async () => {
668
- // wrap in a transaction so we get all of it or none of it
669
- for (let i = 0; i < newEntities.length; ++i) {
670
- // process each of the new entities
671
- await createNewEntity(ds, newEntities[i], md);
672
- }
673
- });
674
- if (exports.newEntityList.length > 0) {
675
- // only do this if we actually created new entities
676
- (0, core_1.LogStatus)(` Done creating entities, refreshing metadata to reflect new entities...`);
677
- await md.Refresh(); // refresh now since we've added some new entities
678
- }
679
- }
680
- return true; // if we get here, we succeeded
681
- }
682
- catch (e) {
683
- (0, core_1.LogError)(e);
684
- return false;
685
- }
686
- }
687
- async function shouldCreateNewEntity(ds, newEntity) {
688
- // validate that the new entity meets our criteria for creation
689
- // criteria:
690
- // 1) entity has a field that is a primary key
691
- // validate all of these factors by getting the sql from SQL Server and check the result, if failure, shouldCreate=false and generate validation message, otherwise return empty validation message and true for shouldCreate.
692
- const query = `EXEC ${core_1.Metadata.Provider.ConfigData.MJCoreSchemaName}.spGetPrimaryKeyForTable @TableName='${newEntity.TableName}', @SchemaName='${newEntity.SchemaName}'`;
693
- try {
694
- const result = await ds.query(query);
695
- if (result.length === 0) {
696
- return { shouldCreate: false, validationMessage: "No primary key found" };
697
- }
698
- return { shouldCreate: true, validationMessage: '' };
699
- }
700
- catch (error) {
701
- const errorMsg = 'Error validating new entity for table:' + newEntity?.TableName;
702
- console.error(errorMsg, error);
703
- return { shouldCreate: false, validationMessage: errorMsg };
704
- }
705
- }
706
- async function createNewEntityName(newEntity) {
707
- const ag = new advanced_generation_1.AdvancedGeneration();
708
- if (ag.featureEnabled('EntityNames')) {
709
- // get the LLM for this entity
710
- const chat = ag.LLM;
711
- const prompt = ag.getPrompt('EntityNames');
712
- const systemPrompt = ag.fillTemplate(prompt.systemPrompt, newEntity);
713
- const userMessage = ag.fillTemplate(prompt.userMessage, newEntity);
714
- const result = await chat.ChatCompletion({
715
- model: ag.AIModel,
716
- messages: [
717
- {
718
- role: 'system',
719
- content: systemPrompt
720
- },
721
- {
722
- role: 'user',
723
- content: userMessage
724
- }
725
- ]
726
- });
727
- if (result?.success) {
728
- const resultText = result?.data.choices[0].message.content;
729
- try {
730
- const structuredResult = JSON.parse(resultText);
731
- if (structuredResult?.entityName) {
732
- return structuredResult.entityName;
869
+ // if we get here we created a new entity safely, otherwise we get exception
870
+ // add it to the new entity list
871
+ ManageMetadataBase_1.newEntityList.push(newEntityName);
872
+ // next, check if this entity is in a schema that is new (e.g. no other entities have been added to this schema yet), if so and if
873
+ // our config option is set to create new applications from new schemas, then create a new application for this schema
874
+ if (isNewSchema && config_1.configInfo.newSchemaDefaults.CreateNewApplicationWithSchemaName) {
875
+ // new schema and config option is to create a new application from the schema name so do that
876
+ if (!await this.applicationExists(ds, newEntity.SchemaName))
877
+ await this.createNewApplication(ds, newEntity.SchemaName);
878
+ }
879
+ else {
880
+ // not a new schema, attempt to look up the application for this schema
881
+ await this.getApplicationIDForSchema(ds, newEntity.SchemaName);
882
+ }
883
+ // now we have an application ID, but make sure that we are configured to add this new entity to an application at all
884
+ if (config_1.configInfo.newEntityDefaults.AddToApplicationWithSchemaName) {
885
+ // we should add this entity to the application
886
+ const appName = newEntity.SchemaName === (0, config_1.mj_core_schema)() ? 'Admin' : newEntity.SchemaName; // for the __mj schema or whatever it is installed as for mj_core - we want to drop stuff into the admin app
887
+ const sSQLInsertApplicationEntity = `INSERT INTO ${(0, config_1.mj_core_schema)()}.ApplicationEntity
888
+ (ApplicationName, EntityID, Sequence) VALUES
889
+ ('${appName}', ${newEntityID}, (SELECT ISNULL(MAX(Sequence),0)+1 FROM ${(0, config_1.mj_core_schema)()}.ApplicationEntity WHERE ApplicationName = '${appName}'))`;
890
+ await ds.query(sSQLInsertApplicationEntity);
891
+ }
892
+ // next up, we need to check if we're configured to add permissions for new entities, and if so, add them
893
+ if (config_1.configInfo.newEntityDefaults.PermissionDefaults && config_1.configInfo.newEntityDefaults.PermissionDefaults.AutoAddPermissionsForNewEntities) {
894
+ // we are asked to add permissions for new entities, so do that by looping through the permissions and adding them
895
+ const permissions = config_1.configInfo.newEntityDefaults.PermissionDefaults.Permissions;
896
+ for (const p of permissions) {
897
+ const sSQLInsertPermission = `INSERT INTO ${(0, config_1.mj_core_schema)()}.EntityPermission
898
+ (EntityID, RoleName, CanRead, CanCreate, CanUpdate, CanDelete) VALUES
899
+ (${newEntityID}, '${p.RoleName}', ${p.CanRead ? 1 : 0}, ${p.CanCreate ? 1 : 0}, ${p.CanUpdate ? 1 : 0}, ${p.CanDelete ? 1 : 0})`;
900
+ await ds.query(sSQLInsertPermission);
901
+ }
902
+ }
903
+ (0, core_1.LogStatus)(` Created new entity ${newEntityName} for table ${newEntity.SchemaName}.${newEntity.TableName}`);
733
904
  }
734
905
  else {
735
- console.warn(' >>> Advanced Generation Error: LLM returned a blank entity name, falling back to simple generated entity name');
736
- return simpleNewEntityName(newEntity.TableName);
906
+ (0, core_1.LogError)(`ERROR: Unable to get next entity ID for ${newEntity.SchemaName}.${newEntity.TableName} - it is possible that the schema has reached its MAX Id,
907
+ check the Schema Info entity for this schema to see if all ID values have been allocated.`);
737
908
  }
738
909
  }
739
- catch (e) {
740
- console.warn(' >>> Advanced Generation Error: LLM returned invalid result, falling back to simple generated entity name. Result from LLM: ' + resultText, e);
741
- return simpleNewEntityName(newEntity.TableName);
910
+ else {
911
+ (0, core_1.LogStatus)(` Skipping new entity ${newEntity.TableName} because it doesn't qualify to be created. Reason: ${validationMessage}`);
912
+ return;
742
913
  }
743
914
  }
744
- else {
745
- console.warn(' >>> Advanced Generation Error: LLM call failed, falling back to simple generated entity name.');
746
- return simpleNewEntityName(newEntity.TableName);
747
- }
748
- }
749
- else {
750
- return simpleNewEntityName(newEntity.TableName);
751
- }
752
- }
753
- function simpleNewEntityName(tableName) {
754
- return convertCamelCaseToHaveSpaces(generatePluralName(tableName));
755
- }
756
- async function createNewEntity(ds, newEntity, md) {
757
- try {
758
- const { shouldCreate, validationMessage } = await shouldCreateNewEntity(ds, newEntity);
759
- if (shouldCreate) {
760
- // process a single new entity
761
- let newEntityName = await createNewEntityName(newEntity);
762
- let suffix = '';
763
- const existingEntity = md.Entities.find(e => e.Name === newEntityName);
764
- const existingEntityInNewEntityList = exports.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
765
- if (existingEntity || existingEntityInNewEntityList) {
766
- // the generated name is already in place, so we need another name
767
- // use Entity Name __ SchemaName instead of just the table name as basis
768
- suffix = '__' + newEntity.SchemaName;
769
- newEntityName = newEntityName + suffix;
770
- (0, core_1.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.`);
915
+ catch (e) {
916
+ (0, core_1.LogError)(`Failed to create new entity ${newEntity?.TableName}`);
917
+ }
918
+ }
919
+ async isSchemaNew(ds, schemaName) {
920
+ // check to see if there are any entities in the db with this schema name
921
+ const sSQL = `SELECT COUNT(*) AS Count FROM [${(0, config_1.mj_core_schema)()}].Entity WHERE SchemaName = '${schemaName}'`;
922
+ const result = await ds.query(sSQL);
923
+ return result && result.length > 0 ? result[0].Count === 0 : true;
924
+ }
925
+ async createNewApplication(ds, appName) {
926
+ const sSQL = "INSERT INTO [" + (0, config_1.mj_core_schema)() + "].Application (Name, Description) VALUES ('" + appName + "', 'Generated for Schema'); SELECT @@IDENTITY AS ID";
927
+ const result = await ds.query(sSQL);
928
+ return result && result.length > 0 ? result[0].ID : null;
929
+ }
930
+ async applicationExists(ds, applicationName) {
931
+ const sSQL = `SELECT ID FROM [${(0, config_1.mj_core_schema)()}].Application WHERE Name = '${applicationName}'`;
932
+ const result = await ds.query(sSQL);
933
+ return result && result.length > 0 ? result[0].ID > 0 : false;
934
+ }
935
+ async getApplicationIDForSchema(ds, schemaName) {
936
+ const sSQL = `SELECT ID FROM [${(0, config_1.mj_core_schema)()}].Application WHERE Name = '${schemaName}'`;
937
+ const result = await ds.query(sSQL);
938
+ return result && result.length > 0 ? result[0].ID : null;
939
+ }
940
+ createNewEntityInsertSQL(newEntityID, newEntityName, newEntity, newEntitySuffix) {
941
+ const newEntityDefaults = config_1.configInfo.newEntityDefaults;
942
+ const newEntityDescriptionEscaped = newEntity.Description ? `'${newEntity.Description.replace(/'/g, "''")}` : null;
943
+ const sSQLInsert = `INSERT INTO [${(0, config_1.mj_core_schema)()}].Entity (
944
+ ID,
945
+ Name,
946
+ Description,
947
+ NameSuffix,
948
+ BaseTable,
949
+ BaseView,
950
+ SchemaName,
951
+ IncludeInAPI,
952
+ AllowUserSearchAPI
953
+ ${newEntityDefaults.TrackRecordChanges === undefined ? '' : ', TrackRecordChanges'}
954
+ ${newEntityDefaults.AuditRecordAccess === undefined ? '' : ', AuditRecordAccess'}
955
+ ${newEntityDefaults.AuditViewRuns === undefined ? '' : ', AuditViewRuns'}
956
+ ${newEntityDefaults.AllowAllRowsAPI === undefined ? '' : ', AllowAllRowsAPI'}
957
+ ${newEntityDefaults.AllowCreateAPI === undefined ? '' : ', AllowCreateAPI'}
958
+ ${newEntityDefaults.AllowUpdateAPI === undefined ? '' : ', AllowUpdateAPI'}
959
+ ${newEntityDefaults.AllowDeleteAPI === undefined ? '' : ', AllowDeleteAPI'}
960
+ ${newEntityDefaults.UserViewMaxRows === undefined ? '' : ', UserViewMaxRows'}
961
+ )
962
+ VALUES (
963
+ ${newEntityID},
964
+ '${newEntityName}',
965
+ ${newEntityDescriptionEscaped ? newEntityDescriptionEscaped : 'NULL' /*if no description, then null*/},
966
+ ${newEntitySuffix && newEntitySuffix.length > 0 ? `'${newEntitySuffix}'` : 'NULL'},
967
+ '${newEntity.TableName}',
968
+ 'vw${this.generatePluralName(newEntity.TableName) + (newEntitySuffix && newEntitySuffix.length > 0 ? newEntitySuffix : '')}',
969
+ '${newEntity.SchemaName}',
970
+ 1,
971
+ ${newEntityDefaults.AllowUserSearchAPI === undefined ? 1 : newEntityDefaults.AllowUserSearchAPI ? 1 : 0}
972
+ ${newEntityDefaults.TrackRecordChanges === undefined ? '' : ', ' + (newEntityDefaults.TrackRecordChanges ? '1' : '0')}
973
+ ${newEntityDefaults.AuditRecordAccess === undefined ? '' : ', ' + (newEntityDefaults.AuditRecordAccess ? '1' : '0')}
974
+ ${newEntityDefaults.AuditViewRuns === undefined ? '' : ', ' + (newEntityDefaults.AuditViewRuns ? '1' : '0')}
975
+ ${newEntityDefaults.AllowAllRowsAPI === undefined ? '' : ', ' + (newEntityDefaults.AllowAllRowsAPI ? '1' : '0')}
976
+ ${newEntityDefaults.AllowCreateAPI === undefined ? '' : ', ' + (newEntityDefaults.AllowCreateAPI ? '1' : '0')}
977
+ ${newEntityDefaults.AllowUpdateAPI === undefined ? '' : ', ' + (newEntityDefaults.AllowUpdateAPI ? '1' : '0')}
978
+ ${newEntityDefaults.AllowDeleteAPI === undefined ? '' : ', ' + (newEntityDefaults.AllowDeleteAPI ? '1' : '0')}
979
+ ${newEntityDefaults.UserViewMaxRows === undefined ? '' : ', ' + (newEntityDefaults.UserViewMaxRows)}
980
+ )`;
981
+ return sSQLInsert;
982
+ }
983
+ stripTrailingChars(s, charsToStrip, skipIfExactMatch) {
984
+ if (s && charsToStrip) {
985
+ if (s.endsWith(charsToStrip) && (skipIfExactMatch ? s !== charsToStrip : true))
986
+ return s.substring(0, s.length - charsToStrip.length);
987
+ else
988
+ return s;
989
+ }
990
+ else
991
+ return s;
992
+ }
993
+ convertCamelCaseToHaveSpaces(s) {
994
+ let result = '';
995
+ for (let i = 0; i < s.length; ++i) {
996
+ if (s[i] === s[i].toUpperCase() && // current character is upper case
997
+ i > 0 && // not first character
998
+ s[i - 1] !== ' ' && // previous character is not a space - needed for strings like "Database Version" that already have spaces
999
+ (s[i - 1] !== s[i - 1].toUpperCase()) // previous character is not upper case handles not putting space between I and D in ID as an example of consecutive upper case
1000
+ ) {
1001
+ result += ' ';
771
1002
  }
772
- // get the next entity ID
773
- const params = [newEntity.SchemaName];
774
- const sSQLNewEntityID = `EXEC [${(0, config_1.mj_core_schema)()}].spGetNextEntityID @SchemaName=@0`;
775
- const newEntityIDRaw = await ds.query(sSQLNewEntityID, params);
776
- const newEntityID = newEntityIDRaw && newEntityIDRaw.length > 0 ? newEntityIDRaw[0].NextID : null;
777
- if (newEntityID && newEntityID > 0) {
778
- const isNewSchema = await isSchemaNew(ds, newEntity.SchemaName);
779
- const sSQLInsert = createNewEntityInsertSQL(newEntityID, newEntityName, newEntity, suffix);
780
- await ds.query(sSQLInsert);
781
- // if we get here we created a new entity safely, otherwise we get exception
782
- // add it to the new entity list
783
- exports.newEntityList.push(newEntityName);
784
- // next, check if this entity is in a schema that is new (e.g. no other entities have been added to this schema yet), if so and if
785
- // our config option is set to create new applications from new schemas, then create a new application for this schema
786
- if (isNewSchema && config_1.configInfo.newSchemaDefaults.CreateNewApplicationWithSchemaName) {
787
- // new schema and config option is to create a new application from the schema name so do that
788
- if (!await applicationExists(ds, newEntity.SchemaName))
789
- await createNewApplication(ds, newEntity.SchemaName);
790
- }
791
- else {
792
- // not a new schema, attempt to look up the application for this schema
793
- await getApplicationIDForSchema(ds, newEntity.SchemaName);
794
- }
795
- // now we have an application ID, but make sure that we are configured to add this new entity to an application at all
796
- if (config_1.configInfo.newEntityDefaults.AddToApplicationWithSchemaName) {
797
- // we should add this entity to the application
798
- const appName = newEntity.SchemaName === (0, config_1.mj_core_schema)() ? 'Admin' : newEntity.SchemaName; // for the __mj schema or whatever it is installed as for mj_core - we want to drop stuff into the admin app
799
- const sSQLInsertApplicationEntity = `INSERT INTO ${(0, config_1.mj_core_schema)()}.ApplicationEntity
800
- (ApplicationName, EntityID, Sequence) VALUES
801
- ('${appName}', ${newEntityID}, (SELECT ISNULL(MAX(Sequence),0)+1 FROM ${(0, config_1.mj_core_schema)()}.ApplicationEntity WHERE ApplicationName = '${appName}'))`;
802
- await ds.query(sSQLInsertApplicationEntity);
803
- }
804
- // next up, we need to check if we're configured to add permissions for new entities, and if so, add them
805
- if (config_1.configInfo.newEntityDefaults.PermissionDefaults && config_1.configInfo.newEntityDefaults.PermissionDefaults.AutoAddPermissionsForNewEntities) {
806
- // we are asked to add permissions for new entities, so do that by looping through the permissions and adding them
807
- const permissions = config_1.configInfo.newEntityDefaults.PermissionDefaults.Permissions;
808
- for (const p of permissions) {
809
- const sSQLInsertPermission = `INSERT INTO ${(0, config_1.mj_core_schema)()}.EntityPermission
810
- (EntityID, RoleName, CanRead, CanCreate, CanUpdate, CanDelete) VALUES
811
- (${newEntityID}, '${p.RoleName}', ${p.CanRead ? 1 : 0}, ${p.CanCreate ? 1 : 0}, ${p.CanUpdate ? 1 : 0}, ${p.CanDelete ? 1 : 0})`;
812
- await ds.query(sSQLInsertPermission);
813
- }
814
- }
815
- (0, core_1.LogStatus)(` Created new entity ${newEntityName} for table ${newEntity.SchemaName}.${newEntity.TableName}`);
1003
+ result += s[i];
1004
+ }
1005
+ return result;
1006
+ }
1007
+ stripWhitespace(s) {
1008
+ return s.replace(/\s/g, '');
1009
+ }
1010
+ generatePluralName(singularName) {
1011
+ if (singularName.endsWith('y') && singularName.length > 1) {
1012
+ // Check if the letter before 'y' is a vowel
1013
+ const secondLastChar = singularName[singularName.length - 2].toLowerCase();
1014
+ if ('aeiou'.includes(secondLastChar)) {
1015
+ // If it's a vowel, just add 's', example "key/keys"
1016
+ return singularName + 's';
816
1017
  }
817
1018
  else {
818
- (0, core_1.LogError)(`ERROR: Unable to get next entity ID for ${newEntity.SchemaName}.${newEntity.TableName} - it is possible that the schema has reached its MAX Id,
819
- check the Schema Info entity for this schema to see if all ID values have been allocated.`);
1019
+ // If it's a consonant, replace 'y' with 'ies' - example "party/parties"
1020
+ return singularName.substring(0, singularName.length - 1) + 'ies';
820
1021
  }
821
1022
  }
822
- else {
823
- (0, core_1.LogStatus)(` Skipping new entity ${newEntity.TableName} because it doesn't qualify to be created. Reason: ${validationMessage}`);
824
- return;
825
- }
826
- }
827
- catch (e) {
828
- (0, core_1.LogError)(`Failed to create new entity ${newEntity?.TableName}`);
829
- }
830
- }
831
- async function isSchemaNew(ds, schemaName) {
832
- // check to see if there are any entities in the db with this schema name
833
- const sSQL = `SELECT COUNT(*) AS Count FROM [${(0, config_1.mj_core_schema)()}].Entity WHERE SchemaName = '${schemaName}'`;
834
- const result = await ds.query(sSQL);
835
- return result && result.length > 0 ? result[0].Count === 0 : true;
836
- }
837
- async function createNewApplication(ds, appName) {
838
- const sSQL = "INSERT INTO [" + (0, config_1.mj_core_schema)() + "].Application (Name, Description) VALUES ('" + appName + "', 'Generated for Schema'); SELECT @@IDENTITY AS ID";
839
- const result = await ds.query(sSQL);
840
- return result && result.length > 0 ? result[0].ID : null;
841
- }
842
- async function applicationExists(ds, applicationName) {
843
- const sSQL = `SELECT ID FROM [${(0, config_1.mj_core_schema)()}].Application WHERE Name = '${applicationName}'`;
844
- const result = await ds.query(sSQL);
845
- return result && result.length > 0 ? result[0].ID > 0 : false;
846
- }
847
- async function getApplicationIDForSchema(ds, schemaName) {
848
- const sSQL = `SELECT ID FROM [${(0, config_1.mj_core_schema)()}].Application WHERE Name = '${schemaName}'`;
849
- const result = await ds.query(sSQL);
850
- return result && result.length > 0 ? result[0].ID : null;
851
- }
852
- function createNewEntityInsertSQL(newEntityID, newEntityName, newEntity, newEntitySuffix) {
853
- const newEntityDefaults = config_1.configInfo.newEntityDefaults;
854
- const newEntityDescriptionEscaped = newEntity.Description ? `'${newEntity.Description.replace(/'/g, "''")}` : null;
855
- const sSQLInsert = `INSERT INTO [${(0, config_1.mj_core_schema)()}].Entity (
856
- ID,
857
- Name,
858
- Description,
859
- NameSuffix,
860
- BaseTable,
861
- BaseView,
862
- SchemaName,
863
- IncludeInAPI,
864
- AllowUserSearchAPI
865
- ${newEntityDefaults.TrackRecordChanges === undefined ? '' : ', TrackRecordChanges'}
866
- ${newEntityDefaults.AuditRecordAccess === undefined ? '' : ', AuditRecordAccess'}
867
- ${newEntityDefaults.AuditViewRuns === undefined ? '' : ', AuditViewRuns'}
868
- ${newEntityDefaults.AllowAllRowsAPI === undefined ? '' : ', AllowAllRowsAPI'}
869
- ${newEntityDefaults.AllowCreateAPI === undefined ? '' : ', AllowCreateAPI'}
870
- ${newEntityDefaults.AllowUpdateAPI === undefined ? '' : ', AllowUpdateAPI'}
871
- ${newEntityDefaults.AllowDeleteAPI === undefined ? '' : ', AllowDeleteAPI'}
872
- ${newEntityDefaults.UserViewMaxRows === undefined ? '' : ', UserViewMaxRows'}
873
- )
874
- VALUES (
875
- ${newEntityID},
876
- '${newEntityName}',
877
- ${newEntityDescriptionEscaped ? newEntityDescriptionEscaped : 'NULL' /*if no description, then null*/},
878
- ${newEntitySuffix && newEntitySuffix.length > 0 ? `'${newEntitySuffix}'` : 'NULL'},
879
- '${newEntity.TableName}',
880
- 'vw${generatePluralName(newEntity.TableName) + (newEntitySuffix && newEntitySuffix.length > 0 ? newEntitySuffix : '')}',
881
- '${newEntity.SchemaName}',
882
- 1,
883
- ${newEntityDefaults.AllowUserSearchAPI === undefined ? 1 : newEntityDefaults.AllowUserSearchAPI ? 1 : 0}
884
- ${newEntityDefaults.TrackRecordChanges === undefined ? '' : ', ' + (newEntityDefaults.TrackRecordChanges ? '1' : '0')}
885
- ${newEntityDefaults.AuditRecordAccess === undefined ? '' : ', ' + (newEntityDefaults.AuditRecordAccess ? '1' : '0')}
886
- ${newEntityDefaults.AuditViewRuns === undefined ? '' : ', ' + (newEntityDefaults.AuditViewRuns ? '1' : '0')}
887
- ${newEntityDefaults.AllowAllRowsAPI === undefined ? '' : ', ' + (newEntityDefaults.AllowAllRowsAPI ? '1' : '0')}
888
- ${newEntityDefaults.AllowCreateAPI === undefined ? '' : ', ' + (newEntityDefaults.AllowCreateAPI ? '1' : '0')}
889
- ${newEntityDefaults.AllowUpdateAPI === undefined ? '' : ', ' + (newEntityDefaults.AllowUpdateAPI ? '1' : '0')}
890
- ${newEntityDefaults.AllowDeleteAPI === undefined ? '' : ', ' + (newEntityDefaults.AllowDeleteAPI ? '1' : '0')}
891
- ${newEntityDefaults.UserViewMaxRows === undefined ? '' : ', ' + (newEntityDefaults.UserViewMaxRows)}
892
- )`;
893
- return sSQLInsert;
894
- }
895
- function stripTrailingChars(s, charsToStrip, skipIfExactMatch) {
896
- if (s && charsToStrip) {
897
- if (s.endsWith(charsToStrip) && (skipIfExactMatch ? s !== charsToStrip : true))
898
- return s.substring(0, s.length - charsToStrip.length);
899
- else
900
- return s;
901
- }
902
- else
903
- return s;
904
- }
905
- function convertCamelCaseToHaveSpaces(s) {
906
- let result = '';
907
- for (let i = 0; i < s.length; ++i) {
908
- if (s[i] === s[i].toUpperCase() && // current character is upper case
909
- i > 0 && // not first character
910
- s[i - 1] !== ' ' && // previous character is not a space - needed for strings like "Database Version" that already have spaces
911
- (s[i - 1] !== s[i - 1].toUpperCase()) // previous character is not upper case handles not putting space between I and D in ID as an example of consecutive upper case
912
- ) {
913
- result += ' ';
914
- }
915
- result += s[i];
916
- }
917
- return result;
918
- }
919
- function stripWhitespace(s) {
920
- return s.replace(/\s/g, '');
921
- }
922
- function generatePluralName(singularName) {
923
- if (singularName.endsWith('y') && singularName.length > 1) {
924
- // Check if the letter before 'y' is a vowel
925
- const secondLastChar = singularName[singularName.length - 2].toLowerCase();
926
- if ('aeiou'.includes(secondLastChar)) {
927
- // If it's a vowel, just add 's', example "key/keys"
1023
+ else if (singularName.endsWith('y')) {
1024
+ // If the string is just 'y', treat it like a vowel and just add 's'
928
1025
  return singularName + 's';
929
1026
  }
1027
+ else if (singularName.endsWith('s')) {
1028
+ // Singular name already ends with 's', so just return it
1029
+ return singularName;
1030
+ }
1031
+ else if (singularName.endsWith('ch') || singularName.endsWith('sh') || singularName.endsWith('x') || singularName.endsWith('z')) {
1032
+ // If the singular name ends with 'ch', 'sh', 'x', or 'z', add 'es' - example "box/boxes", "index/indexes", "church/churches", "dish/dishes", "buzz/buzzes"
1033
+ return singularName + 'es';
1034
+ }
930
1035
  else {
931
- // If it's a consonant, replace 'y' with 'ies' - example "party/parties"
932
- return singularName.substring(0, singularName.length - 1) + 'ies';
1036
+ // For other cases, just add 's'
1037
+ return singularName + 's';
933
1038
  }
934
1039
  }
935
- else if (singularName.endsWith('y')) {
936
- // If the string is just 'y', treat it like a vowel and just add 's'
937
- return singularName + 's';
938
- }
939
- else if (singularName.endsWith('s')) {
940
- // Singular name already ends with 's', so just return it
941
- return singularName;
942
- }
943
- else if (singularName.endsWith('ch') || singularName.endsWith('sh') || singularName.endsWith('x') || singularName.endsWith('z')) {
944
- // If the singular name ends with 'ch', 'sh', 'x', or 'z', add 'es' - example "box/boxes", "index/indexes", "church/churches", "dish/dishes", "buzz/buzzes"
945
- return singularName + 'es';
946
- }
947
- else {
948
- // For other cases, just add 's'
949
- return singularName + 's';
950
- }
951
- }
1040
+ };
1041
+ exports.ManageMetadataBase = ManageMetadataBase;
1042
+ ManageMetadataBase._newEntityList = [];
1043
+ exports.ManageMetadataBase = ManageMetadataBase = ManageMetadataBase_1 = __decorate([
1044
+ (0, global_1.RegisterClass)(ManageMetadataBase)
1045
+ ], ManageMetadataBase);
952
1046
  //# sourceMappingURL=manageMetadata.js.map