Compare commits

..
Author SHA1 Message Date
Philipinho 64e47b5679 Merge branch 'main' into fix/package-updates 2026-08-19 19:56:49 +01:00
Philipinho 1ad94c03dc chore: package updates 2026-08-19 19:52:22 +01:00
14 changed files with 26 additions and 93 deletions
+2 -13
View File
@@ -22,8 +22,7 @@ import { TelemetryModule } from './integrations/telemetry/telemetry.module';
import { RedisModule } from '@nestjs-labs/nestjs-ioredis'; import { RedisModule } from '@nestjs-labs/nestjs-ioredis';
import { RedisConfigService } from './integrations/redis/redis-config.service'; import { RedisConfigService } from './integrations/redis/redis-config.service';
import { CacheModule } from '@nestjs/cache-manager'; import { CacheModule } from '@nestjs/cache-manager';
import KeyvRedis, { defaultReconnectStrategy } from '@keyv/redis'; import KeyvRedis from '@keyv/redis';
import { parseRedisUrl } from './common/helpers';
import { LoggerModule } from './common/logger/logger.module'; import { LoggerModule } from './common/logger/logger.module';
import { ClsModule } from 'nestjs-cls'; import { ClsModule } from 'nestjs-cls';
import { NoopAuditModule } from './integrations/audit/audit.module'; import { NoopAuditModule } from './integrations/audit/audit.module';
@@ -63,20 +62,10 @@ try {
isGlobal: true, isGlobal: true,
useFactory: async (environmentService: EnvironmentService) => { useFactory: async (environmentService: EnvironmentService) => {
const redisUrl = environmentService.getRedisUrl(); const redisUrl = environmentService.getRedisUrl();
const { family, tls } = parseRedisUrl(redisUrl);
return { return {
ttl: 5 * 1000, ttl: 5 * 1000,
stores: [ stores: [new KeyvRedis(redisUrl)],
new KeyvRedis({
url: redisUrl,
socket: {
family,
reconnectStrategy: defaultReconnectStrategy,
...tls,
},
}),
],
}; };
}, },
inject: [EnvironmentService], inject: [EnvironmentService],
@@ -66,7 +66,6 @@ export class CollaborationGateway {
password: this.redisConfig.password, password: this.redisConfig.password,
db: this.redisConfig.db, db: this.redisConfig.db,
family: this.redisConfig.family, family: this.redisConfig.family,
tls: this.redisConfig.tls,
retryStrategy: createRetryStrategy(), retryStrategy: createRetryStrategy(),
}), }),
serverId: `collab-${os?.hostname()}-${nanoid(10)}`, serverId: `collab-${os?.hostname()}-${nanoid(10)}`,
+4 -12
View File
@@ -30,14 +30,13 @@ export type RedisConfig = {
db: number; db: number;
password?: string; password?: string;
family?: number; family?: number;
tls?: { rejectUnauthorized?: boolean };
}; };
export function parseRedisUrl(redisUrl: string): RedisConfig { export function parseRedisUrl(redisUrl: string): RedisConfig {
// format - redis[s]://[[username][:password]@][host][:port][/db-number][?family=4|6][&rejectUnauthorized=false] // format - redis[s]://[[username][:password]@][host][:port][/db-number][?family=4|6]
const url = new URL(redisUrl); const url = new URL(redisUrl);
const { hostname, port, password, pathname, protocol, searchParams } = url; const { hostname, port, password, pathname, searchParams } = url;
const portInt = port ? parseInt(port, 10) : 6379; const portInt = parseInt(port, 10);
let db: number = 0; let db: number = 0;
// extract db value if present // extract db value if present
@@ -55,14 +54,7 @@ export function parseRedisUrl(redisUrl: string): RedisConfig {
family = parseInt(familyParam, 10); family = parseInt(familyParam, 10);
} }
const tls = return { host: hostname, port: portInt, password, db, family };
protocol === 'rediss:'
? searchParams.get('rejectUnauthorized') === 'false'
? { rejectUnauthorized: false }
: {}
: undefined;
return { host: hostname, port: portInt, password: password || undefined, db, family, tls };
} }
export function createRetryStrategy() { export function createRetryStrategy() {
@@ -339,25 +339,15 @@ export class SpaceMemberService {
return; return;
} }
await executeTx(this.db, async (trx) => { if (spaceMember.role === SpaceRole.ADMIN) {
await trx await this.validateLastAdmin(dto.spaceId);
.selectFrom('spaces') }
.select('id')
.where('id', '=', dto.spaceId)
.forUpdate()
.executeTakeFirst();
if (spaceMember.role === SpaceRole.ADMIN) { await this.spaceMemberRepo.updateSpaceMember(
await this.validateLastAdmin(dto.spaceId, trx); { role: dto.role },
} spaceMember.id,
dto.spaceId,
await this.spaceMemberRepo.updateSpaceMember( );
{ role: dto.role },
spaceMember.id,
dto.spaceId,
trx,
);
});
this.auditService.log({ this.auditService.log({
event: AuditEvent.SPACE_MEMBER_ROLE_CHANGED, event: AuditEvent.SPACE_MEMBER_ROLE_CHANGED,
@@ -378,14 +368,10 @@ export class SpaceMemberService {
}); });
} }
async validateLastAdmin( async validateLastAdmin(spaceId: string): Promise<void> {
spaceId: string,
trx?: KyselyTransaction,
): Promise<void> {
const spaceOwnerCount = await this.spaceMemberRepo.roleCountBySpaceId( const spaceOwnerCount = await this.spaceMemberRepo.roleCountBySpaceId(
SpaceRole.ADMIN, SpaceRole.ADMIN,
spaceId, spaceId,
trx,
); );
if (spaceOwnerCount === 1) { if (spaceOwnerCount === 1) {
throw new BadRequestException( throw new BadRequestException(
@@ -396,10 +396,7 @@ export class WorkspaceService {
} }
} }
if ( if (updateWorkspaceDto.aiSearch) {
updateWorkspaceDto.aiSearch &&
this.environmentService.getAiVectorDriver() !== 'turbopuffer'
) {
const tableExists = await isPageEmbeddingsTableExists(this.db); const tableExists = await isPageEmbeddingsTableExists(this.db);
if (!tableExists) { if (!tableExists) {
throw new BadRequestException( throw new BadRequestException(
@@ -46,10 +46,8 @@ export class SpaceMemberRepo {
updatableSpaceMember: UpdatableSpaceMember, updatableSpaceMember: UpdatableSpaceMember,
spaceMemberId: string, spaceMemberId: string,
spaceId: string, spaceId: string,
trx?: KyselyTransaction,
): Promise<void> { ): Promise<void> {
const db = dbOrTx(this.db, trx); await this.db
await db
.updateTable('spaceMembers') .updateTable('spaceMembers')
.set(updatableSpaceMember) .set(updatableSpaceMember)
.where('id', '=', spaceMemberId) .where('id', '=', spaceMemberId)
@@ -94,13 +92,8 @@ export class SpaceMemberRepo {
.execute(); .execute();
} }
async roleCountBySpaceId( async roleCountBySpaceId(role: string, spaceId: string): Promise<number> {
role: string, const { count } = await this.db
spaceId: string,
trx?: KyselyTransaction,
): Promise<number> {
const db = dbOrTx(this.db, trx);
const { count } = await db
.selectFrom('spaceMembers') .selectFrom('spaceMembers')
.select((eb) => eb.fn.count('role').as('count')) .select((eb) => eb.fn.count('role').as('count'))
.where('role', '=', role) .where('role', '=', role)
@@ -5,7 +5,6 @@ import {
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { EnvironmentService } from '../environment/environment.service'; import { EnvironmentService } from '../environment/environment.service';
import { Redis } from 'ioredis'; import { Redis } from 'ioredis';
import { parseRedisUrl } from '../../common/helpers';
@Injectable() @Injectable()
export class RedisHealthIndicator { export class RedisHealthIndicator {
@@ -20,10 +19,8 @@ export class RedisHealthIndicator {
const indicator = this.healthIndicatorService.check(key); const indicator = this.healthIndicatorService.check(key);
try { try {
const redisUrl = this.environmentService.getRedisUrl(); const redis = new Redis(this.environmentService.getRedisUrl(), {
const redis = new Redis(redisUrl, {
maxRetriesPerRequest: 15, maxRetriesPerRequest: 15,
tls: parseRedisUrl(redisUrl).tls,
}); });
await redis.ping(); await redis.ping();
@@ -97,15 +97,6 @@ export function xwikiFormatter($: CheerioAPI, $root: Cheerio<any>) {
} }
} }
function isBareLink($el: Cheerio<any>): boolean {
const href = $el.attr("href")?.trim();
const text = $el.text().trim();
if(!text || !href) return false
return text === href;
}
export function defaultHtmlFormatter($: CheerioAPI, $root: Cheerio<any>) { export function defaultHtmlFormatter($: CheerioAPI, $root: Cheerio<any>) {
normalizeTableColumnWidths($, $root); normalizeTableColumnWidths($, $root);
@@ -113,9 +104,7 @@ export function defaultHtmlFormatter($: CheerioAPI, $root: Cheerio<any>) {
const $el = $(el); const $el = $(el);
const url = $el.attr('href')!; const url = $el.attr('href')!;
const { provider } = getEmbedUrlAndProvider(url); const { provider } = getEmbedUrlAndProvider(url);
if (provider === 'iframe' || !isBareLink($el)) { if (provider === 'iframe') return;
return;
}
const embed = `<div data-type=\"embed\" data-src=\"${url}\" data-provider=\"${provider}\" data-align=\"center\" data-width=\"640\" data-height=\"480\"></div>`; const embed = `<div data-type=\"embed\" data-src=\"${url}\" data-provider=\"${provider}\" data-align=\"center\" data-width=\"640\" data-height=\"480\"></div>`;
$el.replaceWith(embed); $el.replaceWith(embed);
@@ -18,7 +18,6 @@ import { GeneralQueueProcessor } from './processors/general-queue.processor';
password: redisConfig.password, password: redisConfig.password,
db: redisConfig.db, db: redisConfig.db,
family: redisConfig.family, family: redisConfig.family,
tls: redisConfig.tls,
retryStrategy: createRetryStrategy(), retryStrategy: createRetryStrategy(),
}, },
defaultJobOptions: { defaultJobOptions: {
@@ -19,7 +19,6 @@ export class RedisConfigService implements RedisOptionsFactory {
password: redisConfig.password, password: redisConfig.password,
db: redisConfig.db, db: redisConfig.db,
family: redisConfig.family, family: redisConfig.family,
tls: redisConfig.tls,
retryStrategy: createRetryStrategy(), retryStrategy: createRetryStrategy(),
}, },
}; };
@@ -3,7 +3,7 @@ import { ThrottlerModule } from '@nestjs/throttler';
import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis'; import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis';
import { EnvironmentService } from '../environment/environment.service'; import { EnvironmentService } from '../environment/environment.service';
import { EnvironmentModule } from '../environment/environment.module'; import { EnvironmentModule } from '../environment/environment.module';
import { createRetryStrategy, parseRedisUrl } from '../../common/helpers'; import { parseRedisUrl } from '../../common/helpers';
import { AUTH_THROTTLER, AI_CHAT_THROTTLER } from './throttler-names'; import { AUTH_THROTTLER, AI_CHAT_THROTTLER } from './throttler-names';
import Redis from 'ioredis'; import Redis from 'ioredis';
@@ -27,8 +27,6 @@ import Redis from 'ioredis';
password: redisConfig.password, password: redisConfig.password,
db: redisConfig.db, db: redisConfig.db,
family: redisConfig.family, family: redisConfig.family,
tls: redisConfig.tls,
retryStrategy: createRetryStrategy(),
keyPrefix: 'throttle:', keyPrefix: 'throttle:',
}), }),
), ),
@@ -17,7 +17,6 @@ export class WsRedisIoAdapter extends IoAdapter {
const options: RedisOptions = { const options: RedisOptions = {
family: this.redisConfig.family, family: this.redisConfig.family,
tls: this.redisConfig.tls,
retryStrategy: createRetryStrategy(), retryStrategy: createRetryStrategy(),
}; };
@@ -73,13 +73,9 @@ export const embedProviders: IEmbedProvider[] = [
id: "vimeo", id: "vimeo",
name: "Vimeo", name: "Vimeo",
regex: regex:
/^(https:)?\/\/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|album\/(\d+)\/video\/|video\/|)(\d+)(?:\/([\da-zA-Z]+))?/, /^(https:)?\/\/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|album\/(\d+)\/video\/|video\/|)(\d+)/,
getEmbedUrl: (match, url: string) => { getEmbedUrl: (match) => {
// preserve ?h= hash for unlisted videos return `https://player.vimeo.com/video/${match[4]}`;
const hash =
match[5] ?? new URL(url, "https://vimeo.com").searchParams.get("h");
const base = `https://player.vimeo.com/video/${match[4]}`;
return hash ? `${base}?h=${hash}` : base;
}, },
}, },
{ {