Browse Source

Merge pull request #7373 from nocodb/nc-feat/crupby

feat: CreatedBy & LastModifiedBy
pull/7400/head
Raju Udava 8 months ago committed by GitHub
parent
commit
30e6cfdf72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      packages/nc-gui/components/cell/ReadOnlyUser.vue
  2. 25
      packages/nc-gui/components/cell/User.vue
  3. 7
      packages/nc-gui/components/erd/utils.ts
  4. 2
      packages/nc-gui/components/smartsheet/Form.vue
  5. 3
      packages/nc-gui/components/smartsheet/VirtualCell.vue
  6. 2
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  7. 14
      packages/nc-gui/components/smartsheet/column/FormulaOptions.vue
  8. 18
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  9. 4
      packages/nc-gui/components/smartsheet/grid/GroupBy.vue
  10. 16
      packages/nc-gui/components/smartsheet/grid/Table.vue
  11. 13
      packages/nc-gui/components/smartsheet/header/Menu.vue
  12. 3
      packages/nc-gui/components/smartsheet/header/VirtualCellIcon.ts
  13. 8
      packages/nc-gui/components/smartsheet/toolbar/CreateGroupBy.vue
  14. 7
      packages/nc-gui/components/smartsheet/toolbar/CreateSort.vue
  15. 17
      packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue
  16. 9
      packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue
  17. 2
      packages/nc-gui/components/template/Editor.vue
  18. 2
      packages/nc-gui/components/virtual-cell/Lookup.vue
  19. 2
      packages/nc-gui/composables/useData.ts
  20. 4
      packages/nc-gui/composables/useMultiSelect/convertCellData.ts
  21. 16
      packages/nc-gui/composables/useMultiSelect/index.ts
  22. 9
      packages/nc-gui/composables/useViewColumns.ts
  23. 12
      packages/nc-gui/composables/useViewGroupBy.ts
  24. 2
      packages/nc-gui/utils/cell.ts
  25. 10
      packages/nc-gui/utils/columnUtils.ts
  26. 40
      packages/nc-gui/utils/filterUtils.ts
  27. 36
      packages/nc-gui/utils/iconUtils.ts
  28. 12
      packages/nocodb-sdk/src/lib/Api.ts
  29. 16
      packages/nocodb-sdk/src/lib/UITypes.ts
  30. 3
      packages/nocodb-sdk/src/lib/formulaHelpers.ts
  31. 1
      packages/nocodb-sdk/src/lib/index.ts
  32. 46
      packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts
  33. 46
      packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts
  34. 55
      packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts
  35. 55
      packages/nocodb-sdk/src/lib/sqlUi/SnowflakeUi.ts
  36. 50
      packages/nocodb-sdk/src/lib/sqlUi/SqliteUi.ts
  37. 250
      packages/nocodb/src/db/BaseModelSqlv2.ts
  38. 7
      packages/nocodb/src/db/conditionV2.ts
  39. 2
      packages/nocodb/src/db/formulav2/formulaQueryBuilderv2.ts
  40. 4
      packages/nocodb/src/db/sortV2.ts
  41. 11
      packages/nocodb/src/helpers/columnHelpers.ts
  42. 5
      packages/nocodb/src/helpers/getAst.ts
  43. 8
      packages/nocodb/src/modules/datas/helpers.ts
  44. 2
      packages/nocodb/src/modules/global/init-meta-service.provider.ts
  45. 5
      packages/nocodb/src/modules/jobs/jobs/export-import/export.service.ts
  46. 9
      packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts
  47. 10
      packages/nocodb/src/run/testDocker.ts
  48. 8
      packages/nocodb/src/schema/swagger-v2.json
  49. 8
      packages/nocodb/src/schema/swagger.json
  50. 4
      packages/nocodb/src/services/api-docs/swagger/getSwaggerColumnMetas.ts
  51. 4
      packages/nocodb/src/services/api-docs/swaggerV2/getSwaggerColumnMetas.ts
  52. 45
      packages/nocodb/src/services/columns.service.ts
  53. 46
      packages/nocodb/src/services/tables.service.ts
  54. 4
      packages/nocodb/src/version-upgrader/NcUpgrader.ts
  55. 272
      packages/nocodb/src/version-upgrader/ncXcdbCreatedAndUpdatedSystemFieldsUpgrader.ts
  56. 9
      packages/nocodb/tests/unit/factory/row.ts
  57. 3
      packages/nocodb/tests/unit/init/index.ts
  58. 50
      packages/nocodb/tests/unit/model/tests/baseModelSql.test.ts
  59. 262
      packages/nocodb/tests/unit/rest/tests/columnTypeSpecific.test.ts
  60. 24
      packages/nocodb/tests/unit/rest/tests/newDataApis.test.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>

25
packages/nc-gui/components/cell/User.vue

