Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions apps/nestjs-backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { PluginPanelModule } from './features/plugin-panel/plugin-panel.module';
import { SelectionModule } from './features/selection/selection.module';
import { AdminOpenApiModule } from './features/setting/open-api/admin-open-api.module';
import { SettingOpenApiModule } from './features/setting/open-api/setting-open-api.module';
import { BaseShareModule } from './features/base-share/base-share.module';
import { ShareModule } from './features/share/share.module';
import { SpaceModule } from './features/space/space.module';
import { TemplateOpenApiModule } from './features/template/template-open-api.module';
Expand Down Expand Up @@ -83,6 +84,7 @@ export const appModules = {
CollaboratorModule,
InvitationModule,
ShareModule,
BaseShareModule,
NotificationModule,
AccessTokenModule,
ImportOpenApiModule,
Expand Down
15 changes: 15 additions & 0 deletions apps/nestjs-backend/src/cache/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export enum OperationName {
UpdateRecordsOrder = 'updateRecordsOrder',
CreateFields = 'createFields',
ConvertField = 'convertField',
ConvertFieldV2 = 'convertFieldV2',
DeleteFields = 'deleteFields',
PasteSelection = 'pasteSelection',
}
Expand Down Expand Up @@ -177,6 +178,19 @@ export interface IConvertFieldOperation extends IUndoRedoOperationBase {
};
}

export interface IConvertFieldV2Operation extends IUndoRedoOperationBase {
name: OperationName.ConvertFieldV2;
params: {
tableId: string;
};
result: {
oldField: IFieldVo;
newField: IFieldVo;
modifiedOps?: IOpsMap;
references?: string[];
};
}

