Browse Source

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

feat: CreatedBy & LastModifiedBy
pull/7400/head
Raju Udava 10 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' import MdiCloseCircle from '~icons/mdi/close-circle'
interface Props { interface Props {
modelValue?: UserFieldRecordType[] | string | null modelValue?: UserFieldRecordType[] | UserFieldRecordType | string | null
rowIndex?: number rowIndex?: number
location?: 'cell' | 'filter' location?: 'cell' | 'filter'
forceMulti?: boolean forceMulti?: boolean
@ -113,17 +113,18 @@ const vModel = computed({
return acc return acc
}, [] as { label: string; value: string }[]) }, [] as { label: string; value: string }[])
} else { } else {
selected = selected = modelValue
modelValue?.reduce((acc, item) => { ? (Array.isArray(modelValue) ? modelValue : [modelValue]).reduce((acc, item) => {
const label = item?.display_name || item?.email const label = item?.display_name || item?.email
if (label) { if (label) {
acc.push({ acc.push({
label, label,
value: item.id, value: item.id,
}) })
} }
return acc return acc
}, [] as { label: string; value: string }[]) || [] }, [] as { label: string; value: string }[])
: []
} }
return selected 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 if (!table.id) return acc
const columns = const columns =
metasWithIdAsKey.value[table.id].columns?.filter( metasWithIdAsKey.value[table.id].columns?.filter((col) => {
(col) => config.value.showAllColumns || (!config.value.showAllColumns && isLinksOrLTAR(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 const pkAndFkColumns = columns
.filter(() => config.value.showPkAndFk) .filter(() => config.value.showPkAndFk)

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

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

@ -5,6 +5,7 @@ import jsep from 'jsep'
import { import {
FormulaError, FormulaError,
UITypes, UITypes,
isCreatedOrLastModifiedByCol,
jsepCurlyHook, jsepCurlyHook,
substituteColumnIdWithAliasInFormula, substituteColumnIdWithAliasInFormula,
validateFormulaAndExtractTreeWithType, validateFormulaAndExtractTreeWithType,
@ -51,7 +52,18 @@ const { predictFunction: _predictFunction } = useNocoEe()
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
const supportedColumns = computed( 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() const { getMeta } = useMetas()

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

@ -1,6 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnType, TableType, ViewType } from 'nocodb-sdk' 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 type { Ref } from 'vue'
import MdiChevronDown from '~icons/mdi/chevron-down' import MdiChevronDown from '~icons/mdi/chevron-down'
@ -459,7 +466,14 @@ const onIsExpandedUpdate = (v: boolean) => {
const isReadOnlyVirtualCell = (column: ColumnType) => { const isReadOnlyVirtualCell = (column: ColumnType) => {
return ( 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)] 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 { try {
const parsedKey = JSON.parse(key) const parsedKey = JSON.parse(key)
return [parsedKey] return [parsedKey]
@ -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,

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

@ -129,6 +129,7 @@ const duplicateVirtualColumn = async () => {
id: undefined, id: undefined,
colOptions: undefined, colOptions: undefined,
order: undefined, order: undefined,
system: false,
} }
try { try {
@ -166,7 +167,17 @@ const duplicateVirtualColumn = async () => {
const openDuplicateDlg = async () => { const openDuplicateDlg = async () => {
if (!column?.value) return 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() duplicateVirtualColumn()
} else { } else {
const gridViewColumnList = (await $api.dbViewColumn.list(view.value?.id as string)).list 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.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.phUser, color: 'text-grey' }
} }
return { icon: iconMap.generic, 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> <script lang="ts" setup>
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk' 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<{ const props = defineProps<{
// As we need to focus search box when the parent is opened // 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 */ /** ignore virtual fields which are system fields ( mm relation ) and qr code fields */
return false return false
} }
if (isCreatedOrLastModifiedByCol(c)) {
/** ignore created by and last modified by system field */
return false
}
return showSystemFields.value return showSystemFields.value
} else if (c.uidt === UITypes.QrCode || c.uidt === UITypes.Barcode || c.uidt === UITypes.ID) { } else if (c.uidt === UITypes.QrCode || c.uidt === UITypes.Barcode || c.uidt === UITypes.ID) {
return false return false

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

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

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

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

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

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

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 {

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

@ -112,13 +112,15 @@ export function useMultiSelect(
textToCopy = !!textToCopy textToCopy = !!textToCopy
} }
if (columnObj.uidt === UITypes.User) { if ([UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(columnObj.uidt as UITypes)) {
if (textToCopy && Array.isArray(textToCopy)) { if (textToCopy) {
textToCopy = textToCopy textToCopy = Array.isArray(textToCopy)
.map((user: UserFieldRecordType) => { ? textToCopy
return user.email : [textToCopy]
}) .map((user: UserFieldRecordType) => {
.join(', ') 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 { ColumnType, GridColumnReqType, GridColumnType, MapType, TableType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue' import type { ComputedRef, Ref } from 'vue'
import { computed, ref, storeToRefs, useBase, useNuxtApp, useRoles, useUndoRedo, watch } from '#imports' import { computed, ref, storeToRefs, useBase, useNuxtApp, useRoles, useUndoRedo, watch } from '#imports'
@ -70,7 +70,12 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState(
}, {}) }, {})
fields.value = meta.value?.columns 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!] || {} const currentColumnField = fieldById[column.id!] || {}
return { 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 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) { if (!value) {
return GROUP_BY_VARS.NULL 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'})` 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 ([UITypes.User, UITypes.CreatedBy, UITypes.LastModifiedBy].includes(curr.column_uidt as UITypes)) {
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,${(Array.isArray(value) ? value : [value])
} catch (e) {} .map((v: any) => v.id)
.join(',')})`
} catch (e) {
console.error(e)
}
} else { } else {
acc += `${acc.length ? '~and' : ''}(${curr.title},gb_eq,${curr.key})` 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 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

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

@ -136,7 +136,7 @@ const uiTypes = [
}, },
{ {
name: UITypes.User, name: UITypes.User,
icon: iconMap.account, icon: iconMap.phUser,
}, },
{ {
name: UITypes.CreatedTime, name: UITypes.CreatedTime,
@ -146,6 +146,14 @@ const uiTypes = [
name: UITypes.LastModifiedTime, name: UITypes.LastModifiedTime,
icon: iconMap.datetime, icon: iconMap.datetime,
}, },
{
name: UITypes.CreatedBy,
icon: iconMap.phUser,
},
{
name: UITypes.LastModifiedBy,
icon: iconMap.phUser,
},
] ]
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',

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 TablerColumnInsertLeft from '~icons/tabler/column-insert-left'
import TablerColumnInsertRight from '~icons/tabler/column-insert-right' import TablerColumnInsertRight from '~icons/tabler/column-insert-right'
import MdiEyeCircleOutline from '~icons/mdi/eye-circle-outline' 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 MsGroup from '~icons/material-symbols/groups-outline-rounded'
import MsAddBoxOutline from '~icons/nc-icons/add-box' import MsAddBoxOutline from '~icons/nc-icons/add-box'
import MsDownloadRounded from '~icons/nc-icons/download' import MsDownloadRounded from '~icons/nc-icons/download'
import LogosAirtable from '~icons/logos/airtable' 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 MsSort from '~icons/material-symbols/sort'
import MaterialSymbolsEdit from '~icons/material-symbols/edit-outline-rounded' import MaterialSymbolsEdit from '~icons/material-symbols/edit-outline-rounded'
import MaterialDuplicate from '~icons/material-symbols/file-copy-outline-rounded' import MaterialDuplicate from '~icons/material-symbols/file-copy-outline-rounded'
import MaterialSymbolsWarningOutlineRounded from '~icons/material-symbols/warning-outline-rounded' import MaterialSymbolsWarningOutlineRounded from '~icons/material-symbols/warning-outline-rounded'
import MaterialSymbolsDragIndicator from '~icons/ri/draggable' 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 PlusSquare from '~icons/nc-icons/plus-square'
import MobileShare from '~icons/nc-icons/share' import MobileShare from '~icons/nc-icons/share'
import PhLayout from '~icons/ph/layout' 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 LogosRedditIcon from '~icons/logos/reddit-icon'
import RiTwitterXFill from '~icons/ri/twitter-x-line' import RiTwitterXFill from '~icons/ri/twitter-x-line'
import PhGithubLogoLight from '~icons/ph/github-logo-light' 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 MsInfo from '~icons/material-symbols/info-outline-rounded'
import PhSparkleFill from '~icons/ph/sparkle-fill' import PhSparkleFill from '~icons/ph/sparkle-fill'
import NcArticle from '~icons/nc-icons/article'
import MsDatabase from '~icons/mdi/database-outline' import MsDatabase from '~icons/mdi/database-outline'
import MdiDatabaseSearch from '~icons/mdi/database-search' import MdiDatabaseSearch from '~icons/mdi/database-search'
import MdiMagicStaff from '~icons/mdi/magic-staff' import MdiMagicStaff from '~icons/mdi/magic-staff'
import PhCaretDoubleLeftThin from '~icons/ph/caret-double-left-light' 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 Right from '~icons/material-symbols/chevron-right-rounded'
import Left from '~icons/material-symbols/chevron-left-rounded' import Left from '~icons/material-symbols/chevron-left-rounded'
import Up from '~icons/material-symbols/keyboard-arrow-up-rounded' import Up from '~icons/material-symbols/keyboard-arrow-up-rounded'
import Down from '~icons/material-symbols/keyboard-arrow-down-rounded' import Down from '~icons/material-symbols/keyboard-arrow-down-rounded'
import PhTriangleFill from '~icons/ph/triangle-fill' import PhTriangleFill from '~icons/ph/triangle-fill'
import LcSend from '~icons/lucide/send' 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 HasManyIcon from '~icons/nc-icons/hasmany'
import ManytoManyIcon from '~icons/nc-icons/manytomany' 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 Group from '~icons/nc-icons/group'
import Sort from '~icons/nc-icons/sort' 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 // keep it for reference
// todo: remove it after all icons are migrated // todo: remove it after all icons are migrated
/* export const iconMapOld = { /* export const iconMapOld = {

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

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

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: '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: '', uicn: '',
system: true, 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: '', uicn: '',
system: true, 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 = {}; 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: '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 = {}; 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: '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 = {}; 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') {

250
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) {
@ -3302,6 +3346,9 @@ class BaseModelSqlv2 {
this.dbDriver, this.dbDriver,
); );
if (!args.skipValidationAndHooks) await this.validate(updateData); if (!args.skipValidationAndHooks) await this.validate(updateData);
await this.prepareNocoData(updateData, false, cookie);
const pkValues = await this._extractPksValues(updateData); const pkValues = await this._extractPksValues(updateData);
if (pkValues) { if (pkValues) {
// pk is specified - by pass // pk is specified - by pass
@ -3847,7 +3894,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 +4069,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 +4099,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 +4124,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 +4223,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 +4251,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 +4274,10 @@ class BaseModelSqlv2 {
{ raw: true }, { raw: true },
); );
await this.updateLastModifiedTime({ await this.updateLastModified({
model: parentTable, model: parentTable,
rowIds: [childId], rowIds: [childId],
cookie,
}); });
} }
break; break;
@ -4660,22 +4719,43 @@ class BaseModelSqlv2 {
await this.model.getColumns(); await this.model.getColumns();
} }
const userColumns = []; let userColumns = [];
const columns = childTable ? childTable.columns : this.model.columns; const columns = childTable ? childTable.columns : this.model.columns;
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);
} }
} }
} }
// 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) { if (userColumns.length) {
const baseUsers = await BaseUser.getUsersList({ const baseUsers = await BaseUser.getUsersList({
base_id: childTable ? childTable.base_id : this.model.base_id, base_id: childTable ? childTable.base_id : this.model.base_id,
@ -4694,30 +4774,32 @@ class BaseModelSqlv2 {
} }
protected _convertUserFormat( protected _convertUserFormat(
userColumns: Record<string, any>[], userColumns: Column[],
baseUsers: Partial<User>[], baseUsers: Partial<User>[],
d: Record<string, any>, d: Record<string, any>,
) { ) {
try { try {
if (d) { if (d) {
for (const col of userColumns) { const availableUserColumns = userColumns.filter(
if (d[col.id] && d[col.id].length) { (col) => d[col.id] && d[col.id].length,
d[col.id] = d[col.id].split(','); );
} else { for (const col of availableUserColumns) {
d[col.id] = null; d[col.id] = d[col.id].split(',');
}
if (d[col.id]?.length) { d[col.id] = d[col.id].map((fid) => {
d[col.id] = d[col.id].map((fid) => { const { id, email, display_name } = baseUsers.find(
const { id, email, display_name } = baseUsers.find( (u) => u.id === fid,
(u) => u.id === fid, );
); return {
return { id,
id, email,
email, display_name: display_name?.length ? display_name : null,
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, 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 +5297,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 +5345,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 +5492,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 +5583,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 +5634,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 +5724,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 +5766,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 +5774,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 +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]) { if (data[column.column_name]) {
const userIds = []; const userIds = [];

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

@ -453,9 +453,14 @@ 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)
) { ) {
// get column name for CreatedBy, LastModifiedBy
column.column_name = await getColumnName(column);
const baseUsers = await BaseUser.getUsersList({ const baseUsers = await BaseUser.getUsersList({
base_id: column.base_id, base_id: column.base_id,
}); });

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,

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

@ -295,9 +295,14 @@ export const getRefColumnIfAlias = async (
columns?: Column[], columns?: Column[],
) => { ) => {
if ( 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; return column;

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

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

8
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') {
@ -179,7 +181,9 @@ export async function serializeCellValue({
} }
} catch {} } catch {}
return (data || []).map((user) => `${user.email}`).join(', '); return (data ? (Array.isArray(data) ? data : [data]) : [])
.map((user) => `${user.email}`)
.join(', ');
} }
case UITypes.Lookup: 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(); const config = await NcConfig.createByEnv();
// set version // set version
process.env.NC_VERSION = '0111004'; process.env.NC_VERSION = '0111005';
// init cache // init cache
await NocoCache.init(); await NocoCache.init();

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

@ -454,9 +454,12 @@ 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 }[]) { const userRecord = Array.isArray(v) ? v : [v];
for (const user of userRecord) {
userIds.push(user.id); userIds.push(user.id);
} }
row[colId] = userIds.join(','); 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.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,13 +899,18 @@ 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({
tableId: getIdOrExternalId(getParentIdentifier(col.id)), tableId: getIdOrExternalId(getParentIdentifier(col.id)),
column: withoutId({ column: withoutId({
...flatCol, ...flatCol,
// provide column_name to avoid ajv error
// it will be ignored by the service
column_name: 'system',
system: false, system: false,
}) as any, }) as any,
req: param.req, 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); 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++) { for (let i = 0; i < 4; i++) {

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,
@ -174,6 +175,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,
@ -200,6 +202,7 @@ export class ColumnsService {
if ( if (
isCreatedOrLastModifiedTimeCol(column) || isCreatedOrLastModifiedTimeCol(column) ||
isCreatedOrLastModifiedByCol(column) ||
[ [
UITypes.Lookup, UITypes.Lookup,
UITypes.Rollup, UITypes.Rollup,
@ -320,7 +323,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, {
@ -1711,6 +1719,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();
@ -1721,10 +1731,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 = 'nc_created_by';
break;
case UITypes.LastModifiedBy:
columnName = 'updated_by';
columnTitle = 'nc_updated_by';
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);
@ -1765,10 +1791,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,
@ -2233,9 +2256,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,
@ -391,21 +392,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 = 'nc_created_by';
break;
case UITypes.LastModifiedBy:
columnName = 'updated_by';
columnTitle = 'nc_updated_by';
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)),
@ -521,7 +550,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);
@ -556,7 +586,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)),

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

@ -16,7 +16,7 @@ import ncHookUpgrader from './ncHookUpgrader';
import ncProjectConfigUpgrader from './ncProjectConfigUpgrader'; 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 ncXcdbCreatedAndUpdatedSystemFieldsUpgrader from './ncXcdbCreatedAndUpdatedSystemFieldsUpgrader';
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';
@ -145,7 +145,7 @@ export default class NcUpgrader {
{ name: '0107004', handler: ncProjectConfigUpgrader }, { name: '0107004', handler: ncProjectConfigUpgrader },
{ name: '0108002', handler: ncXcdbLTARUpgrader }, { name: '0108002', handler: ncXcdbLTARUpgrader },
{ name: '0111002', handler: ncXcdbLTARIndexUpgrader }, { 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 { NcUpgraderCtx } from './NcUpgrader';
import type { MetaService } from '~/meta/meta.service'; import type { MetaService } from '~/meta/meta.service';
import type { Base } from '~/models'; import type { Base } from '~/models';
import Noco from '~/Noco';
import { MetaTable } from '~/utils/globals'; import { MetaTable } from '~/utils/globals';
import { Column, Model, Source } from '~/models'; import { Column, Model, Source } from '~/models';
import { import {
@ -15,15 +16,18 @@ import NcConnectionMgrv2 from '~/utils/common/NcConnectionMgrv2';
import getColumnUiType from '~/helpers/getColumnUiType'; import getColumnUiType from '~/helpers/getColumnUiType';
import RequestQueue from '~/utils/RequestQueue'; import RequestQueue from '~/utils/RequestQueue';
// Example Usage:
// An upgrader for upgrading created_at and updated_at columns // An upgrader for upgrading created_at and updated_at columns
// to system column and convert to new uidt CreatedTime and LastModifiedTime // to system column and convert to new uidt CreatedTime and LastModifiedTime
const logger = { const logger = {
log: (message: string) => { log: (message: string) => {
console.log( 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, ncMeta,
source, source,
base, base,
models,
}: { }: {
ncMeta: MetaService; ncMeta: MetaService;
source: Source; source: Source;
base: Base; base: Base;
models: Model[];
}) { }) {
const models = await Model.list( // get existing columns from database
{ const sqlClient = await NcConnectionMgrv2.getSqlClient(source, ncMeta.knex);
base_id: source.base_id, const sqlMgr = ProjectMgrv2.getSqlMgr({ id: source.base_id }, ncMeta);
source_id: source.id,
},
ncMeta,
);
await Promise.all( await Promise.all(
models.map(async (model: any) => { models.map(async (model) => {
if (model.mm) return; 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( if (column.uidt === UITypes.CreatedBy && column.system) {
`Upgrading model '${model.title}'(${model.id}) from base '${base.title}'(${base.id}})`, isCreatedByExists = true;
); continue;
}
const columns = await model.getColumns(ncMeta);
const oldColumns = columns.map((c) => ({ ...c, cn: c.column_name })); if (column.uidt === UITypes.LastModifiedBy && column.system) {
let isCreatedTimeExists = false; isLastModifiedByExists = true;
let isLastModifiedTimeExists = false; continue;
for (const column of columns) { }
if (column.uidt !== UITypes.DateTime) continue;
if (
// if column is created_at or updated_at, update the uidt in meta [UITypes.CreatedBy, UITypes.LastModifiedBy].includes(column.uidt)
if (column.column_name === 'created_at') { ) {
isCreatedTimeExists = true; continue;
await Column.update( }
column.id,
{ if (column.uidt === UITypes.CreatedTime && column.system) {
...column, isCreatedTimeExists = true;
uidt: UITypes.CreatedTime, continue;
system: true, }
},
ncMeta, if (column.uidt === UITypes.LastModifiedTime && column.system) {
true, 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') { if (source.type === 'pg') {
// delete pg trigger if exists // delete pg trigger if exists
await deletePgTrigger({ column, ncMeta, model }); await deletePgTrigger({ column, ncMeta, model });
}*/ }
} */
if (column.column_name === 'updated_at') { }
isLastModifiedTimeExists = true; if (column.column_name === 'updated_at') {
await Column.update( isLastModifiedTimeExists = true;
column.id, if (column.uidt !== UITypes.LastModifiedTime || !column.system) {
{ await Column.update(
...column, column.id,
uidt: UITypes.LastModifiedTime, {
system: true, ...column,
cdf: '', uidt: UITypes.LastModifiedTime,
au: false, system: true,
}, },
ncMeta, ncMeta,
true, true,
); );
}
}
} }
}
if (!isCreatedTimeExists || !isLastModifiedTimeExists) { if (
// get existing columns from database isCreatedTimeExists &&
isLastModifiedTimeExists &&
const sqlClient = await NcConnectionMgrv2.getSqlClient( isCreatedByExists &&
source, isLastModifiedByExists
ncMeta.knex, ) {
); return;
}
const dbColumns = const dbColumns =
( (
@ -143,7 +182,7 @@ async function upgradeModels({
return; return;
} }
// create created_at and updated_at columns // create created_at & updated_at and created_by & updated_by columns
const newColumns = []; const newColumns = [];
const existingDbColumns = []; 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 // alter table and add new columns if any
if (newColumns.length) { if (newColumns.length) {
logger.log(
`Altering table '${model.title}'(${model.id}) from base '${base.title}'(${base.id}}) for new columns`,
);
// update column in db // update column in db
const tableUpdateBody = { const tableUpdateBody = {
...model, ...model,
@ -235,24 +337,24 @@ async function upgradeModels({
cn: c.column_name, cn: c.column_name,
})), })),
}; };
const sqlMgr = ProjectMgrv2.getSqlMgr({ id: source.base_id }, ncMeta);
await sqlMgr.sqlOpPlus(source, 'tableUpdate', tableUpdateBody); await sqlMgr.sqlOpPlus(source, 'tableUpdate', tableUpdateBody);
} }
for (const newColumn of [...existingDbColumns, ...newColumns]) { for (const newColumn of [...existingDbColumns, ...newColumns]) {
await Column.insert( await Column.insert(
{ {
...newColumn, ...newColumn,
system: 1, system: true,
fk_model_id: model.id, fk_model_id: model.id,
}, },
ncMeta, 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, eq: 1,
}, },
}, },
{ ...(Noco.isEE()
is_local: { ? [
eq: 1, {
}, is_local: {
}, eq: 1,
},
},
]
: []),
], ],
}, },
}); });
@ -289,7 +395,7 @@ export default async function ({ ncMeta }: NcUpgraderCtx) {
if (!base || base.deleted) { if (!base || base.deleted) {
logger.log( logger.log(
`Skipped deleted base source '${source.alias || source.id}' - ${ `Skipped deleted base source '${source.alias || source.id}' - ${
base.id base?.id
}`, }`,
); );
return Promise.resolve(); return Promise.resolve();
@ -297,13 +403,25 @@ export default async function ({ ncMeta }: NcUpgraderCtx) {
// update the meta props // update the meta props
return requestQueue.enqueue(async () => { 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( logger.log(
`Upgrading base ${base.title}(${base.id},${source.id}) (${i + 1}/${ `Upgrading base ${base.title}(${base.id},${source.id}) (${i + 1}/${
sources.length sources.length
})`, })`,
); );
return upgradeModels({ ncMeta, source, base }).then(() => { await upgradeModels({ ncMeta, source, models, base }).then(() => {
logger.log( logger.log(
`Upgraded base '${base.title}'(${base.id},${source.id}) (${i + 1}/${ `Upgraded base '${base.title}'(${base.id},${source.id}) (${i + 1}/${
sources.length 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 request from 'supertest';
import Model from '../../../src/models/Model'; import Model from '../../../src/models/Model';
import NcConnectionMgrv2 from '../../../src/utils/common/NcConnectionMgrv2'; import NcConnectionMgrv2 from '../../../src/utils/common/NcConnectionMgrv2';
@ -258,7 +262,8 @@ const generateDefaultRowAttributes = ({
column.uidt === UITypes.LinkToAnotherRecord || column.uidt === UITypes.LinkToAnotherRecord ||
column.uidt === UITypes.ForeignKey || column.uidt === UITypes.ForeignKey ||
column.uidt === UITypes.ID || column.uidt === UITypes.ID ||
isCreatedOrLastModifiedTimeCol(column) isCreatedOrLastModifiedTimeCol(column) ||
isCreatedOrLastModifiedByCol(column)
) { ) {
return acc; return acc;
} }

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

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

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

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

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

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

Loading…
Cancel
Save