Browse Source

Merge pull request #7304 from nocodb/nc-feat/create-and-updated-uidt

feat: CreateTime and LastModifiedTime datatypes
pull/7370/head
Raju Udava 8 months ago committed by GitHub
parent
commit
f4af2579e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      packages/nc-gui/components/cell/DateTimePicker.vue
  2. 22
      packages/nc-gui/components/cell/ReadOnlyDateTimePicker.vue
  3. 11
      packages/nc-gui/components/smartsheet/Form.vue
  4. 2
      packages/nc-gui/components/smartsheet/VirtualCell.vue
  5. 9
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  6. 10
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  7. 14
      packages/nc-gui/components/smartsheet/grid/GroupBy.vue
  8. 12
      packages/nc-gui/components/smartsheet/grid/Table.vue
  9. 12
      packages/nc-gui/components/smartsheet/header/Menu.vue
  10. 3
      packages/nc-gui/components/smartsheet/header/VirtualCellIcon.ts
  11. 14
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  12. 3
      packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue
  13. 11
      packages/nc-gui/components/template/Editor.vue
  14. 12
      packages/nc-gui/composables/useColumnCreateStore.ts
  15. 19
      packages/nc-gui/composables/useData.ts
  16. 4
      packages/nc-gui/composables/useMultiSelect/index.ts
  17. 4
      packages/nc-gui/lang/ar.json
  18. 4
      packages/nc-gui/lang/bn_IN.json
  19. 4
      packages/nc-gui/lang/cs.json
  20. 4
      packages/nc-gui/lang/da.json
  21. 4
      packages/nc-gui/lang/de.json
  22. 5
      packages/nc-gui/lang/en.json
  23. 4
      packages/nc-gui/lang/es.json
  24. 4
      packages/nc-gui/lang/eu.json
  25. 4
      packages/nc-gui/lang/fa.json
  26. 4
      packages/nc-gui/lang/fi.json
  27. 4
      packages/nc-gui/lang/fr.json
  28. 4
      packages/nc-gui/lang/he.json
  29. 4
      packages/nc-gui/lang/hi.json
  30. 4
      packages/nc-gui/lang/hr.json
  31. 4
      packages/nc-gui/lang/id.json
  32. 4
      packages/nc-gui/lang/it.json
  33. 4
      packages/nc-gui/lang/ja.json
  34. 4
      packages/nc-gui/lang/ko.json
  35. 4
      packages/nc-gui/lang/lv.json
  36. 4
      packages/nc-gui/lang/nl.json
  37. 4
      packages/nc-gui/lang/no.json
  38. 4
      packages/nc-gui/lang/pl.json
  39. 4
      packages/nc-gui/lang/pt.json
  40. 4
      packages/nc-gui/lang/pt_BR.json
  41. 4
      packages/nc-gui/lang/ru.json
  42. 4
      packages/nc-gui/lang/sk.json
  43. 4
      packages/nc-gui/lang/sl.json
  44. 4
      packages/nc-gui/lang/sv.json
  45. 4
      packages/nc-gui/lang/th.json
  46. 4
      packages/nc-gui/lang/tr.json
  47. 4
      packages/nc-gui/lang/uk.json
  48. 4
      packages/nc-gui/lang/vi.json
  49. 4
      packages/nc-gui/lang/zh-Hans.json
  50. 4
      packages/nc-gui/lang/zh-Hant.json
  51. 2
      packages/nc-gui/utils/cell.ts
  52. 10
      packages/nc-gui/utils/columnUtils.ts
  53. 65
      packages/nc-gui/utils/filterUtils.ts
  54. 2
      packages/nc-gui/utils/sortUtils.ts
  55. 2
      packages/nocodb-sdk/src/lib/CustomAPI.ts
  56. 15
      packages/nocodb-sdk/src/lib/UITypes.ts
  57. 4
      packages/nocodb-sdk/src/lib/columnRules/QrAndBarcodeRules.ts
  58. 12
      packages/nocodb-sdk/src/lib/enums.ts
  59. 2
      packages/nocodb-sdk/src/lib/formulaHelpers.ts
  60. 2
      packages/nocodb-sdk/src/lib/globals.ts
  61. 4
      packages/nocodb-sdk/src/lib/helperFunctions.ts
  62. 1
      packages/nocodb-sdk/src/lib/index.ts
  63. 1
      packages/nocodb-sdk/src/lib/mergeSwaggerSchema.ts
  64. 18
      packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts
  65. 21
      packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts
  66. 4
      packages/nocodb-sdk/src/lib/sqlUi/OracleUi.ts
  67. 21
      packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts
  68. 17
      packages/nocodb-sdk/src/lib/sqlUi/SnowflakeUi.ts
  69. 2
      packages/nocodb-sdk/src/lib/sqlUi/SqlUiFactory.ts
  70. 18
      packages/nocodb-sdk/src/lib/sqlUi/SqliteUi.ts
  71. 308
      packages/nocodb/src/db/BaseModelSqlv2.ts
  72. 53
      packages/nocodb/src/db/conditionV2.ts
  73. 90
      packages/nocodb/src/db/formulav2/formulaQueryBuilderv2.ts
  74. 2
      packages/nocodb/src/db/generateLookupSelectQuery.ts
  75. 3
      packages/nocodb/src/db/sortV2.ts
  76. 24
      packages/nocodb/src/helpers/columnHelpers.ts
  77. 16
      packages/nocodb/src/helpers/getAst.ts
  78. 16
      packages/nocodb/src/helpers/getColumnPropsFromUIDT.ts
  79. 2
      packages/nocodb/src/helpers/populateSamplePayload.ts
  80. 12
      packages/nocodb/src/models/Column.ts
  81. 2
      packages/nocodb/src/modules/global/init-meta-service.provider.ts
  82. 4
      packages/nocodb/src/modules/jobs/jobs/at-import/at-import.processor.ts
  83. 24
      packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts
  84. 4
      packages/nocodb/src/schema/swagger-v2.json
  85. 4
      packages/nocodb/src/schema/swagger.json
  86. 4
      packages/nocodb/src/services/api-docs/swagger/getSwaggerColumnMetas.ts
  87. 4
      packages/nocodb/src/services/api-docs/swaggerV2/getSwaggerColumnMetas.ts
  88. 107
      packages/nocodb/src/services/columns.service.ts
  89. 82
      packages/nocodb/src/services/tables.service.ts
  90. 2
      packages/nocodb/src/version-upgrader/NcUpgrader.ts
  91. 194
      packages/nocodb/src/version-upgrader/ncXcdbCreatedAndUpdatedTimeUpgrader.ts
  92. 18
      packages/nocodb/tests/unit/factory/column.ts
  93. 5
      packages/nocodb/tests/unit/factory/row.ts
  94. 33
      packages/nocodb/tests/unit/factory/table.ts
  95. 65
      packages/nocodb/tests/unit/model/tests/baseModelSql.test.ts
  96. 311
      packages/nocodb/tests/unit/rest/tests/columnTypeSpecific.test.ts
  97. 88
      packages/nocodb/tests/unit/rest/tests/newDataApis.test.ts

1
packages/nc-gui/components/cell/DateTimePicker.vue

@ -253,6 +253,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
const cellClickHook = inject(CellClickHookInj, null)
const cellClickHandler = () => {
if (readOnly.value) return
open.value = (active.value || editable.value) && !open.value
}

22
packages/nc-gui/components/cell/ReadOnlyDateTimePicker.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">
<LazyCellDateTimePicker class="z-0" :model-value="modelValue" :is-pk="false" />
<div class="w-full h-full z-1 absolute top-0 left-0"></div>
</div>
</template>

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

@ -34,7 +34,16 @@ provide(IsGalleryInj, ref(false))
// todo: generate hideCols based on default values
const hiddenCols = ['created_at', 'updated_at']
const hiddenColTypes = [UITypes.Rollup, UITypes.Lookup, UITypes.Formula, UITypes.QrCode, UITypes.Barcode, UITypes.SpecificDBType]
const hiddenColTypes = [
UITypes.Rollup,
UITypes.Lookup,
UITypes.Formula,
UITypes.QrCode,
UITypes.Barcode,
UITypes.SpecificDBType,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
]
const { isMobileMode, user } = useGlobal()

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

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

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

@ -79,7 +79,14 @@ const mounted = ref(false)
const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber]
const onlyNameUpdateOnEditColumns = [UITypes.LinkToAnotherRecord, UITypes.Lookup, UITypes.Rollup, UITypes.Links]
const onlyNameUpdateOnEditColumns = [
UITypes.LinkToAnotherRecord,
UITypes.Lookup,
UITypes.Rollup,
UITypes.Links,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
]
// To close column type dropdown on escape and
// close modal only when the type popup is close

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

