@memberjunction/server 2.1.5 → 2.2.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/apolloServer/TransactionPlugin.d.ts +1 -1
- package/dist/apolloServer/TransactionPlugin.d.ts.map +1 -1
- package/dist/apolloServer/TransactionPlugin.js.map +1 -1
- package/dist/apolloServer/index.d.ts +1 -1
- package/dist/apolloServer/index.d.ts.map +1 -1
- package/dist/apolloServer/index.js +2 -2
- package/dist/apolloServer/index.js.map +1 -1
- package/dist/auth/exampleNewUserSubClass.d.ts +1 -1
- package/dist/auth/exampleNewUserSubClass.d.ts.map +1 -1
- package/dist/auth/exampleNewUserSubClass.js +7 -7
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +18 -8
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/newUsers.js +1 -1
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/context.d.ts +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +4 -4
- package/dist/context.js.map +1 -1
- package/dist/directives/Public.d.ts +1 -1
- package/dist/directives/Public.d.ts.map +1 -1
- package/dist/directives/index.d.ts +1 -1
- package/dist/directives/index.d.ts.map +1 -1
- package/dist/directives/index.js +1 -1
- package/dist/directives/index.js.map +1 -1
- package/dist/entitySubclasses/entityPermissions.server.d.ts +1 -1
- package/dist/entitySubclasses/entityPermissions.server.d.ts.map +1 -1
- package/dist/entitySubclasses/entityPermissions.server.js +5 -6
- package/dist/entitySubclasses/entityPermissions.server.js.map +1 -1
- package/dist/generated/generated.d.ts +5 -31
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +33 -189
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +3 -3
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +1 -1
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +2 -2
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +1 -1
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +21 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -27
- package/dist/index.js.map +1 -1
- package/dist/orm.js +4 -4
- package/dist/orm.js.map +1 -1
- package/dist/resolvers/AskSkipResolver.d.ts +3 -3
- package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
- package/dist/resolvers/AskSkipResolver.js +57 -55
- package/dist/resolvers/AskSkipResolver.js.map +1 -1
- package/dist/resolvers/ColorResolver.d.ts +1 -1
- package/dist/resolvers/ColorResolver.d.ts.map +1 -1
- package/dist/resolvers/ColorResolver.js +1 -1
- package/dist/resolvers/ColorResolver.js.map +1 -1
- package/dist/resolvers/DatasetResolver.d.ts +1 -1
- package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityCommunicationsResolver.d.ts +2 -2
- package/dist/resolvers/EntityCommunicationsResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityCommunicationsResolver.js +9 -4
- package/dist/resolvers/EntityCommunicationsResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +2 -2
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +2 -2
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/EntityResolver.d.ts +2 -2
- package/dist/resolvers/EntityResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityResolver.js +1 -1
- package/dist/resolvers/EntityResolver.js.map +1 -1
- package/dist/resolvers/FileCategoryResolver.d.ts +1 -1
- package/dist/resolvers/FileCategoryResolver.d.ts.map +1 -1
- package/dist/resolvers/FileCategoryResolver.js +2 -2
- package/dist/resolvers/FileCategoryResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +2 -2
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +3 -3
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.d.ts +2 -2
- package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.js +4 -2
- package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts +2 -2
- package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts.map +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +1 -1
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/ReportResolver.d.ts +1 -1
- package/dist/resolvers/ReportResolver.d.ts.map +1 -1
- package/dist/resolvers/ReportResolver.js +16 -14
- package/dist/resolvers/ReportResolver.js.map +1 -1
- package/dist/resolvers/UserFavoriteResolver.d.ts +1 -1
- package/dist/resolvers/UserFavoriteResolver.d.ts.map +1 -1
- package/dist/resolvers/UserFavoriteResolver.js +17 -16
- package/dist/resolvers/UserFavoriteResolver.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 +1 -1
- package/dist/resolvers/UserResolver.js.map +1 -1
- package/dist/resolvers/UserViewResolver.d.ts +1 -1
- package/dist/resolvers/UserViewResolver.d.ts.map +1 -1
- package/dist/resolvers/UserViewResolver.js +2 -2
- package/dist/resolvers/UserViewResolver.js.map +1 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +11 -5
- package/dist/util.js.map +1 -1
- package/package.json +27 -23
- package/src/apolloServer/TransactionPlugin.ts +53 -0
- package/src/apolloServer/index.ts +33 -0
- package/src/auth/exampleNewUserSubClass.ts +79 -0
- package/src/auth/index.ts +171 -0
- package/src/auth/newUsers.ts +58 -0
- package/src/auth/tokenExpiredError.ts +12 -0
- package/src/cache.ts +10 -0
- package/src/config.ts +89 -0
- package/src/context.ts +111 -0
- package/src/directives/Public.ts +42 -0
- package/src/directives/index.ts +1 -0
- package/src/entitySubclasses/DuplicateRunEntity.server.ts +29 -0
- package/src/entitySubclasses/entityPermissions.server.ts +104 -0
- package/src/entitySubclasses/userViewEntity.server.ts +187 -0
- package/src/generated/generated.ts +25265 -0
- package/src/generic/DeleteOptionsInput.ts +13 -0
- package/src/generic/KeyInputOutputTypes.ts +35 -0
- package/src/generic/KeyValuePairInput.ts +14 -0
- package/src/generic/PushStatusResolver.ts +40 -0
- package/src/generic/ResolverBase.ts +767 -0
- package/src/generic/RunViewResolver.ts +579 -0
- package/src/index.ts +171 -0
- package/src/orm.ts +36 -0
- package/src/resolvers/AskSkipResolver.ts +1109 -0
- package/src/resolvers/ColorResolver.ts +61 -0
- package/src/resolvers/DatasetResolver.ts +115 -0
- package/src/resolvers/EntityCommunicationsResolver.ts +221 -0
- package/src/resolvers/EntityRecordNameResolver.ts +75 -0
- package/src/resolvers/EntityResolver.ts +35 -0
- package/src/resolvers/FileCategoryResolver.ts +69 -0
- package/src/resolvers/FileResolver.ts +152 -0
- package/src/resolvers/MergeRecordsResolver.ts +175 -0
- package/src/resolvers/PotentialDuplicateRecordResolver.ts +91 -0
- package/src/resolvers/QueryResolver.ts +42 -0
- package/src/resolvers/ReportResolver.ts +144 -0
- package/src/resolvers/UserFavoriteResolver.ts +176 -0
- package/src/resolvers/UserResolver.ts +33 -0
- package/src/resolvers/UserViewResolver.ts +64 -0
- package/src/types.ts +40 -0
- package/src/util.ts +112 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { GraphQLError } from 'graphql';
|
|
2
|
+
|
|
3
|
+
export class TokenExpiredError extends GraphQLError {
|
|
4
|
+
constructor(expiryDate: Date, message = 'The provided token has expired. Please authenticate again.') {
|
|
5
|
+
super(message, {
|
|
6
|
+
extensions: {
|
|
7
|
+
code: 'JWT_EXPIRED',
|
|
8
|
+
expiryDate: expiryDate.toISOString(),
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
}
|
package/src/cache.ts
ADDED
package/src/config.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import env from 'env-var';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
export const nodeEnv = env.get('NODE_ENV').asString();
|
|
7
|
+
|
|
8
|
+
export const dbHost = env.get('DB_HOST').required().asString();
|
|
9
|
+
export const dbPort = env.get('DB_PORT').default('1433').asPortNumber();
|
|
10
|
+
export const dbUsername = env.get('DB_USERNAME').required().asString();
|
|
11
|
+
export const dbPassword = env.get('DB_PASSWORD').required().asString();
|
|
12
|
+
export const dbDatabase = env.get('DB_DATABASE').required().asString();
|
|
13
|
+
export const dbInstanceName = env.get('DB_INSTANCE_NAME').asString();
|
|
14
|
+
export const dbTrustServerCertificate = env.get('DB_TRUST_SERVER_CERTIFICATE').asBool();
|
|
15
|
+
|
|
16
|
+
export const graphqlPort = env.get('PORT').default('4000').asPortNumber();
|
|
17
|
+
|
|
18
|
+
export const ___codeGenAPIURL = env.get('CODEGEN_API_URL').asString();
|
|
19
|
+
export const ___codeGenAPIPort = env.get('CODEGEN_API_PORT').default('3999').asPortNumber();
|
|
20
|
+
export const ___codeGenAPISubmissionDelay = env.get('CODEGEN_API_SUBMISSION_DELAY').default(5000).asIntPositive();
|
|
21
|
+
|
|
22
|
+
export const graphqlRootPath = env.get('ROOT_PATH').default('/').asString();
|
|
23
|
+
|
|
24
|
+
export const webClientID = env.get('WEB_CLIENT_ID').asString();
|
|
25
|
+
export const tenantID = env.get('TENANT_ID').asString();
|
|
26
|
+
|
|
27
|
+
export const enableIntrospection = env.get('ENABLE_INTROSPECTION').default('false').asBool();
|
|
28
|
+
export const websiteRunFromPackage = env.get('WEBSITE_RUN_FROM_PACKAGE').asIntPositive();
|
|
29
|
+
export const userEmailMap = env.get('USER_EMAIL_MAP').default('{}').asJsonObject() as Record<string, string>;
|
|
30
|
+
|
|
31
|
+
export const ___skipAPIurl = env.get('ASK_SKIP_API_URL').asString();
|
|
32
|
+
export const ___skipAPIOrgId = env.get('ASK_SKIP_ORGANIZATION_ID').asString();
|
|
33
|
+
|
|
34
|
+
export const auth0Domain = env.get('AUTH0_DOMAIN').asString();
|
|
35
|
+
export const auth0WebClientID = env.get('AUTH0_CLIENT_ID').asString();
|
|
36
|
+
export const auth0ClientSecret = env.get('AUTH0_CLIENT_SECRET').asString();
|
|
37
|
+
|
|
38
|
+
export const mj_core_schema = env.get('MJ_CORE_SCHEMA').asString();
|
|
39
|
+
|
|
40
|
+
export const configFile = env.get('CONFIG_FILE').asString();
|
|
41
|
+
|
|
42
|
+
const userHandlingInfoSchema = z.object({
|
|
43
|
+
autoCreateNewUsers: z.boolean().optional().default(false),
|
|
44
|
+
newUserLimitedToAuthorizedDomains: z.boolean().optional().default(false),
|
|
45
|
+
newUserAuthorizedDomains: z.array(z.string()).optional().default([]),
|
|
46
|
+
newUserRoles: z.array(z.string()).optional().default([]),
|
|
47
|
+
updateCacheWhenNotFound: z.boolean().optional().default(false),
|
|
48
|
+
updateCacheWhenNotFoundDelay: z.number().optional().default(30000),
|
|
49
|
+
contextUserForNewUserCreation: z.string().optional().default(''),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const databaseSettingsInfoSchema = z.object({
|
|
53
|
+
connectionTimeout: z.number(),
|
|
54
|
+
requestTimeout: z.number(),
|
|
55
|
+
metadataCacheRefreshInterval: z.number(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const viewingSystemInfoSchema = z.object({
|
|
59
|
+
enableSmartFilters: z.boolean().optional(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const askSkipInfoSchema = z.object({
|
|
63
|
+
organizationInfo: z.string().optional(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const configInfoSchema = z.object({
|
|
67
|
+
userHandling: userHandlingInfoSchema,
|
|
68
|
+
databaseSettings: databaseSettingsInfoSchema,
|
|
69
|
+
viewingSystem: viewingSystemInfoSchema.optional(),
|
|
70
|
+
askSkip: askSkipInfoSchema.optional(),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export type UserHandlingInfo = z.infer<typeof userHandlingInfoSchema>;
|
|
74
|
+
export type DatabaseSettingsInfo = z.infer<typeof databaseSettingsInfoSchema>;
|
|
75
|
+
export type ViewingSystemSettingsInfo = z.infer<typeof viewingSystemInfoSchema>;
|
|
76
|
+
export type ConfigInfo = z.infer<typeof configInfoSchema>;
|
|
77
|
+
|
|
78
|
+
export const configInfo: ConfigInfo = loadConfig();
|
|
79
|
+
|
|
80
|
+
export function loadConfig() {
|
|
81
|
+
const configPath = configFile ?? path.resolve('config.json');
|
|
82
|
+
|
|
83
|
+
if (!fs.existsSync(configPath)) {
|
|
84
|
+
throw new Error(`Config file ${configPath} does not exist.`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const configData = fs.readFileSync(configPath, 'utf-8');
|
|
88
|
+
return configInfoSchema.parse(JSON.parse(configData));
|
|
89
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { IncomingMessage } from 'http';
|
|
2
|
+
import * as url from 'url';
|
|
3
|
+
import { default as jwt } 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/index.js';
|
|
9
|
+
import { authCache } from './cache.js';
|
|
10
|
+
import { userEmailMap } from './config.js';
|
|
11
|
+
import { UserPayload } from './types.js';
|
|
12
|
+
import { TokenExpiredError } from './auth/index.js';
|
|
13
|
+
|
|
14
|
+
const verifyAsync = async (issuer: string, options: jwt.VerifyOptions, token: string): Promise<jwt.JwtPayload> =>
|
|
15
|
+
new Promise((resolve, reject) => {
|
|
16
|
+
jwt.verify(token, getSigningKeys(issuer), options, (err, jwt) => {
|
|
17
|
+
if (jwt && typeof jwt !== 'string' && !err) {
|
|
18
|
+
const payload = jwt.payload ?? jwt;
|
|
19
|
+
|
|
20
|
+
console.log(`Valid token: ${payload.name} (${payload.email ? payload.email : payload.preferred_username})`); // temporary fix to check preferred_username if email is not present
|
|
21
|
+
resolve(payload);
|
|
22
|
+
} else {
|
|
23
|
+
console.warn('Invalid token');
|
|
24
|
+
reject(err);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const getUserPayload = async (
|
|
30
|
+
bearerToken: string,
|
|
31
|
+
sessionId = 'default',
|
|
32
|
+
dataSource: DataSource,
|
|
33
|
+
requestDomain?: string
|
|
34
|
+
): Promise<UserPayload> => {
|
|
35
|
+
try {
|
|
36
|
+
const token = bearerToken.replace('Bearer ', '');
|
|
37
|
+
|
|
38
|
+
if (!token) {
|
|
39
|
+
console.warn('No token to validate');
|
|
40
|
+
throw new AuthenticationError('Missing token');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const payload = jwt.decode(token);
|
|
44
|
+
if (!payload || typeof payload === 'string') {
|
|
45
|
+
throw new AuthenticationError('Invalid token payload');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const expiryDate = new Date((payload.exp ?? 0) * 1000);
|
|
49
|
+
if (expiryDate.getTime() <= Date.now()) {
|
|
50
|
+
throw new TokenExpiredError(expiryDate);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!authCache.has(token)) {
|
|
54
|
+
const issuer = payload.iss;
|
|
55
|
+
if (!issuer) {
|
|
56
|
+
console.warn('No issuer claim on token');
|
|
57
|
+
throw new AuthenticationError('Missing issuer claim on token');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await verifyAsync(issuer, validationOptions[issuer], token);
|
|
61
|
+
authCache.set(token, true);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const email = payload?.email ? userEmailMap[payload?.email] ?? payload?.email : payload?.preferred_username; // temporary fix to check preferred_username if email is not present
|
|
65
|
+
const fullName = payload?.name;
|
|
66
|
+
const firstName = payload?.given_name || fullName?.split(' ')[0];
|
|
67
|
+
const lastName = payload?.family_name || fullName?.split(' ')[1] || fullName?.split(' ')[0];
|
|
68
|
+
const userRecord = await verifyUserRecord(email, firstName, lastName, requestDomain, dataSource);
|
|
69
|
+
|
|
70
|
+
if (!userRecord) {
|
|
71
|
+
console.error(`User ${email} not found`);
|
|
72
|
+
throw new AuthorizationError();
|
|
73
|
+
} else if (!userRecord.IsActive) {
|
|
74
|
+
console.error(`User ${email} found but inactive`);
|
|
75
|
+
throw new AuthorizationError();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { userRecord, email, sessionId };
|
|
79
|
+
} catch (e) {
|
|
80
|
+
console.error(e);
|
|
81
|
+
if (e instanceof TokenExpiredError) {
|
|
82
|
+
throw e;
|
|
83
|
+
} else return {} as UserPayload;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const contextFunction =
|
|
88
|
+
({ setupComplete$, dataSource }: { setupComplete$: Subject<unknown>; dataSource: DataSource }) =>
|
|
89
|
+
async ({ req }: { req: IncomingMessage }) => {
|
|
90
|
+
await firstValueFrom(setupComplete$); // wait for setup to complete before processing the request
|
|
91
|
+
|
|
92
|
+
const sessionIdRaw = req.headers['x-session-id'];
|
|
93
|
+
const requestDomain = url.parse(req.headers.origin || '');
|
|
94
|
+
const sessionId = sessionIdRaw ? sessionIdRaw.toString() : '';
|
|
95
|
+
const bearerToken = req.headers.authorization ?? '';
|
|
96
|
+
|
|
97
|
+
const userPayload = await getUserPayload(
|
|
98
|
+
bearerToken,
|
|
99
|
+
sessionId,
|
|
100
|
+
dataSource,
|
|
101
|
+
requestDomain?.hostname ? requestDomain.hostname : undefined
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
105
|
+
const operationName: string | undefined = (req as any).body?.operationName;
|
|
106
|
+
if (operationName !== 'IntrospectionQuery') {
|
|
107
|
+
console.log({ operationName });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { dataSource, userPayload };
|
|
111
|
+
};
|
|
@@ -0,0 +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.js';
|
|
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
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Public.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BaseEntity, PotentialDuplicateRequest } from "@memberjunction/core";
|
|
2
|
+
import { RegisterClass } from "@memberjunction/global";
|
|
3
|
+
import { DuplicateRunEntity } from "@memberjunction/core-entities";
|
|
4
|
+
import { DuplicateRecordDetector } from "@memberjunction/ai-vector-dupe";
|
|
5
|
+
|
|
6
|
+
@RegisterClass(BaseEntity, 'Duplicate Runs', 3)
|
|
7
|
+
export class DuplicateRunEntity_Server extends DuplicateRunEntity {
|
|
8
|
+
public async Save(): Promise<boolean> {
|
|
9
|
+
const saveResult: boolean = await super.Save();
|
|
10
|
+
if (saveResult && this.EndedAt === null) {
|
|
11
|
+
// do something
|
|
12
|
+
const duplicateRecordDetector: DuplicateRecordDetector = new DuplicateRecordDetector();
|
|
13
|
+
let request: PotentialDuplicateRequest = new PotentialDuplicateRequest();
|
|
14
|
+
request.EntityID = this.EntityID;
|
|
15
|
+
request.ListID = this.SourceListID;
|
|
16
|
+
request.Options = {
|
|
17
|
+
DuplicateRunID: this.ID,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const response = await duplicateRecordDetector.getDuplicateRecords(request, this.ContextCurrentUser);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return saveResult;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function LoadDuplicateRunEntityServerSubClass() {
|
|
28
|
+
|
|
29
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
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.js';
|
|
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: string[] = [];
|
|
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(): string[] {
|
|
27
|
+
return this._entityIDQueue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public static ClearQueue(): void {
|
|
31
|
+
this._entityIDQueue = [];
|
|
32
|
+
this._submissionTimer = null;
|
|
33
|
+
}
|
|
34
|
+
public static AddToQueue(entityID: string): void {
|
|
35
|
+
if (this._entityIDQueue.indexOf(entityID) === -1) this._entityIDQueue.push(entityID);
|
|
36
|
+
this._lastModifiedTime = new Date();
|
|
37
|
+
this.CheckStartSubmissionTimer();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
protected static CheckStartSubmissionTimer(): void {
|
|
41
|
+
if (this._submissionTimer === null) {
|
|
42
|
+
this.StartSubmissionTimer();
|
|
43
|
+
} else {
|
|
44
|
+
// we need to cancel the existing timer and start a new one
|
|
45
|
+
clearTimeout(this._submissionTimer);
|
|
46
|
+
this.StartSubmissionTimer();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
protected static StartSubmissionTimer(): void {
|
|
51
|
+
this._submissionTimer = setTimeout(() => {
|
|
52
|
+
this.SubmitQueue();
|
|
53
|
+
}, this._submissionDelay);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
protected static async SubmitQueue(): Promise<void> {
|
|
57
|
+
this._lastModifiedTime = null;
|
|
58
|
+
|
|
59
|
+
// now, use Axios to submit the queue to the API server
|
|
60
|
+
// Check if there's anything to submit
|
|
61
|
+
if (this._entityIDQueue.length > 0) {
|
|
62
|
+
try {
|
|
63
|
+
// Use Axios to submit the queue to the API server
|
|
64
|
+
const response = await axios.post(this.getSubmissionURL(), {
|
|
65
|
+
entityIDArray: this._entityIDQueue,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Check the Axios response code implicitly and API response explicitly
|
|
69
|
+
if (response.status === 200 && response.data.status === 'ok') {
|
|
70
|
+
console.log('Queue submitted successfully.');
|
|
71
|
+
// now, clear the queue and timer
|
|
72
|
+
this.ClearQueue();
|
|
73
|
+
} else {
|
|
74
|
+
// Handle API indicating a failure
|
|
75
|
+
console.error('Failed to submit queue:', response.data.errorMessage || 'Unknown error');
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// Handle errors here
|
|
79
|
+
console.error('Failed to submit queue:', error);
|
|
80
|
+
// Consider re-trying or logging the error based on your requirements
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
console.log('No entities to submit.');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
override Save(options?: EntitySaveOptions): Promise<boolean> {
|
|
88
|
+
// simply queue up the entity ID
|
|
89
|
+
if (this.Dirty || options?.IgnoreDirtyState) EntityPermissionsEntity_Server.AddToQueue(this.EntityID);
|
|
90
|
+
|
|
91
|
+
return super.Save(options);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
override async Delete(): Promise<boolean> {
|
|
95
|
+
const success = await super.Delete();
|
|
96
|
+
|
|
97
|
+
// simply queue up the entity ID if the delete worked
|
|
98
|
+
if (success) EntityPermissionsEntity_Server.AddToQueue(this.EntityID);
|
|
99
|
+
|
|
100
|
+
return success;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function LoadEntityPermissionsServerSubClass() {}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { CleanJSON, MJGlobal, RegisterClass } from "@memberjunction/global";
|
|
2
|
+
import { BaseEntity, EntityInfo, LogError, Metadata } from "@memberjunction/core";
|
|
3
|
+
import { AIModelEntity, AIModelEntityExtended, UserViewEntityExtended } from '@memberjunction/core-entities'
|
|
4
|
+
import { BaseLLM, ChatParams, GetAIAPIKey } from "@memberjunction/ai";
|
|
5
|
+
import { AIEngine } from "@memberjunction/aiengine";
|
|
6
|
+
import { LoadOpenAILLM } from "@memberjunction/ai-openai";
|
|
7
|
+
LoadOpenAILLM(); // this is to prevent tree shaking since the openai package is not directly used and rather instantiated dynamically in the LoadOpenAILLM function. Since no static code path exists tree shaking can result in this class being optimized out
|
|
8
|
+
|
|
9
|
+
@RegisterClass(BaseEntity, 'User Views')
|
|
10
|
+
export class UserViewEntity_Server extends UserViewEntityExtended {
|
|
11
|
+
/**
|
|
12
|
+
* This property is hard-coded to true in this class because we DO support smart filters in this class. If you want to disable smart filters for a specific view you can override this property in your subclass and set it to false.
|
|
13
|
+
*/
|
|
14
|
+
protected override get SmartFilterImplemented(): boolean {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Default implementation simply returns 'OpenAI' - override this in your subclass if you are using a different AI vendor.
|
|
20
|
+
* @returns
|
|
21
|
+
*/
|
|
22
|
+
protected get AIVendorName(): string {
|
|
23
|
+
return 'OpenAI';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Default implementation simply grabs the first AI model that matches GetAIModelName().
|
|
28
|
+
* @returns
|
|
29
|
+
*/
|
|
30
|
+
protected async GetAIModel(): Promise<AIModelEntityExtended> {
|
|
31
|
+
await AIEngine.Instance.Config(false, this.ContextCurrentUser); // most of the time this is already loaded, but just in case it isn't we will load it here
|
|
32
|
+
const models = AIEngine.Instance.Models.filter(m => m.AIModelType.trim().toLowerCase() === 'llm' &&
|
|
33
|
+
m.Vendor.trim().toLowerCase() === this.AIVendorName.trim().toLowerCase())
|
|
34
|
+
// next, sort the models by the PowerRank field so that the highest power rank model is the first array element
|
|
35
|
+
models.sort((a, b) => b.PowerRank - a.PowerRank); // highest power rank first
|
|
36
|
+
return models[0];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* This method will use AI to return a valid WHERE clause based on the provided prompt. This is automatically called at the right time if the view has SmartFilterEnabled turned on and the SmartFilterPrompt is set. If you want
|
|
41
|
+
* to call this directly to get back a WHERE clause for other purposes you can call this method directly and provide both a prompt and the entity that the view is based on.
|
|
42
|
+
* @param prompt
|
|
43
|
+
*/
|
|
44
|
+
public async GenerateSmartFilterWhereClause(prompt: string, entityInfo: EntityInfo): Promise<{whereClause: string, userExplanation: string}> {
|
|
45
|
+
try {
|
|
46
|
+
const model = await this.GetAIModel();
|
|
47
|
+
const llm = MJGlobal.Instance.ClassFactory.CreateInstance<BaseLLM>(BaseLLM, model.DriverClass, GetAIAPIKey(model.DriverClass));
|
|
48
|
+
|
|
49
|
+
const chatParams: ChatParams = {
|
|
50
|
+
model: model.APINameOrName,
|
|
51
|
+
messages: [
|
|
52
|
+
{
|
|
53
|
+
role: 'system',
|
|
54
|
+
content: this.GenerateSysPrompt(entityInfo)
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
role: 'user',
|
|
58
|
+
content: `${prompt}`,
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
}
|
|
62
|
+
const result = await llm.ChatCompletion(chatParams);
|
|
63
|
+
if (result && result.data) {
|
|
64
|
+
const llmResponse = result.data.choices[0].message.content;
|
|
65
|
+
if (llmResponse) {
|
|
66
|
+
// try to parse it as JSON
|
|
67
|
+
try {
|
|
68
|
+
const cleansed = CleanJSON(llmResponse);
|
|
69
|
+
if (!cleansed)
|
|
70
|
+
throw new Error('Invalid JSON response from AI: ' + llmResponse);
|
|
71
|
+
|
|
72
|
+
const parsed = JSON.parse(cleansed);
|
|
73
|
+
if (parsed.whereClause && parsed.whereClause.length > 0) {
|
|
74
|
+
// we have the where clause. Sometimes the LLM prefixes it with WHERE and somtimes not, we need to strip WHERE if it is there
|
|
75
|
+
const trimmed = parsed.whereClause.trim();
|
|
76
|
+
let ret: string = '';
|
|
77
|
+
if (trimmed.toLowerCase().startsWith('where '))
|
|
78
|
+
ret = trimmed.substring(6);
|
|
79
|
+
else
|
|
80
|
+
ret = parsed.whereClause;
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
whereClause: ret,
|
|
84
|
+
userExplanation: parsed.userExplanationMessage
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
else if (parsed.whereClause !== undefined && parsed.whereClause !== null) {
|
|
88
|
+
return {
|
|
89
|
+
whereClause: '', // empty string is valid, it means no where clause
|
|
90
|
+
userExplanation: parsed.userExplanationMessage
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// if we get here, no whereClause property was provided by the LLM, that is an error
|
|
95
|
+
throw new Error('Invalid response from AI, no whereClause property found in response: ' + llmResponse);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
LogError(e);
|
|
100
|
+
throw new Error('Error parsing JSON response from AI: ' + llmResponse);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else
|
|
104
|
+
throw new Error('Null response from AI');
|
|
105
|
+
}
|
|
106
|
+
else
|
|
107
|
+
throw new Error('No result returned from AI');
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
LogError(e);
|
|
111
|
+
throw e;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public GenerateSysPrompt(entityInfo: EntityInfo): string {
|
|
116
|
+
const processedViews: string[] = [entityInfo.BaseView];
|
|
117
|
+
const md = new Metadata();
|
|
118
|
+
const gptSysPrompt: string = `You are an expert in SQL and Microsoft SQL Server.
|
|
119
|
+
You will be provided a user prompt representing how they want to filter the data.
|
|
120
|
+
You may *NOT* use JOINS, only sub-queries for related tables.
|
|
121
|
+
|
|
122
|
+
I am a bot and can only understand JSON. Your response must be parsable into this type:
|
|
123
|
+
const returnType = {
|
|
124
|
+
whereClause: string,
|
|
125
|
+
orderByClause: string
|
|
126
|
+
userExplanationMessage: string
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
The view that the user is querying is called ${entityInfo.BaseView} and has these fields:
|
|
130
|
+
${entityInfo.Fields.map(f => {
|
|
131
|
+
let ret: string = `${f.Name} (${f.Type})`;
|
|
132
|
+
if (f.RelatedEntity) {
|
|
133
|
+
ret += ` (fkey to ${f.RelatedEntityBaseView})`;
|
|
134
|
+
}
|
|
135
|
+
return ret;
|
|
136
|
+
}).join(',')}`
|
|
137
|
+
|
|
138
|
+
const fkeyFields = entityInfo.Fields.filter(f => f.RelatedEntity && f.RelatedEntity.length > 0);
|
|
139
|
+
const fkeyBaseViewsDistinct = fkeyFields.map(f => f.RelatedEntityBaseView).filter((v, i, a) => a.indexOf(v) === i);
|
|
140
|
+
const relationships: string = `
|
|
141
|
+
In addition, ${entityInfo.BaseView} has links to other views, as shown here, you can use these views in sub-queries to achieve the request from the user.
|
|
142
|
+
If there are multiple filters related to a single related view, attempt to combine them into a single sub-query for efficiency.
|
|
143
|
+
${
|
|
144
|
+
// this part returns a list of all the related views and the fields that are related to the current view via fkeys in the current view
|
|
145
|
+
fkeyBaseViewsDistinct.map(v => {
|
|
146
|
+
if (processedViews.indexOf(v) === -1) {
|
|
147
|
+
const e = md.Entities.find(e => e.BaseView === v);
|
|
148
|
+
if (e) {
|
|
149
|
+
processedViews.push(v); // already processed this view now, so we won't repeat it
|
|
150
|
+
return `* ${e.SchemaName}.${e.BaseView}: ${e.Fields.map(ef => {
|
|
151
|
+
return ef.Name + ' (' + ef.Type + ')';
|
|
152
|
+
}).join(',') }`
|
|
153
|
+
}
|
|
154
|
+
else
|
|
155
|
+
return '';
|
|
156
|
+
}
|
|
157
|
+
else
|
|
158
|
+
return ''; // already did this at some point
|
|
159
|
+
}).join('\n')
|
|
160
|
+
}
|
|
161
|
+
${
|
|
162
|
+
// this part returns a list of all the related views and the fields that are related to the current view fkeys in THOSE views
|
|
163
|
+
entityInfo.RelatedEntities.map(r => {
|
|
164
|
+
const e = md.Entities.find(e => e.Name === r.RelatedEntity);
|
|
165
|
+
if (e) {
|
|
166
|
+
if (processedViews.indexOf(e.BaseView) === -1) {
|
|
167
|
+
processedViews.push(e.BaseView); // note that we are processing this view now, so we won't repeat it
|
|
168
|
+
return `* ${e.SchemaName}.${e.BaseView}: ${e.Fields.map(ef => {
|
|
169
|
+
let ret: string = `${ef.Name} (${ef.Type})`;
|
|
170
|
+
if (ef.RelatedEntity) {
|
|
171
|
+
ret += ` (fkey to ${ef.RelatedEntityBaseView})`;
|
|
172
|
+
}
|
|
173
|
+
return ret;
|
|
174
|
+
}).join(',') }`
|
|
175
|
+
}
|
|
176
|
+
else
|
|
177
|
+
return ''; // already did this at some point
|
|
178
|
+
}
|
|
179
|
+
else
|
|
180
|
+
return '';
|
|
181
|
+
}).join('\n')
|
|
182
|
+
}`
|
|
183
|
+
|
|
184
|
+
return gptSysPrompt + (processedViews.length > 1 /*we always have 1 from the entity base view*/ ? relationships : '') + `
|
|
185
|
+
**** REMEMBER **** I am a BOT, do not return anything other than JSON to me or I will choke on your response!`;
|
|
186
|
+
}
|
|
187
|
+
}
|