@memberjunction/server 1.0.6 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.eslintignore +4 -4
  2. package/.eslintrc +24 -24
  3. package/CHANGELOG.json +92 -0
  4. package/CHANGELOG.md +25 -0
  5. package/README.md +141 -141
  6. package/dist/config.d.ts +3 -0
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +4 -1
  9. package/dist/config.js.map +1 -1
  10. package/dist/entitySubclasses/entityPermissions.server.d.ts +23 -0
  11. package/dist/entitySubclasses/entityPermissions.server.d.ts.map +1 -0
  12. package/dist/entitySubclasses/entityPermissions.server.js +99 -0
  13. package/dist/entitySubclasses/entityPermissions.server.js.map +1 -0
  14. package/dist/entitySubclasses/userViewEntity.server.js +17 -17
  15. package/dist/generated/generated.d.ts.map +1 -1
  16. package/dist/generated/generated.js.map +1 -1
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/resolvers/AskSkipResolver.js +10 -10
  22. package/dist/resolvers/FileCategoryResolver.js +2 -2
  23. package/dist/resolvers/ReportResolver.js +4 -4
  24. package/package.json +80 -80
  25. package/src/apolloServer/TransactionPlugin.ts +57 -57
  26. package/src/apolloServer/index.ts +33 -33
  27. package/src/auth/exampleNewUserSubClass.ts +73 -73
  28. package/src/auth/index.ts +151 -151
  29. package/src/auth/newUsers.ts +56 -56
  30. package/src/auth/tokenExpiredError.ts +12 -12
  31. package/src/cache.ts +10 -10
  32. package/src/config.ts +89 -84
  33. package/src/context.ts +119 -119
  34. package/src/directives/Public.ts +42 -42
  35. package/src/directives/index.ts +1 -1
  36. package/src/entitySubclasses/entityPermissions.server.ts +111 -0
  37. package/src/entitySubclasses/userViewEntity.server.ts +187 -187
  38. package/src/generated/generated.ts +2573 -2573
  39. package/src/generic/PushStatusResolver.ts +40 -40
  40. package/src/generic/ResolverBase.ts +331 -331
  41. package/src/generic/RunViewResolver.ts +350 -350
  42. package/src/index.ts +133 -137
  43. package/src/orm.ts +36 -36
  44. package/src/resolvers/AskSkipResolver.ts +782 -782
  45. package/src/resolvers/ColorResolver.ts +72 -72
  46. package/src/resolvers/DatasetResolver.ts +115 -115
  47. package/src/resolvers/EntityRecordNameResolver.ts +77 -77
  48. package/src/resolvers/EntityResolver.ts +37 -37
  49. package/src/resolvers/FileCategoryResolver.ts +38 -38
  50. package/src/resolvers/FileResolver.ts +110 -110
  51. package/src/resolvers/MergeRecordsResolver.ts +198 -198
  52. package/src/resolvers/PotentialDuplicateRecordResolver.ts +59 -59
  53. package/src/resolvers/QueryResolver.ts +42 -42
  54. package/src/resolvers/ReportResolver.ts +131 -131
  55. package/src/resolvers/UserFavoriteResolver.ts +102 -102
  56. package/src/resolvers/UserResolver.ts +29 -29
  57. package/src/resolvers/UserViewResolver.ts +64 -64
  58. package/src/types.ts +19 -19
  59. package/src/util.ts +106 -106
  60. package/tsconfig.json +31 -31
  61. package/typedoc.json +4 -4
  62. package/build.log.json +0 -47