@ -458,7 +458,9 @@ const onIsExpandedUpdate = (v: boolean) => {
}
const isReadOnlyVirtualCell = (column: ColumnType) => {
return isRollup(column) || isFormula(column) || isBarcode(column) || isLookup(column) || isQrCode(column)
return (
isRollup(column) || isFormula(column) || isBarcode(column) || isLookup(column) || isQrCode(column) || isSystemColumn(column)
)
}
// Small hack. We need to scroll to the bottom of the form after its mounted and back to top.
@ -686,7 +688,7 @@ export default {
:ref="i ? null : (el: any) => (cellWrapperEl = el)"
class="bg-white w-80 xs:w-full px-1 sm:min-h-[35px] xs:min-h-13 flex items-center relative"
:class="{
'!bg-gray-50 !px-0 !select-text': isReadOnlyVirtualCell(col),
'!bg-gray-50 !px-0 !select-text nc-system-field': isReadOnlyVirtualCell(col),
}"
>
<LazySmartsheetVirtualCell
@ -924,4 +926,8 @@ export default {
.nc-data-cell:focus-within {
@apply !border-1 !border-brand-500 !rounded-lg !shadow-none !ring-0;
}
:deep(.nc-system-field input) {
@apply bg-transparent;
}
</style>

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

@ -5,8 +5,8 @@ import Table from './Table.vue'
import GroupBy from './GroupBy.vue'
import GroupByTable from './GroupByTable.vue'
import GroupByLabel from './GroupByLabel.vue'
import { GROUP_BY_VARS, computed, ref } from '#imports'
import type { Group, Row } from '#imports'
import { GROUP_BY_VARS, computed, ref } from '#imports'
const props = defineProps<{
group: Group
@ -182,7 +182,17 @@ const parseKey = (group: Group) => {
}
const shouldRenderCell = (column) =>
[UITypes.Lookup, UITypes.Attachment, UITypes.Barcode, UITypes.QrCode, UITypes.Links, UITypes.User].includes(column?.uidt)
[
UITypes.Lookup,
UITypes.Attachment,
UITypes.Barcode,
UITypes.QrCode,
UITypes.Links,
UITypes.User,
UITypes.DateTime,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
].includes(column?.uidt)
</script>
<template>

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

@ -1,8 +1,8 @@
<script lang="ts" setup>
import axios from 'axios'
import { nextTick } from '@vue/runtime-core'
import type { ColumnReqType, ColumnType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import { UITypes, ViewTypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } 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 { useColumnDrag } from './useColumnDrag'
import usePaginationShortcuts from './usePaginationShortcuts'
@ -1010,7 +1010,8 @@ const showFillHandle = computed(
!(
isLookup(fields.value[activeCell.col]) ||
isRollup(fields.value[activeCell.col]) ||
isFormula(fields.value[activeCell.col])
isFormula(fields.value[activeCell.col]) ||
isCreatedOrLastModifiedTimeCol(fields.value[activeCell.col])
),
)
@ -1570,7 +1571,10 @@ onKeyStroke('ArrowDown', onDown)
'align-top': rowHeight && rowHeight !== 1,
'filling': isCellInFillRange(rowIndex, colIndex),
'readonly':
(isLookup(columnObj) || isRollup(columnObj) || isFormula(columnObj)) &&
(isLookup(columnObj) ||
isRollup(columnObj) ||
isFormula(columnObj) ||
isCreatedOrLastModifiedTimeCol(columnObj)) &&
hasEditPermission &&
isCellSelected(rowIndex, colIndex),
'!border-r-blue-400 !border-r-3': toBeDroppedColId === columnObj.id,

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

@ -1,6 +1,7 @@
<script lang="ts" setup>
import type { ColumnReqType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isLinksOrLTAR } from 'nocodb-sdk'
import { computed } from 'vue'
import {
ActiveViewInj,
ColumnInj,
@ -276,6 +277,13 @@ const onInsertAfter = () => {
isOpen.value = false
addColumn()
}
const isDeleteAllowed = computed(() => {
return column?.value && !column.value.system
})
const isDuplicateAllowed = computed(() => {
return column?.value && !column.value.system
})
</script>
<template>
@ -345,7 +353,7 @@ const onInsertAfter = () => {
<a-divider v-if="!column?.pk" class="!my-0" />
<NcMenuItem v-if="!column?.pk" @click="openDuplicateDlg">
<NcMenuItem v-if="!column?.pk" :disabled="!isDuplicateAllowed" @click="openDuplicateDlg">
<div v-e="['a:field:duplicate']" class="nc-column-duplicate nc-header-menu-item">
<component :is="iconMap.duplicate" class="text-gray-700" />
<!-- Duplicate -->
@ -368,7 +376,7 @@ const onInsertAfter = () => {
</NcMenuItem>
<a-divider v-if="!column?.pv" class="!my-0" />
<NcMenuItem v-if="!column?.pv" class="!hover:bg-red-50" @click="handleDelete">
<NcMenuItem v-if="!column?.pv" :disabled="!isDeleteAllowed" class="!hover:bg-red-50" @click="handleDelete">
<div class="nc-column-delete nc-header-menu-item text-red-600">
<component :is="iconMap.delete" />
<!-- Delete -->

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

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

14
packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue

@ -1,6 +1,7 @@
<script setup lang="ts">
import type { ColumnType, FilterType } from 'nocodb-sdk'
import { PlanLimitTypes, UITypes } from 'nocodb-sdk'
import type { Filter } from '#imports'
import {
ActiveViewInj,
AllFiltersInj,
@ -17,7 +18,6 @@ import {
useViewFilters,
watch,
} from '#imports'
import type { Filter } from '#imports'
interface Props {
nestedLevel?: number
@ -143,7 +143,7 @@ const filterUpdateCondition = (filter: FilterType, i: number) => {
// hence remove the previous value
filter.value = null
filter.comparison_sub_op = null
} else if ([UITypes.Date, UITypes.DateTime].includes(col.uidt as UITypes)) {
} else if (isDateType(col.uidt as UITypes)) {
// for date / datetime,
// the input type could be decimal or datepicker / datetime picker
// hence remove the previous value
@ -241,7 +241,7 @@ const selectFilterField = (filter: Filter, index: number) => {
isComparisonOpAllowed(filter, compOp),
)?.value as FilterType['comparison_op']
if ([UITypes.Date, UITypes.DateTime].includes(col.uidt as UITypes) && !['blank', 'notblank'].includes(filter.comparison_op!)) {
if (isDateType(col.uidt as UITypes) && !['blank', 'notblank'].includes(filter.comparison_op!)) {
if (filter.comparison_op === 'isWithin') {
filter.comparison_sub_op = 'pastNumberOfDays'
} else {
@ -335,6 +335,10 @@ onMounted(async () => {
onBeforeUnmount(() => {
if (parentId.value) delete allFilters.value[parentId.value]
})
function isDateType(uidt: UITypes) {
return [UITypes.Date, UITypes.DateTime, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(uidt)
}
</script>
<template>
@ -477,7 +481,7 @@ onBeforeUnmount(() => {
<div v-if="['blank', 'notblank'].includes(filter.comparison_op)" class="flex flex-grow"></div>
<NcSelect
v-else-if="[UITypes.Date, UITypes.DateTime].includes(getColumn(filter)?.uidt)"
v-else-if="isDateType(getColumn(filter)?.uidt)"
v-model:value="filter.comparison_sub_op"
v-e="['c:filter:sub-comparison-op:select']"
:dropdown-match-select-width="false"
@ -527,7 +531,7 @@ onBeforeUnmount(() => {
@update-filter-value="(value) => updateFilterValue(value, filter, i)"
@click.stop
/>
<div v-else-if="![UITypes.Date, UITypes.DateTime].includes(getColumn(filter)?.uidt)" class="flex-grow"></div>
<div v-else-if="!isDateType(getColumn(filter)?.uidt)" class="flex-grow"></div>
<NcButton
v-if="!filter.readOnly"

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

@ -18,6 +18,7 @@ import {
isMultiSelect,
isPercent,
isRating,
isReadonlyDateTime,
isSingleSelect,
isTextArea,
isTime,
@ -80,6 +81,7 @@ const checkTypeFunctions = {
isPercent,
isCurrency,
isDecimal,
isReadonlyDateTime,
isInt,
isFloat,
isTextArea,
@ -142,6 +144,7 @@ const componentMap: Partial<Record<FilterType, any>> = computed(() => {
isDate: renderDateFilterInput(props.filter.comparison_sub_op!),
isYear: YearPicker,
isDateTime: renderDateFilterInput(props.filter.comparison_sub_op!),
isReadonlyDateTime: renderDateFilterInput(props.filter.comparison_sub_op!),
isTime: TimePicker,
isRating: Rating,
isDuration: Duration,

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

@ -100,9 +100,14 @@ const uiTypeOptions = ref<Option[]>(
.filter(
(uiType) =>
!isVirtualCol(UITypes[uiType]) &&
![UITypes.ForeignKey, UITypes.ID, UITypes.CreateTime, UITypes.LastModifiedTime, UITypes.Barcode, UITypes.Button].includes(
UITypes[uiType],
),
![
UITypes.ForeignKey,
UITypes.ID,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.Barcode,
UITypes.Button,
].includes(UITypes[uiType]),
)
.map<Option>((uiType) => ({
value: uiType,

12
packages/nc-gui/composables/useColumnCreateStore.ts

@ -113,6 +113,18 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
{
validator: (rule: any, value: any) => {
return new Promise<void>((resolve, reject) => {
if (
(tableExplorerColumns?.value || meta.value?.columns)?.some(
(c) =>
c.id !== formState.value.id && // ignore current column
// compare against column_name and title
((value || '').toLowerCase() === (c.column_name || '').toLowerCase() ||
(value || '').toLowerCase() === (c.title || '').toLowerCase()) &&
c.system,
)
) {
return reject(new Error(t('msg.error.duplicateSystemColumnName')))
}
if (
(tableExplorerColumns?.value || meta.value?.columns)?.some(
(c) =>

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

@ -236,14 +236,17 @@ export function useData(args: {
toUpdate.row,
metaValue!.columns!.reduce<Record<string, any>>((acc: Record<string, any>, col: ColumnType) => {
if (
col.uidt === UITypes.Formula ||
col.uidt === UITypes.QrCode ||
col.uidt === UITypes.Barcode ||
col.uidt === UITypes.Rollup ||
col.uidt === UITypes.Checkbox ||
col.uidt === UITypes.User ||
col.au ||
col.cdf?.includes(' on update ')
col.title in updatedRowData &&
(col.uidt === UITypes.Formula ||
col.uidt === UITypes.QrCode ||
col.uidt === UITypes.Barcode ||
col.uidt === UITypes.Rollup ||
col.uidt === UITypes.Checkbox ||
col.uidt === UITypes.User ||
col.uidt === UITypes.LastModifiedTime ||
col.uidt === UITypes.Lookup ||
col.au ||
(col.cdf && / on update /i.test(col.cdf)))
)
acc[col.title!] = updatedRowData[col.title!]
return acc

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

@ -1,6 +1,6 @@
import type { Ref } from 'vue'
import { computed } from 'vue'
import dayjs from 'dayjs'
import type { Ref } from 'vue'
import type { MaybeRef } from '@vueuse/core'
import type { ColumnType, LinkToAnotherRecordType, TableType, UserFieldRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, dateFormats, isDateMonthFormat, isSystemColumn, isVirtualCol, timeFormats } from 'nocodb-sdk'
@ -136,7 +136,7 @@ export function useMultiSelect(
})
}
if (columnObj.uidt === UITypes.DateTime) {
if ([UITypes.DateTime, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(columnObj.uidt)) {
// remove `"`
// e.g. "2023-05-12T08:03:53.000Z" -> 2023-05-12T08:03:53.000Z
textToCopy = textToCopy.replace(/["']/g, '')

4
packages/nc-gui/lang/ar.json

@ -273,7 +273,7 @@
"Count": "العد",
"Lookup": "مشاهدة بيانات",
"DateTime": "تاريخ وقت",
"CreateTime": "إنشاء وقت",
"CreatedTime": "إنشاء وقت",
"LastModifiedTime": "وقت آخر تعديل",
"AutoNumber": "عدد تلقائي",
"Barcode": "رمز",
@ -1347,4 +1347,4 @@
"roleUpdated": "Role updated successfully"
}
}
}
}

4
packages/nc-gui/lang/bn_IN.json

@ -273,7 +273,7 @@
"Count": "গণন",
"Lookup": "খ",
"DateTime": "তিখ সময",
"CreateTime": "সমযি করন",
"CreatedTime": "সমযি করন",
"LastModifiedTime": "শষ পরিবরিত সময",
"AutoNumber": "অট নমবর",
"Barcode": "বরকড",
@ -1347,4 +1347,4 @@
"roleUpdated": "Role updated successfully"
}
}
}
}

4
packages/nc-gui/lang/cs.json

@ -273,7 +273,7 @@
"Count": "Počet",
"Lookup": "Vyhledávání",
"DateTime": "Datum a čas",
"CreateTime": "Vytvořit čas",
"CreatedTime": "Vytvořit čas",
"LastModifiedTime": "Čas poslední úpravy",
"AutoNumber": "Automatické číslo",
"Barcode": "Čárový kód",
@ -1347,4 +1347,4 @@
"roleUpdated": "Role byla úspěšně aktualizována"
}
}
}
}

4
packages/nc-gui/lang/da.json

@ -273,7 +273,7 @@
"Count": "Tælle",
"Lookup": "Kig op",
"DateTime": "Dato tid",
"CreateTime": "Opret tid",
"CreatedTime": "Opret tid",
"LastModifiedTime": "Sidste ændret tid",
"AutoNumber": "Auto nummer.",
"Barcode": "Stregkode.",
@ -1347,4 +1347,4 @@
"roleUpdated": "Rolle opdateret med succes"
}
}
}
}

4
packages/nc-gui/lang/de.json

@ -273,7 +273,7 @@
"Count": "Zählen",
"Lookup": "Nachschlagen",
"DateTime": "Datum/Zeit",
"CreateTime": "Zeit erstellen",
"CreatedTime": "Zeit erstellen",
"LastModifiedTime": "Zuletzt bearbeitet",
"AutoNumber": "Auto-Nummerierung",
"Barcode": "Barcode",
@ -1347,4 +1347,4 @@
"roleUpdated": "Rolle erfolgreich aktualisiert"
}
}
}
}

5
packages/nc-gui/lang/en.json

@ -273,7 +273,7 @@
"Count": "Count",
"Lookup": "Lookup",
"DateTime": "Date Time",
"CreateTime": "Create Time",
"CreatedTime": "Create Time",
"LastModifiedTime": "Last Modified Time",
"AutoNumber": "Auto Number",
"Barcode": "Barcode",
@ -1274,6 +1274,7 @@
"followingCharactersAreNotAllowed": "Following characters are not allowed",
"columnNameRequired": "Field name is required",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of field name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Base name exceeds 50 characters",
@ -1347,4 +1348,4 @@
"roleUpdated": "Role updated successfully"
}
}
}
}

4
packages/nc-gui/lang/es.json

@ -273,7 +273,7 @@
"Count": "Cuenta",
"Lookup": "Búsqueda",
"DateTime": "Fecha y hora",
"CreateTime": "Fecha de creación",
"CreatedTime": "Fecha de creación",
"LastModifiedTime": "Fecha de modificación",
"AutoNumber": "Número automático",
"Barcode": "Código de barras",
@ -1347,4 +1347,4 @@
"roleUpdated": "Función actualizada correctamente"
}
}
}
}

4
packages/nc-gui/lang/eu.json

@ -273,7 +273,7 @@
"Count": "Count",
"Lookup": "Lookup",
"DateTime": "Date Time",
"CreateTime": "Create Time",
"CreatedTime": "Create Time",
"LastModifiedTime": "Azkeneko aldatze ordua",
"AutoNumber": "Auto Number",
"Barcode": "Barcode",
@ -1347,4 +1347,4 @@
"roleUpdated": "Role updated successfully"
}
}
}
}

4
packages/nc-gui/lang/fa.json

@ -273,7 +273,7 @@
"Count": "شمردن",
"Lookup": "جستوجو",
"DateTime": "تاریخ و زمان",
"CreateTime": "ایجاد زمان",
"CreatedTime": "ایجاد زمان",
"LastModifiedTime": "زمان آخرین اصلاح",
"AutoNumber": "شماره خودکار",
"Barcode": "بارکد",
@ -1347,4 +1347,4 @@
"roleUpdated": "نقش با موفقیت بهروزرسانی شد"
}
}
}
}

4
packages/nc-gui/lang/fi.json

@ -273,7 +273,7 @@
"Count": "Kreivi",
"Lookup": "Katso ylös",
"DateTime": "Treffiaika",
"CreateTime": "Luoda aikaa",
"CreatedTime": "Luoda aikaa",
"LastModifiedTime": "Viimeksi muutettu aika",
"AutoNumber": "Automaattinen numero",
"Barcode": "Viivakoodi",
@ -1347,4 +1347,4 @@
"roleUpdated": "Rooli päivitetty onnistuneesti"
}
}
}
}

4
packages/nc-gui/lang/fr.json

@ -273,7 +273,7 @@
"Count": "Compteur",
"Lookup": "Consulter",
"DateTime": "Date et heure",
"CreateTime": "Date de création",
"CreatedTime": "Date de création",
"LastModifiedTime": "Dernière modification",
"AutoNumber": "Numérotation automatique",
"Barcode": "Code-barres",
@ -1347,4 +1347,4 @@
"roleUpdated": "Rôle mis à jour avec succès"
}
}
}
}

4
packages/nc-gui/lang/he.json

@ -273,7 +273,7 @@
"Count": "ספירה",
"Lookup": "הבט מעלה",
"DateTime": "תאריך שעה",
"CreateTime": "צור זמן",
"CreatedTime": "צור זמן",
"LastModifiedTime": "שונה לאחרונה",
"AutoNumber": "מספר אוטומטי",
"Barcode": "ברקוד",
@ -1347,4 +1347,4 @@
"roleUpdated": "תפקיד שונה בהצלחה"
}
}
}
}

4
packages/nc-gui/lang/hi.json

@ -273,7 +273,7 @@
"Count": "गिनत करन",
"Lookup": "द",
"DateTime": "दिक और समय",
"CreateTime": "निण क समय",
"CreatedTime": "निण क समय",
"LastModifiedTime": "अिम सित समय",
"AutoNumber": "वहन नबर",
"Barcode": "बरकड",
@ -1347,4 +1347,4 @@
"roleUpdated": "Role updated successfully"
}
}
}
}

4
packages/nc-gui/lang/hr.json

@ -273,7 +273,7 @@
"Count": "Računati",
"Lookup": "Pogledaj",
"DateTime": "Datum vrijeme",
"CreateTime": "Stvoriti vrijeme",
"CreatedTime": "Stvoriti vrijeme",
"LastModifiedTime": "Posljednje izmijenjeno vrijeme",
"AutoNumber": "Automatski broj",
"Barcode": "Barkod",
@ -1347,4 +1347,4 @@
"roleUpdated": "Uloga je uspješno aktualizirana"
}
}
}
}

4
packages/nc-gui/lang/id.json

@ -273,7 +273,7 @@
"Count": "Menghitung",
"Lookup": "Mencari",
"DateTime": "Tanggal Waktu",
"CreateTime": "Buat waktu",
"CreatedTime": "Buat waktu",
"LastModifiedTime": "Waktu yang dimodifikasi terakhir",
"AutoNumber": "Nomor otomatis",
"Barcode": "Barcode.",
@ -1347,4 +1347,4 @@
"roleUpdated": "Peran berhasil diperbarui"
}
}
}
}

4
packages/nc-gui/lang/it.json

@ -273,7 +273,7 @@
"Count": "Contatore",
"Lookup": "Consultazione",
"DateTime": "Data e ora",
"CreateTime": "Data di creazione",
"CreatedTime": "Data di creazione",
"LastModifiedTime": "Data di ultima modifica",
"AutoNumber": "Numerazione automatica",
"Barcode": "Codice a barre",
@ -1347,4 +1347,4 @@
"roleUpdated": "Ruolo aggiornato con successo"
}
}
}
}

4
packages/nc-gui/lang/ja.json

@ -273,7 +273,7 @@
"Count": "カウント",
"Lookup": "ルックアップ",
"DateTime": "DateTime型",
"CreateTime": "作成時刻",
"CreatedTime": "作成時刻",
"LastModifiedTime": "最終更新日時",
"AutoNumber": "自動採番",
"Barcode": "バーコード",
@ -1347,4 +1347,4 @@
"roleUpdated": "ロールを更新しました"
}
}
}
}

4
packages/nc-gui/lang/ko.json

@ -273,7 +273,7 @@
"Count": "카운트",
"Lookup": "조회",
"DateTime": "일시",
"CreateTime": "생성시간",
"CreatedTime": "생성시간",
"LastModifiedTime": "최종수정시간",
"AutoNumber": "자동번호",
"Barcode": "바코드",
@ -1347,4 +1347,4 @@
"roleUpdated": "역할이 성공적으로 업데이트되었습니다."
}
}
}
}

4
packages/nc-gui/lang/lv.json

@ -273,7 +273,7 @@
"Count": "Skaits",
"Lookup": "Uzmeklēšana",
"DateTime": "Datums un laiks",
"CreateTime": "Izveidošanas laiks",
"CreatedTime": "Izveidošanas laiks",
"LastModifiedTime": "Modificēšanas laiks",
"AutoNumber": "Automātiska numerācija",
"Barcode": "Svītru kods",
@ -1347,4 +1347,4 @@
"roleUpdated": "Loma veiksmīgi atjaunināta"
}
}
}
}

4
packages/nc-gui/lang/nl.json

@ -273,7 +273,7 @@
"Count": "Telling",
"Lookup": "Zoekopdracht",
"DateTime": "Tijdstip",
"CreateTime": "Maak Tijd",
"CreatedTime": "Maak Tijd",
"LastModifiedTime": "Laatst gewijzigd",
"AutoNumber": "Automatische nummering",
"Barcode": "Barcode",
@ -1347,4 +1347,4 @@
"roleUpdated": "Rol succesvol bijgewerkt"
}
}
}
}

4
packages/nc-gui/lang/no.json

@ -273,7 +273,7 @@
"Count": "Telle",
"Lookup": "Se opp",
"DateTime": "Dato tid",
"CreateTime": "Skape tid",
"CreatedTime": "Skape tid",
"LastModifiedTime": "Sist endret tid",
"AutoNumber": "Auto nummer",
"Barcode": "Strekkode",
@ -1347,4 +1347,4 @@
"roleUpdated": "Role updated successfully"
}
}
}
}

4
packages/nc-gui/lang/pl.json

@ -273,7 +273,7 @@
"Count": "Licznik",
"Lookup": "Wyszukiwanie",
"DateTime": "Data i czas",
"CreateTime": "Data utworzenia",
"CreatedTime": "Data utworzenia",
"LastModifiedTime": "Data ostatniej modyfikacji",
"AutoNumber": "Automatyczny numer",
"Barcode": "Kod kreskowy",
@ -1347,4 +1347,4 @@
"roleUpdated": "Rola została pomyślnie zaktualizowana"
}
}
}
}

4
packages/nc-gui/lang/pt.json

@ -273,7 +273,7 @@
"Count": "Contar",
"Lookup": "Olho para cima",
"DateTime": "Data hora",
"CreateTime": "Criar tempo",
"CreatedTime": "Criar tempo",
"LastModifiedTime": "Última hora modificada",
"AutoNumber": "Número automático",
"Barcode": "Código de barras",
@ -1347,4 +1347,4 @@
"roleUpdated": "Papel actualizado com sucesso"
}
}
}
}

4
packages/nc-gui/lang/pt_BR.json

@ -273,7 +273,7 @@
"Count": "Quantidade",
"Lookup": "Procurar",
"DateTime": "Data e hora",
"CreateTime": "Hora da criação",
"CreatedTime": "Hora da criação",
"LastModifiedTime": "Hora da última alteração",
"AutoNumber": "Número automático",
"Barcode": "Código de barras",
@ -1347,4 +1347,4 @@
"roleUpdated": "Papel actualizado com sucesso"
}
}
}
}

