feat: better server lint (#2850)

* feat: add stylistic eslint plugin

* feat: add missing line return

* feat: secure line-break style

* feat: disallow break before else

* feat: line between class members

* feat: better new line lint rule
This commit is contained in:
Jérémy M
2023-12-06 12:19:00 +01:00
committed by GitHub
parent e388d90976
commit 9df83c9a5a
75 changed files with 318 additions and 21 deletions

View File

@ -5,7 +5,7 @@ module.exports = {
tsconfigRootDir : __dirname, tsconfigRootDir : __dirname,
sourceType: 'module', sourceType: 'module',
}, },
plugins: ['@typescript-eslint/eslint-plugin', 'import', 'unused-imports'], plugins: ['@typescript-eslint/eslint-plugin', 'import', 'unused-imports', '@stylistic'],
extends: [ extends: [
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended', 'plugin:prettier/recommended',
@ -77,5 +77,16 @@ module.exports = {
'import/no-duplicates': ["error", {"considerQueryString": true}], 'import/no-duplicates': ["error", {"considerQueryString": true}],
'unused-imports/no-unused-imports': 'warn', 'unused-imports/no-unused-imports': 'warn',
"@typescript-eslint/consistent-type-imports": ["error", { "prefer": "no-type-imports" }], "@typescript-eslint/consistent-type-imports": ["error", { "prefer": "no-type-imports" }],
"@stylistic/linebreak-style": ["error", "unix"],
"@stylistic/lines-between-class-members": ["error", { "enforce": [
{ blankLine: "always", prev: "method", next: "method" }
]}],
"@stylistic/padding-line-between-statements": [
"error",
{ blankLine: "always", prev: "*", next: "return" },
{ blankLine: "always", prev: ["const", "let", "var"], next: "*"},
{ blankLine: "any", prev: ["const", "let", "var"], next: ["const", "let", "var"] },
{ blankLine: "always", prev: "*", next: ["interface", "type"] }
]
}, },
}; };

View File

@ -1,4 +1,5 @@
{ {
"singleQuote": true, "singleQuote": true,
"trailingComma": "all" "trailingComma": "all",
} "brakeBeforeElse": false
}

View File

@ -106,6 +106,7 @@
"@nestjs/cli": "^9.0.0", "@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.0", "@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0", "@nestjs/testing": "^9.0.0",
"@stylistic/eslint-plugin": "^1.5.0",
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/bytes": "^3.1.1", "@types/bytes": "^3.1.1",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",

View File

@ -69,6 +69,7 @@ import { ExceptionFilter } from './filters/exception.filter';
); );
const contextId = ContextIdFactory.create(); const contextId = ContextIdFactory.create();
AppModule.moduleRef.registerRequestByContextId(request, contextId); AppModule.moduleRef.registerRequestByContextId(request, contextId);
// Get the SchemaGenerationService from the AppModule // Get the SchemaGenerationService from the AppModule

View File

@ -11,24 +11,28 @@ export class ApiRestController {
@Get() @Get()
async handleApiGet(@Req() request: Request): Promise<object> { async handleApiGet(@Req() request: Request): Promise<object> {
const result = await this.apiRestService.get(request); const result = await this.apiRestService.get(request);
return result.data; return result.data;
} }
@Delete() @Delete()
async handleApiDelete(@Req() request: Request): Promise<object> { async handleApiDelete(@Req() request: Request): Promise<object> {
const result = await this.apiRestService.delete(request); const result = await this.apiRestService.delete(request);
return result.data; return result.data;
} }
@Post() @Post()
async handleApiPost(@Req() request: Request): Promise<object> { async handleApiPost(@Req() request: Request): Promise<object> {
const result = await this.apiRestService.create(request); const result = await this.apiRestService.create(request);
return result.data; return result.data;
} }
@Put() @Put()
async handleApiPut(@Req() request: Request): Promise<object> { async handleApiPut(@Req() request: Request): Promise<object> {
const result = await this.apiRestService.update(request); const result = await this.apiRestService.update(request);
return result.data; return result.data;
} }
} }

View File

