chore: sync

This commit is contained in:
Pranav C
2026-01-27 12:17:41 +05:30
parent 3cdbcf0044
commit 321f5d5de7
19 changed files with 628 additions and 289 deletions

View File

@@ -20,9 +20,7 @@ const emit = defineEmits(['update:modelValue', 'back'])
const { $api } = useNuxtApp()
const baseURL = $api.instance.defaults.baseURL
const { $state, $poller } = useNuxtApp()
const { $poller } = useNuxtApp()
const workspace = useWorkspace()

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { ColumnType, GalleryType, KanbanType, LookupType } from 'nocodb-sdk'
import { ProjectRoles, UITypes, ViewTypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import { UITypes, ViewTypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import Draggable from 'vuedraggable'
import type { SelectProps } from 'ant-design-vue'
@@ -11,7 +11,7 @@ const meta = inject(MetaInj, ref())
const reloadViewDataHook = inject(ReloadViewDataHookInj, undefined)!
const { isMobileMode, user } = useGlobal()
const { isMobileMode } = useGlobal()
const { isUIAllowed } = useRoles()

View File

@@ -10,7 +10,7 @@ const currentVersion = ref<any>(null)
// Load managed app info and current version
const loadManagedApp = async () => {
if (!(base.value as any)?.managed_app_id || !base.value?.fk_workspace_id) return
if (!base.value?.managed_app_id || !base.value?.managed_app_master || !base.value?.fk_workspace_id) return
try {
const response = await $api.internal.getOperation(base.value.fk_workspace_id, base.value.id!, {
@@ -27,7 +27,7 @@ const loadManagedApp = async () => {
// Load current version info
const loadCurrentVersion = async () => {
if (!base.value?.managed_app_version_id || !base.value?.fk_workspace_id) return
if (!base.value?.managed_app_version_id || !base.value?.managed_app_master || !base.value?.fk_workspace_id) return
try {
// Get version details from versions list

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@@ -1,6 +1,7 @@
import { arrFlatMap } from 'nocodb-sdk';
import type { DBQueryClient } from '~/dbQueryClient/types';
import type { XKnex } from '~/db/CustomKnex';
import type { Knex, XKnex } from '~/db/CustomKnex';
import type { PagedResponseImpl } from '~/helpers/PagedResponse';
export abstract class GenericDBQueryClient implements DBQueryClient {
temporaryTableRaw({
@@ -45,4 +46,32 @@ export abstract class GenericDBQueryClient implements DBQueryClient {
abstract concat(fields: string[]): string;
abstract simpleCast(field: string, asType: string): string;
generateNestedRowSelectQuery(_param: any): Knex.Raw<any> {
throw new Error('Not implemented');
}
async singleQueryList(
_context: any,
_ctx: any,
): Promise<
PagedResponseImpl<Record<string, any>> | Array<Record<string, any>>
> {
throw new Error('Not implemented');
}
async singleQueryRead(
_context: any,
_ctx: any,
): Promise<PagedResponseImpl<Record<string, any>>> {
throw new Error('Not implemented');
}
async extractColumns(_param: any): Promise<void> {
throw new Error('Not implemented');
}
async extractColumn(_param: any): Promise<{
isArray?: boolean;
}> {
throw new Error('Not implemented');
}
}

View File

@@ -1,10 +1,11 @@
import { ClientType } from 'nocodb-sdk';
import type { DBQueryClient as DBQueryClientType } from '~/dbQueryClient/types';
import { PGDBQueryClient } from '~/dbQueryClient/pg';
import { MySqlDBQueryClient } from '~/dbQueryClient/mysql';
import { SqliteDBQueryClient } from '~/dbQueryClient/sqlite';
export class DBQueryClient {
static get(clientType: ClientType) {
static get(clientType: ClientType): DBQueryClientType {
switch (clientType) {
case ClientType.PG: {
return new PGDBQueryClient();

View File

@@ -1,6 +1,7 @@
import {
isCreatedOrLastModifiedByCol,
isCreatedOrLastModifiedTimeCol,
isHiddenCol,
isLinksOrLTAR,
isOrderCol,
isSystemColumn,
@@ -341,6 +342,7 @@ const getAst = async (
} else if (getHiddenColumn) {
isRequested =
!isSystemColumn(col) ||
((!view || !!view?.show_system_fields) && !isHiddenCol(col, model)) ||
(isCreatedOrLastModifiedTimeCol(col) && col.system) ||
// include all non-has-many system links(self-link) columns since has-many is part of mm relation and which is not required
(isLinksOrLTAR(col) &&

View File

@@ -3,10 +3,6 @@
!!! Do not edit this file manually !!!
*/
import type { IntegrationEntry } from '@noco-local-integrations/core';
export default [
] as IntegrationEntry[];
export default [] as IntegrationEntry[];

View File

@@ -294,7 +294,7 @@ export class DataAttachmentV3Service {
size: fileSize,
};
processedAttachments.push(processedAttachment);
if (supportsThumbnails({ mimetype: mimeType })) {
if (supportsThumbnails({ mimetype: mimeType, size: fileSize })) {
generateThumbnailAttachments.push(processedAttachment);
}
} catch (error) {

View File

@@ -8,6 +8,12 @@ export const isOfficeDocument = (..._args) => {
export const supportsThumbnails = (attachment: any) => {
const mimetype = attachment.mimetype || attachment.mimeType;
const size = attachment.size;
// Skip thumbnail generation if size is missing, not a number, or exceeds limit
if (!size || !ncIsNumber(size) || size > getThumbnailMaxSize()) {
return false;
}
return !!imageMimeTypes.includes(mimetype);
};

View File

@@ -95,3 +95,16 @@ export const NC_DISABLE_GROUP_BY_LIMIT =
export const NC_DISABLE_GROUP_BY_AGG =
process.env.NC_DISABLE_GROUP_BY_AGG === 'true' || false;
const DEFAULT_THUMBNAIL_MAX_SIZE = 3 * 1024 * 1024;
export const getThumbnailMaxSize = () => {
const envValue = process.env.NC_THUMBNAIL_MAX_SIZE;
if (envValue) {
const parsed = parseInt(envValue, 10);
if (!isNaN(parsed) && parsed > 0) {
return parsed;
}
}
return DEFAULT_THUMBNAIL_MAX_SIZE;
};

View File

@@ -33,18 +33,17 @@ const nocoExecute = async (
dataTree = {},
rootArgs = null,
): Promise<any> => {
// Handle array of resolvers by executing nocoExecute on each and returning a Promise.all
// Handle array of resolvers by executing nocoExecute on each sequentially
if (Array.isArray(resolverObj)) {
return Promise.all(
resolverObj.map((resolver, i) =>
nocoExecuteSingle(
requestObj,
resolver,
(dataTree[i] = dataTree[i] || {}),
rootArgs,
),
),
);
const results = [];
for (let i = 0; i < resolverObj.length; i++) {
const resolver = resolverObj[i];
dataTree[i] = dataTree[i] || {};
results.push(
await nocoExecuteSingle(requestObj, resolver, dataTree[i], rootArgs),
);
}
return results;
} else {
return nocoExecuteSingle(requestObj, resolverObj, dataTree, rootArgs);
}
@@ -66,12 +65,12 @@ const nocoExecuteSingle = async (
* @param args arguments passed to resolver functions
* @returns Promise resolving the nested value
*/
const extractNested = (
const extractNested = async (
path: string[],
dataTreeObj: any,
resolver: ResolverObj = {},
args = {},
): any => {
): Promise<any> => {
if (path.length) {
const key = path[0];
// If key doesn't exist in dataTree, resolve using resolver or create a placeholder
@@ -102,23 +101,23 @@ const nocoExecuteSingle = async (
}
// Recursively handle nested arrays or resolve promises
return (
dataTreeObj[path[0]] instanceof Promise
? dataTreeObj[path[0]]
: Promise.resolve(dataTreeObj[path[0]])
).then((res1) => {
if (Array.isArray(res1)) {
return Promise.all(
res1.map((r) => extractNested(path.slice(1), r, {}, args)),
);
} else {
return res1 !== null && res1 !== undefined
? extractNested(path.slice(1), res1, {}, args)
: Promise.resolve(null);
const res1 = await (dataTreeObj[path[0]] instanceof Promise
? dataTreeObj[path[0]]
: Promise.resolve(dataTreeObj[path[0]]));
if (Array.isArray(res1)) {
const results = [];
for (const r of res1) {
results.push(await extractNested(path.slice(1), r, {}, args));
}
});
return results;
} else {
return res1 !== null && res1 !== undefined
? await extractNested(path.slice(1), res1, {}, args)
: null;
}
} else {
return Promise.resolve(dataTreeObj); // If path is exhausted, return data tree object
return dataTreeObj; // If path is exhausted, return data tree object
}
};
@@ -149,17 +148,16 @@ const nocoExecuteSingle = async (
dataTree[key] = res[key]; // Store result in dataTree
} else {
// If nested, extract the nested value using extractNested function
res[key] = extractNested(
resolverObj?.__proto__?.__columnAliases?.[key]?.path,
dataTree,
resolverObj,
args?.nested?.[key],
).then((res1) => {
return Promise.resolve(
// Flatten the array if it's nested
Array.isArray(res1) ? flattenArray(res1) : res1,
res[key] = (async () => {
const res1 = await extractNested(
resolverObj?.__proto__?.__columnAliases?.[key]?.path,
dataTree,
resolverObj,
args?.nested?.[key],
);
});
// Flatten the array if it's nested
return Array.isArray(res1) ? flattenArray(res1) : res1;
})();
}
}
@@ -170,35 +168,37 @@ const nocoExecuteSingle = async (
: Object.keys(resolverObj);
const out: any = {}; // Holds the final output
const resolPromises = []; // Holds all the promises for asynchronous resolution
for (const key of extractKeys) {
// Extract the field for each key
extractField(key, rootArgs?.nested?.[key]);
// Handle nested request objects by recursively calling nocoExecute
if (requestObj[key] && typeof requestObj[key] === 'object') {
res[key] = res[key].then((res1) => {
if (res[key]) {
const res1 = await res[key];
if (Array.isArray(res1)) {
// Handle arrays of results by executing nocoExecute on each element
return (dataTree[key] = Promise.all(
res1.map((r, i) =>
nocoExecute(
requestObj[key] as XcRequest,
r,
dataTree?.[key]?.[i],
Object.assign(
{
nestedPage: rootArgs?.nestedPage,
limit: rootArgs?.nestedLimit,
},
rootArgs?.nested?.[key] || {},
),
// Handle arrays of results by executing nocoExecute on each element sequentially
dataTree[key] = dataTree[key] || [];
for (let i = 0; i < res1.length; i++) {
const r = res1[i];
dataTree[key][i] = dataTree[key][i] || {};
dataTree[key][i] = await nocoExecute(
requestObj[key] as XcRequest,
r,
dataTree[key][i],
Object.assign(
{
nestedPage: rootArgs?.nestedPage,
limit: rootArgs?.nestedLimit,
},
rootArgs?.nested?.[key] || {},
),
),
));
);
}
res[key] = dataTree[key];
} else if (res1) {
// Handle single objects
return (dataTree[key] = nocoExecute(
res[key] = await nocoExecute(
requestObj[key] as XcRequest,
res1,
dataTree[key],
@@ -209,24 +209,19 @@ const nocoExecuteSingle = async (
},
rootArgs?.nested?.[key] || {},
),
));
);
dataTree[key] = res[key];
} else {
res[key] = res1;
}
return res1; // Return result if no further nesting
});
}
}
// Push resolved promises to resolPromises array
if (res[key]) {
resolPromises.push(
(async () => {
out[key] = await res[key];
})(),
);
out[key] = await res[key];
}
}
// Wait for all promises to resolve before returning the final output
await Promise.all(resolPromises);
return out; // Return the final resolved output
};

View File

@@ -1,183 +0,0 @@
import {
ButtonActionsType,
checkboxIconList,
durationOptions,
ratingIconList,
UITypes,
} from 'nocodb-sdk';
interface Field {
id: string;
name: string;
type: string;
meta: string | null;
options: any;
}
export function transformFieldConfig(field: Field): Field {
const newField = { ...field };
let metaObj = {} as Record<string, any>;
try {
metaObj = field.meta ? JSON.parse(field.meta) : {};
} catch (e) {
metaObj = typeof field.meta === 'object' ? field.meta : {};
}
newField.options = newField.options || {};
switch (field.type) {
case UITypes.LongText:
newField.options = {
...newField.options,
rich_text: metaObj.richMode || false,
ai: metaObj.ai || false,
};
break;
case UITypes.PhoneNumber:
case UITypes.URL:
case UITypes.Email:
newField.options = {
...newField.options,
validation: metaObj.validate || false,
};
break;
case UITypes.Number:
newField.options = {
...newField.options,
locale_string: metaObj.isLocaleString || false,
};
break;
case UITypes.Decimal:
case UITypes.Rollup:
newField.options = {
...newField.options,
precision: metaObj.precision || 1,
locale_string: metaObj.isLocaleString || false,
};
break;
case UITypes.Currency:
newField.options = {
...newField.options,
locale: metaObj.currency_locale || 'en-US',
code: metaObj.currency_code || 'USD',
};
break;
case UITypes.Percent:
newField.options = {
...newField.options,
show_as_progress: metaObj.is_progress || false,
};
break;
case UITypes.Duration:
newField.options = {
...newField.options,
duration_format: durationOptions[metaObj?.duration || 0]?.title,
};
break;
case UITypes.DateTime:
case UITypes.CreatedTime:
case UITypes.LastModifiedBy:
newField.options = {
...newField.options,
date_format: metaObj.date_format || 'YYYY/MM/DD',
time_format: metaObj.time_format || 'HH:mm:ss',
['12hr_format']: metaObj.is12hrFormat || false,
display_timezone: metaObj.isDisplayTimezone,
timezone: metaObj.timezone,
use_same_timezone_for_all: metaObj.useSameTimezoneForAll || false,
};
break;
case UITypes.Date:
newField.options = {
...newField.options,
date_format: metaObj.date_format || 'YYYY/MM/DD',
};
break;
case UITypes.Time:
newField.options = {
...newField.options,
time_format: metaObj.time_format || 'HH:mm',
['12hr_format']: metaObj.is12hrFormat || false,
};
break;
case UITypes.Checkbox:
if (metaObj.icon) {
newField.options = {
...newField.options,
icon: checkboxIconList[metaObj?.iconIdx ?? 0].label,
color: metaObj.color || '#232323',
};
}
break;
case UITypes.Rating:
newField.options = {
...newField.options,
icon: ratingIconList[metaObj?.iconIdx ?? 0].label,
max_value: metaObj.max || 5,
color: metaObj.color || '#232323',
};
break;
case UITypes.Barcode:
newField.options = {
...newField.options,
barcode_format: metaObj.barcodeFormat || 'CODE128',
barcode_value_field_id: field.options?.barcode_value_field_id,
};
break;
case UITypes.User:
newField.options = {
...newField.options,
allow_multiple_users: metaObj.is_multi || false,
notify_user_when_added: metaObj.notify || false,
};
break;
case UITypes.Formula:
if (metaObj.display_column_meta) {
newField.options = {
...newField.options,
result: metaObj.display_type
? transformFieldConfig({
type: metaObj.display_type,
meta: metaObj.display_column_meta.meta,
options: {},
} as Field)
: null,
};
}
break;
case UITypes.Button: {
if (newField.options.type === ButtonActionsType.Ai) {
newField.options = {
...newField.options,
prompt: newField.options.formula || '',
};
}
break;
}
case UITypes.Links:
case UITypes.LinkToAnotherRecord:
newField.options = {
...newField.options,
singular: metaObj.singular,
plural: metaObj.plural,
};
}
delete newField.meta;
return newField;
}

View File

@@ -1,7 +1,7 @@
import 'mocha';
// @ts-ignore
import { expect } from 'chai';
import { UITypes, ViewTypes } from 'nocodb-sdk';
import { APIContext, UITypes, ViewTypes } from 'nocodb-sdk';
import request from 'supertest';
import { createProject } from '../../factory/base';
import {
@@ -9,9 +9,15 @@ import {
createLookupColumn,
createLtarColumn,
createRollupColumn,
updateViewColumn,
} from '../../factory/column';
import { createChildRow, createRow, getRow } from '../../factory/row';
import {
countRows,
createChildRow,
createRow,
getRow,
listRow,
} from '../../factory/row';
import { listenForJob } from '../../factory/job';
import { createTable, getTable } from '../../factory/table';
import { createView } from '../../factory/view';
import init from '../../init';
@@ -114,6 +120,8 @@ function viewRowLocalStaticTests() {
},
});
await linkInitTables(context, base);
console.timeEnd('#### viewRowLocalTests');
});
@@ -600,7 +608,7 @@ function viewRowLocalTests() {
.expect(200);
expect(ascResponse.body.pageInfo.totalRows).greaterThan(0);
expect(JSON.stringify(ascResponse.body.list[0][lookupColumn.title])).equal(
JSON.stringify(['ANGELA']),
JSON.stringify(['AARON']),
);
const descResponse = await request(context.app)
@@ -1186,9 +1194,98 @@ function viewRowLocalTests() {
});
const testFindOneSortedFilteredNestedFieldsDataWithRollup = async (
_viewType: ViewTypes,
viewType: ViewTypes,
) => {
// TODO: Implement test logic
const rollupColumn = await createRollupColumn(context, {
base: base,
title: 'Number of rentals',
rollupFunction: 'count',
table: customerTable,
relatedTableName: rentalTable.table_name,
relatedTableColumnTitle: 'RentalDate',
});
const view = await createView(context, {
title: 'View',
table: customerTable,
type: viewType,
});
const viewColumns = await view.getColumns(ctx);
const rollupViewColumn = viewColumns.find(
(vc) => vc.fk_column_id === rollupColumn.id,
);
await updateViewColumns(context, {
view: view,
viewColumns: {
[rollupViewColumn.id]: { show: true },
},
});
const activeColumn = (await customerTable.getColumns(ctx)).find(
(c: ColumnType) => c.title === 'Active',
);
const nestedFields = {
Rentals: { f: 'RentalDate,ReturnDate' },
};
const nestedFilter = [
{
fk_column_id: rollupColumn?.id,
status: 'create',
logical_op: 'and',
comparison_op: 'gte',
value: 1,
},
{
is_group: true,
status: 'create',
logical_op: 'or',
children: [
{
fk_column_id: rollupColumn?.id,
status: 'create',
logical_op: 'and',
comparison_op: 'lte',
value: 10,
},
{
is_group: true,
status: 'create',
logical_op: 'and',
children: [
{
logical_op: 'and',
fk_column_id: activeColumn?.id,
status: 'create',
comparison_op: 'eq',
value: 1,
},
],
},
],
},
];
const ascResponse = await request(context.app)
.get(
`/api/v1/db/data/noco/${base.id}/${customerTable.id}/views/${view.id}/find-one`,
)
.set('xc-auth', context.token)
.query({
nested: nestedFields,
filterArrJson: JSON.stringify([nestedFilter]),
sort: `${rollupColumn.title}`,
})
.expect(200);
// Verify rollup column exists and has a value
expect(parseInt(ascResponse.body[rollupColumn.title])).to.be.greaterThan(0);
// Verify nested rentals are returned
expect(ascResponse.body).to.have.property('Rentals');
};
it('Find one view sorted filtered view with nested fields data list with a rollup column in customer table GRID', async function () {
@@ -1254,8 +1351,62 @@ function viewRowLocalTests() {
await testGroupDescSorted(ViewTypes.CALENDAR);
});
const testGroupWithOffset = async (_viewType: ViewTypes) => {
// TODO: Implement test logic
const testGroupWithOffset = async (viewType: ViewTypes) => {
const view = await createView(context, {
title: 'View',
table: customerTable,
type: viewType,
});
const firstNameColumn = customerColumns.find(
(col: ColumnType) => col.title === 'FirstName',
);
const rollupColumn = await createRollupColumn(context, {
base: base,
title: 'Rollup',
rollupFunction: 'count',
table: customerTable,
relatedTableName: rentalTable.table_name,
relatedTableColumnTitle: 'RentalDate',
});
const visibleColumns = [firstNameColumn];
const sortInfo = `-FirstName, +${rollupColumn.title}`;
// First get results without offset to know what to expect
const responseNoOffset = await request(context.app)
.get(
`/api/v1/db/data/noco/${base.id}/${customerTable.id}/views/${view.id}/groupby`,
)
.set('xc-auth', context.token)
.query({
fields: visibleColumns.map((c) => c.title),
sort: sortInfo,
column_name: firstNameColumn.title,
})
.expect(200);
// Now get results with offset and verify
const response = await request(context.app)
.get(
`/api/v1/db/data/noco/${base.id}/${customerTable.id}/views/${view.id}/groupby`,
)
.set('xc-auth', context.token)
.query({
fields: visibleColumns.map((c) => c.title),
sort: sortInfo,
column_name: firstNameColumn.title,
offset: 4,
})
.expect(200);
// Verify that offset works - first item with offset=4 should match 5th item without offset
if (responseNoOffset.body.list.length > 4) {
expect(response.body.list[0]['FirstName']).to.equal(
responseNoOffset.body.list[4]['FirstName'],
);
}
};
it('Groupby desc sorted and with rollup view data list with required columns GALLERY', async function () {
@@ -1276,8 +1427,40 @@ function viewRowLocalTests() {
//#endregion Group by tests
//#region Count tests
const testCount = async (_viewType: ViewTypes) => {
// TODO: Implement test logic
const testCount = async (viewType: ViewTypes) => {
let calendar_range = {};
let table;
if (viewType === ViewTypes.CALENDAR) {
table = rentalTable;
calendar_range = {
fk_from_column_id: rentalColumns.find(
(c: ColumnType) => c.title === 'RentalDate',
)?.id,
};
} else {
table = customerTable;
}
const view = await createView(context, {
title: 'View ' + viewType,
table: table,
type: viewType,
range: calendar_range,
});
const response = await request(context.app)
.get(`/api/v1/db/data/noco/${base.id}/${table.id}/views/${view.id}/count`)
.set('xc-auth', context.token)
.expect(200);
if (viewType === ViewTypes.CALENDAR) {
// Rental table has 40 rows
expect(parseInt(response.body.count)).to.equal(40);
} else {
// Customer table has 33 rows
expect(parseInt(response.body.count)).to.equal(33);
}
};
it('Count view data list with required columns', async function () {
@@ -1289,8 +1472,44 @@ function viewRowLocalTests() {
//#endregion Count tests
//#region Read/Exist tests
const testReadViewRow = async (_viewType: ViewTypes) => {
// TODO: Implement test logic
const testReadViewRow = async (viewType: ViewTypes) => {
let table;
let calendar_range = {};
const idColumn = 'Id';
if (viewType === ViewTypes.CALENDAR) {
table = rentalTable;
calendar_range = {
fk_from_column_id: rentalColumns.find(
(c: ColumnType) => c.title === 'RentalDate',
)?.id,
};
} else {
table = customerTable;
}
const view = await createView(context, {
title: 'View ' + viewType,
table: table,
type: viewType,
range: calendar_range,
});
const listResponse = await request(context.app)
.get(`/api/v1/db/data/noco/${base.id}/${table.id}/views/${view.id}`)
.set('xc-auth', context.token)
.expect(200);
const row = listResponse.body.list[0];
const readResponse = await request(context.app)
.get(
`/api/v1/db/data/noco/${base.id}/${table.id}/views/${view.id}/${row[idColumn]}`,
)
.set('xc-auth', context.token)
.expect(200);
expect(row[idColumn]).to.equal(readResponse.body[idColumn]);
};
it('Read view row', async function () {
@@ -1300,8 +1519,45 @@ function viewRowLocalTests() {
await testReadViewRow(ViewTypes.CALENDAR);
});
const testViewRowExists = async (_viewType: ViewTypes) => {
// TODO: Implement test logic
const testViewRowExists = async (viewType: ViewTypes) => {
let table;
let calendar_range = {};
const idColumn = 'Id';
if (viewType === ViewTypes.CALENDAR) {
table = rentalTable;
calendar_range = {
fk_from_column_id: rentalColumns.find(
(c: ColumnType) => c.title === 'RentalDate',
)?.id,
};
} else {
table = customerTable;
}
const view = await createView(context, {
title: 'View ' + viewType,
table: table,
type: viewType,
range: calendar_range,
});
// Get first row to test existence
const listResponse = await request(context.app)
.get(`/api/v1/db/data/noco/${base.id}/${table.id}/views/${view.id}`)
.set('xc-auth', context.token)
.expect(200);
const row = listResponse.body.list[0];
const response = await request(context.app)
.get(
`/api/v1/db/data/noco/${base.id}/${table.id}/views/${view.id}/${row[idColumn]}/exist`,
)
.set('xc-auth', context.token)
.expect(200);
expect(response.body).to.be.true;
};
it('Exist view row : should return true when row exists in view', async function () {
@@ -1311,8 +1567,36 @@ function viewRowLocalTests() {
await testViewRowExists(ViewTypes.CALENDAR);
});
const testViewRowNotExists = async (_viewType: ViewTypes) => {
// TODO: Implement test logic
const testViewRowNotExists = async (viewType: ViewTypes) => {
let calendar_range = {};
let table;
if (viewType === ViewTypes.CALENDAR) {
table = rentalTable;
calendar_range = {
fk_from_column_id: rentalColumns.find(
(c: ColumnType) => c.title === 'RentalDate',
)?.id,
};
} else {
table = customerTable;
}
const view = await createView(context, {
title: 'View ' + viewType,
table: table,
type: viewType,
range: calendar_range,
});
const response = await request(context.app)
.get(
`/api/v1/db/data/noco/${base.id}/${table.id}/views/${view.id}/999999/exist`,
)
.set('xc-auth', context.token)
.expect(200);
expect(response.body).to.be.false;
};
it("Exist view row : should return false when row doesn't exist in view", async function () {
@@ -1325,15 +1609,80 @@ function viewRowLocalTests() {
//#region Calendar-specific tests
const testCalendarDataApi = async () => {
// TODO: Implement test logic
const table = rentalTable;
const calendar_range = {
fk_from_column_id: rentalColumns.find(
(c: ColumnType) => c.title === 'RentalDate',
)?.id,
};
const view = await createView(context, {
title: 'View',
table: table,
type: ViewTypes.CALENDAR,
range: calendar_range,
});
const response = await request(context.app)
.get(
`/api/v1/db/calendar-data/noco/${base.id}/${table.id}/views/${view.id}`,
)
.query({
from_date: '2005-05-24',
to_date: '2005-05-26',
next_date: '2005-05-27',
prev_date: '2005-05-24',
})
.set('xc-auth', context.token)
.expect(200);
// Local data has rentals in the 2005-05-24 to 2005-05-26 range
expect(response.body.list.length).to.be.greaterThan(0);
};
it('Calendar data', async function () {
await testCalendarDataApi();
});
const testCountDatesByRange = async (_viewType: ViewTypes) => {
// TODO: Implement test logic
const testCountDatesByRange = async (viewType: ViewTypes) => {
let calendar_range = {};
let expectStatus = 400;
if (viewType === ViewTypes.CALENDAR) {
calendar_range = {
fk_from_column_id: rentalColumns.find(
(c: ColumnType) => c.title === 'RentalDate',
)?.id,
};
expectStatus = 200;
}
const view = await createView(context, {
title: 'View',
table: rentalTable,
type: viewType,
range: calendar_range,
});
const response = await request(context.app)
.get(
`/api/v1/db/calendar-data/noco/${base.id}/${rentalTable.id}/views/${view.id}/countByDate/`,
)
.query({
from_date: '2005-05-24',
to_date: '2005-05-26',
next_date: '2005-05-27',
prev_date: '2005-05-24',
})
.set('xc-auth', context.token)
.expect(expectStatus);
if (expectStatus === 200) {
expect(response.body).to.have.property('count');
expect(response.body.count).to.be.greaterThan(0);
} else if (expectStatus === 400) {
expect(response.body.msg).to.equal('View is not a calendar view');
}
};
it('Count dates by range Calendar', async () => {
@@ -1359,13 +1708,130 @@ function viewRowLocalTests() {
//#region Export tests
it('Export csv GRID', async function () {
// TODO: Implement test logic
const view = await createView(context, {
title: 'View',
table: customerTable,
type: ViewTypes.GRID,
});
// get row count
const rowCount = await countRows({
base: base,
table: customerTable,
view: view,
});
// Start export job
const jobResponse = await request(context.app)
.post(`/api/v2/export/${view.id}/csv`)
.set('xc-auth', context.token)
.expect(200);
// Verify we got a job ID
const jobId = jobResponse.body.id;
expect(jobId).to.be.a('string');
// Wait for job completion
const resultData = await listenForJob({
context,
base_id: base.id,
job_id: jobId,
});
// Verify the exported file
expect(resultData).to.be.an('object');
expect(resultData.url).to.be.a('string');
const fileUrl = resultData.url;
// Download the file
const fileResponse = await request(context.app)
.get(`/${encodeURI(fileUrl)}`)
.set('xc-auth', context.token)
.expect(200);
// Check file content
expect(fileResponse.headers['content-type']).to.include('text/csv');
expect(fileResponse.text).to.be.a('string').and.not.empty;
expect(fileResponse.text.split('\n').length).to.equal(rowCount + 1);
});
//#endregion Export tests
//#region View column API tests
// FIXME: still has cache race condition issue
it('Test view column v3 apis', async function () {
// TODO: Implement test logic
// Use filmTable which was already initialized
const view = await createView(context, {
title: 'Film View',
table: filmTable,
type: ViewTypes.GRID,
});
const columns = await filmTable.getColumns(ctx);
// get rows before hiding columns
const listResponse = await request(context.app)
.get(`/api/v1/db/data/noco/${base.id}/${filmTable.id}/views/${view.id}`)
.set('xc-auth', context.token)
.query({ limit: 1 })
.expect(200);
const rows = listResponse.body.list;
// hide few columns using update view column API
const columnsToHide = ['Rating', 'Description', 'ReleaseYear'];
// generate key value pair of column id and object with show as false
const viewColumnsObj: any = columnsToHide.reduce(
(acc: any, columnTitle) => {
const column = columns.find((c: ColumnType) => c.title === columnTitle);
if (column) {
acc[column.id] = {
show: false,
};
}
return acc;
},
{},
);
await updateViewColumns(context, {
view,
viewColumns: viewColumnsObj,
});
// get rows after update
const rowsAfterUpdate = await listRow({
base: base,
table: filmTable,
view,
options: {
limit: 1,
},
});
// verify column visible in old and hidden in new
for (const title of columnsToHide) {
expect(rows[0]).to.have.property(title);
expect(rowsAfterUpdate[0]).to.not.have.property(title);
}
// get view columns and verify hidden columns
const viewColApiRes: any = await getViewColumns(context, {
view,
});
for (const colId of Object.keys(viewColApiRes[APIContext.VIEW_COLUMNS])) {
const column = columns.find((c: ColumnType) => c.id === colId);
if (column && columnsToHide.includes(column.title)) {
expect(viewColApiRes[APIContext.VIEW_COLUMNS][colId]).to.have.property(
'show',
);
expect(!!viewColApiRes[APIContext.VIEW_COLUMNS][colId].show).to.be.eq(
false,
);
}
}
});
//#endregion View column API tests
}

View File

@@ -1411,6 +1411,10 @@ export const linkInitTables = async (context: any, base: any) => {
id: '23',
customerId: '8',
},
{
id: '30',
customerId: '8',
},
{
id: '24',
customerId: '7',
@@ -1445,7 +1449,7 @@ export const linkInitTables = async (context: any, base: any) => {
},
{
id: '4',
customerId: '8',
customerId: '12',
},
{
id: '4',
@@ -1463,6 +1467,18 @@ export const linkInitTables = async (context: any, base: any) => {
id: '5',
customerId: '12',
},
{
id: '29',
customerId: '31',
},
{
id: '27',
customerId: '32',
},
{
id: '28',
customerId: '33',
},
];
const linkTo_Customer_MM_Rental_LTAR = (rowId: string, body: any[]) => {