export interface ICreateFieldsOperation extends IUndoRedoOperationBase {
name: OperationName.CreateFields;
params: {
Expand Down Expand Up @@ -253,6 +267,7 @@ export type IUndoRedoOperation =
| ICreateFieldsOperation
| IDeleteFieldsOperation
| IConvertFieldOperation
| IConvertFieldV2Operation
| IPasteSelectionOperation
| ICreateViewOperation
| IDeleteViewOperation
Expand Down
1 change: 1 addition & 0 deletions apps/nestjs-backend/src/configs/threshold.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const thresholdConfig = registerAs('threshold', () => ({
initialBackoff: Number(process.env.BACKEND_DB_DEADLOCK_INITIAL_BACKOFF ?? 100),
jitter: Number(process.env.BACKEND_DB_DEADLOCK_JITTER ?? 1.0),
},
baseNodeMaxFolderDepth: Number(process.env.BASE_NODE_MAX_FOLDER_DEPTH ?? 2),
changeEmailSendCodeMailRate: Number(process.env.BACKEND_CHANGE_EMAIL_SEND_CODE_MAIL_RATE ?? 30),
resetPasswordSendMailRate: Number(process.env.BACKEND_RESET_PASSWORD_SEND_MAIL_RATE ?? 30),
signupVerificationSendCodeMailRate: Number(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export abstract class GeneratedColumnQueryAbstract implements IGeneratedColumnQu
abstract toNow(date: string, unit?: string): string;
abstract weekNum(date: string): string;
abstract weekday(date: string, startDayOfWeek?: string): string;
abstract workday(startDate: string, days: string): string;
abstract workday(startDate: string, days: string, holidayStr?: string): string;
abstract workdayDiff(startDate: string, endDate: string): string;
abstract year(date: string): string;
abstract createdTime(): string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1099,11 +1099,11 @@ export class GeneratedColumnQueryPostgres extends GeneratedColumnQueryAbstract {
const trustedDatetimeInput = this.hasTrustedDatetimeInput(0);

if (format == null) {
return trustedDatetimeInput ? valueExpr : this.guardDefaultDatetimeParse(valueExpr);
return trustedDatetimeInput ? valueExpr : this.parseDatetimeParseWithoutFormat(valueExpr);
}
const trimmedFormat = format.trim();
if (!trimmedFormat || trimmedFormat === 'undefined' || trimmedFormat.toLowerCase() === 'null') {
return trustedDatetimeInput ? valueExpr : this.guardDefaultDatetimeParse(valueExpr);
return trustedDatetimeInput ? valueExpr : this.parseDatetimeParseWithoutFormat(valueExpr);
}
if (trustedDatetimeInput) {
return valueExpr;
Expand Down Expand Up @@ -1223,7 +1223,7 @@ export class GeneratedColumnQueryPostgres extends GeneratedColumnQueryAbstract {
return `EXTRACT(DOW FROM ${this.castToTimestamp(date, 0)})`;
}

workday(startDate: string, days: string): string {
workday(startDate: string, days: string, _holidayStr?: string): string {
if (!this.isDateLikeOperand(0)) {
return 'NULL';
}
Expand Down Expand Up @@ -1509,6 +1509,29 @@ export class GeneratedColumnQueryPostgres extends GeneratedColumnQueryAbstract {
return `(CASE WHEN ${valueExpr} IS NULL THEN NULL WHEN ${sanitizedExpr} IS NULL THEN NULL WHEN ${sanitizedExpr} ~ '${pattern}' THEN ${valueExpr} ELSE NULL END)`;
}

private parseDatetimeParseWithoutFormat(valueExpr: string): string {
const textExpr = `${valueExpr}::text`;
const trimmedExpr = `NULLIF(BTRIM(${textExpr}), '')`;
const sanitizedExpr = `CASE WHEN ${trimmedExpr} IS NULL THEN NULL WHEN LOWER(${trimmedExpr}) IN ('null', 'undefined') THEN NULL ELSE ${trimmedExpr} END`;
const pattern = getDefaultDatetimeParsePattern();
const hasClockTime = `(${sanitizedExpr} ~ '[ T][0-9]{1,2}:[0-9]{2}')`;
const hasExplicitTimeZone = `(${sanitizedExpr} ~* '(Z|[+-][0-9]{2}:[0-9]{2}|[+-][0-9]{4}|[+-][0-9]{2})$')`;
const safeTz = (this.context?.timeZone ?? 'UTC').replace(/'/g, "''");
const localTimestampExpr = `(${sanitizedExpr})::timestamp AT TIME ZONE '${safeTz}'`;
const explicitZoneExpr = `(${sanitizedExpr})::timestamptz`;

return `(CASE
WHEN ${valueExpr} IS NULL THEN NULL
WHEN ${sanitizedExpr} IS NULL THEN NULL
WHEN ${sanitizedExpr} ~ '${pattern}' THEN
(CASE
WHEN ${hasClockTime} AND NOT ${hasExplicitTimeZone} THEN ${localTimestampExpr}
ELSE ${explicitZoneExpr}
END)
ELSE NULL
END)`;
}

private buildDatetimeParseGuardRegex(formatLiteral: string): string | null {
if (!formatLiteral.startsWith("'") || !formatLiteral.endsWith("'")) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ export class GeneratedColumnQuerySqlite extends GeneratedColumnQueryAbstract {
return `(CAST(STRFTIME('%w', ${date}) AS INTEGER) + 1)`;
}

workday(startDate: string, days: string): string {
workday(startDate: string, days: string, _holidayStr?: string): string {
return `DATE(${startDate}, '+' || ${days} || ' days')`;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1334,11 +1334,11 @@ export class SelectQueryPostgres extends SelectQueryAbstract {
const trustedDatetimeInput = this.hasTrustedDatetimeInput(0);

if (format == null) {
return trustedDatetimeInput ? valueExpr : this.guardDefaultDatetimeParse(valueExpr);
return trustedDatetimeInput ? valueExpr : this.parseDatetimeParseWithoutFormat(valueExpr);
}
const trimmedFormat = format.trim();
if (!trimmedFormat || trimmedFormat === 'undefined' || trimmedFormat.toLowerCase() === 'null') {
return trustedDatetimeInput ? valueExpr : this.guardDefaultDatetimeParse(valueExpr);
return trustedDatetimeInput ? valueExpr : this.parseDatetimeParseWithoutFormat(valueExpr);
}
if (trustedDatetimeInput) {
return valueExpr;
Expand Down Expand Up @@ -1462,13 +1462,56 @@ export class SelectQueryPostgres extends SelectQueryAbstract {
return `CASE WHEN ${normalizedStartDay} = 'monday' THEN ((${weekdaySql} + 6) % 7) ELSE ${weekdaySql} END`;
}

workday(startDate: string, days: string): string {
workday(startDate: string, days: string, holidayStr?: string): string {
if (!this.isDateLikeOperand(0)) {
return 'NULL';
}
// Simplified implementation in the target timezone; tzWrap sanitizes untrusted inputs
// Use interval multiplication so dynamic expressions (e.g. field references) are valid SQL.
return `(${this.tzWrap(startDate, 0)})::date + INTERVAL '1 day' * (${days})::double precision`;
const startDateSql = `(${this.tzWrap(startDate, 0)})::date`;
const dayCountSql = `COALESCE((${this.toNumericSafe(days, 1)})::integer, 0)`;
const holidayTextSql = holidayStr ? `COALESCE((${holidayStr})::text, '')` : `''`;

return `(
WITH params AS (
SELECT ${startDateSql} AS start_date, ${dayCountSql} AS day_count, ${holidayTextSql} AS holiday_text
),
holiday_parts AS (
SELECT BTRIM(part) AS holiday_part
FROM params p
CROSS JOIN LATERAL regexp_split_to_table(p.holiday_text, ',') AS part
),
holiday_dates AS (
SELECT DISTINCT TO_DATE(LEFT(holiday_part, 10), 'YYYY-MM-DD') AS holiday_date
FROM holiday_parts
WHERE holiday_part <> ''
AND holiday_part ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}'
AND TO_CHAR(TO_DATE(LEFT(holiday_part, 10), 'YYYY-MM-DD'), 'YYYY-MM-DD') = LEFT(holiday_part, 10)
),
candidates AS (
SELECT
(p.start_date + CASE WHEN p.day_count >= 0 THEN seq.n ELSE -seq.n END)::date AS candidate_date,
seq.n
FROM params p
CROSS JOIN LATERAL generate_series(1, ABS(p.day_count) * 7 + 366) AS seq(n)
),
workdays AS (
SELECT c.candidate_date, c.n
FROM candidates c
LEFT JOIN holiday_dates h ON h.holiday_date = c.candidate_date
WHERE EXTRACT(DOW FROM c.candidate_date)::int NOT IN (0, 6)
AND h.holiday_date IS NULL
ORDER BY c.n
)
SELECT CASE
WHEN p.day_count = 0 THEN p.start_date::timestamp
ELSE (
SELECT w.candidate_date::timestamp
FROM workdays w
OFFSET ABS(p.day_count) - 1
LIMIT 1
)
END
FROM params p
)`;
}

workdayDiff(startDate: string, endDate: string): string {
Expand Down Expand Up @@ -1955,6 +1998,29 @@ export class SelectQueryPostgres extends SelectQueryAbstract {
return `(CASE WHEN ${valueExpr} IS NULL THEN NULL WHEN ${sanitizedExpr} IS NULL THEN NULL WHEN ${sanitizedExpr} ~ '${pattern}' THEN ${valueExpr} ELSE NULL END)`;
}

private parseDatetimeParseWithoutFormat(valueExpr: string): string {
const textExpr = `${valueExpr}::text`;
const trimmedExpr = `NULLIF(BTRIM(${textExpr}), '')`;
const sanitizedExpr = `CASE WHEN ${trimmedExpr} IS NULL THEN NULL WHEN LOWER(${trimmedExpr}) IN ('null', 'undefined') THEN NULL ELSE ${trimmedExpr} END`;
const pattern = getDefaultDatetimeParsePattern();
const hasClockTime = `(${sanitizedExpr} ~ '[ T][0-9]{1,2}:[0-9]{2}')`;
const hasExplicitTimeZone = `(${sanitizedExpr} ~* '(Z|[+-][0-9]{2}:[0-9]{2}|[+-][0-9]{4}|[+-][0-9]{2})$')`;
const safeTz = (this.context?.timeZone ?? 'UTC').replace(/'/g, "''");
const localTimestampExpr = `(${sanitizedExpr})::timestamp AT TIME ZONE '${safeTz}'`;
const explicitZoneExpr = `(${sanitizedExpr})::timestamptz`;

return `(CASE
WHEN ${valueExpr} IS NULL THEN NULL
WHEN ${sanitizedExpr} IS NULL THEN NULL
WHEN ${sanitizedExpr} ~ '${pattern}' THEN
(CASE
WHEN ${hasClockTime} AND NOT ${hasExplicitTimeZone} THEN ${localTimestampExpr}
ELSE ${explicitZoneExpr}
END)
ELSE NULL
END)`;
}

private buildDatetimeParseGuardRegex(formatLiteral: string): string | null {
if (!formatLiteral.startsWith("'") || !formatLiteral.endsWith("'")) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export abstract class SelectQueryAbstract implements ISelectQueryInterface {
abstract toNow(date: string, unit?: string): string;
abstract weekNum(date: string): string;
abstract weekday(date: string, startDayOfWeek?: string): string;
abstract workday(startDate: string, days: string): string;
abstract workday(startDate: string, days: string, holidayStr?: string): string;
abstract workdayDiff(startDate: string, endDate: string): string;
abstract year(date: string): string;
abstract createdTime(): string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -549,9 +549,66 @@ export class SelectQuerySqlite extends SelectQueryAbstract {
return `CASE WHEN ${normalizedStartDay} = 'monday' THEN ${mondayWeekdaySql} ELSE ${weekdaySql} END`;
}

workday(startDate: string, days: string): string {
// Simplified implementation
return `DATE(${startDate}, '+' || ${days} || ' days')`;
workday(startDate: string, days: string, holidayStr?: string): string {
const dayCountSql = `CAST(${this.coalesceNumeric(days)} AS INTEGER)`;
const holidayTextSql = holidayStr ? `COALESCE(CAST(${holidayStr} AS TEXT), '')` : `''`;

return `(
WITH RECURSIVE
params AS (
SELECT DATE(${startDate}) AS start_date, ${dayCountSql} AS day_count, ${holidayTextSql} AS holiday_text
),
split(rest, part) AS (
SELECT (SELECT holiday_text FROM params), ''
UNION ALL
SELECT
CASE WHEN INSTR(rest, ',') = 0 THEN '' ELSE SUBSTR(rest, INSTR(rest, ',') + 1) END,
TRIM(CASE WHEN INSTR(rest, ',') = 0 THEN rest ELSE SUBSTR(rest, 1, INSTR(rest, ',') - 1) END)
FROM split
WHERE rest <> ''
),
holiday_dates AS (
SELECT DISTINCT DATE(SUBSTR(part, 1, 10)) AS holiday_date
FROM split
WHERE part <> ''
AND part GLOB '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]*'
AND DATE(SUBSTR(part, 1, 10)) = SUBSTR(part, 1, 10)
),
seq(n) AS (
SELECT 1
UNION ALL
SELECT n + 1
FROM seq
WHERE n < (SELECT ABS(day_count) * 7 + 366 FROM params)
),
candidates AS (
SELECT
DATE(
p.start_date,
PRINTF('%+d day', CASE WHEN p.day_count >= 0 THEN seq.n ELSE -seq.n END)
) AS candidate_date,
seq.n
FROM params p
CROSS JOIN seq
),
workdays AS (
SELECT c.candidate_date, c.n
FROM candidates c
LEFT JOIN holiday_dates h ON h.holiday_date = c.candidate_date
WHERE CAST(STRFTIME('%w', c.candidate_date) AS INTEGER) NOT IN (0, 6)
AND h.holiday_date IS NULL
ORDER BY c.n
)
SELECT CASE
WHEN p.day_count = 0 THEN p.start_date
ELSE (
SELECT w.candidate_date
FROM workdays w
LIMIT 1 OFFSET ABS(p.day_count) - 1
)
END
FROM params p
)`;
}

workdayDiff(startDate: string, endDate: string): string {
Expand Down
Loading
Loading