Skip to content

Commit 8201203

Browse files
github-actions[bot]caoxing9boris-wJocky-Teablehammond-lj
committed
[sync] fix(t3417): enhance BaseNodePermissionGuard with resource ID resolution & t3434
Synced from teableio/teable-ee@bfb9bec Co-authored-by: Aries X <caoxing9@gmail.com> Co-authored-by: Boris <boris2code@outlook.com> Co-authored-by: Jocky-Teable <jocky@teable.ai> Co-authored-by: Jun Lu <hammond@teable.io> Co-authored-by: Uno <uno@teable.ai> Co-authored-by: nichenqin <nichenqin@hotmail.com>
1 parent ceba9a7 commit 8201203

124 files changed

Lines changed: 5497 additions & 654 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/nestjs-backend/src/cache/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export interface ICacheStore {
3636
[key: `oauth:token-rate:${string}:${string}`]: number;
3737
[key: `automation:email:rate:${string}:${number}`]: number;
3838
[key: `automation:email-att:${string}`]: string[];
39+
[key: `automation:fail-notify-count:${string}`]: number;
3940
// Distributed lock keys
4041
[key: `lock:${string}`]: string;
4142
[key: `import:result:manifest:${string}`]: {
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
/* eslint-disable @typescript-eslint/naming-convention */
2+
import {
3+
CellValueType,
4+
DateFieldCore,
5+
DateFormattingPreset,
6+
DriverClient,
7+
FieldType,
8+
NumberFieldCore,
9+
SingleLineTextFieldCore,
10+
TimeFormatting,
11+
} from '@teable/core';
12+
import type { FieldCore, IFilter } from '@teable/core';
13+
import knex from 'knex';
14+
import type { IRecordQueryFilterContext } from '../../../features/record/query-builder/record-query-builder.interface';
15+
import type { IDbProvider } from '../../db.provider.interface';
16+
import type { AbstractCellValueFilter } from '../cell-value-filter.abstract';
17+
import { AbstractFilterQuery } from '../filter-query.abstract';
18+
import { FilterQueryPostgres } from '../postgres/filter-query.postgres';
19+
20+
const knexBuilder = knex({ client: 'pg' });
21+
const dbProviderStub = { driver: DriverClient.Pg } as unknown as IDbProvider;
22+
const mainTableAlias = 'main_table as main';
23+
24+
function assignBaseField<T extends FieldCore>(
25+
field: T,
26+
params: {
27+
id: string;
28+
name?: string;
29+
dbFieldName: string;
30+
type: FieldType;
31+
cellValueType: CellValueType;
32+
options: T['options'];
33+
}
34+
): T {
35+
field.id = params.id;
36+
field.name = params.name ?? params.id;
37+
field.dbFieldName = params.dbFieldName;
38+
field.type = params.type;
39+
field.options = params.options;
40+
field.cellValueType = params.cellValueType;
41+
field.isMultipleCellValue = false;
42+
field.isLookup = false;
43+
field.updateDbFieldType();
44+
return field;
45+
}
46+
47+
function createNumberField(id: string, dbFieldName: string): NumberFieldCore {
48+
return assignBaseField(new NumberFieldCore(), {
49+
id,
50+
dbFieldName,
51+
type: FieldType.Number,
52+
cellValueType: CellValueType.Number,
53+
options: NumberFieldCore.defaultOptions(),
54+
});
55+
}
56+
57+
function createTextField(id: string, dbFieldName: string, name?: string): SingleLineTextFieldCore {
58+
return assignBaseField(new SingleLineTextFieldCore(), {
59+
id,
60+
name,
61+
dbFieldName,
62+
type: FieldType.SingleLineText,
63+
cellValueType: CellValueType.String,
64+
options: SingleLineTextFieldCore.defaultOptions(),
65+
});
66+
}
67+
68+
function createDateField(id: string, dbFieldName: string): DateFieldCore {
69+
const options = DateFieldCore.defaultOptions();
70+
options.formatting = {
71+
date: DateFormattingPreset.ISO,
72+
time: TimeFormatting.None,
73+
timeZone: 'UTC',
74+
};
75+
return assignBaseField(new DateFieldCore(), {
76+
id,
77+
dbFieldName,
78+
type: FieldType.Date,
79+
cellValueType: CellValueType.DateTime,
80+
options,
81+
});
82+
}
83+
84+
class ThrowingFilterQuery extends AbstractFilterQuery {
85+
private createThrowingFilter(): AbstractCellValueFilter {
86+
return {
87+
compiler: () => {
88+
throw new Error('unexpected adapter failure');
89+
},
90+
} as unknown as AbstractCellValueFilter;
91+
}
92+
93+
booleanFilter(_field: FieldCore, _context?: IRecordQueryFilterContext): AbstractCellValueFilter {
94+
return this.createThrowingFilter();
95+
}
96+
97+
numberFilter(_field: FieldCore, _context?: IRecordQueryFilterContext): AbstractCellValueFilter {
98+
return this.createThrowingFilter();
99+
}
100+
101+
dateTimeFilter(_field: FieldCore, _context?: IRecordQueryFilterContext): AbstractCellValueFilter {
102+
return this.createThrowingFilter();
103+
}
104+
105+
stringFilter(_field: FieldCore, _context?: IRecordQueryFilterContext): AbstractCellValueFilter {
106+
return this.createThrowingFilter();
107+
}
108+
109+
jsonFilter(_field: FieldCore, _context?: IRecordQueryFilterContext): AbstractCellValueFilter {
110+
return this.createThrowingFilter();
111+
}
112+
}
113+
114+
describe('filter-query invalid filter skip', () => {
115+
it('skips filter item with invalid operator instead of throwing', () => {
116+
const numberField = createNumberField('fld_num', 'num_col');
117+
const filter = {
118+
conjunction: 'and',
119+
filterSet: [
120+
{
121+
fieldId: numberField.id,
122+
operator: 'contains',
123+
value: 'whatever',
124+
},
125+
],
126+
} as unknown as IFilter;
127+
128+
const qb = knexBuilder(mainTableAlias);
129+
const filterQuery = new FilterQueryPostgres(
130+
qb,
131+
{ [numberField.id]: numberField },
132+
filter,
133+
undefined,
134+
dbProviderStub
135+
);
136+
137+
expect(() => filterQuery.appendQueryBuilder()).not.toThrow();
138+
expect(qb.toQuery()).not.toContain('num_col');
139+
});
140+
141+
it('preserves valid filter items alongside skipped invalid ones', () => {
142+
const numberField = createNumberField('fld_num', 'num_col');
143+
const textField = createTextField('fld_text', 'text_col');
144+
const filter = {
145+
conjunction: 'and',
146+
filterSet: [
147+
{
148+
fieldId: numberField.id,
149+
operator: 'contains',
150+
value: 'whatever',
151+
},
152+
{
153+
fieldId: textField.id,
154+
operator: 'contains',
155+
value: 'hello',
156+
},
157+
],
158+
} as unknown as IFilter;
159+
160+
const qb = knexBuilder(mainTableAlias);
161+
const filterQuery = new FilterQueryPostgres(
162+
qb,
163+
{ [numberField.id]: numberField, [textField.id]: textField },
164+
filter,
165+
undefined,
166+
dbProviderStub
167+
);
168+
169+
expect(() => filterQuery.appendQueryBuilder()).not.toThrow();
170+
const sql = qb.toQuery();
171+
expect(sql).toContain('text_col');
172+
expect(sql).not.toContain('num_col');
173+
});
174+
175+
it('keeps filter items keyed by field name when fields map supports name keys', () => {
176+
const textField = createTextField('fld_text_name', 'text_name_col', 'Display Name');
177+
const filter = {
178+
conjunction: 'and',
179+
filterSet: [
180+
{
181+
fieldId: textField.name,
182+
operator: 'contains',
183+
value: 'hello',
184+
},
185+
],
186+
} as unknown as IFilter;
187+
188+
const qb = knexBuilder(mainTableAlias);
189+
const filterQuery = new FilterQueryPostgres(
190+
qb,
191+
{ [textField.id]: textField, [textField.name]: textField },
192+
filter,
193+
undefined,
194+
dbProviderStub
195+
);
196+
197+
expect(() => filterQuery.appendQueryBuilder()).not.toThrow();
198+
expect(qb.toQuery()).toContain('text_name_col');
199+
});
200+
201+
it('skips filter item with invalid sub-operator mode instead of throwing', () => {
202+
const dateField = createDateField('fld_date', 'date_col');
203+
const filter = {
204+
conjunction: 'and',
205+
filterSet: [
206+
{
207+
fieldId: dateField.id,
208+
operator: 'isWithIn',
209+
value: { mode: 'invalidMode', exactDate: null, timeZone: 'UTC' },
210+
},
211+
],
212+
} as unknown as IFilter;
213+
214+
const qb = knexBuilder(mainTableAlias);
215+
const filterQuery = new FilterQueryPostgres(
216+
qb,
217+
{ [dateField.id]: dateField },
218+
filter,
219+
undefined,
220+
dbProviderStub
221+
);
222+
223+
expect(() => filterQuery.appendQueryBuilder()).not.toThrow();
224+
expect(qb.toQuery()).not.toContain('date_col');
225+
});
226+
227+
it('skips filter item whose value shape fails inside the adapter compiler', () => {
228+
const dateField = createDateField('fld_date_shape', 'date_shape_col');
229+
// value is a string, but isWithIn requires an object { mode, ... }
230+
const filter = {
231+
conjunction: 'and',
232+
filterSet: [
233+
{
234+
fieldId: dateField.id,
235+
operator: 'isWithIn',
236+
value: 'today',
237+
},
238+
],
239+
} as unknown as IFilter;
240+
241+
const qb = knexBuilder(mainTableAlias).select('id');
242+
const filterQuery = new FilterQueryPostgres(
243+
qb,
244+
{ [dateField.id]: dateField },
245+
filter,
246+
undefined,
247+
dbProviderStub
248+
);
249+
250+
expect(() => filterQuery.appendQueryBuilder()).not.toThrow();
251+
expect(() => qb.toQuery()).not.toThrow();
252+
});
253+
254+
it('rethrows non-user compiler errors instead of swallowing them', () => {
255+
const numberField = createNumberField('fld_num_system', 'num_system_col');
256+
const filter = {
257+
conjunction: 'and',
258+
filterSet: [
259+
{
260+
fieldId: numberField.id,
261+
operator: 'is',
262+
value: 1,
263+
},
264+
],
265+
} as unknown as IFilter;
266+
267+
const qb = knexBuilder(mainTableAlias).select('id');
268+
const filterQuery = new ThrowingFilterQuery(
269+
qb,
270+
{ [numberField.id]: numberField },
271+
filter,
272+
undefined,
273+
dbProviderStub
274+
);
275+
276+
expect(() => filterQuery.appendQueryBuilder()).not.toThrow();
277+
expect(() => qb.toQuery()).toThrow();
278+
});
279+
280+
it('rethrows field-reference context errors instead of skipping them', () => {
281+
const textField = createTextField('fld_text_ref_context', 'text_ref_context_col');
282+
const refField = createTextField('fld_ref_context', 'ref_context_col');
283+
const filter = {
284+
conjunction: 'and',
285+
filterSet: [
286+
{
287+
fieldId: textField.id,
288+
operator: 'is',
289+
value: { type: 'field', fieldId: refField.id },
290+
},
291+
],
292+
} as unknown as IFilter;
293+
294+
const qb = knexBuilder(mainTableAlias).select('id');
295+
const filterQuery = new FilterQueryPostgres(
296+
qb,
297+
{ [textField.id]: textField, [refField.id]: refField },
298+
filter,
299+
undefined,
300+
dbProviderStub
301+
);
302+
303+
expect(() => filterQuery.appendQueryBuilder()).not.toThrow();
304+
expect(() => qb.toQuery()).toThrow('not available for reference comparisons');
305+
});
306+
});

0 commit comments

Comments
 (0)