@memberjunction/server 1.0.4 → 1.0.7-next.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/.eslintignore +4 -4
- package/.eslintrc +24 -24
- package/CHANGELOG.json +92 -0
- package/CHANGELOG.md +25 -0
- package/README.md +141 -141
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -1
- package/dist/config.js.map +1 -1
- 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 +99 -0
- package/dist/entitySubclasses/entityPermissions.server.js.map +1 -0
- package/dist/entitySubclasses/userViewEntity.server.js +17 -17
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/resolvers/AskSkipResolver.js +10 -10
- package/dist/resolvers/FileCategoryResolver.js +2 -2
- package/dist/resolvers/ReportResolver.js +4 -4
- package/package.json +80 -80
- package/src/apolloServer/TransactionPlugin.ts +57 -57
- package/src/apolloServer/index.ts +33 -33
- package/src/auth/exampleNewUserSubClass.ts +73 -73
- package/src/auth/index.ts +151 -151
- package/src/auth/newUsers.ts +56 -56
- package/src/auth/tokenExpiredError.ts +12 -12
- package/src/cache.ts +10 -10
- package/src/config.ts +89 -84
- package/src/context.ts +119 -119
- package/src/directives/Public.ts +42 -42
- package/src/directives/index.ts +1 -1
- package/src/entitySubclasses/entityPermissions.server.ts +111 -0
- package/src/entitySubclasses/userViewEntity.server.ts +187 -187
- package/src/generated/generated.ts +2573 -2573
- package/src/generic/PushStatusResolver.ts +40 -40
- package/src/generic/ResolverBase.ts +331 -331
- package/src/generic/RunViewResolver.ts +350 -350
- package/src/index.ts +133 -137
- package/src/orm.ts +36 -36
- package/src/resolvers/AskSkipResolver.ts +782 -782
- package/src/resolvers/ColorResolver.ts +72 -72
- package/src/resolvers/DatasetResolver.ts +115 -115
- package/src/resolvers/EntityRecordNameResolver.ts +77 -77
- package/src/resolvers/EntityResolver.ts +37 -37
- package/src/resolvers/FileCategoryResolver.ts +38 -38
- package/src/resolvers/FileResolver.ts +110 -110
- package/src/resolvers/MergeRecordsResolver.ts +198 -198
- package/src/resolvers/PotentialDuplicateRecordResolver.ts +59 -59
- package/src/resolvers/QueryResolver.ts +42 -42
- package/src/resolvers/ReportResolver.ts +131 -131
- package/src/resolvers/UserFavoriteResolver.ts +102 -102
- package/src/resolvers/UserResolver.ts +29 -29
- package/src/resolvers/UserViewResolver.ts +64 -64
- package/src/types.ts +19 -19
- package/src/util.ts +106 -106
- package/tsconfig.json +31 -31
- package/typedoc.json +4 -4
- package/build.log.json +0 -44
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
|
+
};
|
package/src/directives/Public.ts
CHANGED
|
@@ -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
|
+
};
|
package/src/directives/index.ts
CHANGED
|
@@ -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
|
+
}
|