@memberjunction/server 2.1.1 → 2.1.3
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.
- package/dist/apolloServer/TransactionPlugin.d.ts +4 -0
- package/dist/apolloServer/TransactionPlugin.d.ts.map +1 -0
- package/dist/apolloServer/TransactionPlugin.js +46 -0
- package/dist/apolloServer/TransactionPlugin.js.map +1 -0
- package/dist/apolloServer/index.d.ts +11 -0
- package/dist/apolloServer/index.d.ts.map +1 -0
- package/dist/apolloServer/index.js +25 -0
- package/dist/apolloServer/index.js.map +1 -0
- package/dist/auth/exampleNewUserSubClass.d.ts +6 -0
- package/dist/auth/exampleNewUserSubClass.d.ts.map +1 -0
- package/dist/auth/exampleNewUserSubClass.js +49 -0
- package/dist/auth/exampleNewUserSubClass.js.map +1 -0
- package/dist/auth/index.d.ts +30 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +121 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/newUsers.d.ts +5 -0
- package/dist/auth/newUsers.d.ts.map +1 -0
- package/{src/auth/newUsers.ts → dist/auth/newUsers.js} +20 -14
- package/dist/auth/newUsers.js.map +1 -0
- package/dist/auth/tokenExpiredError.d.ts +5 -0
- package/dist/auth/tokenExpiredError.d.ts.map +1 -0
- package/dist/auth/tokenExpiredError.js +12 -0
- package/dist/auth/tokenExpiredError.js.map +1 -0
- package/dist/cache.d.ts +3 -0
- package/dist/cache.d.ts.map +1 -0
- package/{src/cache.ts → dist/cache.js} +4 -6
- package/dist/cache.js.map +1 -0
- package/dist/config.d.ts +196 -0
- package/dist/config.d.ts.map +1 -0
- package/{src/config.ts → dist/config.js} +25 -49
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +17 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +84 -0
- package/dist/context.js.map +1 -0
- package/dist/directives/Public.d.ts +4 -0
- package/dist/directives/Public.d.ts.map +1 -0
- package/dist/directives/Public.js +30 -0
- package/dist/directives/Public.js.map +1 -0
- package/dist/directives/index.d.ts +2 -0
- package/dist/directives/index.d.ts.map +1 -0
- package/dist/directives/index.js +2 -0
- package/dist/directives/index.js.map +1 -0
- package/dist/entitySubclasses/DuplicateRunEntity.server.d.ts +6 -0
- package/dist/entitySubclasses/DuplicateRunEntity.server.d.ts.map +1 -0
- package/dist/entitySubclasses/DuplicateRunEntity.server.js +33 -0
- package/dist/entitySubclasses/DuplicateRunEntity.server.js.map +1 -0
- package/dist/entitySubclasses/entityPermissions.server.d.ts +23 -0
- package/dist/entitySubclasses/entityPermissions.server.d.ts.map +1 -0
- package/dist/entitySubclasses/entityPermissions.server.js +93 -0
- package/dist/entitySubclasses/entityPermissions.server.js.map +1 -0
- package/dist/entitySubclasses/userViewEntity.server.d.ts +13 -0
- package/dist/entitySubclasses/userViewEntity.server.d.ts.map +1 -0
- package/dist/entitySubclasses/userViewEntity.server.js +161 -0
- package/dist/entitySubclasses/userViewEntity.server.js.map +1 -0
- package/dist/generated/generated.d.ts +6679 -0
- package/dist/generated/generated.d.ts.map +1 -0
- package/dist/generated/generated.js +40786 -0
- package/dist/generated/generated.js.map +1 -0
- package/dist/generic/DeleteOptionsInput.d.ts +5 -0
- package/dist/generic/DeleteOptionsInput.d.ts.map +1 -0
- package/dist/generic/DeleteOptionsInput.js +27 -0
- package/dist/generic/DeleteOptionsInput.js.map +1 -0
- package/dist/generic/KeyInputOutputTypes.d.ts +16 -0
- package/dist/generic/KeyInputOutputTypes.d.ts.map +1 -0
- package/dist/generic/KeyInputOutputTypes.js +65 -0
- package/dist/generic/KeyInputOutputTypes.js.map +1 -0
- package/dist/generic/KeyValuePairInput.d.ts +5 -0
- package/dist/generic/KeyValuePairInput.d.ts.map +1 -0
- package/dist/generic/KeyValuePairInput.js +27 -0
- package/dist/generic/KeyValuePairInput.js.map +1 -0
- package/dist/generic/PushStatusResolver.d.ts +14 -0
- package/dist/generic/PushStatusResolver.d.ts.map +1 -0
- package/dist/generic/PushStatusResolver.js +58 -0
- package/dist/generic/PushStatusResolver.js.map +1 -0
- package/dist/generic/ResolverBase.d.ts +39 -0
- package/dist/generic/ResolverBase.d.ts.map +1 -0
- package/dist/generic/ResolverBase.js +573 -0
- package/dist/generic/ResolverBase.js.map +1 -0
- package/dist/generic/RunViewResolver.d.ts +123 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -0
- package/dist/generic/RunViewResolver.js +728 -0
- package/dist/generic/RunViewResolver.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +108 -0
- package/dist/index.js.map +1 -0
- package/dist/orm.d.ts +4 -0
- package/dist/orm.d.ts.map +1 -0
- package/dist/orm.js +32 -0
- package/dist/orm.js.map +1 -0
- package/dist/resolvers/AskSkipResolver.d.ts +54 -0
- package/dist/resolvers/AskSkipResolver.d.ts.map +1 -0
- package/dist/resolvers/AskSkipResolver.js +805 -0
- package/dist/resolvers/AskSkipResolver.js.map +1 -0
- package/dist/resolvers/ColorResolver.d.ts +23 -0
- package/dist/resolvers/ColorResolver.d.ts.map +1 -0
- package/dist/resolvers/ColorResolver.js +97 -0
- package/dist/resolvers/ColorResolver.js.map +1 -0
- package/dist/resolvers/DatasetResolver.d.ts +42 -0
- package/dist/resolvers/DatasetResolver.d.ts.map +1 -0
- package/dist/resolvers/DatasetResolver.js +179 -0
- package/dist/resolvers/DatasetResolver.js.map +1 -0
- package/dist/resolvers/EntityCommunicationsResolver.d.ts +50 -0
- package/dist/resolvers/EntityCommunicationsResolver.d.ts.map +1 -0
- package/dist/resolvers/EntityCommunicationsResolver.js +280 -0
- package/dist/resolvers/EntityCommunicationsResolver.js.map +1 -0
- package/dist/resolvers/EntityRecordNameResolver.d.ts +21 -0
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -0
- package/dist/resolvers/EntityRecordNameResolver.js +117 -0
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -0
- package/dist/resolvers/EntityResolver.d.ts +6 -0
- package/dist/resolvers/EntityResolver.d.ts.map +1 -0
- package/dist/resolvers/EntityResolver.js +57 -0
- package/dist/resolvers/EntityResolver.js.map +1 -0
- package/dist/resolvers/FileCategoryResolver.d.ts +6 -0
- package/dist/resolvers/FileCategoryResolver.d.ts.map +1 -0
- package/dist/resolvers/FileCategoryResolver.js +61 -0
- package/dist/resolvers/FileCategoryResolver.js.map +1 -0
- package/dist/resolvers/FileResolver.d.ts +24 -0
- package/dist/resolvers/FileResolver.d.ts.map +1 -0
- package/dist/resolvers/FileResolver.js +166 -0
- package/dist/resolvers/FileResolver.js.map +1 -0
- package/dist/resolvers/MergeRecordsResolver.d.ts +59 -0
- package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -0
- package/dist/resolvers/MergeRecordsResolver.js +282 -0
- package/dist/resolvers/MergeRecordsResolver.js.map +1 -0
- package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts +29 -0
- package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts.map +1 -0
- package/dist/resolvers/PotentialDuplicateRecordResolver.js +125 -0
- package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -0
- package/dist/resolvers/QueryResolver.d.ts +13 -0
- package/dist/resolvers/QueryResolver.d.ts.map +1 -0
- package/dist/resolvers/QueryResolver.js +77 -0
- package/dist/resolvers/QueryResolver.js.map +1 -0
- package/dist/resolvers/ReportResolver.d.ts +20 -0
- package/dist/resolvers/ReportResolver.d.ts.map +1 -0
- package/dist/resolvers/ReportResolver.js +182 -0
- package/dist/resolvers/ReportResolver.js.map +1 -0
- package/dist/resolvers/UserFavoriteResolver.d.ts +42 -0
- package/dist/resolvers/UserFavoriteResolver.d.ts.map +1 -0
- package/dist/resolvers/UserFavoriteResolver.js +230 -0
- package/dist/resolvers/UserFavoriteResolver.js.map +1 -0
- package/dist/resolvers/UserResolver.d.ts +10 -0
- package/dist/resolvers/UserResolver.d.ts.map +1 -0
- package/dist/resolvers/UserResolver.js +71 -0
- package/dist/resolvers/UserResolver.js.map +1 -0
- package/dist/resolvers/UserViewResolver.d.ts +13 -0
- package/dist/resolvers/UserViewResolver.d.ts.map +1 -0
- package/dist/resolvers/UserViewResolver.js +99 -0
- package/dist/resolvers/UserViewResolver.js.map +1 -0
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +4 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +85 -0
- package/dist/util.js.map +1 -0
- package/package.json +25 -21
- package/CHANGELOG.json +0 -3248
- package/CHANGELOG.md +0 -710
- package/src/apolloServer/TransactionPlugin.ts +0 -57
- package/src/apolloServer/index.ts +0 -33
- package/src/auth/exampleNewUserSubClass.ts +0 -74
- package/src/auth/index.ts +0 -151
- package/src/auth/tokenExpiredError.ts +0 -12
- package/src/context.ts +0 -111
- package/src/directives/Public.ts +0 -42
- package/src/directives/index.ts +0 -1
- package/src/entitySubclasses/DuplicateRunEntity.server.ts +0 -29
- package/src/entitySubclasses/entityPermissions.server.ts +0 -111
- package/src/entitySubclasses/userViewEntity.server.ts +0 -187
- package/src/generated/generated.ts +0 -25369
- package/src/generic/DeleteOptionsInput.ts +0 -13
- package/src/generic/KeyInputOutputTypes.ts +0 -35
- package/src/generic/KeyValuePairInput.ts +0 -14
- package/src/generic/PushStatusResolver.ts +0 -40
- package/src/generic/ResolverBase.ts +0 -767
- package/src/generic/RunViewResolver.ts +0 -582
- package/src/index.ts +0 -161
- package/src/orm.ts +0 -36
- package/src/resolvers/AskSkipResolver.ts +0 -926
- package/src/resolvers/ColorResolver.ts +0 -61
- package/src/resolvers/DatasetResolver.ts +0 -115
- package/src/resolvers/EntityCommunicationsResolver.ts +0 -216
- package/src/resolvers/EntityRecordNameResolver.ts +0 -78
- package/src/resolvers/EntityResolver.ts +0 -37
- package/src/resolvers/FileCategoryResolver.ts +0 -62
- package/src/resolvers/FileResolver.ts +0 -147
- package/src/resolvers/MergeRecordsResolver.ts +0 -182
- package/src/resolvers/PotentialDuplicateRecordResolver.ts +0 -91
- package/src/resolvers/QueryResolver.ts +0 -42
- package/src/resolvers/ReportResolver.ts +0 -147
- package/src/resolvers/UserFavoriteResolver.ts +0 -166
- package/src/resolvers/UserResolver.ts +0 -33
- package/src/resolvers/UserViewResolver.ts +0 -64
- package/src/types.ts +0 -40
- package/src/util.ts +0 -106
- package/tsconfig.json +0 -30
- package/typedoc.json +0 -4
|
@@ -1,767 +0,0 @@
|
|
|
1
|
-
import { BaseEntity, CompositeKey, EntityFieldTSType, EntityPermissionType, LogError, Metadata, RunView, RunViewParams, RunViewResult, UserInfo } from '@memberjunction/core';
|
|
2
|
-
import { AuditLogEntity, UserViewEntity } from '@memberjunction/core-entities';
|
|
3
|
-
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
4
|
-
import { PubSubEngine } from 'type-graphql';
|
|
5
|
-
import { GraphQLError } from 'graphql';
|
|
6
|
-
import { DataSource } from 'typeorm';
|
|
7
|
-
|
|
8
|
-
import { RunViewGenericParams, UserPayload } from '../types';
|
|
9
|
-
import { RunDynamicViewInput, RunViewByIDInput, RunViewByNameInput } from './RunViewResolver';
|
|
10
|
-
import { DeleteOptionsInput } from './DeleteOptionsInput';
|
|
11
|
-
import { MJGlobal } from '@memberjunction/global';
|
|
12
|
-
import { PUSH_STATUS_UPDATES_TOPIC } from './PushStatusResolver';
|
|
13
|
-
import { FieldMapper } from '@memberjunction/graphql-dataprovider';
|
|
14
|
-
|
|
15
|
-
export class ResolverBase {
|
|
16
|
-
protected MapFieldNamesToCodeNames(entityName: string, dataObject: any) {
|
|
17
|
-
// for the given entity name provided, check to see if there are any fields
|
|
18
|
-
// where the code name is different from the field name, and for just those
|
|
19
|
-
// fields, iterate through the dataObject and REPLACE the property that has the field name
|
|
20
|
-
// with the CodeName, because we can't transfer those via GraphQL as they are not
|
|
21
|
-
// valid property names in GraphQL
|
|
22
|
-
if (dataObject) {
|
|
23
|
-
const md = new Metadata();
|
|
24
|
-
const entityInfo = md.Entities.find((e) => e.Name === entityName);
|
|
25
|
-
if (!entityInfo) throw new Error(`Entity ${entityName} not found in metadata`);
|
|
26
|
-
// const fields = entityInfo.Fields.filter((f) => f.Name !== f.CodeName || f.Name.startsWith('__mj_'));
|
|
27
|
-
const mapper = new FieldMapper();
|
|
28
|
-
entityInfo.Fields.forEach((f) => {
|
|
29
|
-
if (dataObject.hasOwnProperty(f.Name)) {
|
|
30
|
-
// GraphQL doesn't allow us to pass back fields with __ so we are mapping our special field cases that start with __mj_ to _mj__ for transport - they are converted back on the other side automatically
|
|
31
|
-
const mappedFieldName = mapper.MapFieldName(f.CodeName)
|
|
32
|
-
if (mappedFieldName !== f.Name) {
|
|
33
|
-
dataObject[mappedFieldName] = dataObject[f.Name];
|
|
34
|
-
delete dataObject[f.Name];
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
return dataObject;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
protected ArrayMapFieldNamesToCodeNames(entityName: string, dataObjectArray: []) {
|
|
43
|
-
// iterate through the array and call MapFieldNamesToCodeNames for each element
|
|
44
|
-
if (dataObjectArray && dataObjectArray.length > 0) {
|
|
45
|
-
dataObjectArray.forEach((element) => {
|
|
46
|
-
this.MapFieldNamesToCodeNames(entityName, element);
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
return dataObjectArray;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
protected async findBy(dataSource: DataSource, entity: string, params: any) {
|
|
53
|
-
// build the SQL query based on the params passed in
|
|
54
|
-
const md = new Metadata();
|
|
55
|
-
const e = md.Entities.find((e) => e.Name === entity);
|
|
56
|
-
if (!e) throw new Error(`Entity ${entity} not found in metadata`);
|
|
57
|
-
// now build a SQL string using the entityInfo and using the properties in the params object
|
|
58
|
-
let sql = `SELECT * FROM ${e.SchemaName}.${e.BaseView} WHERE `;
|
|
59
|
-
const keys = Object.keys(params);
|
|
60
|
-
keys.forEach((k, i) => {
|
|
61
|
-
if (i > 0) sql += ' AND ';
|
|
62
|
-
// look up the field in the entityInfo to see if it needs quotes
|
|
63
|
-
const field = e.Fields.find((f) => f.Name === k);
|
|
64
|
-
if (!field) throw new Error(`Field ${k} not found in entity ${entity}`);
|
|
65
|
-
const quotes = field.NeedsQuotes ? "'" : '';
|
|
66
|
-
sql += `${k} = ${quotes}${params[k]}${quotes}`;
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// ok, now we have a SQL string, run it and return the results
|
|
70
|
-
const result = await dataSource.query(sql);
|
|
71
|
-
return result;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async RunViewByNameGeneric(viewInput: RunViewByNameInput, dataSource: DataSource, userPayload: UserPayload, pubSub: PubSubEngine) {
|
|
75
|
-
try {
|
|
76
|
-
const viewInfo: UserViewEntity = this.safeFirstArrayElement(
|
|
77
|
-
await this.findBy(dataSource, 'User Views', { Name: viewInput.ViewName })
|
|
78
|
-
);
|
|
79
|
-
return this.RunViewGenericInternal(
|
|
80
|
-
viewInfo,
|
|
81
|
-
dataSource,
|
|
82
|
-
viewInput.ExtraFilter,
|
|
83
|
-
viewInput.OrderBy,
|
|
84
|
-
viewInput.UserSearchString,
|
|
85
|
-
viewInput.ExcludeUserViewRunID,
|
|
86
|
-
viewInput.OverrideExcludeFilter,
|
|
87
|
-
viewInput.SaveViewResults,
|
|
88
|
-
viewInput.Fields,
|
|
89
|
-
viewInput.IgnoreMaxRows,
|
|
90
|
-
viewInput.ExcludeDataFromAllPriorViewRuns,
|
|
91
|
-
viewInput.ForceAuditLog,
|
|
92
|
-
viewInput.AuditLogDescription,
|
|
93
|
-
viewInput.ResultType,
|
|
94
|
-
userPayload,
|
|
95
|
-
pubSub
|
|
96
|
-
);
|
|
97
|
-
} catch (err) {
|
|
98
|
-
console.log(err);
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async RunViewByIDGeneric(viewInput: RunViewByIDInput, dataSource: DataSource, userPayload: UserPayload, pubSub: PubSubEngine) {
|
|
104
|
-
try {
|
|
105
|
-
const viewInfo: UserViewEntity = this.safeFirstArrayElement(await this.findBy(dataSource, 'User Views', { ID: viewInput.ViewID }));
|
|
106
|
-
return this.RunViewGenericInternal(
|
|
107
|
-
viewInfo,
|
|
108
|
-
dataSource,
|
|
109
|
-
viewInput.ExtraFilter,
|
|
110
|
-
viewInput.OrderBy,
|
|
111
|
-
viewInput.UserSearchString,
|
|
112
|
-
viewInput.ExcludeUserViewRunID,
|
|
113
|
-
viewInput.OverrideExcludeFilter,
|
|
114
|
-
viewInput.SaveViewResults,
|
|
115
|
-
viewInput.Fields,
|
|
116
|
-
viewInput.IgnoreMaxRows,
|
|
117
|
-
viewInput.ExcludeDataFromAllPriorViewRuns,
|
|
118
|
-
viewInput.ForceAuditLog,
|
|
119
|
-
viewInput.AuditLogDescription,
|
|
120
|
-
viewInput.ResultType,
|
|
121
|
-
userPayload,
|
|
122
|
-
pubSub
|
|
123
|
-
);
|
|
124
|
-
} catch (err) {
|
|
125
|
-
console.log(err);
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async RunDynamicViewGeneric(viewInput: RunDynamicViewInput, dataSource: DataSource, userPayload: UserPayload, pubSub: PubSubEngine) {
|
|
131
|
-
try {
|
|
132
|
-
const md = new Metadata();
|
|
133
|
-
const entity = md.Entities.find((e) => e.Name === viewInput.EntityName);
|
|
134
|
-
if (!entity) throw new Error(`Entity ${viewInput.EntityName} not found in metadata`);
|
|
135
|
-
|
|
136
|
-
const viewInfo: UserViewEntity = {
|
|
137
|
-
ID: "",
|
|
138
|
-
Entity: viewInput.EntityName,
|
|
139
|
-
EntityID: entity.ID,
|
|
140
|
-
EntityBaseView: entity.BaseView as string,
|
|
141
|
-
} as UserViewEntity; // only providing a few bits of data here, but it's enough to get the view to run
|
|
142
|
-
|
|
143
|
-
return this.RunViewGenericInternal(
|
|
144
|
-
viewInfo,
|
|
145
|
-
dataSource,
|
|
146
|
-
viewInput.ExtraFilter,
|
|
147
|
-
viewInput.OrderBy,
|
|
148
|
-
viewInput.UserSearchString,
|
|
149
|
-
viewInput.ExcludeUserViewRunID,
|
|
150
|
-
viewInput.OverrideExcludeFilter,
|
|
151
|
-
false,
|
|
152
|
-
viewInput.Fields,
|
|
153
|
-
viewInput.IgnoreMaxRows,
|
|
154
|
-
false,
|
|
155
|
-
viewInput.ForceAuditLog,
|
|
156
|
-
viewInput.AuditLogDescription,
|
|
157
|
-
viewInput.ResultType,
|
|
158
|
-
userPayload,
|
|
159
|
-
pubSub
|
|
160
|
-
);
|
|
161
|
-
} catch (err) {
|
|
162
|
-
console.log(err);
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
async RunViewsGeneric(viewInputs: (RunViewByNameInput & RunViewByIDInput & RunDynamicViewInput)[], dataSource: DataSource, userPayload: UserPayload, pubSub: PubSubEngine) {
|
|
168
|
-
let md: Metadata | null = null;
|
|
169
|
-
let params: RunViewGenericParams[] = [];
|
|
170
|
-
for(const viewInput of viewInputs) {
|
|
171
|
-
try {
|
|
172
|
-
let viewInfo: UserViewEntity | null = null;
|
|
173
|
-
|
|
174
|
-
if(viewInput.ViewName) {
|
|
175
|
-
viewInfo = this.safeFirstArrayElement(
|
|
176
|
-
await this.findBy(dataSource, 'User Views', { Name: viewInput.ViewName })
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
else if(viewInput.ViewID) {
|
|
180
|
-
viewInfo = this.safeFirstArrayElement(await this.findBy(dataSource, 'User Views', { ID: viewInput.ViewID }));
|
|
181
|
-
}
|
|
182
|
-
else if(viewInput.EntityName) {
|
|
183
|
-
md = md || new Metadata();
|
|
184
|
-
const entity = md.Entities.find((e) => e.Name === viewInput.EntityName);
|
|
185
|
-
if (!entity) {
|
|
186
|
-
throw new Error(`Entity ${viewInput.EntityName} not found in metadata`);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// only providing a few bits of data here, but it's enough to get the view to run
|
|
190
|
-
viewInfo = {
|
|
191
|
-
ID: "",
|
|
192
|
-
Entity: viewInput.EntityName,
|
|
193
|
-
EntityID: entity.ID,
|
|
194
|
-
EntityBaseView: entity.BaseView,
|
|
195
|
-
} as UserViewEntity;
|
|
196
|
-
}
|
|
197
|
-
else{
|
|
198
|
-
throw new Error("Unable to determine input type");
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
params.push({
|
|
202
|
-
viewInfo: viewInfo,
|
|
203
|
-
dataSource: dataSource,
|
|
204
|
-
extraFilter: viewInput.ExtraFilter,
|
|
205
|
-
orderBy: viewInput.OrderBy,
|
|
206
|
-
userSearchString: viewInput.UserSearchString,
|
|
207
|
-
excludeUserViewRunID: viewInput.ExcludeUserViewRunID,
|
|
208
|
-
overrideExcludeFilter: viewInput.OverrideExcludeFilter,
|
|
209
|
-
saveViewResults: viewInput.EntityName ? false : viewInput.SaveViewResults,
|
|
210
|
-
fields: viewInput.Fields,
|
|
211
|
-
ignoreMaxRows: viewInput.IgnoreMaxRows,
|
|
212
|
-
excludeDataFromAllPriorViewRuns: viewInput.EntityName ? false : viewInput.ExcludeDataFromAllPriorViewRuns,
|
|
213
|
-
forceAuditLog: viewInput.ForceAuditLog,
|
|
214
|
-
auditLogDescription: viewInput.AuditLogDescription,
|
|
215
|
-
resultType: viewInput.ResultType,
|
|
216
|
-
userPayload,
|
|
217
|
-
pubSub
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
} catch (err) {
|
|
221
|
-
LogError(err);
|
|
222
|
-
return null;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
let results: RunViewResult[] = await this.RunViewsGenericInternal(params);
|
|
227
|
-
return results;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
protected CheckUserReadPermissions(entityName: string, userPayload: UserPayload | null) {
|
|
233
|
-
const md = new Metadata();
|
|
234
|
-
const entityInfo = md.Entities.find((e) => e.Name === entityName);
|
|
235
|
-
if (!userPayload) throw new Error(`userPayload is null`);
|
|
236
|
-
|
|
237
|
-
// first check permissions, the logged in user must have read permissions on the entity to run the view
|
|
238
|
-
if (entityInfo) {
|
|
239
|
-
const userInfo = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload.email.toLowerCase().trim()); // get the user record from MD so we have ROLES attached, don't use the one from payload directly
|
|
240
|
-
if (!userInfo) throw new Error(`User ${userPayload.email} not found in metadata`);
|
|
241
|
-
|
|
242
|
-
const userPermissions = entityInfo.GetUserPermisions(userInfo);
|
|
243
|
-
if (!userPermissions.CanRead) throw new Error(`User ${userPayload.email} does not have read permissions on ${entityInfo.Name}`);
|
|
244
|
-
} else throw new Error(`Entity not found in metadata`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
protected async RunViewGenericInternal(
|
|
248
|
-
viewInfo: UserViewEntity,
|
|
249
|
-
dataSource: DataSource,
|
|
250
|
-
extraFilter: string,
|
|
251
|
-
orderBy: string,
|
|
252
|
-
userSearchString: string,
|
|
253
|
-
excludeUserViewRunID: string | undefined,
|
|
254
|
-
overrideExcludeFilter: string | undefined,
|
|
255
|
-
saveViewResults: boolean | undefined,
|
|
256
|
-
fields: string[] | undefined,
|
|
257
|
-
ignoreMaxRows: boolean | undefined,
|
|
258
|
-
excludeDataFromAllPriorViewRuns: boolean | undefined,
|
|
259
|
-
forceAuditLog: boolean | undefined,
|
|
260
|
-
auditLogDescription: string | undefined,
|
|
261
|
-
resultType: string | undefined,
|
|
262
|
-
userPayload: UserPayload | null,
|
|
263
|
-
pubSub: PubSubEngine
|
|
264
|
-
) {
|
|
265
|
-
try {
|
|
266
|
-
if (viewInfo && userPayload) {
|
|
267
|
-
const md = new Metadata();
|
|
268
|
-
const user = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload?.email.toLowerCase().trim());
|
|
269
|
-
if (!user) throw new Error(`User ${userPayload?.email} not found in metadata`);
|
|
270
|
-
|
|
271
|
-
const entityInfo = md.Entities.find((e) => e.Name === viewInfo.Entity);
|
|
272
|
-
if (!entityInfo) throw new Error(`Entity ${viewInfo.Entity} not found in metadata`);
|
|
273
|
-
|
|
274
|
-
const rv = new RunView();
|
|
275
|
-
|
|
276
|
-
// figure out the result type from the input string (if provided)
|
|
277
|
-
let rt: 'simple' | 'entity_object' | 'count_only' = 'simple';
|
|
278
|
-
switch (resultType?.trim().toLowerCase()) {
|
|
279
|
-
case 'count_only':
|
|
280
|
-
rt = 'count_only';
|
|
281
|
-
break;
|
|
282
|
-
case 'entity_object':
|
|
283
|
-
default:
|
|
284
|
-
rt = 'simple'; // use simple as the default AND for entity_object becuase on teh server we don't really pass back a true entity_object anyway, just passing back the simple object anyway
|
|
285
|
-
break;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const result = await rv.RunView(
|
|
289
|
-
{
|
|
290
|
-
ViewID: viewInfo.ID,
|
|
291
|
-
ViewName: viewInfo.Name,
|
|
292
|
-
EntityName: viewInfo.Entity,
|
|
293
|
-
ExtraFilter: extraFilter,
|
|
294
|
-
OrderBy: orderBy,
|
|
295
|
-
Fields: fields,
|
|
296
|
-
UserSearchString: userSearchString,
|
|
297
|
-
ExcludeUserViewRunID: excludeUserViewRunID,
|
|
298
|
-
OverrideExcludeFilter: overrideExcludeFilter,
|
|
299
|
-
SaveViewResults: saveViewResults,
|
|
300
|
-
ExcludeDataFromAllPriorViewRuns: excludeDataFromAllPriorViewRuns,
|
|
301
|
-
IgnoreMaxRows: ignoreMaxRows,
|
|
302
|
-
ForceAuditLog: forceAuditLog,
|
|
303
|
-
AuditLogDescription: auditLogDescription,
|
|
304
|
-
ResultType: rt,
|
|
305
|
-
},
|
|
306
|
-
user
|
|
307
|
-
);
|
|
308
|
-
// go through the result and convert all fields that start with __mj_*** to _mj__*** for GraphQL transport
|
|
309
|
-
const mapper = new FieldMapper();
|
|
310
|
-
if (result && result.Success) {
|
|
311
|
-
for (const r of result.Results) {
|
|
312
|
-
mapper.MapFields(r);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return result;
|
|
316
|
-
} else return null;
|
|
317
|
-
} catch (err) {
|
|
318
|
-
console.log(err);
|
|
319
|
-
throw err;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
protected async RunViewsGenericInternal(params: RunViewGenericParams[]): Promise<RunViewResult[]> {
|
|
324
|
-
try {
|
|
325
|
-
let md: Metadata | null = null;
|
|
326
|
-
const rv = new RunView();
|
|
327
|
-
let RunViewParams: RunViewParams[] = [];
|
|
328
|
-
let contextUser: UserInfo | null = null;
|
|
329
|
-
for(const param of params){
|
|
330
|
-
if (param.viewInfo && param.userPayload) {
|
|
331
|
-
md = md || new Metadata();
|
|
332
|
-
const user: UserInfo = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === param.userPayload?.email.toLowerCase().trim());
|
|
333
|
-
if (!user) {
|
|
334
|
-
throw new Error(`User ${param.userPayload?.email} not found in metadata`);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
contextUser = contextUser || user;
|
|
338
|
-
|
|
339
|
-
const entityInfo = md.Entities.find((e) => e.Name === param.viewInfo.Entity);
|
|
340
|
-
if (!entityInfo){
|
|
341
|
-
throw new Error(`Entity ${param.viewInfo.Entity} not found in metadata`);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// figure out the result type from the input string (if provided)
|
|
346
|
-
let rt: 'simple' | 'entity_object' | 'count_only' = 'simple';
|
|
347
|
-
switch (param.resultType?.trim().toLowerCase()) {
|
|
348
|
-
case 'count_only':
|
|
349
|
-
rt = 'count_only';
|
|
350
|
-
break;
|
|
351
|
-
// use simple as the default AND for entity_object
|
|
352
|
-
// becuase on teh server we don't really pass back
|
|
353
|
-
// a true entity_object anyway, just passing back
|
|
354
|
-
// the simple object anyway
|
|
355
|
-
case 'entity_object':
|
|
356
|
-
default:
|
|
357
|
-
rt = 'simple';
|
|
358
|
-
break;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
RunViewParams.push({
|
|
362
|
-
ViewID: param.viewInfo.ID,
|
|
363
|
-
ViewName: param.viewInfo.Name,
|
|
364
|
-
EntityName: param.viewInfo.Entity,
|
|
365
|
-
ExtraFilter: param.extraFilter,
|
|
366
|
-
OrderBy: param.orderBy,
|
|
367
|
-
Fields: param.fields,
|
|
368
|
-
UserSearchString: param.userSearchString,
|
|
369
|
-
ExcludeUserViewRunID: param.excludeUserViewRunID,
|
|
370
|
-
OverrideExcludeFilter: param.overrideExcludeFilter,
|
|
371
|
-
SaveViewResults: param.saveViewResults,
|
|
372
|
-
ExcludeDataFromAllPriorViewRuns: param.excludeDataFromAllPriorViewRuns,
|
|
373
|
-
IgnoreMaxRows: param.ignoreMaxRows,
|
|
374
|
-
ForceAuditLog: param.forceAuditLog,
|
|
375
|
-
AuditLogDescription: param.auditLogDescription,
|
|
376
|
-
ResultType: rt,
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
let runViewResults: RunViewResult[] = await rv.RunViews(RunViewParams, contextUser);
|
|
381
|
-
|
|
382
|
-
// go through the result and convert all fields that start with __mj_*** to _mj__*** for GraphQL transport
|
|
383
|
-
const mapper = new FieldMapper();
|
|
384
|
-
for(const runViewResult of runViewResults){
|
|
385
|
-
if (runViewResult && runViewResult.Success) {
|
|
386
|
-
for (const result of runViewResult.Results) {
|
|
387
|
-
mapper.MapFields(result);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
return runViewResults;
|
|
393
|
-
}
|
|
394
|
-
catch (err) {
|
|
395
|
-
console.log(err);
|
|
396
|
-
throw err;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
protected async createRecordAccessAuditLogRecord(userPayload: UserPayload, entityName: string, recordId: any): Promise<any> {
|
|
401
|
-
try {
|
|
402
|
-
const md = new Metadata();
|
|
403
|
-
const entityInfo = md.Entities.find((e) => e.Name.trim().toLowerCase() === entityName.trim().toLowerCase());
|
|
404
|
-
if (!entityInfo) throw new Error(`Entity ${entityName} not found in metadata`);
|
|
405
|
-
|
|
406
|
-
if (entityInfo.AuditRecordAccess) {
|
|
407
|
-
const userInfo = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload?.email.toLowerCase().trim());
|
|
408
|
-
const auditLogTypeName = 'Record Accessed';
|
|
409
|
-
const auditLogType = md.AuditLogTypes.find((a) => a.Name.trim().toLowerCase() === auditLogTypeName.trim().toLowerCase());
|
|
410
|
-
|
|
411
|
-
if (!userInfo) throw new Error(`User ${userPayload?.email} not found in metadata`);
|
|
412
|
-
if (!auditLogType) throw new Error(`Audit Log Type ${auditLogTypeName} not found in metadata`);
|
|
413
|
-
|
|
414
|
-
return await this.createAuditLogRecord(userPayload, null, auditLogTypeName, 'Success', null, entityInfo.ID, recordId);
|
|
415
|
-
}
|
|
416
|
-
} catch (e) {
|
|
417
|
-
console.log(e);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
protected getRowLevelSecurityWhereClause(entityName: string, userPayload: UserPayload, type: EntityPermissionType, returnPrefix: string) {
|
|
422
|
-
const md = new Metadata();
|
|
423
|
-
const entityInfo = md.Entities.find((e) => e.Name.trim().toLowerCase() === entityName.trim().toLowerCase());
|
|
424
|
-
if (!entityInfo) throw new Error(`Entity ${entityName} not found in metadata`);
|
|
425
|
-
const user = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload?.email.toLowerCase().trim());
|
|
426
|
-
if (!user) throw new Error(`User ${userPayload?.email} not found in metadata`);
|
|
427
|
-
|
|
428
|
-
return entityInfo.GetUserRowLevelSecurityWhereClause(user, type, returnPrefix);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
protected async createAuditLogRecord(
|
|
432
|
-
userPayload: UserPayload,
|
|
433
|
-
authorizationName: string | null,
|
|
434
|
-
auditLogTypeName: string,
|
|
435
|
-
status: string,
|
|
436
|
-
details: string | null,
|
|
437
|
-
entityId: string,
|
|
438
|
-
recordId: any | null
|
|
439
|
-
): Promise<any> {
|
|
440
|
-
try {
|
|
441
|
-
const md = new Metadata();
|
|
442
|
-
const userInfo = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload?.email.toLowerCase().trim());
|
|
443
|
-
const authorization = authorizationName
|
|
444
|
-
? md.Authorizations.find((a) => a.Name.trim().toLowerCase() === authorizationName.trim().toLowerCase())
|
|
445
|
-
: null;
|
|
446
|
-
const auditLogType = md.AuditLogTypes.find((a) => a.Name.trim().toLowerCase() === auditLogTypeName.trim().toLowerCase());
|
|
447
|
-
|
|
448
|
-
if (!userInfo) throw new Error(`User ${userPayload?.email} not found in metadata`);
|
|
449
|
-
if (!auditLogType) throw new Error(`Audit Log Type ${auditLogTypeName} not found in metadata`);
|
|
450
|
-
|
|
451
|
-
const auditLog = await md.GetEntityObject<AuditLogEntity>('Audit Logs', userInfo); // must pass user context on back end as we're not authenticated the same way as the front end
|
|
452
|
-
auditLog.NewRecord();
|
|
453
|
-
auditLog.UserID = userInfo.ID;
|
|
454
|
-
auditLog.AuditLogTypeID = auditLogType.ID;
|
|
455
|
-
|
|
456
|
-
if (authorization)
|
|
457
|
-
auditLog.AuthorizationID = authorization.ID;
|
|
458
|
-
|
|
459
|
-
if (status?.trim().toLowerCase() === 'success') auditLog.Status = 'Success';
|
|
460
|
-
else auditLog.Status = 'Failed';
|
|
461
|
-
|
|
462
|
-
if (details) auditLog.Details = details;
|
|
463
|
-
|
|
464
|
-
auditLog.EntityID = entityId;
|
|
465
|
-
|
|
466
|
-
if (recordId) auditLog.RecordID = recordId;
|
|
467
|
-
|
|
468
|
-
if (await auditLog.Save()) return auditLog;
|
|
469
|
-
else throw new Error(`Error saving audit log record`);
|
|
470
|
-
} catch (err) {
|
|
471
|
-
console.log(err);
|
|
472
|
-
return null;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
protected safeFirstArrayElement(arr: any[]) {
|
|
477
|
-
if (arr && arr.length > 0) {
|
|
478
|
-
return arr[0];
|
|
479
|
-
}
|
|
480
|
-
return null;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
protected packageSPParam(paramValue: any, quoteString: string) {
|
|
484
|
-
return paramValue === null || paramValue === undefined ? null : quoteString + paramValue + quoteString;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
protected GetUserFromEmail(email: string): UserInfo | undefined {
|
|
488
|
-
const md = new Metadata();
|
|
489
|
-
return UserCache.Users.find((u) => u.Email.toLowerCase().trim() === email.toLowerCase().trim());
|
|
490
|
-
}
|
|
491
|
-
protected GetUserFromPayload(userPayload: UserPayload): UserInfo | undefined {
|
|
492
|
-
if (!userPayload || !userPayload.email) return undefined;
|
|
493
|
-
|
|
494
|
-
const md = new Metadata();
|
|
495
|
-
return UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload.email.toLowerCase().trim());
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
public get MJCoreSchema(): string {
|
|
499
|
-
return Metadata.Provider.ConfigData.MJCoreSchemaName;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
protected ListenForEntityMessages(entityObject: BaseEntity, pubSub: PubSubEngine, userPayload: UserPayload) {
|
|
503
|
-
// listen for events from the entityObject in case it is a long running task and we can push messages back to the client via pubSub
|
|
504
|
-
MJGlobal.Instance.GetEventListener(false).subscribe((event) => {
|
|
505
|
-
if (event) {
|
|
506
|
-
if (event.component === entityObject && event.args && event.args.message) {
|
|
507
|
-
// message from our entity object, relay it to the client
|
|
508
|
-
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
509
|
-
message: JSON.stringify({
|
|
510
|
-
status: 'OK',
|
|
511
|
-
type: 'EntityObjectStatusMessage',
|
|
512
|
-
entityName: entityObject.EntityInfo.Name,
|
|
513
|
-
primaryKey: entityObject.PrimaryKey,
|
|
514
|
-
message: event.args.message,
|
|
515
|
-
}),
|
|
516
|
-
sessionId: userPayload.sessionId,
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
protected async CreateRecord(entityName: string, input: any, dataSource: DataSource, userPayload: UserPayload, pubSub: PubSubEngine) {
|
|
524
|
-
if (await this.BeforeCreate(dataSource, input)) {
|
|
525
|
-
// fire event and proceed if it wasn't cancelled
|
|
526
|
-
const entityObject = await new Metadata().GetEntityObject(entityName, this.GetUserFromPayload(userPayload));
|
|
527
|
-
entityObject.NewRecord();
|
|
528
|
-
entityObject.SetMany(input);
|
|
529
|
-
|
|
530
|
-
this.ListenForEntityMessages(entityObject, pubSub, userPayload);
|
|
531
|
-
|
|
532
|
-
if (await entityObject.Save()) {
|
|
533
|
-
// save worked, fire the AfterCreate event and then return all the data
|
|
534
|
-
await this.AfterCreate(dataSource, input); // fire event
|
|
535
|
-
return this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll());
|
|
536
|
-
}
|
|
537
|
-
// save failed, return null
|
|
538
|
-
else throw entityObject.LatestResult.Message;
|
|
539
|
-
} else return null;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// Before/After CREATE Event Hooks for Sub-Classes to Override
|
|
543
|
-
protected async BeforeCreate(dataSource: DataSource, input: any): Promise<boolean> {
|
|
544
|
-
return true;
|
|
545
|
-
}
|
|
546
|
-
protected async AfterCreate(dataSource: DataSource, input: any) {}
|
|
547
|
-
|
|
548
|
-
protected async UpdateRecord(entityName: string, input: any, dataSource: DataSource, userPayload: UserPayload, pubSub: PubSubEngine) {
|
|
549
|
-
if (await this.BeforeUpdate(dataSource, input)) {
|
|
550
|
-
// fire event and proceed if it wasn't cancelled
|
|
551
|
-
const md = new Metadata();
|
|
552
|
-
const userInfo = this.GetUserFromPayload(userPayload);
|
|
553
|
-
const entityObject = await md.GetEntityObject(entityName, userInfo);
|
|
554
|
-
const entityInfo = entityObject.EntityInfo;
|
|
555
|
-
const clientNewValues = {};
|
|
556
|
-
Object.keys(input).forEach((key) => {
|
|
557
|
-
if (key !== 'OldValues___')
|
|
558
|
-
clientNewValues[key] = input[key];
|
|
559
|
-
}); // grab all the props except for the OldValues property
|
|
560
|
-
|
|
561
|
-
if (entityInfo.TrackRecordChanges || !input.OldValues___) {
|
|
562
|
-
// the entity tracks record changes, so we need to load the old values from the DB to make sure they are not inconsistent
|
|
563
|
-
// with the old values from the input.OldValues property. If they are different, but on different fields, we allow it
|
|
564
|
-
// but if they are different on fields that the current UpdateRecord call is trying to update, we throw an error.
|
|
565
|
-
const cKey = new CompositeKey(
|
|
566
|
-
entityInfo.PrimaryKeys.map((pk) => {
|
|
567
|
-
return {
|
|
568
|
-
FieldName: pk.Name,
|
|
569
|
-
Value: input[pk.CodeName],
|
|
570
|
-
};
|
|
571
|
-
})
|
|
572
|
-
);
|
|
573
|
-
|
|
574
|
-
if (await entityObject.InnerLoad(cKey)) {
|
|
575
|
-
// load worked, now, if we HAVE OldValues, we need to check them against the values in the DB we just loaded.
|
|
576
|
-
if (!input.OldValues___) {
|
|
577
|
-
// no OldValues, so we can just set the new values from input
|
|
578
|
-
entityObject.SetMany(input);
|
|
579
|
-
} else {
|
|
580
|
-
// we DO have OldValues, so we need to do a more in depth analysis
|
|
581
|
-
this.TestAndSetClientOldValuesToDBValues(input, clientNewValues, entityObject);
|
|
582
|
-
}
|
|
583
|
-
} else {
|
|
584
|
-
// save failed, return null
|
|
585
|
-
throw new GraphQLError(`Record not found for ${entityName} with key ${JSON.stringify(cKey)}`, {
|
|
586
|
-
extensions: { code: 'LOAD_ENTITY_ERROR', entityName },
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
} else {
|
|
590
|
-
// not tracking changes and we DO have OldValues, so we can load from them
|
|
591
|
-
const oldValues = {};
|
|
592
|
-
// for each item in the oldValues array, add it to the oldValues object
|
|
593
|
-
input.OldValues___?.forEach((item) => (oldValues[item.Key] = item.Value));
|
|
594
|
-
|
|
595
|
-
// 1) load the old values, this will be the initial state of the object
|
|
596
|
-
entityObject.LoadFromData(oldValues);
|
|
597
|
-
|
|
598
|
-
// 2) set the new values from the input, not including the OldValues property
|
|
599
|
-
entityObject.SetMany(clientNewValues);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
this.ListenForEntityMessages(entityObject, pubSub, userPayload);
|
|
603
|
-
if (await entityObject.Save()) {
|
|
604
|
-
// save worked, fire afterevent and return all the data
|
|
605
|
-
await this.AfterUpdate(dataSource, input); // fire event
|
|
606
|
-
return this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll());
|
|
607
|
-
} else {
|
|
608
|
-
throw new GraphQLError(entityObject.LatestResult?.Message ?? 'Unknown error', {
|
|
609
|
-
extensions: { code: 'SAVE_ENTITY_ERROR', entityName },
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
} else
|
|
613
|
-
throw new GraphQLError('Save Canceled by BeforeSave() handler in ResolverBase', {
|
|
614
|
-
extensions: { code: 'SAVE_ENTITY_ERROR', entityName },
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
/**
|
|
619
|
-
* This routine compares the OldValues property in the input object to the values in the DB that we just loaded. If there are differences, we need to check to see if the client
|
|
620
|
-
* is trying to update any of those fields (e.g. overlap). If there is overlap, we throw an error. If there is no overlap, we can proceed with the update even if the DB Values
|
|
621
|
-
* and the ClientOldValues are not 100% the same, so long as there is no overlap in the specific FIELDS that are different.
|
|
622
|
-
*
|
|
623
|
-
* ASSUMES: input object has an OldValues___ property that is an array of Key/Value pairs that represent the old values of the record that the client is trying to update.
|
|
624
|
-
*/
|
|
625
|
-
protected TestAndSetClientOldValuesToDBValues(input: any, clientNewValues: any, entityObject: BaseEntity) {
|
|
626
|
-
// we have OldValues, so we need to compare them to the values we just loaded from the DB
|
|
627
|
-
const clientOldValues = {};
|
|
628
|
-
// for each item in the oldValues array, add it to the clientOldValues object
|
|
629
|
-
input.OldValues___.forEach((item) => {
|
|
630
|
-
// we need to do a quick transform on the values to make sure they match the TS Type for the given field because item.Value will always be a string
|
|
631
|
-
const field = entityObject.EntityInfo.Fields.find((f) => f.CodeName === item.Key);
|
|
632
|
-
let val = item.Value;
|
|
633
|
-
if ((val === null || val === undefined) && field.DefaultValue !== null && field.DefaultValue !== undefined && !field.AllowsNull)
|
|
634
|
-
val = field.DefaultValue; // set default value as the field was never set and it does NOT allow nulls
|
|
635
|
-
|
|
636
|
-
if (field) {
|
|
637
|
-
switch (field.TSType) {
|
|
638
|
-
case EntityFieldTSType.Number:
|
|
639
|
-
val = val !== null && val !== undefined ? parseInt(val) : null;
|
|
640
|
-
break;
|
|
641
|
-
case EntityFieldTSType.Boolean:
|
|
642
|
-
val = val === null || val === undefined || val === 'false' || val === '0' || parseInt(val) === 0 ? false : true;
|
|
643
|
-
break;
|
|
644
|
-
case EntityFieldTSType.Date:
|
|
645
|
-
// first, if val is a string and it is actually a number (milliseconds since epoch), convert it to a number.
|
|
646
|
-
if (val !== null && val !== undefined && val.toString().trim() !== '' && !isNaN(val)) val = parseInt(val);
|
|
647
|
-
|
|
648
|
-
val = val !== null && val !== undefined ? new Date(val) : null;
|
|
649
|
-
break;
|
|
650
|
-
default:
|
|
651
|
-
break; // already a string
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
clientOldValues[item.Key] = val;
|
|
655
|
-
});
|
|
656
|
-
|
|
657
|
-
// clientOldValues now has all of the oldValues the CLIENT passed us. Now we need to build the same kind of object
|
|
658
|
-
// with the DB values
|
|
659
|
-
const dbValues = entityObject.GetAll();
|
|
660
|
-
|
|
661
|
-
// now we need to compare clientOldValues and dbValues and have a new array that has entries for any differences and have FieldName, clientOldValue and dbValue as properties
|
|
662
|
-
const dbDifferences = [];
|
|
663
|
-
Object.keys(clientOldValues).forEach((key) => {
|
|
664
|
-
const f = entityObject.EntityInfo.Fields.find((f) => f.CodeName === key);
|
|
665
|
-
let different = false;
|
|
666
|
-
switch (typeof clientOldValues[key]) {
|
|
667
|
-
case 'number':
|
|
668
|
-
different = clientOldValues[key] !== dbValues[key];
|
|
669
|
-
break;
|
|
670
|
-
case 'boolean':
|
|
671
|
-
different = clientOldValues[key] !== dbValues[key];
|
|
672
|
-
break;
|
|
673
|
-
case 'object':
|
|
674
|
-
if (clientOldValues[key] instanceof Date) {
|
|
675
|
-
different = clientOldValues[key].getTime() !== dbValues[key].getTime();
|
|
676
|
-
}
|
|
677
|
-
break;
|
|
678
|
-
default:
|
|
679
|
-
different = clientOldValues[key] !== dbValues[key];
|
|
680
|
-
break;
|
|
681
|
-
}
|
|
682
|
-
if (different && f && !f.ReadOnly) {
|
|
683
|
-
// only include updateable fields
|
|
684
|
-
dbDifferences.push({
|
|
685
|
-
FieldName: key,
|
|
686
|
-
ClientOldValue: clientOldValues[key],
|
|
687
|
-
DBValue: dbValues[key],
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
if (dbDifferences.length > 0) {
|
|
693
|
-
// now we have an array of any dbDifferences with length > 0, between the clientOldValues and the dbValues, we need to check to see if any of the differences are on fields that the client is trying to update
|
|
694
|
-
// first step is to get clientNewValues into an object that is like clientOldValues, get the diff and then compare that diff to the differences array that shows diff between DB and ClientOld
|
|
695
|
-
const clientDifferences = [];
|
|
696
|
-
Object.keys(clientOldValues).forEach((key) => {
|
|
697
|
-
const f = entityObject.EntityInfo.Fields.find((f) => f.CodeName === key);
|
|
698
|
-
if (clientOldValues[key] !== clientNewValues[key] && f && f.AllowUpdateAPI && !f.IsPrimaryKey) {
|
|
699
|
-
// only include updateable fields
|
|
700
|
-
clientDifferences.push({
|
|
701
|
-
FieldName: key,
|
|
702
|
-
ClientOldValue: clientOldValues[key],
|
|
703
|
-
ClientNewValue: clientNewValues[key],
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
// now we have clientDifferences which shows what the client thinks they are changing. And, we have the dbDifferences array that shows changes between the clientOldValues and the dbValues
|
|
709
|
-
// if there is ANY overlap in the FIELDS that appear in both arrays, we need to throw an error
|
|
710
|
-
const overlap = clientDifferences.filter((cd) => dbDifferences.find((dd) => dd.FieldName === cd.FieldName));
|
|
711
|
-
if (overlap.length > 0) {
|
|
712
|
-
const msg = {
|
|
713
|
-
Message:
|
|
714
|
-
'Inconsistency between old values provided for changed fields, and the values of one or more of those fields in the database. Update operation cancelled.',
|
|
715
|
-
ClientDifferences: clientDifferences,
|
|
716
|
-
DBDifferences: dbDifferences,
|
|
717
|
-
Overlap: overlap,
|
|
718
|
-
};
|
|
719
|
-
throw new Error(JSON.stringify(msg));
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
// If we get here that means we've not thrown an exception, so there is
|
|
724
|
-
// NO OVERLAP, so we can set the new values from the data provided from the client now...
|
|
725
|
-
entityObject.SetMany(clientNewValues);
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
protected async DeleteRecord(
|
|
729
|
-
entityName: string,
|
|
730
|
-
key: CompositeKey,
|
|
731
|
-
options: DeleteOptionsInput,
|
|
732
|
-
dataSource: DataSource,
|
|
733
|
-
userPayload: UserPayload,
|
|
734
|
-
pubSub: PubSubEngine
|
|
735
|
-
) {
|
|
736
|
-
if (await this.BeforeDelete(dataSource, key)) {
|
|
737
|
-
// fire event and proceed if it wasn't cancelled
|
|
738
|
-
const entityObject = await new Metadata().GetEntityObject(entityName, this.GetUserFromPayload(userPayload));
|
|
739
|
-
await entityObject.InnerLoad(key);
|
|
740
|
-
const returnValue = entityObject.GetAll(); // grab the values before we delete so we can return last state before delete if we are successful.
|
|
741
|
-
if (await entityObject.Delete(options)) {
|
|
742
|
-
await this.AfterDelete(dataSource, key); // fire event
|
|
743
|
-
return returnValue;
|
|
744
|
-
} else {
|
|
745
|
-
throw new GraphQLError(entityObject.LatestResult?.Message ?? 'Unknown error', {
|
|
746
|
-
extensions: { code: 'DELETE_ENTITY_ERROR', entityName },
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
} else {
|
|
750
|
-
throw new GraphQLError('Delete operation canceled by BeforeDelete() handler in ResolverBase', {
|
|
751
|
-
extensions: { code: 'DELETE_ENTITY_ERROR', entityName },
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// Before/After DELETE Event Hooks for Sub-Classes to Override
|
|
757
|
-
protected async BeforeDelete(dataSource: DataSource, key: CompositeKey): Promise<boolean> {
|
|
758
|
-
return true;
|
|
759
|
-
}
|
|
760
|
-
protected async AfterDelete(dataSource: DataSource, key: CompositeKey) {}
|
|
761
|
-
|
|
762
|
-
// Before/After UPDATE Event Hooks for Sub-Classes to Override
|
|
763
|
-
protected async BeforeUpdate(dataSource: DataSource, input: any): Promise<boolean> {
|
|
764
|
-
return true;
|
|
765
|
-
}
|
|
766
|
-
protected async AfterUpdate(dataSource: DataSource, input: any) {}
|
|
767
|
-
}
|