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,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin', 'import', 'unused-imports'],
plugins: ['@typescript-eslint/eslint-plugin', 'import', 'unused-imports', '@stylistic'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
@ -77,5 +77,16 @@ module.exports = {
'import/no-duplicates': ["error", {"considerQueryString": true}],
'unused-imports/no-unused-imports': 'warn',
"@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,
"trailingComma": "all"
}
"trailingComma": "all",
"brakeBeforeElse": false
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -77,6 +77,7 @@ export class FileUploadService {
const name = `${id}${ext ? `.${ext}` : ''}`;
const cropSizes = settings.storage.imageCropSizes[fileFolder];
if (!cropSizes) {
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
instance.input.id = uuidv4();
instance.input.updatedAt = new Date();
return instance;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,6 +18,7 @@ export const seedCoreSchema = async (
workspaceId: string,
) => {
const schemaName = 'core';
await seedWorkspaces(workspaceDataSource, schemaName, workspaceId);
await seedUsers(workspaceDataSource, schemaName, workspaceId);
await seedFeatureFlags(workspaceDataSource, schemaName, workspaceId);
@ -28,6 +29,7 @@ export const deleteCoreSchema = async (
workspaceId: string,
) => {
const schemaName = 'core';
await deleteUsersByWorkspace(workspaceDataSource, schemaName, workspaceId);
await deleteFeatureFlags(workspaceDataSource, schemaName, workspaceId);
// 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) => {
const schemaName = 'metadata';
await seedDataSource(workspaceDataSource, schemaName);
await seedObjectMetadata(workspaceDataSource, schemaName);

View File

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

View File

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

View File

@ -1,18 +1,29 @@
import { MigrationInterface, QueryRunner } from "typeorm";
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddEnumOptions1700663879152 implements MigrationInterface {
name = 'AddEnumOptions1700663879152'
name = 'AddEnumOptions1700663879152';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "metadata"."fieldMetadata" RENAME COLUMN "enums" TO "options"`);
await queryRunner.query(`ALTER TABLE "metadata"."fieldMetadata" DROP COLUMN "options"`);
await queryRunner.query(`ALTER TABLE "metadata"."fieldMetadata" ADD "options" jsonb`);
}
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"`);
}
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "metadata"."fieldMetadata" RENAME COLUMN "enums" TO "options"`,
);
await queryRunner.query(
`ALTER TABLE "metadata"."fieldMetadata" DROP COLUMN "options"`,
);
await queryRunner.query(
`ALTER TABLE "metadata"."fieldMetadata" ADD "options" jsonb`,
);
}
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) {
const request = getRequest(context);
return request;
}
handleRequest(err, user, info) {
if (err || info) return null;
return user;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ export class IsDurationConstraint implements ValidatorConstraintInterface {
validate(duration: string) {
const regex =
/^-?[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
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -182,6 +182,7 @@ export class WorkspaceManagerService {
const createdObjectMetadataByNameSingular = createdObjectMetadata.reduce(
(acc, curr) => {
acc[curr.nameSingular] = curr;
return acc;
},
{},
@ -326,6 +327,7 @@ export class WorkspaceManagerService {
if (value === null || typeof value !== 'object') {
return [key, value];
}
return [key, filterIgnoredProperties(value, fieldPropertiesToIgnore)];
}),
);
@ -346,6 +348,7 @@ export class WorkspaceManagerService {
// We only handle CHANGE here as REMOVE and CREATE are handled earlier.
if (diff.type === 'CHANGE') {
const property = diff.path[0];
objectsToUpdate[objectInDB.id] = {
...objectsToUpdate[objectInDB.id],
[property]: diff.value,
@ -357,12 +360,14 @@ export class WorkspaceManagerService {
if (diff.type === 'CREATE') {
const fieldName = diff.path[0];
const fieldMetadata = standardObjectFields[fieldName];
fieldsToCreate.push(fieldMetadata);
}
if (diff.type === 'CHANGE') {
const fieldName = diff.path[0];
const property = diff.path[diff.path.length - 1];
const fieldMetadata = objectInDBFields[fieldName];
fieldsToUpdate[fieldMetadata.id] = {
...fieldsToUpdate[fieldMetadata.id],
[property]: diff.value,
@ -371,6 +376,7 @@ export class WorkspaceManagerService {
if (diff.type === 'REMOVE') {
const fieldName = diff.path[0];
const fieldMetadata = objectInDBFields[fieldName];
fieldsToDelete.push(fieldMetadata);
}
}
@ -429,6 +435,7 @@ export class WorkspaceManagerService {
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
workspaceId,
);
await workspaceDataSource.query(
`comment on schema ${schemaName} is e'@graphql({"max_rows": 60})'`,
);
@ -460,6 +467,7 @@ export class WorkspaceManagerService {
createdObjectMetadata,
);
}
/**
*
* 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()
export class ArgsStringFactory {
constructor(private readonly argsAliasFactory: ArgsAliasFactory) {}
create(
initialArgs: Record<string, any> | undefined,
fieldMetadataCollection: FieldMetadataInterface[],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1270,7 +1270,7 @@
dependencies:
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"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==
@ -2622,6 +2622,51 @@
resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.5.tgz#3abc203c79b8c3e90fd6c156a0c62d5403520e12"
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":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276"
@ -2868,7 +2913,7 @@
expect "^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"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
@ -3086,7 +3131,7 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
"@types/semver@^7.3.12":
"@types/semver@^7.3.12", "@types/semver@^7.5.0":
version "7.5.6"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339"
integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==
@ -3184,6 +3229,14 @@
"@typescript-eslint/types" "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":
version "5.62.0"
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"
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":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b"
@ -3212,6 +3270,19 @@
semver "^7.3.7"
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":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86"
@ -3226,6 +3297,19 @@
eslint-scope "^5.1.1"
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":
version "5.62.0"
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"
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":
version "1.2.0"
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"
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"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b"
integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==
@ -4981,7 +5073,7 @@ estraverse@^4.1.1:
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
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"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
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"
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:
version "7.0.3"
resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-7.0.3.tgz#686fd155a02133eedcc5362dc8b5056cde3e5a38"