Skip to content

Commit

Permalink
Merge pull request #2467 from ideafast/feat/trpc-domain
Browse files Browse the repository at this point in the history
Feat/trpc domain
  • Loading branch information
wsy19961129 authored Jul 13, 2024
2 parents 23b53b0 + a131784 commit 9f70883
Show file tree
Hide file tree
Showing 36 changed files with 1,951 additions and 307 deletions.
3 changes: 2 additions & 1 deletion packages/itmat-cores/config/config.sample.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"pubkeys_collection": "PUBKEY_COLLECTION",
"standardizations_collection": "STANDARDIZATION_COLLECTION",
"colddata_collection": "COLDDATA_COLLECTION",
"cache_collection": "CACHE_COLLECTION"
"cache_collection": "CACHE_COLLECTION",
"domains_collection": "DOMAIN_COLLECTION"
}
},
"server": {
Expand Down
43 changes: 28 additions & 15 deletions packages/itmat-cores/src/GraphQLCore/logCore.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { ILogEntry, IUserWithoutToken, LOG_ACTION, LOG_STATUS, LOG_TYPE, enumUserTypes } from '@itmat-broker/itmat-types';
import { ILog, IUserWithoutToken, enumEventStatus, enumEventType, enumUserTypes } from '@itmat-broker/itmat-types';
import { DBType } from '../database/database';
import { GraphQLError } from 'graphql';
import { errorCodes } from '../utils/errors';
import { Filter } from 'mongodb';

// the GraphQL log APIs will be removed in further development
export class LogCore {
db: DBType;
constructor(db: DBType) {
Expand All @@ -15,7 +16,7 @@ export class LogCore {
UPLOAD_FILE: ['file', 'description']
};

public async getLogs(requester: IUserWithoutToken | undefined, requesterName?: string, requesterType?: enumUserTypes, logType?: LOG_TYPE, actionType?: LOG_ACTION, status?: LOG_STATUS) {
public async getLogs(requester: IUserWithoutToken | undefined, requesterName?: string, requesterType?: enumUserTypes, logType?: enumEventType, actionType?: string, status?: enumEventStatus) {
if (!requester) {
throw new GraphQLError(errorCodes.CLIENT_ACTION_ON_NON_EXISTENT_ENTRY);
}
Expand All @@ -24,32 +25,44 @@ export class LogCore {
throw new GraphQLError(errorCodes.NO_PERMISSION_ERROR);
}

const queryObj: Filter<ILogEntry> = {};
if (requesterName) { queryObj.requesterName = requesterName; }
if (requesterType) { queryObj.requesterType = requesterType; }
if (logType) { queryObj.logType = logType; }
if (actionType) { queryObj.actionType = actionType; }
const queryObj: Filter<ILog> = {};
if (requesterName) { queryObj.requester = requesterName; }
if (logType) { queryObj.type = logType; }
if (actionType) { queryObj.event = actionType; }
if (status) { queryObj.status = status; }

const logData = await this.db.collections.log_collection.find<ILogEntry>(queryObj, { projection: { _id: 0 } }).limit(1000).sort('time', -1).toArray();
const logData = await this.db.collections.log_collection.find(queryObj, { projection: { _id: 0 } }).limit(1000).sort('life.createdTime', -1).toArray();
// log information decoration
for (const i in logData) {
logData[i].actionData = JSON.stringify(await this.logDecorationHelper(logData[i].actionData, logData[i].actionType));
logData[i].parameters = await this.logDecorationHelper(logData[i].parameters ?? {}, logData[i].event);
}

return logData.map((log) => {
return {
id: log.id,
requesterName: log.requester,
requesterType: requester.type,
userAgent: 'MOZILLA',
logType: log.type,
actionType: log.event,
actionData: JSON.stringify(log.parameters),
time: log.life.createdTime,
status: log.status,
error: log.errors
};
});
return logData;
}

public async logDecorationHelper(actionData: string, actionType: string) {
const obj = JSON.parse(actionData) ?? {};
public async logDecorationHelper(actionData: Record<string, unknown>, actionType: string) {
const obj = { ...actionData };
if (Object.keys(LogCore.hiddenFields).includes(actionType)) {
for (let i = 0; i < LogCore.hiddenFields[actionType as keyof typeof LogCore.hiddenFields].length; i++) {
delete obj[LogCore.hiddenFields[actionType as keyof typeof LogCore.hiddenFields][i]];
}
}
if (actionType === LOG_ACTION.getStudy) {
const studyId = obj['studyId'];
const study = await this.db.collections.studies_collection.findOne({ id: studyId, deleted: null });
if (actionType === 'getStudy') {
const studyId = obj['studyId'] ?? '';
const study = await this.db.collections.studies_collection.findOne({ 'id': studyId, 'life.deletedTime': null });
if (study === null || study === undefined) {
obj['name'] = '';
}
Expand Down
10 changes: 6 additions & 4 deletions packages/itmat-cores/src/database/database.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IField, IFile, IJobEntry, ILogEntry, IOrganisation, IProject, IPubkey, IQueryEntry, IRole, IStudy, IUser, IStandardization, IConfig, IData, IDrive, ICache } from '@itmat-broker/itmat-types';
import type { IField, IFile, IJobEntry, ILog, IOrganisation, IProject, IPubkey, IQueryEntry, IRole, IStudy, IUser, IStandardization, IConfig, IData, IDrive, ICache, IDomain } from '@itmat-broker/itmat-types';
import { Database as DatabaseBase, IDatabaseBaseConfig } from '@itmat-broker/itmat-commons';
import type { Collection } from 'mongodb';

Expand All @@ -20,7 +20,8 @@ export interface IDatabaseConfig extends IDatabaseBaseConfig {
configs_collection: string,
drives_collection: string,
colddata_collection: string,
cache_collection: string
cache_collection: string,
domains_collection: string
};
}

Expand All @@ -34,13 +35,14 @@ export interface IDatabaseCollectionConfig {
roles_collection: Collection<IRole>,
files_collection: Collection<IFile>,
organisations_collection: Collection<IOrganisation>,
log_collection: Collection<ILogEntry>,
log_collection: Collection<ILog>,
pubkeys_collection: Collection<IPubkey>,
data_collection: Collection<IData>,
standardizations_collection: Collection<IStandardization>,
configs_collection: Collection<IConfig>,
drives_collection: Collection<IDrive>,
colddata_collection: Collection<IData>,
cache_collection: Collection<ICache>
cache_collection: Collection<ICache>,
domains_collection: Collection<IDomain>
}
export type DBType = DatabaseBase<IDatabaseBaseConfig, IDatabaseCollectionConfig>;
2 changes: 2 additions & 0 deletions packages/itmat-cores/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export * from './trpcCore/userCore';
export * from './trpcCore/dataCore';
export * from './trpcCore/transformationCore';
export * from './trpcCore/permissionCore';
export * from './trpcCore/logCore';
export * from './trpcCore/domainCore';
export * from './rest/fileDownload';
export * from './authentication/pubkeyAuthentication';
export * from './log/logPlugin';
Expand Down
76 changes: 41 additions & 35 deletions packages/itmat-cores/src/log/logPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,64 @@
import { v4 as uuid } from 'uuid';
import { LOG_TYPE, LOG_ACTION, LOG_STATUS, USER_AGENT, enumUserTypes } from '@itmat-broker/itmat-types';
import { enumAPIResolver, enumEventStatus, enumEventType, enumReservedUsers } from '@itmat-broker/itmat-types';
import { GraphQLRequestContextWillSendResponse } from '@apollo/server';
import { ApolloServerContext } from '../utils/ApolloServerContext';
import { DBType } from '../database/database';

// only requests in white list will be recorded
export const logActionRecordWhiteList = Object.keys(LOG_ACTION);

// only requests in white list will be recorded
export const logActionShowWhiteList = Object.keys(LOG_ACTION);

export class LogPlugin {
db: DBType;
constructor(db: DBType) {
this.db = db;
}

public async serverWillStartLogPlugin(): Promise<null> {
public async serverWillStartLogPlugin() {
await this.db.collections.log_collection.insertOne({
id: uuid(),
requesterName: enumUserTypes.SYSTEM,
requesterType: enumUserTypes.SYSTEM,
logType: LOG_TYPE.SYSTEM_LOG,
actionType: LOG_ACTION.startSERVER,
actionData: JSON.stringify({}),
time: Date.now(),
status: LOG_STATUS.SUCCESS,
errors: '',
userAgent: USER_AGENT.OTHER
requester: enumReservedUsers.SYSTEM,
type: enumEventType.SYSTEM_LOG,
apiResolver: null,
event: 'SERVER_START',
parameters: undefined,
status: enumEventStatus.SUCCESS,
errors: undefined,
timeConsumed: null,
life: {
createdTime: Date.now(),
createdUser: enumReservedUsers.SYSTEM,
deletedTime: null,
deletedUser: null
},
metadata: {}
});
return null;
return;
}

public async requestDidStartLogPlugin(requestContext: GraphQLRequestContextWillSendResponse<ApolloServerContext>): Promise<null> {
if (!requestContext.operationName || !logActionRecordWhiteList.includes(requestContext.operationName)) {
return null;
}
if (LOG_ACTION[requestContext.operationName] === undefined || LOG_ACTION[requestContext.operationName] === null) {
return null;
public async requestDidStartLogPlugin(requestContext: GraphQLRequestContextWillSendResponse<ApolloServerContext>, startTime: number, executionTime: number) {
if (!requestContext.operationName) {
return;
}

const variables = requestContext.request.variables ?? {}; // Add null check
await this.db.collections.log_collection.insertOne({
id: uuid(),
requesterName: requestContext.contextValue?.req?.user?.username ?? 'NA',
requesterType: requestContext.contextValue?.req?.user?.type ?? enumUserTypes.SYSTEM,
userAgent: (requestContext.contextValue.req?.headers['user-agent'] as string)?.startsWith('Mozilla') ? USER_AGENT.MOZILLA : USER_AGENT.OTHER,
logType: LOG_TYPE.REQUEST_LOG,
actionType: LOG_ACTION[requestContext.operationName],
actionData: JSON.stringify(ignoreFieldsHelper(variables, requestContext.operationName)), // Use the null-checked variables
time: Date.now(),
status: requestContext.errors === undefined ? LOG_STATUS.SUCCESS : LOG_STATUS.FAIL,
errors: requestContext.errors === undefined ? '' : requestContext.errors[0].message
requester: requestContext.contextValue?.req?.user?.username ?? 'NA',
type: enumEventType.API_LOG,
apiResolver: enumAPIResolver.GraphQL,
event: requestContext.operationName,
parameters: ignoreFieldsHelper(variables, requestContext.operationName),
status: requestContext.errors === undefined ? enumEventStatus.SUCCESS : enumEventStatus.FAIL,
errors: requestContext.errors === undefined ? undefined : requestContext.errors[0].message,
timeConsumed: executionTime,
life: {
createdTime: Date.now(),
createdUser: 'SYSTEMAGENT',
deletedTime: null,
deletedUser: null
},
metadata: {
startTime: startTime,
endTime: startTime + executionTime
}
});
return null;
}
}

Expand Down Expand Up @@ -117,5 +123,5 @@ function ignoreFieldsHelper(dataObj: DataObj, operationName: LogOperationName) {
} else if (operationName === 'uploadFile') {
delete dataObj['file'];
}
return dataObj;
return dataObj as Record<string, unknown>;
}
111 changes: 108 additions & 3 deletions packages/itmat-cores/src/trpcCore/configCore.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { CoreError, defaultSettings, enumConfigType, enumCoreErrors, enumReservedUsers } from '@itmat-broker/itmat-types';
import { CoreError, ICacheConfig, IConfig, IDocConfig, IDomainConfig, IStudyConfig, ISystemConfig, IUserConfig, IUserWithoutToken, defaultSettings, enumConfigType, enumCoreErrors, enumReservedUsers, enumUserTypes } from '@itmat-broker/itmat-types';
import { DBType } from '../database/database';
import { v4 as uuid } from 'uuid';
import { makeGenericReponse } from '../utils';

export class TRPCConfigCore {
db: DBType;
constructor(db: DBType) {
this.db = db;
}
/**
* Get the config.
* Get the config. Suggestions: always set useDefault to true.
*
* @param configType - The type of the config..
* @param key - The key of the config. studyid, userid, or null for system.
Expand Down Expand Up @@ -40,8 +41,15 @@ export class TRPCConfigCore {
return defaultSettings.systemConfig;
} else if (configType === enumConfigType.USERCONFIG) {
return defaultSettings.userConfig;
} else if (configType === enumConfigType.DOCCONFIG) {
return defaultSettings.docConfig;
} else if (configType === enumConfigType.DOMAINCONFIG) {
return defaultSettings.domainConfig;
} else {
return defaultSettings.userConfig;
throw new CoreError(
enumCoreErrors.CLIENT_MALFORMED_INPUT,
'Config type not recognised.'
);
}
})()
};
Expand All @@ -54,4 +62,101 @@ export class TRPCConfigCore {
}
return config;
}

/**
* Edit the config.
*
* @param requester - The requester.
* @param configType - The type of the config.
* @param key - The key of the config.
* @param properties - The updated properties.
*
* @returns IGenericResponse
*/
public async editConfig(requester: IUserWithoutToken | undefined, configType: enumConfigType, key: string | null, properties: ISystemConfig | IStudyConfig | IUserConfig | IDocConfig | ICacheConfig | IDomainConfig) {
if (!requester) {
throw new CoreError(
enumCoreErrors.NOT_LOGGED_IN,
enumCoreErrors.NOT_LOGGED_IN
);
}

// for now only admin can edit config, update it for further use
if (requester.type !== enumUserTypes.ADMIN) {
throw new CoreError(
enumCoreErrors.NO_PERMISSION_ERROR,
enumCoreErrors.NO_PERMISSION_ERROR
);
}

// validate properties and config type match
if (!((configType === enumConfigType.SYSTEMCONFIG && 'defaultBackgroundColor' in properties) ||
(configType === enumConfigType.STUDYCONFIG && 'defaultStudyProfile' in properties) ||
(configType === enumConfigType.USERCONFIG && 'defaultUserExpiredDays' in properties) ||
(configType === enumConfigType.DOCCONFIG && properties['defaultFileBucketId'] === 'doc') ||
(configType === enumConfigType.CACHECONFIG && properties['defaultFileBucketId'] === 'cache') ||
(configType === enumConfigType.DOMAINCONFIG && properties['defaultFileBucketId'] === 'domain')
)) {
throw new CoreError(
enumCoreErrors.CLIENT_MALFORMED_INPUT,
'Config type and properties do not match.'
);
}

const config = await this.db.collections.configs_collection.findOne({ 'type': configType, 'key': key, 'life.deletedTime': null });
if (!config) {
throw new CoreError(
enumCoreErrors.CLIENT_MALFORMED_INPUT,
'Config does not exist.'
);
}
await this.db.collections.configs_collection.updateOne({ 'type': configType, 'key': key, 'life.deletedTime': null }, { $set: { properties: properties } });

return makeGenericReponse(config.id, true, undefined, 'Config updated.');
}

/**
* Create a new config.
* Note, this API is used in case sth unusual happen. In most cases the API should neven be used.
*
* @param requester - The requester.
* @param configType - The type of the config.
* @param key - The key of the config.
* @param properties - The properties of the config.
*
* @returns IConfig
*/
public async createConfig(requester: IUserWithoutToken | undefined, configType: enumConfigType, key: string | null, properties: ISystemConfig | IStudyConfig | IUserConfig | IDocConfig | ICacheConfig | IDomainConfig) {
if (!requester) {
throw new CoreError(
enumCoreErrors.NOT_LOGGED_IN,
enumCoreErrors.NOT_LOGGED_IN
);
}

if (requester.type !== enumUserTypes.ADMIN) {
throw new CoreError(
enumCoreErrors.NO_PERMISSION_ERROR,
enumCoreErrors.NO_PERMISSION_ERROR
);
}

const config: IConfig = {
id: uuid(),
type: configType,
key: key,
life: {
createdTime: Date.now(),
createdUser: requester.id,
deletedTime: null,
deletedUser: null
},
metadata: {},
properties: properties
};

await this.db.collections.configs_collection.insertOne(config);
return config;

}
}
Loading

0 comments on commit 9f70883

Please sign in to comment.