4
packages/nc-gui/lang/ru.json

@ -273,7 +273,7 @@
"Count": "Количество",
"Lookup": "Подстановка (Lookup)",
"DateTime": "Дата и время",
"CreateTime": "Создан",
"CreatedTime": "Создан",
"LastModifiedTime": "Изменен",
"AutoNumber": "Счетчик",
"Barcode": "Штрих-код",
@ -1347,4 +1347,4 @@
"roleUpdated": "Роль успешно обновлена"
}
}
}
}

4
packages/nc-gui/lang/sk.json

@ -273,7 +273,7 @@
"Count": "Počítajte",
"Lookup": "Vyhľadávanie",
"DateTime": "Dátum Čas",
"CreateTime": "Vytvoriť čas",
"CreatedTime": "Vytvoriť čas",
"LastModifiedTime": "Čas poslednej úpravy",
"AutoNumber": "Automatické číslo",
"Barcode": "Čiarový kód",
@ -1347,4 +1347,4 @@
"roleUpdated": "Úloha bola úspešne aktualizovaná"
}
}
}
}

4
packages/nc-gui/lang/sl.json

@ -273,7 +273,7 @@
"Count": "Count.",
"Lookup": "Poglej gor",
"DateTime": "Datum čas",
"CreateTime": "Ustvarite čas",
"CreatedTime": "Ustvarite čas",
"LastModifiedTime": "Zadnji spremenjen čas",
"AutoNumber": "Samodejna številka",
"Barcode": "Črtna koda",
@ -1347,4 +1347,4 @@
"roleUpdated": "Vloga je bila uspešno posodobljena"
}
}
}
}

4
packages/nc-gui/lang/sv.json

@ -273,7 +273,7 @@
"Count": "Räkna",
"Lookup": "Slå upp",
"DateTime": "Datum Tid",
"CreateTime": "Skapa tid",
"CreatedTime": "Skapa tid",
"LastModifiedTime": "Senast ändrad tid",
"AutoNumber": "Automatisk nummer",
"Barcode": "Streckkod",
@ -1347,4 +1347,4 @@
"roleUpdated": "Rollen har uppdaterats framgångsrikt"
}
}
}
}

4
packages/nc-gui/lang/th.json

@ -273,7 +273,7 @@
"Count": "นบ",
"Lookup": "การคนหา",
"DateTime": "วนเวลา",
"CreateTime": "สรางเวลา",
"CreatedTime": "สรางเวลา",
"LastModifiedTime": "เวลาทแกไขลาสด",
"AutoNumber": "หมายเลขอตโนม",
"Barcode": "บารโคด",
@ -1347,4 +1347,4 @@
"roleUpdated": "Role updated successfully"
}
}
}
}

4
packages/nc-gui/lang/tr.json

@ -273,7 +273,7 @@
"Count": "Say",
"Lookup": "Referans",
"DateTime": "Tarih saat",
"CreateTime": "Oluşturma zamanı",
"CreatedTime": "Oluşturma zamanı",
"LastModifiedTime": "Son değiştirilme",
"AutoNumber": "Otomatik Sayı",
"Barcode": "Barkod",
@ -1347,4 +1347,4 @@
"roleUpdated": "Rol başarıyla güncellendi"
}
}
}
}

4
packages/nc-gui/lang/uk.json

@ -273,7 +273,7 @@
"Count": "Кількість",
"Lookup": "Пошук",
"DateTime": "Дата і час",
"CreateTime": "Час створення",
"CreatedTime": "Час створення",
"LastModifiedTime": "Час останньої зміни",
"AutoNumber": "Автоматичне прирощування",
"Barcode": "Штрих-код",
@ -1347,4 +1347,4 @@
"roleUpdated": "Роль успішно оновлено"
}
}
}
}

4
packages/nc-gui/lang/vi.json

@ -273,7 +273,7 @@
"Count": "Đếm",
"Lookup": "Tra cứu",
"DateTime": "Ngày giờ",
"CreateTime": "Tạo thời gian",
"CreatedTime": "Tạo thời gian",
"LastModifiedTime": "Thời gian sửa đổi lần cuối",
"AutoNumber": "Số tự động",
"Barcode": "Mã vạch.",
@ -1347,4 +1347,4 @@
"roleUpdated": "Role updated successfully"
}
}
}
}

4
packages/nc-gui/lang/zh-Hans.json

@ -273,7 +273,7 @@
"Count": "计数",
"Lookup": "查找",
"DateTime": "日期时间",
"CreateTime": "创建时间",
"CreatedTime": "创建时间",
"LastModifiedTime": "最后修改时间",
"AutoNumber": "自动编号",
"Barcode": "条形码",
@ -1347,4 +1347,4 @@
"roleUpdated": "权限已更新"
}
}
}
}

4
packages/nc-gui/lang/zh-Hant.json

@ -273,7 +273,7 @@
"Count": "計數",
"Lookup": "查閱",
"DateTime": "日期時間",
"CreateTime": "創建時間",
"CreatedTime": "創建時間",
"LastModifiedTime": "最後修改時間",
"AutoNumber": "自動編號",
"Barcode": "條碼",
@ -1347,4 +1347,4 @@
"roleUpdated": "角色已成功更新"
}
}
}
}

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

@ -15,6 +15,8 @@ export const isYear = (column: ColumnType, abstractType: any) => abstractType ==
export const isTime = (column: ColumnType, abstractType: any) => abstractType === 'time' || column.uidt === UITypes.Time
export const isDateTime = (column: ColumnType, abstractType: any) =>
abstractType === 'datetime' || column.uidt === UITypes.DateTime
export const isReadonlyDateTime = (column: ColumnType, _abstractType: any) =>
column.uidt === UITypes.CreatedTime || column.uidt === UITypes.LastModifiedTime
export const isJSON = (column: ColumnType) => column.uidt === UITypes.JSON
export const isEnum = (column: ColumnType) => column.uidt === UITypes.SingleSelect
export const isSingleSelect = (column: ColumnType) => column.uidt === UITypes.SingleSelect

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

@ -138,6 +138,14 @@ const uiTypes = [
name: UITypes.User,
icon: iconMap.account,
},
{
name: UITypes.CreatedTime,
icon: iconMap.datetime,
},
{
name: UITypes.LastModifiedTime,
icon: iconMap.datetime,
},
]
const getUIDTIcon = (uidt: UITypes | string) => {
@ -145,7 +153,7 @@ const getUIDTIcon = (uidt: UITypes | string) => {
[
...uiTypes,
{
name: UITypes.CreateTime,
name: UITypes.CreatedTime,
icon: iconMap.calendar,
},
{

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

@ -239,19 +239,40 @@ export const comparisonOpList = (
text: getGtText(fieldUiType),
value: 'gt',
ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime, UITypes.Time],
includedTypes: [
...numericUITypes,
UITypes.Date,
UITypes.DateTime,
UITypes.LastModifiedTime,
UITypes.CreatedTime,
UITypes.Time,
],
},
{
text: getLtText(fieldUiType),
value: 'lt',
ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime, UITypes.Time],
includedTypes: [
...numericUITypes,
UITypes.Date,
UITypes.DateTime,
UITypes.LastModifiedTime,
UITypes.CreatedTime,
UITypes.Time,
],
},
{
text: getGteText(fieldUiType),
value: 'gte',
ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime, UITypes.Time],
includedTypes: [
...numericUITypes,
UITypes.Date,
UITypes.DateTime,
UITypes.LastModifiedTime,
UITypes.CreatedTime,
UITypes.Time,
],
},
{
text: getLteText(fieldUiType),
@ -263,7 +284,7 @@ export const comparisonOpList = (
text: 'is within',
value: 'isWithin',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
includedTypes: [UITypes.Date, UITypes.DateTime, UITypes.LastModifiedTime, UITypes.CreatedTime],
},
{
text: 'is blank',
@ -298,49 +319,49 @@ export const comparisonSubOpList = (
text: 'the past week',
value: 'pastWeek',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
includedTypes: [UITypes.Date, UITypes.DateTime, UITypes.LastModifiedTime, UITypes.CreatedTime],
},
{
text: 'the past month',
value: 'pastMonth',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
includedTypes: [UITypes.Date, UITypes.DateTime, UITypes.LastModifiedTime, UITypes.CreatedTime],
},
{
text: 'the past year',
value: 'pastYear',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
includedTypes: [UITypes.Date, UITypes.DateTime, UITypes.LastModifiedTime, UITypes.CreatedTime],
},
{
text: 'the next week',
value: 'nextWeek',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
includedTypes: [UITypes.Date, UITypes.DateTime, UITypes.LastModifiedTime, UITypes.CreatedTime],
},
{
text: 'the next month',
value: 'nextMonth',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
includedTypes: [UITypes.Date, UITypes.DateTime, UITypes.LastModifiedTime, UITypes.CreatedTime],
},
{
text: 'the next year',
value: 'nextYear',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
includedTypes: [UITypes.Date, UITypes.DateTime, UITypes.LastModifiedTime, UITypes.CreatedTime],
},
{
text: 'the next number of days',
value: 'nextNumberOfDays',
ignoreVal: false,
includedTypes: [UITypes.Date, UITypes.DateTime],
includedTypes: [UITypes.Date, UITypes.DateTime, UITypes.LastModifiedTime, UITypes.CreatedTime],
},
{
text: 'the past number of days',
value: 'pastNumberOfDays',
ignoreVal: false,
includedTypes: [UITypes.Date, UITypes.DateTime],
includedTypes: [UITypes.Date, UITypes.DateTime, UITypes.LastModifiedTime, UITypes.CreatedTime],
},
]
}
@ -349,61 +370,61 @@ export const comparisonSubOpList = (
text: 'today',
value: 'today',
ignoreVal: true,
includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTime])],
includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTime, UITypes.LastModifiedTime, UITypes.CreatedTime])],
},
{
text: 'tomorrow',
value: 'tomorrow',
ignoreVal: true,
includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTime])],
includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTime, UITypes.LastModifiedTime, UITypes.CreatedTime])],
},
{
text: 'yesterday',
value: 'yesterday',
ignoreVal: true,
includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTime])],
includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTim, UITypes.LastModifiedTime, UITypes.CreatedTimee])],
},
{
text: 'one week ago',
value: 'oneWeekAgo',
ignoreVal: true,
includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTime])],
includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTime, UITypes.LastModifiedTime, UITypes.CreatedTime])],
},
{
text: 'one week from now',
value: 'oneWeekFromNow',
ignoreVal: true,
includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTime])],
includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTime, UITypes.LastModifiedTime, UITypes.CreatedTime])],
},
{
text: 'one month ago',
value: 'oneMonthAgo',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
includedTypes: [UITypes.Date, UITypes.DateTime, UITypes.LastModifiedTime, UITypes.CreatedTime],
},
{
text: 'one month from now',
value: 'oneMonthFromNow',
ignoreVal: true,
includedTypes: [UITypes.Date, UITypes.DateTime],
includedTypes: [UITypes.Date, UITypes.DateTime, UITypes.LastModifiedTime, UITypes.CreatedTime],
},
{
text: 'number of days ago',
value: 'daysAgo',
ignoreVal: false,
includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTime])],
includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTime, UITypes.LastModifiedTime, UITypes.CreatedTime])],
},
{
text: 'number of days from now',
value: 'daysFromNow',
ignoreVal: false,
includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTime])],
includedTypes: [...(isDateMonth ? [] : [UITypes.Date, UITypes.DateTime, UITypes.LastModifiedTime, UITypes.CreatedTime])],
},
{
text: isDateMonth ? 'exact month' : 'exact date',
value: 'exactDate',
ignoreVal: false,
includedTypes: [UITypes.Date, UITypes.DateTime],
includedTypes: [UITypes.Date, UITypes.DateTime, UITypes.LastModifiedTime, UITypes.CreatedTime],
},
]
}

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

