@memberjunction/server 2.22.2 → 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.
Files changed (50) hide show
  1. package/dist/config.d.ts +27 -1
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +5 -1
  4. package/dist/config.js.map +1 -1
  5. package/dist/context.d.ts +5 -3
  6. package/dist/context.d.ts.map +1 -1
  7. package/dist/context.js +9 -6
  8. package/dist/context.js.map +1 -1
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +22 -3
  12. package/dist/index.js.map +1 -1
  13. package/dist/resolvers/FileResolver.d.ts +3 -3
  14. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  15. package/dist/resolvers/FileResolver.js +10 -10
  16. package/dist/resolvers/FileResolver.js.map +1 -1
  17. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  18. package/dist/resolvers/GetDataResolver.js +6 -1
  19. package/dist/resolvers/GetDataResolver.js.map +1 -1
  20. package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
  21. package/dist/resolvers/SyncDataResolver.js +35 -0
  22. package/dist/resolvers/SyncDataResolver.js.map +1 -1
  23. package/dist/resolvers/UserResolver.d.ts +1 -1
  24. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  25. package/dist/resolvers/UserResolver.js +2 -2
  26. package/dist/resolvers/UserResolver.js.map +1 -1
  27. package/dist/resolvers/UserViewResolver.d.ts +4 -5
  28. package/dist/resolvers/UserViewResolver.d.ts.map +1 -1
  29. package/dist/resolvers/UserViewResolver.js +7 -7
  30. package/dist/resolvers/UserViewResolver.js.map +1 -1
  31. package/dist/types.d.ts +18 -0
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/types.js +18 -1
  34. package/dist/types.js.map +1 -1
  35. package/dist/util.d.ts +6 -0
  36. package/dist/util.d.ts.map +1 -1
  37. package/dist/util.js +20 -0
  38. package/dist/util.js.map +1 -1
  39. package/package.json +22 -22
  40. package/src/config.ts +6 -0
  41. package/src/context.ts +11 -7
  42. package/src/index.ts +30 -3
  43. package/src/resolvers/FileResolver.ts +10 -10
  44. package/src/resolvers/GetDataResolver.ts +10 -3
  45. package/src/resolvers/SyncDataResolver.ts +39 -0
  46. package/src/resolvers/SyncRolesUsersResolver.ts +1 -1
  47. package/src/resolvers/UserResolver.ts +2 -2
  48. package/src/resolvers/UserViewResolver.ts +7 -7
  49. package/src/types.ts +29 -0
  50. 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() { dataSource, userPayload }: AppContext,
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' }, { dataSource, userPayload }, pubSub)) as File_;
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() { dataSource, userPayload }: AppContext,
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, { dataSource, userPayload }, pubSub);
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() { dataSource, userPayload }: AppContext,
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, { dataSource, userPayload }, pubSub);
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 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.
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 context.dataSource.query(query);
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;
@@ -398,4 +398,4 @@ export class SyncRolesAndUsersResolver {
398
398
  }
399
399
  }
400
400
  }
401
-
401
+
@@ -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() { dataSource, userPayload }: AppContext) {
8
- const result = await this.UserByEmail(userPayload.email, { dataSource, userPayload });
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() { dataSource, userPayload }: AppContext) {
27
- return await this.findBy(dataSource, 'User Views', {
28
- UserID: await this.getCurrentUserID(dataSource, userPayload),
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(dataSource: DataSource, userPayload: UserPayload): Promise<number> {
34
+ protected async getCurrentUserID(context: AppContext): Promise<number> {
35
35
  const userResolver = new UserResolver();
36
- const user = await userResolver.UserByEmail(userPayload.email, { dataSource, userPayload });
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() { dataSource, userPayload }: AppContext) {
42
- return this.findBy(dataSource, 'User Views', { UserID: await this.getCurrentUserID(dataSource, userPayload), EntityID });
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
+ }