Browse Source

feat: createdBy & lastModifiedBy

pull/7373/head
mertmit 9 months ago
parent
commit
97b956f4ee
  1. 22
      packages/nc-gui/components/cell/ReadOnlyUser.vue
  2. 2
      packages/nc-gui/components/smartsheet/Form.vue
  3. 3
      packages/nc-gui/components/smartsheet/VirtualCell.vue
  4. 2
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  5. 2
      packages/nc-gui/components/smartsheet/grid/GroupBy.vue
  6. 16
      packages/nc-gui/components/smartsheet/grid/Table.vue
  7. 3
      packages/nc-gui/components/smartsheet/header/VirtualCellIcon.ts
  8. 2
      packages/nc-gui/components/template/Editor.vue
  9. 2
      packages/nc-gui/components/virtual-cell/Lookup.vue
  10. 2
      packages/nc-gui/composables/useData.ts
  11. 4
      packages/nc-gui/composables/useMultiSelect/convertCellData.ts
  12. 2
      packages/nc-gui/composables/useMultiSelect/index.ts
  13. 8
      packages/nc-gui/composables/useViewGroupBy.ts
  14. 2
      packages/nc-gui/utils/cell.ts
  15. 8
      packages/nc-gui/utils/columnUtils.ts
  16. 40
      packages/nc-gui/utils/filterUtils.ts
  17. 16
      packages/nocodb-sdk/src/lib/UITypes.ts
  18. 3
      packages/nocodb-sdk/src/lib/formulaHelpers.ts
  19. 1
      packages/nocodb-sdk/src/lib/index.ts
  20. 46
      packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts
  21. 46
      packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts
  22. 55
      packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts
  23. 55
      packages/nocodb-sdk/src/lib/sqlUi/SnowflakeUi.ts
  24. 50
      packages/nocodb-sdk/src/lib/sqlUi/SqliteUi.ts
  25. 194
      packages/nocodb/src/db/BaseModelSqlv2.ts
  26. 4
      packages/nocodb/src/db/conditionV2.ts
  27. 2
      packages/nocodb/src/db/formulav2/formulaQueryBuilderv2.ts
  28. 4
      packages/nocodb/src/db/sortV2.ts
  29. 2
      packages/nocodb/src/helpers/getAst.ts
  30. 4
      packages/nocodb/src/modules/datas/helpers.ts
  31. 2
      packages/nocodb/src/modules/jobs/jobs/export-import/export.service.ts
  32. 6
      packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts
  33. 8
      packages/nocodb/src/schema/swagger-v2.json
  34. 8
      packages/nocodb/src/schema/swagger.json
  35. 4
      packages/nocodb/src/services/api-docs/swagger/getSwaggerColumnMetas.ts
  36. 4
      packages/nocodb/src/services/api-docs/swaggerV2/getSwaggerColumnMetas.ts
  37. 45
      packages/nocodb/src/services/columns.service.ts
  38. 46
      packages/nocodb/src/services/tables.service.ts
  39. 2
      packages/nocodb/src/version-upgrader/NcUpgrader.ts
  40. 125
      packages/nocodb/src/version-upgrader/ncXcdbCreatedAndUpdatedByUpgrader.ts

22
packages/nc-gui/components/cell/ReadOnlyUser.vue

@ -0,0 +1,22 @@
<script setup lang="ts">
import { ActiveCellInj, EditModeInj, ReadonlyInj, provide, ref } from '#imports'
interface Props {
modelValue?: string | null
}
defineProps<Props>()
provide(ReadonlyInj, ref(true))
provide(EditModeInj, ref(true))
provide(ActiveCellInj, ref(true))
</script>
<template>
<div class="relative">
<LazyCellUser class="z-0" :model-value="modelValue" />
<div class="w-full h-full z-1 absolute top-0 left-0"></div>
</div>
</template>

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

@ -43,6 +43,8 @@ const hiddenColTypes = [
UITypes.SpecificDBType, UITypes.SpecificDBType,
UITypes.CreatedTime, UITypes.CreatedTime,
UITypes.LastModifiedTime, UITypes.LastModifiedTime,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
] ]
const { isMobileMode, user } = useGlobal() const { isMobileMode, user } = useGlobal()

3
packages/nc-gui/components/smartsheet/VirtualCell.vue

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk' import type { ColumnType } from 'nocodb-sdk'
import { isCreatedOrLastModifiedTimeCol } from 'nocodb-sdk' import { isCreatedOrLastModifiedByCol, isCreatedOrLastModifiedTimeCol } from 'nocodb-sdk'
import { import {
ActiveCellInj, ActiveCellInj,
CellValueInj, CellValueInj,
@ -118,6 +118,7 @@ onUnmounted(() => {
<LazyVirtualCellCount v-else-if="isCount(column)" /> <LazyVirtualCellCount v-else-if="isCount(column)" />
<LazyVirtualCellLookup v-else-if="isLookup(column)" /> <LazyVirtualCellLookup v-else-if="isLookup(column)" />
<LazyCellReadOnlyDateTimePicker v-else-if="isCreatedOrLastModifiedTimeCol(column)" :model-value="modelValue" /> <LazyCellReadOnlyDateTimePicker v-else-if="isCreatedOrLastModifiedTimeCol(column)" :model-value="modelValue" />
<LazyCellReadOnlyUser v-else-if="isCreatedOrLastModifiedByCol(column)" :model-value="modelValue" />
</template> </template>
</div> </div>
</template> </template>

2
packages/nc-gui/components/smartsheet/column/EditOrAdd.vue

@ -86,6 +86,8 @@ const onlyNameUpdateOnEditColumns = [
UITypes.Links, UITypes.Links,
UITypes.CreatedTime, UITypes.CreatedTime,
UITypes.LastModifiedTime, UITypes.LastModifiedTime,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
] ]
// To close column type dropdown on escape and // To close column type dropdown on escape and

2
packages/nc-gui/components/smartsheet/grid/GroupBy.vue

@ -192,6 +192,8 @@ const shouldRenderCell = (column) =>
UITypes.DateTime, UITypes.DateTime,
UITypes.CreatedTime, UITypes.CreatedTime,
UITypes.LastModifiedTime, UITypes.LastModifiedTime,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
].includes(column?.uidt) ].includes(column?.uidt)
</script> </script>

16
packages/nc-gui/components/smartsheet/grid/Table.vue

@ -2,7 +2,15 @@
import axios from 'axios' import axios from 'axios'
import { nextTick } from '@vue/runtime-core' import { nextTick } from '@vue/runtime-core'
import { type ColumnReqType, type ColumnType, type PaginatedType, type TableType, type ViewType } from 'nocodb-sdk' import { type ColumnReqType, type ColumnType, type PaginatedType, type TableType, type ViewType } from 'nocodb-sdk'
import { UITypes, ViewTypes, isCreatedOrLastModifiedTimeCol, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import {
UITypes,
ViewTypes,
isCreatedOrLastModifiedByCol,
isCreatedOrLastModifiedTimeCol,
isLinksOrLTAR,
isSystemColumn,
isVirtualCol,
} from 'nocodb-sdk'
import { useColumnDrag } from './useColumnDrag' import { useColumnDrag } from './useColumnDrag'
import usePaginationShortcuts from './usePaginationShortcuts' import usePaginationShortcuts from './usePaginationShortcuts'
@ -1011,7 +1019,8 @@ const showFillHandle = computed(
isLookup(fields.value[activeCell.col]) || isLookup(fields.value[activeCell.col]) ||
isRollup(fields.value[activeCell.col]) || isRollup(fields.value[activeCell.col]) ||
isFormula(fields.value[activeCell.col]) || isFormula(fields.value[activeCell.col]) ||
isCreatedOrLastModifiedTimeCol(fields.value[activeCell.col]) isCreatedOrLastModifiedTimeCol(fields.value[activeCell.col]) ||
isCreatedOrLastModifiedByCol(fields.value[activeCell.col])
), ),
) )
@ -1574,7 +1583,8 @@ onKeyStroke('ArrowDown', onDown)
(isLookup(columnObj) || (isLookup(columnObj) ||
isRollup(columnObj) || isRollup(columnObj) ||
isFormula(columnObj) || isFormula(columnObj) ||
isCreatedOrLastModifiedTimeCol(columnObj)) && isCreatedOrLastModifiedTimeCol(columnObj) ||
isCreatedOrLastModifiedByCol(columnObj)) &&
hasEditPermission && hasEditPermission &&
isCellSelected(rowIndex, colIndex), isCellSelected(rowIndex, colIndex),
'!border-r-blue-400 !border-r-3': toBeDroppedColId === columnObj.id, '!border-r-blue-400 !border-r-3': toBeDroppedColId === columnObj.id,