package/src/context.ts CHANGED
@@ -1,119 +1,119 @@
1
- import { IncomingMessage } from 'http';
2
- import * as url from 'url';
3
- import { JwtPayload, VerifyOptions, decode, verify } from 'jsonwebtoken';
4
- import 'reflect-metadata';
5
- import { Subject, firstValueFrom } from 'rxjs';
6
- import { AuthenticationError, AuthorizationError } from 'type-graphql';
7
- import { DataSource } from 'typeorm';
8
- import { getSigningKeys, validationOptions, verifyUserRecord } from './auth';
9
- import { authCache } from './cache';
10
- import { userEmailMap } from './config';
11
- import { UserPayload } from './types';
12
- import { TokenExpiredError } from './auth';
13
-
14
- const verifyAsync = async (
15
- issuer: string,
16
- options: VerifyOptions,
17
- token: string
18
- ): Promise<JwtPayload> =>
19
- new Promise((resolve, reject) => {
20
- verify(token, getSigningKeys(issuer), options, (err, jwt) => {
21
- if (jwt && typeof jwt !== 'string' && !err) {
22
- const payload = jwt.payload ?? jwt;
23
-
24
- console.log(
25
- `Valid token: ${payload.name} (${
26
- payload.email ? payload.email : payload.preferred_username
27
- })`
28
- ); // temporary fix to check preferred_username if email is not present
29
- resolve(payload);
30
- } else {
31
- console.warn('Invalid token');
32
- reject(err);
33
- }
34
- });
35
- });
36
-
37
- export const getUserPayload = async (
38
- bearerToken: string,
39
- sessionId = 'default',
40
- dataSource: DataSource,
41
- requestDomain?: string
42
- ): Promise<UserPayload> => {
43
- try {
44
- const token = bearerToken.replace('Bearer ', '');
45
-
46
- if (!token) {
47
- console.warn('No token to validate');
48
- throw new AuthenticationError('Missing token');
49
- }
50
-
51
- const payload = decode(token);
52
- if (!payload || typeof payload === 'string') {
53
- throw new AuthenticationError('Invalid token payload');
54
- }
55
-
56
- const expiryDate = new Date( (payload.exp ?? 0) * 1000);
57
- if (expiryDate.getTime() <= Date.now()) {
58
- throw new TokenExpiredError(expiryDate);
59
- }
60
-
61
- if (!authCache.has(token)) {
62
- const issuer = payload.iss;
63
- if (!issuer) {
64
- console.warn('No issuer claim on token');
65
- throw new AuthenticationError('Missing issuer claim on token');
66
- }
67
-
68
- await verifyAsync(issuer, validationOptions[issuer], token);
69
- authCache.set(token, true);
70
- }
71
-
72
- const email = payload?.email
73
- ? userEmailMap[payload?.email] ?? payload?.email
74
- : payload?.preferred_username; // temporary fix to check preferred_username if email is not present
75
- const fullName = payload?.name;
76
- const firstName = payload?.given_name || fullName?.split(' ')[0];
77
- const lastName = payload?.family_name || fullName?.split(' ')[1] || fullName?.split(' ')[0];
78
- const userRecord = await verifyUserRecord(email, firstName, lastName, requestDomain, dataSource);
79
-
80
- if (!userRecord) {
81
- console.error(`User ${email} not found`);
82
- throw new AuthorizationError();
83
- }
84
- else if (!userRecord.IsActive) {
85
- console.error(`User ${email} found but inactive`);
86
- throw new AuthorizationError();
87
- }
88
-
89
- return { userRecord, email, sessionId };
90
- } catch (e) {
91
- console.error(e);
92
- if (e instanceof TokenExpiredError) {
93
- throw e;
94
- }
95
- else
96
- return {} as UserPayload;
97
- }
98
- };
99
-
100
- export const contextFunction =
101
- ({ setupComplete$, dataSource }: { setupComplete$: Subject<unknown>; dataSource: DataSource }) =>
102
- async ({ req }: { req: IncomingMessage }) => {
103
- await firstValueFrom(setupComplete$); // wait for setup to complete before processing the request
104
-
105
- const sessionIdRaw = req.headers['x-session-id'];
106
- const requestDomain = url.parse(req.headers.origin || '')
107
- const sessionId = sessionIdRaw ? sessionIdRaw.toString() : '';
108
- const bearerToken = req.headers.authorization ?? '';
109
-
110
- const userPayload = await getUserPayload(bearerToken, sessionId, dataSource, requestDomain?.hostname ? requestDomain.hostname : undefined);
111
-
112
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
113
- const operationName: string| undefined = (req as any).body?.operationName;
114
- if (operationName !== 'IntrospectionQuery') {
115
- console.log({ operationName });
116
- }
117
-
118
- return { dataSource, userPayload };
119
- };
1
+ import { IncomingMessage } from 'http';
2
+ import * as url from 'url';
3
+ import { JwtPayload, VerifyOptions, decode, verify } from 'jsonwebtoken';
4
+ import 'reflect-metadata';
5
+ import { Subject, firstValueFrom } from 'rxjs';
6
+ import { AuthenticationError, AuthorizationError } from 'type-graphql';
7
+ import { DataSource } from 'typeorm';
8
+ import { getSigningKeys, validationOptions, verifyUserRecord } from './auth';
9
+ import { authCache } from './cache';
10
+ import { userEmailMap } from './config';
11
+ import { UserPayload } from './types';
12
+ import { TokenExpiredError } from './auth';
13
+
14
+ const verifyAsync = async (
15
+ issuer: string,
16
+ options: VerifyOptions,
17
+ token: string
18
+ ): Promise<JwtPayload> =>
19
+ new Promise((resolve, reject) => {
20
+ verify(token, getSigningKeys(issuer), options, (err, jwt) => {
21
+ if (jwt && typeof jwt !== 'string' && !err) {
22
+ const payload = jwt.payload ?? jwt;
23
+
24
+ console.log(
25
+ `Valid token: ${payload.name} (${
26
+ payload.email ? payload.email : payload.preferred_username
27
+ })`
28
+ ); // temporary fix to check preferred_username if email is not present
29
+ resolve(payload);
30
+ } else {
31
+ console.warn('Invalid token');
32
+ reject(err);
33
+ }
34
+ });
35
+ });
36
+
37
+ export const getUserPayload = async (
38
+ bearerToken: string,
39
+ sessionId = 'default',
40
+ dataSource: DataSource,
41
+ requestDomain?: string
42
+ ): Promise<UserPayload> => {
43
+ try {
44
+ const token = bearerToken.replace('Bearer ', '');
45
+
46
+ if (!token) {
47
+ console.warn('No token to validate');
48
+ throw new AuthenticationError('Missing token');
49
+ }
50
+
51
+ const payload = decode(token);
52
+ if (!payload || typeof payload === 'string') {
53
+ throw new AuthenticationError('Invalid token payload');
54
+ }
55
+
56
+ const expiryDate = new Date( (payload.exp ?? 0) * 1000);
57
+ if (expiryDate.getTime() <= Date.now()) {
58
+ throw new TokenExpiredError(expiryDate);
59
+ }
60
+
61
+ if (!authCache.has(token)) {
62
+ const issuer = payload.iss;
63
+ if (!issuer) {
64
+ console.warn('No issuer claim on token');
65
+ throw new AuthenticationError('Missing issuer claim on token');
66
+ }
67
+
68
+ await verifyAsync(issuer, validationOptions[issuer], token);
69
+ authCache.set(token, true);
70
+ }
71
+
72
+ const email = payload?.email
73
+ ? userEmailMap[payload?.email] ?? payload?.email
74
+ : payload?.preferred_username; // temporary fix to check preferred_username if email is not present
75
+ const fullName = payload?.name;
76
+ const firstName = payload?.given_name || fullName?.split(' ')[0];
77
+ const lastName = payload?.family_name || fullName?.split(' ')[1] || fullName?.split(' ')[0];
78
+ const userRecord = await verifyUserRecord(email, firstName, lastName, requestDomain, dataSource);
79
+
80
+ if (!userRecord) {
81
+ console.error(`User ${email} not found`);
82
+ throw new AuthorizationError();
83
+ }
84
+ else if (!userRecord.IsActive) {
85
+ console.error(`User ${email} found but inactive`);
86
+ throw new AuthorizationError();
87
+ }
88
+
89
+ return { userRecord, email, sessionId };
90
+ } catch (e) {
91
+ console.error(e);
92
+ if (e instanceof TokenExpiredError) {
93
+ throw e;
94
+ }
95
+ else
96
+ return {} as UserPayload;
97
+ }
98
+ };
99
+
100
+ export const contextFunction =
101
+ ({ setupComplete$, dataSource }: { setupComplete$: Subject<unknown>; dataSource: DataSource }) =>
102
+ async ({ req }: { req: IncomingMessage }) => {
103
+ await firstValueFrom(setupComplete$); // wait for setup to complete before processing the request
104
+
105
+ const sessionIdRaw = req.headers['x-session-id'];
106
+ const requestDomain = url.parse(req.headers.origin || '')
107
+ const sessionId = sessionIdRaw ? sessionIdRaw.toString() : '';
108
+ const bearerToken = req.headers.authorization ?? '';
109
+
110
+ const userPayload = await getUserPayload(bearerToken, sessionId, dataSource, requestDomain?.hostname ? requestDomain.hostname : undefined);
111
+
112
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
113
+ const operationName: string| undefined = (req as any).body?.operationName;
114
+ if (operationName !== 'IntrospectionQuery') {
115
+ console.log({ operationName });
116
+ }
117
+
118
+ return { dataSource, userPayload };
119
+ };
@@ -1,42 +1,42 @@
1
- import { FieldMapper, MapperKind, getDirective, mapSchema } from '@graphql-tools/utils';
2
- import { GraphQLFieldResolver, defaultFieldResolver } from 'graphql';
3
- import { AuthorizationError, Directive } from 'type-graphql';
4
- import { AppContext, DirectiveBuilder } from '../types';
5
-
6
- const DIRECTIVE_NAME = 'Public';
7
-
8
- export function Public(): PropertyDecorator & MethodDecorator & ClassDecorator;
9
- export function Public(): PropertyDecorator | MethodDecorator | ClassDecorator {
10
- return (targetOrPrototype, propertyKey, descriptor) =>
11
- Directive(`@${DIRECTIVE_NAME}`)(targetOrPrototype, propertyKey, descriptor);
12
- }
13
-
14
- export const publicDirective: DirectiveBuilder = {
15
- typeDefs: `directive @${DIRECTIVE_NAME} on FIELD_DEFINITION`,
16
- transformer: (schema) => {
17
- const fieldMapper: FieldMapper = (fieldConfig) => {
18
- const directive = getDirective(schema, fieldConfig, DIRECTIVE_NAME)?.[0];
19
- if (directive) {
20
- return fieldConfig;
21
- } else {
22
- // `@Public` directive not present, so will require auth
23
- const { resolve = defaultFieldResolver } = fieldConfig;
24
- const directiveResolver: GraphQLFieldResolver<unknown, AppContext> = async (
25
- source,
26
- args,
27
- context,
28
- info
29
- ) => {
30
- // eslint-disable-next-line
31
- if (!context?.userPayload?.userRecord?.IsActive) {
32
- throw new AuthorizationError();
33
- }
34
- return await resolve(source, args, context, info);
35
- };
36
-
37
- return { ...fieldConfig, resolve: directiveResolver };
38
- }
39
- };
40
- return mapSchema(schema, { [MapperKind.OBJECT_FIELD]: fieldMapper });
41
- },
42
- };
1
+ import { FieldMapper, MapperKind, getDirective, mapSchema } from '@graphql-tools/utils';
2
+ import { GraphQLFieldResolver, defaultFieldResolver } from 'graphql';
3
+ import { AuthorizationError, Directive } from 'type-graphql';
4
+ import { AppContext, DirectiveBuilder } from '../types';
5
+
6
+ const DIRECTIVE_NAME = 'Public';
7
+
8
+ export function Public(): PropertyDecorator & MethodDecorator & ClassDecorator;
9
+ export function Public(): PropertyDecorator | MethodDecorator | ClassDecorator {
10
+ return (targetOrPrototype, propertyKey, descriptor) =>
11
+ Directive(`@${DIRECTIVE_NAME}`)(targetOrPrototype, propertyKey, descriptor);
12
+ }
13
+
14
+ export const publicDirective: DirectiveBuilder = {
15
+ typeDefs: `directive @${DIRECTIVE_NAME} on FIELD_DEFINITION`,
16
+ transformer: (schema) => {
17
+ const fieldMapper: FieldMapper = (fieldConfig) => {
18
+ const directive = getDirective(schema, fieldConfig, DIRECTIVE_NAME)?.[0];
19
+ if (directive) {
20
+ return fieldConfig;
21
+ } else {
22
+ // `@Public` directive not present, so will require auth
23
+ const { resolve = defaultFieldResolver } = fieldConfig;
24
+ const directiveResolver: GraphQLFieldResolver<unknown, AppContext> = async (
25
+ source,
26
+ args,
27
+ context,
28
+ info
29
+ ) => {
30
+ // eslint-disable-next-line
31
+ if (!context?.userPayload?.userRecord?.IsActive) {
32
+ throw new AuthorizationError();
33
+ }
34
+ return await resolve(source, args, context, info);
35
+ };
36
+
37
+ return { ...fieldConfig, resolve: directiveResolver };
38
+ }
39
+ };
40
+ return mapSchema(schema, { [MapperKind.OBJECT_FIELD]: fieldMapper });
41
+ },
42
+ };
@@ -1 +1 @@
1
- export * from './Public';
1
+ export * from './Public';
@@ -0,0 +1,111 @@
1
+ import { RegisterClass } from "@memberjunction/global";
2
+ import { BaseEntity, EntitySaveOptions } from "@memberjunction/core";
3
+ import { EntityPermissionEntity } from '@memberjunction/core-entities'
4
+ import axios from 'axios';
5
+ import { ___codeGenAPIPort, ___codeGenAPISubmissionDelay, ___codeGenAPIURL } from "../config";
6
+
7
+ /**
8
+ * Server-side only class that extends the entity permissions object to watch for changes to entity permissions, build a queue of entities that have been changed, and then from time to time, submit
9
+ * them to an API server that will execute the underlying permission changes at the database level.
10
+ */
11
+ @RegisterClass(BaseEntity, 'Entity Permissions', 3)
12
+ export class EntityPermissionsEntity_Server extends EntityPermissionEntity {
13
+ protected static _entityIDQueue: number[] = [];
14
+ protected static _lastModifiedTime: Date | null = null;
15
+ protected static _submissionTimer: NodeJS.Timeout | null = null;
16
+ protected static _submissionDelay: number = ___codeGenAPISubmissionDelay;
17
+ protected static _baseURL: string = ___codeGenAPIURL;
18
+ protected static _port: number = ___codeGenAPIPort;
19
+ protected static _apiEndpoint: string = '/api/entity-permissions';
20
+
21
+ // Method to construct the full URL dynamically
22
+ protected static getSubmissionURL(): string {
23
+ return `${this._baseURL}:${this._port}${this._apiEndpoint}`;
24
+ }
25
+
26
+ public static get EntityIDQueue(): number[] {
27
+ return this._entityIDQueue;
28
+ }
29
+
30
+ public static ClearQueue(): void {
31
+ this._entityIDQueue = [];
32
+ this._submissionTimer = null;
33
+ }
34
+ public static AddToQueue(entityID: number): void {
35
+ if (this._entityIDQueue.indexOf(entityID) === -1)
36
+ this._entityIDQueue.push(entityID);
37
+ this._lastModifiedTime = new Date();
38
+ this.CheckStartSubmissionTimer();
39
+ }
40
+
41
+ protected static CheckStartSubmissionTimer(): void {
42
+ if (this._submissionTimer === null) {
43
+ this.StartSubmissionTimer();
44
+ }
45
+ else {
46
+ // we need to cancel the existing timer and start a new one
47
+ clearTimeout(this._submissionTimer);
48
+ this.StartSubmissionTimer();
49
+ }
50
+ }
51
+
52
+ protected static StartSubmissionTimer(): void {
53
+ this._submissionTimer = setTimeout(() => {
54
+ this.SubmitQueue();
55
+ }, this._submissionDelay);
56
+ }
57
+
58
+ protected static async SubmitQueue(): Promise<void> {
59
+ this._lastModifiedTime = null;
60
+
61
+ // now, use Axios to submit the queue to the API server
62
+ // Check if there's anything to submit
63
+ if (this._entityIDQueue.length > 0) {
64
+ try {
65
+ // Use Axios to submit the queue to the API server
66
+ const response = await axios.post(this.getSubmissionURL(), {
67
+ entityIDArray: this._entityIDQueue
68
+ });
69
+
70
+ // Check the Axios response code implicitly and API response explicitly
71
+ if (response.status === 200 && response.data.status === 'ok') {
72
+ console.log('Queue submitted successfully.');
73
+ // now, clear the queue and timer
74
+ this.ClearQueue();
75
+ } else {
76
+ // Handle API indicating a failure
77
+ console.error('Failed to submit queue:', response.data.errorMessage || 'Unknown error');
78
+ }
79
+
80
+ } catch (error) {
81
+ // Handle errors here
82
+ console.error('Failed to submit queue:', error);
83
+ // Consider re-trying or logging the error based on your requirements
84
+ }
85
+ } else {
86
+ console.log('No entities to submit.');
87
+ }
88
+ }
89
+
90
+ override Save(options?: EntitySaveOptions): Promise<boolean> {
91
+ // simply queue up the entity ID
92
+ if (this.Dirty || options?.IgnoreDirtyState)
93
+ EntityPermissionsEntity_Server.AddToQueue(this.EntityID);
94
+
95
+ return super.Save(options);
96
+ }
97
+
98
+ override async Delete(): Promise<boolean> {
99
+ const success = await super.Delete();
100
+
101
+ // simply queue up the entity ID if the delete worked
102
+ if (success)
103
+ EntityPermissionsEntity_Server.AddToQueue(this.EntityID);
104
+
105
+ return success;
106
+ }
107
+ }
108
+
109
+ export function LoadEntityPermissionsServerSubClass() {
110
+
111
+ }