Browse Source

Merge pull request #6817 from nocodb/fix/5973-6068-webhook

fix: Webhook related bugs
pull/6937/head
Raju Udava 10 months ago committed by GitHub
parent
commit
d2e33f530d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      packages/nc-gui/components/smartsheet/Form.vue
  2. 10
      packages/nc-gui/components/smartsheet/Row.vue
  3. 7
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  4. 2
      packages/nc-gui/components/virtual-cell/Links.vue
  5. 2
      packages/nc-gui/composables/useData.ts
  6. 2
      packages/nc-gui/composables/useExpandedFormStore.ts
  7. 470
      packages/nocodb/src/db/BaseModelSqlv2.ts
  8. 2
      packages/nocodb/src/services/datas.service.ts

8
packages/nc-gui/components/smartsheet/Form.vue

@ -67,7 +67,7 @@ reloadEventHook.on(async () => {
const { showAll, hideAll, saveOrUpdate } = useViewColumnsOrThrow()
const { syncLTARRefs, row } = useProvideSmartsheetRowStore(
const { state, row } = useProvideSmartsheetRowStore(
meta,
ref({
row: formState,
@ -124,11 +124,7 @@ async function submitForm() {
if (e.errorFields.length) return
}
const insertedRowData = await insertRow({ row: formState, oldRow: {}, rowMeta: { new: true } })
if (insertedRowData) {
await syncLTARRefs(insertedRowData)
}
await insertRow({ row: { ...formState, ...state.value }, oldRow: {}, rowMeta: { new: true } })
submitted.value = true
}

10
packages/nc-gui/components/smartsheet/Row.vue

@ -24,16 +24,6 @@ const { meta } = useSmartsheetStoreOrThrow()
const { isNew, state, syncLTARRefs, clearLTARCell, addLTARRef } = useProvideSmartsheetRowStore(meta as Ref<TableType>, currentRow)
// on changing isNew(new record insert) status sync LTAR cell values
watch(isNew, async (nextVal, prevVal) => {
if (prevVal && !nextVal) {
await syncLTARRefs(currentRow.value.row)
// update row values without invoking api
currentRow.value.row = { ...currentRow.value.row, ...state.value }
currentRow.value.oldRow = { ...currentRow.value.row, ...state.value }
}
})
const reloadViewDataTrigger = inject(ReloadViewDataHookInj)!
// override reload trigger and use it to reload row

7
packages/nc-gui/components/smartsheet/expanded-form/index.vue

@ -187,7 +187,6 @@ const onDuplicateRow = () => {
const save = async () => {
if (isNew.value) {
const data = await _save(rowState.value)
await syncLTARRefs(data)
reloadTrigger?.trigger()
} else {
let kanbanClbk
@ -347,8 +346,7 @@ useActiveKeyupListener(
e.stopPropagation()
if (isNew.value) {
const data = await _save(rowState.value)
await syncLTARRefs(data)
await _save(rowState.value)
reloadHook?.trigger(null)
} else {
await save()
@ -382,8 +380,7 @@ useActiveKeyupListener(
okText: t('general.save'),
cancelText: t('labels.discard'),
onOk: async () => {
const data = await _save(rowState.value)
await syncLTARRefs(data)
await _save(rowState.value)
reloadHook?.trigger(null)
addNewRow()
},

2
packages/nc-gui/components/virtual-cell/Links.vue

@ -47,7 +47,7 @@ const relatedTableDisplayColumn = computed(
loadRelatedTableMeta()
const textVal = computed(() => {
if (isForm?.value) {
if (isForm?.value || isNew.value) {
return state.value?.[colTitle.value]?.length
? `${+state.value?.[colTitle.value]?.length} ${t('msg.recordsLinked')}`
: t('msg.noRecordsLinked')

2
packages/nc-gui/composables/useData.ts

@ -91,7 +91,7 @@ export function useData(args: {
base?.value.id as string,
metaValue?.id as string,
viewMetaValue?.id as string,
insertObj,
{ ...insertObj, ...(ltarState ||{}) },
)
if (!undo) {

2
packages/nc-gui/composables/useExpandedFormStore.ts

@ -180,7 +180,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
if (missingRequiredColumns.size) return
data = await $api.dbTableRow.create('noco', base.value.id as string, meta.value.id, insertObj)
data = await $api.dbTableRow.create('noco', base.value.id as string, meta.value.id, { ...insertObj, ...(ltarState ||{}) })
Object.assign(row.value, {
row: data,

470
packages/nocodb/src/db/BaseModelSqlv2.ts

@ -19,8 +19,8 @@ import Validator from 'validator';
import { customAlphabet } from 'nanoid';
import DOMPurify from 'isomorphic-dompurify';
import { v4 as uuidv4 } from 'uuid';
import { Knex } from 'knex';
import type LookupColumn from '~/models/LookupColumn';
import type { Knex } from 'knex';
import type { XKnex } from '~/db/CustomKnex';
import type {
XcFilter,
@ -65,6 +65,7 @@ import {
} from '~/utils/globals';
import { extractProps } from '~/helpers/extractProps';
import { defaultLimitConfig } from '~/helpers/extractLimitAndOffset';
import Transaction = Knex.Transaction;
dayjs.extend(utc);
@ -1736,28 +1737,35 @@ class BaseModelSqlv2 {
(await column.getColOptions()) as LinkToAnotherRecordColumn;
if (colOptions?.type === 'hm') {
const listLoader = new DataLoader(async (ids: string[]) => {
if (ids.length > 1) {
const data = await this.multipleHmList(
{
colId: column.id,
ids,
},
(listLoader as any).args,
);
return ids.map((id: string) => (data[id] ? data[id] : []));
} else {
return [
await this.hmList(
const listLoader = new DataLoader(
async (ids: string[]) => {
if (ids.length > 1) {
const data = await this.multipleHmList(
{
colId: column.id,
id: ids[0],
ids,
},
(listLoader as any).args,
),
];
}
});
);
return ids.map((id: string) =>
data[id] ? data[id] : [],
);
} else {
return [
await this.hmList(
{
colId: column.id,
id: ids[0],
},
(listLoader as any).args,
),
];
}
},
{
cache: false,
},
);
const self: BaseModelSqlv2 = this;
proto[
@ -1771,29 +1779,34 @@ class BaseModelSqlv2 {
);
};
} else if (colOptions.type === 'mm') {
const listLoader = new DataLoader(async (ids: string[]) => {
if (ids?.length > 1) {
const data = await this.multipleMmList(
{
parentIds: ids,
colId: column.id,
},
(listLoader as any).args,
);
return data;
} else {
return [
await this.mmList(
const listLoader = new DataLoader(
async (ids: string[]) => {
if (ids?.length > 1) {
const data = await this.multipleMmList(
{
parentId: ids[0],
parentIds: ids,
colId: column.id,
},
(listLoader as any).args,
),
];
}
});
);
return data;
} else {
return [
await this.mmList(
{
parentId: ids[0],
colId: column.id,
},
(listLoader as any).args,
),
];
}
},
{
cache: false,
},
);
const self: BaseModelSqlv2 = this;
proto[
@ -1821,55 +1834,62 @@ class BaseModelSqlv2 {
// it takes individual keys and callback is invoked with an array of values and we can get the
// result for all those together and return the value in the same order as in the array
// this way all parents data extracted together
const readLoader = new DataLoader(async (_ids: string[]) => {
// handle binary(16) foreign keys
const ids = _ids.map((id) => {
if (pCol.ct !== 'binary(16)') return id;
// Cast the id to string.
const idAsString = id + '';
// Check if the id is a UUID and the column is binary(16)
const isUUIDBinary16 =
idAsString.length === 36 || idAsString.length === 32;
// If the id is a UUID and the column is binary(16), convert the id to a Buffer. Otherwise, return null to indicate that the id is not a UUID.
const idAsUUID = isUUIDBinary16
? idAsString.length === 32
? idAsString.replace(
/(.{8})(.{4})(.{4})(.{4})(.{12})/,
'$1-$2-$3-$4-$5',
)
: idAsString
: null;
return idAsUUID
? Buffer.from(idAsUUID.replace(/-/g, ''), 'hex')
: id;
});
const data = await (
await Model.getBaseModelSQL({
id: pCol.fk_model_id,
dbDriver: this.dbDriver,
})
).list(
{
fieldsSet: (readLoader as any).args?.fieldsSet,
filterArr: [
new Filter({
id: null,
fk_column_id: pCol.id,
fk_model_id: pCol.fk_model_id,
value: ids as any[],
comparison_op: 'in',
}),
],
},
true,
);
const readLoader = new DataLoader(
async (_ids: string[]) => {
// handle binary(16) foreign keys
const ids = _ids.map((id) => {
if (pCol.ct !== 'binary(16)') return id;
// Cast the id to string.
const idAsString = id + '';
// Check if the id is a UUID and the column is binary(16)
const isUUIDBinary16 =
idAsString.length === 36 || idAsString.length === 32;
// If the id is a UUID and the column is binary(16), convert the id to a Buffer. Otherwise, return null to indicate that the id is not a UUID.
const idAsUUID = isUUIDBinary16
? idAsString.length === 32
? idAsString.replace(
/(.{8})(.{4})(.{4})(.{4})(.{12})/,
'$1-$2-$3-$4-$5',
)
: idAsString
: null;
return idAsUUID
? Buffer.from(idAsUUID.replace(/-/g, ''), 'hex')
: id;
});
const data = await (
await Model.getBaseModelSQL({
id: pCol.fk_model_id,
dbDriver: this.dbDriver,
})
).list(
{
fieldsSet: (readLoader as any).args?.fieldsSet,
filterArr: [
new Filter({
id: null,
fk_column_id: pCol.id,
fk_model_id: pCol.fk_model_id,
value: ids as any[],
comparison_op: 'in',
}),
],
},
true,
);
const groupedList = groupBy(data, pCol.title);
return _ids.map(async (id: string) => groupedList?.[id]?.[0]);
});
const groupedList = groupBy(data, pCol.title);
return _ids.map(
async (id: string) => groupedList?.[id]?.[0],
);
},
{
cache: false,
},
);
// defining BelongsTo read resolver method
proto[column.title] = async function (args?: any) {
@ -2530,71 +2550,15 @@ class BaseModelSqlv2 {
);
let rowId = null;
const postInsertOps = [];
const nestedCols = (await this.model.getColumns()).filter((c) =>
isLinksOrLTAR(c),
);
for (const col of nestedCols) {
if (col.title in data) {
const colOptions =
await col.getColOptions<LinkToAnotherRecordColumn>();
// parse data if it's JSON string
const nestedData =
typeof data[col.title] === 'string'
? JSON.parse(data[col.title])
: data[col.title];
switch (colOptions.type) {
case RelationTypes.BELONGS_TO:
{
const childCol = await colOptions.getChildColumn();
const parentCol = await colOptions.getParentColumn();
insertObj[childCol.column_name] = nestedData?.[parentCol.title];
}
break;
case RelationTypes.HAS_MANY:
{
const childCol = await colOptions.getChildColumn();
const childModel = await childCol.getModel();
await childModel.getColumns();
postInsertOps.push(async () => {
await this.dbDriver(this.getTnPath(childModel.table_name))
.update({
[childCol.column_name]: rowId,
})
.whereIn(
childModel.primaryKey.column_name,
nestedData?.map((r) => r[childModel.primaryKey.title]),
);
});
}
break;
case RelationTypes.MANY_TO_MANY: {
postInsertOps.push(async () => {
const parentModel = await colOptions
.getParentColumn()
.then((c) => c.getModel());
await parentModel.getColumns();
const parentMMCol = await colOptions.getMMParentColumn();
const childMMCol = await colOptions.getMMChildColumn();
const mmModel = await colOptions.getMMModel();
const rows = nestedData.map((r) => ({
[parentMMCol.column_name]: r[parentModel.primaryKey.title],
[childMMCol.column_name]: rowId,
}));
await this.dbDriver(this.getTnPath(mmModel.table_name)).insert(
rows,
);
});
}
}
}
}
const postInsertOps = await this.prepareNestedLinkQb({
nestedCols,
data,
insertObj,
});
await this.validate(insertObj);
@ -2615,51 +2579,50 @@ class BaseModelSqlv2 {
!response ||
(typeof response?.[0] !== 'object' && response?.[0] !== null)
) {
let id;
if (response?.length) {
id = response[0];
rowId = response[0];
} else {
id = (await query)[0];
rowId = (await query)[0];
}
if (ai) {
if (this.isSqlite) {
// sqlite doesnt return id after insert
id = (
rowId = (
await this.dbDriver(this.tnPath)
.select(ai.column_name)
.max(ai.column_name, { as: 'id' })
)[0].id;
} else if (this.isSnowflake) {
id = (
rowId = (
(await this.dbDriver(this.tnPath).max(ai.column_name, {
as: 'id',
})) as any
).rows[0].id;
}
response = await this.readByPk(
id,
false,
{},
{ ignoreView: true, getHiddenColumn: true },
);
// response = await this.readByPk(
// id,
// false,
// {},
// { ignoreView: true, getHiddenColumn: true },
// );
} else {
response = data;
}
} else if (ai) {
response = await this.readByPk(
Array.isArray(response)
? response?.[0]?.[ai.title]
: response?.[ai.title],
);
rowId = Array.isArray(response)
? response?.[0]?.[ai.title]
: response?.[ai.title];
}
response = Array.isArray(response) ? response[0] : response;
if (response)
rowId =
response[this.model.primaryKey.title] ||
response[this.model.primaryKey.column_name];
await Promise.all(postInsertOps.map((f) => f()));
await Promise.all(postInsertOps.map((f) => f(rowId)));
response = await this.readByPk(
rowId,
false,
{},
{ ignoreView: true, getHiddenColumn: true },
);
await this.afterInsert(response, this.dbDriver, cookie);
@ -2670,6 +2633,94 @@ class BaseModelSqlv2 {
}
}
private async prepareNestedLinkQb({
nestedCols,
data,
insertObj,
}: {
nestedCols: Column[];
data: Record<string, any>;
insertObj: Record<string, any>;
}) {
const postInsertOps: ((rowId: any, trx?: any) => Promise<void>)[] = [];
for (const col of nestedCols) {
if (col.title in data) {
const colOptions = await col.getColOptions<LinkToAnotherRecordColumn>();
// parse data if it's JSON string
let nestedData;
try {
nestedData =
typeof data[col.title] === 'string'
? JSON.parse(data[col.title])
: data[col.title];
} catch {
continue;
}
switch (colOptions.type) {
case RelationTypes.BELONGS_TO:
{
if (typeof nestedData !== 'object') continue;
const childCol = await colOptions.getChildColumn();
const parentCol = await colOptions.getParentColumn();
insertObj[childCol.column_name] = nestedData?.[parentCol.title];
}
break;
case RelationTypes.HAS_MANY:
{
if (!Array.isArray(nestedData)) continue;
const childCol = await colOptions.getChildColumn();
const childModel = await childCol.getModel();
await childModel.getColumns();
postInsertOps.push(
async (
rowId,
// todo: use transaction type
trx: any = this.dbDriver,
) => {
await trx(this.getTnPath(childModel.table_name))
.update({
[childCol.column_name]: rowId,
})
.whereIn(
childModel.primaryKey.column_name,
nestedData?.map((r) => r[childModel.primaryKey.title]),
);
},
);
}
break;
case RelationTypes.MANY_TO_MANY: {
if (!Array.isArray(nestedData)) continue;
postInsertOps.push(
async (
rowId,
// todo: use transaction type
trx: any = this.dbDriver,
) => {
const parentModel = await colOptions
.getParentColumn()
.then((c) => c.getModel());
await parentModel.getColumns();
const parentMMCol = await colOptions.getMMParentColumn();
const childMMCol = await colOptions.getMMChildColumn();
const mmModel = await colOptions.getMMModel();
const rows = nestedData.map((r) => ({
[parentMMCol.column_name]: r[parentModel.primaryKey.title],
[childMMCol.column_name]: rowId,
}));
await trx(this.getTnPath(mmModel.table_name)).insert(rows);
},
);
}
}
}
}
return postInsertOps;
}
async bulkInsert(
datas: any[],
{
@ -2694,8 +2745,13 @@ class BaseModelSqlv2 {
try {
// TODO: ag column handling for raw bulk insert
const insertDatas = raw ? datas : [];
let postInsertOps: ((rowId: any, trx?: any) => Promise<void>)[] = [];
if (!raw) {
const nestedCols = (await this.model.getColumns()).filter((c) =>
isLinksOrLTAR(c),
);
await this.model.getColumns();
for (const d of datas) {
@ -2822,6 +2878,15 @@ class BaseModelSqlv2 {
await this.prepareAttachmentData(insertObj);
// prepare nested link data for insert only if it is single record insertion
if (isSingleRecordInsertion) {
postInsertOps = await this.prepareNestedLinkQb({
nestedCols,
data: d,
insertObj,
});
}
insertDatas.push(insertObj);
}
}
@ -2866,7 +2931,10 @@ class BaseModelSqlv2 {
this.isPg || this.isMssql
? await trx
.batchInsert(this.tnPath, insertDatas, chunkSize)
.returning(this.model.primaryKey?.column_name)
.returning({
[this.model.primaryKey?.title]:
this.model.primaryKey?.column_name,
})
: await trx.batchInsert(this.tnPath, insertDatas, chunkSize);
}
@ -2878,6 +2946,12 @@ class BaseModelSqlv2 {
}
}
// insert nested link data for single record insertion
if (isSingleRecordInsertion) {
const rowId = response[0][this.model.primaryKey?.title];
await Promise.all(postInsertOps.map((f) => f(rowId, trx)));
}
await trx.commit();
if (!raw && !skip_hooks) {
@ -3605,6 +3679,12 @@ class BaseModelSqlv2 {
const childTn = this.getTnPath(childTable);
const parentTn = this.getTnPath(parentTable);
const prevData = await this.readByPk(
rowId,
false,
{},
{ ignoreView: true, getHiddenColumn: true },
);
switch (colOptions.type) {
case RelationTypes.MANY_TO_MANY:
@ -3682,7 +3762,7 @@ class BaseModelSqlv2 {
{},
{ ignoreView: true, getHiddenColumn: true },
);
await this.afterInsert(response, this.dbDriver, cookie);
await this.afterUpdate(prevData, response, this.dbDriver, cookie);
await this.afterAddChild(rowId, childId, cookie);
}
@ -4347,7 +4427,7 @@ class BaseModelSqlv2 {
}
async addLinks({
cookie: _cookie,
cookie,
childIds,
colId,
rowId,
@ -4363,9 +4443,12 @@ class BaseModelSqlv2 {
if (!column || !isLinksOrLTAR(column))
NcError.notFound(`Link column ${colId} not found`);
const row = await this.dbDriver(this.tnPath)
.where(await this._wherePk(rowId))
.first();
const row = await this.readByPk(
rowId,
false,
{},
{ ignoreView: true, getHiddenColumn: true },
);
// validate rowId
if (!row) {
@ -4569,13 +4652,20 @@ class BaseModelSqlv2 {
break;
}
// const response = await this.readByPk(rowId);
// await this.afterInsert(response, this.dbDriver, cookie);
// await this.afterAddChild(rowId, childId, cookie);
const updatedRow = await this.readByPk(
rowId,
false,
{},
{ ignoreView: true, getHiddenColumn: true },
);
await this.afterUpdate(row, updatedRow, this.dbDriver, cookie);
for (const childId of childIds) {
await this.afterAddChild(rowId, childId, cookie);
}
}
async removeLinks({
cookie: _cookie,
cookie,
childIds,
colId,
rowId,
@ -4591,9 +4681,12 @@ class BaseModelSqlv2 {
if (!column || !isLinksOrLTAR(column))
NcError.notFound(`Link column ${colId} not found`);
const row = await this.dbDriver(this.tnPath)
.where(await this._wherePk(rowId))
.first();
const row = await this.readByPk(
rowId,
false,
{},
{ ignoreView: true, getHiddenColumn: true },
);
// validate rowId
if (!row) {
@ -4772,9 +4865,16 @@ class BaseModelSqlv2 {
break;
}
// const newData = await this.readByPk(rowId);
// await this.afterUpdate(prevData, newData, this.dbDriver, cookie);
// await this.afterRemoveChild(rowId, childIds, cookie);
const updatedRow = await this.readByPk(
rowId,
false,
{},
{ ignoreView: true, getHiddenColumn: true },
);
await this.afterUpdate(row, updatedRow, this.dbDriver, cookie);
for (const childId of childIds) {
await this.afterRemoveChild(rowId, childId, cookie);
}
}
async btRead(

2
packages/nocodb/src/services/datas.service.ts

@ -78,7 +78,7 @@ export class DatasService {
dbDriver: await NcConnectionMgrv2.get(source),
});
return await baseModel.insert(param.body, null, param.cookie);
return await baseModel.nestedInsert(param.body, null, param.cookie);
}
async dataUpdate(

Loading…
Cancel
Save