3
packages/nc-gui/components/smartsheet/header/VirtualCellIcon.ts

@ -65,6 +65,9 @@ const renderIcon = (column: ColumnType, relationColumn?: ColumnType) => {
case UITypes.CreatedTime: case UITypes.CreatedTime:
case UITypes.LastModifiedTime: case UITypes.LastModifiedTime:
return { icon: iconMap.datetime, color: 'text-grey' } return { icon: iconMap.datetime, color: 'text-grey' }
case UITypes.CreatedBy:
case UITypes.LastModifiedBy:
return { icon: iconMap.account, color: 'text-grey' }
} }
return { icon: iconMap.generic, color: 'text-grey' } return { icon: iconMap.generic, color: 'text-grey' }

2
packages/nc-gui/components/template/Editor.vue

@ -105,6 +105,8 @@ const uiTypeOptions = ref<Option[]>(
UITypes.ID, UITypes.ID,
UITypes.CreatedTime, UITypes.CreatedTime,
UITypes.LastModifiedTime, UITypes.LastModifiedTime,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
UITypes.Barcode, UITypes.Barcode,
UITypes.Button, UITypes.Button,
].includes(UITypes[uiType]), ].includes(UITypes[uiType]),

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

@ -171,6 +171,8 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activ
UITypes.MultiSelect, UITypes.MultiSelect,
UITypes.SingleSelect, UITypes.SingleSelect,
UITypes.User, UITypes.User,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
].includes(lookupColumn.uidt), ].includes(lookupColumn.uidt),
'min-h-0 min-w-0': isAttachment(lookupColumn), 'min-h-0 min-w-0': isAttachment(lookupColumn),
}" }"

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

