@memberjunction/server 2.22.1 → 2.23.0
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.map +1 -1
- package/dist/resolvers/SyncDataResolver.js +35 -0
- package/dist/resolvers/SyncDataResolver.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 +39 -0
- package/src/resolvers/SyncRolesUsersResolver.ts +1 -1
- 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
|
@@ -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
|
+
}
|
|
@@ -266,6 +266,15 @@ export class SyncDataResolver {
|
|
|
266
266
|
result.ErrorMessage = 'Failed to load the item, it is possible the record with the specified primary key does not exist';
|
|
267
267
|
}
|
|
268
268
|
else {
|
|
269
|
+
// pass back the full record as it was JUST BEFORE the delete, often quite useful on the other end
|
|
270
|
+
result.RecordJSON = await altKeyResult.GetDataObjectJSON({
|
|
271
|
+
includeRelatedEntityData: false,
|
|
272
|
+
excludeFields: [],
|
|
273
|
+
omitEmptyStrings: false,
|
|
274
|
+
relatedEntityList: [],
|
|
275
|
+
omitNullValues: false,
|
|
276
|
+
oldValues: false
|
|
277
|
+
});
|
|
269
278
|
if (await altKeyResult.Delete()) {
|
|
270
279
|
result.Success = true;
|
|
271
280
|
}
|
|
@@ -275,6 +284,15 @@ export class SyncDataResolver {
|
|
|
275
284
|
}
|
|
276
285
|
}
|
|
277
286
|
else if (await entityObject.InnerLoad(pk)) {
|
|
287
|
+
// pass back the full record as it was JUST BEFORE the delete, often quite useful on the other end
|
|
288
|
+
result.RecordJSON = await entityObject.GetDataObjectJSON({
|
|
289
|
+
includeRelatedEntityData: false,
|
|
290
|
+
excludeFields: [],
|
|
291
|
+
omitEmptyStrings: false,
|
|
292
|
+
relatedEntityList: [],
|
|
293
|
+
omitNullValues: false,
|
|
294
|
+
oldValues: false
|
|
295
|
+
});
|
|
278
296
|
if (await entityObject.Delete()) {
|
|
279
297
|
result.Success = true;
|
|
280
298
|
}
|
|
@@ -297,6 +315,15 @@ export class SyncDataResolver {
|
|
|
297
315
|
if (await entityObject.Save()) {
|
|
298
316
|
result.Success = true;
|
|
299
317
|
result.PrimaryKey = new CompositeKey(entityObject.PrimaryKeys.map((pk) => ({FieldName: pk.Name, Value: pk.Value})));
|
|
318
|
+
// pass back the full record AFTER the sync, that's often quite useful on the other end
|
|
319
|
+
result.RecordJSON = await entityObject.GetDataObjectJSON({
|
|
320
|
+
includeRelatedEntityData: false,
|
|
321
|
+
excludeFields: [],
|
|
322
|
+
omitEmptyStrings: false,
|
|
323
|
+
relatedEntityList: [],
|
|
324
|
+
omitNullValues: false,
|
|
325
|
+
oldValues: false
|
|
326
|
+
});
|
|
300
327
|
}
|
|
301
328
|
else {
|
|
302
329
|
result.ErrorMessage = 'Failed to create the item :' + entityObject.LatestResult.Message;
|
|
@@ -328,6 +355,18 @@ export class SyncDataResolver {
|
|
|
328
355
|
entityObject.SetMany(fieldValues);
|
|
329
356
|
if (await entityObject.Save()) {
|
|
330
357
|
result.Success = true;
|
|
358
|
+
if (!result.PrimaryKey || result.PrimaryKey.KeyValuePairs.length === 0) {
|
|
359
|
+
result.PrimaryKey = new CompositeKey(entityObject.PrimaryKeys.map((pk) => ({FieldName: pk.Name, Value: pk.Value})));
|
|
360
|
+
}
|
|
361
|
+
// pass back the full record AFTER the sync, that's often quite useful on the other end
|
|
362
|
+
result.RecordJSON = await entityObject.GetDataObjectJSON({
|
|
363
|
+
includeRelatedEntityData: false,
|
|
364
|
+
excludeFields: [],
|
|
365
|
+
omitEmptyStrings: false,
|
|
366
|
+
relatedEntityList: [],
|
|
367
|
+
omitNullValues: false,
|
|
368
|
+
oldValues: false
|
|
369
|
+
});
|
|
331
370
|
}
|
|
332
371
|
else {
|
|
333
372
|
result.ErrorMessage = 'Failed to update the item :' + entityObject.LatestResult.Message;
|
|
@@ -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
|
+
}
|