@memberjunction/server 2.22.2 → 2.23.1
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/config.d.ts +27 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -1
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +5 -3
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +9 -6
- package/dist/context.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -3
- package/dist/index.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +3 -3
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +10 -10
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +6 -1
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/SyncDataResolver.d.ts +2 -0
- package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncDataResolver.js +64 -0
- package/dist/resolvers/SyncDataResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +7 -2
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/resolvers/UserResolver.d.ts +1 -1
- package/dist/resolvers/UserResolver.d.ts.map +1 -1
- package/dist/resolvers/UserResolver.js +2 -2
- package/dist/resolvers/UserResolver.js.map +1 -1
- package/dist/resolvers/UserViewResolver.d.ts +4 -5
- package/dist/resolvers/UserViewResolver.d.ts.map +1 -1
- package/dist/resolvers/UserViewResolver.js +7 -7
- package/dist/resolvers/UserViewResolver.js.map +1 -1
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +18 -1
- package/dist/types.js.map +1 -1
- package/dist/util.d.ts +6 -0
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +20 -0
- package/dist/util.js.map +1 -1
- package/package.json +22 -22
- package/src/config.ts +6 -0
- package/src/context.ts +11 -7
- package/src/index.ts +30 -3
- package/src/resolvers/FileResolver.ts +10 -10
- package/src/resolvers/GetDataResolver.ts +10 -3
- package/src/resolvers/SyncDataResolver.ts +76 -0
- package/src/resolvers/SyncRolesUsersResolver.ts +12 -3
- package/src/resolvers/UserResolver.ts +2 -2
- package/src/resolvers/UserViewResolver.ts +7 -7
- package/src/types.ts +29 -0
- package/src/util.ts +38 -0
package/src/context.ts
CHANGED
|
@@ -8,8 +8,9 @@ import { DataSource } from 'typeorm';
|
|
|
8
8
|
import { getSigningKeys, getSystemUser, validationOptions, verifyUserRecord } from './auth/index.js';
|
|
9
9
|
import { authCache } from './cache.js';
|
|
10
10
|
import { userEmailMap, apiKey } from './config.js';
|
|
11
|
-
import { UserPayload } from './types.js';
|
|
11
|
+
import { DataSourceInfo, UserPayload } from './types.js';
|
|
12
12
|
import { TokenExpiredError } from './auth/index.js';
|
|
13
|
+
import { GetReadOnlyDataSource, GetReadWriteDataSource } from './util.js';
|
|
13
14
|
|
|
14
15
|
const verifyAsync = async (issuer: string, options: jwt.VerifyOptions, token: string): Promise<jwt.JwtPayload> =>
|
|
15
16
|
new Promise((resolve, reject) => {
|
|
@@ -29,15 +30,18 @@ const verifyAsync = async (issuer: string, options: jwt.VerifyOptions, token: st
|
|
|
29
30
|
export const getUserPayload = async (
|
|
30
31
|
bearerToken: string,
|
|
31
32
|
sessionId = 'default',
|
|
32
|
-
|
|
33
|
+
dataSources: DataSourceInfo[],
|
|
33
34
|
requestDomain?: string,
|
|
34
35
|
requestApiKey?: string
|
|
35
36
|
): Promise<UserPayload> => {
|
|
36
37
|
try {
|
|
38
|
+
const readOnlyDataSource = GetReadOnlyDataSource(dataSources, { allowFallbackToReadWrite: true });
|
|
39
|
+
const readWriteDataSource = GetReadWriteDataSource(dataSources);
|
|
40
|
+
|
|
37
41
|
if (requestApiKey && requestApiKey != String(undefined)) {
|
|
38
42
|
// use requestApiKey for auth
|
|
39
43
|
if (requestApiKey === apiKey) {
|
|
40
|
-
const systemUser = await getSystemUser(
|
|
44
|
+
const systemUser = await getSystemUser(readOnlyDataSource);
|
|
41
45
|
return {
|
|
42
46
|
userRecord: systemUser,
|
|
43
47
|
email: systemUser.Email,
|
|
@@ -81,7 +85,7 @@ export const getUserPayload = async (
|
|
|
81
85
|
const fullName = payload?.name;
|
|
82
86
|
const firstName = payload?.given_name || fullName?.split(' ')[0];
|
|
83
87
|
const lastName = payload?.family_name || fullName?.split(' ')[1] || fullName?.split(' ')[0];
|
|
84
|
-
const userRecord = await verifyUserRecord(email, firstName, lastName, requestDomain,
|
|
88
|
+
const userRecord = await verifyUserRecord(email, firstName, lastName, requestDomain, readWriteDataSource);
|
|
85
89
|
|
|
86
90
|
if (!userRecord) {
|
|
87
91
|
console.error(`User ${email} not found`);
|
|
@@ -101,7 +105,7 @@ export const getUserPayload = async (
|
|
|
101
105
|
};
|
|
102
106
|
|
|
103
107
|
export const contextFunction =
|
|
104
|
-
({ setupComplete$, dataSource }: { setupComplete$: Subject<unknown>; dataSource: DataSource }) =>
|
|
108
|
+
({ setupComplete$, dataSource, dataSources }: { setupComplete$: Subject<unknown>; dataSource: DataSource, dataSources: DataSourceInfo[] }) =>
|
|
105
109
|
async ({ req }: { req: IncomingMessage }) => {
|
|
106
110
|
await firstValueFrom(setupComplete$); // wait for setup to complete before processing the request
|
|
107
111
|
|
|
@@ -115,7 +119,7 @@ export const contextFunction =
|
|
|
115
119
|
const userPayload = await getUserPayload(
|
|
116
120
|
bearerToken,
|
|
117
121
|
sessionId,
|
|
118
|
-
|
|
122
|
+
dataSources,
|
|
119
123
|
requestDomain?.hostname ? requestDomain.hostname : undefined,
|
|
120
124
|
apiKey
|
|
121
125
|
);
|
|
@@ -126,5 +130,5 @@ export const contextFunction =
|
|
|
126
130
|
console.log({ operationName });
|
|
127
131
|
}
|
|
128
132
|
|
|
129
|
-
return { dataSource, userPayload };
|
|
133
|
+
return { dataSource, dataSources, userPayload };
|
|
130
134
|
};
|
package/src/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { BuildSchemaOptions, buildSchemaSync, GraphQLTimestamp } from 'type-grap
|
|
|
20
20
|
import { DataSource } from 'typeorm';
|
|
21
21
|
import { WebSocketServer } from 'ws';
|
|
22
22
|
import buildApolloServer from './apolloServer/index.js';
|
|
23
|
-
import { configInfo, graphqlPort, graphqlRootPath, mj_core_schema, websiteRunFromPackage } from './config.js';
|
|
23
|
+
import { configInfo, dbDatabase, dbHost, dbPort, dbUsername, graphqlPort, graphqlRootPath, mj_core_schema, websiteRunFromPackage } from './config.js';
|
|
24
24
|
import { contextFunction, getUserPayload } from './context.js';
|
|
25
25
|
import { requireSystemUserDirective, publicDirective } from './directives/index.js';
|
|
26
26
|
import orm from './orm.js';
|
|
@@ -64,9 +64,12 @@ export * from './resolvers/SyncRolesUsersResolver.js';
|
|
|
64
64
|
export * from './resolvers/SyncDataResolver.js';
|
|
65
65
|
export * from './resolvers/GetDataResolver.js';
|
|
66
66
|
|
|
67
|
+
export { GetReadOnlyDataSource, GetReadWriteDataSource } from './util.js';
|
|
68
|
+
|
|
67
69
|
export * from './generated/generated.js';
|
|
68
70
|
|
|
69
71
|
import { resolve } from 'node:path';
|
|
72
|
+
import { DataSourceInfo } from './types.js';
|
|
70
73
|
|
|
71
74
|
export type MJServerOptions = {
|
|
72
75
|
onBeforeServe?: () => void | Promise<void>;
|
|
@@ -103,6 +106,26 @@ export const serve = async (resolverPaths: Array<string>, app = createApp(), opt
|
|
|
103
106
|
await setupSQLServerClient(config); // datasource is already initialized, so we can setup the client right away
|
|
104
107
|
const md = new Metadata();
|
|
105
108
|
console.log(`Data Source has been initialized. ${md?.Entities ? md.Entities.length : 0} entities loaded.`);
|
|
109
|
+
|
|
110
|
+
const dataSources = [new DataSourceInfo({dataSource, type: 'Read-Write', host: dbHost, port: dbPort, database: dbDatabase, userName: dbUsername})];
|
|
111
|
+
|
|
112
|
+
// Establish a second read-only connection to the database if dbReadOnlyUsername and dbReadOnlyPassword exist
|
|
113
|
+
let readOnlyDataSource: DataSource | null = null;
|
|
114
|
+
if (configInfo.dbReadOnlyUsername && configInfo.dbReadOnlyPassword) {
|
|
115
|
+
const readOnlyConfig = {
|
|
116
|
+
...orm(paths),
|
|
117
|
+
username: configInfo.dbReadOnlyUsername,
|
|
118
|
+
password: configInfo.dbReadOnlyPassword,
|
|
119
|
+
};
|
|
120
|
+
readOnlyDataSource = new DataSource(readOnlyConfig);
|
|
121
|
+
await readOnlyDataSource.initialize();
|
|
122
|
+
|
|
123
|
+
// since we created a read-only data source, add it to the list of data sources
|
|
124
|
+
dataSources.push(new DataSourceInfo({dataSource: readOnlyDataSource, type: 'Read-Only', host: dbHost, port: dbPort, database: dbDatabase, userName: configInfo.dbReadOnlyUsername}));
|
|
125
|
+
console.log('Read-only Data Source has been initialized.');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
106
129
|
setupComplete$.next(true);
|
|
107
130
|
|
|
108
131
|
/******TEST HARNESS FOR CHANGE DETECTION */
|
|
@@ -156,7 +179,7 @@ export const serve = async (resolverPaths: Array<string>, app = createApp(), opt
|
|
|
156
179
|
{
|
|
157
180
|
schema,
|
|
158
181
|
context: async ({ connectionParams }) => {
|
|
159
|
-
const userPayload = await getUserPayload(String(connectionParams?.Authorization), undefined,
|
|
182
|
+
const userPayload = await getUserPayload(String(connectionParams?.Authorization), undefined, dataSources);
|
|
160
183
|
return { userPayload };
|
|
161
184
|
},
|
|
162
185
|
},
|
|
@@ -171,7 +194,11 @@ export const serve = async (resolverPaths: Array<string>, app = createApp(), opt
|
|
|
171
194
|
cors<cors.CorsRequest>(),
|
|
172
195
|
BodyParser.json({ limit: '50mb' }),
|
|
173
196
|
expressMiddleware(apolloServer, {
|
|
174
|
-
context: contextFunction({
|
|
197
|
+
context: contextFunction({
|
|
198
|
+
setupComplete$,
|
|
199
|
+
dataSource, // default read-write data source
|
|
200
|
+
dataSources // all data source
|
|
201
|
+
}),
|
|
175
202
|
})
|
|
176
203
|
);
|
|
177
204
|
|
|
@@ -47,20 +47,20 @@ export class FileResolver extends FileResolverBase {
|
|
|
47
47
|
@Mutation(() => CreateFilePayload)
|
|
48
48
|
async CreateFile(
|
|
49
49
|
@Arg('input', () => CreateFileInput) input: CreateFileInput,
|
|
50
|
-
@Ctx()
|
|
50
|
+
@Ctx() context: AppContext,
|
|
51
51
|
@PubSub() pubSub: PubSubEngine
|
|
52
52
|
) {
|
|
53
53
|
const md = new Metadata();
|
|
54
|
-
const user = this.GetUserFromPayload(userPayload);
|
|
54
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
55
55
|
const fileEntity = await md.GetEntityObject<FileEntity>('Files', user);
|
|
56
56
|
const providerEntity = await md.GetEntityObject<FileStorageProviderEntity>('File Storage Providers', user);
|
|
57
57
|
fileEntity.CheckPermissions(EntityPermissionType.Create, true);
|
|
58
58
|
|
|
59
59
|
// Check to see if there's already an object with that name
|
|
60
|
-
const [sameName] = await this.findBy(dataSource, 'Files', { Name: input.Name, ProviderID: input.ProviderID });
|
|
60
|
+
const [sameName] = await this.findBy(context.dataSource, 'Files', { Name: input.Name, ProviderID: input.ProviderID });
|
|
61
61
|
const NameExists = Boolean(sameName);
|
|
62
62
|
|
|
63
|
-
const fileRecord = (await super.CreateFile({ ...input, Status: 'Pending' },
|
|
63
|
+
const fileRecord = (await super.CreateFile({ ...input, Status: 'Pending' }, context, pubSub)) as File_;
|
|
64
64
|
|
|
65
65
|
// If there's a problem creating the file record, the base resolver will return null
|
|
66
66
|
if (!fileRecord) {
|
|
@@ -98,12 +98,12 @@ export class FileResolver extends FileResolverBase {
|
|
|
98
98
|
@Mutation(() => File_)
|
|
99
99
|
async UpdateFile(
|
|
100
100
|
@Arg('input', () => UpdateFileInput) input: UpdateFileInput,
|
|
101
|
-
@Ctx()
|
|
101
|
+
@Ctx() context: AppContext,
|
|
102
102
|
@PubSub() pubSub: PubSubEngine
|
|
103
103
|
) {
|
|
104
104
|
// if the name is changing, rename the target object as well
|
|
105
105
|
const md = new Metadata();
|
|
106
|
-
const user = this.GetUserFromPayload(userPayload);
|
|
106
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
107
107
|
const fileEntity = await md.GetEntityObject<FileEntity>('Files', user);
|
|
108
108
|
fileEntity.CheckPermissions(EntityPermissionType.Update, true);
|
|
109
109
|
|
|
@@ -119,7 +119,7 @@ export class FileResolver extends FileResolverBase {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
const updatedFile = await super.UpdateFile(input,
|
|
122
|
+
const updatedFile = await super.UpdateFile(input, context, pubSub);
|
|
123
123
|
return updatedFile;
|
|
124
124
|
}
|
|
125
125
|
|
|
@@ -127,11 +127,11 @@ export class FileResolver extends FileResolverBase {
|
|
|
127
127
|
async DeleteFile(
|
|
128
128
|
@Arg('ID', () => String) ID: string,
|
|
129
129
|
@Arg('options___', () => DeleteOptionsInput) options: DeleteOptionsInput,
|
|
130
|
-
@Ctx()
|
|
130
|
+
@Ctx() context: AppContext,
|
|
131
131
|
@PubSub() pubSub: PubSubEngine
|
|
132
132
|
) {
|
|
133
133
|
const md = new Metadata();
|
|
134
|
-
const userInfo = this.GetUserFromPayload(userPayload);
|
|
134
|
+
const userInfo = this.GetUserFromPayload(context.userPayload);
|
|
135
135
|
|
|
136
136
|
const fileEntity = await md.GetEntityObject<FileEntity>('Files', userInfo);
|
|
137
137
|
await fileEntity.Load(ID);
|
|
@@ -147,6 +147,6 @@ export class FileResolver extends FileResolverBase {
|
|
|
147
147
|
await deleteObject(providerEntity, fileEntity.ProviderKey ?? fileEntity.Name);
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
return super.DeleteFile(ID, options,
|
|
150
|
+
return super.DeleteFile(ID, options, context, pubSub);
|
|
151
151
|
}
|
|
152
152
|
}
|
|
@@ -3,6 +3,7 @@ import { AppContext } from '../types.js';
|
|
|
3
3
|
import { LogError, LogStatus, Metadata } from '@memberjunction/core';
|
|
4
4
|
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
5
5
|
import { v4 as uuidv4 } from 'uuid';
|
|
6
|
+
import { GetReadOnlyDataSource } from '../util.js';
|
|
6
7
|
|
|
7
8
|
@InputType()
|
|
8
9
|
export class GetDataInputType {
|
|
@@ -98,7 +99,7 @@ export class SimpleEntityFieldOutputType {
|
|
|
98
99
|
|
|
99
100
|
export class GetDataResolver {
|
|
100
101
|
/**
|
|
101
|
-
* This
|
|
102
|
+
* This query will sync the specified items with the existing system. Items will be processed in order and the results of each operation will be returned in the Results array within the return value.
|
|
102
103
|
* @param items - an array of ActionItemInputType objects that specify the action to be taken on the specified entity with the specified primary key and the JSON representation of the field values.
|
|
103
104
|
* @param token - the short-lived access token that is required to perform this operation.
|
|
104
105
|
*/
|
|
@@ -118,12 +119,18 @@ export class GetDataResolver {
|
|
|
118
119
|
throw new Error(`Token ${input.Token} is not valid or has expired`);
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
// Use the read-only connection for executing queries
|
|
123
|
+
const readOnlyDataSource = GetReadOnlyDataSource(context.dataSources, {allowFallbackToReadWrite: false})
|
|
124
|
+
if (!readOnlyDataSource) {
|
|
125
|
+
throw new Error('Read-only data source not found');
|
|
126
|
+
}
|
|
127
|
+
|
|
121
128
|
// Execute all queries in parallel, but execute each individual query in its own try catch block so that if one fails, the others can still be processed
|
|
122
129
|
// and also so that we can capture the error message for each query and return it
|
|
123
130
|
const results = await Promise.allSettled(
|
|
124
131
|
input.Queries.map(async (query) => {
|
|
125
132
|
try {
|
|
126
|
-
const result = await
|
|
133
|
+
const result = await readOnlyDataSource.query(query);
|
|
127
134
|
return { result, error: null };
|
|
128
135
|
} catch (err) {
|
|
129
136
|
return { result: null, error: err };
|
|
@@ -258,4 +265,4 @@ export function recordTokenUse(token: string, usePayload: any) {
|
|
|
258
265
|
else {
|
|
259
266
|
throw new Error(`Token ${token} does not exist`);
|
|
260
267
|
}
|
|
261
|
-
}
|
|
268
|
+
}
|
|
@@ -3,6 +3,7 @@ import { AppContext } from '../types.js';
|
|
|
3
3
|
import { BaseEntity, CompositeKey, LogError, Metadata, RunView, UserInfo } from '@memberjunction/core';
|
|
4
4
|
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
5
5
|
import { CompositeKeyInputType, CompositeKeyOutputType } from '../generic/KeyInputOutputTypes.js';
|
|
6
|
+
import { DatasetItemEntity } from '@memberjunction/core-entities';
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
|
|
@@ -95,6 +96,9 @@ export class SyncDataResultType {
|
|
|
95
96
|
Results: ActionItemOutputType[] = [];
|
|
96
97
|
}
|
|
97
98
|
|
|
99
|
+
|
|
100
|
+
const __metadata_DatasetItems: string[] = [];
|
|
101
|
+
|
|
98
102
|
export class SyncDataResolver {
|
|
99
103
|
/**
|
|
100
104
|
* This mutation will sync the specified items with the existing system. Items will be processed in order and the results of each operation will be returned in the Results array within the return value.
|
|
@@ -114,6 +118,10 @@ export class SyncDataResolver {
|
|
|
114
118
|
results.push(await this.SyncSingleItem(item, context, md));
|
|
115
119
|
}
|
|
116
120
|
|
|
121
|
+
if (await this.DoSyncItemsAffectMetadata(items)) {
|
|
122
|
+
await md.Refresh(); // force refesh the metadata which will cause a reload from the DB
|
|
123
|
+
}
|
|
124
|
+
|
|
117
125
|
const overallSuccess = !results.some((r) => !r.Success); // if any element in the array of results has a Success value of false, then the overall success is false
|
|
118
126
|
return { Success: overallSuccess, Results: results };
|
|
119
127
|
}
|
|
@@ -123,6 +131,35 @@ export class SyncDataResolver {
|
|
|
123
131
|
}
|
|
124
132
|
}
|
|
125
133
|
|
|
134
|
+
protected async GetLowercaseMetadatEntitiesList(forceRefresh: boolean = false): Promise<string[]> {
|
|
135
|
+
if (forceRefresh || __metadata_DatasetItems.length === 0) {
|
|
136
|
+
const rv = new RunView(); // cache this, veyr simple - should use an engine for this stuff later
|
|
137
|
+
const result = await rv.RunView<DatasetItemEntity>({
|
|
138
|
+
EntityName: "Dataset Items",
|
|
139
|
+
ExtraFilter: "Dataset = 'MJ_Metadata'",
|
|
140
|
+
})
|
|
141
|
+
if (result && result.Success) {
|
|
142
|
+
__metadata_DatasetItems.length = 0;
|
|
143
|
+
__metadata_DatasetItems.push(...result.Results.map((r) => {
|
|
144
|
+
return r.Entity.trim().toLowerCase();
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// now return the list of entities
|
|
149
|
+
return __metadata_DatasetItems;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
protected async DoSyncItemsAffectMetadata(items: ActionItemInputType[]): Promise<boolean> {
|
|
153
|
+
// check to see if any of the items affect any of these entities:
|
|
154
|
+
const entitiesToCheck = await this.GetLowercaseMetadatEntitiesList(false);
|
|
155
|
+
for (const item of items) {
|
|
156
|
+
if (entitiesToCheck.find(e => e === item.EntityName.trim().toLowerCase()) ) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return false; // didn't find any
|
|
161
|
+
}
|
|
162
|
+
|
|
126
163
|
protected async SyncSingleItem(item: ActionItemInputType, context: AppContext, md: Metadata): Promise<ActionItemOutputType> {
|
|
127
164
|
const result = new ActionItemOutputType();
|
|
128
165
|
result.AlternateKey = item.AlternateKey;
|
|
@@ -266,6 +303,15 @@ export class SyncDataResolver {
|
|
|
266
303
|
result.ErrorMessage = 'Failed to load the item, it is possible the record with the specified primary key does not exist';
|
|
267
304
|
}
|
|
268
305
|
else {
|
|
306
|
+
// pass back the full record as it was JUST BEFORE the delete, often quite useful on the other end
|
|
307
|
+
result.RecordJSON = await altKeyResult.GetDataObjectJSON({
|
|
308
|
+
includeRelatedEntityData: false,
|
|
309
|
+
excludeFields: [],
|
|
310
|
+
omitEmptyStrings: false,
|
|
311
|
+
relatedEntityList: [],
|
|
312
|
+
omitNullValues: false,
|
|
313
|
+
oldValues: false
|
|
314
|
+
});
|
|
269
315
|
if (await altKeyResult.Delete()) {
|
|
270
316
|
result.Success = true;
|
|
271
317
|
}
|
|
@@ -275,6 +321,15 @@ export class SyncDataResolver {
|
|
|
275
321
|
}
|
|
276
322
|
}
|
|
277
323
|
else if (await entityObject.InnerLoad(pk)) {
|
|
324
|
+
// pass back the full record as it was JUST BEFORE the delete, often quite useful on the other end
|
|
325
|
+
result.RecordJSON = await entityObject.GetDataObjectJSON({
|
|
326
|
+
includeRelatedEntityData: false,
|
|
327
|
+
excludeFields: [],
|
|
328
|
+
omitEmptyStrings: false,
|
|
329
|
+
relatedEntityList: [],
|
|
330
|
+
omitNullValues: false,
|
|
331
|
+
oldValues: false
|
|
332
|
+
});
|
|
278
333
|
if (await entityObject.Delete()) {
|
|
279
334
|
result.Success = true;
|
|
280
335
|
}
|
|
@@ -297,6 +352,15 @@ export class SyncDataResolver {
|
|
|
297
352
|
if (await entityObject.Save()) {
|
|
298
353
|
result.Success = true;
|
|
299
354
|
result.PrimaryKey = new CompositeKey(entityObject.PrimaryKeys.map((pk) => ({FieldName: pk.Name, Value: pk.Value})));
|
|
355
|
+
// pass back the full record AFTER the sync, that's often quite useful on the other end
|
|
356
|
+
result.RecordJSON = await entityObject.GetDataObjectJSON({
|
|
357
|
+
includeRelatedEntityData: false,
|
|
358
|
+
excludeFields: [],
|
|
359
|
+
omitEmptyStrings: false,
|
|
360
|
+
relatedEntityList: [],
|
|
361
|
+
omitNullValues: false,
|
|
362
|
+
oldValues: false
|
|
363
|
+
});
|
|
300
364
|
}
|
|
301
365
|
else {
|
|
302
366
|
result.ErrorMessage = 'Failed to create the item :' + entityObject.LatestResult.Message;
|
|
@@ -328,6 +392,18 @@ export class SyncDataResolver {
|
|
|
328
392
|
entityObject.SetMany(fieldValues);
|
|
329
393
|
if (await entityObject.Save()) {
|
|
330
394
|
result.Success = true;
|
|
395
|
+
if (!result.PrimaryKey || result.PrimaryKey.KeyValuePairs.length === 0) {
|
|
396
|
+
result.PrimaryKey = new CompositeKey(entityObject.PrimaryKeys.map((pk) => ({FieldName: pk.Name, Value: pk.Value})));
|
|
397
|
+
}
|
|
398
|
+
// pass back the full record AFTER the sync, that's often quite useful on the other end
|
|
399
|
+
result.RecordJSON = await entityObject.GetDataObjectJSON({
|
|
400
|
+
includeRelatedEntityData: false,
|
|
401
|
+
excludeFields: [],
|
|
402
|
+
omitEmptyStrings: false,
|
|
403
|
+
relatedEntityList: [],
|
|
404
|
+
omitNullValues: false,
|
|
405
|
+
oldValues: false
|
|
406
|
+
});
|
|
331
407
|
}
|
|
332
408
|
else {
|
|
333
409
|
result.ErrorMessage = 'Failed to update the item :' + entityObject.LatestResult.Message;
|
|
@@ -3,6 +3,7 @@ import { AppContext } from '../types.js';
|
|
|
3
3
|
import { LogError, Metadata, RunView, UserInfo } from '@memberjunction/core';
|
|
4
4
|
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
5
5
|
import { RoleEntity, UserEntity, UserRoleEntity } from '@memberjunction/core-entities';
|
|
6
|
+
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
6
7
|
|
|
7
8
|
@ObjectType()
|
|
8
9
|
export class SyncRolesAndUsersResultType {
|
|
@@ -88,8 +89,16 @@ export class SyncRolesAndUsersResolver {
|
|
|
88
89
|
try {
|
|
89
90
|
// first we sync the roles, then the users
|
|
90
91
|
const roleResult = await this.SyncRoles(data.Roles, context);
|
|
91
|
-
if (roleResult
|
|
92
|
-
|
|
92
|
+
if (roleResult?.Success) {
|
|
93
|
+
const usersResult = await this.SyncUsers(data.Users, context);
|
|
94
|
+
if (usersResult?.Success) {
|
|
95
|
+
// refresh the user cache, don't set an auto-refresh
|
|
96
|
+
// interval here becuase that is alreayd done at startup
|
|
97
|
+
// and will keep going on its own as per the config. This is a
|
|
98
|
+
// special one-time refresh since we made changes here.
|
|
99
|
+
await UserCache.Instance.Refresh(context.dataSource);
|
|
100
|
+
}
|
|
101
|
+
return usersResult;
|
|
93
102
|
}
|
|
94
103
|
else {
|
|
95
104
|
return roleResult;
|
|
@@ -398,4 +407,4 @@ export class SyncRolesAndUsersResolver {
|
|
|
398
407
|
}
|
|
399
408
|
}
|
|
400
409
|
}
|
|
401
|
-
|
|
410
|
+
|
|
@@ -4,8 +4,8 @@ import { User_, UserResolverBase } from '../generated/generated.js';
|
|
|
4
4
|
@Resolver(User_)
|
|
5
5
|
export class UserResolver extends UserResolverBase {
|
|
6
6
|
@Query(() => User_)
|
|
7
|
-
async CurrentUser(@Ctx()
|
|
8
|
-
const result = await this.UserByEmail(userPayload.email,
|
|
7
|
+
async CurrentUser(@Ctx() context: AppContext) {
|
|
8
|
+
const result = await this.UserByEmail(context.userPayload.email, context);
|
|
9
9
|
console.log('CurrentUser', result);
|
|
10
10
|
return result;
|
|
11
11
|
}
|
|
@@ -23,23 +23,23 @@ export class UserViewResolver extends UserViewResolverBase {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
@Query(() => [UserView_])
|
|
26
|
-
async CurrentUserDefaultViewByEntityID(@Arg('EntityID', () => Int) EntityID: number, @Ctx()
|
|
27
|
-
return await this.findBy(dataSource, 'User Views', {
|
|
28
|
-
UserID: await this.getCurrentUserID(
|
|
26
|
+
async CurrentUserDefaultViewByEntityID(@Arg('EntityID', () => Int) EntityID: number, @Ctx() context: AppContext) {
|
|
27
|
+
return await this.findBy(context.dataSource, 'User Views', {
|
|
28
|
+
UserID: await this.getCurrentUserID(context),
|
|
29
29
|
EntityID,
|
|
30
30
|
IsDefault: true,
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
protected async getCurrentUserID(
|
|
34
|
+
protected async getCurrentUserID(context: AppContext): Promise<number> {
|
|
35
35
|
const userResolver = new UserResolver();
|
|
36
|
-
const user = await userResolver.UserByEmail(userPayload.email,
|
|
36
|
+
const user = await userResolver.UserByEmail(context.userPayload.email, context);
|
|
37
37
|
return user.ID;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
@Query(() => [UserView_])
|
|
41
|
-
async CurrentUserUserViewsByEntityID(@Arg('EntityID', () => Int) EntityID: number, @Ctx()
|
|
42
|
-
return this.findBy(dataSource, 'User Views', { UserID: await this.getCurrentUserID(
|
|
41
|
+
async CurrentUserUserViewsByEntityID(@Arg('EntityID', () => Int) EntityID: number, @Ctx() context: AppContext) {
|
|
42
|
+
return this.findBy(context.dataSource, 'User Views', { UserID: await this.getCurrentUserID(context), EntityID });
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
@Query(() => [UserView_])
|
package/src/types.ts
CHANGED
|
@@ -11,10 +11,39 @@ export type UserPayload = {
|
|
|
11
11
|
apiKey?: string;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* AppContext is the context object that is passed to all resolvers.
|
|
16
|
+
*/
|
|
14
17
|
export type AppContext = {
|
|
18
|
+
/**
|
|
19
|
+
* The default and backwards compatible data source.
|
|
20
|
+
*/
|
|
15
21
|
dataSource: DataSource;
|
|
16
22
|
userPayload: UserPayload;
|
|
17
23
|
queryRunner?: QueryRunner;
|
|
24
|
+
/**
|
|
25
|
+
* Array of data sources that have additional information about their intended use e.g. Admin, Read-Write, Read-Only.
|
|
26
|
+
*/
|
|
27
|
+
dataSources: DataSourceInfo[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export class DataSourceInfo {
|
|
31
|
+
dataSource: DataSource;
|
|
32
|
+
host: string;
|
|
33
|
+
port: number;
|
|
34
|
+
instance?: string;
|
|
35
|
+
database: string;
|
|
36
|
+
userName: string;
|
|
37
|
+
type: "Admin" | "Read-Write" | "Read-Only" | "Other";
|
|
38
|
+
|
|
39
|
+
constructor(init: {dataSource: DataSource, type: "Admin" | "Read-Write" | "Read-Only" | "Other", host: string, port: number, database: string, userName: string} ) {
|
|
40
|
+
this.dataSource = init.dataSource;
|
|
41
|
+
this.host = init.host;
|
|
42
|
+
this.port = init.port;
|
|
43
|
+
this.database = init.database;
|
|
44
|
+
this.userName = init.userName;
|
|
45
|
+
this.type = init.type;
|
|
46
|
+
}
|
|
18
47
|
};
|
|
19
48
|
|
|
20
49
|
export type DirectiveBuilder = {
|
package/src/util.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { gzip as gzipCallback, createGunzip } from 'zlib';
|
|
|
4
4
|
import { promisify } from 'util';
|
|
5
5
|
import { URL } from 'url';
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
+
import { DataSourceInfo } from './types';
|
|
8
|
+
import { DataSource } from 'typeorm';
|
|
7
9
|
|
|
8
10
|
const gzip = promisify(gzipCallback);
|
|
9
11
|
|
|
@@ -110,3 +112,39 @@ export async function sendPostRequest(url: string, payload: any, useCompression:
|
|
|
110
112
|
}
|
|
111
113
|
);
|
|
112
114
|
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Returns the read-only data source if it exists, otherwise returns the read-write data source if options is not provided or if options.allowFallbackToReadWrite is true.
|
|
119
|
+
* @param dataSources
|
|
120
|
+
* @param options
|
|
121
|
+
* @returns
|
|
122
|
+
*/
|
|
123
|
+
export function GetReadOnlyDataSource(dataSources: DataSourceInfo[], options?: {allowFallbackToReadWrite: boolean}): DataSource {
|
|
124
|
+
const readOnlyDataSource = dataSources.find((ds) => ds.type === 'Read-Only');
|
|
125
|
+
if (readOnlyDataSource) {
|
|
126
|
+
return readOnlyDataSource.dataSource;
|
|
127
|
+
}
|
|
128
|
+
else if (!options || options.allowFallbackToReadWrite) {
|
|
129
|
+
// default behavior for backward compatibility prior to MJ 2.22.3 where we introduced this functionality was to have a single
|
|
130
|
+
// connection, so for back-compatability, if we don't have a read-only data source, we'll fall back to the read-write data source
|
|
131
|
+
const readWriteDataSource = dataSources.find((ds) => ds.type === 'Read-Write');
|
|
132
|
+
if (readWriteDataSource) {
|
|
133
|
+
return readWriteDataSource.dataSource;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
throw new Error('No suitable data source found');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Returns the read-write data source if it exists, otherwise throws an error.
|
|
141
|
+
* @param dataSources
|
|
142
|
+
* @returns
|
|
143
|
+
*/
|
|
144
|
+
export function GetReadWriteDataSource(dataSources: DataSourceInfo[]): DataSource {
|
|
145
|
+
const readWriteDataSource = dataSources.find((ds) => ds.type === 'Read-Write');
|
|
146
|
+
if (readWriteDataSource) {
|
|
147
|
+
return readWriteDataSource.dataSource;
|
|
148
|
+
}
|
|
149
|
+
throw new Error('No suitable read-write data source found');
|
|
150
|
+
}
|