@ -15,7 +15,7 @@ export const getSortDirectionOptions = (uidt: UITypes | string) => {
case UITypes.PhoneNumber:
case UITypes.Date:
case UITypes.DateTime:
case UITypes.CreateTime:
case UITypes.CreatedTime:
case UITypes.LastModifiedTime:
return [
{ text: '1 → 9', value: 'asc' },

2
packages/nocodb-sdk/src/lib/CustomAPI.ts

@ -1,9 +1,9 @@
import {
Api,
BaseType,
ColumnType,
FilterType,
HookType,
BaseType,
SortType,
} from './Api';

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

@ -29,7 +29,7 @@ enum UITypes {
Rollup = 'Rollup',
Count = 'Count',
DateTime = 'DateTime',
CreateTime = 'CreateTime',
CreatedTime = 'CreatedTime',
LastModifiedTime = 'LastModifiedTime',
AutoNumber = 'AutoNumber',
Geometry = 'Geometry',
@ -83,9 +83,22 @@ export function isVirtualCol(
UITypes.Rollup,
UITypes.Lookup,
UITypes.Links,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
// UITypes.Count,
].includes(<UITypes>(typeof col === 'object' ? col?.uidt : col));
}
export function isCreatedOrLastModifiedTimeCol(
col:
| UITypes
| { readonly uidt: UITypes | string }
| ColumnReqType
| ColumnType
) {
return [UITypes.CreatedTime, UITypes.LastModifiedTime].includes(
<UITypes>(typeof col === 'object' ? col?.uidt : col)
);
}
export function isLinksOrLTAR(
colOrUidt: ColumnType | { uidt: UITypes | string } | UITypes | string

4
packages/nocodb-sdk/src/lib/columnRules/QrAndBarcodeRules.ts

@ -9,6 +9,6 @@ export const AllowedColumnTypesForQrAndBarcodes = [
UITypes.Email,
UITypes.Decimal,
UITypes.Number,
UITypes.AutoNumber,
UITypes.ID
UITypes.AutoNumber,
UITypes.ID,
];

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

@ -197,18 +197,22 @@ export const RoleColors = {
export const RoleDescriptions = {
[WorkspaceUserRoles.OWNER]: 'Full access to workspace',
[WorkspaceUserRoles.CREATOR]: 'Can create bases, sync tables, views, setup web-hooks and more',
[WorkspaceUserRoles.CREATOR]:
'Can create bases, sync tables, views, setup web-hooks and more',
[WorkspaceUserRoles.EDITOR]: 'Can edit data in workspace bases',
[WorkspaceUserRoles.COMMENTER]: 'Can view and comment data in workspace bases',
[WorkspaceUserRoles.COMMENTER]:
'Can view and comment data in workspace bases',
[WorkspaceUserRoles.VIEWER]: 'Can view data in workspace bases',
[ProjectRoles.OWNER]: 'Full access to base',
[ProjectRoles.CREATOR]: 'Can create tables, views, setup webhook, invite collaborators and more',
[ProjectRoles.CREATOR]:
'Can create tables, views, setup webhook, invite collaborators and more',
[ProjectRoles.EDITOR]: 'Can view, add & modify records, add comments on them',
[ProjectRoles.COMMENTER]: 'Can view records and add comment on them',
[ProjectRoles.VIEWER]: 'Can only view records',
[ProjectRoles.NO_ACCESS]: 'Cannot access this base',
[OrgUserRoles.SUPER_ADMIN]: 'Full access to all',
[OrgUserRoles.CREATOR]: 'Can create bases, sync tables, views, setup web-hooks and more',
[OrgUserRoles.CREATOR]:
'Can create bases, sync tables, views, setup web-hooks and more',
[OrgUserRoles.VIEWER]: 'Can only view bases',
};

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

@ -1528,7 +1528,7 @@ async function extractColumnIdentifierType({
// date
case UITypes.Date:
case UITypes.DateTime:
case UITypes.CreateTime:
case UITypes.CreatedTime:
case UITypes.LastModifiedTime:
res.dataType = FormulaDataTypes.DATE;
break;

2
packages/nocodb-sdk/src/lib/globals.ts

@ -1,4 +1,4 @@
import { OrgUserRoles, ProjectRoles, WorkspaceUserRoles } from "./enums";
import { OrgUserRoles, ProjectRoles, WorkspaceUserRoles } from './enums';
export enum ViewTypes {
FORM = 1,

4
packages/nocodb-sdk/src/lib/helperFunctions.ts

@ -16,8 +16,8 @@ const getSystemColumns = (columns) => columns.filter(isSystemColumn) || [];
const isSystemColumn = (col): boolean =>
col &&
(col.uidt === UITypes.ForeignKey ||
col.column_name === 'created_at' ||
col.column_name === 'updated_at' ||
((col.column_name === 'created_at' || col.column_name === 'updated_at') &&
col.uidt === UITypes.DateTime) ||
(col.pk && (col.ai || col.cdf)) ||
(col.pk && col.meta && col.meta.ag) ||
col.system);

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

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

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

@ -1,4 +1,3 @@
export function mergeSwaggerSchema(swaggerCE, swaggerEE) {
return {
...swaggerCE,

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

@ -93,30 +93,30 @@ export class MssqlUi {
title: 'CreatedAt',
dt: 'datetime',
dtx: 'specificType',
ct: 'varchar(45)',
ct: 'datetime',
nrqd: true,
rqd: false,
ck: false,
pk: false,
un: false,
ai: false,
cdf: 'GETDATE()',
clen: 45,
np: null,
ns: null,
dtxp: '',
dtxs: '',
altered: 1,
uidt: UITypes.DateTime,
uidt: UITypes.CreatedTime,
uip: '',
uicn: '',
system: true,
},
{
column_name: 'updated_at',
title: 'UpdatedAt',
dt: 'datetime',
dtx: 'specificType',
ct: 'varchar(45)',
ct: 'datetime',
nrqd: true,
rqd: false,
ck: false,
@ -124,16 +124,16 @@ export class MssqlUi {
un: false,
ai: false,
au: true,
cdf: 'GETDATE()',
clen: 45,
np: null,
ns: null,
dtxp: '',
dtxs: '',
altered: 1,
uidt: UITypes.DateTime,
uidt: UITypes.LastModifiedTime,
uip: '',
uicn: '',
system: true,
},
];
}
@ -591,7 +591,7 @@ export class MssqlUi {
case 'date':
return 'Date';
case 'datetime':
return 'CreateTime';
return 'CreatedTime';
case 'time':
return 'Time';
case 'year':
@ -733,7 +733,7 @@ export class MssqlUi {
case 'DateTime':
colProp.dt = 'datetimeoffset';
break;
case 'CreateTime':
case 'CreatedTime':
colProp.dt = 'datetime';
break;
case 'LastModifiedTime':
@ -885,7 +885,7 @@ export class MssqlUi {
return ['date'];
case 'DateTime':
case 'CreateTime':
case 'CreatedTime':
case 'LastModifiedTime':
return [
'datetimeoffset',

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

@ -97,46 +97,47 @@ export class MysqlUi {
title: 'CreatedAt',
dt: 'timestamp',
dtx: 'specificType',
ct: 'varchar(45)',
ct: 'timestamp',
nrqd: true,
rqd: false,
ck: false,
pk: false,
un: false,
ai: false,
cdf: 'CURRENT_TIMESTAMP',
cdf: null,
clen: 45,
np: null,
ns: null,
dtxp: '',
dtxs: '',
altered: 1,
uidt: UITypes.DateTime,
uidt: UITypes.CreatedTime,
uip: '',
uicn: '',
system: true,
},
{
column_name: 'updated_at',
title: 'UpdatedAt',
dt: 'timestamp',
dtx: 'specificType',
ct: 'varchar(45)',
ct: 'timestamp',
nrqd: true,
rqd: false,
ck: false,
pk: false,
un: false,
ai: false,
cdf: 'CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP',
clen: 45,
np: null,
ns: null,
dtxp: '',
dtxs: '',
altered: 1,
uidt: UITypes.DateTime,
uidt: UITypes.LastModifiedTime,
uip: '',
uicn: '',
system: true,
},
];
}
@ -1071,11 +1072,11 @@ export class MysqlUi {
case 'DateTime':
colProp.dt = 'datetime';
break;
case 'CreateTime':
colProp.dt = 'datetime';
case 'CreatedTime':
colProp.dt = 'timestamp';
break;
case 'LastModifiedTime':
colProp.dt = 'datetime';
colProp.dt = 'timestamp';
break;
case 'AutoNumber':
colProp.dt = 'int';
@ -1261,7 +1262,7 @@ export class MysqlUi {
return ['date', 'datetime', 'timestamp', 'varchar'];
case 'DateTime':
case 'CreateTime':
case 'CreatedTime':
case 'LastModifiedTime':
return ['datetime', 'timestamp', 'varchar'];

4
packages/nocodb-sdk/src/lib/sqlUi/OracleUi.ts

@ -775,7 +775,7 @@ export class OracleUi {
case 'date':
return 'Date';
case 'datetime':
return 'CreateTime';
return 'CreatedTime';
case 'time':
return 'Time';
case 'year':
@ -915,7 +915,7 @@ export class OracleUi {
case 'DateTime':
colProp.dt = 'timestamp';
break;
case 'CreateTime':
case 'CreatedTime':
colProp.dt = 'timestamp';
break;
case 'LastModifiedTime':

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

@ -156,47 +156,46 @@ export class PgUi {
title: 'CreatedAt',
dt: 'timestamp',
dtx: 'specificType',
ct: 'varchar(45)',
ct: 'timestamp',
nrqd: true,
rqd: false,
ck: false,
pk: false,
un: false,
ai: false,
cdf: 'now()',
clen: 45,
np: null,
ns: null,
dtxp: '',
dtxs: '',
altered: 1,
uidt: UITypes.DateTime,
uidt: UITypes.CreatedTime,
uip: '',
uicn: '',
system: true,
},
{
column_name: 'updated_at',
title: 'UpdatedAt',
dt: 'timestamp',
dtx: 'specificType',
ct: 'varchar(45)',
ct: 'timestamp',
nrqd: true,
rqd: false,
ck: false,
pk: false,
un: false,
ai: false,
au: true,
cdf: 'now()',
clen: 45,
np: null,
ns: null,
dtxp: '',
dtxs: '',
altered: 1,
uidt: UITypes.DateTime,
uidt: UITypes.LastModifiedTime,
uip: '',
uicn: '',
system: true,
},
];
}
@ -1159,7 +1158,7 @@ export class PgUi {
static columnEditable(colObj) {
return colObj.tn !== '_evolutions' || colObj.tn !== 'nc_evolutions';
}
/*
/*
static extractFunctionName(query) {
const reg =
@ -1549,7 +1548,7 @@ export class PgUi {
case 'date':
return 'Date';
case 'datetime':
return 'CreateTime';
return 'CreatedTime';
case 'time':
return 'Time';
case 'year':
@ -1685,7 +1684,7 @@ export class PgUi {
case 'DateTime':
colProp.dt = 'timestamp';
break;
case 'CreateTime':
case 'CreatedTime':
colProp.dt = 'timestamp';
break;
case 'LastModifiedTime':
@ -1933,7 +1932,7 @@ export class PgUi {
];
case 'DateTime':
case 'CreateTime':
case 'CreatedTime':
case 'LastModifiedTime':
return [
'timestamp',

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

@ -92,47 +92,46 @@ export class SnowflakeUi {
title: 'CreatedAt',
dt: 'timestamp',
dtx: 'specificType',
ct: 'varchar(45)',
ct: 'timestamp',
nrqd: true,
rqd: false,
ck: false,
pk: false,
un: false,
ai: false,
cdf: 'current_timestamp()',
clen: 45,
np: null,
ns: null,
dtxp: '',
dtxs: '',
altered: 1,
uidt: UITypes.DateTime,
uidt: UITypes.CreatedTime,
uip: '',
uicn: '',
system: true,
},
{
column_name: 'updated_at',
title: 'UpdatedAt',
dt: 'timestamp',
dtx: 'specificType',
ct: 'varchar(45)',
ct: 'timestamp',
nrqd: true,
rqd: false,
ck: false,
pk: false,
un: false,
ai: false,
au: true,
cdf: 'current_timestamp()',
clen: 45,
np: null,
ns: null,
dtxp: '',
dtxs: '',
altered: 1,
uidt: UITypes.DateTime,
uidt: UITypes.LastModifiedTime,
uip: '',
uicn: '',
system: true,
},
];
}
@ -776,7 +775,7 @@ export class SnowflakeUi {
case 'DateTime':
colProp.dt = 'TIMESTAMP';
break;
case 'CreateTime':
case 'CreatedTime':
colProp.dt = 'TIMESTAMP';
break;
case 'LastModifiedTime':
@ -952,7 +951,7 @@ export class SnowflakeUi {
return ['DATE', 'TIMESTAMP'];
case 'DateTime':
case 'CreateTime':
case 'CreatedTime':
case 'LastModifiedTime':
return ['TIMESTAMP'];

2
packages/nocodb-sdk/src/lib/sqlUi/SqlUiFactory.ts

@ -1,4 +1,4 @@
import { BoolType } from '../Api'
import { BoolType } from '../Api';
import UITypes from '../UITypes';
import { MssqlUi } from './MssqlUi';

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

@ -80,46 +80,46 @@ export class SqliteUi {
title: 'CreatedAt',
dt: 'datetime',
dtx: 'specificType',
ct: 'varchar',
ct: 'datetime',
nrqd: true,
rqd: false,
ck: false,
pk: false,
un: false,
ai: false,
cdf: 'CURRENT_TIMESTAMP',
clen: 45,
np: null,
ns: null,
dtxp: '',
dtxs: '',
altered: 1,
uidt: UITypes.DateTime,
uidt: UITypes.CreatedTime,
uip: '',
uicn: '',
system: true,
},
{
column_name: 'updated_at',
title: 'UpdatedAt',
dt: 'datetime',
dtx: 'specificType',
ct: 'varchar',
ct: 'datetime',
nrqd: true,
rqd: false,
ck: false,
pk: false,
un: false,
ai: false,
cdf: 'CURRENT_TIMESTAMP',
clen: 45,
np: null,
ns: null,
dtxp: '',
dtxs: '',
altered: 1,
uidt: UITypes.DateTime,
uidt: UITypes.LastModifiedTime,
uip: '',
uicn: '',
system: true,
},
];
}
@ -490,7 +490,7 @@ export class SqliteUi {
case 'date':
return 'Date';
case 'datetime':
return 'CreateTime';
return 'CreatedTime';
case 'time':
return 'Time';
case 'year':
@ -626,7 +626,7 @@ export class SqliteUi {
case 'DateTime':
colProp.dt = 'datetime';
break;
case 'CreateTime':
case 'CreatedTime':
colProp.dt = 'datetime';
break;
case 'LastModifiedTime':
@ -837,7 +837,7 @@ export class SqliteUi {
return ['date', 'varchar'];
case 'DateTime':
case 'CreateTime':
case 'CreatedTime':
case 'LastModifiedTime':
return ['datetime', 'timestamp'];

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

@ -8,6 +8,7 @@ import { nocoExecute } from 'nc-help';
import {
AuditOperationSubTypes,
AuditOperationTypes,
isCreatedOrLastModifiedTimeCol,
isLinksOrLTAR,
isSystemColumn,
isVirtualCol,
@ -18,6 +19,7 @@ import Validator from 'validator';
import { customAlphabet } from 'nanoid';
import DOMPurify from 'isomorphic-dompurify';
import { v4 as uuidv4 } from 'uuid';
import type { SortType } from 'nocodb-sdk';
import type { Knex } from 'knex';
import type LookupColumn from '~/models/LookupColumn';
import type { XKnex } from '~/db/CustomKnex';
@ -35,15 +37,6 @@ import type {
SelectOption,
User,
} from '~/models';
import type { SortType } from 'nocodb-sdk';
import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2';
import genRollupSelectv2 from '~/db/genRollupSelectv2';
import conditionV2 from '~/db/conditionV2';
import sortV2 from '~/db/sortV2';
import { customValidators } from '~/db/util/customValidators';
import { extractLimitAndOffset } from '~/helpers';
import { NcError } from '~/helpers/catchError';
import getAst from '~/helpers/getAst';
import {
Audit,
BaseUser,
@ -55,6 +48,14 @@ import {
Source,
View,
} from '~/models';
import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2';
import genRollupSelectv2 from '~/db/genRollupSelectv2';
import conditionV2 from '~/db/conditionV2';
import sortV2 from '~/db/sortV2';
import { customValidators } from '~/db/util/customValidators';
import { extractLimitAndOffset } from '~/helpers';
import { NcError } from '~/helpers/catchError';
import getAst from '~/helpers/getAst';
import { sanitize, unsanitize } from '~/helpers/sqlSanitize';
import Noco from '~/Noco';
import { HANDLE_WEBHOOK } from '~/services/hook-handler.service';
@ -101,6 +102,31 @@ function checkColumnRequired(
return !fields || fields.includes(column.title);
}
export async function getColumnName(column: Column<any>, columns?: Column[]) {
if (!isCreatedOrLastModifiedTimeCol(column)) return column.column_name;
columns = columns || (await Column.list({ fk_model_id: column.fk_model_id }));
switch (column.uidt) {
case UITypes.CreatedTime: {
const createdTimeSystemCol = columns.find(
(col) => col.system && col.uidt === UITypes.CreatedTime,
);
if (createdTimeSystemCol) return createdTimeSystemCol.column_name;
return column.column_name || 'created_at';
}
case UITypes.LastModifiedTime: {
const lastModifiedTimeSystemCol = columns.find(
(col) => col.system && col.uidt === UITypes.LastModifiedTime,
);
if (lastModifiedTimeSystemCol)
return lastModifiedTimeSystemCol.column_name;
return column.column_name || 'updated_at';
}
default:
return column.column_name;
}
}
/**
* Base class for models
*
@ -641,10 +667,13 @@ class BaseModelSqlv2 {
}
break;
default:
selectors.push(
this.dbDriver.raw('?? as ??', [column.column_name, column.id]),
);
groupBySelectors.push(sanitize(column.id));
{
const columnName = await getColumnName(column, cols);
selectors.push(
this.dbDriver.raw('?? as ??', [columnName, column.id]),
);
groupBySelectors.push(sanitize(column.id));
}
break;
}
}),
@ -849,10 +878,13 @@ class BaseModelSqlv2 {
}
break;
default:
selectors.push(
this.dbDriver.raw('?? as ??', [column.column_name, column.id]),
);
groupBySelectors.push(sanitize(column.id));
{
const columnName = await getColumnName(column, cols);
selectors.push(
this.dbDriver.raw('?? as ??', [columnName, column.id]),
);
groupBySelectors.push(sanitize(column.id));
}
break;
}
}),
@ -2113,53 +2145,60 @@ class BaseModelSqlv2 {
if (!checkColumnRequired(column, fields, extractPkAndPv)) continue;
switch (column.uidt) {
case UITypes.CreatedTime:
case UITypes.LastModifiedTime:
case UITypes.DateTime:
if (this.isMySQL) {
// MySQL stores timestamp in UTC but display in timezone
// To verify the timezone, run `SELECT @@global.time_zone, @@session.time_zone;`
// If it's SYSTEM, then the timezone is read from the configuration file
// if a timezone is set in a DB, the retrieved value would be converted to the corresponding timezone
// for example, let's say the global timezone is +08:00 in DB
// the value 2023-01-01 10:00:00 (UTC) would display as 2023-01-01 18:00:00 (UTC+8)
// our existing logic is based on UTC, during the query, we need to take the UTC value
// hence, we use CONVERT_TZ to convert back to UTC value
res[sanitize(column.id || column.column_name)] = this.dbDriver.raw(
`CONVERT_TZ(??, @@GLOBAL.time_zone, '+00:00')`,
[`${sanitize(alias || this.tnPath)}.${column.column_name}`],
{
const columnName = await getColumnName(
column,
await this.model.getColumns(),
);
break;
} else if (this.isPg) {
// if there is no timezone info,
// convert to database timezone,
// then convert to UTC
if (
column.dt !== 'timestamp with time zone' &&
column.dt !== 'timestamptz'
) {
res[sanitize(column.id || column.column_name)] = this.dbDriver
.raw(
`?? AT TIME ZONE CURRENT_SETTING('timezone') AT TIME ZONE 'UTC'`,
[`${sanitize(alias || this.tnPath)}.${column.column_name}`],
)
.wrap('(', ')');
if (this.isMySQL) {
// MySQL stores timestamp in UTC but display in timezone
// To verify the timezone, run `SELECT @@global.time_zone, @@session.time_zone;`
// If it's SYSTEM, then the timezone is read from the configuration file
// if a timezone is set in a DB, the retrieved value would be converted to the corresponding timezone
// for example, let's say the global timezone is +08:00 in DB
// the value 2023-01-01 10:00:00 (UTC) would display as 2023-01-01 18:00:00 (UTC+8)
// our existing logic is based on UTC, during the query, we need to take the UTC value
// hence, we use CONVERT_TZ to convert back to UTC value
res[sanitize(column.id || columnName)] = this.dbDriver.raw(
`CONVERT_TZ(??, @@GLOBAL.time_zone, '+00:00')`,
[`${sanitize(alias || this.tnPath)}.${columnName}`],
);
break;
}
} else if (this.isMssql) {
// if there is no timezone info,
// convert to database timezone,
// then convert to UTC
if (column.dt !== 'datetimeoffset') {
res[sanitize(column.id || column.column_name)] =
this.dbDriver.raw(
} else if (this.isPg) {
// if there is no timezone info,
// convert to database timezone,
// then convert to UTC
if (
column.dt !== 'timestamp with time zone' &&
column.dt !== 'timestamptz'
) {
res[sanitize(column.id || columnName)] = this.dbDriver
.raw(
`?? AT TIME ZONE CURRENT_SETTING('timezone') AT TIME ZONE 'UTC'`,
[`${sanitize(alias || this.tnPath)}.${columnName}`],
)
.wrap('(', ')');
break;
}
} else if (this.isMssql) {
// if there is no timezone info,
// convert to database timezone,
// then convert to UTC
if (column.dt !== 'datetimeoffset') {
res[sanitize(column.id || columnName)] = this.dbDriver.raw(
`CONVERT(DATETIMEOFFSET, ?? AT TIME ZONE 'UTC')`,
[`${sanitize(alias || this.tnPath)}.${column.column_name}`],
[`${sanitize(alias || this.tnPath)}.${columnName}`],
);
break;
break;
}
}
res[sanitize(column.id || columnName)] = sanitize(
`${alias || this.tnPath}.${columnName}`,
);
}
res[sanitize(column.id || column.column_name)] = sanitize(
`${alias || this.tnPath}.${column.column_name}`,
);
break;
case UITypes.LinkToAnotherRecord:
case UITypes.Lookup:
@ -2329,7 +2368,7 @@ class BaseModelSqlv2 {
await this.beforeInsert(insertObj, trx, cookie);
}
await this.prepareNocoData(insertObj);
await this.prepareNocoData(insertObj, true);
let response;
// const driver = trx ? trx : this.dbDriver;
@ -2680,7 +2719,6 @@ class BaseModelSqlv2 {
this.clientMeta,
this.dbDriver,
);
let rowId = null;
const nestedCols = (await this.model.getColumns()).filter((c) =>
@ -2696,6 +2734,8 @@ class BaseModelSqlv2 {
await this.beforeInsert(insertObj, this.dbDriver, cookie);
await this.prepareNocoData(insertObj, true);
let response;
const query = this.dbDriver(this.tnPath).insert(insertObj);
@ -2922,6 +2962,12 @@ class BaseModelSqlv2 {
for (let i = 0; i < this.model.columns.length; ++i) {
const col = this.model.columns[i];
if (col.title in d && isCreatedOrLastModifiedTimeCol(col)) {
NcError.badRequest(
`Column "${col.title}" is auto generated and cannot be updated`,
);
}
// populate pk columns
if (col.pk) {
if (col.meta?.ag && !d[col.title]) {
@ -3039,7 +3085,7 @@ class BaseModelSqlv2 {
}
}
await this.prepareNocoData(insertObj);
await this.prepareNocoData(insertObj, true);
// prepare nested link data for insert only if it is single record insertion
if (isSingleRecordInsertion) {
@ -3152,6 +3198,13 @@ class BaseModelSqlv2 {
try {
if (raw) await this.model.getColumns();
// validate update data
if (!raw) {
for (const d of datas) {
await this.validate(d);
}
}
const updateDatas = raw
? datas
: await Promise.all(
@ -3165,7 +3218,6 @@ class BaseModelSqlv2 {
const updatePkValues = [];
const toBeUpdated = [];
for (const d of updateDatas) {
if (!raw) await this.validate(d);
const pkValues = await this._extractPksValues(d);
if (!pkValues) {
// throw or skip if no pk provided
@ -3789,12 +3841,18 @@ class BaseModelSqlv2 {
protected async errorDelete(_e, _id, _trx, _cookie) {}
async validate(columns) {
async validate(data: Record<string, any>): Promise<boolean> {
await this.model.getColumns();
// let cols = Object.keys(this.columns);
for (let i = 0; i < this.model.columns.length; ++i) {
const column = this.model.columns[i];
await this.validateOptions(column, columns);
if (column.title in data && isCreatedOrLastModifiedTimeCol(column)) {
NcError.badRequest(
`Column "${column.title}" is auto generated and cannot be updated`,
);
}
await this.validateOptions(column, data);
// skip validation if `validate` is undefined or false
if (!column?.meta?.validate || !column?.validate) continue;
@ -3812,7 +3870,7 @@ class BaseModelSqlv2 {
? customValidators[func[j]]
: Validator[func[j]]
: func[j];
const columnValue = columns?.[cn] || columns?.[columnTitle];
const columnValue = data?.[cn] || data?.[columnTitle];
const arg =
typeof func[j] === 'string' ? columnValue + '' : columnValue;
if (
@ -3959,6 +4017,15 @@ class BaseModelSqlv2 {
{ raw: true },
);
}
await this.updateLastModifiedTime({
model: parentTable,
rowIds: [childId],
});
await this.updateLastModifiedTime({
model: childTable,
rowIds: [rowId],
});
}
break;
case RelationTypes.HAS_MANY:
@ -3978,6 +4045,11 @@ class BaseModelSqlv2 {
null,
{ raw: true },
);
await this.updateLastModifiedTime({
model: parentTable,
rowIds: [rowId],
});
}
break;
case RelationTypes.BELONGS_TO:
@ -3997,6 +4069,11 @@ class BaseModelSqlv2 {
null,
{ raw: true },
);
await this.updateLastModifiedTime({
model: parentTable,
rowIds: [childId],
});
}
break;
}
@ -4090,6 +4167,15 @@ class BaseModelSqlv2 {
null,
{ raw: true },
);
await this.updateLastModifiedTime({
model: parentTable,
rowIds: [childId],
});
await this.updateLastModifiedTime({
model: childTable,
rowIds: [rowId],
});
}
break;
case RelationTypes.HAS_MANY:
@ -4107,6 +4193,11 @@ class BaseModelSqlv2 {
null,
{ raw: true },
);
await this.updateLastModifiedTime({
model: parentTable,
rowIds: [rowId],
});
}
break;
case RelationTypes.BELONGS_TO:
@ -4124,6 +4215,11 @@ class BaseModelSqlv2 {
null,
{ raw: true },
);
await this.updateLastModifiedTime({
model: parentTable,
rowIds: [childId],
});
}
break;
}
@ -5033,6 +5129,15 @@ class BaseModelSqlv2 {
await this.execAndParse(this.dbDriver(vTn).insert(insertData), null, {
raw: true,
});
await this.updateLastModifiedTime({
model: parentTable,
rowIds: childIds,
});
await this.updateLastModifiedTime({
model: childTable,
rowIds: [rowId],
});
}
break;
case RelationTypes.HAS_MANY:
@ -5107,6 +5212,11 @@ class BaseModelSqlv2 {
);
}
await this.execAndParse(updateQb, null, { raw: true });
await this.updateLastModifiedTime({
model: parentTable,
rowIds: [rowId],
});
}
break;
case RelationTypes.BELONGS_TO:
@ -5149,6 +5259,11 @@ class BaseModelSqlv2 {
null,
{ raw: true },
);
await this.updateLastModifiedTime({
model: parentTable,
rowIds: [rowId],
});
}
break;
}
@ -5290,6 +5405,15 @@ class BaseModelSqlv2 {
: childIds,
);
await this.execAndParse(delQb, null, { raw: true });
await this.updateLastModifiedTime({
model: parentTable,
rowIds: childIds,
});
await this.updateLastModifiedTime({
model: childTable,
rowIds: [rowId],
});
}
break;
case RelationTypes.HAS_MANY:
@ -5370,6 +5494,11 @@ class BaseModelSqlv2 {
null,
{ raw: true },
);
await this.updateLastModifiedTime({
model: parentTable,
rowIds: [rowId],
});
}
break;
case RelationTypes.BELONGS_TO:
@ -5415,6 +5544,11 @@ class BaseModelSqlv2 {
null,
{ raw: true },
);
await this.updateLastModifiedTime({
model: parentTable,
rowIds: [childIds[0]],
});
}
break;
}
@ -5500,13 +5634,55 @@ class BaseModelSqlv2 {
}
}
async prepareNocoData(data) {
async updateLastModifiedTime({
rowIds,
model = this.model,
knex = this.dbDriver,
}: {
rowIds: any | any[];
model?: Model;
knex?: XKnex;
}) {
const columnName = await model.getColumns().then((columns) => {
return columns.find((c) => c.uidt === UITypes.LastModifiedTime)
?.column_name;
});
if (!columnName) return;
const qb = knex(model.table_name).update({
[columnName]: Noco.ncMeta.now(),
});
for (const rowId of Array.isArray(rowIds) ? rowIds : [rowIds]) {
qb.orWhere(await this._wherePk(rowId));
}
await this.execAndParse(qb, null, { raw: true });
}
async prepareNocoData(data, isInsertData = false) {
if (
this.model.columns.some((c) =>
[UITypes.Attachment, UITypes.User].includes(c.uidt),
[
UITypes.Attachment,
UITypes.User,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
].includes(c.uidt),
)
) {
for (const column of this.model.columns) {
if (
isInsertData &&
column.uidt === UITypes.CreatedTime &&
column.system
) {
data[column.column_name] = Noco.ncMeta.now();
}
if (column.uidt === UITypes.LastModifiedTime && column.system) {
data[column.column_name] = isInsertData ? null : Noco.ncMeta.now();
}
if (column.uidt === UITypes.Attachment) {
if (data[column.column_name]) {
if (Array.isArray(data[column.column_name])) {

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

@ -13,6 +13,7 @@ import type Column from '~/models/Column';
import type LookupColumn from '~/models/LookupColumn';
import type RollupColumn from '~/models/RollupColumn';
import type FormulaColumn from '~/models/FormulaColumn';
import { getColumnName } from '~/db/BaseModelSqlv2';
import { type BarcodeColumn, BaseUser, type QrCodeColumn } from '~/models';
import { NcError } from '~/helpers/catchError';
import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2';
@ -21,6 +22,7 @@ import { sanitize } from '~/helpers/sqlSanitize';
import Filter from '~/models/Filter';
import generateLookupSelectQuery from '~/db/generateLookupSelectQuery';
import { getAliasGenerator } from '~/utils';
import { getRefColumnIfAlias } from '~/helpers';
// tod: tobe fixed
// extend(customParseFormat);
@ -128,7 +130,10 @@ const parseConditionV2 = async (
(filter.comparison_op as any) === 'gb_eq' ||
(filter.comparison_op as any) === 'gb_null'
) {
const column = await filter.getColumn();
(filter as any).groupby = true;
const column = await getRefColumnIfAlias(await filter.getColumn());
if (
column.uidt === UITypes.Lookup ||
column.uidt === UITypes.LinkToAnotherRecord
@ -157,7 +162,7 @@ const parseConditionV2 = async (
}
}
const column = await filter.getColumn();
const column = await getRefColumnIfAlias(await filter.getColumn());
if (!column) {
if (throwErrorIfInvalid) {
NcError.unprocessableEntity(`Invalid field: ${filter.fk_column_id}`);
@ -542,6 +547,9 @@ const parseConditionV2 = async (
);
const _val = customWhereClause ? customWhereClause : filter.value;
// get column name for CreateTime, LastModifiedTime
column.column_name = await getColumnName(column);
return (qb: Knex.QueryBuilder) => {
let [field, val] = [_field, _val];
@ -550,7 +558,14 @@ const parseConditionV2 = async (
? 'YYYY-MM-DD HH:mm:ss'
: 'YYYY-MM-DD HH:mm:ssZ';
if ([UITypes.Date, UITypes.DateTime].includes(column.uidt)) {
if (
[
UITypes.Date,
UITypes.DateTime,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
].includes(column.uidt)
) {
let now = dayjs(new Date());
const dateFormatFromMeta = column?.meta?.date_format;
if (dateFormatFromMeta && isDateMonthFormat(dateFormatFromMeta)) {
@ -661,8 +676,18 @@ const parseConditionV2 = async (
qb = qb.where(knex.raw('BINARY ?? = ?', [field, val]));
}
} else {
if (column.uidt === UITypes.DateTime) {
if (
[
UITypes.DateTime,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
].includes(column.uidt)
) {
if (qb.client.config.client === 'pg') {
// todo: enbale back if group by date required custom implementation
// if ((filter as any).groupby)
// qb = qb.where(knex.raw('??::timestamp = ?', [field, val]));
// else
qb = qb.where(knex.raw('??::date = ?', [field, val]));
} else {
qb = qb.where(knex.raw('DATE(??) = DATE(?)', [field, val]));
@ -945,9 +970,13 @@ const parseConditionV2 = async (
qb = qb.whereNull(customWhereClause || field);
if (
!isNumericCol(column.uidt) &&
![UITypes.Date, UITypes.DateTime, UITypes.Time].includes(
column.uidt,
)
![
UITypes.Date,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.DateTime,
UITypes.Time,
].includes(column.uidt)
) {
qb = qb.orWhere(field, '');
}
@ -963,9 +992,13 @@ const parseConditionV2 = async (
qb = qb.whereNotNull(customWhereClause || field);
if (
!isNumericCol(column.uidt) &&
![UITypes.Date, UITypes.DateTime, UITypes.Time].includes(
column.uidt,
)
![
UITypes.Date,
UITypes.DateTime,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.Time,
].includes(column.uidt)
) {
qb = qb.whereNot(field, '');
}

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

@ -20,6 +20,7 @@ import { CacheGetType, CacheScope } from '~/utils/globals';
import { convertDateFormatForConcat } from '~/helpers/formulaFnHelper';
import FormulaColumn from '~/models/FormulaColumn';
import { Base, BaseUser } from '~/models';
import { getRefColumnIfAlias } from '~/helpers';
const logger = new Logger('FormulaQueryBuilderv2');
@ -637,49 +638,60 @@ async function _formulaQueryBuilder(
};
};
break;
case UITypes.CreatedTime:
case UITypes.LastModifiedTime:
case UITypes.DateTime:
if (knex.clientType().startsWith('mysql')) {
aliasToColumn[col.id] = async (): Promise<any> => {
return {
// convert from DB timezone to UTC
builder: knex.raw(
`CONVERT_TZ(??, @@GLOBAL.time_zone, '+00:00')`,
[col.column_name],
),
{
const refCol = await getRefColumnIfAlias(col);
if (refCol.id in aliasToColumn) {
aliasToColumn[col.id] = aliasToColumn[refCol.id];
break;
}
if (knex.clientType().startsWith('mysql')) {
aliasToColumn[col.id] = async (): Promise<any> => {
return {
// convert from DB timezone to UTC
builder: knex.raw(
`CONVERT_TZ(??, @@GLOBAL.time_zone, '+00:00')`,
[refCol.column_name],
),
};
};
};
} else if (
knex.clientType() === 'pg' &&
col.dt !== 'timestamp with time zone' &&
col.dt !== 'timestamptz'
) {
aliasToColumn[col.id] = async (): Promise<any> => {
return {
// convert from DB timezone to UTC
builder: knex
.raw(
`?? AT TIME ZONE CURRENT_SETTING('timezone') AT TIME ZONE 'UTC'`,
[col.column_name],
)
.wrap('(', ')'),
} else if (
knex.clientType() === 'pg' &&
refCol.dt !== 'timestamp with time zone' &&
refCol.dt !== 'timestamptz'
) {
aliasToColumn[col.id] = async (): Promise<any> => {
return {
// convert from DB timezone to UTC
builder: knex
.raw(
`?? AT TIME ZONE CURRENT_SETTING('timezone') AT TIME ZONE 'UTC'`,
[refCol.column_name],
)
.wrap('(', ')'),
};
};
};
} else if (
knex.clientType() === 'mssql' &&
col.dt !== 'datetimeoffset'
) {
// convert from DB timezone to UTC
aliasToColumn[col.id] = async (): Promise<any> => {
return {
builder: knex.raw(
`CONVERT(DATETIMEOFFSET, ?? AT TIME ZONE 'UTC')`,
[col.column_name],
),
} else if (
knex.clientType() === 'mssql' &&
refCol.dt !== 'datetimeoffset'
) {
// convert from DB timezone to UTC
aliasToColumn[col.id] = async (): Promise<any> => {
return {
builder: knex.raw(
`CONVERT(DATETIMEOFFSET, ?? AT TIME ZONE 'UTC')`,
[refCol.column_name],
),
};
};
};
} else {
aliasToColumn[col.id] = () =>
Promise.resolve({ builder: col.column_name });
} else {
aliasToColumn[col.id] = () =>
Promise.resolve({ builder: refCol.column_name });
}
aliasToColumn[refCol.id] = aliasToColumn[col.id];
}
break;
case UITypes.User:

2
packages/nocodb/src/db/generateLookupSelectQuery.ts

@ -312,6 +312,8 @@ export default async function generateLookupSelectQuery({
}
break;
case UITypes.DateTime:
case UITypes.LastModifiedTime:
case UITypes.CreatedTime:
{
await baseModelSqlv2.selectObject({
qb: selectQb,

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

@ -8,6 +8,7 @@ import genRollupSelectv2 from '~/db/genRollupSelectv2';
import { sanitize } from '~/helpers/sqlSanitize';
import { Base, BaseUser, Sort } from '~/models';
import generateLookupSelectQuery from '~/db/generateLookupSelectQuery';
import { getRefColumnIfAlias } from '~/helpers';
export default async function sortV2(
baseModelSqlv2: BaseModelSqlv2,
@ -29,7 +30,7 @@ export default async function sortV2(
} else {
sort = new Sort(_sort);
}
const column = await sort.getColumn();
const column = await getRefColumnIfAlias(await sort.getColumn());
if (!column) {
if (throwErrorIfInvalid) {
NcError.unprocessableEntity(`Invalid field: ${sort.fk_column_id}`);

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

@ -1,7 +1,6 @@
import { customAlphabet } from 'nanoid';
import { getAvailableRollupForUiType, UITypes } from 'nocodb-sdk';
import { pluralize, singularize } from 'inflection';
import type { RollupColumn } from '~/models';
import type {
BoolType,
ColumnReqType,
@ -11,13 +10,14 @@ import type {
RollupColumnReqType,
TableType,
} from 'nocodb-sdk';
import type { RollupColumn } from '~/models';
import type LinkToAnotherRecordColumn from '~/models/LinkToAnotherRecordColumn';
import type LookupColumn from '~/models/LookupColumn';
import type Model from '~/models/Model';
import { GridViewColumn } from '~/models';
import validateParams from '~/helpers/validateParams';
import { getUniqueColumnAliasName } from '~/helpers/getUniqueName';
import Column from '~/models/Column';
import { GridViewColumn } from '~/models';
export const randomID = customAlphabet(
'1234567890abcdefghijklmnopqrstuvwxyz_',
@ -287,3 +287,23 @@ export const sanitizeColumnName = (name: string) => {
return columnName;
};
// if column is an alias column then return the original column
// for example CreatedTime is an alias column for CreatedTime system column
export const getRefColumnIfAlias = async (
column: Column,
columns?: Column[],
) => {
if (
!([UITypes.CreatedTime, UITypes.LastModifiedTime] as UITypes[]).includes(
column.uidt,
)
)
return column;
return (
(columns || (await Column.list({ fk_model_id: column.fk_model_id }))).find(
(c) => c.system && c.uidt === column.uidt,
) || column
);
};

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

@ -1,4 +1,10 @@
import { isSystemColumn, RelationTypes, UITypes, ViewTypes } from 'nocodb-sdk';
import {
isCreatedOrLastModifiedTimeCol,
isSystemColumn,
RelationTypes,
UITypes,
ViewTypes,
} from 'nocodb-sdk';
import type {
Column,
LinkToAnotherRecordColumn,
@ -149,13 +155,15 @@ const getAst = async ({
if (getHiddenColumn) {
isRequested =
!isSystemColumn(col) ||
col.column_name === 'created_at' ||
col.column_name === 'updated_at' ||
(isCreatedOrLastModifiedTimeCol(col) && col.system) ||
col.pk;
} else if (allowedCols && (!includePkByDefault || !col.pk)) {
isRequested =
allowedCols[col.id] &&
(!isSystemColumn(col) || view.show_system_fields || col.pv) &&
(!isSystemColumn(col) ||
(!view && isCreatedOrLastModifiedTimeCol(col)) ||
view.show_system_fields ||
col.pv) &&
(!fields?.length || fields.includes(col.title)) &&
value;
} else if (fields?.length) {

16
packages/nocodb/src/helpers/getColumnPropsFromUIDT.ts

@ -37,5 +37,19 @@ export default async function getColumnPropsFromUIDT(
newColumn.altered = column.altered || 2;
return { ...newColumn, ...column };
const finalColumnMeta = { ...newColumn, ...column };
if (
finalColumnMeta.uidt === UITypes.CreatedTime &&
!('column_name' in finalColumnMeta)
) {
finalColumnMeta.column_name = 'created_at';
} else if (
finalColumnMeta.uidt === UITypes.LastModifiedTime &&
!('column_name' in finalColumnMeta)
) {
finalColumnMeta.column_name = 'updated_at';
}
return finalColumnMeta;
}

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

@ -270,7 +270,7 @@ async function getSampleColumnValue(column: Column): Promise<any> {
return new Date();
}
break;
case UITypes.CreateTime:
case UITypes.CreatedTime:
{
return new Date();
}

12
packages/nocodb/src/models/Column.ts

@ -1170,13 +1170,17 @@ export default class Column<T = any> implements ColumnType {
// whenever a new request comes for that formula, it will be populated again
getFormulasReferredTheColumn({
column: updatedColumn,
columns: await Column.list({ fk_model_id: column.fk_model_id }),
columns: await Column.list({ fk_model_id: column.fk_model_id }, ncMeta),
})
.then(async (formulas) => {
for (const formula of formulas) {
await FormulaColumn.update(formula.id, {
parsed_tree: null,
});
await FormulaColumn.update(
formula.id,
{
parsed_tree: null,
},
ncMeta,
);
}
})
// ignore the error and continue, if formula is no longer valid it will be captured in the next run

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

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

4
packages/nocodb/src/modules/jobs/jobs/at-import/at-import.processor.ts

@ -364,7 +364,7 @@ export class AtImportProcessor {
// types email & url are marked as text
// types currency & percent, duration are marked as number
// types createTime & modifiedTime are marked as formula
// types CreatedTime & modifiedTime are marked as formula
switch (col.type) {
case 'text':
@ -1437,7 +1437,7 @@ export class AtImportProcessor {
break;
case UITypes.DateTime:
case UITypes.CreateTime:
case UITypes.CreatedTime:
case UITypes.LastModifiedTime:
rec[key] = dayjs(value).format('YYYY-MM-DD HH:mm');
break;

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

@ -766,6 +766,8 @@ export class ImportService {
a.uidt === UITypes.Rollup ||
a.uidt === UITypes.Formula ||
a.uidt === UITypes.QrCode ||
a.uidt === UITypes.CreatedTime ||
a.uidt === UITypes.LastModifiedTime ||
a.uidt === UITypes.Barcode,
),
);
@ -820,6 +822,7 @@ export class ImportService {
}
// create referenced columns
// sort the column sets to create the system columns first
for (const col of sortedReferencedColumnSet) {
const { colOptions, ...flatCol } = col;
if (col.uidt === UITypes.Lookup) {
@ -886,6 +889,27 @@ export class ImportService {
user: param.user,
});
for (const nColumn of freshModelData.columns) {
if (nColumn.title === col.title) {
idMap.set(col.id, nColumn.id);
break;
}
}
} else if (
col.uidt === UITypes.CreatedTime ||
col.uidt === UITypes.LastModifiedTime
) {
if (col.system) continue;
const freshModelData = await this.columnsService.columnAdd({
tableId: getIdOrExternalId(getParentIdentifier(col.id)),
column: withoutId({
...flatCol,
system: false,
}) as any,
req: param.req,
user: param.user,
});
for (const nColumn of freshModelData.columns) {
if (nColumn.title === col.title) {
idMap.set(col.id, nColumn.id);

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

@ -12096,7 +12096,7 @@
"Checkbox",
"Collaborator",
"Count",
"CreateTime",
"CreatedTime",
"Currency",
"Date",
"DateTime",
@ -15314,7 +15314,7 @@
"Checkbox",
"Collaborator",
"Count",
"CreateTime",
"CreatedTime",
"Currency",
"Date",
"DateTime",

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

@ -17313,7 +17313,7 @@
"Checkbox",
"Collaborator",
"Count",
"CreateTime",
"CreatedTime",
"Currency",
"Date",
"DateTime",
@ -20534,7 +20534,7 @@
"Checkbox",
"Collaborator",
"Count",
"CreateTime",
"CreatedTime",
"Currency",
"Date",
"DateTime",

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

@ -52,6 +52,10 @@ export default async (
$ref: `#/components/schemas/Attachment`,
};
break;
case UITypes.LastModifiedTime:
case UITypes.CreatedTime:
field.type = 'string';
break;
default:
field.virtual = false;
SwaggerTypes.setSwaggerType(c, field, dbType);

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

@ -45,6 +45,10 @@ export default async (
$ref: `#/components/schemas/Attachment`,
};
break;
case UITypes.LastModifiedTime:
case UITypes.CreatedTime:
field.type = 'string';
break;
default:
field.virtual = false;
SwaggerTypes.setSwaggerType(c, field, dbType);

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

@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import {
AppEvents,
isCreatedOrLastModifiedTimeCol,
isLinksOrLTAR,
isVirtualCol,
substituteColumnAliasWithIdInFormula,
@ -171,6 +172,7 @@ export class ColumnsService {
if (
!isVirtualCol(param.column) &&
!isCreatedOrLastModifiedTimeCol(param.column) &&
!(await Column.checkTitleAvailable({
column_name: param.column.column_name,
fk_model_id: column.fk_model_id,
@ -195,6 +197,7 @@ export class ColumnsService {
parsed_tree?: any;
};
if (
isCreatedOrLastModifiedTimeCol(column) ||
[
UITypes.Lookup,
UITypes.Rollup,
@ -292,6 +295,14 @@ export class ColumnsService {
NcError.notImplemented(
`Updating ${colBody.uidt} => ${colBody.uidt} is not implemented`,
);
} else if (
[UITypes.CreatedTime, UITypes.LastModifiedTime].includes(colBody.uidt)
) {
// allow updating of title only
await Column.update(param.columnId, {
...column,
title: colBody.title,
});
} else if (
[UITypes.SingleSelect, UITypes.MultiSelect].includes(colBody.uidt)
) {
@ -1673,6 +1684,84 @@ export class ColumnsService {
fk_model_id: table.id,
});
break;
case UITypes.CreatedTime:
case UITypes.LastModifiedTime:
{
let columnName: string;
const columns = await table.getColumns();
// check if column already exists, then just create a new column in meta
// else create a new column in meta and db
const existingColumn = columns.find(
(c) => c.uidt === colBody.uidt && c.system,
);
if (!existingColumn) {
columnName =
colBody.uidt === UITypes.CreatedTime
? 'created_at'
: 'updated_at';
// todo: check type as well
const dbColumn = columns.find((c) => c.column_name === columnName);
if (dbColumn) {
columnName = getUniqueColumnName(columns, columnName);
}
{
colBody = await getColumnPropsFromUIDT(colBody, source);
// remove default value for SQLite since it doesn't support default value as function when adding column
// only support default value as constant value
if (source.type === 'sqlite3') {
colBody.cdf = null;
}
// create column in db
const tableUpdateBody = {
...table,
tn: table.table_name,
originalColumns: table.columns.map((c) => ({
...c,
cn: c.column_name,
})),
columns: [
...table.columns.map((c) => ({ ...c, cn: c.column_name })),
{
...colBody,
cn: columnName,
altered: Altered.NEW_COLUMN,
},
],
};
const sqlMgr = await reuseOrSave('sqlMgr', reuse, async () =>
ProjectMgrv2.getSqlMgr({ id: source.base_id }),
);
await sqlMgr.sqlOpPlus(source, 'tableUpdate', tableUpdateBody);
}
const title = getUniqueColumnAliasName(
table.columns,
UITypes.CreatedTime ? 'CreatedAt' : 'UpdatedAt',
);
await Column.insert({
...colBody,
title,
system: 1,
fk_model_id: table.id,
column_name: columnName,
});
} else {
columnName = existingColumn.column_name;
}
await Column.insert({
...colBody,
fk_model_id: table.id,
column_name: null,
});
}
break;
default:
{
@ -1930,6 +2019,15 @@ export class ColumnsService {
const reuse = param.reuse || {};
const column = await Column.get({ colId: param.columnId }, ncMeta);
if (column.system) {
NcError.badRequest(
`The column '${
column.title || column.column_name
}' is a system column and cannot be deleted.`,
);
}
const table = await reuseOrSave('table', reuse, async () =>
Model.getWithInfo(
{
@ -2110,6 +2208,14 @@ export class ColumnsService {
}
/* falls through to default */
}
// on delete create time or last modified time, keep the column in table and delete the column from meta
case UITypes.CreatedTime:
case UITypes.LastModifiedTime:
{
await Column.delete(param.columnId, ncMeta);
}
break;
default: {
const tableUpdateBody = {
...table,
@ -2682,6 +2788,7 @@ export class ColumnsService {
await Column.update(column.id, colBody);
}
}
async columnsHash(tableId: string) {
const table = await Model.getWithInfo({
id: tableId,

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

@ -1,24 +1,26 @@
import { Injectable } from '@nestjs/common';
import DOMPurify from 'isomorphic-dompurify';
import {
AppEvents,
isCreatedOrLastModifiedTimeCol,
isLinksOrLTAR,
isVirtualCol,
ModelTypes,
ProjectRoles,
UITypes,
} from 'nocodb-sdk';
import { AppEvents } from 'nocodb-sdk';
import { MetaDiffsService } from './meta-diffs.service';
import { ColumnsService } from './columns.service';
import type { MetaService } from '~/meta/meta.service';
import type { LinkToAnotherRecordColumn, User, View } from '~/models';
import type {
ColumnType,
NormalColumnRequestType,
TableReqType,
UserType,
} from 'nocodb-sdk';
import type { MetaService } from '~/meta/meta.service';
import type { LinkToAnotherRecordColumn, User, View } from '~/models';
import type { NcRequest } from '~/interface/config';
import { Base, Column, Model, ModelRoleVisibility } from '~/models';
import { AppHooksService } from '~/services/app-hooks/app-hooks.service';
import ProjectMgrv2 from '~/db/sql-mgr/v2/ProjectMgrv2';
import { NcError } from '~/helpers/catchError';
@ -26,10 +28,13 @@ import getColumnPropsFromUIDT from '~/helpers/getColumnPropsFromUIDT';
import getColumnUiType from '~/helpers/getColumnUiType';
import getTableNameAlias, { getColumnNameAlias } from '~/helpers/getTableName';
import mapDefaultDisplayValue from '~/helpers/mapDefaultDisplayValue';
import { Base, Column, Model, ModelRoleVisibility } from '~/models';
import Noco from '~/Noco';
import NcConnectionMgrv2 from '~/utils/common/NcConnectionMgrv2';
import { sanitizeColumnName, validatePayload } from '~/helpers';
import {
getUniqueColumnAliasName,
getUniqueColumnName,
} from '~/helpers/getUniqueName';
@Injectable()
export class TablesService {
@ -379,6 +384,55 @@ export class TablesService {
source = base.sources.find((b) => b.id === param.sourceId);
}
// add CreatedBy and LastModifiedBy system columns if missing in request payload
{
for (const uidt of [UITypes.CreatedTime, UITypes.LastModifiedTime]) {
const col = tableCreatePayLoad.columns.find(
(c) => c.uidt === uidt,
) as ColumnType;
const colName = getUniqueColumnName(
tableCreatePayLoad.columns as any[],
uidt === UITypes.CreatedTime ? 'created_at' : 'updated_at',
);
const colAlias = getUniqueColumnAliasName(
tableCreatePayLoad.columns as any[],
uidt === UITypes.CreatedTime ? 'CreatedAt' : 'UpdatedAt',
);
if (!col || !col.system) {
tableCreatePayLoad.columns.push({
...(await getColumnPropsFromUIDT({ uidt } as any, source)),
column_name: colName,
cn: colName,
title: colAlias,
system: true,
});
} else {
// temporary fix for updating if user passed system columns with duplicate names
if (
tableCreatePayLoad.columns.some(
(c: ColumnType) =>
c.uidt !== uidt && c.column_name === col.column_name,
)
) {
Object.assign(col, {
column_name: colName,
cn: colName,
});
}
if (
tableCreatePayLoad.columns.some(
(c: ColumnType) => c.uidt !== uidt && c.title === col.title,
)
) {
Object.assign(col, {
title: colAlias,
});
}
}
}
}
if (
!tableCreatePayLoad.table_name ||
(base.prefix && base.prefix === tableCreatePayLoad.table_name)
@ -458,7 +512,10 @@ export class TablesService {
const uniqueColumnNameCount = {};
for (const column of param.table.columns) {
if (!isVirtualCol(column)) {
if (
!isVirtualCol(column) ||
(isCreatedOrLastModifiedTimeCol(column) && (column as any).system)
) {
const mxColumnLength = Column.getMaxColumnNameLength(sqlClientType);
// - 5 is a buffer for suffix
@ -489,11 +546,16 @@ export class TablesService {
}
tableCreatePayLoad.columns = await Promise.all(
param.table.columns?.map(async (c) => ({
...(await getColumnPropsFromUIDT(c as any, source)),
cn: c.column_name,
column_name: c.column_name,
})),
param.table.columns
// exclude alias columns from column list
?.filter((c) => {
return !isCreatedOrLastModifiedTimeCol(c) || (c as any).system;
})
.map(async (c) => ({
...(await getColumnPropsFromUIDT(c as any, source)),
cn: c.column_name,
column_name: c.column_name,
})),
);
await sqlMgr.sqlOpPlus(source, 'tableCreate', {
...tableCreatePayLoad,

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

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

194
packages/nocodb/src/version-upgrader/ncXcdbCreatedAndUpdatedTimeUpgrader.ts

@ -0,0 +1,194 @@
import { UITypes } from 'nocodb-sdk';
import { Logger } from '@nestjs/common';
import type { NcUpgraderCtx } from './NcUpgrader';
import type { MetaService } from '~/meta/meta.service';
import { MetaTable } from '~/utils/globals';
import { Column, Model, Source } from '~/models';
import {
getUniqueColumnAliasName,
getUniqueColumnName,
} from '~/helpers/getUniqueName';
import getColumnPropsFromUIDT from '~/helpers/getColumnPropsFromUIDT';
import ProjectMgrv2 from '~/db/sql-mgr/v2/ProjectMgrv2';
import { Altered } from '~/services/columns.service';
// An upgrader for upgrading created_at and updated_at columns
// to system column and convert to new uidt CreatedTime and LastModifiedTime
const logger = new Logger('ncXcdbCreatedAndUpdatedTimeUpgrader');
/* Enable if planning to remove trigger
async function deletePgTrigger({
column,
ncMeta,
model,
}: {
ncMeta: MetaService;
column: Column;
model: Model;
}) {
// delete pg trigger
const triggerFnName = `xc_au_${model.table_name}_${column.column_name}`;
const triggerName = `xc_trigger_${model.table_name}_${column.column_name}`;
await ncMeta.knex.raw(`DROP TRIGGER IF EXISTS ?? ON ??;`, [
triggerName,
model.table_name,
]);
await ncMeta.knex.raw(`DROP FUNCTION IF EXISTS ??()`, [triggerFnName]);
}
*/
async function upgradeModels({
ncMeta,
source,
}: {
ncMeta: MetaService;
source: any;
}) {
const models = await Model.list(
{
base_id: source.base_id,
source_id: source.id,
},
ncMeta,
);
await Promise.all(
models.map(async (model: any) => {
if (model.mm) return;
const columns = await model.getColumns(ncMeta);
const oldColumns = columns.map((c) => ({ ...c, cn: c.column_name }));
let isCreatedTimeExists = false;
let isLastModifiedTimeExists = false;
for (const column of columns) {
if (column.uidt !== UITypes.DateTime) continue;
if (column.column_name === 'created_at') {
isCreatedTimeExists = true;
await Column.update(
column.id,
{
...column,
uidt: UITypes.CreatedTime,
system: true,
},
ncMeta,
);
/* Enable if planning to remove trigger
if (source.type === 'pg') {
// delete pg trigger if exists
await deletePgTrigger({ column, ncMeta, model });
}*/
}
if (column.column_name === 'updated_at') {
isLastModifiedTimeExists = true;
await Column.update(
column.id,
{
...column,
uidt: UITypes.LastModifiedTime,
system: true,
cdf: '',
au: false,
},
ncMeta,
);
}
}
if (!isCreatedTimeExists || !isLastModifiedTimeExists) {
// create created_at and updated_at columns
const newColumns = [];
if (!isCreatedTimeExists) {
newColumns.push({
...(await getColumnPropsFromUIDT(
{
uidt: UITypes.CreatedTime,
column_name: getUniqueColumnName(columns, 'created_at'),
title: getUniqueColumnAliasName(columns, 'CreatedAt'),
},
source,
)),
cdf: null,
system: true,
altered: Altered.NEW_COLUMN,
});
}
if (!isLastModifiedTimeExists) {
newColumns.push({
...(await getColumnPropsFromUIDT(
{
uidt: UITypes.LastModifiedTime,
column_name: getUniqueColumnName(columns, 'updated_at'),
title: getUniqueColumnAliasName(columns, 'UpdatedAt'),
},
source,
)),
cdf: null,
system: true,
altered: Altered.NEW_COLUMN,
});
}
// update column in db
const tableUpdateBody = {
...model,
tn: model.table_name,
originalColumns: oldColumns,
columns: [...columns, ...newColumns].map((c) => ({
...c,
cn: c.column_name,
})),
};
const sqlMgr = ProjectMgrv2.getSqlMgr({ id: source.base_id }, ncMeta);
await sqlMgr.sqlOpPlus(source, 'tableUpdate', tableUpdateBody);
for (const newColumn of newColumns) {
await Column.insert(
{
...newColumn,
system: 1,
fk_model_id: model.id,
},
ncMeta,
);
}
}
logger.log(`Upgraded model ${model.name} from source ${source.name}`);
}),
);
}
// database to virtual relation and create an index for it
export default async function ({ ncMeta }: NcUpgraderCtx) {
// get all xcdb sources
const sources = await ncMeta.metaList2(null, null, MetaTable.BASES, {
xcCondition: {
_or: [
{
is_meta: {
eq: 1,
},
},
{
is_local: {
eq: 1,
},
},
],
},
});
// iterate and upgrade each base
for (const source of sources) {
logger.log(`Upgrading source ${source.name}`);
// update the meta props
await upgradeModels({ ncMeta, source: new Source(source) });
}
}

18
packages/nocodb/tests/unit/factory/column.ts

@ -27,7 +27,8 @@ const defaultColumns = function (context) {
title: 'CreatedAt',
dtxp: '',
dtxs: '',
uidt: 'DateTime',
uidt: 'CreatedTime',
system:true,
dt: isPg(context) ? 'timestamp without time zone' : undefined,
},
{
@ -40,7 +41,8 @@ const defaultColumns = function (context) {
title: 'UpdatedAt',
dtxp: '',
dtxs: '',
uidt: 'DateTime',
uidt: 'LastModifiedTime',
system:true,
dt: isPg(context) ? 'timestamp without time zone' : undefined,
},
];
@ -422,6 +424,17 @@ const updateColumn = async (
return updatedColumn;
};
const deleteColumn = async (
context,
{ table, column }: { column: Column; table: Model },
) => {
const res = await request(context.app)
.delete(`/api/v2/meta/columns/${column.id}`)
.set('xc-auth', context.token)
.send({})
.expect(200);
};
export {
customColumns,
defaultColumns,
@ -433,4 +446,5 @@ export {
createLtarColumn,
updateViewColumn,
updateColumn,
deleteColumn,
};

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

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

33
packages/nocodb/tests/unit/factory/table.ts

@ -20,13 +20,23 @@ const createTable = async (context, base, args = {}) => {
return table;
};
const getTable = async ({
base,
name,
}: {
base: Base;
name: string;
}) => {
const getTableByAPI = async (context, base) => {
const response = await request(context.app)
.get(`/api/v1/db/meta/projects/${base.id}/tables`)
.set('xc-auth', context.token);
return response.body;
};
const getColumnsByAPI = async (context, base, table) => {
const response = await request(context.app)
.get(`/api/v2/meta/tables/${table.id}`)
.set('xc-auth', context.token);
return response.body;
};
const getTable = async ({ base, name }: { base: Base; name: string }) => {
const sources = await base.getSources();
return await Model.getByIdOrName({
base_id: base.id,
@ -57,4 +67,11 @@ const updateTable = async (
return response.body;
};
export { createTable, getTable, getAllTables, updateTable };
export {
createTable,
getTable,
getAllTables,
updateTable,
getTableByAPI,
getColumnsByAPI,
};

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

@ -54,27 +54,15 @@ function baseModelSqlTests() {
);
const insertedRow = (await baseModelSql.list())[0];
if (isPg(context)) {
inputData.CreatedAt = new Date(inputData.CreatedAt).toISOString();
inputData.UpdatedAt = new Date(inputData.UpdatedAt).toISOString();
insertedRow.CreatedAt = new Date(insertedRow.CreatedAt).toISOString();
insertedRow.UpdatedAt = new Date(insertedRow.UpdatedAt).toISOString();
response.CreatedAt = new Date(response.CreatedAt).toISOString();
response.UpdatedAt = new Date(response.UpdatedAt).toISOString();
} else if (isSqlite(context)) {
// append +00:00 to the date string
inputData.CreatedAt = `${inputData.CreatedAt}+00:00`;
inputData.UpdatedAt = `${inputData.UpdatedAt}+00:00`;
}
inputData.CreatedAt = response.CreatedAt;
inputData.UpdatedAt = response.UpdatedAt;
expect(insertedRow).to.include(inputData);
expect(insertedRow).to.include(response);
const rowInsertedAudit = (
await Audit.baseAuditList(base.id, {})
).find((audit) => audit.op_sub_type === 'INSERT');
const rowInsertedAudit = (await Audit.baseAuditList(base.id, {})).find(
(audit) => audit.op_sub_type === 'INSERT',
);
expect(rowInsertedAudit).to.include({
user: 'test@example.com',
ip: '::ffff:192.0.0.1',
@ -110,19 +98,15 @@ function baseModelSqlTests() {
bulkData.forEach((inputData: any, index) => {
if (isPg(context)) {
inputData.CreatedAt = new Date(inputData.CreatedAt).toISOString();
inputData.UpdatedAt = new Date(inputData.UpdatedAt).toISOString();
} else if (isSqlite(context)) {
// append +00:00 to the date string
inputData.CreatedAt = `${inputData.CreatedAt}+00:00`;
inputData.UpdatedAt = `${inputData.UpdatedAt}+00:00`;
inputData.CreatedAt = insertedRows[index].CreatedAt;
inputData.UpdatedAt = insertedRows[index].UpdatedAt;
}
expect(insertedRows[index]).to.include(inputData);
});
const rowBulkInsertedAudit = (
await Audit.baseAuditList(base.id, {})
).find((audit) => audit.op_sub_type === 'BULK_INSERT');
const rowBulkInsertedAudit = (await Audit.baseAuditList(base.id, {})).find(
(audit) => audit.op_sub_type === 'BULK_INSERT',
);
expect(rowBulkInsertedAudit).to.include({
user: 'test@example.com',
ip: '::ffff:192.0.0.1',
@ -188,7 +172,10 @@ function baseModelSqlTests() {
const insertedRows: any[] = await baseModelSql.list();
await baseModelSql.bulkUpdate(
insertedRows.map((row) => ({ ...row, Title: `new-${row['Title']}` })),
insertedRows.map(({ CreatedAt: _, UpdatedAt: __, ...row }) => ({
...row,
Title: `new-${row['Title']}`,
})),
{ cookie: request },
);
@ -197,9 +184,9 @@ function baseModelSqlTests() {
updatedRows.forEach((row, index) => {
expect(row['Title']).to.equal(`new-test-${index}`);
});
const rowBulkUpdateAudit = (
await Audit.baseAuditList(base.id, {})
).find((audit) => audit.op_sub_type === 'BULK_UPDATE');
const rowBulkUpdateAudit = (await Audit.baseAuditList(base.id, {})).find(
(audit) => audit.op_sub_type === 'BULK_UPDATE',
);
expect(rowBulkUpdateAudit).to.include({
user: 'test@example.com',
ip: '::ffff:192.0.0.1',
@ -248,9 +235,9 @@ function baseModelSqlTests() {
updatedRows.forEach((row) => {
if (row.id < 5) expect(row['Title']).to.equal('new-1');
});
const rowBulkUpdateAudit = (
await Audit.baseAuditList(base.id, {})
).find((audit) => audit.op_sub_type === 'BULK_UPDATE');
const rowBulkUpdateAudit = (await Audit.baseAuditList(base.id, {})).find(
(audit) => audit.op_sub_type === 'BULK_UPDATE',
);
expect(rowBulkUpdateAudit).to.include({
user: 'test@example.com',
ip: '::ffff:192.0.0.1',
@ -327,9 +314,9 @@ function baseModelSqlTests() {
expect(remainingRows).to.length(6);
const rowBulkDeleteAudit = (
await Audit.baseAuditList(base.id, {})
).find((audit) => audit.op_sub_type === 'BULK_DELETE');
const rowBulkDeleteAudit = (await Audit.baseAuditList(base.id, {})).find(
(audit) => audit.op_sub_type === 'BULK_DELETE',
);
expect(rowBulkDeleteAudit).to.include({
user: 'test@example.com',
@ -376,9 +363,9 @@ function baseModelSqlTests() {
const remainingRows = await baseModelSql.list();
expect(remainingRows).to.length(6);
const rowBulkDeleteAudit = (
await Audit.baseAuditList(base.id, {})
).find((audit) => audit.op_sub_type === 'BULK_DELETE');
const rowBulkDeleteAudit = (await Audit.baseAuditList(base.id, {})).find(
(audit) => audit.op_sub_type === 'BULK_DELETE',
);
expect(rowBulkDeleteAudit).to.include({
user: 'test@example.com',
ip: '::ffff:192.0.0.1',

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

@ -5,8 +5,18 @@ import { UITypes } from 'nocodb-sdk';
import { expect } from 'chai';
import init from '../../init';
import { createProject, createSakilaProject } from '../../factory/base';
import { createColumn, createQrCodeColumn } from '../../factory/column';
import { getTable } from '../../factory/table';
import {
createColumn,
createQrCodeColumn,
deleteColumn,
} from '../../factory/column';
import {
createTable,
getColumnsByAPI,
getTable,
getTableByAPI,
} from '../../factory/table';
import { createBulkRows, listRow, rowMixedValue } from '../../factory/row';
import type Model from '../../../../src/models/Model';
import type Base from '~/models/Base';
import type Column from '../../../../src/models/Column';
@ -27,34 +37,34 @@ function columnTypeSpecificTests() {
const qrValueReferenceColumnTitle = 'Qr Value Column';
const qrCodeReferenceColumnTitle = 'Qr Code Column';
beforeEach(async function () {
console.time('#### columnTypeSpecificTests');
context = await init(true);
describe('Qr Code Column', () => {
beforeEach(async function () {
console.time('#### columnTypeSpecificTests');
context = await init(true);
sakilaProject = await createSakilaProject(context);
base = await createProject(context);
sakilaProject = await createSakilaProject(context);
base = await createProject(context);
customerTable = await getTable({
base: sakilaProject,
name: 'customer',
});
customerTable = await getTable({
base: sakilaProject,
name: 'customer',
});
qrValueReferenceColumn = await createColumn(context, customerTable, {
title: qrValueReferenceColumnTitle,
uidt: UITypes.SingleLineText,
table_name: customerTable.table_name,
column_name: qrValueReferenceColumnTitle,
});
qrValueReferenceColumn = await createColumn(context, customerTable, {
title: qrValueReferenceColumnTitle,
uidt: UITypes.SingleLineText,
table_name: customerTable.table_name,
column_name: qrValueReferenceColumnTitle,
});
await createQrCodeColumn(context, {
title: qrCodeReferenceColumnTitle,
table: customerTable,
referencedQrValueTableColumnTitle: qrValueReferenceColumnTitle,
await createQrCodeColumn(context, {
title: qrCodeReferenceColumnTitle,
table: customerTable,
referencedQrValueTableColumnTitle: qrValueReferenceColumnTitle,
});
console.timeEnd('#### columnTypeSpecificTests');
});
console.timeEnd('#### columnTypeSpecificTests');
});
describe('Qr Code Column', () => {
it('delivers the same cell values as the referenced column', async () => {
const resp = await request(context.app)
.get(`/api/v1/db/data/noco/${sakilaProject.id}/${customerTable.id}`)
@ -93,6 +103,259 @@ function columnTypeSpecificTests() {
).to.eq(false);
});
});
// Created-at, Last-modified-at field
let table: Model;
let columns: any[];
let unfilteredRecords: any[] = [];
describe('CreatedAt, LastModifiedAt Field', () => {
beforeEach(async function () {
context = await init();
base = await createProject(context);
table = await createTable(context, base, {
table_name: 'dateBased',
title: 'dateBased',
columns: [
{
column_name: 'Id',
title: 'Id',
uidt: UITypes.ID,
},
{
column_name: 'DateField',
title: 'DateField',
uidt: UITypes.Date,
},
],
});
columns = await table.getColumns();
const rowAttributes = [];
for (let i = 0; i < 100; i++) {
const row = {
DateField: rowMixedValue(columns[1], i),
};
rowAttributes.push(row);
}
await createBulkRows(context, {
base,
table,
values: rowAttributes,
});
unfilteredRecords = await listRow({ base, table });
// verify length of unfiltered records to be 800
expect(unfilteredRecords.length).to.equal(100);
});
describe('Basic verification', async () => {
it('New table: verify system fields are added by default', async () => {
// Id, Date, CreatedAt, LastModifiedAt
expect(columns.length).to.equal(4);
expect(columns[2].title).to.equal('CreatedAt');
expect(columns[2].uidt).to.equal(UITypes.CreatedTime);
expect(!!columns[2].system).to.equal(true);
expect(columns[3].title).to.equal('UpdatedAt');
expect(columns[3].uidt).to.equal(UITypes.LastModifiedTime);
expect(!!columns[3].system).to.equal(true);
});
it('New table: should not be able to delete system fields', async () => {
await request(context.app)
.delete(`/api/v2/meta/columns/${columns[2].id}`)
.set('xc-auth', context.token)
.send({})
.expect(400);
await request(context.app)
.delete(`/api/v2/meta/columns/${columns[3].id}`)
.set('xc-auth', context.token)
.send({})
.expect(400);
// try to delete system fields (using v1 api)
await request(context.app)
.delete(`/api/v1/db/meta/columns/${columns[2].id}`)
.set('xc-auth', context.token)
.send({})
.expect(400);
await request(context.app)
.delete(`/api/v1/db/meta/columns/${columns[3].id}`)
.set('xc-auth', context.token)
.send({})
.expect(400);
});
it('New record: verify created-at is filled with current dateTime, last-modified-at is null', async () => {
// get current date time
const currentDateTime = new Date();
const storedDateTime = new Date(unfilteredRecords[0].CreatedAt);
// calculate difference between current date time and stored date time
const difference = currentDateTime.getTime() - storedDateTime.getTime();
expect(difference).to.be.lessThan(2000);
expect(unfilteredRecords[0].CreatedAt).to.not.equal(null);
expect(unfilteredRecords[0].UpdatedAt).to.equal(null);
});
it('Modify record: verify last-modified-at is updated', async () => {
// get current date time
const currentDateTime = new Date();
const d1 = new Date();
d1.setDate(d1.getDate() - 200);
// sleep for 3 seconds
await new Promise((resolve) => setTimeout(resolve, 3000));
// update record
await request(context.app)
.patch(`/api/v2/tables/${table.id}/records`)
.set('xc-auth', context.token)
.send([
{
Id: unfilteredRecords[0].Id,
DateField: d1.toISOString().slice(0, 10),
},
])
.expect(200);
// get updated record
let updatedRecord = await listRow({
base,
table,
options: { limit: 1 },
});
// get stored date time
const storedDateTime1 = new Date(updatedRecord[0].UpdatedAt);
// calculate difference between current date time and stored date time
let difference = storedDateTime1.getTime() - currentDateTime.getTime();
expect(difference).to.be.greaterThan(1500);
expect(updatedRecord[0].UpdatedAt).to.not.equal(null);
// Update again & confirm last modified time is updated
// sleep for 3 seconds
await new Promise((resolve) => setTimeout(resolve, 3100));
// update record
d1.setDate(d1.getDate() - 100);
await request(context.app)
.patch(`/api/v2/tables/${table.id}/records`)
.set('xc-auth', context.token)
.send([
{
Id: unfilteredRecords[0].Id,
DateField: d1.toISOString().slice(0, 10),
},
])
.expect(200);
// get updated record
updatedRecord = await listRow({
base,
table,
options: { limit: 1 },
});
// get stored date time
const storedDateTime2 = new Date(updatedRecord[0].UpdatedAt);
// calculate difference between current date time and stored date time
difference = storedDateTime2.getTime() - storedDateTime1.getTime();
expect(difference).to.be.greaterThan(1500);
});
it('Modify record: verify that system fields are RO', async () => {
const d1 = new Date();
d1.setDate(d1.getDate() - 200);
// update record
await request(context.app)
.patch(`/api/v2/tables/${table.id}/records`)
.set('xc-auth', context.token)
.send([
{
Id: unfilteredRecords[0].Id,
CreatedAt: d1.toISOString().slice(0, 10),
UpdatedAt: d1.toISOString().slice(0, 10),
},
])
.expect(400);
});
it('Add field: verify contents of both fields are same & new field is RO', async () => {
// add another CreatedTime field
await createColumn(context, table, {
title: 'CreatedAt2',
uidt: UITypes.CreatedTime,
column_name: 'CreatedAt2',
});
// 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[4].title).to.equal('CreatedAt2');
expect(columns.columns[4].uidt).to.equal(UITypes.CreatedTime);
expect(!!columns.columns[4].system).to.equal(false);
expect(records[0].CreatedAt).to.equal(records[0].CreatedAt2);
const d1 = new Date();
d1.setDate(d1.getDate() - 200);
// 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,
CreatedAt2: d1.toISOString().slice(0, 10),
},
])
.expect(400);
});
it('Delete & add field: verify contents of both fields are same', async () => {
// add another CreatedTime field
await createColumn(context, table, {
title: 'CreatedAt2',
uidt: UITypes.CreatedTime,
column_name: 'CreatedAt2',
});
// get all columns
let columns = await getColumnsByAPI(context, base, table);
// delete the field
await deleteColumn(context, { table, column: columns.columns[4] });
// create column again
await createColumn(context, table, {
title: 'CreatedAt2',
uidt: UITypes.CreatedTime,
column_name: 'CreatedAt2',
});
// 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[4].title).to.equal('CreatedAt2');
expect(columns.columns[4].uidt).to.equal(UITypes.CreatedTime);
expect(!!columns.columns[4].system).to.equal(false);
expect(records[0].CreatedAt).to.equal(records[0].CreatedAt2);
});
});
});
}
export default function () {

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

@ -82,9 +82,15 @@
*/
import 'mocha';
import {UITypes, ViewTypes, WorkspaceUserRoles} from 'nocodb-sdk';
import {
isCreatedOrLastModifiedTimeCol,
UITypes,
ViewTypes,
WorkspaceUserRoles,
} from 'nocodb-sdk';
import { expect } from 'chai';
import request from 'supertest';
import validator from 'validator';
import init from '../../init';
import { createProject, createSakilaProject } from '../../factory/base';
import { createTable, getTable } from '../../factory/table';
@ -102,6 +108,7 @@ import { defaultUserArgs } from '../../factory/user';
import type { ColumnType } from 'nocodb-sdk';
import type Base from '~/models/Base';
import type Model from '../../../../src/models/Model';
import isCreditCard = validator.isCreditCard;
const debugMode = false;
@ -157,10 +164,12 @@ async function ncAxiosPost({
url = `/api/v2/tables/${table.id}/records`,
body = {},
status = 200,
}: { url?: string; body?: any; status?: number } = {}) {
query = {},
}: { url?: string; body?: any; status?: number; query?: any } = {}) {
const response = await request(context.app)
.post(url)
.set('xc-auth', context.token)
.query(query)
.send(body);
expect(response.status).to.equal(status);
return response;
@ -667,7 +676,12 @@ function textBased() {
// verify sorted order
// Would contain all 'Afghanistan' as we have 31 records for it
expect(verifyColumnsInRsp(rsp.body.list[0], columns)).to.equal(true);
expect(
verifyColumnsInRsp(
rsp.body.list[0],
columns.filter((c) => !isCreatedOrLastModifiedTimeCol(c) || !c.system),
),
).to.equal(true);
const filteredArray = rsp.body.list.map((r) => r.SingleLineText);
expect(filteredArray).to.deep.equal(filteredArray.fill('Afghanistan'));
@ -683,7 +697,11 @@ function textBased() {
viewId: gridView.id,
},
});
const displayColumns = columns.filter((c) => c.title !== 'SingleLineText');
const displayColumns = columns.filter(
(c) =>
c.title !== 'SingleLineText' &&
(!isCreatedOrLastModifiedTimeCol(c) || !c.system),
);
expect(verifyColumnsInRsp(rsp.body.list[0], displayColumns)).to.equal(true);
});
@ -723,10 +741,15 @@ function textBased() {
});
// fetch records from view
const rsp = await ncAxiosGet({ query: { viewId: gridView.id } });
const rsp = await ncAxiosGet({
query: { viewId: gridView.id },
});
expect(rsp.body.pageInfo.totalRows).to.equal(61);
const displayColumns = columns.filter(
(c) => c.title !== 'MultiLineText' && c.title !== 'Email',
(c) =>
c.title !== 'MultiLineText' &&
c.title !== 'Email' &&
!isCreatedOrLastModifiedTimeCol(c),
);
expect(verifyColumnsInRsp(rsp.body.list[0], displayColumns)).to.equal(true);
return gridView;
@ -743,7 +766,10 @@ function textBased() {
},
});
const displayColumns = columns.filter(
(c) => c.title !== 'MultiLineText' && c.title !== 'Email',
(c) =>
c.title !== 'MultiLineText' &&
c.title !== 'Email' &&
!isCreatedOrLastModifiedTimeCol(c),
);
expect(rsp.body.pageInfo.totalRows).to.equal(61);
expect(verifyColumnsInRsp(rsp.body.list[0], displayColumns)).to.equal(true);
@ -762,7 +788,10 @@ function textBased() {
},
});
const displayColumns = columns.filter(
(c) => c.title !== 'MultiLineText' && c.title !== 'Email',
(c) =>
c.title !== 'MultiLineText' &&
c.title !== 'Email' &&
!isCreatedOrLastModifiedTimeCol(c),
);
expect(rsp.body.pageInfo.totalRows).to.equal(7);
expect(verifyColumnsInRsp(rsp.body.list[0], displayColumns)).to.equal(true);
@ -989,6 +1018,9 @@ function textBased() {
it('Update: partial', async function () {
const recordBeforeUpdate = await ncAxiosGet({
url: `/api/v2/tables/${table.id}/records/1`,
query: {
fields: 'Id,SingleLineText,MultiLineText',
},
});
const rsp = await ncAxiosPatch({
@ -1004,6 +1036,9 @@ function textBased() {
const recordAfterUpdate = await ncAxiosGet({
url: `/api/v2/tables/${table.id}/records/1`,
query: {
fields: 'Id,SingleLineText,MultiLineText',
},
});
expect(recordAfterUpdate.body).to.deep.equal({
...recordBeforeUpdate.body,
@ -1233,6 +1268,7 @@ function numberBased() {
let rsp = await ncAxiosGet({
query: {
limit: 10,
fields: 'Id,Number,Decimal,Currency,Percent,Duration,Rating',
},
});
const pageInfo = {
@ -1266,6 +1302,9 @@ function numberBased() {
// read record with Id 401
rsp = await ncAxiosGet({
url: `/api/v2/tables/${table.id}/records/401`,
query: {
fields: 'Id,Number,Decimal,Currency,Percent,Duration,Rating',
},
});
expect(rsp.body).to.deep.equal({ ...records[0], Id: 401 });
@ -1311,6 +1350,7 @@ function numberBased() {
query: {
limit: 4,
offset: 400,
fields: 'Id,Number,Decimal,Currency,Percent,Duration,Rating',
},
});
@ -1424,6 +1464,7 @@ function selectBased() {
let rsp = await ncAxiosGet({
query: {
limit: 10,
fields: 'Id,SingleSelect,MultiSelect',
},
});
const pageInfo = {
@ -1457,6 +1498,9 @@ function selectBased() {
// read record with Id 401
rsp = await ncAxiosGet({
url: `/api/v2/tables/${table.id}/records/401`,
query: {
fields: 'Id,SingleSelect,MultiSelect',
},
});
expect(rsp.body).to.deep.equal({ Id: 401, ...records[0] });
@ -1497,6 +1541,7 @@ function selectBased() {
query: {
limit: 4,
offset: 400,
fields: 'Id,SingleSelect,MultiSelect',
},
});
expect(rsp.body.list).to.deep.equal(updatedRecords);
@ -1579,7 +1624,12 @@ function dateBased() {
// insert 10 records
// remove Id's from record array
records.forEach((r) => delete r.Id);
records.forEach((r) => {
delete r.Id;
delete r.CreatedAt;
delete r.UpdatedAt;
delete r.Id;
});
rsp = await ncAxiosPost({
body: records,
});
@ -1596,8 +1646,15 @@ function dateBased() {
// read record with Id 801
rsp = await ncAxiosGet({
url: `/api/v2/tables/${table.id}/records/801`,
query: {
fields: 'Id,Date,DateTime',
},
});
expect(rsp.body).to.deep.equal({
Id: 801,
Date: records[0].Date,
DateTime: records[0].DateTime,
});
expect(rsp.body).to.deep.equal({ Id: 801, ...records[0] });
///////////////////////////////////////////////////////////////////////////
@ -1636,6 +1693,7 @@ function dateBased() {
query: {
limit: 4,
offset: 800,
fields: 'Id,Date,DateTime',
},
});
expect(rsp.body.list).to.deep.equal(updatedRecords);
@ -2024,6 +2082,7 @@ function linkBased() {
// Links
expect(rsp.body.list.length).to.equal(20);
rsp.body.list.sort((a, b) => a.Id - b.Id);
for (let i = 1; i <= 20; i++) {
expect(rsp.body.list[i - 1]).to.deep.equal({
Id: i,
@ -2040,7 +2099,7 @@ function linkBased() {
},
});
expect(rsp.body.list.length).to.equal(1);
expect(rsp.body.list[0]).to.deep.equal({
expect(rsp.body.list.sort((a, b) => a.Id - b.Id)[0]).to.deep.equal({
Id: 1,
Film: `Film 1`,
});
@ -2054,6 +2113,7 @@ function linkBased() {
},
});
expect(rsp.body.list.length).to.equal(20);
rsp.body.list.sort((a, b) => a.Id - b.Id);
for (let i = 1; i <= 20; i++) {
expect(rsp.body.list[i - 1]).to.deep.equal({
Id: i,
@ -2081,6 +2141,7 @@ function linkBased() {
},
});
expect(rsp.body.list.length).to.equal(25);
rsp.body.list.sort((a, b) => a.Id - b.Id);
// paginated response, limit to 25
for (let i = 1; i <= 25; i++) {
expect(rsp.body.list[i - 1]).to.deep.equal({
@ -2099,6 +2160,7 @@ function linkBased() {
},
});
expect(rsp.body.list.length).to.equal(1);
rsp.body.list.sort((a, b) => a.Id - b.Id);
expect(rsp.body.list[0]).to.deep.equal({
Id: 1,
Actor: `Actor 1`,
@ -2141,6 +2203,7 @@ function linkBased() {
},
});
expect(rsp.body.list.length).to.equal(15);
rsp.body.list.sort((a, b) => a.Id - b.Id);
for (let i = 2; i <= 30; i += 2) {
expect(rsp.body.list[i / 2 - 1]).to.deep.equal({
Id: i,
@ -2159,6 +2222,7 @@ function linkBased() {
});
if (i % 2 === 0) {
expect(rsp.body.list.length).to.equal(1);
rsp.body.list.sort((a, b) => a.Id - b.Id);
expect(rsp.body.list[0]).to.deep.equal({
Id: 1,
Actor: `Actor 1`,
@ -2799,7 +2863,7 @@ function userFieldBased() {
// invite users to workspace
if (process.env.EE === 'true') {
let rsp = await request(context.app)
const rsp = await request(context.app)
.post(`/api/v1/workspaces/${context.fk_workspace_id}/invitations`)
.set('xc-auth', context.token)
.send({ email, roles: WorkspaceUserRoles.VIEWER });

Loading…
Cancel
Save