@ -244,6 +244,7 @@ export function useData(args: {
col.uidt === UITypes.Checkbox || col.uidt === UITypes.Checkbox ||
col.uidt === UITypes.User || col.uidt === UITypes.User ||
col.uidt === UITypes.LastModifiedTime || col.uidt === UITypes.LastModifiedTime ||
col.uidt === UITypes.LastModifiedBy ||
col.uidt === UITypes.Lookup || col.uidt === UITypes.Lookup ||
col.au || col.au ||
(col.cdf && / on update /i.test(col.cdf))) (col.cdf && / on update /i.test(col.cdf)))
@ -393,6 +394,7 @@ export function useData(args: {
UITypes.Lookup, UITypes.Lookup,
UITypes.Rollup, UITypes.Rollup,
UITypes.LinkToAnotherRecord, UITypes.LinkToAnotherRecord,
UITypes.LastModifiedBy,
].includes(col.uidt), ].includes(col.uidt),
) )
) { ) {

4
packages/nc-gui/composables/useMultiSelect/convertCellData.ts

@ -195,7 +195,9 @@ export default function convertCellData(
return validVals.join(',') return validVals.join(',')
} }
case UITypes.User: { case UITypes.User:
case UITypes.CreatedBy:
case UITypes.LastModifiedBy: {
let parsedVal let parsedVal
try { try {
try { try {

2
packages/nc-gui/composables/useMultiSelect/index.ts

@ -112,7 +112,7 @@ export function useMultiSelect(
textToCopy = !!textToCopy textToCopy = !!textToCopy
} }
if (columnObj.uidt === UITypes.User) { if (columnObj.uidt === UITypes.User || columnObj.uidt === UITypes.CreatedBy || columnObj.uidt === UITypes.LastModifiedBy) {
if (textToCopy && Array.isArray(textToCopy)) { if (textToCopy && Array.isArray(textToCopy)) {
textToCopy = textToCopy textToCopy = textToCopy
.map((user: UserFieldRecordType) => { .map((user: UserFieldRecordType) => {

8
packages/nc-gui/composables/useViewGroupBy.ts

@ -90,7 +90,7 @@ export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: Computed
return value ? GROUP_BY_VARS.TRUE : GROUP_BY_VARS.FALSE return value ? GROUP_BY_VARS.TRUE : GROUP_BY_VARS.FALSE
} }
if (col.uidt === UITypes.User) { if (col.uidt === UITypes.User || col.uidt === UITypes.CreatedBy || col.uidt === UITypes.LastModifiedBy) {
if (!value) { if (!value) {
return GROUP_BY_VARS.NULL return GROUP_BY_VARS.NULL
} }
@ -161,7 +161,11 @@ export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: Computed
acc += `${acc.length ? '~and' : ''}(${curr.title},${curr.key === GROUP_BY_VARS.TRUE ? 'checked' : 'notchecked'})` acc += `${acc.length ? '~and' : ''}(${curr.title},${curr.key === GROUP_BY_VARS.TRUE ? 'checked' : 'notchecked'})`
} else if ([UITypes.Date, UITypes.DateTime].includes(curr.column_uidt as UITypes)) { } else if ([UITypes.Date, UITypes.DateTime].includes(curr.column_uidt as UITypes)) {
acc += `${acc.length ? '~and' : ''}(${curr.title},eq,exactDate,${curr.key})` acc += `${acc.length ? '~and' : ''}(${curr.title},eq,exactDate,${curr.key})`
} else if (curr.column_uidt === UITypes.User) { } else if (
curr.column_uidt === UITypes.User ||
curr.column_uidt === UITypes.CreatedBy ||
curr.column_uidt === UITypes.LastModifiedBy
) {
try { try {
const value = JSON.parse(curr.key) const value = JSON.parse(curr.key)
acc += `${acc.length ? '~and' : ''}(${curr.title},gb_eq,${value.map((v: any) => v.id).join(',')})` acc += `${acc.length ? '~and' : ''}(${curr.title},gb_eq,${value.map((v: any) => v.id).join(',')})`

2
packages/nc-gui/utils/cell.ts

@ -17,6 +17,8 @@ export const isDateTime = (column: ColumnType, abstractType: any) =>
abstractType === 'datetime' || column.uidt === UITypes.DateTime abstractType === 'datetime' || column.uidt === UITypes.DateTime
export const isReadonlyDateTime = (column: ColumnType, _abstractType: any) => export const isReadonlyDateTime = (column: ColumnType, _abstractType: any) =>
column.uidt === UITypes.CreatedTime || column.uidt === UITypes.LastModifiedTime column.uidt === UITypes.CreatedTime || column.uidt === UITypes.LastModifiedTime
export const isReadonlyUser = (column: ColumnType, _abstractType: any) =>
column.uidt === UITypes.CreatedBy || column.uidt === UITypes.LastModifiedBy
export const isJSON = (column: ColumnType) => column.uidt === UITypes.JSON export const isJSON = (column: ColumnType) => column.uidt === UITypes.JSON
export const isEnum = (column: ColumnType) => column.uidt === UITypes.SingleSelect export const isEnum = (column: ColumnType) => column.uidt === UITypes.SingleSelect
export const isSingleSelect = (column: ColumnType) => column.uidt === UITypes.SingleSelect export const isSingleSelect = (column: ColumnType) => column.uidt === UITypes.SingleSelect

8
packages/nc-gui/utils/columnUtils.ts

@ -146,6 +146,14 @@ const uiTypes = [
name: UITypes.LastModifiedTime, name: UITypes.LastModifiedTime,
icon: iconMap.datetime, icon: iconMap.datetime,
}, },
{
name: UITypes.CreatedBy,
icon: iconMap.account,
},
{
name: UITypes.LastModifiedBy,
icon: iconMap.account,
},
] ]
const getUIDTIcon = (uidt: UITypes | string) => { const getUIDTIcon = (uidt: UITypes | string) => {

40
packages/nc-gui/utils/filterUtils.ts

@ -95,13 +95,27 @@ export const comparisonOpList = (
text: getEqText(fieldUiType), text: getEqText(fieldUiType),
value: 'eq', value: 'eq',
ignoreVal: false, ignoreVal: false,
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.Attachment, UITypes.User], excludedTypes: [
UITypes.Checkbox,
UITypes.MultiSelect,
UITypes.Attachment,
UITypes.User,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
],
}, },
{ {
text: getNeqText(fieldUiType), text: getNeqText(fieldUiType),
value: 'neq', value: 'neq',
ignoreVal: false, ignoreVal: false,
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.Attachment, UITypes.User], excludedTypes: [
UITypes.Checkbox,
UITypes.MultiSelect,
UITypes.Attachment,
UITypes.User,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
],
}, },
{ {
text: getLikeText(fieldUiType), text: getLikeText(fieldUiType),
@ -112,6 +126,8 @@ export const comparisonOpList = (
UITypes.SingleSelect, UITypes.SingleSelect,
UITypes.MultiSelect, UITypes.MultiSelect,
UITypes.User, UITypes.User,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
UITypes.Collaborator, UITypes.Collaborator,
UITypes.Date, UITypes.Date,
UITypes.DateTime, UITypes.DateTime,
@ -128,6 +144,8 @@ export const comparisonOpList = (
UITypes.SingleSelect, UITypes.SingleSelect,
UITypes.MultiSelect, UITypes.MultiSelect,
UITypes.User, UITypes.User,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
UITypes.Collaborator, UITypes.Collaborator,
UITypes.Date, UITypes.Date,
UITypes.DateTime, UITypes.DateTime,
@ -144,6 +162,8 @@ export const comparisonOpList = (
UITypes.SingleSelect, UITypes.SingleSelect,
UITypes.MultiSelect, UITypes.MultiSelect,
UITypes.User, UITypes.User,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
UITypes.Collaborator, UITypes.Collaborator,
UITypes.Attachment, UITypes.Attachment,
UITypes.LinkToAnotherRecord, UITypes.LinkToAnotherRecord,
@ -163,6 +183,8 @@ export const comparisonOpList = (
UITypes.SingleSelect, UITypes.SingleSelect,
UITypes.MultiSelect, UITypes.MultiSelect,
UITypes.User, UITypes.User,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
UITypes.Collaborator, UITypes.Collaborator,
UITypes.Attachment, UITypes.Attachment,
UITypes.LinkToAnotherRecord, UITypes.LinkToAnotherRecord,
@ -183,6 +205,8 @@ export const comparisonOpList = (
UITypes.SingleSelect, UITypes.SingleSelect,
UITypes.MultiSelect, UITypes.MultiSelect,
UITypes.User, UITypes.User,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
UITypes.Collaborator, UITypes.Collaborator,
UITypes.Attachment, UITypes.Attachment,
UITypes.LinkToAnotherRecord, UITypes.LinkToAnotherRecord,
@ -202,6 +226,8 @@ export const comparisonOpList = (
UITypes.SingleSelect, UITypes.SingleSelect,
UITypes.MultiSelect, UITypes.MultiSelect,
UITypes.User, UITypes.User,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
UITypes.Collaborator, UITypes.Collaborator,
UITypes.Attachment, UITypes.Attachment,
UITypes.LinkToAnotherRecord, UITypes.LinkToAnotherRecord,
@ -215,25 +241,25 @@ export const comparisonOpList = (
text: 'contains all of', text: 'contains all of',
value: 'allof', value: 'allof',
ignoreVal: false, ignoreVal: false,
includedTypes: [UITypes.MultiSelect, UITypes.User], includedTypes: [UITypes.MultiSelect, UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy],
}, },
{ {
text: 'contains any of', text: 'contains any of',
value: 'anyof', value: 'anyof',
ignoreVal: false, ignoreVal: false,
includedTypes: [UITypes.MultiSelect, UITypes.SingleSelect, UITypes.User], includedTypes: [UITypes.MultiSelect, UITypes.SingleSelect, UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy],
}, },
{ {
text: 'does not contain all of', text: 'does not contain all of',
value: 'nallof', value: 'nallof',
ignoreVal: false, ignoreVal: false,
includedTypes: [UITypes.MultiSelect, UITypes.User], includedTypes: [UITypes.MultiSelect, UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy],
}, },
{ {
text: 'does not contain any of', text: 'does not contain any of',
value: 'nanyof', value: 'nanyof',
ignoreVal: false, ignoreVal: false,
includedTypes: [UITypes.MultiSelect, UITypes.SingleSelect, UITypes.User], includedTypes: [UITypes.MultiSelect, UITypes.SingleSelect, UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy],
}, },
{ {
text: getGtText(fieldUiType), text: getGtText(fieldUiType),
@ -382,7 +408,7 @@ export const comparisonSubOpList = (
text: 'yesterday', text: 'yesterday',
value: 'yesterday', value: 'yesterday',
ignoreVal: true, ignoreVal: true,
includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTim, UITypes.LastModifiedTime, UITypes.CreatedTimee])], includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTime, UITypes.LastModifiedTime, UITypes.CreatedTime])],
}, },
{ {
text: 'one week ago', text: 'one week ago',

16
packages/nocodb-sdk/src/lib/UITypes.ts

@ -40,6 +40,8 @@ enum UITypes {
Button = 'Button', Button = 'Button',
Links = 'Links', Links = 'Links',
User = 'User', User = 'User',
CreatedBy = 'CreatedBy',
LastModifiedBy = 'LastModifiedBy',
} }
export const numericUITypes = [ export const numericUITypes = [
@ -85,6 +87,8 @@ export function isVirtualCol(
UITypes.Links, UITypes.Links,
UITypes.CreatedTime, UITypes.CreatedTime,
UITypes.LastModifiedTime, UITypes.LastModifiedTime,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
// UITypes.Count, // UITypes.Count,
].includes(<UITypes>(typeof col === 'object' ? col?.uidt : col)); ].includes(<UITypes>(typeof col === 'object' ? col?.uidt : col));
} }
@ -100,6 +104,18 @@ export function isCreatedOrLastModifiedTimeCol(
); );
} }
export function isCreatedOrLastModifiedByCol(
col:
| UITypes
| { readonly uidt: UITypes | string }
| ColumnReqType
| ColumnType
) {
return [UITypes.CreatedBy, UITypes.LastModifiedBy].includes(
<UITypes>(typeof col === 'object' ? col?.uidt : col)
);
}
export function isLinksOrLTAR( export function isLinksOrLTAR(
colOrUidt: ColumnType | { uidt: UITypes | string } | UITypes | string colOrUidt: ColumnType | { uidt: UITypes | string } | UITypes | string
) { ) {

3
packages/nocodb-sdk/src/lib/formulaHelpers.ts

@ -1514,6 +1514,9 @@ async function extractColumnIdentifierType({
case UITypes.PhoneNumber: case UITypes.PhoneNumber:
case UITypes.Email: case UITypes.Email:
case UITypes.URL: case UITypes.URL:
case UITypes.User:
case UITypes.CreatedBy:
case UITypes.LastModifiedBy:
res.dataType = FormulaDataTypes.STRING; res.dataType = FormulaDataTypes.STRING;
break; break;
// numeric // numeric

1
packages/nocodb-sdk/src/lib/index.ts

@ -14,6 +14,7 @@ export {
isVirtualCol, isVirtualCol,
isLinksOrLTAR, isLinksOrLTAR,
isCreatedOrLastModifiedTimeCol, isCreatedOrLastModifiedTimeCol,
isCreatedOrLastModifiedByCol,
} from '~/lib/UITypes'; } from '~/lib/UITypes';
export { default as CustomAPI, FileType } from '~/lib/CustomAPI'; export { default as CustomAPI, FileType } from '~/lib/CustomAPI';
export { default as TemplateGenerator } from '~/lib/TemplateGenerator'; export { default as TemplateGenerator } from '~/lib/TemplateGenerator';

46
packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts

@ -135,6 +135,52 @@ export class MssqlUi {
uicn: '', uicn: '',
system: true, system: true,
}, },
{
column_name: 'created_by',
title: 'CreatedBy',
dt: 'varchar',
dtx: 'specificType',
ct: 'varchar(45)',
nrqd: true,
rqd: false,
ck: false,
pk: false,
un: false,
ai: false,
clen: 45,
np: null,
ns: null,
dtxp: '',
dtxs: '',
altered: 1,
uidt: UITypes.CreatedBy,
uip: '',
uicn: '',
system: true,
},
{
column_name: 'updated_by',
title: 'UpdatedBy',
dt: 'varchar',
dtx: 'specificType',
ct: 'varchar(45)',
nrqd: true,
rqd: false,
ck: false,
pk: false,
un: false,
ai: false,
clen: 45,
np: null,
ns: null,
dtxp: '',
dtxs: '',
altered: 1,
uidt: UITypes.LastModifiedBy,
uip: '',
uicn: '',
system: true,
},
]; ];
} }

46
packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts

@ -139,6 +139,52 @@ export class MysqlUi {
uicn: '', uicn: '',
system: true, system: true,
}, },
{
column_name: 'created_by',
title: 'CreatedBy',
dt: 'varchar',
dtx: 'specificType',
ct: 'varchar(45)',
nrqd: true,
rqd: false,
ck: false,
pk: false,
un: false,
ai: false,
clen: 45,
np: null,
ns: null,
dtxp: '',
dtxs: '',
altered: 1,
uidt: UITypes.CreatedBy,
uip: '',
uicn: '',
system: true,
},
{
column_name: 'updated_by',
title: 'UpdatedBy',
dt: 'varchar',
dtx: 'specificType',
ct: 'varchar(45)',
nrqd: true,
rqd: false,
ck: false,
pk: false,
un: false,
ai: false,
clen: 45,
np: null,
ns: null,
dtxp: '',
dtxs: '',
altered: 1,
uidt: UITypes.LastModifiedBy,
uip: '',
uicn: '',
system: true,
},
]; ];
} }

55
packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts

@ -197,6 +197,52 @@ export class PgUi {
uicn: '', uicn: '',
system: true, system: true,
}, },
{
column_name: 'created_by',
title: 'CreatedBy',
dt: 'varchar',
dtx: 'specificType',
ct: 'varchar(45)',
nrqd: true,
rqd: false,
ck: false,
pk: false,
un: false,
ai: false,
clen: 45,
np: null,
ns: null,
dtxp: '',
dtxs: '',
altered: 1,
uidt: UITypes.CreatedBy,
uip: '',
uicn: '',
system: true,
},
{
column_name: 'updated_by',
title: 'UpdatedBy',
dt: 'varchar',
dtx: 'specificType',
ct: 'varchar(45)',
nrqd: true,
rqd: false,
ck: false,
pk: false,
un: false,
ai: false,
clen: 45,
np: null,
ns: null,
dtxp: '',
dtxs: '',
altered: 1,
uidt: UITypes.LastModifiedBy,
uip: '',
uicn: '',
system: true,
},
]; ];
} }
@ -1568,7 +1614,7 @@ export class PgUi {
} }
} }
static getDataTypeForUiType(col: { uidt: UITypes }, idType?: IDType) { static getDataTypeForUiType(col: { uidt: UITypes; }, idType?: IDType) {
const colProp: any = {}; const colProp: any = {};
switch (col.uidt) { switch (col.uidt) {
case 'ID': case 'ID':
@ -1709,7 +1755,7 @@ export class PgUi {
return colProp; return colProp;
} }
static getDataTypeListForUiType(col: { uidt?: UITypes }, idType: IDType) { static getDataTypeListForUiType(col: { uidt?: UITypes; }, idType: IDType) {
switch (col.uidt) { switch (col.uidt) {
case 'ID': case 'ID':
if (idType === 'AG') { if (idType === 'AG') {
@ -1941,6 +1987,11 @@ export class PgUi {
'timestamp with time zone', 'timestamp with time zone',
]; ];
case 'User':
case 'CreatedBy':
case 'LastModifiedBy':
return ['character varying'];
case 'AutoNumber': case 'AutoNumber':
return [ return [
'int', 'int',

55
packages/nocodb-sdk/src/lib/sqlUi/SnowflakeUi.ts

@ -133,6 +133,52 @@ export class SnowflakeUi {
uicn: '', uicn: '',
system: true, system: true,
}, },
{
column_name: 'created_by',
title: 'CreatedBy',
dt: 'varchar',
dtx: 'specificType',
ct: 'varchar(45)',
nrqd: true,
rqd: false,
ck: false,
pk: false,
un: false,
ai: false,
clen: 45,
np: null,
ns: null,
dtxp: '',
dtxs: '',
altered: 1,
uidt: UITypes.CreatedBy,
uip: '',
uicn: '',
system: true,
},
{
column_name: 'updated_by',
title: 'UpdatedBy',
dt: 'varchar',
dtx: 'specificType',
ct: 'varchar(45)',
nrqd: true,
rqd: false,
ck: false,
pk: false,
un: false,
ai: false,
clen: 45,
np: null,
ns: null,
dtxp: '',
dtxs: '',
altered: 1,
uidt: UITypes.LastModifiedBy,
uip: '',
uicn: '',
system: true,
},
]; ];
} }
@ -660,7 +706,7 @@ export class SnowflakeUi {
} }
} }
static getDataTypeForUiType(col: { uidt: UITypes }, idType?: IDType) { static getDataTypeForUiType(col: { uidt: UITypes; }, idType?: IDType) {
const colProp: any = {}; const colProp: any = {};
switch (col.uidt) { switch (col.uidt) {
case 'ID': case 'ID':
@ -800,7 +846,7 @@ export class SnowflakeUi {
return colProp; return colProp;
} }
static getDataTypeListForUiType(col: { uidt: UITypes }, idType: IDType) { static getDataTypeListForUiType(col: { uidt: UITypes; }, idType: IDType) {
switch (col.uidt) { switch (col.uidt) {
case 'ID': case 'ID':
if (idType === 'AG') { if (idType === 'AG') {
@ -955,6 +1001,11 @@ export class SnowflakeUi {
case 'LastModifiedTime': case 'LastModifiedTime':
return ['TIMESTAMP']; return ['TIMESTAMP'];
case 'User':
case 'CreatedBy':
case 'LastModifiedBy':
return ['VARCHAR'];
case 'AutoNumber': case 'AutoNumber':
return ['NUMBER', 'INT', 'INTEGER', 'BIGINT']; return ['NUMBER', 'INT', 'INTEGER', 'BIGINT'];

50
packages/nocodb-sdk/src/lib/sqlUi/SqliteUi.ts

@ -121,6 +121,52 @@ export class SqliteUi {
uicn: '', uicn: '',
system: true, system: true,
}, },
{
column_name: 'created_by',
title: 'CreatedBy',
dt: 'varchar',
dtx: 'specificType',
ct: 'varchar',
nrqd: true,
rqd: false,
ck: false,
pk: false,
un: false,
ai: false,
clen: 45,
np: null,
ns: null,
dtxp: '',
dtxs: '',
altered: 1,
uidt: UITypes.CreatedBy,
uip: '',
uicn: '',
system: true,
},
{
column_name: 'updated_by',
title: 'UpdatedBy',
dt: 'varchar',
dtx: 'specificType',
ct: 'varchar',
nrqd: true,
rqd: false,
ck: false,
pk: false,
un: false,
ai: false,
clen: 45,
np: null,
ns: null,
dtxp: '',
dtxs: '',
altered: 1,
uidt: UITypes.LastModifiedBy,
uip: '',
uicn: '',
system: true,
},
]; ];
} }
@ -510,7 +556,7 @@ export class SqliteUi {
} }
} }
static getDataTypeForUiType(col: { uidt: UITypes }, idType?: IDType) { static getDataTypeForUiType(col: { uidt: UITypes; }, idType?: IDType) {
const colProp: any = {}; const colProp: any = {};
switch (col.uidt) { switch (col.uidt) {
case 'ID': case 'ID':
@ -651,7 +697,7 @@ export class SqliteUi {
return colProp; return colProp;
} }
static getDataTypeListForUiType(col: { uidt: UITypes }, idType?: IDType) { static getDataTypeListForUiType(col: { uidt: UITypes; }, idType?: IDType) {
switch (col.uidt) { switch (col.uidt) {
case 'ID': case 'ID':
if (idType === 'AG') { if (idType === 'AG') {

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

@ -8,6 +8,7 @@ import { nocoExecute } from 'nc-help';
import { import {
AuditOperationSubTypes, AuditOperationSubTypes,
AuditOperationTypes, AuditOperationTypes,
isCreatedOrLastModifiedByCol,
isCreatedOrLastModifiedTimeCol, isCreatedOrLastModifiedTimeCol,
isLinksOrLTAR, isLinksOrLTAR,
isSystemColumn, isSystemColumn,
@ -103,7 +104,11 @@ function checkColumnRequired(
} }
export async function getColumnName(column: Column<any>, columns?: Column[]) { export async function getColumnName(column: Column<any>, columns?: Column[]) {
if (!isCreatedOrLastModifiedTimeCol(column)) return column.column_name; if (
!isCreatedOrLastModifiedTimeCol(column) &&
!isCreatedOrLastModifiedByCol(column)
)
return column.column_name;
columns = columns || (await Column.list({ fk_model_id: column.fk_model_id })); columns = columns || (await Column.list({ fk_model_id: column.fk_model_id }));
switch (column.uidt) { switch (column.uidt) {
@ -122,6 +127,20 @@ export async function getColumnName(column: Column<any>, columns?: Column[]) {
return lastModifiedTimeSystemCol.column_name; return lastModifiedTimeSystemCol.column_name;
return column.column_name || 'updated_at'; return column.column_name || 'updated_at';
} }
case UITypes.CreatedBy: {
const createdBySystemCol = columns.find(
(col) => col.system && col.uidt === UITypes.CreatedBy,
);
if (createdBySystemCol) return createdBySystemCol.column_name;
return column.column_name || 'created_by';
}
case UITypes.LastModifiedBy: {
const lastModifiedBySystemCol = columns.find(
(col) => col.system && col.uidt === UITypes.LastModifiedBy,
);
if (lastModifiedBySystemCol) return lastModifiedBySystemCol.column_name;
return column.column_name || 'updated_by';
}
default: default:
return column.column_name; return column.column_name;
} }
@ -739,7 +758,16 @@ class BaseModelSqlv2 {
const column = groupByColumns[sort.fk_column_id]; const column = groupByColumns[sort.fk_column_id];
if (column.uidt === UITypes.User) { if (
[UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(
column.uidt as UITypes,
)
) {
const columnName = await getColumnName(
column,
await this.model.getColumns(),
);
const baseUsers = await BaseUser.getUsersList({ const baseUsers = await BaseUser.getUsersList({
base_id: column.base_id, base_id: column.base_id,
}); });
@ -751,7 +779,7 @@ class BaseModelSqlv2 {
user.display_name || user.email, user.display_name || user.email,
]); ]);
return qb.toQuery(); return qb.toQuery();
}, this.dbDriver.raw(`??`, [column.column_name]).toQuery()); }, this.dbDriver.raw(`??`, [columnName]).toQuery());
qb.orderBy( qb.orderBy(
sanitize(this.dbDriver.raw(finalStatement)), sanitize(this.dbDriver.raw(finalStatement)),
@ -2314,6 +2342,18 @@ class BaseModelSqlv2 {
).builder.as(sanitize(column.id)), ).builder.as(sanitize(column.id)),
); );
break; break;
case UITypes.CreatedBy:
case UITypes.LastModifiedBy: {
const columnName = await getColumnName(
column,
await this.model.getColumns(),
);
res[sanitize(column.id || columnName)] = sanitize(
`${alias || this.tnPath}.${columnName}`,
);
break;
}
default: default:
if (this.isPg) { if (this.isPg) {
if (column.dt === 'bytea') { if (column.dt === 'bytea') {
@ -2368,7 +2408,7 @@ class BaseModelSqlv2 {
await this.beforeInsert(insertObj, trx, cookie); await this.beforeInsert(insertObj, trx, cookie);
} }
await this.prepareNocoData(insertObj, true); await this.prepareNocoData(insertObj, true, cookie);
let response; let response;
// const driver = trx ? trx : this.dbDriver; // const driver = trx ? trx : this.dbDriver;
@ -2613,7 +2653,7 @@ class BaseModelSqlv2 {
await this.beforeUpdate(data, trx, cookie); await this.beforeUpdate(data, trx, cookie);
await this.prepareNocoData(updateObj); await this.prepareNocoData(updateObj, false, cookie);
const prevData = await this.readByPk( const prevData = await this.readByPk(
id, id,
@ -2734,7 +2774,7 @@ class BaseModelSqlv2 {
await this.beforeInsert(insertObj, this.dbDriver, cookie); await this.beforeInsert(insertObj, this.dbDriver, cookie);
await this.prepareNocoData(insertObj, true); await this.prepareNocoData(insertObj, true, cookie);
let response; let response;
const query = this.dbDriver(this.tnPath).insert(insertObj); const query = this.dbDriver(this.tnPath).insert(insertObj);
@ -2962,7 +3002,11 @@ class BaseModelSqlv2 {
for (let i = 0; i < this.model.columns.length; ++i) { for (let i = 0; i < this.model.columns.length; ++i) {
const col = this.model.columns[i]; const col = this.model.columns[i];
if (col.title in d && isCreatedOrLastModifiedTimeCol(col)) { if (
col.title in d &&
(isCreatedOrLastModifiedTimeCol(col) ||
isCreatedOrLastModifiedByCol(col))
) {
NcError.badRequest( NcError.badRequest(
`Column "${col.title}" is auto generated and cannot be updated`, `Column "${col.title}" is auto generated and cannot be updated`,
); );
@ -3085,7 +3129,7 @@ class BaseModelSqlv2 {
} }
} }
await this.prepareNocoData(insertObj, true); await this.prepareNocoData(insertObj, true, cookie);
// prepare nested link data for insert only if it is single record insertion // prepare nested link data for insert only if it is single record insertion
if (isSingleRecordInsertion) { if (isSingleRecordInsertion) {
@ -3229,7 +3273,7 @@ class BaseModelSqlv2 {
continue; continue;
} }
if (!raw) { if (!raw) {
await this.prepareNocoData(d); await this.prepareNocoData(d, false, cookie);
const oldRecord = await this.readByPk(pkValues); const oldRecord = await this.readByPk(pkValues);
if (!oldRecord) { if (!oldRecord) {
@ -3847,7 +3891,11 @@ class BaseModelSqlv2 {
for (let i = 0; i < this.model.columns.length; ++i) { for (let i = 0; i < this.model.columns.length; ++i) {
const column = this.model.columns[i]; const column = this.model.columns[i];
if (column.title in data && isCreatedOrLastModifiedTimeCol(column)) { if (
column.title in data &&
(isCreatedOrLastModifiedTimeCol(column) ||
isCreatedOrLastModifiedByCol(column))
) {
NcError.badRequest( NcError.badRequest(
`Column "${column.title}" is auto generated and cannot be updated`, `Column "${column.title}" is auto generated and cannot be updated`,
); );
@ -4018,13 +4066,15 @@ class BaseModelSqlv2 {
); );
} }
await this.updateLastModifiedTime({ await this.updateLastModified({
model: parentTable, model: parentTable,
rowIds: [childId], rowIds: [childId],
cookie,
}); });
await this.updateLastModifiedTime({ await this.updateLastModified({
model: childTable, model: childTable,
rowIds: [rowId], rowIds: [rowId],
cookie,
}); });
} }
break; break;
@ -4046,9 +4096,10 @@ class BaseModelSqlv2 {
{ raw: true }, { raw: true },
); );
await this.updateLastModifiedTime({ await this.updateLastModified({
model: parentTable, model: parentTable,
rowIds: [rowId], rowIds: [rowId],
cookie,
}); });
} }
break; break;
@ -4070,9 +4121,10 @@ class BaseModelSqlv2 {
{ raw: true }, { raw: true },
); );
await this.updateLastModifiedTime({ await this.updateLastModified({
model: parentTable, model: parentTable,
rowIds: [childId], rowIds: [childId],
cookie,
}); });
} }
break; break;
@ -4168,13 +4220,15 @@ class BaseModelSqlv2 {
{ raw: true }, { raw: true },
); );
await this.updateLastModifiedTime({ await this.updateLastModified({
model: parentTable, model: parentTable,
rowIds: [childId], rowIds: [childId],
cookie,
}); });
await this.updateLastModifiedTime({ await this.updateLastModified({
model: childTable, model: childTable,
rowIds: [rowId], rowIds: [rowId],
cookie,
}); });
} }
break; break;
@ -4194,9 +4248,10 @@ class BaseModelSqlv2 {
{ raw: true }, { raw: true },
); );
await this.updateLastModifiedTime({ await this.updateLastModified({
model: parentTable, model: parentTable,
rowIds: [rowId], rowIds: [rowId],
cookie,
}); });
} }
break; break;
@ -4216,9 +4271,10 @@ class BaseModelSqlv2 {
{ raw: true }, { raw: true },
); );
await this.updateLastModifiedTime({ await this.updateLastModified({
model: parentTable, model: parentTable,
rowIds: [childId], rowIds: [childId],
cookie,
}); });
} }
break; break;
@ -4666,11 +4722,19 @@ class BaseModelSqlv2 {
for (const col of columns) { for (const col of columns) {
if (col.uidt === UITypes.Lookup) { if (col.uidt === UITypes.Lookup) {
if ((await this.getNestedUidt(col)) === UITypes.User) { if (
[UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(
(await this.getNestedUidt(col)) as UITypes,
)
) {
userColumns.push(col); userColumns.push(col);
} }
} else { } else {
if (col.uidt === UITypes.User) { if (
[UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(
col.uidt,
)
) {
userColumns.push(col); userColumns.push(col);
} }
} }
@ -5130,13 +5194,15 @@ class BaseModelSqlv2 {
raw: true, raw: true,
}); });
await this.updateLastModifiedTime({ await this.updateLastModified({
model: parentTable, model: parentTable,
rowIds: childIds, rowIds: childIds,
cookie,
}); });
await this.updateLastModifiedTime({ await this.updateLastModified({
model: childTable, model: childTable,
rowIds: [rowId], rowIds: [rowId],
cookie,
}); });
} }
break; break;
@ -5213,9 +5279,10 @@ class BaseModelSqlv2 {
} }
await this.execAndParse(updateQb, null, { raw: true }); await this.execAndParse(updateQb, null, { raw: true });
await this.updateLastModifiedTime({ await this.updateLastModified({
model: parentTable, model: parentTable,
rowIds: [rowId], rowIds: [rowId],
cookie,
}); });
} }
break; break;
@ -5260,9 +5327,10 @@ class BaseModelSqlv2 {
{ raw: true }, { raw: true },
); );
await this.updateLastModifiedTime({ await this.updateLastModified({
model: parentTable, model: parentTable,
rowIds: [rowId], rowIds: [rowId],
cookie,
}); });
} }
break; break;
@ -5406,13 +5474,15 @@ class BaseModelSqlv2 {
); );
await this.execAndParse(delQb, null, { raw: true }); await this.execAndParse(delQb, null, { raw: true });
await this.updateLastModifiedTime({ await this.updateLastModified({
model: parentTable, model: parentTable,
rowIds: childIds, rowIds: childIds,
cookie,
}); });
await this.updateLastModifiedTime({ await this.updateLastModified({
model: childTable, model: childTable,
rowIds: [rowId], rowIds: [rowId],
cookie,
}); });
} }
break; break;
@ -5495,9 +5565,10 @@ class BaseModelSqlv2 {
{ raw: true }, { raw: true },
); );
await this.updateLastModifiedTime({ await this.updateLastModified({
model: parentTable, model: parentTable,
rowIds: [rowId], rowIds: [rowId],
cookie,
}); });
} }
break; break;
@ -5545,9 +5616,10 @@ class BaseModelSqlv2 {
{ raw: true }, { raw: true },
); );
await this.updateLastModifiedTime({ await this.updateLastModified({
model: parentTable, model: parentTable,
rowIds: [childIds[0]], rowIds: [childIds[0]],
cookie,
}); });
} }
break; break;
@ -5634,26 +5706,40 @@ class BaseModelSqlv2 {
} }
} }
async updateLastModifiedTime({ async updateLastModified({
rowIds, rowIds,
cookie,
model = this.model, model = this.model,
knex = this.dbDriver, knex = this.dbDriver,
}: { }: {
rowIds: any | any[]; rowIds: any | any[];
cookie?: { user?: any };
model?: Model; model?: Model;
knex?: XKnex; knex?: XKnex;
}) { }) {
const columnName = await model.getColumns().then((columns) => { const columns = await model.getColumns();
return columns.find(
(c) => c.uidt === UITypes.LastModifiedTime && c.system,
)?.column_name;
});
if (!columnName) return; const updateObject = {};
const qb = knex(model.table_name).update({ const lastModifiedTimeColumn = columns.find(
[columnName]: Noco.ncMeta.now(), (c) => c.uidt === UITypes.LastModifiedTime && c.system,
}); );
const lastModifiedByColumn = columns.find(
(c) => c.uidt === UITypes.LastModifiedBy && c.system,
);
if (lastModifiedTimeColumn) {
updateObject[lastModifiedTimeColumn.column_name] = Noco.ncMeta.now();
}
if (lastModifiedByColumn) {
updateObject[lastModifiedByColumn.column_name] = cookie?.user?.id;
}
if (Object.keys(updateObject).length === 0) return;
const qb = knex(model.table_name).update(updateObject);
for (const rowId of Array.isArray(rowIds) ? rowIds : [rowIds]) { for (const rowId of Array.isArray(rowIds) ? rowIds : [rowIds]) {
qb.orWhere(await this._wherePk(rowId)); qb.orWhere(await this._wherePk(rowId));
@ -5662,7 +5748,7 @@ class BaseModelSqlv2 {
await this.execAndParse(qb, null, { raw: true }); await this.execAndParse(qb, null, { raw: true });
} }
async prepareNocoData(data, isInsertData = false) { async prepareNocoData(data, isInsertData = false, cookie?: { user?: any }) {
if ( if (
this.model.columns.some((c) => this.model.columns.some((c) =>
[ [
@ -5670,19 +5756,25 @@ class BaseModelSqlv2 {
UITypes.User, UITypes.User,
UITypes.CreatedTime, UITypes.CreatedTime,
UITypes.LastModifiedTime, UITypes.LastModifiedTime,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
].includes(c.uidt), ].includes(c.uidt),
) )
) { ) {
for (const column of this.model.columns) { for (const column of this.model.columns) {
if ( if (column.system) {
isInsertData && if (isInsertData) {
column.uidt === UITypes.CreatedTime && if (column.uidt === UITypes.CreatedTime) {
column.system data[column.column_name] = Noco.ncMeta.now();
) { } else if (column.uidt === UITypes.CreatedBy) {
data[column.column_name] = Noco.ncMeta.now(); data[column.column_name] = cookie?.user?.id;
} }
if (column.uidt === UITypes.LastModifiedTime && column.system) { }
data[column.column_name] = isInsertData ? null : Noco.ncMeta.now(); if (column.uidt === UITypes.LastModifiedTime) {
data[column.column_name] = isInsertData ? null : Noco.ncMeta.now();
} else if (column.uidt === UITypes.LastModifiedBy) {
data[column.column_name] = isInsertData ? null : cookie?.user?.id;
}
} }
if (column.uidt === UITypes.Attachment) { if (column.uidt === UITypes.Attachment) {
if (data[column.column_name]) { if (data[column.column_name]) {
@ -5699,7 +5791,11 @@ class BaseModelSqlv2 {
} }
} }
} }
} else if (column.uidt === UITypes.User) { } else if (
[UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(
column.uidt,
)
) {
if (data[column.column_name]) { if (data[column.column_name]) {
const userIds = []; const userIds = [];

4
packages/nocodb/src/db/conditionV2.ts

@ -453,7 +453,9 @@ const parseConditionV2 = async (
builder, builder,
); );
} else if ( } else if (
column.uidt === UITypes.User && [UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(
column.uidt,
) &&
['like', 'nlike'].includes(filter.comparison_op) ['like', 'nlike'].includes(filter.comparison_op)
) { ) {
const baseUsers = await BaseUser.getUsersList({ const baseUsers = await BaseUser.getUsersList({

2
packages/nocodb/src/db/formulav2/formulaQueryBuilderv2.ts

@ -695,6 +695,8 @@ async function _formulaQueryBuilder(
} }
break; break;
case UITypes.User: case UITypes.User:
case UITypes.CreatedBy:
case UITypes.LastModifiedBy:
{ {
const base = await Base.get(model.base_id); const base = await Base.get(model.base_id);
const baseUsers = await BaseUser.getUsersList({ const baseUsers = await BaseUser.getUsersList({

4
packages/nocodb/src/db/sortV2.ts

@ -139,7 +139,9 @@ export default async function sortV2(
} }
break; break;
} }
case UITypes.User: { case UITypes.User:
case UITypes.CreatedBy:
case UITypes.LastModifiedBy: {
const base = await Base.get(model.base_id); const base = await Base.get(model.base_id);
const baseUsers = await BaseUser.getUsersList({ const baseUsers = await BaseUser.getUsersList({
base_id: base.id, base_id: base.id,

2
packages/nocodb/src/helpers/getAst.ts

@ -1,4 +1,5 @@
import { import {
isCreatedOrLastModifiedByCol,
isCreatedOrLastModifiedTimeCol, isCreatedOrLastModifiedTimeCol,
isSystemColumn, isSystemColumn,
RelationTypes, RelationTypes,
@ -156,6 +157,7 @@ const getAst = async ({
isRequested = isRequested =
!isSystemColumn(col) || !isSystemColumn(col) ||
(isCreatedOrLastModifiedTimeCol(col) && col.system) || (isCreatedOrLastModifiedTimeCol(col) && col.system) ||
(isCreatedOrLastModifiedByCol(col) && col.system) ||
col.pk; col.pk;
} else if (allowedCols && (!includePkByDefault || !col.pk)) { } else if (allowedCols && (!includePkByDefault || !col.pk)) {
isRequested = isRequested =

4
packages/nocodb/src/modules/datas/helpers.ts

@ -171,7 +171,9 @@ export async function serializeCellValue({
) )
.join(', '); .join(', ');
} }
case UITypes.User: { case UITypes.User:
case UITypes.CreatedBy:
case UITypes.LastModifiedBy: {
let data = value; let data = value;
try { try {
if (typeof value === 'string') { if (typeof value === 'string') {

2
packages/nocodb/src/modules/jobs/jobs/export-import/export.service.ts

@ -454,6 +454,8 @@ export class ExportService {
} }
break; break;
case UITypes.User: case UITypes.User:
case UITypes.CreatedBy:
case UITypes.LastModifiedBy:
if (v) { if (v) {
const userIds = []; const userIds = [];
for (const user of v as { id: string }[]) { for (const user of v as { id: string }[]) {

6
packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts

@ -768,6 +768,8 @@ export class ImportService {
a.uidt === UITypes.QrCode || a.uidt === UITypes.QrCode ||
a.uidt === UITypes.CreatedTime || a.uidt === UITypes.CreatedTime ||
a.uidt === UITypes.LastModifiedTime || a.uidt === UITypes.LastModifiedTime ||
a.uidt === UITypes.CreatedBy ||
a.uidt === UITypes.LastModifiedBy ||
a.uidt === UITypes.Barcode, a.uidt === UITypes.Barcode,
), ),
); );
@ -897,7 +899,9 @@ export class ImportService {
} }
} else if ( } else if (
col.uidt === UITypes.CreatedTime || col.uidt === UITypes.CreatedTime ||
col.uidt === UITypes.LastModifiedTime col.uidt === UITypes.LastModifiedTime ||
col.uidt === UITypes.CreatedBy ||
col.uidt === UITypes.LastModifiedBy
) { ) {
if (col.system) continue; if (col.system) continue;
const freshModelData = await this.columnsService.columnAdd({ const freshModelData = await this.columnsService.columnAdd({

8
packages/nocodb/src/schema/swagger-v2.json

@ -12127,7 +12127,9 @@
"Year", "Year",
"QrCode", "QrCode",
"Links", "Links",
"User" "User",
"CreatedBy",
"LastModifiedBy"
], ],
"type": "string" "type": "string"
}, },
@ -15345,7 +15347,9 @@
"Year", "Year",
"QrCode", "QrCode",
"Links", "Links",
"User" "User",
"CreatedBy",
"LastModifiedBy"
], ],
"type": "string", "type": "string",
"description": "UI Data Type" "description": "UI Data Type"

8
packages/nocodb/src/schema/swagger.json

@ -17344,7 +17344,9 @@
"Year", "Year",
"QrCode", "QrCode",
"Links", "Links",
"User" "User",
"CreatedBy",
"LastModifiedBy"
], ],
"type": "string" "type": "string"
}, },
@ -20565,7 +20567,9 @@
"Year", "Year",
"QrCode", "QrCode",
"Links", "Links",
"User" "User",
"CreatedBy",
"LastModifiedBy"
], ],
"type": "string", "type": "string",
"description": "UI Data Type" "description": "UI Data Type"

4
packages/nocodb/src/services/api-docs/swagger/getSwaggerColumnMetas.ts

@ -56,6 +56,10 @@ export default async (
case UITypes.CreatedTime: case UITypes.CreatedTime:
field.type = 'string'; field.type = 'string';
break; break;
case UITypes.LastModifiedBy:
case UITypes.CreatedBy:
field.type = 'object';
break;
default: default:
field.virtual = false; field.virtual = false;
SwaggerTypes.setSwaggerType(c, field, dbType); SwaggerTypes.setSwaggerType(c, field, dbType);

4
packages/nocodb/src/services/api-docs/swaggerV2/getSwaggerColumnMetas.ts

@ -49,6 +49,10 @@ export default async (
case UITypes.CreatedTime: case UITypes.CreatedTime:
field.type = 'string'; field.type = 'string';
break; break;
case UITypes.LastModifiedBy:
case UITypes.CreatedBy:
field.type = 'object';
break;
default: default:
field.virtual = false; field.virtual = false;
SwaggerTypes.setSwaggerType(c, field, dbType); SwaggerTypes.setSwaggerType(c, field, dbType);

45
packages/nocodb/src/services/columns.service.ts

@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { import {
AppEvents, AppEvents,
isCreatedOrLastModifiedByCol,
isCreatedOrLastModifiedTimeCol, isCreatedOrLastModifiedTimeCol,
isLinksOrLTAR, isLinksOrLTAR,
isVirtualCol, isVirtualCol,
@ -173,6 +174,7 @@ export class ColumnsService {
if ( if (
!isVirtualCol(param.column) && !isVirtualCol(param.column) &&
!isCreatedOrLastModifiedTimeCol(param.column) && !isCreatedOrLastModifiedTimeCol(param.column) &&
!isCreatedOrLastModifiedByCol(param.column) &&
!(await Column.checkTitleAvailable({ !(await Column.checkTitleAvailable({
column_name: param.column.column_name, column_name: param.column.column_name,
fk_model_id: column.fk_model_id, fk_model_id: column.fk_model_id,
@ -198,6 +200,7 @@ export class ColumnsService {
}; };
if ( if (
isCreatedOrLastModifiedTimeCol(column) || isCreatedOrLastModifiedTimeCol(column) ||
isCreatedOrLastModifiedByCol(column) ||
[ [
UITypes.Lookup, UITypes.Lookup,
UITypes.Rollup, UITypes.Rollup,
@ -296,7 +299,12 @@ export class ColumnsService {
`Updating ${colBody.uidt} => ${colBody.uidt} is not implemented`, `Updating ${colBody.uidt} => ${colBody.uidt} is not implemented`,
); );
} else if ( } else if (
[UITypes.CreatedTime, UITypes.LastModifiedTime].includes(colBody.uidt) [
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
].includes(colBody.uidt)
) { ) {
// allow updating of title only // allow updating of title only
await Column.update(param.columnId, { await Column.update(param.columnId, {
@ -1687,6 +1695,8 @@ export class ColumnsService {
break; break;
case UITypes.CreatedTime: case UITypes.CreatedTime:
case UITypes.LastModifiedTime: case UITypes.LastModifiedTime:
case UITypes.CreatedBy:
case UITypes.LastModifiedBy:
{ {
let columnName: string; let columnName: string;
const columns = await table.getColumns(); const columns = await table.getColumns();
@ -1697,10 +1707,26 @@ export class ColumnsService {
); );
if (!existingColumn) { if (!existingColumn) {
columnName = let columnTitle;
colBody.uidt === UITypes.CreatedTime
? 'created_at' switch (colBody.uidt) {
: 'updated_at'; case UITypes.CreatedTime:
columnName = 'created_at';
columnTitle = 'CreatedAt';
break;
case UITypes.LastModifiedTime:
columnName = 'updated_at';
columnTitle = 'UpdatedAt';
break;
case UITypes.CreatedBy:
columnName = 'created_by';
columnTitle = 'CreatedBy';
break;
case UITypes.LastModifiedBy:
columnName = 'updated_by';
columnTitle = 'UpdatedBy';
break;
}
// todo: check type as well // todo: check type as well
const dbColumn = columns.find((c) => c.column_name === columnName); const dbColumn = columns.find((c) => c.column_name === columnName);
@ -1741,10 +1767,7 @@ export class ColumnsService {
await sqlMgr.sqlOpPlus(source, 'tableUpdate', tableUpdateBody); await sqlMgr.sqlOpPlus(source, 'tableUpdate', tableUpdateBody);
} }
const title = getUniqueColumnAliasName( const title = getUniqueColumnAliasName(table.columns, columnTitle);
table.columns,
UITypes.CreatedTime ? 'CreatedAt' : 'UpdatedAt',
);
await Column.insert({ await Column.insert({
...colBody, ...colBody,
@ -2209,9 +2232,11 @@ export class ColumnsService {
/* falls through to default */ /* falls through to default */
} }
// on delete create time or last modified time, keep the column in table and delete the column from meta // on deleting created/last modified columns, keep the column in table and delete the column from meta
case UITypes.CreatedTime: case UITypes.CreatedTime:
case UITypes.LastModifiedTime: case UITypes.LastModifiedTime:
case UITypes.CreatedBy:
case UITypes.LastModifiedBy:
{ {
await Column.delete(param.columnId, ncMeta); await Column.delete(param.columnId, ncMeta);
} }

46
packages/nocodb/src/services/tables.service.ts

@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import DOMPurify from 'isomorphic-dompurify'; import DOMPurify from 'isomorphic-dompurify';
import { import {
AppEvents, AppEvents,
isCreatedOrLastModifiedByCol,
isCreatedOrLastModifiedTimeCol, isCreatedOrLastModifiedTimeCol,
isLinksOrLTAR, isLinksOrLTAR,
isVirtualCol, isVirtualCol,
@ -384,21 +385,49 @@ export class TablesService {
source = base.sources.find((b) => b.id === param.sourceId); source = base.sources.find((b) => b.id === param.sourceId);
} }
// add CreatedBy and LastModifiedBy system columns if missing in request payload // add CreatedTime and LastModifiedTime system columns if missing in request payload
{ {
for (const uidt of [UITypes.CreatedTime, UITypes.LastModifiedTime]) { for (const uidt of [
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
]) {
const col = tableCreatePayLoad.columns.find( const col = tableCreatePayLoad.columns.find(
(c) => c.uidt === uidt, (c) => c.uidt === uidt,
) as ColumnType; ) as ColumnType;
let columnName, columnTitle;
switch (uidt) {
case UITypes.CreatedTime:
columnName = 'created_at';
columnTitle = 'CreatedAt';
break;
case UITypes.LastModifiedTime:
columnName = 'updated_at';
columnTitle = 'UpdatedAt';
break;
case UITypes.CreatedBy:
columnName = 'created_by';
columnTitle = 'CreatedBy';
break;
case UITypes.LastModifiedBy:
columnName = 'updated_by';
columnTitle = 'UpdatedBy';
break;
}
const colName = getUniqueColumnName( const colName = getUniqueColumnName(
tableCreatePayLoad.columns as any[], tableCreatePayLoad.columns as any[],
uidt === UITypes.CreatedTime ? 'created_at' : 'updated_at', columnName,
); );
const colAlias = getUniqueColumnAliasName( const colAlias = getUniqueColumnAliasName(
tableCreatePayLoad.columns as any[], tableCreatePayLoad.columns as any[],
uidt === UITypes.CreatedTime ? 'CreatedAt' : 'UpdatedAt', columnTitle,
); );
if (!col || !col.system) { if (!col || !col.system) {
tableCreatePayLoad.columns.push({ tableCreatePayLoad.columns.push({
...(await getColumnPropsFromUIDT({ uidt } as any, source)), ...(await getColumnPropsFromUIDT({ uidt } as any, source)),
@ -514,7 +543,8 @@ export class TablesService {
for (const column of param.table.columns) { for (const column of param.table.columns) {
if ( if (
!isVirtualCol(column) || !isVirtualCol(column) ||
(isCreatedOrLastModifiedTimeCol(column) && (column as any).system) (isCreatedOrLastModifiedTimeCol(column) && (column as any).system) ||
(isCreatedOrLastModifiedByCol(column) && (column as any).system)
) { ) {
const mxColumnLength = Column.getMaxColumnNameLength(sqlClientType); const mxColumnLength = Column.getMaxColumnNameLength(sqlClientType);
@ -549,7 +579,11 @@ export class TablesService {
param.table.columns param.table.columns
// exclude alias columns from column list // exclude alias columns from column list
?.filter((c) => { ?.filter((c) => {
return !isCreatedOrLastModifiedTimeCol(c) || (c as any).system; return (
!isCreatedOrLastModifiedTimeCol(c) ||
!isCreatedOrLastModifiedByCol(c) ||
(c as any).system
);
}) })
.map(async (c) => ({ .map(async (c) => ({
...(await getColumnPropsFromUIDT(c as any, source)), ...(await getColumnPropsFromUIDT(c as any, source)),

2
packages/nocodb/src/version-upgrader/NcUpgrader.ts

@ -17,6 +17,7 @@ import ncProjectConfigUpgrader from './ncProjectConfigUpgrader';
import ncXcdbLTARUpgrader from './ncXcdbLTARUpgrader'; import ncXcdbLTARUpgrader from './ncXcdbLTARUpgrader';
import ncXcdbLTARIndexUpgrader from './ncXcdbLTARIndexUpgrader'; import ncXcdbLTARIndexUpgrader from './ncXcdbLTARIndexUpgrader';
import ncXcdbCreatedAndUpdatedTimeUpgrader from './ncXcdbCreatedAndUpdatedTimeUpgrader'; import ncXcdbCreatedAndUpdatedTimeUpgrader from './ncXcdbCreatedAndUpdatedTimeUpgrader';
import ncXcdbCreatedAndUpdatedByUpgrader from './ncXcdbCreatedAndUpdatedByUpgrader';
import type { MetaService } from '~/meta/meta.service'; import type { MetaService } from '~/meta/meta.service';
import type { NcConfig } from '~/interface/config'; import type { NcConfig } from '~/interface/config';
@ -146,6 +147,7 @@ export default class NcUpgrader {
{ name: '0108002', handler: ncXcdbLTARUpgrader }, { name: '0108002', handler: ncXcdbLTARUpgrader },
{ name: '0111002', handler: ncXcdbLTARIndexUpgrader }, { name: '0111002', handler: ncXcdbLTARIndexUpgrader },
{ name: '0111004', handler: ncXcdbCreatedAndUpdatedTimeUpgrader }, { name: '0111004', handler: ncXcdbCreatedAndUpdatedTimeUpgrader },
{ name: '0111005', handler: ncXcdbCreatedAndUpdatedByUpgrader },
]; ];
} }
} }

125
packages/nocodb/src/version-upgrader/ncXcdbCreatedAndUpdatedByUpgrader.ts

@ -0,0 +1,125 @@
import { UITypes } from 'nocodb-sdk';
import { Logger } from '@nestjs/common';
import type { NcUpgraderCtx } from './NcUpgrader';
import type { MetaService } from '~/meta/meta.service';
import { MetaTable } from '~/utils/globals';
import { Column, Model, Source } from '~/models';
import {
getUniqueColumnAliasName,
getUniqueColumnName,
} from '~/helpers/getUniqueName';
import getColumnPropsFromUIDT from '~/helpers/getColumnPropsFromUIDT';
import ProjectMgrv2 from '~/db/sql-mgr/v2/ProjectMgrv2';
import { Altered } from '~/services/columns.service';
// An upgrader for adding created_by and updated_by columns to all tables as system column
const logger = new Logger('ncXcdbCreatedAndUpdatedByUpgrader');
async function upgradeModels({
ncMeta,
source,
}: {
ncMeta: MetaService;
source: any;
}) {
const models = await Model.list(
{
base_id: source.base_id,
source_id: source.id,
},
ncMeta,
);
await Promise.all(
models.map(async (model: any) => {
if (model.mm) return;
const newColumns = [];
const columns = await model.getColumns(ncMeta);
const oldColumns = columns.map((c) => ({ ...c, cn: c.column_name }));
newColumns.push({
...(await getColumnPropsFromUIDT(
{
uidt: UITypes.CreatedBy,
column_name: getUniqueColumnName(columns, 'created_by'),
title: getUniqueColumnAliasName(columns, 'CreatedBy'),
},
source,
)),
cdf: null,
system: true,
altered: Altered.NEW_COLUMN,
});
newColumns.push({
...(await getColumnPropsFromUIDT(
{
uidt: UITypes.LastModifiedBy,
column_name: getUniqueColumnName(columns, 'updated_by'),
title: getUniqueColumnAliasName(columns, 'UpdatedBy'),
},
source,
)),
cdf: null,
system: true,
altered: Altered.NEW_COLUMN,
});
// update column in db
const tableUpdateBody = {
...model,
tn: model.table_name,
originalColumns: oldColumns,
columns: [...columns, ...newColumns].map((c) => ({
...c,
cn: c.column_name,
})),
};
const sqlMgr = ProjectMgrv2.getSqlMgr({ id: source.base_id }, ncMeta);
await sqlMgr.sqlOpPlus(source, 'tableUpdate', tableUpdateBody);
for (const newColumn of newColumns) {
await Column.insert(
{
...newColumn,
system: 1,
fk_model_id: model.id,
},
ncMeta,
);
}
logger.log(`Upgraded model ${model.name} from source ${source.name}`);
}),
);
}
export default async function ({ ncMeta }: NcUpgraderCtx) {
// get all xcdb sources
const sources = await ncMeta.metaList2(null, null, MetaTable.BASES, {
xcCondition: {
_or: [
{
is_meta: {
eq: 1,
},
},
{
is_local: {
eq: 1,
},
},
],
},
});
// iterate and upgrade each base
for (const source of sources) {
logger.log(`Upgrading source ${source.name}`);
// update the meta props
await upgradeModels({ ncMeta, source: new Source(source) });
}
}
Loading…
Cancel
Save