@ -27,7 +27,7 @@ import {
import MdiCloseCircle from '~icons/mdi/close-circle'
interface Props {
modelValue?: UserFieldRecordType[] | string | null
modelValue?: UserFieldRecordType[] | UserFieldRecordType | string | null
rowIndex?: number
location?: 'cell' | 'filter'
forceMulti?: boolean
@ -113,17 +113,18 @@ const vModel = computed({
return acc
}, [] as { label: string; value: string }[])
} else {
selected =
modelValue?.reduce((acc, item) => {
const label = item?.display_name || item?.email
if (label) {
acc.push({
label,
value: item.id,
})
}
return acc
}, [] as { label: string; value: string }[]) || []
selected = modelValue
? (Array.isArray(modelValue) ? modelValue : [modelValue]).reduce((acc, item) => {
const label = item?.display_name || item?.email
if (label) {
acc.push({
label,
value: item.id,
})
}
return acc
}, [] as { label: string; value: string }[])
: []
}
return selected

7
packages/nc-gui/components/erd/utils.ts

@ -175,9 +175,10 @@ export function useErdElements(tables: MaybeRef<TableType[]>, props: MaybeRef<ER
if (!table.id) return acc
const columns =
metasWithIdAsKey.value[table.id].columns?.filter(
(col) => config.value.showAllColumns || (!config.value.showAllColumns && isLinksOrLTAR(col)),
) || []
metasWithIdAsKey.value[table.id].columns?.filter((col) => {
if ([UITypes.CreatedBy, UITypes.LastModifiedBy].includes(col.uidt as UITypes) && col.system) return false
return config.value.showAllColumns || (!config.value.showAllColumns && isLinksOrLTAR(col))
}) || []
const pkAndFkColumns = columns
.filter(() => config.value.showPkAndFk)

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

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

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

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

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

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

14
packages/nc-gui/components/smartsheet/column/FormulaOptions.vue

@ -5,6 +5,7 @@ import jsep from 'jsep'
import {
FormulaError,
UITypes,
isCreatedOrLastModifiedByCol,
jsepCurlyHook,
substituteColumnIdWithAliasInFormula,
validateFormulaAndExtractTreeWithType,
@ -51,7 +52,18 @@ const { predictFunction: _predictFunction } = useNocoEe()
const meta = inject(MetaInj, ref())
const supportedColumns = computed(
() => meta?.value?.columns?.filter((col) => !uiTypesNotSupportedInFormulas.includes(col.uidt as UITypes)) || [],
() =>
meta?.value?.columns?.filter((col) => {
if (uiTypesNotSupportedInFormulas.includes(col.uidt as UITypes)) {
return false
}
if (isCreatedOrLastModifiedByCol(col) && col.system) {
return false
}
return true
}) || [],
)
const { getMeta } = useMetas()

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

@ -1,6 +1,13 @@
<script setup lang="ts">
import type { ColumnType, TableType, ViewType } from 'nocodb-sdk'
import { ViewTypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import {
ViewTypes,
isCreatedOrLastModifiedByCol,
isCreatedOrLastModifiedTimeCol,
isLinksOrLTAR,
isSystemColumn,
isVirtualCol,
} from 'nocodb-sdk'
import type { Ref } from 'vue'
import MdiChevronDown from '~icons/mdi/chevron-down'
@ -459,7 +466,14 @@ const onIsExpandedUpdate = (v: boolean) => {
const isReadOnlyVirtualCell = (column: ColumnType) => {
return (
isRollup(column) || isFormula(column) || isBarcode(column) || isLookup(column) || isQrCode(column) || isSystemColumn(column)
isRollup(column) ||
isFormula(column) ||
isBarcode(column) ||
isLookup(column) ||
isQrCode(column) ||
isSystemColumn(column) ||
isCreatedOrLastModifiedTimeCol(column) ||
isCreatedOrLastModifiedByCol(column)
)
}

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

@ -169,7 +169,7 @@ const parseKey = (group: Group) => {
return [parseStringDateTime(key, timeFormats[0], false)]
}
if (key && group.column?.uidt === UITypes.User) {
if (key && [UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(group.column?.uidt as UITypes)) {
try {
const parsedKey = JSON.parse(key)
return [parsedKey]
@ -192,6 +192,8 @@ const shouldRenderCell = (column) =>
UITypes.DateTime,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
].includes(column?.uidt)
</script>

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

@ -2,7 +2,15 @@
import axios from 'axios'
import { nextTick } from '@vue/runtime-core'
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 usePaginationShortcuts from './usePaginationShortcuts'
@ -1011,7 +1019,8 @@ const showFillHandle = computed(
isLookup(fields.value[activeCell.col]) ||
isRollup(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) ||
isRollup(columnObj) ||
isFormula(columnObj) ||
isCreatedOrLastModifiedTimeCol(columnObj)) &&
isCreatedOrLastModifiedTimeCol(columnObj) ||
isCreatedOrLastModifiedByCol(columnObj)) &&
hasEditPermission &&
isCellSelected(rowIndex, colIndex),
'!border-r-blue-400 !border-r-3': toBeDroppedColId === columnObj.id,

13
packages/nc-gui/components/smartsheet/header/Menu.vue

@ -129,6 +129,7 @@ const duplicateVirtualColumn = async () => {
id: undefined,
colOptions: undefined,
order: undefined,
system: false,
}
try {
@ -166,7 +167,17 @@ const duplicateVirtualColumn = async () => {
const openDuplicateDlg = async () => {
if (!column?.value) return
if (column.value.uidt && [UITypes.Lookup, UITypes.Rollup].includes(column.value.uidt as UITypes)) {
if (
column.value.uidt &&
[
UITypes.Lookup,
UITypes.Rollup,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
].includes(column.value.uidt as UITypes)
) {
duplicateVirtualColumn()
} else {
const gridViewColumnList = (await $api.dbViewColumn.list(view.value?.id as string)).list

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

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

8
packages/nc-gui/components/smartsheet/toolbar/CreateGroupBy.vue

@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import { RelationTypes, UITypes, isCreatedOrLastModifiedByCol, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
const props = defineProps<{
// As we need to focus search box when the parent is opened
@ -39,6 +39,12 @@ const options = computed<ColumnType[]>(
/** ignore virtual fields which are system fields ( mm relation ) and qr code fields */
return false
}
if (isCreatedOrLastModifiedByCol(c)) {
/** ignore created by and last modified by system field */
return false
}
return showSystemFields.value
} else if (c.uidt === UITypes.QrCode || c.uidt === UITypes.Barcode || c.uidt === UITypes.ID) {
return false

7
packages/nc-gui/components/smartsheet/toolbar/CreateSort.vue

@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import { RelationTypes, UITypes, isCreatedOrLastModifiedByCol, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
const props = defineProps<{
// As we need to focus search box when the parent is opened
@ -33,6 +33,11 @@ const options = computed<ColumnType[]>(
return true
}
if (isSystemColumn(metaColumnById?.value?.[c.id!])) {
if (isCreatedOrLastModifiedByCol(c)) {
/** ignore created by and last modified by system field */
return false
}
return (
/** hide system columns if not enabled */
showSystemFields.value

17
packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue

@ -1,7 +1,7 @@
<script setup lang="ts">
import type { SelectProps } from 'ant-design-vue'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { RelationTypes, UITypes, isCreatedOrLastModifiedByCol, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { MetaInj, computed, inject, ref, resolveComponent, useViewColumnsOrThrow } from '#imports'
const { modelValue, isSort, allowEmpty, ...restProps } = defineProps<{
@ -26,12 +26,25 @@ const { showSystemFields, metaColumnById } = useViewColumnsOrThrow()
const options = computed<SelectProps['options']>(() =>
(
customColumns.value ||
customColumns.value?.filter((c: ColumnType) => {
if (isSystemColumn(metaColumnById?.value?.[c.id!])) {
if (isCreatedOrLastModifiedByCol(c)) {
/** ignore created by and last modified by system field */
return false
}
}
return true
}) ||
meta.value?.columns?.filter((c: ColumnType) => {
if (c.uidt === UITypes.Links) {
return true
}
if (isSystemColumn(metaColumnById?.value?.[c.id!])) {
if (isCreatedOrLastModifiedByCol(c)) {
/** ignore created by and last modified by system field */
return false
}
return (
/** if the field is used in filter, then show it anyway */
localValue.value === c.id ||

9
packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue

@ -19,6 +19,7 @@ import {
isPercent,
isRating,
isReadonlyDateTime,
isReadonlyUser,
isSingleSelect,
isTextArea,
isTime,
@ -87,6 +88,7 @@ const checkTypeFunctions = {
isTextArea,
isLinks: (col: ColumnType) => col.uidt === UITypes.Links,
isUser,
isReadonlyUser,
}
type FilterType = keyof typeof checkTypeFunctions
@ -155,6 +157,7 @@ const componentMap: Partial<Record<FilterType, any>> = computed(() => {
isFloat: Float,
isLinks: Integer,
isUser: User,
isReadonlyUser: User,
}
})
@ -181,6 +184,12 @@ const componentProps = computed(() => {
case 'isUser': {
return { forceMulti: true }
}
case 'isReadonlyUser': {
if (['anyof', 'nanyof'].includes(props.filter.comparison_op!)) {
return { forceMulti: true }
}
return {}
}
default: {
return {}
}

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

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

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

@ -171,6 +171,8 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activ
UITypes.MultiSelect,
UITypes.SingleSelect,
UITypes.User,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
].includes(lookupColumn.uidt),
'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.User ||
col.uidt === UITypes.LastModifiedTime ||
col.uidt === UITypes.LastModifiedBy ||
col.uidt === UITypes.Lookup ||
col.au ||
(col.cdf && / on update /i.test(col.cdf)))
@ -393,6 +394,7 @@ export function useData(args: {
UITypes.Lookup,
UITypes.Rollup,
UITypes.LinkToAnotherRecord,
UITypes.LastModifiedBy,
].includes(col.uidt),
)
) {

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

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

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

@ -112,13 +112,15 @@ export function useMultiSelect(
textToCopy = !!textToCopy
}
if (columnObj.uidt === UITypes.User) {
if (textToCopy && Array.isArray(textToCopy)) {
textToCopy = textToCopy
.map((user: UserFieldRecordType) => {
return user.email
})
.join(', ')
if ([UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(columnObj.uidt as UITypes)) {
if (textToCopy) {
textToCopy = Array.isArray(textToCopy)
? textToCopy
: [textToCopy]
.map((user: UserFieldRecordType) => {
return user.email
})
.join(', ')
}
}

9
packages/nc-gui/composables/useViewColumns.ts

@ -1,4 +1,4 @@
import { ViewTypes, isSystemColumn } from 'nocodb-sdk'
import { ViewTypes, isCreatedOrLastModifiedByCol, isSystemColumn } from 'nocodb-sdk'
import type { ColumnType, GridColumnReqType, GridColumnType, MapType, TableType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import { computed, ref, storeToRefs, useBase, useNuxtApp, useRoles, useUndoRedo, watch } from '#imports'
@ -70,7 +70,12 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState(
}, {})
fields.value = meta.value?.columns
?.map((column: ColumnType) => {
?.filter((column: ColumnType) => {
// filter created by and last modified by system columns
if (isCreatedOrLastModifiedByCol(column) && column.system) return false
return true
})
.map((column: ColumnType) => {
const currentColumnField = fieldById[column.id!] || {}
return {

12
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
}
if (col.uidt === UITypes.User) {
if ([UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(col.uidt as UITypes)) {
if (!value) {
return GROUP_BY_VARS.NULL
}
@ -161,11 +161,15 @@ export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: Computed
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)) {
acc += `${acc.length ? '~and' : ''}(${curr.title},eq,exactDate,${curr.key})`
} else if (curr.column_uidt === UITypes.User) {
} else if ([UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(curr.column_uidt as UITypes)) {
try {
const value = JSON.parse(curr.key)
acc += `${acc.length ? '~and' : ''}(${curr.title},gb_eq,${value.map((v: any) => v.id).join(',')})`
} catch (e) {}
acc += `${acc.length ? '~and' : ''}(${curr.title},gb_eq,${(Array.isArray(value) ? value : [value])
.map((v: any) => v.id)
.join(',')})`
} catch (e) {
console.error(e)
}
} else {
acc += `${acc.length ? '~and' : ''}(${curr.title},gb_eq,${curr.key})`
}

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

@ -17,6 +17,8 @@ export const isDateTime = (column: ColumnType, abstractType: any) =>
abstractType === 'datetime' || column.uidt === UITypes.DateTime
export const isReadonlyDateTime = (column: ColumnType, _abstractType: any) =>
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 isEnum = (column: ColumnType) => column.uidt === UITypes.SingleSelect
export const isSingleSelect = (column: ColumnType) => column.uidt === UITypes.SingleSelect

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

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

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

@ -95,13 +95,27 @@ export const comparisonOpList = (
text: getEqText(fieldUiType),
value: 'eq',
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),
value: 'neq',
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),
@ -112,6 +126,8 @@ export const comparisonOpList = (
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.User,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
UITypes.Collaborator,
UITypes.Date,
UITypes.DateTime,
@ -128,6 +144,8 @@ export const comparisonOpList = (
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.User,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
UITypes.Collaborator,
UITypes.Date,
UITypes.DateTime,
@ -144,6 +162,8 @@ export const comparisonOpList = (
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.User,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
UITypes.Collaborator,
UITypes.Attachment,
UITypes.LinkToAnotherRecord,
@ -163,6 +183,8 @@ export const comparisonOpList = (
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.User,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
UITypes.Collaborator,
UITypes.Attachment,
UITypes.LinkToAnotherRecord,
@ -183,6 +205,8 @@ export const comparisonOpList = (
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.User,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
UITypes.Collaborator,
UITypes.Attachment,
UITypes.LinkToAnotherRecord,
@ -202,6 +226,8 @@ export const comparisonOpList = (
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.User,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
UITypes.Collaborator,
UITypes.Attachment,
UITypes.LinkToAnotherRecord,
@ -215,25 +241,25 @@ export const comparisonOpList = (
text: 'contains all of',
value: 'allof',
ignoreVal: false,
includedTypes: [UITypes.MultiSelect, UITypes.User],
includedTypes: [UITypes.MultiSelect, UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy],
},
{
text: 'contains any of',
value: 'anyof',
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',
value: 'nallof',
ignoreVal: false,
includedTypes: [UITypes.MultiSelect, UITypes.User],
includedTypes: [UITypes.MultiSelect, UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy],
},
{
text: 'does not contain any of',
value: 'nanyof',
ignoreVal: false,
includedTypes: [UITypes.MultiSelect, UITypes.SingleSelect, UITypes.User],
includedTypes: [UITypes.MultiSelect, UITypes.SingleSelect, UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy],
},
{
text: getGtText(fieldUiType),
@ -382,7 +408,7 @@ export const comparisonSubOpList = (
text: 'yesterday',
value: 'yesterday',
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',

36
packages/nc-gui/utils/iconUtils.ts

@ -19,23 +19,15 @@ import MdiTableLarge from '~icons/mdi/table-large'
import TablerColumnInsertLeft from '~icons/tabler/column-insert-left'
import TablerColumnInsertRight from '~icons/tabler/column-insert-right'
import MdiEyeCircleOutline from '~icons/mdi/eye-circle-outline'
import NcEye from '~icons/nc-icons/eye'
import MsGroup from '~icons/material-symbols/groups-outline-rounded'
import MsAddBoxOutline from '~icons/nc-icons/add-box'
import MsDownloadRounded from '~icons/nc-icons/download'
import LogosAirtable from '~icons/logos/airtable'
import NcStar from '~icons/nc-icons/star'
import NcUnStar from '~icons/nc-icons/star-remove'
import MsSort from '~icons/material-symbols/sort'
import MaterialSymbolsEdit from '~icons/material-symbols/edit-outline-rounded'
import MaterialDuplicate from '~icons/material-symbols/file-copy-outline-rounded'
import MaterialSymbolsWarningOutlineRounded from '~icons/material-symbols/warning-outline-rounded'
import MaterialSymbolsDragIndicator from '~icons/ri/draggable'
import NcSearch from '~icons/nc-icons/search'
import NcCreditCard from '~icons/nc-icons/credit-card'
import NcLayers from '~icons/nc-icons/layers'
import NcUsers from '~icons/nc-icons/users'
import NcCheck from '~icons/nc-icons/check'
import PlusSquare from '~icons/nc-icons/plus-square'
import MobileShare from '~icons/nc-icons/share'
import PhLayout from '~icons/ph/layout'
@ -58,27 +50,18 @@ import LogosDiscordIcon from '~icons/logos/discord-icon'
import LogosRedditIcon from '~icons/logos/reddit-icon'
import RiTwitterXFill from '~icons/ri/twitter-x-line'
import PhGithubLogoLight from '~icons/ph/github-logo-light'
import NcIconsRowHeightMedium from '~icons/nc-icons/row-height-medium'
import NcIconsRowHeightShort from '~icons/nc-icons/row-height-short'
import NcIconsRowHeightTall from '~icons/nc-icons/row-height-tall'
import NcIconsRowHeightExtraTall from '~icons/nc-icons/row-height-extra-tall'
import MsInfo from '~icons/material-symbols/info-outline-rounded'
import PhSparkleFill from '~icons/ph/sparkle-fill'
import NcArticle from '~icons/nc-icons/article'
import MsDatabase from '~icons/mdi/database-outline'
import MdiDatabaseSearch from '~icons/mdi/database-search'
import MdiMagicStaff from '~icons/mdi/magic-staff'
import PhCaretDoubleLeftThin from '~icons/ph/caret-double-left-light'
import NcNotification from '~icons/material-symbols/notifications-outline'
import Right from '~icons/material-symbols/chevron-right-rounded'
import Left from '~icons/material-symbols/chevron-left-rounded'
import Up from '~icons/material-symbols/keyboard-arrow-up-rounded'
import Down from '~icons/material-symbols/keyboard-arrow-down-rounded'
import PhTriangleFill from '~icons/ph/triangle-fill'
import LcSend from '~icons/lucide/send'
import NcCommentHere from '~icons/nc-icons/comment-here'
import NcAddDataSource from '~icons/nc-icons/add-data-source'
import NcDatabaseIcon from '~icons/nc-icons/database'
import HasManyIcon from '~icons/nc-icons/hasmany'
import ManytoManyIcon from '~icons/nc-icons/manytomany'
@ -109,6 +92,25 @@ import Filter from '~icons/nc-icons/filter'
import Group from '~icons/nc-icons/group'
import Sort from '~icons/nc-icons/sort'
// NocoDB Icons
import NcEye from '~icons/nc-icons/eye'
import NcStar from '~icons/nc-icons/star'
import NcUnStar from '~icons/nc-icons/star-remove'
import NcSearch from '~icons/nc-icons/search'
import NcCreditCard from '~icons/nc-icons/credit-card'
import NcLayers from '~icons/nc-icons/layers'
import NcUsers from '~icons/nc-icons/users'
import NcCheck from '~icons/nc-icons/check'
import NcIconsRowHeightMedium from '~icons/nc-icons/row-height-medium'
import NcIconsRowHeightShort from '~icons/nc-icons/row-height-short'
import NcIconsRowHeightTall from '~icons/nc-icons/row-height-tall'
import NcIconsRowHeightExtraTall from '~icons/nc-icons/row-height-extra-tall'
import NcArticle from '~icons/nc-icons/article'
import NcNotification from '~icons/material-symbols/notifications-outline'
import NcCommentHere from '~icons/nc-icons/comment-here'
import NcAddDataSource from '~icons/nc-icons/add-data-source'
import NcDatabaseIcon from '~icons/nc-icons/database'
// keep it for reference
// todo: remove it after all icons are migrated
/* export const iconMapOld = {

12
packages/nocodb-sdk/src/lib/Api.ts

@ -433,7 +433,7 @@ export interface ColumnType {
| 'Checkbox'
| 'Collaborator'
| 'Count'
| 'CreateTime'
| 'CreatedTime'
| 'Currency'
| 'Date'
| 'DateTime'
@ -464,7 +464,9 @@ export interface ColumnType {
| 'Year'
| 'QrCode'
| 'Links'
| 'User';
| 'User'
| 'CreatedBy'
| 'LastModifiedBy';
/** Is Unsigned? */
un?: BoolType;
/** Is unique? */
@ -1751,7 +1753,7 @@ export interface NormalColumnRequestType {
| 'Checkbox'
| 'Collaborator'
| 'Count'
| 'CreateTime'
| 'CreatedTime'
| 'Currency'
| 'Date'
| 'DateTime'
@ -1782,7 +1784,9 @@ export interface NormalColumnRequestType {
| 'Year'
| 'QrCode'
| 'Links'
| 'User';
| 'User'
| 'CreatedBy'
| 'LastModifiedBy';
/** Is this column unique? */
un?: BoolType;
/** Is this column unique? */

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

@ -40,6 +40,8 @@ enum UITypes {
Button = 'Button',
Links = 'Links',
User = 'User',
CreatedBy = 'CreatedBy',
LastModifiedBy = 'LastModifiedBy',
}
export const numericUITypes = [
@ -85,6 +87,8 @@ export function isVirtualCol(
UITypes.Links,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
// UITypes.Count,
].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(
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.Email:
case UITypes.URL:
case UITypes.User:
case UITypes.CreatedBy:
case UITypes.LastModifiedBy:
res.dataType = FormulaDataTypes.STRING;
break;
// numeric

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

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

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

@ -135,6 +135,52 @@ export class MssqlUi {
uicn: '',
system: true,
},
{
column_name: 'created_by',
title: 'nc_created_by',
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: '45',
dtxs: '',
altered: 1,
uidt: UITypes.CreatedBy,
uip: '',
uicn: '',
system: true,
},
{
column_name: 'updated_by',
title: 'nc_updated_by',
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: '45',
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: '',
system: true,
},
{
column_name: 'created_by',
title: 'nc_created_by',
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: '45',
dtxs: '',
altered: 1,
uidt: UITypes.CreatedBy,
uip: '',
uicn: '',
system: true,
},
{
column_name: 'updated_by',
title: 'nc_updated_by',
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: '45',
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: '',
system: true,
},
{
column_name: 'created_by',
title: 'nc_created_by',
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: '45',
dtxs: '',
altered: 1,
uidt: UITypes.CreatedBy,
uip: '',
uicn: '',
system: true,
},
{
column_name: 'updated_by',
title: 'nc_updated_by',
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: '45',
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 = {};
switch (col.uidt) {
case 'ID':
@ -1709,7 +1755,7 @@ export class PgUi {
return colProp;
}
static getDataTypeListForUiType(col: { uidt?: UITypes }, idType: IDType) {
static getDataTypeListForUiType(col: { uidt?: UITypes; }, idType: IDType) {
switch (col.uidt) {
case 'ID':
if (idType === 'AG') {
@ -1941,6 +1987,11 @@ export class PgUi {
'timestamp with time zone',
];
case 'User':
case 'CreatedBy':
case 'LastModifiedBy':
return ['character varying'];
case 'AutoNumber':
return [
'int',

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

@ -133,6 +133,52 @@ export class SnowflakeUi {
uicn: '',
system: true,
},
{
column_name: 'created_by',
title: 'nc_created_by',
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: '45',
dtxs: '',
altered: 1,
uidt: UITypes.CreatedBy,
uip: '',
uicn: '',
system: true,
},
{
column_name: 'updated_by',
title: 'nc_updated_by',
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: '45',
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 = {};
switch (col.uidt) {
case 'ID':
@ -800,7 +846,7 @@ export class SnowflakeUi {
return colProp;
}
static getDataTypeListForUiType(col: { uidt: UITypes }, idType: IDType) {
static getDataTypeListForUiType(col: { uidt: UITypes; }, idType: IDType) {
switch (col.uidt) {
case 'ID':
if (idType === 'AG') {
@ -955,6 +1001,11 @@ export class SnowflakeUi {
case 'LastModifiedTime':
return ['TIMESTAMP'];
case 'User':
case 'CreatedBy':
case 'LastModifiedBy':
return ['VARCHAR'];
case 'AutoNumber':
return ['NUMBER', 'INT', 'INTEGER', 'BIGINT'];

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

@ -121,6 +121,52 @@ export class SqliteUi {
uicn: '',
system: true,
},
{
column_name: 'created_by',
title: 'nc_created_by',
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: 'nc_updated_by',
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 = {};
switch (col.uidt) {
case 'ID':
@ -651,7 +697,7 @@ export class SqliteUi {
return colProp;
}
static getDataTypeListForUiType(col: { uidt: UITypes }, idType?: IDType) {
static getDataTypeListForUiType(col: { uidt: UITypes; }, idType?: IDType) {
switch (col.uidt) {
case 'ID':
if (idType === 'AG') {

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

@ -8,6 +8,7 @@ import { nocoExecute } from 'nc-help';
import {
AuditOperationSubTypes,
AuditOperationTypes,
isCreatedOrLastModifiedByCol,
isCreatedOrLastModifiedTimeCol,
isLinksOrLTAR,
isSystemColumn,
@ -103,7 +104,11 @@ function checkColumnRequired(
}
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 }));
switch (column.uidt) {
@ -122,6 +127,20 @@ export async function getColumnName(column: Column<any>, columns?: Column[]) {
return lastModifiedTimeSystemCol.column_name;
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:
return column.column_name;
}
@ -739,7 +758,16 @@ class BaseModelSqlv2 {
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({
base_id: column.base_id,
});
@ -751,7 +779,7 @@ class BaseModelSqlv2 {
user.display_name || user.email,
]);
return qb.toQuery();
}, this.dbDriver.raw(`??`, [column.column_name]).toQuery());
}, this.dbDriver.raw(`??`, [columnName]).toQuery());
qb.orderBy(
sanitize(this.dbDriver.raw(finalStatement)),
@ -2314,6 +2342,18 @@ class BaseModelSqlv2 {
).builder.as(sanitize(column.id)),
);
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:
if (this.isPg) {
if (column.dt === 'bytea') {
@ -2368,7 +2408,7 @@ class BaseModelSqlv2 {
await this.beforeInsert(insertObj, trx, cookie);
}
await this.prepareNocoData(insertObj, true);
await this.prepareNocoData(insertObj, true, cookie);
let response;
// const driver = trx ? trx : this.dbDriver;
@ -2613,7 +2653,7 @@ class BaseModelSqlv2 {
await this.beforeUpdate(data, trx, cookie);
await this.prepareNocoData(updateObj);
await this.prepareNocoData(updateObj, false, cookie);
const prevData = await this.readByPk(
id,
@ -2734,7 +2774,7 @@ class BaseModelSqlv2 {
await this.beforeInsert(insertObj, this.dbDriver, cookie);
await this.prepareNocoData(insertObj, true);
await this.prepareNocoData(insertObj, true, cookie);
let response;
const query = this.dbDriver(this.tnPath).insert(insertObj);
@ -2962,7 +3002,11 @@ class BaseModelSqlv2 {
for (let i = 0; i < this.model.columns.length; ++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(
`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
if (isSingleRecordInsertion) {
@ -3229,7 +3273,7 @@ class BaseModelSqlv2 {
continue;
}
if (!raw) {
await this.prepareNocoData(d);
await this.prepareNocoData(d, false, cookie);
const oldRecord = await this.readByPk(pkValues);
if (!oldRecord) {
@ -3302,6 +3346,9 @@ class BaseModelSqlv2 {
this.dbDriver,
);
if (!args.skipValidationAndHooks) await this.validate(updateData);
await this.prepareNocoData(updateData, false, cookie);
const pkValues = await this._extractPksValues(updateData);
if (pkValues) {
// pk is specified - by pass
@ -3847,7 +3894,11 @@ class BaseModelSqlv2 {
for (let i = 0; i < this.model.columns.length; ++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(
`Column "${column.title}" is auto generated and cannot be updated`,
);
@ -4018,13 +4069,15 @@ class BaseModelSqlv2 {
);
}
await this.updateLastModifiedTime({
await this.updateLastModified({
model: parentTable,
rowIds: [childId],
cookie,
});
await this.updateLastModifiedTime({
await this.updateLastModified({
model: childTable,
rowIds: [rowId],
cookie,
});
}
break;
@ -4046,9 +4099,10 @@ class BaseModelSqlv2 {
{ raw: true },
);
await this.updateLastModifiedTime({
await this.updateLastModified({
model: parentTable,
rowIds: [rowId],
cookie,
});
}
break;
@ -4070,9 +4124,10 @@ class BaseModelSqlv2 {
{ raw: true },
);
await this.updateLastModifiedTime({
await this.updateLastModified({
model: parentTable,
rowIds: [childId],
cookie,
});
}
break;
@ -4168,13 +4223,15 @@ class BaseModelSqlv2 {
{ raw: true },
);
await this.updateLastModifiedTime({
await this.updateLastModified({
model: parentTable,
rowIds: [childId],
cookie,
});
await this.updateLastModifiedTime({
await this.updateLastModified({
model: childTable,
rowIds: [rowId],
cookie,
});
}
break;
@ -4194,9 +4251,10 @@ class BaseModelSqlv2 {
{ raw: true },
);
await this.updateLastModifiedTime({
await this.updateLastModified({
model: parentTable,
rowIds: [rowId],
cookie,
});
}
break;
@ -4216,9 +4274,10 @@ class BaseModelSqlv2 {
{ raw: true },
);
await this.updateLastModifiedTime({
await this.updateLastModified({
model: parentTable,
rowIds: [childId],
cookie,
});
}
break;
@ -4660,22 +4719,43 @@ class BaseModelSqlv2 {
await this.model.getColumns();
}
const userColumns = [];
let userColumns = [];
const columns = childTable ? childTable.columns : this.model.columns;
for (const col of columns) {
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);
}
} else {
if (col.uidt === UITypes.User) {
if (
[UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(
col.uidt,
)
) {
userColumns.push(col);
}
}
}
// filter user columns that are not present in data
if (userColumns.length) {
if (Array.isArray(data)) {
const row = data[0];
if (row) {
userColumns = userColumns.filter((col) => col.id in row);
}
} else {
userColumns = userColumns.filter((col) => col.id in data);
}
}
// process user columns that are present in data
if (userColumns.length) {
const baseUsers = await BaseUser.getUsersList({
base_id: childTable ? childTable.base_id : this.model.base_id,
@ -4694,30 +4774,32 @@ class BaseModelSqlv2 {
}
protected _convertUserFormat(
userColumns: Record<string, any>[],
userColumns: Column[],
baseUsers: Partial<User>[],
d: Record<string, any>,
) {
try {
if (d) {
for (const col of userColumns) {
if (d[col.id] && d[col.id].length) {
d[col.id] = d[col.id].split(',');
} else {
d[col.id] = null;
}
const availableUserColumns = userColumns.filter(
(col) => d[col.id] && d[col.id].length,
);
for (const col of availableUserColumns) {
d[col.id] = d[col.id].split(',');
if (d[col.id]?.length) {
d[col.id] = d[col.id].map((fid) => {
const { id, email, display_name } = baseUsers.find(
(u) => u.id === fid,
);
return {
id,
email,
display_name: display_name?.length ? display_name : null,
};
});
d[col.id] = d[col.id].map((fid) => {
const { id, email, display_name } = baseUsers.find(
(u) => u.id === fid,
);
return {
id,
email,
display_name: display_name?.length ? display_name : null,
};
});
// CreatedBy and LastModifiedBy are always singular
if ([UITypes.CreatedBy, UITypes.LastModifiedBy].includes(col.uidt)) {
d[col.id] = d[col.id][0];
}
}
}
@ -5130,13 +5212,15 @@ class BaseModelSqlv2 {
raw: true,
});
await this.updateLastModifiedTime({
await this.updateLastModified({
model: parentTable,
rowIds: childIds,
cookie,
});
await this.updateLastModifiedTime({
await this.updateLastModified({
model: childTable,
rowIds: [rowId],
cookie,
});
}
break;
@ -5213,9 +5297,10 @@ class BaseModelSqlv2 {
}
await this.execAndParse(updateQb, null, { raw: true });
await this.updateLastModifiedTime({
await this.updateLastModified({
model: parentTable,
rowIds: [rowId],
cookie,
});
}
break;
@ -5260,9 +5345,10 @@ class BaseModelSqlv2 {
{ raw: true },
);
await this.updateLastModifiedTime({
await this.updateLastModified({
model: parentTable,
rowIds: [rowId],
cookie,
});
}
break;
@ -5406,13 +5492,15 @@ class BaseModelSqlv2 {
);
await this.execAndParse(delQb, null, { raw: true });
await this.updateLastModifiedTime({
await this.updateLastModified({
model: parentTable,
rowIds: childIds,
cookie,
});
await this.updateLastModifiedTime({
await this.updateLastModified({
model: childTable,
rowIds: [rowId],
cookie,
});
}
break;
@ -5495,9 +5583,10 @@ class BaseModelSqlv2 {
{ raw: true },
);
await this.updateLastModifiedTime({
await this.updateLastModified({
model: parentTable,
rowIds: [rowId],
cookie,
});
}
break;
@ -5545,9 +5634,10 @@ class BaseModelSqlv2 {
{ raw: true },
);
await this.updateLastModifiedTime({
await this.updateLastModified({
model: parentTable,
rowIds: [childIds[0]],
cookie,
});
}
break;
@ -5634,26 +5724,40 @@ class BaseModelSqlv2 {
}
}
async updateLastModifiedTime({
async updateLastModified({
rowIds,
cookie,
model = this.model,
knex = this.dbDriver,
}: {
rowIds: any | any[];
cookie?: { user?: any };
model?: Model;
knex?: XKnex;
}) {
const columnName = await model.getColumns().then((columns) => {
return columns.find(
(c) => c.uidt === UITypes.LastModifiedTime && c.system,
)?.column_name;
});
const columns = await model.getColumns();
if (!columnName) return;
const updateObject = {};
const qb = knex(model.table_name).update({
[columnName]: Noco.ncMeta.now(),
});
const lastModifiedTimeColumn = columns.find(
(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]) {
qb.orWhere(await this._wherePk(rowId));
@ -5662,7 +5766,7 @@ class BaseModelSqlv2 {
await this.execAndParse(qb, null, { raw: true });
}
async prepareNocoData(data, isInsertData = false) {
async prepareNocoData(data, isInsertData = false, cookie?: { user?: any }) {
if (
this.model.columns.some((c) =>
[
@ -5670,19 +5774,25 @@ class BaseModelSqlv2 {
UITypes.User,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
].includes(c.uidt),
)
) {
for (const column of this.model.columns) {
if (
isInsertData &&
column.uidt === UITypes.CreatedTime &&
column.system
) {
data[column.column_name] = Noco.ncMeta.now();
}
if (column.uidt === UITypes.LastModifiedTime && column.system) {
data[column.column_name] = isInsertData ? null : Noco.ncMeta.now();
if (column.system) {
if (isInsertData) {
if (column.uidt === UITypes.CreatedTime) {
data[column.column_name] = Noco.ncMeta.now();
} else if (column.uidt === UITypes.CreatedBy) {
data[column.column_name] = cookie?.user?.id;
}
}
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 (data[column.column_name]) {
@ -5699,7 +5809,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]) {
const userIds = [];

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

@ -453,9 +453,14 @@ const parseConditionV2 = async (
builder,
);
} else if (
column.uidt === UITypes.User &&
[UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(
column.uidt,
) &&
['like', 'nlike'].includes(filter.comparison_op)
) {
// get column name for CreatedBy, LastModifiedBy
column.column_name = await getColumnName(column);
const baseUsers = await BaseUser.getUsersList({
base_id: column.base_id,
});

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

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

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

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

11
packages/nocodb/src/helpers/columnHelpers.ts

@ -295,9 +295,14 @@ export const getRefColumnIfAlias = async (
columns?: Column[],
) => {
if (
!([UITypes.CreatedTime, UITypes.LastModifiedTime] as UITypes[]).includes(
column.uidt,
)
!(
[
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
] as UITypes[]
).includes(column.uidt)
)
return column;

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

@ -1,4 +1,5 @@
import {
isCreatedOrLastModifiedByCol,
isCreatedOrLastModifiedTimeCol,
isSystemColumn,
RelationTypes,
@ -152,7 +153,9 @@ const getAst = async ({
}
let isRequested;
if (getHiddenColumn) {
if (isCreatedOrLastModifiedByCol(col) && col.system) {
isRequested = false;
} else if (getHiddenColumn) {
isRequested =
!isSystemColumn(col) ||
(isCreatedOrLastModifiedTimeCol(col) && col.system) ||

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

@ -171,7 +171,9 @@ export async function serializeCellValue({
)
.join(', ');
}
case UITypes.User: {
case UITypes.User:
case UITypes.CreatedBy:
case UITypes.LastModifiedBy: {
let data = value;
try {
if (typeof value === 'string') {
@ -179,7 +181,9 @@ export async function serializeCellValue({
}
} catch {}
return (data || []).map((user) => `${user.email}`).join(', ');
return (data ? (Array.isArray(data) ? data : [data]) : [])
.map((user) => `${user.email}`)
.join(', ');
}
case UITypes.Lookup:
{

2
packages/nocodb/src/modules/global/init-meta-service.provider.ts

@ -27,7 +27,7 @@ export const InitMetaServiceProvider: Provider = {
const config = await NcConfig.createByEnv();
// set version
process.env.NC_VERSION = '0111004';
process.env.NC_VERSION = '0111005';
// init cache
await NocoCache.init();

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

@ -454,9 +454,12 @@ export class ExportService {
}
break;
case UITypes.User:
case UITypes.CreatedBy:
case UITypes.LastModifiedBy:
if (v) {
const userIds = [];
for (const user of v as { id: string }[]) {
const userRecord = Array.isArray(v) ? v : [v];
for (const user of userRecord) {
userIds.push(user.id);
}
row[colId] = userIds.join(',');

9
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.CreatedTime ||
a.uidt === UITypes.LastModifiedTime ||
a.uidt === UITypes.CreatedBy ||
a.uidt === UITypes.LastModifiedBy ||
a.uidt === UITypes.Barcode,
),
);
@ -897,13 +899,18 @@ export class ImportService {
}
} else if (
col.uidt === UITypes.CreatedTime ||
col.uidt === UITypes.LastModifiedTime
col.uidt === UITypes.LastModifiedTime ||
col.uidt === UITypes.CreatedBy ||
col.uidt === UITypes.LastModifiedBy
) {
if (col.system) continue;
const freshModelData = await this.columnsService.columnAdd({
tableId: getIdOrExternalId(getParentIdentifier(col.id)),
column: withoutId({
...flatCol,
// provide column_name to avoid ajv error
// it will be ignored by the service
column_name: 'system',
system: false,
}) as any,
req: param.req,

10
packages/nocodb/src/run/testDocker.ts

@ -46,6 +46,16 @@ process.env[`NC_ALLOW_LOCAL_HOOKS`] = 'true';
},
);
console.log(admin_response.data);
} else {
admin_response = await axios.post(
`http://localhost:${
process.env.PORT || 8080
}/api/v1/auth/user/signin`,
{
email: 'user@nocodb.com',
password: 'Password123.',
},
);
}
for (let i = 0; i < 4; i++) {

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

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

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

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

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

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

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

@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import {
AppEvents,
isCreatedOrLastModifiedByCol,
isCreatedOrLastModifiedTimeCol,
isLinksOrLTAR,
isVirtualCol,
@ -174,6 +175,7 @@ export class ColumnsService {
if (
!isVirtualCol(param.column) &&
!isCreatedOrLastModifiedTimeCol(param.column) &&
!isCreatedOrLastModifiedByCol(param.column) &&
!(await Column.checkTitleAvailable({
column_name: param.column.column_name,
fk_model_id: column.fk_model_id,
@ -200,6 +202,7 @@ export class ColumnsService {
if (
isCreatedOrLastModifiedTimeCol(column) ||
isCreatedOrLastModifiedByCol(column) ||
[
UITypes.Lookup,
UITypes.Rollup,
@ -320,7 +323,12 @@ export class ColumnsService {
`Updating ${colBody.uidt} => ${colBody.uidt} is not implemented`,
);
} 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
await Column.update(param.columnId, {
@ -1711,6 +1719,8 @@ export class ColumnsService {
break;
case UITypes.CreatedTime:
case UITypes.LastModifiedTime:
case UITypes.CreatedBy:
case UITypes.LastModifiedBy:
{
let columnName: string;
const columns = await table.getColumns();
@ -1721,10 +1731,26 @@ export class ColumnsService {
);
if (!existingColumn) {
columnName =
colBody.uidt === UITypes.CreatedTime
? 'created_at'
: 'updated_at';
let columnTitle;
switch (colBody.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 = 'nc_created_by';
break;
case UITypes.LastModifiedBy:
columnName = 'updated_by';
columnTitle = 'nc_updated_by';
break;
}
// todo: check type as well
const dbColumn = columns.find((c) => c.column_name === columnName);
@ -1765,10 +1791,7 @@ export class ColumnsService {
await sqlMgr.sqlOpPlus(source, 'tableUpdate', tableUpdateBody);
}
const title = getUniqueColumnAliasName(
table.columns,
UITypes.CreatedTime ? 'CreatedAt' : 'UpdatedAt',
);
const title = getUniqueColumnAliasName(table.columns, columnTitle);
await Column.insert({
...colBody,
@ -2233,9 +2256,11 @@ export class ColumnsService {
/* 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.LastModifiedTime:
case UITypes.CreatedBy:
case UITypes.LastModifiedBy:
{
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 {
AppEvents,
isCreatedOrLastModifiedByCol,
isCreatedOrLastModifiedTimeCol,
isLinksOrLTAR,
isVirtualCol,
@ -391,21 +392,49 @@ export class TablesService {
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(
(c) => c.uidt === uidt,
) 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 = 'nc_created_by';
break;
case UITypes.LastModifiedBy:
columnName = 'updated_by';
columnTitle = 'nc_updated_by';
break;
}
const colName = getUniqueColumnName(
tableCreatePayLoad.columns as any[],
uidt === UITypes.CreatedTime ? 'created_at' : 'updated_at',
columnName,
);
const colAlias = getUniqueColumnAliasName(
tableCreatePayLoad.columns as any[],
uidt === UITypes.CreatedTime ? 'CreatedAt' : 'UpdatedAt',
columnTitle,
);
if (!col || !col.system) {
tableCreatePayLoad.columns.push({
...(await getColumnPropsFromUIDT({ uidt } as any, source)),
@ -521,7 +550,8 @@ export class TablesService {
for (const column of param.table.columns) {
if (
!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);
@ -556,7 +586,11 @@ export class TablesService {
param.table.columns
// exclude alias columns from column list
?.filter((c) => {
return !isCreatedOrLastModifiedTimeCol(c) || (c as any).system;
return (
!isCreatedOrLastModifiedTimeCol(c) ||
!isCreatedOrLastModifiedByCol(c) ||
(c as any).system
);
})
.map(async (c) => ({
...(await getColumnPropsFromUIDT(c as any, source)),

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

@ -16,7 +16,7 @@ import ncHookUpgrader from './ncHookUpgrader';
import ncProjectConfigUpgrader from './ncProjectConfigUpgrader';
import ncXcdbLTARUpgrader from './ncXcdbLTARUpgrader';
import ncXcdbLTARIndexUpgrader from './ncXcdbLTARIndexUpgrader';
import ncXcdbCreatedAndUpdatedTimeUpgrader from './ncXcdbCreatedAndUpdatedTimeUpgrader';
import ncXcdbCreatedAndUpdatedSystemFieldsUpgrader from './ncXcdbCreatedAndUpdatedSystemFieldsUpgrader';
import type { MetaService } from '~/meta/meta.service';
import type { NcConfig } from '~/interface/config';
@ -145,7 +145,7 @@ export default class NcUpgrader {
{ name: '0107004', handler: ncProjectConfigUpgrader },
{ name: '0108002', handler: ncXcdbLTARUpgrader },
{ name: '0111002', handler: ncXcdbLTARIndexUpgrader },
{ name: '0111004', handler: ncXcdbCreatedAndUpdatedTimeUpgrader },
{ name: '0111005', handler: ncXcdbCreatedAndUpdatedSystemFieldsUpgrader },
];
}
}

272
packages/nocodb/src/version-upgrader/ncXcdbCreatedAndUpdatedTimeUpgrader.ts → packages/nocodb/src/version-upgrader/ncXcdbCreatedAndUpdatedSystemFieldsUpgrader.ts

@ -2,6 +2,7 @@ import { UITypes } from 'nocodb-sdk';
import type { NcUpgraderCtx } from './NcUpgrader';
import type { MetaService } from '~/meta/meta.service';
import type { Base } from '~/models';
import Noco from '~/Noco';
import { MetaTable } from '~/utils/globals';
import { Column, Model, Source } from '~/models';
import {
@ -15,15 +16,18 @@ import NcConnectionMgrv2 from '~/utils/common/NcConnectionMgrv2';
import getColumnUiType from '~/helpers/getColumnUiType';
import RequestQueue from '~/utils/RequestQueue';
// Example Usage:
// An upgrader for upgrading created_at and updated_at columns
// to system column and convert to new uidt CreatedTime and LastModifiedTime
const logger = {
log: (message: string) => {
console.log(
`[ncXcdbCreatedAndUpdatedTimeUpgrader ${Date.now()}] ` + message,
`[ncXcdbCreatedAndUpdatedSystemFieldsUpgrader ${Date.now()}] ` + message,
);
},
error: (message: string) => {
console.error(
`[ncXcdbCreatedAndUpdatedSystemFieldsUpgrader ${Date.now()}] ` + message,
);
},
};
@ -54,78 +58,113 @@ async function upgradeModels({
ncMeta,
source,
base,
models,
}: {
ncMeta: MetaService;
source: Source;
base: Base;
models: Model[];
}) {
const models = await Model.list(
{
base_id: source.base_id,
source_id: source.id,
},
ncMeta,
);
// get existing columns from database
const sqlClient = await NcConnectionMgrv2.getSqlClient(source, ncMeta.knex);
const sqlMgr = ProjectMgrv2.getSqlMgr({ id: source.base_id }, ncMeta);
await Promise.all(
models.map(async (model: any) => {
models.map(async (model) => {
if (model.mm) return;
try {
const columns = model.columns;
const oldColumns = columns.map((c) => ({ ...c, cn: c.column_name }));
let isCreatedTimeExists = false;
let isLastModifiedTimeExists = false;
let isCreatedByExists = false;
let isLastModifiedByExists = false;
for (const column of columns) {
if (
![
UITypes.DateTime,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
].includes(column.uidt)
)
continue;
logger.log(
`Upgrading model '${model.title}'(${model.id}) from base '${base.title}'(${base.id}})`,
);
const columns = await model.getColumns(ncMeta);
const oldColumns = columns.map((c) => ({ ...c, cn: c.column_name }));
let isCreatedTimeExists = false;
let isLastModifiedTimeExists = false;
for (const column of columns) {
if (column.uidt !== UITypes.DateTime) continue;
// if column is created_at or updated_at, update the uidt in meta
if (column.column_name === 'created_at') {
isCreatedTimeExists = true;
await Column.update(
column.id,
{
...column,
uidt: UITypes.CreatedTime,
system: true,
},
ncMeta,
true,
);
if (column.uidt === UITypes.CreatedBy && column.system) {
isCreatedByExists = true;
continue;
}
if (column.uidt === UITypes.LastModifiedBy && column.system) {
isLastModifiedByExists = true;
continue;
}
if (
[UITypes.CreatedBy, UITypes.LastModifiedBy].includes(column.uidt)
) {
continue;
}
if (column.uidt === UITypes.CreatedTime && column.system) {
isCreatedTimeExists = true;
continue;
}
if (column.uidt === UITypes.LastModifiedTime && column.system) {
isLastModifiedTimeExists = true;
continue;
}
// if column is created_at or updated_at, update the uidt in meta
if (column.column_name === 'created_at') {
isCreatedTimeExists = true;
if (column.uidt !== UITypes.CreatedTime || !column.system) {
await Column.update(
column.id,
{
...column,
uidt: UITypes.CreatedTime,
system: true,
},
ncMeta,
true,
);
}
/* Enable if planning to remove trigger
/* Enable if planning to remove trigger
if (source.type === 'pg') {
// delete pg trigger if exists
await deletePgTrigger({ column, ncMeta, model });
}*/
}
if (column.column_name === 'updated_at') {
isLastModifiedTimeExists = true;
await Column.update(
column.id,
{
...column,
uidt: UITypes.LastModifiedTime,
system: true,
cdf: '',
au: false,
},
ncMeta,
true,
);
}
*/
}
if (column.column_name === 'updated_at') {
isLastModifiedTimeExists = true;
if (column.uidt !== UITypes.LastModifiedTime || !column.system) {
await Column.update(
column.id,
{
...column,
uidt: UITypes.LastModifiedTime,
system: true,
},
ncMeta,
true,
);
}
}
}
}
if (!isCreatedTimeExists || !isLastModifiedTimeExists) {
// get existing columns from database
const sqlClient = await NcConnectionMgrv2.getSqlClient(
source,
ncMeta.knex,
);
if (
isCreatedTimeExists &&
isLastModifiedTimeExists &&
isCreatedByExists &&
isLastModifiedByExists
) {
return;
}
const dbColumns =
(
@ -143,7 +182,7 @@ async function upgradeModels({
return;
}
// create created_at and updated_at columns
// create created_at & updated_at and created_by & updated_by columns
const newColumns = [];
const existingDbColumns = [];
@ -220,11 +259,74 @@ async function upgradeModels({
}
}
if (!isCreatedByExists) {
const columnName = getUniqueColumnName(columns, 'created_by');
const dbColumn = dbColumns.find((c) => c.cn === columnName);
// if column already exist in db but not in meta, just update the meta (partial upgraded case)
if (dbColumn) {
existingDbColumns.push({
uidt: UITypes.CreatedBy,
...dbColumn,
column_name: columnName,
title: getUniqueColumnAliasName(columns, 'nc_created_by'),
system: true,
});
} else {
newColumns.push({
...(await getColumnPropsFromUIDT(
{
uidt: UITypes.CreatedBy,
column_name: getUniqueColumnName(
[...columns, ...dbColumns],
'created_by',
),
title: getUniqueColumnAliasName(columns, 'nc_created_by'),
},
source,
)),
cdf: null,
system: true,
altered: Altered.NEW_COLUMN,
});
}
}
if (!isLastModifiedByExists) {
const columnName = getUniqueColumnName(columns, 'updated_by');
const dbColumn = dbColumns.find((c) => c.cn === columnName);
// if column already exist in db but not in meta, just update the meta (partial upgraded case)
if (dbColumn) {
existingDbColumns.push({
uidt: UITypes.LastModifiedBy,
...dbColumn,
column_name: columnName,
title: getUniqueColumnAliasName(columns, 'nc_updated_by'),
system: true,
});
} else {
newColumns.push({
...(await getColumnPropsFromUIDT(
{
uidt: UITypes.LastModifiedBy,
column_name: getUniqueColumnName(
[...columns, ...dbColumns],
'updated_by',
),
title: getUniqueColumnAliasName(columns, 'nc_updated_by'),
},
source,
)),
cdf: null,
system: true,
altered: Altered.NEW_COLUMN,
});
}
}
// alter table and add new columns if any
if (newColumns.length) {
logger.log(
`Altering table '${model.title}'(${model.id}) from base '${base.title}'(${base.id}}) for new columns`,
);
// update column in db
const tableUpdateBody = {
...model,
@ -235,24 +337,24 @@ async function upgradeModels({
cn: c.column_name,
})),
};
const sqlMgr = ProjectMgrv2.getSqlMgr({ id: source.base_id }, ncMeta);
await sqlMgr.sqlOpPlus(source, 'tableUpdate', tableUpdateBody);
}
for (const newColumn of [...existingDbColumns, ...newColumns]) {
await Column.insert(
{
...newColumn,
system: 1,
system: true,
fk_model_id: model.id,
},
ncMeta,
);
}
} catch (e) {
logger.error(
`Upgrading model '${model.title}'(${model.id}) from base '${base.title}'(${base.id}}) failed`,
);
throw e;
}
logger.log(
`Upgraded model '${model.title}'(${model.id}) from base '${base.title}'(${base.id}})`,
);
}),
);
}
@ -268,11 +370,15 @@ export default async function ({ ncMeta }: NcUpgraderCtx) {
eq: 1,
},
},
{
is_local: {
eq: 1,
},
},
...(Noco.isEE()
? [
{
is_local: {
eq: 1,
},
},
]
: []),
],
},
});
@ -289,7 +395,7 @@ export default async function ({ ncMeta }: NcUpgraderCtx) {
if (!base || base.deleted) {
logger.log(
`Skipped deleted base source '${source.alias || source.id}' - ${
base.id
base?.id
}`,
);
return Promise.resolve();
@ -297,13 +403,25 @@ export default async function ({ ncMeta }: NcUpgraderCtx) {
// update the meta props
return requestQueue.enqueue(async () => {
const models = await Model.list(
{
base_id: source.base_id,
source_id: source.id,
},
ncMeta,
);
for (const model of models) {
await model.getColumns(ncMeta);
}
logger.log(
`Upgrading base ${base.title}(${base.id},${source.id}) (${i + 1}/${
sources.length
})`,
);
return upgradeModels({ ncMeta, source, base }).then(() => {
await upgradeModels({ ncMeta, source, models, base }).then(() => {
logger.log(
`Upgraded base '${base.title}'(${base.id},${source.id}) (${i + 1}/${
sources.length

9
packages/nocodb/tests/unit/factory/row.ts

@ -1,4 +1,8 @@
import { isCreatedOrLastModifiedTimeCol, UITypes } from 'nocodb-sdk'
import {
isCreatedOrLastModifiedByCol,
isCreatedOrLastModifiedTimeCol,
UITypes,
} from 'nocodb-sdk';
import request from 'supertest';
import Model from '../../../src/models/Model';
import NcConnectionMgrv2 from '../../../src/utils/common/NcConnectionMgrv2';
@ -258,7 +262,8 @@ const generateDefaultRowAttributes = ({
column.uidt === UITypes.LinkToAnotherRecord ||
column.uidt === UITypes.ForeignKey ||
column.uidt === UITypes.ID ||
isCreatedOrLastModifiedTimeCol(column)
isCreatedOrLastModifiedTimeCol(column) ||
isCreatedOrLastModifiedByCol(column)
) {
return acc;
}

3
packages/nocodb/tests/unit/init/index.ts

@ -39,7 +39,7 @@ export default async function (forceReset = false, roles = 'editor') {
// }
await cleanupMeta();
const { token } = await createUser({ app: server }, { roles });
const { token, user } = await createUser({ app: server }, { roles });
const extra: any = {};
@ -61,6 +61,7 @@ export default async function (forceReset = false, roles = 'editor') {
return {
app: server,
token,
user,
dbConfig: TestDbMngr.dbConfig,
sakilaDbConfig: TestDbMngr.getSakilaDbConfig(),
...extra,

50
packages/nocodb/tests/unit/model/tests/baseModelSql.test.ts

@ -42,7 +42,7 @@ function baseModelSqlTests() {
it('Insert record', async () => {
const request = {
clientIp: '::ffff:192.0.0.1',
user: { email: 'test@example.com' },
user: context.user,
};
const columns = await table.getColumns();
@ -54,11 +54,15 @@ function baseModelSqlTests() {
);
const insertedRow = (await baseModelSql.list())[0];
inputData.CreatedAt = response.CreatedAt;
inputData.UpdatedAt = response.UpdatedAt;
inputData.CreatedBy = {
id: context.user.id,
email: context.user.email,
display_name: context.user.display_name,
};
inputData.UpdatedBy = null;
expect(insertedRow).to.include(inputData);
expect(insertedRow).to.include(response);
expect(insertedRow).to.deep.include(inputData);
expect(insertedRow).to.deep.include(response);
const rowInsertedAudit = (await Audit.baseAuditList(base.id, {})).find(
(audit) => audit.op_sub_type === 'INSERT',
@ -80,7 +84,7 @@ function baseModelSqlTests() {
const columns = await table.getColumns();
const request = {
clientIp: '::ffff:192.0.0.1',
user: { email: 'test@example.com' },
user: context.user,
};
const bulkData = Array(10)
.fill(0)
@ -125,7 +129,7 @@ function baseModelSqlTests() {
it('Update record', async () => {
const request = {
clientIp: '::ffff:192.0.0.1',
user: { email: 'test@example.com' },
user: context.user,
};
const columns = await table.getColumns();
@ -162,7 +166,7 @@ function baseModelSqlTests() {
const columns = await table.getColumns();
const request = {
clientIp: '::ffff:192.0.0.1',
user: { email: 'test@example.com' },
user: context.user,
};
const bulkData = Array(10)
.fill(0)
@ -172,10 +176,18 @@ function baseModelSqlTests() {
const insertedRows: any[] = await baseModelSql.list();
await baseModelSql.bulkUpdate(
insertedRows.map(({ CreatedAt: _, UpdatedAt: __, ...row }) => ({
...row,
Title: `new-${row['Title']}`,
})),
insertedRows.map(
({
CreatedAt: _,
UpdatedAt: __,
CreatedBy: ___,
UpdatedBy: ____,
...row
}) => ({
...row,
Title: `new-${row['Title']}`,
}),
),
{ cookie: request },
);
@ -206,7 +218,7 @@ function baseModelSqlTests() {
const columns = await table.getColumns();
const request = {
clientIp: '::ffff:192.0.0.1',
user: { email: 'test@example.com' },
user: context.user,
};
const bulkData = Array(10)
.fill(0)
@ -256,7 +268,7 @@ function baseModelSqlTests() {
it('Delete record', async () => {
const request = {
clientIp: '::ffff:192.0.0.1',
user: { email: 'test@example.com' },
user: context.user,
params: { id: 1 },
};
@ -294,7 +306,7 @@ function baseModelSqlTests() {
const columns = await table.getColumns();
const request = {
clientIp: '::ffff:192.0.0.1',
user: { email: 'test@example.com' },
user: context.user,
};
const bulkData = Array(10)
.fill(0)
@ -337,7 +349,7 @@ function baseModelSqlTests() {
const columns = await table.getColumns();
const request = {
clientIp: '::ffff:192.0.0.1',
user: { email: 'test@example.com' },
user: context.user,
};
const bulkData = Array(10)
.fill(0)
@ -403,7 +415,7 @@ function baseModelSqlTests() {
const columns = await table.getColumns();
const request = {
clientIp: '::ffff:192.0.0.1',
user: { email: 'test@example.com' },
user: context.user,
};
await baseModelSql.nestedInsert(
@ -462,7 +474,7 @@ function baseModelSqlTests() {
const columns = await table.getColumns();
const request = {
clientIp: '::ffff:192.0.0.1',
user: { email: 'test@example.com' },
user: context.user,
};
await baseModelSql.insert(
@ -530,7 +542,7 @@ function baseModelSqlTests() {
const columns = await table.getColumns();
const request = {
clientIp: '::ffff:192.0.0.1',
user: { email: 'test@example.com' },
user: context.user,
};
await baseModelSql.insert(

262
packages/nocodb/tests/unit/rest/tests/columnTypeSpecific.test.ts

@ -1,5 +1,4 @@
import 'mocha';
import { title } from 'process';
import request from 'supertest';
import { UITypes } from 'nocodb-sdk';
import { expect } from 'chai';
@ -10,12 +9,7 @@ import {
createQrCodeColumn,
deleteColumn,
} from '../../factory/column';
import {
createTable,
getColumnsByAPI,
getTable,
getTableByAPI,
} from '../../factory/table';
import { createTable, getColumnsByAPI, getTable } from '../../factory/table';
import { createBulkRows, listRow, rowMixedValue } from '../../factory/row';
import type Model from '../../../../src/models/Model';
import type Base from '~/models/Base';
@ -37,6 +31,29 @@ function columnTypeSpecificTests() {
const qrValueReferenceColumnTitle = 'Qr Value Column';
const qrCodeReferenceColumnTitle = 'Qr Code Column';
const defaultTableColumns = [
{
title: 'Id',
uidt: UITypes.ID,
system: false,
},
{
title: 'DateField',
uidt: UITypes.Date,
system: false,
},
{
title: 'CreatedAt',
uidt: UITypes.CreatedTime,
system: true,
},
{
title: 'UpdatedAt',
uidt: UITypes.LastModifiedTime,
system: true,
},
];
describe('Qr Code Column', () => {
beforeEach(async function () {
console.time('#### columnTypeSpecificTests');
@ -89,7 +106,7 @@ function columnTypeSpecificTests() {
),
).to.eq(true);
const response = await request(context.app)
const _response = await request(context.app)
.delete(`/api/v1/db/meta/columns/${qrValueReferenceColumn.id}`)
.set('xc-auth', context.token)
.send({});
@ -110,7 +127,7 @@ function columnTypeSpecificTests() {
let columns: any[];
let unfilteredRecords: any[] = [];
describe('CreatedAt, LastModifiedAt Field', () => {
describe('System fields', () => {
beforeEach(async function () {
context = await init();
base = await createProject(context);
@ -133,7 +150,7 @@ function columnTypeSpecificTests() {
columns = await table.getColumns();
const rowAttributes = [];
const rowAttributes: any = [];
for (let i = 0; i < 100; i++) {
const row = {
DateField: rowMixedValue(columns[1], i),
@ -155,43 +172,37 @@ function columnTypeSpecificTests() {
describe('Basic verification', async () => {
it('New table: verify system fields are added by default', async () => {
// Id, Date, CreatedAt, LastModifiedAt
expect(columns.length).to.equal(4);
expect(columns[2].title).to.equal('CreatedAt');
expect(columns[2].uidt).to.equal(UITypes.CreatedTime);
expect(!!columns[2].system).to.equal(true);
expect(columns[3].title).to.equal('UpdatedAt');
expect(columns[3].uidt).to.equal(UITypes.LastModifiedTime);
expect(!!columns[3].system).to.equal(true);
expect(columns.length).to.equal(defaultTableColumns.length);
for (let i = 0; i < defaultTableColumns.length; i++) {
expect(columns[i].title).to.equal(defaultTableColumns[i].title);
expect(columns[i].uidt).to.equal(defaultTableColumns[i].uidt);
expect(columns[i].system).to.equal(defaultTableColumns[i].system);
}
});
it('New table: should not be able to delete system fields', async () => {
await request(context.app)
.delete(`/api/v2/meta/columns/${columns[2].id}`)
.set('xc-auth', context.token)
.send({})
.expect(400);
await request(context.app)
.delete(`/api/v2/meta/columns/${columns[3].id}`)
.set('xc-auth', context.token)
.send({})
.expect(400);
// try to delete system fields (using v1 api)
await request(context.app)
.delete(`/api/v1/db/meta/columns/${columns[2].id}`)
.set('xc-auth', context.token)
.send({})
.expect(400);
await request(context.app)
.delete(`/api/v1/db/meta/columns/${columns[3].id}`)
.set('xc-auth', context.token)
.send({})
.expect(400);
// try to delete system fields
for (let i = 0; i < defaultTableColumns.length; i++) {
if (!defaultTableColumns[i].system) return;
await request(context.app)
.delete(`/api/v2/meta/columns/${columns[i].id}`)
.set('xc-auth', context.token)
.send({})
.expect(400);
// try to delete system fields (using v1 api)
await request(context.app)
.delete(`/api/v1/db/meta/columns/${columns[i].id}`)
.set('xc-auth', context.token)
.send({})
.expect(400);
}
});
it('New record: verify created-at is filled with current dateTime, last-modified-at is null', async () => {
// get current date time
it.only('New record: verify system fields', async () => {
// created-at is filled with current dateTime, last-modified-at is null
// created-by is filled with current user, last-modified-by is null
const currentDateTime = new Date();
const storedDateTime = new Date(unfilteredRecords[0].CreatedAt);
@ -203,7 +214,7 @@ function columnTypeSpecificTests() {
expect(unfilteredRecords[0].UpdatedAt).to.equal(null);
});
it('Modify record: verify last-modified-at is updated', async () => {
it('Modify record: verify last-modified-at & modified-by is updated', async () => {
// get current date time
const currentDateTime = new Date();
const d1 = new Date();
@ -269,13 +280,20 @@ function columnTypeSpecificTests() {
// calculate difference between current date time and stored date time
difference = storedDateTime2.getTime() - storedDateTime1.getTime();
expect(difference).to.be.greaterThan(1500);
// verify modified by
expect(updatedRecord[0].UpdatedBy).to.not.equal(null);
expect(updatedRecord[0].UpdatedBy[0].email).to.equal(
'test@example.com',
);
expect(updatedRecord[0].UpdatedBy[0].display_name).to.equal(null);
});
it('Modify record: verify that system fields are RO', async () => {
const d1 = new Date();
d1.setDate(d1.getDate() - 200);
// update record
// update record with date system fields
await request(context.app)
.patch(`/api/v2/tables/${table.id}/records`)
.set('xc-auth', context.token)
@ -287,9 +305,22 @@ function columnTypeSpecificTests() {
},
])
.expect(400);
// update record with user system fields
await request(context.app)
.patch(`/api/v2/tables/${table.id}/records`)
.set('xc-auth', context.token)
.send([
{
Id: unfilteredRecords[0].Id,
CreatedBy: 'test@example.com',
UpdatedBy: 'test@example.com',
},
])
.expect(400);
});
it('Add field: verify contents of both fields are same & new field is RO', async () => {
it('Add field: CreatedAt, verify contents of both fields are same & new field is RO', async () => {
// add another CreatedTime field
await createColumn(context, table, {
title: 'CreatedAt2',
@ -304,9 +335,16 @@ function columnTypeSpecificTests() {
const records = await listRow({ base, table });
// verify contents of both fields are same
expect(columns.columns[4].title).to.equal('CreatedAt2');
expect(columns.columns[4].uidt).to.equal(UITypes.CreatedTime);
expect(!!columns.columns[4].system).to.equal(false);
expect(columns.columns[defaultTableColumns.length].title).to.equal(
'CreatedAt2',
);
expect(columns.columns[defaultTableColumns.length].uidt).to.equal(
UITypes.CreatedTime,
);
expect(columns.columns[defaultTableColumns.length].system).to.equal(
false,
);
expect(records[0].CreatedAt).to.equal(records[0].CreatedAt2);
const d1 = new Date();
@ -325,7 +363,79 @@ function columnTypeSpecificTests() {
.expect(400);
});
it('Delete & add field: verify contents of both fields are same', async () => {
it('Add field: CreatedBy, LastModifiedBy verify contents of both fields are proper & new field is RO', async () => {
// add another CreatedBy field
await createColumn(context, table, {
title: 'CreatedBy',
uidt: UITypes.CreatedBy,
column_name: 'CreatedBy',
});
// add another ModifiedBy field
await createColumn(context, table, {
title: 'LastModifiedBy',
uidt: UITypes.LastModifiedBy,
column_name: 'LastModifiedBy',
});
// get all columns
const columns = await getColumnsByAPI(context, base, table);
// get all records
const records = await listRow({ base, table });
// verify contents of both fields are same
expect(columns.columns[defaultTableColumns.length].title).to.equal(
'CreatedBy',
);
expect(columns.columns[defaultTableColumns.length].uidt).to.equal(
UITypes.CreatedBy,
);
expect(columns.columns[defaultTableColumns.length].system).to.equal(
false,
);
expect(records[0].CreatedBy).to.deep.equal({
id: context.user.id,
email: context.user.email,
display_name: context.user.display_name,
});
expect(columns.columns[defaultTableColumns.length + 1].title).to.equal(
'LastModifiedBy',
);
expect(columns.columns[defaultTableColumns.length + 1].uidt).to.equal(
UITypes.LastModifiedBy,
);
expect(columns.columns[defaultTableColumns.length + 1].system).to.equal(
false,
);
expect(records[0].UpdatedBy).to.deep.equal(null);
// update record should fail
await request(context.app)
.patch(`/api/v2/tables/${table.id}/records`)
.set('xc-auth', context.token)
.send([
{
Id: unfilteredRecords[0].Id,
CreatedBy: 'user@example.com',
},
])
.expect(400);
await request(context.app)
.patch(`/api/v2/tables/${table.id}/records`)
.set('xc-auth', context.token)
.send([
{
Id: unfilteredRecords[0].Id,
LastModifiedBy: 'user@example.com',
},
])
.expect(400);
});
it('Delete & add field: (CreatedAt) verify contents of both fields are same', async () => {
// add another CreatedTime field
await createColumn(context, table, {
title: 'CreatedAt2',
@ -335,7 +445,10 @@ function columnTypeSpecificTests() {
// get all columns
let columns = await getColumnsByAPI(context, base, table);
// delete the field
await deleteColumn(context, { table, column: columns.columns[4] });
await deleteColumn(context, {
table,
column: columns.columns[defaultTableColumns.length],
});
// create column again
await createColumn(context, table, {
title: 'CreatedAt2',
@ -349,11 +462,54 @@ function columnTypeSpecificTests() {
const records = await listRow({ base, table });
// verify contents of both fields are same
expect(columns.columns[4].title).to.equal('CreatedAt2');
expect(columns.columns[4].uidt).to.equal(UITypes.CreatedTime);
expect(!!columns.columns[4].system).to.equal(false);
expect(columns.columns[defaultTableColumns.length].title).to.equal(
'CreatedAt2',
);
expect(columns.columns[defaultTableColumns.length].uidt).to.equal(
UITypes.CreatedTime,
);
expect(columns.columns[defaultTableColumns.length].system).to.equal(
false,
);
expect(records[0].CreatedAt).to.equal(records[0].CreatedAt2);
});
it('Delete & add field: (CreatedBy) verify contents of both fields are same', async () => {
// add another CreatedBy field
await createColumn(context, table, {
title: 'CreatedBy',
uidt: UITypes.CreatedBy,
column_name: 'CreatedBy',
});
// get all columns
let columns = await getColumnsByAPI(context, base, table);
// delete the field
await deleteColumn(context, { table, column: columns.columns[6] });
// create column again
await createColumn(context, table, {
title: 'CreatedBy',
uidt: UITypes.CreatedBy,
column_name: 'CreatedBy',
});
// get all columns
columns = await getColumnsByAPI(context, base, table);
// get all records
const records = await listRow({ base, table });
// verify contents of both fields are same
expect(columns.columns[defaultTableColumns.length].title).to.equal(
'CreatedBy',
);
expect(columns.columns[defaultTableColumns.length].uidt).to.equal(
UITypes.CreatedBy,
);
expect(columns.columns[defaultTableColumns.length].system).to.equal(
false,
);
expect(records[0].CreatedBy).to.deep.equal(records[0].CreatedBy);
});
});
});
}

24
packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts

@ -83,6 +83,7 @@
import 'mocha';
import {
isCreatedOrLastModifiedByCol,
isCreatedOrLastModifiedTimeCol,
UITypes,
ViewTypes,
@ -679,7 +680,12 @@ function textBased() {
expect(
verifyColumnsInRsp(
rsp.body.list[0],
columns.filter((c) => !isCreatedOrLastModifiedTimeCol(c) || !c.system),
columns.filter(
(c) =>
(!isCreatedOrLastModifiedTimeCol(c) &&
!isCreatedOrLastModifiedByCol(c)) ||
!c.system,
),
),
).to.equal(true);
const filteredArray = rsp.body.list.map((r) => r.SingleLineText);
@ -700,7 +706,9 @@ function textBased() {
const displayColumns = columns.filter(
(c) =>
c.title !== 'SingleLineText' &&
(!isCreatedOrLastModifiedTimeCol(c) || !c.system),
((!isCreatedOrLastModifiedTimeCol(c) &&
!isCreatedOrLastModifiedByCol(c)) ||
!c.system),
);
expect(verifyColumnsInRsp(rsp.body.list[0], displayColumns)).to.equal(true);
});
@ -749,7 +757,8 @@ function textBased() {
(c) =>
c.title !== 'MultiLineText' &&
c.title !== 'Email' &&
!isCreatedOrLastModifiedTimeCol(c),
!isCreatedOrLastModifiedTimeCol(c) &&
!isCreatedOrLastModifiedByCol(c),
);
expect(verifyColumnsInRsp(rsp.body.list[0], displayColumns)).to.equal(true);
return gridView;
@ -769,7 +778,8 @@ function textBased() {
(c) =>
c.title !== 'MultiLineText' &&
c.title !== 'Email' &&
!isCreatedOrLastModifiedTimeCol(c),
!isCreatedOrLastModifiedTimeCol(c) &&
!isCreatedOrLastModifiedByCol(c),
);
expect(rsp.body.pageInfo.totalRows).to.equal(61);
expect(verifyColumnsInRsp(rsp.body.list[0], displayColumns)).to.equal(true);
@ -791,7 +801,8 @@ function textBased() {
(c) =>
c.title !== 'MultiLineText' &&
c.title !== 'Email' &&
!isCreatedOrLastModifiedTimeCol(c),
!isCreatedOrLastModifiedTimeCol(c) &&
!isCreatedOrLastModifiedByCol(c),
);
expect(rsp.body.pageInfo.totalRows).to.equal(7);
expect(verifyColumnsInRsp(rsp.body.list[0], displayColumns)).to.equal(true);
@ -1628,7 +1639,8 @@ function dateBased() {
delete r.Id;
delete r.CreatedAt;
delete r.UpdatedAt;
delete r.Id;
delete r.CreatedBy;
delete r.UpdatedBy;
});
rsp = await ncAxiosPost({
body: records,

Loading…
Cancel
Save