@ -7,6 +7,7 @@ import { EnvironmentService } from 'src/integrations/environment/environment.ser
describe('ApiRestService', () => { describe('ApiRestService', () => {
let service: ApiRestService; let service: ApiRestService;
const objectMetadataItem = { fields: [{ name: 'field', type: 'NUMBER' }] }; const objectMetadataItem = { fields: [{ name: 'field', type: 'NUMBER' }] };
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [ providers: [

View File

@ -24,6 +24,7 @@ enum FILTER_COMPARATORS {
const ALLOWED_DEPTH_VALUES = [1, 2]; const ALLOWED_DEPTH_VALUES = [1, 2];
const DEFAULT_DEPTH_VALUE = 2; const DEFAULT_DEPTH_VALUE = 2;
const DEFAULT_ORDER_DIRECTION = OrderByDirection.AscNullsFirst; const DEFAULT_ORDER_DIRECTION = OrderByDirection.AscNullsFirst;
enum CONJUNCTIONS { enum CONJUNCTIONS {
or = 'or', or = 'or',
and = 'and', and = 'and',
@ -275,16 +276,19 @@ export class ApiRestService {
const [objectMetadata] = objectMetadataItems.filter( const [objectMetadata] = objectMetadataItems.filter(
(object) => object.namePlural === parsedObject, (object) => object.namePlural === parsedObject,
); );
if (!objectMetadata) { if (!objectMetadata) {
const [wrongObjectMetadata] = objectMetadataItems.filter( const [wrongObjectMetadata] = objectMetadataItems.filter(
(object) => object.nameSingular === parsedObject, (object) => object.nameSingular === parsedObject,
); );
let hint = 'eg: companies'; let hint = 'eg: companies';
if (wrongObjectMetadata) { if (wrongObjectMetadata) {
hint = `Did you mean '${wrongObjectMetadata.namePlural}'?`; hint = `Did you mean '${wrongObjectMetadata.namePlural}'?`;
} }
throw Error(`object '${parsedObject}' not found. ${hint}`); throw Error(`object '${parsedObject}' not found. ${hint}`);
} }
return { return {
objectMetadataItems, objectMetadataItems,
objectMetadataItem: objectMetadata, objectMetadataItem: objectMetadata,
@ -295,6 +299,7 @@ export class ApiRestService {
if (!(filterQuery.includes('(') && filterQuery.includes(')'))) { if (!(filterQuery.includes('(') && filterQuery.includes(')'))) {
return `${DEFAULT_FILTER_CONJUNCTION}(${filterQuery})`; return `${DEFAULT_FILTER_CONJUNCTION}(${filterQuery})`;
} }
return filterQuery; return filterQuery;
} }
@ -302,6 +307,7 @@ export class ApiRestService {
const countOpenedBrackets = (filterQuery.match(/\(/g) || []).length; const countOpenedBrackets = (filterQuery.match(/\(/g) || []).length;
const countClosedBrackets = (filterQuery.match(/\)/g) || []).length; const countClosedBrackets = (filterQuery.match(/\)/g) || []).length;
const diff = countOpenedBrackets - countClosedBrackets; const diff = countOpenedBrackets - countClosedBrackets;
if (diff !== 0) { if (diff !== 0) {
const hint = const hint =
diff > 0 diff > 0
@ -309,8 +315,10 @@ export class ApiRestService {
: `${Math.abs(diff)} close bracket${ : `${Math.abs(diff)} close bracket${
Math.abs(diff) > 1 ? 's are' : ' is' Math.abs(diff) > 1 ? 's are' : ' is'
}`; }`;
throw Error(`'filter' invalid. ${hint} missing in the query`); throw Error(`'filter' invalid. ${hint} missing in the query`);
} }
return; return;
} }
@ -318,6 +326,7 @@ export class ApiRestService {
let parenthesisCounter = 0; let parenthesisCounter = 0;
const predicates: string[] = []; const predicates: string[] = [];
let currentPredicates = ''; let currentPredicates = '';
for (const c of filterQuery) { for (const c of filterQuery) {
if (c === '(') { if (c === '(') {
parenthesisCounter++; parenthesisCounter++;
@ -337,6 +346,7 @@ export class ApiRestService {
if (currentPredicates.length) { if (currentPredicates.length) {
predicates.push(currentPredicates); predicates.push(currentPredicates);
} }
return predicates; return predicates;
} }
@ -345,11 +355,13 @@ export class ApiRestService {
const match = filterQuery.match( const match = filterQuery.match(
`^(${Object.values(CONJUNCTIONS).join('|')})((.+))$`, `^(${Object.values(CONJUNCTIONS).join('|')})((.+))$`,
); );
if (match) { if (match) {
const conjunction = match[1]; const conjunction = match[1];
const subResult = this.parseFilterQueryContent(filterQuery).map((elem) => const subResult = this.parseFilterQueryContent(filterQuery).map((elem) =>
this.parseStringFilter(elem, objectMetadataItem), this.parseStringFilter(elem, objectMetadataItem),
); );
if (conjunction === CONJUNCTIONS.not) { if (conjunction === CONJUNCTIONS.not) {
if (subResult.length > 1) { if (subResult.length > 1) {
throw Error( throw Error(
@ -360,8 +372,10 @@ export class ApiRestService {
} else { } else {
result[conjunction] = subResult; result[conjunction] = subResult;
} }
return result; return result;
} }
return this.parseSimpleFilter(filterQuery, objectMetadataItem); return this.parseSimpleFilter(filterQuery, objectMetadataItem);
} }
@ -376,6 +390,7 @@ export class ApiRestService {
} }
const [fieldAndComparator, value] = filterString.split(':'); const [fieldAndComparator, value] = filterString.split(':');
const [field, comparator] = fieldAndComparator.replace(']', '').split('['); const [field, comparator] = fieldAndComparator.replace(']', '').split('[');
if (!Object.keys(FILTER_COMPARATORS).includes(comparator)) { if (!Object.keys(FILTER_COMPARATORS).includes(comparator)) {
throw Error( throw Error(
`'filter' invalid for '${filterString}', comparator ${comparator} not in ${Object.keys( `'filter' invalid for '${filterString}', comparator ${comparator} not in ${Object.keys(
@ -384,9 +399,11 @@ export class ApiRestService {
); );
} }
const fields = field.split('.'); const fields = field.split('.');
this.checkFields(objectMetadataItem, fields, 'filter'); this.checkFields(objectMetadataItem, fields, 'filter');
const fieldType = this.getFieldType(objectMetadataItem, fields[0]); const fieldType = this.getFieldType(objectMetadataItem, fields[0]);
const formattedValue = this.formatFieldValue(value, fieldType); const formattedValue = this.formatFieldValue(value, fieldType);
return fields.reverse().reduce( return fields.reverse().reduce(
(acc, currentValue) => { (acc, currentValue) => {
return { [currentValue]: acc }; return { [currentValue]: acc };
@ -402,35 +419,42 @@ export class ApiRestService {
if (fieldType === 'BOOLEAN') { if (fieldType === 'BOOLEAN') {
return value.toLowerCase() === 'true'; return value.toLowerCase() === 'true';
} }
return value; return value;
} }
parseFilter(request, objectMetadataItem) { parseFilter(request, objectMetadataItem) {
const parsedObjectId = this.parseObject(request)[1]; const parsedObjectId = this.parseObject(request)[1];
if (parsedObjectId) { if (parsedObjectId) {
return { id: { eq: parsedObjectId } }; return { id: { eq: parsedObjectId } };
} }
const rawFilterQuery = request.query.filter; const rawFilterQuery = request.query.filter;
if (typeof rawFilterQuery !== 'string') { if (typeof rawFilterQuery !== 'string') {
return {}; return {};
} }
this.checkFilterQuery(rawFilterQuery); this.checkFilterQuery(rawFilterQuery);
const filterQuery = this.addDefaultConjunctionIfMissing(rawFilterQuery); const filterQuery = this.addDefaultConjunctionIfMissing(rawFilterQuery);
return this.parseStringFilter(filterQuery, objectMetadataItem); return this.parseStringFilter(filterQuery, objectMetadataItem);
} }
parseOrderBy(request, objectMetadataItem) { parseOrderBy(request, objectMetadataItem) {
//?order_by=field_1[AscNullsFirst],field_2[DescNullsLast],field_3 //?order_by=field_1[AscNullsFirst],field_2[DescNullsLast],field_3
const orderByQuery = request.query.order_by; const orderByQuery = request.query.order_by;
if (typeof orderByQuery !== 'string') { if (typeof orderByQuery !== 'string') {
return {}; return {};
} }
const orderByItems = orderByQuery.split(','); const orderByItems = orderByQuery.split(',');
const result = {}; const result = {};
for (const orderByItem of orderByItems) { for (const orderByItem of orderByItems) {
// orderByItem -> field_1[AscNullsFirst] // orderByItem -> field_1[AscNullsFirst]
if (orderByItem.includes('[') && orderByItem.includes(']')) { if (orderByItem.includes('[') && orderByItem.includes(']')) {
const [field, direction] = orderByItem.replace(']', '').split('['); const [field, direction] = orderByItem.replace(']', '').split('[');
// field -> field_1 ; direction -> AscNullsFirst // field -> field_1 ; direction -> AscNullsFirst
if (!(direction in OrderByDirection)) { if (!(direction in OrderByDirection)) {
throw Error( throw Error(
@ -448,6 +472,7 @@ export class ApiRestService {
} }
} }
this.checkFields(objectMetadataItem, Object.keys(result), 'order_by'); this.checkFields(objectMetadataItem, Object.keys(result), 'order_by');
return <RecordOrderBy>result; return <RecordOrderBy>result;
} }
@ -482,21 +507,26 @@ export class ApiRestService {
parseLimit(request) { parseLimit(request) {
const limitQuery = request.query.limit; const limitQuery = request.query.limit;
if (typeof limitQuery !== 'string') { if (typeof limitQuery !== 'string') {
return 60; return 60;
} }
const limitParsed = parseInt(limitQuery); const limitParsed = parseInt(limitQuery);
if (!Number.isInteger(limitParsed)) { if (!Number.isInteger(limitParsed)) {
throw Error(`limit '${limitQuery}' is invalid. Should be an integer`); throw Error(`limit '${limitQuery}' is invalid. Should be an integer`);
} }
return limitParsed; return limitParsed;
} }
parseCursor(request) { parseCursor(request) {
const cursorQuery = request.query.last_cursor; const cursorQuery = request.query.last_cursor;
if (typeof cursorQuery !== 'string') { if (typeof cursorQuery !== 'string') {
return undefined; return undefined;
} }
return cursorQuery; return cursorQuery;
} }
@ -511,6 +541,7 @@ export class ApiRestService {
parseObject(request) { parseObject(request) {
const queryAction = request.path.replace('/rest/', '').split('/'); const queryAction = request.path.replace('/rest/', '').split('/');
if (queryAction.length > 2) { if (queryAction.length > 2) {
throw Error( throw Error(
`Query path '${request.path}' invalid. Valid examples: /rest/companies/id or /rest/companies`, `Query path '${request.path}' invalid. Valid examples: /rest/companies/id or /rest/companies`,
@ -519,14 +550,17 @@ export class ApiRestService {
if (queryAction.length === 1) { if (queryAction.length === 1) {
return [queryAction[0], undefined]; return [queryAction[0], undefined];
} }
return queryAction; return queryAction;
} }
extractWorkspaceId(request: Request) { extractWorkspaceId(request: Request) {
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request); const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
if (!token) { if (!token) {
throw Error('missing authentication token'); throw Error('missing authentication token');
} }
return verify(token, this.environmentService.getAccessTokenSecret())[ return verify(token, this.environmentService.getAccessTokenSecret())[
'workspaceId' 'workspaceId'
]; ];
@ -537,6 +571,7 @@ export class ApiRestService {
typeof request.query.depth === 'string' typeof request.query.depth === 'string'
? parseInt(request.query.depth) ? parseInt(request.query.depth)
: DEFAULT_DEPTH_VALUE; : DEFAULT_DEPTH_VALUE;
if (!ALLOWED_DEPTH_VALUES.includes(depth)) { if (!ALLOWED_DEPTH_VALUES.includes(depth)) {
throw Error( throw Error(
`'depth=${depth}' parameter invalid. Allowed values are ${ALLOWED_DEPTH_VALUES.join( `'depth=${depth}' parameter invalid. Allowed values are ${ALLOWED_DEPTH_VALUES.join(
@ -544,6 +579,7 @@ export class ApiRestService {
)}`, )}`,
); );
} }
return depth; return depth;
} }
@ -583,6 +619,7 @@ export class ApiRestService {
objectMetadata.objectMetadataItem, objectMetadata.objectMetadataItem,
), ),
}; };
return await this.callGraphql(request, data); return await this.callGraphql(request, data);
} catch (err) { } catch (err) {
return { data: { error: `${err}` } }; return { data: { error: `${err}` } };
@ -593,6 +630,7 @@ export class ApiRestService {
try { try {
const objectMetadata = await this.getObjectMetadata(request); const objectMetadata = await this.getObjectMetadata(request);
const id = this.parseObject(request)[1]; const id = this.parseObject(request)[1];
if (!id) { if (!id) {
return { return {
data: { data: {
@ -606,6 +644,7 @@ export class ApiRestService {
id: this.parseObject(request)[1], id: this.parseObject(request)[1],
}, },
}; };
return await this.callGraphql(request, data); return await this.callGraphql(request, data);
} catch (err) { } catch (err) {
return { data: { error: `${err}` } }; return { data: { error: `${err}` } };
@ -626,6 +665,7 @@ export class ApiRestService {
data: request.body, data: request.body,
}, },
}; };
return await this.callGraphql(request, data); return await this.callGraphql(request, data);
} catch (err) { } catch (err) {
return { data: { error: `${err}` } }; return { data: { error: `${err}` } };
@ -637,6 +677,7 @@ export class ApiRestService {
const objectMetadata = await this.getObjectMetadata(request); const objectMetadata = await this.getObjectMetadata(request);
const depth = this.computeDepth(request); const depth = this.computeDepth(request);
const id = this.parseObject(request)[1]; const id = this.parseObject(request)[1];
if (!id) { if (!id) {
return { return {
data: { data: {
@ -655,6 +696,7 @@ export class ApiRestService {
data: request.body, data: request.body,
}, },
}; };
return await this.callGraphql(request, data); return await this.callGraphql(request, data);
} catch (err) { } catch (err) {
return { data: { error: `${err}` } }; return { data: { error: `${err}` } };

View File

@ -47,6 +47,7 @@ export class AuthResolver {
const { exists } = await this.authService.checkUserExists( const { exists } = await this.authService.checkUserExists(
checkUserExistsInput.email, checkUserExistsInput.email,
); );
return { exists }; return { exists };
} }

View File

@ -8,12 +8,14 @@ import { GoogleStrategy } from 'src/core/auth/strategies/google.auth.strategy';
@Injectable() @Injectable()
export class GoogleProviderEnabledGuard implements CanActivate { export class GoogleProviderEnabledGuard implements CanActivate {
constructor(private readonly environmentService: EnvironmentService) {} constructor(private readonly environmentService: EnvironmentService) {}
canActivate(): boolean | Promise<boolean> | Observable<boolean> { canActivate(): boolean | Promise<boolean> | Observable<boolean> {
if (!this.environmentService.isAuthGoogleEnabled()) { if (!this.environmentService.isAuthGoogleEnabled()) {
throw new NotFoundException('Google auth is not enabled'); throw new NotFoundException('Google auth is not enabled');
} }
new GoogleStrategy(this.environmentService); new GoogleStrategy(this.environmentService);
return true; return true;
} }
} }

View File

@ -89,10 +89,12 @@ export class AuthService {
const existingUser = await this.userRepository.findOneBy({ const existingUser = await this.userRepository.findOneBy({
email: email, email: email,
}); });
assert(!existingUser, 'This user already exists', ForbiddenException); assert(!existingUser, 'This user already exists', ForbiddenException);
if (password) { if (password) {
const isPasswordValid = PASSWORD_REGEX.test(password); const isPasswordValid = PASSWORD_REGEX.test(password);
assert(isPasswordValid, 'Password too weak', BadRequestException); assert(isPasswordValid, 'Password too weak', BadRequestException);
} }
@ -115,6 +117,7 @@ export class AuthService {
domainName: '', domainName: '',
inviteHash: v4(), inviteHash: v4(),
}); });
workspace = await this.workspaceRepository.save(workspaceToCreate); workspace = await this.workspaceRepository.save(workspaceToCreate);
await this.workspaceManagerService.init(workspace.id); await this.workspaceManagerService.init(workspace.id);
} }

View File

@ -34,6 +34,7 @@ export class TokenService {
async generateAccessToken(userId: string): Promise<AuthToken> { async generateAccessToken(userId: string): Promise<AuthToken> {
const expiresIn = this.environmentService.getAccessTokenExpiresIn(); const expiresIn = this.environmentService.getAccessTokenExpiresIn();
assert(expiresIn, '', InternalServerErrorException); assert(expiresIn, '', InternalServerErrorException);
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn)); const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
@ -64,6 +65,7 @@ export class TokenService {
async generateRefreshToken(userId: string): Promise<AuthToken> { async generateRefreshToken(userId: string): Promise<AuthToken> {
const secret = this.environmentService.getRefreshTokenSecret(); const secret = this.environmentService.getRefreshTokenSecret();
const expiresIn = this.environmentService.getRefreshTokenExpiresIn(); const expiresIn = this.environmentService.getRefreshTokenExpiresIn();
assert(expiresIn, '', InternalServerErrorException); assert(expiresIn, '', InternalServerErrorException);
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn)); const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
@ -94,6 +96,7 @@ export class TokenService {
async generateLoginToken(email: string): Promise<AuthToken> { async generateLoginToken(email: string): Promise<AuthToken> {
const secret = this.environmentService.getLoginTokenSecret(); const secret = this.environmentService.getLoginTokenSecret();
const expiresIn = this.environmentService.getLoginTokenExpiresIn(); const expiresIn = this.environmentService.getLoginTokenExpiresIn();
assert(expiresIn, '', InternalServerErrorException); assert(expiresIn, '', InternalServerErrorException);
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn)); const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
const jwtPayload = { const jwtPayload = {
@ -122,6 +125,7 @@ export class TokenService {
}; };
const secret = this.environmentService.getAccessTokenSecret(); const secret = this.environmentService.getAccessTokenSecret();
let expiresIn: string | number; let expiresIn: string | number;
if (expiresAt) { if (expiresAt) {
expiresIn = Math.floor( expiresIn = Math.floor(
(new Date(expiresAt).getTime() - new Date().getTime()) / 1000, (new Date(expiresAt).getTime() - new Date().getTime()) / 1000,
@ -134,6 +138,7 @@ export class TokenService {
expiresIn, expiresIn,
jwtid: apiKeyId, jwtid: apiKeyId,
}); });
return { token }; return { token };
} }

View File

@ -59,6 +59,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
picture: photos?.[0]?.value, picture: photos?.[0]?.value,
workspaceInviteHash: state.workspaceInviteHash, workspaceInviteHash: state.workspaceInviteHash,
}; };
done(null, user); done(null, user);
} }
} }

View File

@ -41,6 +41,7 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
const workspace = await this.workspaceRepository.findOneBy({ const workspace = await this.workspaceRepository.findOneBy({
id: payload.workspaceId ?? payload.sub, id: payload.workspaceId ?? payload.sub,
}); });
if (!workspace) { if (!workspace) {
throw new UnauthorizedException(); throw new UnauthorizedException();
} }
@ -66,6 +67,7 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
} }
let user; let user;
if (payload.workspaceId) { if (payload.workspaceId) {
user = await this.userRepository.findOneBy({ user = await this.userRepository.findOneBy({
id: payload.sub, id: payload.sub,

View File

@ -9,6 +9,7 @@ import { FileService } from 'src/core/file/services/file.service';
@Controller('files') @Controller('files')
export class FileController { export class FileController {
constructor(private readonly fileService: FileService) {} constructor(private readonly fileService: FileService) {}
/** /**
* Serve files from local storage * Serve files from local storage
* We recommend using an s3 bucket for production * We recommend using an s3 bucket for production

View File

@ -77,6 +77,7 @@ export class FileUploadService {
const name = `${id}${ext ? `.${ext}` : ''}`; const name = `${id}${ext ? `.${ext}` : ''}`;
const cropSizes = settings.storage.imageCropSizes[fileFolder]; const cropSizes = settings.storage.imageCropSizes[fileFolder];
if (!cropSizes) { if (!cropSizes) {
throw new Error(`No crop sizes found for ${fileFolder}`); throw new Error(`No crop sizes found for ${fileFolder}`);
} }

View File

@ -19,6 +19,7 @@ export class BeforeCreateOneRefreshToken<T extends RefreshToken>
// FIXME: These fields should be autogenerated, we need to run a migration for this // FIXME: These fields should be autogenerated, we need to run a migration for this
instance.input.id = uuidv4(); instance.input.id = uuidv4();
instance.input.updatedAt = new Date(); instance.input.updatedAt = new Date();
return instance; return instance;
} }
} }

View File

@ -72,6 +72,7 @@ export class UserService extends TypeOrmQueryService<User> {
const user = await this.userRepository.findOneBy({ const user = await this.userRepository.findOneBy({
id: userId, id: userId,
}); });
assert(user, 'User not found'); assert(user, 'User not found');
await this.userRepository.delete(user.id); await this.userRepository.delete(user.id);

View File

@ -30,6 +30,7 @@ const getHMACKey = (email?: string, key?: string | null) => {
if (!email || !key) return null; if (!email || !key) return null;
const hmac = crypto.createHmac('sha256', key); const hmac = crypto.createHmac('sha256', key);
return hmac.update(email).digest('hex'); return hmac.update(email).digest('hex');
}; };
@ -47,7 +48,9 @@ export class UserResolver {
const user = await this.userService.findById(id, { const user = await this.userService.findById(id, {
relations: [{ name: 'defaultWorkspace', query: {} }], relations: [{ name: 'defaultWorkspace', query: {} }],
}); });
assert(user, 'User not found'); assert(user, 'User not found');
return user; return user;
} }
@ -66,6 +69,7 @@ export class UserResolver {
return null; return null;
} }
const key = this.environmentService.getSupportFrontHMACKey(); const key = this.environmentService.getSupportFrontHMACKey();
return getHMACKey(parent.email, key); return getHMACKey(parent.email, key);
} }

View File

@ -19,6 +19,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
async deleteWorkspace(id: string) { async deleteWorkspace(id: string) {
const workspace = await this.workspaceRepository.findOneBy({ id }); const workspace = await this.workspaceRepository.findOneBy({ id });
assert(workspace, 'Workspace not found'); assert(workspace, 'Workspace not found');
await this.workspaceManagerService.delete(id); await this.workspaceManagerService.delete(id);

View File

@ -27,7 +27,9 @@ export class WorkspaceResolver {
@Query(() => Workspace) @Query(() => Workspace)
async currentWorkspace(@AuthWorkspace() { id }: Workspace) { async currentWorkspace(@AuthWorkspace() { id }: Workspace) {
const workspace = await this.workspaceService.findById(id); const workspace = await this.workspaceService.findById(id);
assert(workspace, 'User not found'); assert(workspace, 'User not found');
return workspace; return workspace;
} }

View File

@ -29,6 +29,7 @@ export class DataSeedDemoWorkspaceCommand extends CommandRunner {
logging: true, logging: true,
schema: 'public', schema: 'public',
}); });
await dataSource.initialize(); await dataSource.initialize();
const demoWorkspaceIds = this.environmentService.getDemoWorkspaceIds(); const demoWorkspaceIds = this.environmentService.getDemoWorkspaceIds();
@ -46,6 +47,7 @@ export class DataSeedDemoWorkspaceCommand extends CommandRunner {
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return; return;
} }
} }

View File

@ -43,12 +43,14 @@ export class DataSeedWorkspaceCommand extends CommandRunner {
logging: true, logging: true,
schema: 'public', schema: 'public',
}); });
await dataSource.initialize(); await dataSource.initialize();
await seedCoreSchema(dataSource, this.workspaceId); await seedCoreSchema(dataSource, this.workspaceId);
await seedMetadataSchema(dataSource); await seedMetadataSchema(dataSource);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return; return;
} }

View File

@ -18,6 +18,7 @@ export const seedCoreSchema = async (
workspaceId: string, workspaceId: string,
) => { ) => {
const schemaName = 'core'; const schemaName = 'core';
await seedWorkspaces(workspaceDataSource, schemaName, workspaceId); await seedWorkspaces(workspaceDataSource, schemaName, workspaceId);
await seedUsers(workspaceDataSource, schemaName, workspaceId); await seedUsers(workspaceDataSource, schemaName, workspaceId);
await seedFeatureFlags(workspaceDataSource, schemaName, workspaceId); await seedFeatureFlags(workspaceDataSource, schemaName, workspaceId);
@ -28,6 +29,7 @@ export const deleteCoreSchema = async (
workspaceId: string, workspaceId: string,
) => { ) => {
const schemaName = 'core'; const schemaName = 'core';
await deleteUsersByWorkspace(workspaceDataSource, schemaName, workspaceId); await deleteUsersByWorkspace(workspaceDataSource, schemaName, workspaceId);
await deleteFeatureFlags(workspaceDataSource, schemaName, workspaceId); await deleteFeatureFlags(workspaceDataSource, schemaName, workspaceId);
// deleteWorkspaces should be last // deleteWorkspaces should be last

View File

@ -18,6 +18,7 @@ export const seedCoreSchema = async (
workspaceId: string, workspaceId: string,
) => { ) => {
const schemaName = 'core'; const schemaName = 'core';
await seedWorkspaces(workspaceDataSource, schemaName, workspaceId); await seedWorkspaces(workspaceDataSource, schemaName, workspaceId);
await seedUsers(workspaceDataSource, schemaName, workspaceId); await seedUsers(workspaceDataSource, schemaName, workspaceId);
await seedFeatureFlags(workspaceDataSource, schemaName, workspaceId); await seedFeatureFlags(workspaceDataSource, schemaName, workspaceId);
@ -28,6 +29,7 @@ export const deleteCoreSchema = async (
workspaceId: string, workspaceId: string,
) => { ) => {
const schemaName = 'core'; const schemaName = 'core';
await deleteUsersByWorkspace(workspaceDataSource, schemaName, workspaceId); await deleteUsersByWorkspace(workspaceDataSource, schemaName, workspaceId);
await deleteFeatureFlags(workspaceDataSource, schemaName, workspaceId); await deleteFeatureFlags(workspaceDataSource, schemaName, workspaceId);
// deleteWorkspaces should be last // deleteWorkspaces should be last

View File

@ -27,6 +27,7 @@ import { seedWebhookFieldMetadata } from 'src/database/typeorm-seeds/metadata/fi
export const seedMetadataSchema = async (workspaceDataSource: DataSource) => { export const seedMetadataSchema = async (workspaceDataSource: DataSource) => {
const schemaName = 'metadata'; const schemaName = 'metadata';
await seedDataSource(workspaceDataSource, schemaName); await seedDataSource(workspaceDataSource, schemaName);
await seedObjectMetadata(workspaceDataSource, schemaName); await seedObjectMetadata(workspaceDataSource, schemaName);

View File

@ -5,6 +5,7 @@ import { DataSource, DataSourceOptions } from 'typeorm';
import { config } from 'dotenv'; import { config } from 'dotenv';
config(); config();
const configService = new ConfigService(); const configService = new ConfigService();
export const typeORMCoreModuleOptions: TypeOrmModuleOptions = { export const typeORMCoreModuleOptions: TypeOrmModuleOptions = {
url: configService.get<string>('PG_DATABASE_URL'), url: configService.get<string>('PG_DATABASE_URL'),
type: 'postgres', type: 'postgres',

View File

@ -5,6 +5,7 @@ import { DataSource, DataSourceOptions } from 'typeorm';
import { config } from 'dotenv'; import { config } from 'dotenv';
config(); config();
const configService = new ConfigService(); const configService = new ConfigService();
export const typeORMMetadataModuleOptions: TypeOrmModuleOptions = { export const typeORMMetadataModuleOptions: TypeOrmModuleOptions = {
url: configService.get<string>('PG_DATABASE_URL'), url: configService.get<string>('PG_DATABASE_URL'),
type: 'postgres', type: 'postgres',

View File

@ -1,18 +1,29 @@
import { MigrationInterface, QueryRunner } from "typeorm"; import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddEnumOptions1700663879152 implements MigrationInterface { export class AddEnumOptions1700663879152 implements MigrationInterface {
name = 'AddEnumOptions1700663879152' name = 'AddEnumOptions1700663879152';
public async up(queryRunner: QueryRunner): Promise<void> { public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "metadata"."fieldMetadata" RENAME COLUMN "enums" TO "options"`); await queryRunner.query(
await queryRunner.query(`ALTER TABLE "metadata"."fieldMetadata" DROP COLUMN "options"`); `ALTER TABLE "metadata"."fieldMetadata" RENAME COLUMN "enums" TO "options"`,
await queryRunner.query(`ALTER TABLE "metadata"."fieldMetadata" ADD "options" jsonb`); );
} await queryRunner.query(
`ALTER TABLE "metadata"."fieldMetadata" DROP COLUMN "options"`,
public async down(queryRunner: QueryRunner): Promise<void> { );
await queryRunner.query(`ALTER TABLE "metadata"."fieldMetadata" DROP COLUMN "options"`); await queryRunner.query(
await queryRunner.query(`ALTER TABLE "metadata"."fieldMetadata" ADD "options" text array`); `ALTER TABLE "metadata"."fieldMetadata" ADD "options" jsonb`,
await queryRunner.query(`ALTER TABLE "metadata"."fieldMetadata" RENAME COLUMN "options" TO "enums"`); );
} }
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "metadata"."fieldMetadata" DROP COLUMN "options"`,
);
await queryRunner.query(
`ALTER TABLE "metadata"."fieldMetadata" ADD "options" text array`,
);
await queryRunner.query(
`ALTER TABLE "metadata"."fieldMetadata" RENAME COLUMN "options" TO "enums"`,
);
}
} }

View File

@ -11,11 +11,13 @@ export class OptionalJwtAuthGuard extends AuthGuard(['jwt']) {
getRequest(context: ExecutionContext) { getRequest(context: ExecutionContext) {
const request = getRequest(context); const request = getRequest(context);
return request; return request;
} }
handleRequest(err, user, info) { handleRequest(err, user, info) {
if (err || info) return null; if (err || info) return null;
return user; return user;
} }
} }

View File

@ -10,26 +10,31 @@ class TestClass {
describe('CastToLogLevelArray Decorator', () => { describe('CastToLogLevelArray Decorator', () => {
it('should cast "log" to ["log"]', () => { it('should cast "log" to ["log"]', () => {
const transformedClass = plainToClass(TestClass, { logLevels: 'log' }); const transformedClass = plainToClass(TestClass, { logLevels: 'log' });
expect(transformedClass.logLevels).toStrictEqual(['log']); expect(transformedClass.logLevels).toStrictEqual(['log']);
}); });
it('should cast "error" to ["error"]', () => { it('should cast "error" to ["error"]', () => {
const transformedClass = plainToClass(TestClass, { logLevels: 'error' }); const transformedClass = plainToClass(TestClass, { logLevels: 'error' });
expect(transformedClass.logLevels).toStrictEqual(['error']); expect(transformedClass.logLevels).toStrictEqual(['error']);
}); });
it('should cast "warn" to ["warn"]', () => { it('should cast "warn" to ["warn"]', () => {
const transformedClass = plainToClass(TestClass, { logLevels: 'warn' }); const transformedClass = plainToClass(TestClass, { logLevels: 'warn' });
expect(transformedClass.logLevels).toStrictEqual(['warn']); expect(transformedClass.logLevels).toStrictEqual(['warn']);
}); });
it('should cast "debug" to ["debug"]', () => { it('should cast "debug" to ["debug"]', () => {
const transformedClass = plainToClass(TestClass, { logLevels: 'debug' }); const transformedClass = plainToClass(TestClass, { logLevels: 'debug' });
expect(transformedClass.logLevels).toStrictEqual(['debug']); expect(transformedClass.logLevels).toStrictEqual(['debug']);
}); });
it('should cast "verbose" to ["verbose"]', () => { it('should cast "verbose" to ["verbose"]', () => {
const transformedClass = plainToClass(TestClass, { logLevels: 'verbose' }); const transformedClass = plainToClass(TestClass, { logLevels: 'verbose' });
expect(transformedClass.logLevels).toStrictEqual(['verbose']); expect(transformedClass.logLevels).toStrictEqual(['verbose']);
}); });
@ -37,6 +42,7 @@ describe('CastToLogLevelArray Decorator', () => {
const transformedClass = plainToClass(TestClass, { const transformedClass = plainToClass(TestClass, {
logLevels: 'verbose,error,warn', logLevels: 'verbose,error,warn',
}); });
expect(transformedClass.logLevels).toStrictEqual([ expect(transformedClass.logLevels).toStrictEqual([
'verbose', 'verbose',
'error', 'error',
@ -46,6 +52,7 @@ describe('CastToLogLevelArray Decorator', () => {
it('should cast "toto" to undefined', () => { it('should cast "toto" to undefined', () => {
const transformedClass = plainToClass(TestClass, { logLevels: 'toto' }); const transformedClass = plainToClass(TestClass, { logLevels: 'toto' });
expect(transformedClass.logLevels).toBeUndefined(); expect(transformedClass.logLevels).toBeUndefined();
}); });
@ -53,6 +60,7 @@ describe('CastToLogLevelArray Decorator', () => {
const transformedClass = plainToClass(TestClass, { const transformedClass = plainToClass(TestClass, {
logLevels: 'verbose,error,toto', logLevels: 'verbose,error,toto',
}); });
expect(transformedClass.logLevels).toBeUndefined(); expect(transformedClass.logLevels).toBeUndefined();
}); });
}); });

View File

@ -10,21 +10,25 @@ class TestClass {
describe('CastToPositiveNumber Decorator', () => { describe('CastToPositiveNumber Decorator', () => {
it('should cast number to number', () => { it('should cast number to number', () => {
const transformedClass = plainToClass(TestClass, { numberProperty: 123 }); const transformedClass = plainToClass(TestClass, { numberProperty: 123 });
expect(transformedClass.numberProperty).toBe(123); expect(transformedClass.numberProperty).toBe(123);
}); });
it('should cast string to number', () => { it('should cast string to number', () => {
const transformedClass = plainToClass(TestClass, { numberProperty: '123' }); const transformedClass = plainToClass(TestClass, { numberProperty: '123' });
expect(transformedClass.numberProperty).toBe(123); expect(transformedClass.numberProperty).toBe(123);
}); });
it('should cast null to undefined', () => { it('should cast null to undefined', () => {
const transformedClass = plainToClass(TestClass, { numberProperty: null }); const transformedClass = plainToClass(TestClass, { numberProperty: null });
expect(transformedClass.numberProperty).toBe(undefined); expect(transformedClass.numberProperty).toBe(undefined);
}); });
it('should cast negative number to undefined', () => { it('should cast negative number to undefined', () => {
const transformedClass = plainToClass(TestClass, { numberProperty: -12 }); const transformedClass = plainToClass(TestClass, { numberProperty: -12 });
expect(transformedClass.numberProperty).toBe(undefined); expect(transformedClass.numberProperty).toBe(undefined);
}); });
@ -32,6 +36,7 @@ describe('CastToPositiveNumber Decorator', () => {
const transformedClass = plainToClass(TestClass, { const transformedClass = plainToClass(TestClass, {
numberProperty: undefined, numberProperty: undefined,
}); });
expect(transformedClass.numberProperty).toBe(undefined); expect(transformedClass.numberProperty).toBe(undefined);
}); });
@ -39,6 +44,7 @@ describe('CastToPositiveNumber Decorator', () => {
const transformedClass = plainToClass(TestClass, { const transformedClass = plainToClass(TestClass, {
numberProperty: 'toto', numberProperty: 'toto',
}); });
expect(transformedClass.numberProperty).toBe(undefined); expect(transformedClass.numberProperty).toBe(undefined);
}); });
@ -46,6 +52,7 @@ describe('CastToPositiveNumber Decorator', () => {
const transformedClass = plainToClass(TestClass, { const transformedClass = plainToClass(TestClass, {
numberProperty: '-123', numberProperty: '-123',
}); });
expect(transformedClass.numberProperty).toBe(undefined); expect(transformedClass.numberProperty).toBe(undefined);
}); });
}); });

View File

@ -13,5 +13,6 @@ const toBoolean = (value: any) => {
if (['false', 'off', 'no', '0'].includes(value.toLowerCase())) { if (['false', 'off', 'no', '0'].includes(value.toLowerCase())) {
return false; return false;
} }
return undefined; return undefined;
}; };

View File

@ -10,5 +10,6 @@ const toNumber = (value: any) => {
if (typeof value === 'string') { if (typeof value === 'string') {
return isNaN(+value) ? undefined : toNumber(+value); return isNaN(+value) ? undefined : toNumber(+value);
} }
return undefined; return undefined;
}; };

View File

@ -9,6 +9,7 @@ import {
export class IsAWSRegionConstraint implements ValidatorConstraintInterface { export class IsAWSRegionConstraint implements ValidatorConstraintInterface {
validate(region: string) { validate(region: string) {
const regex = /^[a-z]{2}-[a-z]+-\d{1}$/; const regex = /^[a-z]{2}-[a-z]+-\d{1}$/;
return regex.test(region); // Returns true if region matches regex return regex.test(region); // Returns true if region matches regex
} }
} }

View File

@ -10,6 +10,7 @@ export class IsDurationConstraint implements ValidatorConstraintInterface {
validate(duration: string) { validate(duration: string) {
const regex = const regex =
/^-?[0-9]+(.[0-9]+)?(m(illiseconds?)?|s(econds?)?|h((ou)?rs?)?|d(ays?)?|w(eeks?)?|M(onths?)?|y(ears?)?)?$/; /^-?[0-9]+(.[0-9]+)?(m(illiseconds?)?|s(econds?)?|h((ou)?rs?)?|d(ays?)?|w(eeks?)?|M(onths?)?|y(ears?)?)?$/;
return regex.test(duration); // Returns true if duration matches regex return regex.test(duration); // Returns true if duration matches regex
} }
} }

View File

@ -159,6 +159,7 @@ export const validate = (config: Record<string, unknown>) => {
const validatedConfig = plainToClass(EnvironmentVariables, config); const validatedConfig = plainToClass(EnvironmentVariables, config);
const errors = validateSync(validatedConfig); const errors = validateSync(validatedConfig);
assert(!errors.length, errors.toString()); assert(!errors.length, errors.toString());
return validatedConfig; return validatedConfig;

View File

@ -33,6 +33,7 @@ export class FileStorageModule {
provide: STORAGE_DRIVER, provide: STORAGE_DRIVER,
useFactory: async (...args: any[]) => { useFactory: async (...args: any[]) => {
const config = await options.useFactory(...args); const config = await options.useFactory(...args);
return config?.type === 's3' return config?.type === 's3'
? new S3Driver(config.options) ? new S3Driver(config.options)
: new LocalDriver(config.options); : new LocalDriver(config.options);

View File

@ -67,6 +67,7 @@ const loggerModuleFactory = async (
environmentService: EnvironmentService, environmentService: EnvironmentService,
): Promise<LoggerModuleOptions> => { ): Promise<LoggerModuleOptions> => {
const type = environmentService.getLoggerDriver(); const type = environmentService.getLoggerDriver();
switch (type) { switch (type) {
case LoggerDriver.Console: { case LoggerDriver.Console: {
return { return {
@ -100,6 +101,7 @@ const messageQueueModuleFactory = async (
switch (type) { switch (type) {
case MessageQueueType.PgBoss: { case MessageQueueType.PgBoss: {
const connectionString = environmentService.getPGDatabaseUrl(); const connectionString = environmentService.getPGDatabaseUrl();
return { return {
type: MessageQueueType.PgBoss, type: MessageQueueType.PgBoss,
options: { options: {
@ -110,6 +112,7 @@ const messageQueueModuleFactory = async (
case MessageQueueType.BullMQ: { case MessageQueueType.BullMQ: {
const host = environmentService.getRedisHost(); const host = environmentService.getRedisHost();
const port = environmentService.getRedisPort(); const port = environmentService.getRedisPort();
return { return {
type: MessageQueueType.BullMQ, type: MessageQueueType.BullMQ,
options: { options: {

View File

@ -32,6 +32,7 @@ export class LoggerModule {
provide: LOGGER_DRIVER, provide: LOGGER_DRIVER,
useFactory: async (...args: any[]) => { useFactory: async (...args: any[]) => {
const config = await options.useFactory(...args); const config = await options.useFactory(...args);
return config?.type === LoggerDriver.Console return config?.type === LoggerDriver.Console
? new ConsoleLogger() ? new ConsoleLogger()
: new SentryDriver(config.options); : new SentryDriver(config.options);

View File

@ -7,6 +7,7 @@ export class MemoryStorageDefaultSerializer<T>
if (typeof item !== 'string') { if (typeof item !== 'string') {
throw new Error('DefaultSerializer can only serialize strings'); throw new Error('DefaultSerializer can only serialize strings');
} }
return item; return item;
} }

View File

@ -27,6 +27,7 @@ export class BullMQDriver implements MessageQueueDriver {
async stop() { async stop() {
const workers = Object.values(this.workerMap); const workers = Object.values(this.workerMap);
const queues = Object.values(this.queueMap); const queues = Object.values(this.queueMap);
await Promise.all([ await Promise.all([
...queues.map((q) => q.close()), ...queues.map((q) => q.close()),
...workers.map((w) => w.close()), ...workers.map((w) => w.close()),
@ -40,6 +41,7 @@ export class BullMQDriver implements MessageQueueDriver {
const worker = new Worker(queueName, async (job) => { const worker = new Worker(queueName, async (job) => {
await handler(job as { data: T; id: string }); await handler(job as { data: T; id: string });
}); });
this.workerMap[queueName] = worker; this.workerMap[queueName] = worker;
} }

View File

@ -12,6 +12,7 @@ export class PgBossDriver implements MessageQueueDriver {
constructor(options: PgBossDriverOptions) { constructor(options: PgBossDriverOptions) {
this.pgBoss = new PgBoss(options); this.pgBoss = new PgBoss(options);
} }
async stop() { async stop() {
await this.pgBoss.stop(); await this.pgBoss.stop();
} }

View File

@ -30,11 +30,15 @@ export class MessageQueueModule {
provide: QUEUE_DRIVER, provide: QUEUE_DRIVER,
useFactory: async (...args: any[]) => { useFactory: async (...args: any[]) => {
const config = await options.useFactory(...args); const config = await options.useFactory(...args);
if (config.type === MessageQueueType.PgBoss) { if (config.type === MessageQueueType.PgBoss) {
const boss = new PgBossDriver(config.options); const boss = new PgBossDriver(config.options);
await boss.init(); await boss.init();
return boss; return boss;
} }
return new BullMQDriver(config.options); return new BullMQDriver(config.options);
}, },
inject: options.inject || [], inject: options.inject || [],

View File

@ -38,6 +38,7 @@ const bootstrap = async () => {
}), }),
); );
const loggerService = app.get(LoggerService); const loggerService = app.get(LoggerService);
app.useLogger(loggerService); app.useLogger(loggerService);
app.useLogger(app.get(EnvironmentService).getLogLevels()); app.useLogger(app.get(EnvironmentService).getLogLevels());

View File

@ -129,6 +129,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
if (position > acc) { if (position > acc) {
return position; return position;
} }
return acc; return acc;
}, -1); }, -1);

View File

@ -16,11 +16,13 @@ export class BeforeCreateOneField<T extends CreateFieldInput>
context: any, context: any,
): Promise<CreateOneInputType<T>> { ): Promise<CreateOneInputType<T>> {
const workspaceId = context?.req?.user?.workspace?.id; const workspaceId = context?.req?.user?.workspace?.id;
if (!workspaceId) { if (!workspaceId) {
throw new UnauthorizedException(); throw new UnauthorizedException();
} }
instance.input.workspaceId = workspaceId; instance.input.workspaceId = workspaceId;
return instance; return instance;
} }
} }

View File

@ -10,6 +10,7 @@ describe('generateTargetColumnMap', () => {
false, false,
'name', 'name',
); );
expect(textMap).toEqual({ value: 'name' }); expect(textMap).toEqual({ value: 'name' });
const linkMap = generateTargetColumnMap( const linkMap = generateTargetColumnMap(
@ -17,6 +18,7 @@ describe('generateTargetColumnMap', () => {
false, false,
'website', 'website',
); );
expect(linkMap).toEqual({ label: 'websiteLabel', url: 'websiteUrl' }); expect(linkMap).toEqual({ label: 'websiteLabel', url: 'websiteUrl' });
const currencyMap = generateTargetColumnMap( const currencyMap = generateTargetColumnMap(
@ -24,6 +26,7 @@ describe('generateTargetColumnMap', () => {
true, true,
'price', 'price',
); );
expect(currencyMap).toEqual({ expect(currencyMap).toEqual({
amountMicros: '_priceAmountMicros', amountMicros: '_priceAmountMicros',
currencyCode: '_priceCurrencyCode', currencyCode: '_priceCurrencyCode',

View File

@ -40,6 +40,7 @@ describe('serializeDefaultValue', () => {
it('should handle Date static default value', () => { it('should handle Date static default value', () => {
const date = new Date('2023-01-01'); const date = new Date('2023-01-01');
expect(serializeDefaultValue(date)).toBe(`'${date.toISOString()}'`); expect(serializeDefaultValue(date)).toBe(`'${date.toISOString()}'`);
}); });
}); });

View File

@ -36,6 +36,7 @@ export class BeforeCreateOneObject<T extends CreateObjectInput>
); );
} }
instance.input.workspaceId = workspaceId; instance.input.workspaceId = workspaceId;
return instance; return instance;
} }
} }

View File

@ -197,6 +197,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
if (fieldMetadata.type === FieldMetadataType.RELATION) { if (fieldMetadata.type === FieldMetadataType.RELATION) {
acc[fieldMetadata.objectMetadataId] = fieldMetadata; acc[fieldMetadata.objectMetadataId] = fieldMetadata;
} }
return acc; return acc;
}, },
{}, {},
@ -282,6 +283,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
("objectMetadataId", "type", "name") ("objectMetadataId", "type", "name")
VALUES ('${createdObjectMetadata.id}', 'table', 'All ${createdObjectMetadata.namePlural}') RETURNING *`, VALUES ('${createdObjectMetadata.id}', 'table', 'All ${createdObjectMetadata.namePlural}') RETURNING *`,
); );
createdObjectMetadata.fields.map(async (field, index) => { createdObjectMetadata.fields.map(async (field, index) => {
if (field.name === 'id') { if (field.name === 'id') {
return; return;

View File

@ -22,6 +22,7 @@ export class BeforeCreateOneRelation<T extends CreateRelationInput>
} }
instance.input.workspaceId = workspaceId; instance.input.workspaceId = workspaceId;
return instance; return instance;
} }
} }

View File

@ -97,6 +97,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
const objectMetadataMap = objectMetadataEntries.reduce((acc, curr) => { const objectMetadataMap = objectMetadataEntries.reduce((acc, curr) => {
acc[curr.id] = curr; acc[curr.id] = curr;
return acc; return acc;
}, {} as { [key: string]: ObjectMetadataEntity }); }, {} as { [key: string]: ObjectMetadataEntity });
@ -169,6 +170,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
if (fieldMetadata.type === FieldMetadataType.RELATION) { if (fieldMetadata.type === FieldMetadataType.RELATION) {
acc[fieldMetadata.objectMetadataId] = fieldMetadata; acc[fieldMetadata.objectMetadataId] = fieldMetadata;
} }
return acc; return acc;
}, {}); }, {});

View File

@ -94,11 +94,13 @@ export class WorkspaceMigrationFactory {
action: WorkspaceMigrationColumnActionType.CREATE, action: WorkspaceMigrationColumnActionType.CREATE,
fieldMetadata: FieldMetadataInterface, fieldMetadata: FieldMetadataInterface,
): WorkspaceMigrationColumnAction[]; ): WorkspaceMigrationColumnAction[];
createColumnActions( createColumnActions(
action: WorkspaceMigrationColumnActionType.ALTER, action: WorkspaceMigrationColumnActionType.ALTER,
previousFieldMetadata: FieldMetadataInterface, previousFieldMetadata: FieldMetadataInterface,
nextFieldMetadata: FieldMetadataInterface, nextFieldMetadata: FieldMetadataInterface,
): WorkspaceMigrationColumnAction[]; ): WorkspaceMigrationColumnAction[];
createColumnActions( createColumnActions(
action: action:
| WorkspaceMigrationColumnActionType.CREATE | WorkspaceMigrationColumnActionType.CREATE

View File

@ -31,6 +31,7 @@ export class WorkspaceMigrationService {
const insertedStandardMigrationsMapByName = const insertedStandardMigrationsMapByName =
insertedStandardMigrations.reduce((acc, migration) => { insertedStandardMigrations.reduce((acc, migration) => {
acc[migration.name] = migration; acc[migration.name] = migration;
return acc; return acc;
}, {}); }, {});

View File

@ -11,6 +11,7 @@ export class ScalarsExplorerService {
constructor() { constructor() {
this.scalarImplementations = scalars.reduce((acc, scalar) => { this.scalarImplementations = scalars.reduce((acc, scalar) => {
acc[scalar.name] = scalar; acc[scalar.name] = scalar;
return acc; return acc;
}, {}); }, {});
} }
@ -25,6 +26,7 @@ export class ScalarsExplorerService {
for (const typeName in typeMap) { for (const typeName in typeMap) {
const type = typeMap[typeName]; const type = typeMap[typeName];
if (isScalarType(type) && !typeName.startsWith('__')) { if (isScalarType(type) && !typeName.startsWith('__')) {
usedScalarNames.push(type.name); usedScalarNames.push(type.name);
} }
@ -40,6 +42,7 @@ export class ScalarsExplorerService {
for (const scalarName of usedScalarNames) { for (const scalarName of usedScalarNames) {
const scalarImplementation = this.getScalarImplementation(scalarName); const scalarImplementation = this.getScalarImplementation(scalarName);
if (scalarImplementation) { if (scalarImplementation) {
scalarResolvers[scalarName] = scalarImplementation; scalarResolvers[scalarName] = scalarImplementation;
} }

View File

@ -23,6 +23,7 @@ describe('getResolverName', () => {
it('should throw an error for an unknown resolver type', () => { it('should throw an error for an unknown resolver type', () => {
const unknownType = 'unknownType'; const unknownType = 'unknownType';
expect(() => expect(() =>
getResolverName( getResolverName(
metadata, metadata,

View File

@ -18,14 +18,17 @@ export const demoObjectsPrefillData = async (
id: object.id, id: object.id,
fields: object.fields.reduce((acc, field) => { fields: object.fields.reduce((acc, field) => {
acc[field.name] = field.id; acc[field.name] = field.id;
return acc; return acc;
}, {}), }, {}),
}; };
return acc; return acc;
}, {}); }, {});
// TODO: udnerstand why only with this createQueryRunner transaction below works // TODO: udnerstand why only with this createQueryRunner transaction below works
const queryRunner = workspaceDataSource.createQueryRunner(); const queryRunner = workspaceDataSource.createQueryRunner();
await queryRunner.connect(); await queryRunner.connect();
workspaceDataSource.transaction(async (entityManager: EntityManager) => { workspaceDataSource.transaction(async (entityManager: EntityManager) => {

View File

@ -5,6 +5,7 @@ const tableName = 'opportunity';
const getRandomProbability = () => { const getRandomProbability = () => {
const firstDigit = Math.floor(Math.random() * 9) + 1; const firstDigit = Math.floor(Math.random() * 9) + 1;
return firstDigit / 10; return firstDigit / 10;
}; };
@ -13,6 +14,7 @@ const getRandomPipelineStepId = (pipelineStepIds: { id: string }[]) =>
const generateRandomAmountMicros = () => { const generateRandomAmountMicros = () => {
const firstDigit = Math.floor(Math.random() * 9) + 1; const firstDigit = Math.floor(Math.random() * 9) + 1;
return firstDigit * 10000000000; return firstDigit * 10000000000;
}; };

View File

@ -35,6 +35,7 @@ export const viewPrefillData = async (
const viewIdMap = createdViews.raw.reduce((acc, view) => { const viewIdMap = createdViews.raw.reduce((acc, view) => {
acc[view.name] = view.id; acc[view.name] = view.id;
return acc; return acc;
}, {}); }, {});

View File

@ -16,9 +16,11 @@ export const standardObjectsPrefillData = async (
id: object.id, id: object.id,
fields: object.fields.reduce((acc, field) => { fields: object.fields.reduce((acc, field) => {
acc[field.name] = field.id; acc[field.name] = field.id;
return acc; return acc;
}, {}), }, {}),
}; };
return acc; return acc;
}, {}); }, {});

View File

@ -35,6 +35,7 @@ export const viewPrefillData = async (
const viewIdMap = createdViews.raw.reduce((acc, view) => { const viewIdMap = createdViews.raw.reduce((acc, view) => {
acc[view.name] = view.id; acc[view.name] = view.id;
return acc; return acc;
}, {}); }, {});

View File

@ -12,6 +12,7 @@ export class MetadataParser {
if (objectMetadata) { if (objectMetadata) {
const fields = Object.values(fieldMetadata); const fields = Object.values(fieldMetadata);
return { return {
...objectMetadata, ...objectMetadata,
workspaceId, workspaceId,

View File

@ -33,9 +33,11 @@ export const mapObjectMetadataByUniqueIdentifier = (
...curr, ...curr,
fields: curr.fields.reduce((acc, curr) => { fields: curr.fields.reduce((acc, curr) => {
acc[curr.name] = curr; acc[curr.name] = curr;
return acc; return acc;
}, {}), }, {}),
}; };
return acc; return acc;
}, {}); }, {});
}; };

View File

@ -182,6 +182,7 @@ export class WorkspaceManagerService {
const createdObjectMetadataByNameSingular = createdObjectMetadata.reduce( const createdObjectMetadataByNameSingular = createdObjectMetadata.reduce(
(acc, curr) => { (acc, curr) => {
acc[curr.nameSingular] = curr; acc[curr.nameSingular] = curr;
return acc; return acc;
}, },
{}, {},
@ -326,6 +327,7 @@ export class WorkspaceManagerService {
if (value === null || typeof value !== 'object') { if (value === null || typeof value !== 'object') {
return [key, value]; return [key, value];
} }
return [key, filterIgnoredProperties(value, fieldPropertiesToIgnore)]; return [key, filterIgnoredProperties(value, fieldPropertiesToIgnore)];
}), }),
); );
@ -346,6 +348,7 @@ export class WorkspaceManagerService {
// We only handle CHANGE here as REMOVE and CREATE are handled earlier. // We only handle CHANGE here as REMOVE and CREATE are handled earlier.
if (diff.type === 'CHANGE') { if (diff.type === 'CHANGE') {
const property = diff.path[0]; const property = diff.path[0];
objectsToUpdate[objectInDB.id] = { objectsToUpdate[objectInDB.id] = {
...objectsToUpdate[objectInDB.id], ...objectsToUpdate[objectInDB.id],
[property]: diff.value, [property]: diff.value,
@ -357,12 +360,14 @@ export class WorkspaceManagerService {
if (diff.type === 'CREATE') { if (diff.type === 'CREATE') {
const fieldName = diff.path[0]; const fieldName = diff.path[0];
const fieldMetadata = standardObjectFields[fieldName]; const fieldMetadata = standardObjectFields[fieldName];
fieldsToCreate.push(fieldMetadata); fieldsToCreate.push(fieldMetadata);
} }
if (diff.type === 'CHANGE') { if (diff.type === 'CHANGE') {
const fieldName = diff.path[0]; const fieldName = diff.path[0];
const property = diff.path[diff.path.length - 1]; const property = diff.path[diff.path.length - 1];
const fieldMetadata = objectInDBFields[fieldName]; const fieldMetadata = objectInDBFields[fieldName];
fieldsToUpdate[fieldMetadata.id] = { fieldsToUpdate[fieldMetadata.id] = {
...fieldsToUpdate[fieldMetadata.id], ...fieldsToUpdate[fieldMetadata.id],
[property]: diff.value, [property]: diff.value,
@ -371,6 +376,7 @@ export class WorkspaceManagerService {
if (diff.type === 'REMOVE') { if (diff.type === 'REMOVE') {
const fieldName = diff.path[0]; const fieldName = diff.path[0];
const fieldMetadata = objectInDBFields[fieldName]; const fieldMetadata = objectInDBFields[fieldName];
fieldsToDelete.push(fieldMetadata); fieldsToDelete.push(fieldMetadata);
} }
} }
@ -429,6 +435,7 @@ export class WorkspaceManagerService {
await this.workspaceDataSourceService.connectToWorkspaceDataSource( await this.workspaceDataSourceService.connectToWorkspaceDataSource(
workspaceId, workspaceId,
); );
await workspaceDataSource.query( await workspaceDataSource.query(
`comment on schema ${schemaName} is e'@graphql({"max_rows": 60})'`, `comment on schema ${schemaName} is e'@graphql({"max_rows": 60})'`,
); );
@ -460,6 +467,7 @@ export class WorkspaceManagerService {
createdObjectMetadata, createdObjectMetadata,
); );
} }
/** /**
* *
* We are prefilling a few demo objects with data to make it easier for the user to get started. * We are prefilling a few demo objects with data to make it easier for the user to get started.

View File

@ -9,6 +9,7 @@ import { ArgsAliasFactory } from './args-alias.factory';
@Injectable() @Injectable()
export class ArgsStringFactory { export class ArgsStringFactory {
constructor(private readonly argsAliasFactory: ArgsAliasFactory) {} constructor(private readonly argsAliasFactory: ArgsAliasFactory) {}
create( create(
initialArgs: Record<string, any> | undefined, initialArgs: Record<string, any> | undefined,
fieldMetadataCollection: FieldMetadataInterface[], fieldMetadataCollection: FieldMetadataInterface[],

View File

@ -4,6 +4,7 @@ describe('stringifyWithoutKeyQuote', () => {
test('should stringify object correctly without quotes around keys', () => { test('should stringify object correctly without quotes around keys', () => {
const obj = { name: 'John', age: 30, isAdmin: false }; const obj = { name: 'John', age: 30, isAdmin: false };
const result = stringifyWithoutKeyQuote(obj); const result = stringifyWithoutKeyQuote(obj);
expect(result).toBe('{name:"John",age:30,isAdmin:false}'); expect(result).toBe('{name:"John",age:30,isAdmin:false}');
}); });
@ -14,6 +15,7 @@ describe('stringifyWithoutKeyQuote', () => {
address: { city: 'New York', zipCode: 10001 }, address: { city: 'New York', zipCode: 10001 },
}; };
const result = stringifyWithoutKeyQuote(obj); const result = stringifyWithoutKeyQuote(obj);
expect(result).toBe( expect(result).toBe(
'{name:"John",age:30,address:{city:"New York",zipCode:10001}}', '{name:"John",age:30,address:{city:"New York",zipCode:10001}}',
); );
@ -26,6 +28,7 @@ describe('stringifyWithoutKeyQuote', () => {
hobbies: ['reading', 'movies', 'hiking'], hobbies: ['reading', 'movies', 'hiking'],
}; };
const result = stringifyWithoutKeyQuote(obj); const result = stringifyWithoutKeyQuote(obj);
expect(result).toBe( expect(result).toBe(
'{name:"John",age:30,hobbies:["reading","movies","hiking"]}', '{name:"John",age:30,hobbies:["reading","movies","hiking"]}',
); );
@ -34,6 +37,7 @@ describe('stringifyWithoutKeyQuote', () => {
test('should handle empty objects', () => { test('should handle empty objects', () => {
const obj = {}; const obj = {};
const result = stringifyWithoutKeyQuote(obj); const result = stringifyWithoutKeyQuote(obj);
expect(result).toBe('{}'); expect(result).toBe('{}');
}); });
@ -41,6 +45,7 @@ describe('stringifyWithoutKeyQuote', () => {
const num = 10; const num = 10;
const str = 'Hello'; const str = 'Hello';
const bool = false; const bool = false;
expect(stringifyWithoutKeyQuote(num)).toBe('10'); expect(stringifyWithoutKeyQuote(num)).toBe('10');
expect(stringifyWithoutKeyQuote(str)).toBe('"Hello"'); expect(stringifyWithoutKeyQuote(str)).toBe('"Hello"');
expect(stringifyWithoutKeyQuote(bool)).toBe('false'); expect(stringifyWithoutKeyQuote(bool)).toBe('false');

View File

@ -63,6 +63,7 @@ const parseValueNode = (
case Kind.OBJECT: case Kind.OBJECT:
return valueNode.fields.reduce((obj, field) => { return valueNode.fields.reduce((obj, field) => {
obj[field.name.value] = parseValueNode(field.value, variables); obj[field.name.value] = parseValueNode(field.value, variables);
return obj; return obj;
}, {}); }, {});
default: default:

View File

@ -11,6 +11,7 @@ import { ObjectTypeName, RootTypeFactory } from './root-type.factory';
@Injectable() @Injectable()
export class MutationTypeFactory { export class MutationTypeFactory {
constructor(private readonly rootTypeFactory: RootTypeFactory) {} constructor(private readonly rootTypeFactory: RootTypeFactory) {}
create( create(
objectMetadataCollection: ObjectMetadataInterface[], objectMetadataCollection: ObjectMetadataInterface[],
workspaceResolverMethodNames: WorkspaceResolverBuilderMutationMethodNames[], workspaceResolverMethodNames: WorkspaceResolverBuilderMutationMethodNames[],

View File

@ -11,6 +11,7 @@ import { ObjectTypeName, RootTypeFactory } from './root-type.factory';
@Injectable() @Injectable()
export class QueryTypeFactory { export class QueryTypeFactory {
constructor(private readonly rootTypeFactory: RootTypeFactory) {} constructor(private readonly rootTypeFactory: RootTypeFactory) {}
create( create(
objectMetadataCollection: ObjectMetadataInterface[], objectMetadataCollection: ObjectMetadataInterface[],
workspaceResolverMethodNames: WorkspaceResolverBuilderQueryMethodNames[], workspaceResolverMethodNames: WorkspaceResolverBuilderQueryMethodNames[],

View File

@ -15,6 +15,7 @@ export const BigFloatScalarType = new GraphQLScalarType({
if (ast.kind === Kind.FLOAT || ast.kind === Kind.INT) { if (ast.kind === Kind.FLOAT || ast.kind === Kind.INT) {
return String(ast.value); return String(ast.value);
} }
return null; return null;
}, },
}); });

View File

@ -7,18 +7,21 @@ export const CursorScalarType = new GraphQLScalarType({
if (typeof value !== 'string') { if (typeof value !== 'string') {
throw new Error('Cursor must be a string'); throw new Error('Cursor must be a string');
} }
return value; return value;
}, },
parseValue(value) { parseValue(value) {
if (typeof value !== 'string') { if (typeof value !== 'string') {
throw new Error('Cursor must be a string'); throw new Error('Cursor must be a string');
} }
return value; return value;
}, },
parseLiteral(ast) { parseLiteral(ast) {
if (ast.kind !== Kind.STRING) { if (ast.kind !== Kind.STRING) {
throw new Error('Cursor must be a string'); throw new Error('Cursor must be a string');
} }
return ast.value; return ast.value;
}, },
}); });

View File

@ -14,6 +14,7 @@ describe('getFieldMetadataType', () => {
it('should throw an error for an unknown type', () => { it('should throw an error for an unknown type', () => {
const unknownType = 'unknownType'; const unknownType = 'unknownType';
expect(() => getFieldMetadataType(unknownType)).toThrow( expect(() => getFieldMetadataType(unknownType)).toThrow(
`Unknown type ${unknownType}`, `Unknown type ${unknownType}`,
); );

View File

@ -48,6 +48,7 @@ describe('getResolverArgs', () => {
// Test for an unknown resolver type // Test for an unknown resolver type
it('should throw an error for an unknown resolver type', () => { it('should throw an error for an unknown resolver type', () => {
const unknownType = 'unknownType'; const unknownType = 'unknownType';
expect(() => expect(() =>
getResolverArgs(unknownType as WorkspaceResolverBuilderMethodNames), getResolverArgs(unknownType as WorkspaceResolverBuilderMethodNames),
).toThrow(`Unknown resolver type: ${unknownType}`); ).toThrow(`Unknown resolver type: ${unknownType}`);

View File

@ -73,6 +73,7 @@ export class WorkspaceFactory {
objectMetadataCollection, objectMetadataCollection,
workspaceResolverBuilderMethodNames, workspaceResolverBuilderMethodNames,
); );
usedScalarNames = usedScalarNames =
this.scalarsExplorerService.getUsedScalarNames(autoGeneratedSchema); this.scalarsExplorerService.getUsedScalarNames(autoGeneratedSchema);
typeDefs = printSchema(autoGeneratedSchema); typeDefs = printSchema(autoGeneratedSchema);

View File

@ -1270,7 +1270,7 @@
dependencies: dependencies:
tslib "^2.5.0" tslib "^2.5.0"
"@eslint-community/eslint-utils@^4.2.0": "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
version "4.4.0" version "4.4.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==
@ -2622,6 +2622,51 @@
resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.5.tgz#3abc203c79b8c3e90fd6c156a0c62d5403520e12" resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.5.tgz#3abc203c79b8c3e90fd6c156a0c62d5403520e12"
integrity sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw== integrity sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==
"@stylistic/eslint-plugin-js@1.5.0", "@stylistic/eslint-plugin-js@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.5.0.tgz#d502d2470f114a4a2b682930ade6ca4eeebaa9f8"
integrity sha512-TuGQv1bsIshkbJUInCewp4IUWy24W5RFiVNMV0quPSkuZ8gsYoqq6kLHvvaxpjxN9TvwSoOIwnhgrYKei2Tgcw==
dependencies:
acorn "^8.11.2"
escape-string-regexp "^4.0.0"
eslint-visitor-keys "^3.4.3"
espree "^9.6.1"
graphemer "^1.4.0"
"@stylistic/eslint-plugin-jsx@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin-jsx/-/eslint-plugin-jsx-1.5.0.tgz#f8fcb23dcc75b455650426887b24711c4831bee6"
integrity sha512-sqFdA1mS0jwovAatS8xFAiwxPbcy69S2AUjrGMxyhxaKbELPjvqbxPYJL+35ylT0xqirUlm118xZIFDooC8koQ==
dependencies:
"@stylistic/eslint-plugin-js" "^1.5.0"
estraverse "^5.3.0"
"@stylistic/eslint-plugin-plus@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin-plus/-/eslint-plugin-plus-1.5.0.tgz#49ad5560bb6b699c1b5d2940586a4048ebc35b1c"
integrity sha512-+A4qXFuM6V7x25Hj+xqfVIUbEckG+MUSvL6m83M6YtRq3d5zLW+giKKEL7eSCAw12MwnoDwPcEhqIJK6BRDR3w==
dependencies:
"@typescript-eslint/utils" "^6.13.2"
"@stylistic/eslint-plugin-ts@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin-ts/-/eslint-plugin-ts-1.5.0.tgz#ed84ffe3c2809748337079b6309f2906ff268e02"
integrity sha512-OusNGWRXnOV+ywnoXmBFoMtU6Ig/MX1bEu5Jigqmy2cIT8GRMMn7jUl/bXevkv2o66MYnC7PT1Q/3GvN7t0/eg==
dependencies:
"@stylistic/eslint-plugin-js" "1.5.0"
"@typescript-eslint/utils" "^6.13.2"
graphemer "^1.4.0"
"@stylistic/eslint-plugin@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin/-/eslint-plugin-1.5.0.tgz#d89c813c9f2121a33e1e6d3d3c521ff015964e61"
integrity sha512-XmlB5nxk06nlnx1/ka0l+WNqHcjnnXfDts4ZaCvrpCY/6l8lNtHwLwdCKF/UpBYNuRWI/HLWCTtQc0jjfwrfBA==
dependencies:
"@stylistic/eslint-plugin-js" "1.5.0"
"@stylistic/eslint-plugin-jsx" "1.5.0"
"@stylistic/eslint-plugin-plus" "1.5.0"
"@stylistic/eslint-plugin-ts" "1.5.0"
"@tokenizer/token@^0.3.0": "@tokenizer/token@^0.3.0":
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276" resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276"
@ -2868,7 +2913,7 @@
expect "^28.0.0" expect "^28.0.0"
pretty-format "^28.0.0" pretty-format "^28.0.0"
"@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": "@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
version "7.0.15" version "7.0.15"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
@ -3086,7 +3131,7 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
"@types/semver@^7.3.12": "@types/semver@^7.3.12", "@types/semver@^7.5.0":
version "7.5.6" version "7.5.6"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339"
integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A== integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==
@ -3184,6 +3229,14 @@
"@typescript-eslint/types" "5.62.0" "@typescript-eslint/types" "5.62.0"
"@typescript-eslint/visitor-keys" "5.62.0" "@typescript-eslint/visitor-keys" "5.62.0"
"@typescript-eslint/scope-manager@6.13.2":
version "6.13.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.13.2.tgz#5fa4e4adace028dafac212c770640b94e7b61052"
integrity sha512-CXQA0xo7z6x13FeDYCgBkjWzNqzBn8RXaE3QVQVIUm74fWJLkJkaHmHdKStrxQllGh6Q4eUGyNpMe0b1hMkXFA==
dependencies:
"@typescript-eslint/types" "6.13.2"
"@typescript-eslint/visitor-keys" "6.13.2"
"@typescript-eslint/type-utils@5.62.0": "@typescript-eslint/type-utils@5.62.0":
version "5.62.0" version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a"
@ -3199,6 +3252,11 @@
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f"
integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==
"@typescript-eslint/types@6.13.2":
version "6.13.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.13.2.tgz#c044aac24c2f6cefb8e921e397acad5417dd0ae6"
integrity sha512-7sxbQ+EMRubQc3wTfTsycgYpSujyVbI1xw+3UMRUcrhSy+pN09y/lWzeKDbvhoqcRbHdc+APLs/PWYi/cisLPg==
"@typescript-eslint/typescript-estree@5.62.0": "@typescript-eslint/typescript-estree@5.62.0":
version "5.62.0" version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b"
@ -3212,6 +3270,19 @@
semver "^7.3.7" semver "^7.3.7"
tsutils "^3.21.0" tsutils "^3.21.0"
"@typescript-eslint/typescript-estree@6.13.2":
version "6.13.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.2.tgz#ae556ee154c1acf025b48d37c3ef95a1d55da258"
integrity sha512-SuD8YLQv6WHnOEtKv8D6HZUzOub855cfPnPMKvdM/Bh1plv1f7Q/0iFUDLKKlxHcEstQnaUU4QZskgQq74t+3w==
dependencies:
"@typescript-eslint/types" "6.13.2"
"@typescript-eslint/visitor-keys" "6.13.2"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
semver "^7.5.4"
ts-api-utils "^1.0.1"
"@typescript-eslint/utils@5.62.0": "@typescript-eslint/utils@5.62.0":
version "5.62.0" version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86"
@ -3226,6 +3297,19 @@
eslint-scope "^5.1.1" eslint-scope "^5.1.1"
semver "^7.3.7" semver "^7.3.7"
"@typescript-eslint/utils@^6.13.2":
version "6.13.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.13.2.tgz#8eb89e53adc6d703a879b131e528807245486f89"
integrity sha512-b9Ptq4eAZUym4idijCRzl61oPCwwREcfDI8xGk751Vhzig5fFZR9CyzDz4Sp/nxSLBYxUPyh4QdIDqWykFhNmQ==
dependencies:
"@eslint-community/eslint-utils" "^4.4.0"
"@types/json-schema" "^7.0.12"
"@types/semver" "^7.5.0"
"@typescript-eslint/scope-manager" "6.13.2"
"@typescript-eslint/types" "6.13.2"
"@typescript-eslint/typescript-estree" "6.13.2"
semver "^7.5.4"
"@typescript-eslint/visitor-keys@5.62.0": "@typescript-eslint/visitor-keys@5.62.0":
version "5.62.0" version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e"
@ -3234,6 +3318,14 @@
"@typescript-eslint/types" "5.62.0" "@typescript-eslint/types" "5.62.0"
eslint-visitor-keys "^3.3.0" eslint-visitor-keys "^3.3.0"
"@typescript-eslint/visitor-keys@6.13.2":
version "6.13.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.2.tgz#e0a4a80cf842bb08e6127b903284166ac4a5594c"
integrity sha512-OGznFs0eAQXJsp+xSd6k/O1UbFi/K/L7WjqeRoFE7vadjAF9y0uppXhYNQNEqygjou782maGClOoZwPqF0Drlw==
dependencies:
"@typescript-eslint/types" "6.13.2"
eslint-visitor-keys "^3.4.1"
"@ungap/structured-clone@^1.2.0": "@ungap/structured-clone@^1.2.0":
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
@ -3435,7 +3527,7 @@ acorn-walk@^8.1.1:
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.0.tgz#2097665af50fd0cf7a2dfccd2b9368964e66540f" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.0.tgz#2097665af50fd0cf7a2dfccd2b9368964e66540f"
integrity sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA== integrity sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==
acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0: acorn@^8.11.2, acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0:
version "8.11.2" version "8.11.2"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b"
integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==
@ -4981,7 +5073,7 @@ estraverse@^4.1.1:
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
estraverse@^5.1.0, estraverse@^5.2.0: estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0:
version "5.3.0" version "5.3.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
@ -8717,6 +8809,11 @@ tree-kill@1.2.2:
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
ts-api-utils@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331"
integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==
ts-essentials@^7.0.3: ts-essentials@^7.0.3:
version "7.0.3" version "7.0.3"
resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-7.0.3.tgz#686fd155a02133eedcc5362dc8b5056cde3e5a38" resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-7.0.3.tgz#686fd155a02133eedcc5362dc8b5056cde3e5a38"