mirror of https://github.com/nocodb/nocodb
Browse Source
* feat: extensions Signed-off-by: mertmit <mertmit99@gmail.com> * chore: sync Signed-off-by: mertmit <mertmit99@gmail.com> --------- Signed-off-by: mertmit <mertmit99@gmail.com>pull/8326/head
Mert E
8 months ago
committed by
GitHub
59 changed files with 3498 additions and 99 deletions
@ -0,0 +1,110 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import { useVModel } from '#imports' |
||||||
|
|
||||||
|
interface Prop { |
||||||
|
modelValue: boolean |
||||||
|
extensionId: string |
||||||
|
from: 'market' | 'extension' |
||||||
|
} |
||||||
|
|
||||||
|
const props = defineProps<Prop>() |
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']) |
||||||
|
|
||||||
|
const vModel = useVModel(props, 'modelValue', emit) |
||||||
|
|
||||||
|
const { availableExtensions, addExtension, getExtensionIcon, isMarketVisible } = useExtensions() |
||||||
|
|
||||||
|
const onBack = () => { |
||||||
|
vModel.value = false |
||||||
|
isMarketVisible.value = true |
||||||
|
} |
||||||
|
|
||||||
|
const onAddExtension = (ext: any) => { |
||||||
|
addExtension(ext) |
||||||
|
vModel.value = false |
||||||
|
} |
||||||
|
|
||||||
|
const activeExtension = computed(() => { |
||||||
|
return availableExtensions.value.find((ext) => ext.id === props.extensionId) |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<NcModal |
||||||
|
v-model:visible="vModel" |
||||||
|
:body-style="{ 'max-height': '864px', 'height': '85vh' }" |
||||||
|
:class="{ active: vModel }" |
||||||
|
:closable="from === 'extension'" |
||||||
|
:footer="null" |
||||||
|
:width="1280" |
||||||
|
size="medium" |
||||||
|
wrap-class-name="nc-modal-extension-market" |
||||||
|
> |
||||||
|
<div v-if="activeExtension" class="flex flex-col w-full h-full"> |
||||||
|
<div v-if="from === 'market'" class="h-[40px] flex items-start"> |
||||||
|
<div class="flex items-center gap-2 pr-2 pb-2 cursor-pointer hover:text-primary" @click="onBack"> |
||||||
|
<GeneralIcon icon="ncArrowLeft" /> |
||||||
|
<span>Back</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div v-else class="h-[40px]"></div> |
||||||
|
<div class="extension-details"> |
||||||
|
<div class="extension-details-left"> |
||||||
|
<div class="flex"> |
||||||
|
<img :src="getExtensionIcon(activeExtension.iconUrl)" alt="icon" class="h-[90px]" /> |
||||||
|
<div class="flex flex-col p-4"> |
||||||
|
<div class="font-weight-700 text-2xl">{{ activeExtension.title }}</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="p-4"> |
||||||
|
<div class="whitespace-pre-line">{{ activeExtension.description }}</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="extension-details-right"> |
||||||
|
<NcButton class="w-full" @click="onAddExtension(activeExtension)"> |
||||||
|
<div class="flex items-center justify-center">Add Extension</div> |
||||||
|
</NcButton> |
||||||
|
<div class="flex flex-col gap-1"> |
||||||
|
<div class="text-md font-weight-600">Version</div> |
||||||
|
<div>{{ activeExtension.version }}</div> |
||||||
|
</div> |
||||||
|
<div class="flex flex-col gap-1"> |
||||||
|
<div v-if="activeExtension.publisherName" class="text-md font-weight-600">Publisher</div> |
||||||
|
<div>{{ activeExtension.publisherName }}</div> |
||||||
|
</div> |
||||||
|
<div v-if="activeExtension.publisherEmail" class="flex flex-col gap-1"> |
||||||
|
<div class="text-md font-weight-600">Publisher Email</div> |
||||||
|
<div> |
||||||
|
<a :href="`mailto:${activeExtension.publisherEmail}`" target="_blank" rel="noopener noreferrer"> |
||||||
|
{{ activeExtension.publisherEmail }} |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div v-if="activeExtension.publisherUrl" class="flex flex-col gap-1"> |
||||||
|
<div class="text-md font-weight-600">Publisher Website</div> |
||||||
|
<div> |
||||||
|
<a :href="activeExtension.publisherUrl" target="_blank" rel="noopener noreferrer"> |
||||||
|
{{ activeExtension.publisherUrl }} |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</NcModal> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.extension-details { |
||||||
|
@apply flex w-full h-full; |
||||||
|
|
||||||
|
.extension-details-left { |
||||||
|
@apply flex flex-col w-3/4 p-2; |
||||||
|
} |
||||||
|
|
||||||
|
.extension-details-right { |
||||||
|
@apply w-1/4 p-2 flex flex-col gap-4; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,231 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
interface Prop { |
||||||
|
extensionId: string |
||||||
|
error?: any |
||||||
|
} |
||||||
|
|
||||||
|
const { extensionId, error } = defineProps<Prop>() |
||||||
|
|
||||||
|
const { extensionList, extensionsLoaded, availableExtensions, getExtensionIcon, duplicateExtension, showExtensionDetails } = |
||||||
|
useExtensions() |
||||||
|
|
||||||
|
const activeError = ref(error) |
||||||
|
|
||||||
|
const extensionModalRef = ref<HTMLElement>() |
||||||
|
|
||||||
|
const extension = computed(() => { |
||||||
|
const ext = extensionList.value.find((ext) => ext.id === extensionId) |
||||||
|
if (!ext) { |
||||||
|
throw new Error('Extension not found') |
||||||
|
} |
||||||
|
return ext |
||||||
|
}) |
||||||
|
|
||||||
|
const titleInput = ref<HTMLInputElement | null>(null) |
||||||
|
|
||||||
|
const titleEditMode = ref<boolean>(false) |
||||||
|
|
||||||
|
const tempTitle = ref<string>(extension.value.title) |
||||||
|
|
||||||
|
const enableEditMode = () => { |
||||||
|
titleEditMode.value = true |
||||||
|
tempTitle.value = extension.value.title |
||||||
|
nextTick(() => { |
||||||
|
titleInput.value?.focus() |
||||||
|
titleInput.value?.select() |
||||||
|
titleInput.value?.scrollIntoView() |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const updateExtensionTitle = async () => { |
||||||
|
await extension.value.setTitle(tempTitle.value) |
||||||
|
titleEditMode.value = false |
||||||
|
} |
||||||
|
|
||||||
|
const { fullscreen, collapsed } = useProvideExtensionHelper(extension) |
||||||
|
|
||||||
|
const component = ref<any>(null) |
||||||
|
|
||||||
|
const extensionManifest = ref<any>(null) |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
until(extensionsLoaded) |
||||||
|
.toMatch((v) => v) |
||||||
|
.then(() => { |
||||||
|
extensionManifest.value = availableExtensions.value.find((ext) => ext.id === extension.value.extensionId) |
||||||
|
|
||||||
|
if (!extensionManifest) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
import(`../../extensions/${extensionManifest.value.entry}/index.vue`).then((mod) => { |
||||||
|
component.value = markRaw(mod.default) |
||||||
|
}) |
||||||
|
}) |
||||||
|
.catch((err) => { |
||||||
|
if (!extensionManifest.value) { |
||||||
|
activeError.value = 'There was an error loading the extension' |
||||||
|
return |
||||||
|
} |
||||||
|
activeError.value = err |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
// close fullscreen on escape key press |
||||||
|
useEventListener('keydown', (e) => { |
||||||
|
if (e.key === 'Escape') { |
||||||
|
fullscreen.value = false |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// close fullscreen on clicking extensionModalRef directly |
||||||
|
const closeFullscreen = (e: MouseEvent) => { |
||||||
|
if (e.target === extensionModalRef.value) { |
||||||
|
fullscreen.value = false |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="w-full p-2"> |
||||||
|
<div class="extension-wrapper"> |
||||||
|
<div class="extension-header"> |
||||||
|
<div class="extension-header-left"> |
||||||
|
<GeneralIcon icon="drag" /> |
||||||
|
<img v-if="extensionManifest" :src="getExtensionIcon(extensionManifest.iconUrl)" alt="icon" class="h-6" /> |
||||||
|
<input |
||||||
|
v-if="titleEditMode" |
||||||
|
ref="titleInput" |
||||||
|
v-model="tempTitle" |
||||||
|
class="flex-grow leading-1 outline-0 ring-none capitalize !text-inherit !bg-transparent w-4/5" |
||||||
|
@click.stop |
||||||
|
@keyup.enter="updateExtensionTitle" |
||||||
|
@keyup.esc="updateExtensionTitle" |
||||||
|
@blur="updateExtensionTitle" |
||||||
|
/> |
||||||
|
<div v-else class="extension-title" @dblclick="enableEditMode">{{ extension.title }}</div> |
||||||
|
</div> |
||||||
|
<div class="extension-header-right"> |
||||||
|
<GeneralIcon v-if="!activeError" icon="expand" @click="fullscreen = true" /> |
||||||
|
<NcDropdown :trigger="['click']"> |
||||||
|
<GeneralIcon icon="threeDotVertical" /> |
||||||
|
|
||||||
|
<template #overlay> |
||||||
|
<NcMenu> |
||||||
|
<template v-if="!activeError"> |
||||||
|
<NcMenuItem data-rec="true" class="!hover:text-primary" @click="enableEditMode"> |
||||||
|
<GeneralIcon icon="edit" /> |
||||||
|
Rename |
||||||
|
</NcMenuItem> |
||||||
|
<NcMenuItem data-rec="true" class="!hover:text-primary" @click="duplicateExtension(extension.id)"> |
||||||
|
<GeneralIcon icon="duplicate" /> |
||||||
|
Duplicate |
||||||
|
</NcMenuItem> |
||||||
|
<NcMenuItem |
||||||
|
data-rec="true" |
||||||
|
class="!hover:text-primary" |
||||||
|
@click="showExtensionDetails(extension.extensionId, 'extension')" |
||||||
|
> |
||||||
|
<GeneralIcon icon="info" /> |
||||||
|
Details |
||||||
|
</NcMenuItem> |
||||||
|
<NcDivider /> |
||||||
|
</template> |
||||||
|
<NcMenuItem data-rec="true" class="!text-red-500 !hover:bg-red-50" @click="extension.clear()"> |
||||||
|
<GeneralIcon icon="reload" /> |
||||||
|
Clear Data |
||||||
|
</NcMenuItem> |
||||||
|
<NcMenuItem data-rec="true" class="!text-red-500 !hover:bg-red-50" @click="extension.delete()"> |
||||||
|
<GeneralIcon icon="delete" /> |
||||||
|
Delete |
||||||
|
</NcMenuItem> |
||||||
|
</NcMenu> |
||||||
|
</template> |
||||||
|
</NcDropdown> |
||||||
|
<GeneralIcon v-if="collapsed" icon="arrowUp" @click="collapsed = !collapsed" /> |
||||||
|
<GeneralIcon v-else icon="arrowDown" @click="collapsed = !collapsed" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<template v-if="activeError"> |
||||||
|
<div v-show="!collapsed" class="extension-content"> |
||||||
|
<a-result status="error" title="Extension Error"> |
||||||
|
<template #subTitle>{{ activeError }}</template> |
||||||
|
<template #extra> |
||||||
|
<NcButton @click="extension.clear()"> |
||||||
|
<div class="flex items-center gap-2"> |
||||||
|
<GeneralIcon icon="reload" /> |
||||||
|
Clear Data |
||||||
|
</div> |
||||||
|
</NcButton> |
||||||
|
<NcButton type="danger" @click="extension.delete()"> |
||||||
|
<div class="flex items-center gap-2"> |
||||||
|
<GeneralIcon icon="delete" /> |
||||||
|
Delete |
||||||
|
</div> |
||||||
|
</NcButton> |
||||||
|
</template> |
||||||
|
</a-result> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<template v-else> |
||||||
|
<Teleport to="body" :disabled="!fullscreen"> |
||||||
|
<div ref="extensionModalRef" :class="{ 'extension-modal': fullscreen }" @click="closeFullscreen"> |
||||||
|
<div :class="{ 'extension-modal-content': fullscreen }"> |
||||||
|
<div |
||||||
|
v-if="fullscreen" |
||||||
|
class="flex items-center justify-between p-2 bg-gray-100 rounded-t-lg cursor-default h-[40px]" |
||||||
|
> |
||||||
|
<div class="flex items-center gap-2 text-gray-500 font-weight-600"> |
||||||
|
<img v-if="extensionManifest" :src="getExtensionIcon(extensionManifest.iconUrl)" alt="icon" class="w-6 h-6" /> |
||||||
|
<div class="text-sm">{{ extension.title }}</div> |
||||||
|
</div> |
||||||
|
<GeneralIcon class="cursor-pointer" icon="close" @click="fullscreen = false" /> |
||||||
|
</div> |
||||||
|
<div |
||||||
|
v-show="fullscreen || !collapsed" |
||||||
|
class="extension-content" |
||||||
|
:class="{ 'border-1': !fullscreen, 'h-[calc(100%-40px)]': fullscreen }" |
||||||
|
> |
||||||
|
<component :is="component" :key="extension.uiKey" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</Teleport> |
||||||
|
</template> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped lang="scss"> |
||||||
|
.extension-wrapper { |
||||||
|
@apply bg-white rounded-lg p-2 w-full border-1; |
||||||
|
} |
||||||
|
|
||||||
|
.extension-header { |
||||||
|
@apply flex justify-between mb-2; |
||||||
|
|
||||||
|
.extension-header-left { |
||||||
|
@apply flex items-center gap-2; |
||||||
|
} |
||||||
|
|
||||||
|
.extension-header-right { |
||||||
|
@apply flex items-center gap-4; |
||||||
|
} |
||||||
|
|
||||||
|
.extension-title { |
||||||
|
@apply font-weight-600; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.extension-content { |
||||||
|
@apply rounded-lg; |
||||||
|
} |
||||||
|
|
||||||
|
.extension-modal { |
||||||
|
@apply absolute top-0 left-0 z-50 w-full h-full bg-black bg-opacity-50; |
||||||
|
|
||||||
|
.extension-modal-content { |
||||||
|
@apply bg-white rounded-lg w-[90%] h-[90vh] mt-[5vh] mx-auto; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,72 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import { useVModel } from '#imports' |
||||||
|
|
||||||
|
interface Prop { |
||||||
|
modelValue?: boolean |
||||||
|
} |
||||||
|
|
||||||
|
const props = defineProps<Prop>() |
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']) |
||||||
|
|
||||||
|
const vModel = useVModel(props, 'modelValue', emit) |
||||||
|
|
||||||
|
const { availableExtensions, addExtension, getExtensionIcon, showExtensionDetails } = useExtensions() |
||||||
|
|
||||||
|
const onExtensionClick = (extensionId: string) => { |
||||||
|
showExtensionDetails(extensionId) |
||||||
|
vModel.value = false |
||||||
|
} |
||||||
|
|
||||||
|
const onAddExtension = (ext: any) => { |
||||||
|
addExtension(ext) |
||||||
|
vModel.value = false |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<NcModal |
||||||
|
v-model:visible="vModel" |
||||||
|
:body-style="{ 'max-height': '864px', 'height': '85vh' }" |
||||||
|
:class="{ active: vModel }" |
||||||
|
:closable="true" |
||||||
|
:footer="null" |
||||||
|
:width="1280" |
||||||
|
size="medium" |
||||||
|
wrap-class-name="nc-modal-extension-market" |
||||||
|
> |
||||||
|
<div class="flex flex-col h-full"> |
||||||
|
<div class="flex items-center px-4 py-2"> |
||||||
|
<div class="flex items-center gap-2"> |
||||||
|
<GeneralIcon icon="puzzle" /> |
||||||
|
<div class="font-weight-700">Extensions Marketplace</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="flex flex-col flex-1 px-4 py-2"> |
||||||
|
<div class="flex flex-wrap gap-4 p-2"> |
||||||
|
<template v-for="ext of availableExtensions" :key="ext.id"> |
||||||
|
<div class="flex border-1 rounded-lg p-2 w-[360px] cursor-pointer" @click="onExtensionClick(ext.id)"> |
||||||
|
<div class="h-[60px] overflow-hidden m-auto"> |
||||||
|
<img :src="getExtensionIcon(ext.iconUrl)" alt="icon" class="w-full h-full object-cover" /> |
||||||
|
</div> |
||||||
|
<div class="flex flex-grow flex-col ml-3"> |
||||||
|
<div class="flex justify-between"> |
||||||
|
<div class="font-weight-600">{{ ext.title }}</div> |
||||||
|
<NcButton size="xsmall" @click.stop="onAddExtension(ext)"> |
||||||
|
<div class="flex items-center gap-1 mx-1"> |
||||||
|
<GeneralIcon icon="plus" /> |
||||||
|
Add |
||||||
|
</div> |
||||||
|
</NcButton> |
||||||
|
</div> |
||||||
|
<div class="w-[250px] h-[50px] text-xs line-clamp-3">{{ ext.description }}</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</NcModal> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss" scoped></style> |
@ -0,0 +1,59 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { Pane } from 'splitpanes' |
||||||
|
import 'splitpanes/dist/splitpanes.css' |
||||||
|
|
||||||
|
const { extensionList, isPanelExpanded, isDetailsVisible, detailsExtensionId, detailsFrom, isMarketVisible, extensionPanelSize } = |
||||||
|
useExtensions() |
||||||
|
|
||||||
|
const toggleMarket = () => { |
||||||
|
isMarketVisible.value = !isMarketVisible.value |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<Pane v-if="isPanelExpanded" :size="extensionPanelSize" class="flex flex-col bg-orange-50"> |
||||||
|
<div class="flex items-center pl-3 pt-3 font-weight-800 text-orange-500">Extensions</div> |
||||||
|
<template v-if="extensionList.length === 0"> |
||||||
|
<div class="flex items-center flex-col gap-2 w-full nc-scrollbar-md"> |
||||||
|
<div class="w-[100px] h-[100px] bg-gray-200 rounded-lg mt-[100px]"></div> |
||||||
|
<div class="font-weight-700">No extensions added</div> |
||||||
|
<div>Add Extensions from the community extensions marketplace</div> |
||||||
|
<NcButton @click="toggleMarket"> |
||||||
|
<div class="flex items-center gap-2 font-weight-600"> |
||||||
|
<GeneralIcon icon="plus" /> |
||||||
|
Add Extension |
||||||
|
</div> |
||||||
|
</NcButton> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<template v-else> |
||||||
|
<div class="flex w-full items-center justify-between py-2 px-2 bg-orange-50"> |
||||||
|
<div class="flex flex-grow items-center mr-2"> |
||||||
|
<a-input type="text" class="!h-8 !px-3 !py-1 !rounded-lg" placeholder="Search Extension"> |
||||||
|
<template #prefix> |
||||||
|
<GeneralIcon icon="search" class="mr-2 h-4 w-4 text-gray-500 group-hover:text-black" /> |
||||||
|
</template> |
||||||
|
</a-input> |
||||||
|
</div> |
||||||
|
<NcButton type="ghost" size="small" class="!text-primary !bg-white" @click="toggleMarket"> |
||||||
|
<div class="flex items-center gap-1 px-1 text-xs"> |
||||||
|
<GeneralIcon icon="plus" /> |
||||||
|
Add Extension |
||||||
|
</div> |
||||||
|
</NcButton> |
||||||
|
</div> |
||||||
|
<div class="flex items-center flex-col w-full nc-scrollbar-md"> |
||||||
|
<ExtensionsWrapper v-for="ext in extensionList" :key="ext.id" :extension-id="ext.id" /> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<ExtensionsMarket v-if="isMarketVisible" v-model="isMarketVisible" /> |
||||||
|
<ExtensionsDetails |
||||||
|
v-if="isDetailsVisible && detailsExtensionId" |
||||||
|
v-model="isDetailsVisible" |
||||||
|
:extension-id="detailsExtensionId" |
||||||
|
:from="detailsFrom" |
||||||
|
/> |
||||||
|
</Pane> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss"></style> |
@ -0,0 +1,18 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
interface Prop { |
||||||
|
extensionId: string |
||||||
|
} |
||||||
|
|
||||||
|
const { extensionId } = defineProps<Prop>() |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<NuxtErrorBoundary> |
||||||
|
<ExtensionsExtension :extension-id="extensionId" /> |
||||||
|
<template #error="{ error }"> |
||||||
|
<ExtensionsExtension :extension-id="extensionId" :error="error" /> |
||||||
|
</template> |
||||||
|
</NuxtErrorBoundary> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped lang="scss"></style> |
@ -0,0 +1,224 @@ |
|||||||
|
import type { ColumnType, ViewType } from 'nocodb-sdk' |
||||||
|
import type { ExtensionType } from '#imports' |
||||||
|
import { useInjectionState } from '#imports' |
||||||
|
|
||||||
|
const [useProvideExtensionHelper, useExtensionHelper] = useInjectionState((extension: Ref<ExtensionType>) => { |
||||||
|
const { $api } = useNuxtApp() |
||||||
|
|
||||||
|
const basesStore = useBases() |
||||||
|
|
||||||
|
const { activeProjectId: baseId } = storeToRefs(basesStore) |
||||||
|
|
||||||
|
const tableStore = useTablesStore() |
||||||
|
|
||||||
|
const { activeTables: tables } = storeToRefs(tableStore) |
||||||
|
|
||||||
|
const viewStore = useViewsStore() |
||||||
|
|
||||||
|
const { viewsByTable } = storeToRefs(viewStore) |
||||||
|
|
||||||
|
const { getMeta } = useMetas() |
||||||
|
|
||||||
|
const { eventBus } = useSmartsheetStoreOrThrow() |
||||||
|
|
||||||
|
const fullscreen = ref(false) |
||||||
|
|
||||||
|
const collapsed = computed({ |
||||||
|
get: () => extension.value?.meta?.collapsed ?? false, |
||||||
|
set: (value) => { |
||||||
|
extension.value?.setMeta('collapsed', value) |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
|
const getViewsForTable = async (tableId: string) => { |
||||||
|
if (viewsByTable.value.has(tableId)) { |
||||||
|
return viewsByTable.value.get(tableId) as ViewType[] |
||||||
|
} |
||||||
|
|
||||||
|
await viewStore.loadViews({ tableId, ignoreLoading: true }) |
||||||
|
return viewsByTable.value.get(tableId) as ViewType[] |
||||||
|
} |
||||||
|
|
||||||
|
const getData = async (params: { |
||||||
|
tableId: string |
||||||
|
viewId?: string |
||||||
|
eachPage: (records: Record<string, any>[], nextPage: () => void) => Promise<void> | void |
||||||
|
done: () => Promise<void> | void |
||||||
|
}) => { |
||||||
|
const { tableId, viewId, eachPage, done } = params |
||||||
|
|
||||||
|
let page = 0 |
||||||
|
|
||||||
|
const nextPage = async () => { |
||||||
|
const { list: records, pageInfo } = await $api.dbViewRow.list( |
||||||
|
'noco', |
||||||
|
baseId.value!, |
||||||
|
tableId, |
||||||
|
viewId as string, |
||||||
|
{ |
||||||
|
offset: (page - 1) * 25, |
||||||
|
limit: 25, |
||||||
|
} as any, |
||||||
|
) |
||||||
|
|
||||||
|
if (pageInfo?.isLastPage) { |
||||||
|
await eachPage(records, () => {}) |
||||||
|
await done() |
||||||
|
} else { |
||||||
|
page++ |
||||||
|
await eachPage(records, nextPage) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
await nextPage() |
||||||
|
} |
||||||
|
|
||||||
|
const getTableMeta = async (tableId: string) => { |
||||||
|
return getMeta(tableId) |
||||||
|
} |
||||||
|
|
||||||
|
const insertData = async (params: { tableId: string; data: Record<string, any> }) => { |
||||||
|
const { tableId, data } = params |
||||||
|
|
||||||
|
const chunks = [] |
||||||
|
|
||||||
|
let inserted = 0 |
||||||
|
|
||||||
|
// chunk data into 100 records
|
||||||
|
for (let i = 0; i < data.length; i += 100) { |
||||||
|
chunks.push(data.slice(i, i + 100)) |
||||||
|
} |
||||||
|
|
||||||
|
for (const chunk of chunks) { |
||||||
|
inserted += chunk.length |
||||||
|
await $api.dbDataTableRow.create(tableId, chunk) |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
inserted, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const updateData = async (params: { tableId: string; data: Record<string, any> }) => { |
||||||
|
const { tableId, data } = params |
||||||
|
|
||||||
|
const chunks = [] |
||||||
|
|
||||||
|
let updated = 0 |
||||||
|
|
||||||
|
// chunk data into 100 records
|
||||||
|
for (let i = 0; i < data.length; i += 100) { |
||||||
|
chunks.push(data.slice(i, i + 100)) |
||||||
|
} |
||||||
|
|
||||||
|
for (const chunk of chunks) { |
||||||
|
updated += chunk.length |
||||||
|
await $api.dbDataTableRow.update(tableId, chunk) |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
updated, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const upsertData = async (params: { tableId: string; data: Record<string, any>; upsertField: ColumnType }) => { |
||||||
|
const { tableId, data, upsertField } = params |
||||||
|
|
||||||
|
const chunkSize = 100 |
||||||
|
|
||||||
|
const tableMeta = await getMeta(tableId) |
||||||
|
|
||||||
|
if (!tableMeta?.columns) throw new Error('Table not found') |
||||||
|
|
||||||
|
const chunks = [] |
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i += chunkSize) { |
||||||
|
chunks.push(data.slice(i, i + chunkSize)) |
||||||
|
} |
||||||
|
|
||||||
|
const insert = [] |
||||||
|
const update = [] |
||||||
|
|
||||||
|
let insertCounter = 0 |
||||||
|
let updateCounter = 0 |
||||||
|
|
||||||
|
for (const chunk of chunks) { |
||||||
|
// select chunk of data to determine if it's an insert or update
|
||||||
|
const { list } = await $api.dbDataTableRow.list(tableId, { |
||||||
|
where: `(${upsertField.title},in,${chunk.map((record: Record<string, any>) => record[upsertField.title!]).join(',')})`, |
||||||
|
limit: chunkSize, |
||||||
|
}) |
||||||
|
|
||||||
|
insert.push( |
||||||
|
...chunk.filter( |
||||||
|
(record: Record<string, any>) => |
||||||
|
!list.some((r: Record<string, any>) => `${r[upsertField.title!]}` === `${record[upsertField.title!]}`), |
||||||
|
), |
||||||
|
) |
||||||
|
|
||||||
|
update.push( |
||||||
|
...chunk |
||||||
|
.filter((record: Record<string, any>) => |
||||||
|
list.some((r: Record<string, any>) => `${r[upsertField.title!]}` === `${record[upsertField.title!]}`), |
||||||
|
) |
||||||
|
.map((record: Record<string, any>) => { |
||||||
|
const existingRecord = list.find( |
||||||
|
(r: Record<string, any>) => `${r[upsertField.title!]}` === `${record[upsertField.title!]}`, |
||||||
|
) |
||||||
|
return { |
||||||
|
...rowPkData(existingRecord!, tableMeta.columns!), |
||||||
|
...record, |
||||||
|
} |
||||||
|
}), |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
if (insert.length) { |
||||||
|
insertCounter += insert.length |
||||||
|
for (let i = 0; i < insert.length; i += chunkSize) { |
||||||
|
await $api.dbDataTableRow.create(tableId, insert.splice(0, chunkSize)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (update.length) { |
||||||
|
updateCounter += update.length |
||||||
|
for (let i = 0; i < update.length; i += chunkSize) { |
||||||
|
await $api.dbDataTableRow.update(tableId, update.splice(0, chunkSize)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { inserted: insertCounter, updated: updateCounter } |
||||||
|
} |
||||||
|
|
||||||
|
const reloadData = () => { |
||||||
|
eventBus.emit(SmartsheetStoreEvents.DATA_RELOAD) |
||||||
|
} |
||||||
|
|
||||||
|
const reloadMeta = () => { |
||||||
|
eventBus.emit(SmartsheetStoreEvents.FIELD_RELOAD) |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
fullscreen, |
||||||
|
collapsed, |
||||||
|
extension, |
||||||
|
tables, |
||||||
|
getViewsForTable, |
||||||
|
getData, |
||||||
|
getTableMeta, |
||||||
|
$api, |
||||||
|
insertData, |
||||||
|
updateData, |
||||||
|
upsertData, |
||||||
|
reloadData, |
||||||
|
reloadMeta, |
||||||
|
} |
||||||
|
}, 'extension-helper') |
||||||
|
|
||||||
|
export { useProvideExtensionHelper } |
||||||
|
|
||||||
|
export function useExtensionHelperOrThrow() { |
||||||
|
const extensionStore = useExtensionHelper() |
||||||
|
if (extensionStore == null) throw new Error('Please call `useProvideExtensionHelper` on the appropriate parent component') |
||||||
|
return extensionStore |
||||||
|
} |
@ -0,0 +1,394 @@ |
|||||||
|
const extensionsState = createGlobalState(() => { |
||||||
|
const baseExtensions = ref<Record<string, any>>({}) |
||||||
|
return { baseExtensions } |
||||||
|
}) |
||||||
|
|
||||||
|
interface ExtensionManifest { |
||||||
|
id: string |
||||||
|
title: string |
||||||
|
description: string |
||||||
|
entry: string |
||||||
|
version: string |
||||||
|
iconUrl: string |
||||||
|
publisherName: string |
||||||
|
publisherEmail: string |
||||||
|
publisherUrl: string |
||||||
|
} |
||||||
|
|
||||||
|
abstract class ExtensionType { |
||||||
|
abstract id: string |
||||||
|
abstract uiKey: number |
||||||
|
abstract baseId: string |
||||||
|
abstract fkUserId: string |
||||||
|
abstract extensionId: string |
||||||
|
abstract title: string |
||||||
|
abstract kvStore: any |
||||||
|
abstract meta: any |
||||||
|
abstract setTitle(title: string): Promise<any> |
||||||
|
abstract setMeta(key: string, value: any): Promise<any> |
||||||
|
abstract clear(): Promise<any> |
||||||
|
abstract delete(): Promise<any> |
||||||
|
abstract serialize(): any |
||||||
|
abstract deserialize(data: any): void |
||||||
|
} |
||||||
|
|
||||||
|
export { ExtensionType } |
||||||
|
|
||||||
|
export const useExtensions = createSharedComposable(() => { |
||||||
|
const { baseExtensions } = extensionsState() |
||||||
|
|
||||||
|
const { $api } = useNuxtApp() |
||||||
|
|
||||||
|
const { base } = storeToRefs(useBase()) |
||||||
|
|
||||||
|
const extensionsLoaded = ref(false) |
||||||
|
|
||||||
|
const availableExtensions = ref<ExtensionManifest[]>([]) |
||||||
|
|
||||||
|
const extensionPanelSize = ref(40) |
||||||
|
|
||||||
|
const activeBaseExtensions = computed(() => { |
||||||
|
if (!base.value || !base.value.id) { |
||||||
|
return null |
||||||
|
} |
||||||
|
return baseExtensions.value[base.value.id] |
||||||
|
}) |
||||||
|
|
||||||
|
const isPanelExpanded = computed(() => { |
||||||
|
return activeBaseExtensions.value ? activeBaseExtensions.value.expanded : false |
||||||
|
}) |
||||||
|
|
||||||
|
const extensionList = computed<ExtensionType[]>(() => { |
||||||
|
return activeBaseExtensions.value ? activeBaseExtensions.value.extensions : [] |
||||||
|
}) |
||||||
|
|
||||||
|
const toggleExtensionPanel = () => { |
||||||
|
if (activeBaseExtensions.value) { |
||||||
|
activeBaseExtensions.value.expanded = !activeBaseExtensions.value.expanded |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const addExtension = async (extension: any) => { |
||||||
|
if (!base.value || !base.value.id || !baseExtensions.value[base.value.id]) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
const extensionReq = { |
||||||
|
base_id: base.value.id, |
||||||
|
title: extension.title, |
||||||
|
extension_id: extension.id, |
||||||
|
meta: { |
||||||
|
collapsed: false, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
const newExtension = await $api.extensions.create(base.value.id, extensionReq) |
||||||
|
|
||||||
|
if (newExtension) { |
||||||
|
baseExtensions.value[base.value.id].extensions.push(new Extension(newExtension)) |
||||||
|
} |
||||||
|
|
||||||
|
return newExtension |
||||||
|
} |
||||||
|
|
||||||
|
const updateExtension = async (extensionId: string, extension: any) => { |
||||||
|
if (!base.value || !base.value.id || !baseExtensions.value[base.value.id]) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
const updatedExtension = await $api.extensions.update(extensionId, extension) |
||||||
|
|
||||||
|
if (updatedExtension) { |
||||||
|
const extension = baseExtensions.value[base.value.id].extensions.find((ext: any) => ext.id === extensionId) |
||||||
|
|
||||||
|
if (extension) { |
||||||
|
extension.deserialize(updatedExtension) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return updatedExtension |
||||||
|
} |
||||||
|
|
||||||
|
const updateExtensionMeta = async (extensionId: string, key: string, value: any) => { |
||||||
|
const extension = extensionList.value.find((ext: any) => ext.id === extensionId) |
||||||
|
|
||||||
|
if (!extension) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
return updateExtension(extensionId, { |
||||||
|
meta: { |
||||||
|
...extension.meta, |
||||||
|
[key]: value, |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const deleteExtension = async (extensionId: string) => { |
||||||
|
if (!base.value || !base.value.id || !baseExtensions.value[base.value.id]) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
await $api.extensions.delete(extensionId) |
||||||
|
|
||||||
|
baseExtensions.value[base.value.id].extensions = baseExtensions.value[base.value.id].extensions.filter( |
||||||
|
(ext: any) => ext.id !== extensionId, |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
const duplicateExtension = async (extensionId: string) => { |
||||||
|
if (!base.value || !base.value.id || !baseExtensions.value[base.value.id]) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
const extension = extensionList.value.find((ext: any) => ext.id === extensionId) |
||||||
|
|
||||||
|
if (!extension) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
const { id: _id, ...extensionData } = extension.serialize() |
||||||
|
|
||||||
|
const newExtension = await $api.extensions.create(base.value.id, { |
||||||
|
...extensionData, |
||||||
|
title: `${extension.title} (Copy)`, |
||||||
|
}) |
||||||
|
|
||||||
|
if (newExtension) { |
||||||
|
baseExtensions.value[base.value.id].extensions.push(new Extension(newExtension)) |
||||||
|
} |
||||||
|
|
||||||
|
return newExtension |
||||||
|
} |
||||||
|
|
||||||
|
const clearKvStore = async (extensionId: string) => { |
||||||
|
const extension = extensionList.value.find((ext: any) => ext.id === extensionId) |
||||||
|
|
||||||
|
if (!extension) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
return updateExtension(extensionId, { |
||||||
|
kv_store: {}, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const loadExtensionsForBase = async (baseId: string) => { |
||||||
|
if (!baseId) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
const { list } = await $api.extensions.list(baseId) |
||||||
|
|
||||||
|
const extensions = list?.map((ext: any) => new Extension(ext)) |
||||||
|
|
||||||
|
if (baseExtensions.value[baseId]) { |
||||||
|
baseExtensions.value[baseId].extensions = extensions || baseExtensions.value[baseId].extensions |
||||||
|
} else { |
||||||
|
baseExtensions.value[baseId] = { |
||||||
|
extensions: extensions || [], |
||||||
|
expanded: false, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const getExtensionIcon = (pathOrUrl: string) => { |
||||||
|
if (pathOrUrl.startsWith('http')) { |
||||||
|
return pathOrUrl |
||||||
|
} else { |
||||||
|
return new URL(`../extensions/${pathOrUrl}`, import.meta.url).href |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class KvStore { |
||||||
|
private _id: string |
||||||
|
private data: Record<string, any> |
||||||
|
|
||||||
|
constructor(id: string, data: any) { |
||||||
|
this._id = id |
||||||
|
this.data = data || {} |
||||||
|
} |
||||||
|
|
||||||
|
get(key: string) { |
||||||
|
return this.data[key] || null |
||||||
|
} |
||||||
|
|
||||||
|
set(key: string, value: any) { |
||||||
|
this.data[key] = value |
||||||
|
return updateExtension(this._id, { kv_store: this.data }) |
||||||
|
} |
||||||
|
|
||||||
|
delete(key: string) { |
||||||
|
delete this.data[key] |
||||||
|
return updateExtension(this._id, { kv_store: this.data }) |
||||||
|
} |
||||||
|
|
||||||
|
serialize() { |
||||||
|
return this.data |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class Extension implements ExtensionType { |
||||||
|
private _id: string |
||||||
|
private _baseId: string |
||||||
|
private _fkUserId: string |
||||||
|
private _extensionId: string |
||||||
|
private _title: string |
||||||
|
private _kvStore: KvStore |
||||||
|
private _meta: any |
||||||
|
|
||||||
|
public uiKey = 0 |
||||||
|
|
||||||
|
constructor(data: any) { |
||||||
|
this._id = data.id |
||||||
|
this._baseId = data.base_id |
||||||
|
this._fkUserId = data.fk_user_id |
||||||
|
this._extensionId = data.extension_id |
||||||
|
this._title = data.title |
||||||
|
this._kvStore = new KvStore(this._id, data.kv_store) |
||||||
|
this._meta = data.meta |
||||||
|
} |
||||||
|
|
||||||
|
get id() { |
||||||
|
return this._id |
||||||
|
} |
||||||
|
|
||||||
|
get baseId() { |
||||||
|
return this._baseId |
||||||
|
} |
||||||
|
|
||||||
|
get fkUserId() { |
||||||
|
return this._fkUserId |
||||||
|
} |
||||||
|
|
||||||
|
get extensionId() { |
||||||
|
return this._extensionId |
||||||
|
} |
||||||
|
|
||||||
|
get title() { |
||||||
|
return this._title |
||||||
|
} |
||||||
|
|
||||||
|
get kvStore() { |
||||||
|
return this._kvStore |
||||||
|
} |
||||||
|
|
||||||
|
get meta() { |
||||||
|
return this._meta |
||||||
|
} |
||||||
|
|
||||||
|
serialize() { |
||||||
|
return { |
||||||
|
id: this._id, |
||||||
|
base_id: this._baseId, |
||||||
|
fk_user_id: this._fkUserId, |
||||||
|
extension_id: this._extensionId, |
||||||
|
title: this._title, |
||||||
|
kv_store: this._kvStore.serialize(), |
||||||
|
meta: this._meta, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
deserialize(data: any) { |
||||||
|
this._id = data.id |
||||||
|
this._baseId = data.base_id |
||||||
|
this._fkUserId = data.fk_user_id |
||||||
|
this._extensionId = data.extension_id |
||||||
|
this._title = data.title |
||||||
|
this._kvStore = new KvStore(this._id, data.kv_store) |
||||||
|
this._meta = data.meta |
||||||
|
} |
||||||
|
|
||||||
|
setTitle(title: string): Promise<any> { |
||||||
|
return updateExtension(this.id, { title }) |
||||||
|
} |
||||||
|
|
||||||
|
setMeta(key: string, value: any): Promise<any> { |
||||||
|
return updateExtensionMeta(this.id, key, value) |
||||||
|
} |
||||||
|
|
||||||
|
clear(): Promise<any> { |
||||||
|
return clearKvStore(this.id).then(() => { |
||||||
|
this.uiKey++ |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
delete(): Promise<any> { |
||||||
|
return deleteExtension(this.id) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
const modules = import.meta.glob('../extensions/*/*.json') |
||||||
|
for (const path in modules) { |
||||||
|
modules[path]().then((mod: any) => { |
||||||
|
const manifest = mod.default as ExtensionManifest |
||||||
|
availableExtensions.value.push(manifest) |
||||||
|
if (Object.keys(modules).length === availableExtensions.value.length) { |
||||||
|
extensionsLoaded.value = true |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
until(base) |
||||||
|
.toMatch((v) => !!v) |
||||||
|
.then(() => { |
||||||
|
if (!base.value || !base.value.id) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if (!baseExtensions.value[base.value.id]) { |
||||||
|
loadExtensionsForBase(base.value.id) |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
// Extension details modal
|
||||||
|
const isDetailsVisible = ref(false) |
||||||
|
const detailsExtensionId = ref<string>() |
||||||
|
const detailsFrom = ref<'market' | 'extension'>('market') |
||||||
|
|
||||||
|
const showExtensionDetails = (extensionId: string, from?: 'market' | 'extension') => { |
||||||
|
detailsExtensionId.value = extensionId |
||||||
|
isDetailsVisible.value = true |
||||||
|
detailsFrom.value = from || 'market' |
||||||
|
} |
||||||
|
|
||||||
|
// Extension market modal
|
||||||
|
const isMarketVisible = ref(false) |
||||||
|
|
||||||
|
// Egg
|
||||||
|
const extensionsEgg = ref(false) |
||||||
|
|
||||||
|
const extensionsEggCounter = ref(0) |
||||||
|
|
||||||
|
const onEggClick = () => { |
||||||
|
extensionsEggCounter.value++ |
||||||
|
if (extensionsEggCounter.value >= 2) { |
||||||
|
extensionsEgg.value = true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
extensionsLoaded, |
||||||
|
availableExtensions, |
||||||
|
extensionList, |
||||||
|
isPanelExpanded, |
||||||
|
toggleExtensionPanel, |
||||||
|
addExtension, |
||||||
|
duplicateExtension, |
||||||
|
updateExtension, |
||||||
|
updateExtensionMeta, |
||||||
|
clearKvStore, |
||||||
|
deleteExtension, |
||||||
|
getExtensionIcon, |
||||||
|
isDetailsVisible, |
||||||
|
detailsExtensionId, |
||||||
|
detailsFrom, |
||||||
|
showExtensionDetails, |
||||||
|
isMarketVisible, |
||||||
|
onEggClick, |
||||||
|
extensionsEgg, |
||||||
|
extensionPanelSize, |
||||||
|
} |
||||||
|
}) |
@ -0,0 +1,820 @@ |
|||||||
|
import UITypes from '../UITypes'; |
||||||
|
import { IDType } from './index'; |
||||||
|
|
||||||
|
const dbTypes = [ |
||||||
|
'BIGINT', |
||||||
|
'BINARY', |
||||||
|
'BOOLEAN', |
||||||
|
'DATE', |
||||||
|
'DECIMAL', |
||||||
|
'DOUBLE', |
||||||
|
'FLOAT', |
||||||
|
'INT', |
||||||
|
'INTERVAL', |
||||||
|
'VOID', |
||||||
|
'SMALLINT', |
||||||
|
'STRING', |
||||||
|
'TIMESTAMP', |
||||||
|
'TIMESTAMP_NTZ', |
||||||
|
'TINYINT', |
||||||
|
]; |
||||||
|
|
||||||
|
export class DatabricksUi { |
||||||
|
static getNewTableColumns() { |
||||||
|
return [ |
||||||
|
{ |
||||||
|
column_name: 'id', |
||||||
|
title: 'Id', |
||||||
|
dt: 'int', |
||||||
|
dtx: 'int', |
||||||
|
ct: 'int', |
||||||
|
nrqd: false, |
||||||
|
rqd: true, |
||||||
|
ck: false, |
||||||
|
pk: true, |
||||||
|
un: false, |
||||||
|
ai: true, |
||||||
|
cdf: null, |
||||||
|
clen: null, |
||||||
|
np: null, |
||||||
|
ns: 0, |
||||||
|
dtxp: '', |
||||||
|
dtxs: '', |
||||||
|
altered: 1, |
||||||
|
uidt: 'ID', |
||||||
|
uip: '', |
||||||
|
uicn: '', |
||||||
|
}, |
||||||
|
{ |
||||||
|
column_name: 'title', |
||||||
|
title: 'Title', |
||||||
|
dt: 'string', |
||||||
|
dtx: 'specificType', |
||||||
|
ct: 'string', |
||||||
|
nrqd: true, |
||||||
|
rqd: false, |
||||||
|
ck: false, |
||||||
|
pk: false, |
||||||
|
un: false, |
||||||
|
ai: false, |
||||||
|
cdf: null, |
||||||
|
clen: 45, |
||||||
|
np: null, |
||||||
|
ns: null, |
||||||
|
dtxp: '', |
||||||
|
dtxs: '', |
||||||
|
altered: 1, |
||||||
|
uidt: 'SingleLineText', |
||||||
|
uip: '', |
||||||
|
uicn: '', |
||||||
|
}, |
||||||
|
{ |
||||||
|
column_name: 'created_at', |
||||||
|
title: 'CreatedAt', |
||||||
|
dt: 'TIMESTAMP', |
||||||
|
dtx: 'specificType', |
||||||
|
ct: 'TIMESTAMP', |
||||||
|
nrqd: true, |
||||||
|
rqd: false, |
||||||
|
ck: false, |
||||||
|
pk: false, |
||||||
|
un: false, |
||||||
|
ai: false, |
||||||
|
clen: 45, |
||||||
|
np: null, |
||||||
|
ns: null, |
||||||
|
dtxp: '', |
||||||
|
dtxs: '', |
||||||
|
altered: 1, |
||||||
|
uidt: UITypes.CreatedTime, |
||||||
|
uip: '', |
||||||
|
uicn: '', |
||||||
|
system: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
column_name: 'updated_at', |
||||||
|
title: 'UpdatedAt', |
||||||
|
dt: 'TIMESTAMP', |
||||||
|
dtx: 'specificType', |
||||||
|
ct: 'TIMESTAMP', |
||||||
|
nrqd: true, |
||||||
|
rqd: false, |
||||||
|
ck: false, |
||||||
|
pk: false, |
||||||
|
un: false, |
||||||
|
ai: false, |
||||||
|
clen: 45, |
||||||
|
np: null, |
||||||
|
ns: null, |
||||||
|
dtxp: '', |
||||||
|
dtxs: '', |
||||||
|
altered: 1, |
||||||
|
uidt: UITypes.LastModifiedTime, |
||||||
|
uip: '', |
||||||
|
uicn: '', |
||||||
|
system: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
column_name: 'created_by', |
||||||
|
title: 'nc_created_by', |
||||||
|
dt: 'string', |
||||||
|
dtx: 'specificType', |
||||||
|
ct: 'string', |
||||||
|
nrqd: true, |
||||||
|
rqd: false, |
||||||
|
ck: false, |
||||||
|
pk: false, |
||||||
|
un: false, |
||||||
|
ai: false, |
||||||
|
clen: 45, |
||||||
|
np: null, |
||||||
|
ns: null, |
||||||
|
dtxp: '', |
||||||
|
dtxs: '', |
||||||
|
altered: 1, |
||||||
|
uidt: UITypes.CreatedBy, |
||||||
|
uip: '', |
||||||
|
uicn: '', |
||||||
|
system: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
column_name: 'updated_by', |
||||||
|
title: 'nc_updated_by', |
||||||
|
dt: 'string', |
||||||
|
dtx: 'specificType', |
||||||
|
ct: 'string', |
||||||
|
nrqd: true, |
||||||
|
rqd: false, |
||||||
|
ck: false, |
||||||
|
pk: false, |
||||||
|
un: false, |
||||||
|
ai: false, |
||||||
|
clen: 45, |
||||||
|
np: null, |
||||||
|
ns: null, |
||||||
|
dtxp: '', |
||||||
|
dtxs: '', |
||||||
|
altered: 1, |
||||||
|
uidt: UITypes.LastModifiedBy, |
||||||
|
uip: '', |
||||||
|
uicn: '', |
||||||
|
system: true, |
||||||
|
}, |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
static getNewColumn(suffix) { |
||||||
|
return { |
||||||
|
column_name: 'title' + suffix, |
||||||
|
dt: 'string', |
||||||
|
dtx: 'specificType', |
||||||
|
ct: 'string', |
||||||
|
nrqd: true, |
||||||
|
rqd: false, |
||||||
|
ck: false, |
||||||
|
pk: false, |
||||||
|
un: false, |
||||||
|
ai: false, |
||||||
|
cdf: null, |
||||||
|
clen: 45, |
||||||
|
np: null, |
||||||
|
ns: null, |
||||||
|
dtxp: '', |
||||||
|
dtxs: '', |
||||||
|
altered: 1, |
||||||
|
uidt: 'SingleLineText', |
||||||
|
uip: '', |
||||||
|
uicn: '', |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
static getDefaultLengthForDatatype(_type) { |
||||||
|
return ''; |
||||||
|
} |
||||||
|
|
||||||
|
static getDefaultLengthIsDisabled(type): any { |
||||||
|
switch (type) { |
||||||
|
case 'decimal': |
||||||
|
return true; |
||||||
|
|
||||||
|
case 'text': |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static getDefaultValueForDatatype(type): any { |
||||||
|
switch (type) { |
||||||
|
case 'integer': |
||||||
|
return 'eg : ' + 10; |
||||||
|
|
||||||
|
case 'text': |
||||||
|
return 'eg : hey'; |
||||||
|
|
||||||
|
case 'numeric': |
||||||
|
return 'eg : ' + 10; |
||||||
|
|
||||||
|
case 'real': |
||||||
|
return 'eg : ' + 10.0; |
||||||
|
|
||||||
|
case 'blob': |
||||||
|
return 'eg : ' + 100; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static getDefaultScaleForDatatype(type): any { |
||||||
|
switch (type) { |
||||||
|
case 'integer': |
||||||
|
case 'text': |
||||||
|
case 'numeric': |
||||||
|
case 'real': |
||||||
|
case 'blob': |
||||||
|
return ' '; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static colPropAIDisabled(col, columns) { |
||||||
|
// console.log(col);
|
||||||
|
if (col.dt === 'integer') { |
||||||
|
for (let i = 0; i < columns.length; ++i) { |
||||||
|
if (columns[i].cn !== col.cn && columns[i].ai) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
return false; |
||||||
|
} else { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static colPropUNDisabled(_col) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
static onCheckboxChangeAI(col) { |
||||||
|
console.log(col); |
||||||
|
if ( |
||||||
|
col.dt === 'int' || |
||||||
|
col.dt === 'bigint' || |
||||||
|
col.dt === 'smallint' || |
||||||
|
col.dt === 'tinyint' |
||||||
|
) { |
||||||
|
col.altered = col.altered || 2; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static showScale(_columnObj) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
static removeUnsigned(columns) { |
||||||
|
for (let i = 0; i < columns.length; ++i) { |
||||||
|
if ( |
||||||
|
columns[i].altered === 1 && |
||||||
|
!( |
||||||
|
columns[i].dt === 'int' || |
||||||
|
columns[i].dt === 'bigint' || |
||||||
|
columns[i].dt === 'tinyint' || |
||||||
|
columns[i].dt === 'smallint' || |
||||||
|
columns[i].dt === 'mediumint' |
||||||
|
) |
||||||
|
) { |
||||||
|
columns[i].un = false; |
||||||
|
console.log('>> resetting unsigned value', columns[i].cn); |
||||||
|
} |
||||||
|
console.log(columns[i].cn); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/*static extractFunctionName(query) { |
||||||
|
const reg = |
||||||
|
/^\s*CREATE\s+(?:OR\s+REPLACE\s*)?\s*FUNCTION\s+(?:[\w\d_]+\.)?([\w_\d]+)/i; |
||||||
|
const match = query.match(reg); |
||||||
|
return match && match[1]; |
||||||
|
} |
||||||
|
|
||||||
|
static extractProcedureName(query) { |
||||||
|
const reg = |
||||||
|
/^\s*CREATE\s+(?:OR\s+REPLACE\s*)?\s*PROCEDURE\s+(?:[\w\d_]+\.)?([\w_\d]+)/i; |
||||||
|
const match = query.match(reg); |
||||||
|
return match && match[1]; |
||||||
|
}*/ |
||||||
|
static columnEditable(_colObj) { |
||||||
|
return true; // colObj.altered === 1;
|
||||||
|
} |
||||||
|
|
||||||
|
/*static handleRawOutput(result, headers) { |
||||||
|
console.log(result); |
||||||
|
if (Array.isArray(result) && result[0]) { |
||||||
|
const keys = Object.keys(result[0]); |
||||||
|
// set headers before settings result
|
||||||
|
for (let i = 0; i < keys.length; i++) { |
||||||
|
const text = keys[i]; |
||||||
|
headers.push({ text, value: text, sortable: false }); |
||||||
|
} |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
static splitQueries(query) { |
||||||
|
/!*** |
||||||
|
* we are splitting based on semicolon |
||||||
|
* there are mechanism to escape semicolon within single/double quotes(string) |
||||||
|
*!/ |
||||||
|
return query.match(/\b("[^"]*;[^"]*"|'[^']*;[^']*'|[^;])*;/g); |
||||||
|
} |
||||||
|
|
||||||
|
/!** |
||||||
|
* if sql statement is SELECT - it limits to a number |
||||||
|
* @param args |
||||||
|
* @returns {string|*} |
||||||
|
*!/ |
||||||
|
sanitiseQuery(args) { |
||||||
|
let q = args.query.trim().split(';'); |
||||||
|
|
||||||
|
if (q[0].startsWith('Select')) { |
||||||
|
q = q[0] + ` LIMIT 0,${args.limit ? args.limit : 100};`; |
||||||
|
} else if (q[0].startsWith('select')) { |
||||||
|
q = q[0] + ` LIMIT 0,${args.limit ? args.limit : 100};`; |
||||||
|
} else if (q[0].startsWith('SELECT')) { |
||||||
|
q = q[0] + ` LIMIT 0,${args.limit ? args.limit : 100};`; |
||||||
|
} else { |
||||||
|
return args.query; |
||||||
|
} |
||||||
|
|
||||||
|
return q; |
||||||
|
} |
||||||
|
|
||||||
|
static getColumnsFromJson(json, tn) { |
||||||
|
const columns = []; |
||||||
|
|
||||||
|
try { |
||||||
|
if (typeof json === 'object' && !Array.isArray(json)) { |
||||||
|
const keys = Object.keys(json); |
||||||
|
for (let i = 0; i < keys.length; ++i) { |
||||||
|
const column = { |
||||||
|
dp: null, |
||||||
|
tn, |
||||||
|
column_name: keys[i], |
||||||
|
cno: keys[i], |
||||||
|
np: null, |
||||||
|
ns: null, |
||||||
|
clen: null, |
||||||
|
cop: 1, |
||||||
|
pk: false, |
||||||
|
nrqd: false, |
||||||
|
rqd: false, |
||||||
|
un: false, |
||||||
|
ct: 'int(11) unsigned', |
||||||
|
ai: false, |
||||||
|
unique: false, |
||||||
|
cdf: null, |
||||||
|
cc: '', |
||||||
|
csn: null, |
||||||
|
dtx: 'specificType', |
||||||
|
dtxp: null, |
||||||
|
dtxs: 0, |
||||||
|
altered: 1, |
||||||
|
}; |
||||||
|
|
||||||
|
switch (typeof json[keys[i]]) { |
||||||
|
case 'number': |
||||||
|
if (Number.isInteger(json[keys[i]])) { |
||||||
|
if (SqliteUi.isValidTimestamp(keys[i], json[keys[i]])) { |
||||||
|
Object.assign(column, { |
||||||
|
dt: 'timestamp', |
||||||
|
}); |
||||||
|
} else { |
||||||
|
Object.assign(column, { |
||||||
|
dt: 'integer', |
||||||
|
}); |
||||||
|
} |
||||||
|
} else { |
||||||
|
Object.assign(column, { |
||||||
|
dt: 'real', |
||||||
|
}); |
||||||
|
} |
||||||
|
break; |
||||||
|
case 'string': |
||||||
|
// if (SqliteUi.isValidDate(json[keys[i]])) {
|
||||||
|
// Object.assign(column, {
|
||||||
|
// "dt": "datetime"
|
||||||
|
// });
|
||||||
|
// } else
|
||||||
|
if (json[keys[i]].length <= 255) { |
||||||
|
Object.assign(column, { |
||||||
|
dt: 'varchar', |
||||||
|
}); |
||||||
|
} else { |
||||||
|
Object.assign(column, { |
||||||
|
dt: 'text', |
||||||
|
}); |
||||||
|
} |
||||||
|
break; |
||||||
|
case 'boolean': |
||||||
|
Object.assign(column, { |
||||||
|
dt: 'integer', |
||||||
|
}); |
||||||
|
break; |
||||||
|
case 'object': |
||||||
|
Object.assign(column, { |
||||||
|
dt: 'text', |
||||||
|
np: null, |
||||||
|
dtxp: null, |
||||||
|
}); |
||||||
|
break; |
||||||
|
default: |
||||||
|
break; |
||||||
|
} |
||||||
|
columns.push(column); |
||||||
|
} |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
console.log('Error in getColumnsFromJson', e); |
||||||
|
} |
||||||
|
|
||||||
|
return columns; |
||||||
|
} |
||||||
|
|
||||||
|
static isValidTimestamp(key, value) { |
||||||
|
if (typeof value !== 'number') { |
||||||
|
return false; |
||||||
|
} |
||||||
|
return new Date(value).getTime() > 0 && /(?:_|(?=A))[aA]t$/.test(key); |
||||||
|
} |
||||||
|
|
||||||
|
static isValidDate(value) { |
||||||
|
return new Date(value).getTime() > 0; |
||||||
|
}*/ |
||||||
|
|
||||||
|
static onCheckboxChangeAU(col) { |
||||||
|
console.log(col); |
||||||
|
// if (1) {
|
||||||
|
col.altered = col.altered || 2; |
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!col.ai) {
|
||||||
|
// col.dtx = 'specificType'
|
||||||
|
// } else {
|
||||||
|
// col.dtx = ''
|
||||||
|
// }
|
||||||
|
} |
||||||
|
|
||||||
|
static colPropAuDisabled(col) { |
||||||
|
if (col.altered !== 1) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
switch (col.dt) { |
||||||
|
case 'date': |
||||||
|
case 'datetime': |
||||||
|
case 'timestamp': |
||||||
|
case 'time': |
||||||
|
return false; |
||||||
|
|
||||||
|
default: |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static getAbstractType(col): any { |
||||||
|
switch (col.dt?.replace(/\(\d+\)$/).toLowerCase()) { |
||||||
|
case 'bigint': |
||||||
|
case 'tinyint': |
||||||
|
case 'int': |
||||||
|
case 'smallint': |
||||||
|
return 'integer'; |
||||||
|
case 'decimal': |
||||||
|
case 'double': |
||||||
|
case 'float': |
||||||
|
return 'float'; |
||||||
|
case 'boolean': |
||||||
|
return 'boolean'; |
||||||
|
case 'timestamp': |
||||||
|
case 'timestamp_ntz': |
||||||
|
return 'datetime'; |
||||||
|
case 'date': |
||||||
|
return 'date'; |
||||||
|
case 'string': |
||||||
|
return 'string'; |
||||||
|
} |
||||||
|
return 'string'; |
||||||
|
} |
||||||
|
|
||||||
|
static getUIType(col): any { |
||||||
|
switch (this.getAbstractType(col)) { |
||||||
|
case 'integer': |
||||||
|
return 'Number'; |
||||||
|
case 'boolean': |
||||||
|
return 'Checkbox'; |
||||||
|
case 'float': |
||||||
|
return 'Decimal'; |
||||||
|
case 'date': |
||||||
|
return 'Date'; |
||||||
|
case 'datetime': |
||||||
|
return 'CreatedTime'; |
||||||
|
case 'time': |
||||||
|
return 'Time'; |
||||||
|
case 'year': |
||||||
|
return 'Year'; |
||||||
|
case 'string': |
||||||
|
return 'SingleLineText'; |
||||||
|
case 'text': |
||||||
|
return 'LongText'; |
||||||
|
case 'blob': |
||||||
|
return 'Attachment'; |
||||||
|
case 'enum': |
||||||
|
return 'SingleSelect'; |
||||||
|
case 'set': |
||||||
|
return 'MultiSelect'; |
||||||
|
case 'json': |
||||||
|
return 'LongText'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static getDataTypeForUiType(col: { uidt: UITypes; }, idType?: IDType) { |
||||||
|
const colProp: any = {}; |
||||||
|
switch (col.uidt) { |
||||||
|
case 'ID': |
||||||
|
{ |
||||||
|
const isAutoIncId = idType === 'AI'; |
||||||
|
const isAutoGenId = idType === 'AG'; |
||||||
|
colProp.dt = isAutoGenId ? 'varchar' : 'integer'; |
||||||
|
colProp.pk = true; |
||||||
|
colProp.un = isAutoIncId; |
||||||
|
colProp.ai = isAutoIncId; |
||||||
|
colProp.rqd = true; |
||||||
|
colProp.meta = isAutoGenId ? { ag: 'nc' } : undefined; |
||||||
|
} |
||||||
|
break; |
||||||
|
case 'ForeignKey': |
||||||
|
colProp.dt = 'string'; |
||||||
|
break; |
||||||
|
case 'SingleLineText': |
||||||
|
colProp.dt = 'string'; |
||||||
|
break; |
||||||
|
case 'LongText': |
||||||
|
colProp.dt = 'string'; |
||||||
|
break; |
||||||
|
case 'Attachment': |
||||||
|
colProp.dt = 'string'; |
||||||
|
break; |
||||||
|
case 'GeoData': |
||||||
|
colProp.dt = 'string'; |
||||||
|
break; |
||||||
|
case 'Checkbox': |
||||||
|
colProp.dt = 'boolean'; |
||||||
|
break; |
||||||
|
case 'MultiSelect': |
||||||
|
colProp.dt = 'string'; |
||||||
|
break; |
||||||
|
case 'SingleSelect': |
||||||
|
colProp.dt = 'string'; |
||||||
|
break; |
||||||
|
case 'Collaborator': |
||||||
|
colProp.dt = 'string'; |
||||||
|
break; |
||||||
|
case 'Date': |
||||||
|
colProp.dt = 'date'; |
||||||
|
|
||||||
|
break; |
||||||
|
case 'Year': |
||||||
|
colProp.dt = 'number'; |
||||||
|
break; |
||||||
|
case 'Time': |
||||||
|
colProp.dt = 'string'; |
||||||
|
break; |
||||||
|
case 'PhoneNumber': |
||||||
|
colProp.dt = 'string'; |
||||||
|
colProp.validate = { |
||||||
|
func: ['isMobilePhone'], |
||||||
|
args: [''], |
||||||
|
msg: ['Validation failed : isMobilePhone'], |
||||||
|
}; |
||||||
|
break; |
||||||
|
case 'Email': |
||||||
|
colProp.dt = 'string'; |
||||||
|
colProp.validate = { |
||||||
|
func: ['isEmail'], |
||||||
|
args: [''], |
||||||
|
msg: ['Validation failed : isEmail'], |
||||||
|
}; |
||||||
|
break; |
||||||
|
case 'URL': |
||||||
|
colProp.dt = 'string'; |
||||||
|
colProp.validate = { |
||||||
|
func: ['isURL'], |
||||||
|
args: [''], |
||||||
|
msg: ['Validation failed : isURL'], |
||||||
|
}; |
||||||
|
break; |
||||||
|
case 'Number': |
||||||
|
colProp.dt = 'int'; |
||||||
|
break; |
||||||
|
case 'Decimal': |
||||||
|
colProp.dt = 'decimal'; |
||||||
|
break; |
||||||
|
case 'Currency': |
||||||
|
colProp.dt = 'double'; |
||||||
|
colProp.validate = { |
||||||
|
func: ['isCurrency'], |
||||||
|
args: [''], |
||||||
|
msg: ['Validation failed : isCurrency'], |
||||||
|
}; |
||||||
|
break; |
||||||
|
case 'Percent': |
||||||
|
colProp.dt = 'double'; |
||||||
|
break; |
||||||
|
case 'Duration': |
||||||
|
colProp.dt = 'decimal'; |
||||||
|
break; |
||||||
|
case 'Rating': |
||||||
|
colProp.dt = 'int'; |
||||||
|
colProp.cdf = '0'; |
||||||
|
break; |
||||||
|
case 'Formula': |
||||||
|
colProp.dt = 'string'; |
||||||
|
break; |
||||||
|
case 'Rollup': |
||||||
|
colProp.dt = 'string'; |
||||||
|
break; |
||||||
|
case 'Count': |
||||||
|
colProp.dt = 'int'; |
||||||
|
break; |
||||||
|
case 'Lookup': |
||||||
|
colProp.dt = 'string'; |
||||||
|
break; |
||||||
|
case 'DateTime': |
||||||
|
colProp.dt = 'datetime'; |
||||||
|
break; |
||||||
|
case 'CreatedTime': |
||||||
|
colProp.dt = 'datetime'; |
||||||
|
break; |
||||||
|
case 'LastModifiedTime': |
||||||
|
colProp.dt = 'datetime'; |
||||||
|
break; |
||||||
|
case 'AutoNumber': |
||||||
|
colProp.dt = 'int'; |
||||||
|
break; |
||||||
|
case 'Barcode': |
||||||
|
colProp.dt = 'string'; |
||||||
|
break; |
||||||
|
case 'Button': |
||||||
|
colProp.dt = 'string'; |
||||||
|
break; |
||||||
|
case 'JSON': |
||||||
|
colProp.dt = 'string'; |
||||||
|
break; |
||||||
|
default: |
||||||
|
colProp.dt = 'string'; |
||||||
|
break; |
||||||
|
} |
||||||
|
return colProp; |
||||||
|
} |
||||||
|
|
||||||
|
static getDataTypeListForUiType(col: { uidt: UITypes; }, idType?: IDType) { |
||||||
|
switch (col.uidt) { |
||||||
|
case 'ID': |
||||||
|
if (idType === 'AG') { |
||||||
|
return ['character', 'text', 'varchar']; |
||||||
|
} else if (idType === 'AI') { |
||||||
|
return [ |
||||||
|
'int', |
||||||
|
'integer', |
||||||
|
'tinyint', |
||||||
|
'smallint', |
||||||
|
'mediumint', |
||||||
|
'bigint', |
||||||
|
'int2', |
||||||
|
'int8', |
||||||
|
]; |
||||||
|
} else { |
||||||
|
return dbTypes; |
||||||
|
} |
||||||
|
case 'ForeignKey': |
||||||
|
return dbTypes; |
||||||
|
case 'SingleLineText': |
||||||
|
case 'LongText': |
||||||
|
case 'Attachment': |
||||||
|
case 'Collaborator': |
||||||
|
case 'GeoData': |
||||||
|
return ['string']; |
||||||
|
|
||||||
|
case 'Checkbox': |
||||||
|
return [ |
||||||
|
'boolean', |
||||||
|
]; |
||||||
|
|
||||||
|
case 'MultiSelect': |
||||||
|
return ['string']; |
||||||
|
|
||||||
|
case 'SingleSelect': |
||||||
|
return ['string']; |
||||||
|
|
||||||
|
case 'Year': |
||||||
|
return [ |
||||||
|
'int', |
||||||
|
]; |
||||||
|
|
||||||
|
case 'Time': |
||||||
|
return [ |
||||||
|
'string', |
||||||
|
]; |
||||||
|
|
||||||
|
case 'PhoneNumber': |
||||||
|
case 'Email': |
||||||
|
return ['string']; |
||||||
|
|
||||||
|
case 'URL': |
||||||
|
return ['string']; |
||||||
|
|
||||||
|
case 'Number': |
||||||
|
return [ |
||||||
|
'int', |
||||||
|
]; |
||||||
|
|
||||||
|
case 'Decimal': |
||||||
|
return ['decimal', 'float', 'double']; |
||||||
|
|
||||||
|
case 'Currency': |
||||||
|
return [ |
||||||
|
'decimal', |
||||||
|
]; |
||||||
|
|
||||||
|
case 'Percent': |
||||||
|
return [ |
||||||
|
'decimal', |
||||||
|
]; |
||||||
|
|
||||||
|
case 'Duration': |
||||||
|
return [ |
||||||
|
'decimal', |
||||||
|
]; |
||||||
|
|
||||||
|
case 'Rating': |
||||||
|
return [ |
||||||
|
'int', |
||||||
|
]; |
||||||
|
|
||||||
|
case 'Formula': |
||||||
|
return ['string']; |
||||||
|
|
||||||
|
case 'Rollup': |
||||||
|
return ['string']; |
||||||
|
|
||||||
|
case 'Count': |
||||||
|
return [ |
||||||
|
'int', |
||||||
|
]; |
||||||
|
|
||||||
|
case 'Lookup': |
||||||
|
return ['string']; |
||||||
|
|
||||||
|
case 'Date': |
||||||
|
return ['date']; |
||||||
|
|
||||||
|
case 'DateTime': |
||||||
|
case 'CreatedTime': |
||||||
|
case 'LastModifiedTime': |
||||||
|
return ['datetime']; |
||||||
|
|
||||||
|
case 'AutoNumber': |
||||||
|
return [ |
||||||
|
'int', |
||||||
|
]; |
||||||
|
|
||||||
|
case 'Barcode': |
||||||
|
return ['string']; |
||||||
|
|
||||||
|
case 'Geometry': |
||||||
|
return ['string']; |
||||||
|
case 'JSON': |
||||||
|
return ['string']; |
||||||
|
|
||||||
|
case 'Button': |
||||||
|
default: |
||||||
|
return dbTypes; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static getUnsupportedFnList() { |
||||||
|
return [ |
||||||
|
'LOG', |
||||||
|
'EXP', |
||||||
|
'POWER', |
||||||
|
'SQRT', |
||||||
|
'XOR', |
||||||
|
'REGEX_MATCH', |
||||||
|
'REGEX_EXTRACT', |
||||||
|
'REGEX_REPLACE', |
||||||
|
'VALUE', |
||||||
|
'COUNTA', |
||||||
|
'COUNT', |
||||||
|
'ROUNDDOWN', |
||||||
|
'ROUNDUP', |
||||||
|
'DATESTR', |
||||||
|
'DAY', |
||||||
|
'MONTH', |
||||||
|
'HOUR', |
||||||
|
]; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
import { Test } from '@nestjs/testing'; |
||||||
|
import { ExtensionsService } from '../services/extensions.service'; |
||||||
|
import { ExtensionsController } from './extensions.controller'; |
||||||
|
import type { TestingModule } from '@nestjs/testing'; |
||||||
|
|
||||||
|
describe('ExtensionsController', () => { |
||||||
|
let controller: ExtensionsController; |
||||||
|
|
||||||
|
beforeEach(async () => { |
||||||
|
const module: TestingModule = await Test.createTestingModule({ |
||||||
|
controllers: [ExtensionsController], |
||||||
|
providers: [ExtensionsService], |
||||||
|
}).compile(); |
||||||
|
|
||||||
|
controller = module.get<ExtensionsController>(ExtensionsController); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should be defined', () => { |
||||||
|
expect(controller).toBeDefined(); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,80 @@ |
|||||||
|
import { |
||||||
|
Body, |
||||||
|
Controller, |
||||||
|
Delete, |
||||||
|
Get, |
||||||
|
Param, |
||||||
|
Patch, |
||||||
|
Post, |
||||||
|
Req, |
||||||
|
UseGuards, |
||||||
|
} from '@nestjs/common'; |
||||||
|
import type { ExtensionReqType } from 'nocodb-sdk'; |
||||||
|
import { GlobalGuard } from '~/guards/global/global.guard'; |
||||||
|
import { ExtensionsService } from '~/services/extensions.service'; |
||||||
|
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; |
||||||
|
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard'; |
||||||
|
import { NcRequest } from '~/interface/config'; |
||||||
|
import { PagedResponseImpl } from '~/helpers/PagedResponse'; |
||||||
|
|
||||||
|
@Controller() |
||||||
|
@UseGuards(MetaApiLimiterGuard, GlobalGuard) |
||||||
|
export class ExtensionsController { |
||||||
|
constructor(private readonly extensionsService: ExtensionsService) {} |
||||||
|
|
||||||
|
@Get(['/api/v2/extensions/:baseId']) |
||||||
|
@Acl('extensionList') |
||||||
|
async extensionList(@Param('baseId') baseId: string, @Req() _req: NcRequest) { |
||||||
|
return new PagedResponseImpl( |
||||||
|
await this.extensionsService.extensionList({ baseId }), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
@Post(['/api/v2/extensions/:baseId']) |
||||||
|
@Acl('extensionCreate') |
||||||
|
async extensionCreate( |
||||||
|
@Param('baseId') baseId: string, |
||||||
|
@Body() body: Partial<ExtensionReqType>, |
||||||
|
@Req() req: NcRequest, |
||||||
|
) { |
||||||
|
return await this.extensionsService.extensionCreate({ |
||||||
|
extension: { |
||||||
|
...body, |
||||||
|
base_id: baseId, |
||||||
|
}, |
||||||
|
req, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@Get(['/api/v2/extensions/:extensionId']) |
||||||
|
@Acl('extensionRead') |
||||||
|
async extensionRead(@Param('extensionId') extensionId: string) { |
||||||
|
return await this.extensionsService.extensionRead({ extensionId }); |
||||||
|
} |
||||||
|
|
||||||
|
@Patch(['/api/v2/extensions/:extensionId']) |
||||||
|
@Acl('extensionUpdate') |
||||||
|
async extensionUpdate( |
||||||
|
@Param('extensionId') extensionId: string, |
||||||
|
@Body() body: Partial<ExtensionReqType>, |
||||||
|
@Req() req: NcRequest, |
||||||
|
) { |
||||||
|
return await this.extensionsService.extensionUpdate({ |
||||||
|
extensionId, |
||||||
|
extension: body, |
||||||
|
req, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@Delete(['/api/v2/extensions/:extensionId']) |
||||||
|
@Acl('extensionDelete') |
||||||
|
async extensionDelete( |
||||||
|
@Param('extensionId') extensionId: string, |
||||||
|
@Req() req: NcRequest, |
||||||
|
) { |
||||||
|
return await this.extensionsService.extensionDelete({ |
||||||
|
extensionId, |
||||||
|
req, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,44 @@ |
|||||||
|
import commonFns from './commonFns'; |
||||||
|
import type { MapFnArgs } from '../mapFunctionName'; |
||||||
|
|
||||||
|
const databricks = { |
||||||
|
...commonFns, |
||||||
|
AND: async (args: MapFnArgs) => { |
||||||
|
return { |
||||||
|
builder: args.knex.raw( |
||||||
|
`CASE WHEN ${args.knex |
||||||
|
.raw( |
||||||
|
`${( |
||||||
|
await Promise.all( |
||||||
|
args.pt.arguments.map(async (ar) => |
||||||
|
(await args.fn(ar, '', 'AND')).builder.toQuery(), |
||||||
|
), |
||||||
|
) |
||||||
|
).join(' AND ')}`,
|
||||||
|
) |
||||||
|
.wrap('(', ')') |
||||||
|
.toQuery()} THEN TRUE ELSE FALSE END ${args.colAlias}`,
|
||||||
|
), |
||||||
|
}; |
||||||
|
}, |
||||||
|
OR: async (args: MapFnArgs) => { |
||||||
|
return { |
||||||
|
builder: args.knex.raw( |
||||||
|
`CASE WHEN ${args.knex |
||||||
|
.raw( |
||||||
|
`${( |
||||||
|
await Promise.all( |
||||||
|
args.pt.arguments.map(async (ar) => |
||||||
|
(await args.fn(ar, '', 'OR')).builder.toQuery(), |
||||||
|
), |
||||||
|
) |
||||||
|
).join(' OR ')}`,
|
||||||
|
) |
||||||
|
.wrap('(', ')') |
||||||
|
.toQuery()} THEN TRUE ELSE FALSE END ${args.colAlias}`,
|
||||||
|
), |
||||||
|
}; |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
export default databricks; |
@ -0,0 +1,509 @@ |
|||||||
|
import BaseModelXcMeta from './BaseModelXcMeta'; |
||||||
|
|
||||||
|
class ModelXcMetaDatabricks extends BaseModelXcMeta { |
||||||
|
/** |
||||||
|
* @param dir |
||||||
|
* @param filename |
||||||
|
* @param ctx |
||||||
|
* @param ctx.tn |
||||||
|
* @param ctx.columns |
||||||
|
* @param ctx.relations |
||||||
|
*/ |
||||||
|
constructor({ dir, filename, ctx }) { |
||||||
|
super({ dir, filename, ctx }); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Prepare variables used in code template |
||||||
|
*/ |
||||||
|
prepare() { |
||||||
|
const data: any = {}; |
||||||
|
|
||||||
|
/* run of simple variable */ |
||||||
|
data.tn = this.ctx.tn; |
||||||
|
data.dbType = this.ctx.dbType; |
||||||
|
|
||||||
|
/* for complex code provide a func and args - do derivation within the func cbk */ |
||||||
|
data.columns = { |
||||||
|
func: this._renderXcColumns.bind(this), |
||||||
|
args: { |
||||||
|
tn: this.ctx.tn, |
||||||
|
columns: this.ctx.columns, |
||||||
|
relations: this.ctx.relations, |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
/* for complex code provide a func and args - do derivation within the func cbk */ |
||||||
|
data.hasMany = { |
||||||
|
func: this.renderXcHasMany.bind(this), |
||||||
|
args: { |
||||||
|
tn: this.ctx.tn, |
||||||
|
columns: this.ctx.columns, |
||||||
|
hasMany: this.ctx.hasMany, |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
/* for complex code provide a func and args - do derivation within the func cbk */ |
||||||
|
data.belongsTo = { |
||||||
|
func: this.renderXcBelongsTo.bind(this), |
||||||
|
args: { |
||||||
|
tn: this.ctx.tn, |
||||||
|
columns: this.ctx.columns, |
||||||
|
belongsTo: this.ctx.belongsTo, |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
return data; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* |
||||||
|
* @param args |
||||||
|
* @param args.columns |
||||||
|
* @param args.relations |
||||||
|
* @returns {string} |
||||||
|
* @private |
||||||
|
*/ |
||||||
|
_renderXcColumns(args) { |
||||||
|
let str = '[\r\n'; |
||||||
|
|
||||||
|
for (let i = 0; i < args.columns.length; ++i) { |
||||||
|
str += `{\r\n`; |
||||||
|
str += `cn: '${args.columns[i].cn}',\r\n`; |
||||||
|
str += `type: '${this._getAbstractType(args.columns[i])}',\r\n`; |
||||||
|
str += `dt: '${args.columns[i].dt}',\r\n`; |
||||||
|
if (args.columns[i].rqd) str += `rqd: ${args.columns[i].rqd},\r\n`; |
||||||
|
|
||||||
|
if (args.columns[i].cdf) { |
||||||
|
str += `default: "${args.columns[i].cdf}",\r\n`; |
||||||
|
str += `columnDefault: "${args.columns[i].cdf}",\r\n`; |
||||||
|
} |
||||||
|
|
||||||
|
if (args.columns[i].un) str += `un: ${args.columns[i].un},\r\n`; |
||||||
|
|
||||||
|
if (args.columns[i].pk) str += `pk: ${args.columns[i].pk},\r\n`; |
||||||
|
|
||||||
|
if (args.columns[i].ai) str += `ai: ${args.columns[i].ai},\r\n`; |
||||||
|
|
||||||
|
if (args.columns[i].dtxp) str += `dtxp: "${args.columns[i].dtxp}",\r\n`; |
||||||
|
|
||||||
|
if (args.columns[i].dtxs) str += `dtxs: ${args.columns[i].dtxs},\r\n`; |
||||||
|
|
||||||
|
str += `validate: {
|
||||||
|
func: [], |
||||||
|
args: [], |
||||||
|
msg: [] |
||||||
|
},`;
|
||||||
|
str += `},\r\n`; |
||||||
|
} |
||||||
|
|
||||||
|
str += ']\r\n'; |
||||||
|
|
||||||
|
return str; |
||||||
|
} |
||||||
|
|
||||||
|
_getAbstractType(column) { |
||||||
|
return this.getAbstractType(column); |
||||||
|
} |
||||||
|
|
||||||
|
getUIDataType(col): any { |
||||||
|
const dt = col.dt.toLowerCase(); |
||||||
|
switch (dt) { |
||||||
|
case 'bigint': |
||||||
|
case 'tinyint': |
||||||
|
case 'int': |
||||||
|
case 'smallint': |
||||||
|
return 'Number'; |
||||||
|
case 'decimal': |
||||||
|
case 'double': |
||||||
|
case 'float': |
||||||
|
return 'Decimal'; |
||||||
|
case 'boolean': |
||||||
|
return 'Checkbox'; |
||||||
|
case 'timestamp': |
||||||
|
case 'timestamp_ntz': |
||||||
|
return 'DateTime'; |
||||||
|
|
||||||
|
case 'date': |
||||||
|
return 'Date'; |
||||||
|
|
||||||
|
case 'string': |
||||||
|
return 'LongText'; |
||||||
|
|
||||||
|
case 'interval': |
||||||
|
case 'void': |
||||||
|
case 'binary': |
||||||
|
default: |
||||||
|
return 'SpecificDBType'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
getAbstractType(col): any { |
||||||
|
const dt = col.dt.toLowerCase(); |
||||||
|
switch (dt) { |
||||||
|
case 'bigint': |
||||||
|
case 'tinyint': |
||||||
|
case 'decimal': |
||||||
|
case 'double': |
||||||
|
case 'float': |
||||||
|
case 'int': |
||||||
|
case 'smallint': |
||||||
|
return 'integer'; |
||||||
|
case 'binary': |
||||||
|
return dt; |
||||||
|
case 'boolean': |
||||||
|
return 'boolean'; |
||||||
|
case 'interval': |
||||||
|
case 'void': |
||||||
|
return dt; |
||||||
|
|
||||||
|
case 'timestamp': |
||||||
|
case 'timestamp_ntz': |
||||||
|
return 'datetime'; |
||||||
|
|
||||||
|
case 'date': |
||||||
|
return 'date'; |
||||||
|
|
||||||
|
case 'string': |
||||||
|
return 'string'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
_sequelizeGetType(column) { |
||||||
|
let str = ''; |
||||||
|
switch (column.dt) { |
||||||
|
case 'int': |
||||||
|
str += `DataTypes.INTEGER(${column.dtxp})`; |
||||||
|
if (column.un) str += `.UNSIGNED`; |
||||||
|
break; |
||||||
|
case 'tinyint': |
||||||
|
str += `DataTypes.INTEGER(${column.dtxp})`; |
||||||
|
if (column.un) str += `.UNSIGNED`; |
||||||
|
|
||||||
|
break; |
||||||
|
case 'smallint': |
||||||
|
str += `DataTypes.INTEGER(${column.dtxp})`; |
||||||
|
if (column.un) str += `.UNSIGNED`; |
||||||
|
|
||||||
|
break; |
||||||
|
case 'mediumint': |
||||||
|
str += `DataTypes.INTEGER(${column.dtxp})`; |
||||||
|
if (column.un) str += `.UNSIGNED`; |
||||||
|
|
||||||
|
break; |
||||||
|
case 'bigint': |
||||||
|
str += `DataTypes.BIGINT`; |
||||||
|
if (column.un) str += `.UNSIGNED`; |
||||||
|
|
||||||
|
break; |
||||||
|
case 'float': |
||||||
|
str += `DataTypes.FLOAT`; |
||||||
|
break; |
||||||
|
case 'decimal': |
||||||
|
str += `DataTypes.DECIMAL`; |
||||||
|
break; |
||||||
|
case 'double': |
||||||
|
str += `"DOUBLE(${column.dtxp},${column.ns})"`; |
||||||
|
break; |
||||||
|
case 'real': |
||||||
|
str += `DataTypes.FLOAT`; |
||||||
|
break; |
||||||
|
case 'bit': |
||||||
|
str += `DataTypes.BOOLEAN`; |
||||||
|
break; |
||||||
|
case 'boolean': |
||||||
|
str += `DataTypes.STRING(45)`; |
||||||
|
break; |
||||||
|
case 'serial': |
||||||
|
str += `DataTypes.BIGINT`; |
||||||
|
break; |
||||||
|
case 'date': |
||||||
|
str += `DataTypes.DATEONLY`; |
||||||
|
break; |
||||||
|
case 'datetime': |
||||||
|
str += `DataTypes.DATE`; |
||||||
|
break; |
||||||
|
case 'timestamp': |
||||||
|
str += `DataTypes.DATE`; |
||||||
|
break; |
||||||
|
case 'time': |
||||||
|
str += `DataTypes.TIME`; |
||||||
|
break; |
||||||
|
case 'year': |
||||||
|
str += `"YEAR"`; |
||||||
|
break; |
||||||
|
case 'char': |
||||||
|
str += `DataTypes.CHAR(${column.dtxp})`; |
||||||
|
break; |
||||||
|
case 'varchar': |
||||||
|
str += `DataTypes.STRING(${column.dtxp})`; |
||||||
|
break; |
||||||
|
case 'nchar': |
||||||
|
str += `DataTypes.CHAR(${column.dtxp})`; |
||||||
|
break; |
||||||
|
case 'text': |
||||||
|
str += `DataTypes.TEXT`; |
||||||
|
break; |
||||||
|
case 'tinytext': |
||||||
|
str += `DataTypes.TEXT`; |
||||||
|
break; |
||||||
|
case 'mediumtext': |
||||||
|
str += `DataTypes.TEXT`; |
||||||
|
break; |
||||||
|
case 'longtext': |
||||||
|
str += `DataTypes.TEXT`; |
||||||
|
break; |
||||||
|
case 'binary': |
||||||
|
str += `"BINARY(${column.dtxp})"`; |
||||||
|
break; |
||||||
|
case 'varbinary': |
||||||
|
str += `"VARBINARY(${column.dtxp})"`; |
||||||
|
break; |
||||||
|
case 'blob': |
||||||
|
str += `"BLOB"`; |
||||||
|
break; |
||||||
|
case 'tinyblob': |
||||||
|
str += `"TINYBLOB"`; |
||||||
|
break; |
||||||
|
case 'mediumblob': |
||||||
|
str += `"MEDIUMBLOB"`; |
||||||
|
break; |
||||||
|
case 'longblob': |
||||||
|
str += `"LONGBLOB"`; |
||||||
|
break; |
||||||
|
case 'enum': |
||||||
|
str += `DataTypes.ENUM(${column.dtxp})`; |
||||||
|
break; |
||||||
|
case 'set': |
||||||
|
str += `"SET(${column.dtxp})"`; |
||||||
|
break; |
||||||
|
case 'geometry': |
||||||
|
str += `DataTypes.GEOMETRY`; |
||||||
|
break; |
||||||
|
case 'point': |
||||||
|
str += `"POINT"`; |
||||||
|
break; |
||||||
|
case 'linestring': |
||||||
|
str += `"LINESTRING"`; |
||||||
|
break; |
||||||
|
case 'polygon': |
||||||
|
str += `"POLYGON"`; |
||||||
|
break; |
||||||
|
case 'multipoint': |
||||||
|
str += `"MULTIPOINT"`; |
||||||
|
break; |
||||||
|
case 'multilinestring': |
||||||
|
str += `"MULTILINESTRING"`; |
||||||
|
break; |
||||||
|
case 'multipolygon': |
||||||
|
str += `"MULTIPOLYGON"`; |
||||||
|
break; |
||||||
|
case 'json': |
||||||
|
str += `DataTypes.JSON`; |
||||||
|
break; |
||||||
|
default: |
||||||
|
str += `"${column.dt}"`; |
||||||
|
break; |
||||||
|
} |
||||||
|
return str; |
||||||
|
} |
||||||
|
|
||||||
|
_sequelizeGetDefault(column) { |
||||||
|
let str = ''; |
||||||
|
switch (column.dt) { |
||||||
|
case 'int': |
||||||
|
str += `'${column.cdf}'`; |
||||||
|
break; |
||||||
|
case 'tinyint': |
||||||
|
str += `'${column.cdf}'`; |
||||||
|
break; |
||||||
|
case 'smallint': |
||||||
|
str += `'${column.cdf}'`; |
||||||
|
break; |
||||||
|
case 'mediumint': |
||||||
|
str += `'${column.cdf}'`; |
||||||
|
break; |
||||||
|
case 'bigint': |
||||||
|
str += `'${column.cdf}'`; |
||||||
|
break; |
||||||
|
case 'float': |
||||||
|
str += `'${column.cdf}'`; |
||||||
|
break; |
||||||
|
case 'decimal': |
||||||
|
str += `'${column.cdf}'`; |
||||||
|
break; |
||||||
|
case 'double': |
||||||
|
str += `'${column.cdf}'`; |
||||||
|
break; |
||||||
|
case 'real': |
||||||
|
str += `'${column.cdf}'`; |
||||||
|
break; |
||||||
|
case 'bit': |
||||||
|
str += column.cdf ? column.cdf.split('b')[1] : column.cdf; |
||||||
|
break; |
||||||
|
case 'boolean': |
||||||
|
str += column.cdf; |
||||||
|
break; |
||||||
|
case 'serial': |
||||||
|
str += column.cdf; |
||||||
|
break; |
||||||
|
case 'date': |
||||||
|
str += `sequelize.literal('${column.cdf_sequelize}')`; |
||||||
|
break; |
||||||
|
case 'datetime': |
||||||
|
str += `sequelize.literal('${column.cdf_sequelize}')`; |
||||||
|
break; |
||||||
|
case 'timestamp': |
||||||
|
str += `sequelize.literal('${column.cdf_sequelize}')`; |
||||||
|
break; |
||||||
|
case 'time': |
||||||
|
str += `'${column.cdf}'`; |
||||||
|
break; |
||||||
|
case 'year': |
||||||
|
str += `'${column.cdf}'`; |
||||||
|
break; |
||||||
|
case 'char': |
||||||
|
str += `'${column.cdf}'`; |
||||||
|
break; |
||||||
|
case 'varchar': |
||||||
|
str += `'${column.cdf}'`; |
||||||
|
break; |
||||||
|
case 'nchar': |
||||||
|
str += `'${column.cdf}'`; |
||||||
|
break; |
||||||
|
case 'text': |
||||||
|
str += column.cdf; |
||||||
|
break; |
||||||
|
case 'tinytext': |
||||||
|
str += column.cdf; |
||||||
|
break; |
||||||
|
case 'mediumtext': |
||||||
|
str += column.cdf; |
||||||
|
break; |
||||||
|
case 'longtext': |
||||||
|
str += column.cdf; |
||||||
|
break; |
||||||
|
case 'binary': |
||||||
|
str += column.cdf; |
||||||
|
break; |
||||||
|
case 'varbinary': |
||||||
|
str += column.cdf; |
||||||
|
break; |
||||||
|
case 'blob': |
||||||
|
str += column.cdf; |
||||||
|
break; |
||||||
|
case 'tinyblob': |
||||||
|
str += column.cdf; |
||||||
|
break; |
||||||
|
case 'mediumblob': |
||||||
|
str += column.cdf; |
||||||
|
break; |
||||||
|
case 'longblob': |
||||||
|
str += column.cdf; |
||||||
|
break; |
||||||
|
case 'enum': |
||||||
|
str += `'${column.cdf}'`; |
||||||
|
break; |
||||||
|
case 'set': |
||||||
|
str += `'${column.cdf}'`; |
||||||
|
break; |
||||||
|
case 'geometry': |
||||||
|
str += `'${column.cdf}'`; |
||||||
|
break; |
||||||
|
case 'point': |
||||||
|
str += `'${column.cdf}'`; |
||||||
|
break; |
||||||
|
case 'linestring': |
||||||
|
str += `'${column.cdf}'`; |
||||||
|
break; |
||||||
|
case 'polygon': |
||||||
|
str += `'${column.cdf}'`; |
||||||
|
break; |
||||||
|
case 'multipoint': |
||||||
|
str += `'${column.cdf}'`; |
||||||
|
break; |
||||||
|
case 'multilinestring': |
||||||
|
str += `'${column.cdf}'`; |
||||||
|
break; |
||||||
|
case 'multipolygon': |
||||||
|
str += `'${column.cdf}'`; |
||||||
|
break; |
||||||
|
case 'json': |
||||||
|
str += column.cdf; |
||||||
|
break; |
||||||
|
} |
||||||
|
return str; |
||||||
|
} |
||||||
|
|
||||||
|
/* getXcColumnsObject(args) { |
||||||
|
const columnsArr = []; |
||||||
|
|
||||||
|
for (const column of args.columns) { |
||||||
|
const columnObj = { |
||||||
|
validate: { |
||||||
|
func: [], |
||||||
|
args: [], |
||||||
|
msg: [] |
||||||
|
}, |
||||||
|
cn: column.cn, |
||||||
|
_cn: column._cn || column.cn, |
||||||
|
type: this._getAbstractType(column), |
||||||
|
dt: column.dt, |
||||||
|
uidt: column.uidt || this._getUIDataType(column), |
||||||
|
uip: column.uip, |
||||||
|
uicn: column.uicn, |
||||||
|
...column |
||||||
|
}; |
||||||
|
|
||||||
|
if (column.rqd) { |
||||||
|
columnObj.rqd = column.rqd; |
||||||
|
} |
||||||
|
|
||||||
|
if (column.cdf) { |
||||||
|
columnObj.default = column.cdf; |
||||||
|
columnObj.columnDefault = column.cdf; |
||||||
|
} |
||||||
|
|
||||||
|
if (column.un) { |
||||||
|
columnObj.un = column.un; |
||||||
|
} |
||||||
|
|
||||||
|
if (column.pk) { |
||||||
|
columnObj.pk = column.pk; |
||||||
|
} |
||||||
|
|
||||||
|
if (column.ai) { |
||||||
|
columnObj.ai = column.ai; |
||||||
|
} |
||||||
|
|
||||||
|
if (column.dtxp) { |
||||||
|
columnObj.dtxp = column.dtxp; |
||||||
|
} |
||||||
|
|
||||||
|
if (column.dtxs) { |
||||||
|
columnObj.dtxs = column.dtxs; |
||||||
|
} |
||||||
|
|
||||||
|
columnsArr.push(columnObj); |
||||||
|
} |
||||||
|
|
||||||
|
this.mapDefaultDisplayValue(columnsArr); |
||||||
|
return columnsArr; |
||||||
|
}*/ |
||||||
|
|
||||||
|
/* getObject() { |
||||||
|
return { |
||||||
|
tn: this.ctx.tn, |
||||||
|
_tn: this.ctx._tn, |
||||||
|
columns: this.getXcColumnsObject(this.ctx), |
||||||
|
pks: [], |
||||||
|
hasMany: this.ctx.hasMany, |
||||||
|
belongsTo: this.ctx.belongsTo, |
||||||
|
dbType: this.ctx.dbType, |
||||||
|
type: this.ctx.type, |
||||||
|
} |
||||||
|
|
||||||
|
}*/ |
||||||
|
} |
||||||
|
|
||||||
|
export default ModelXcMetaDatabricks; |
@ -0,0 +1,30 @@ |
|||||||
|
import type { Knex } from 'knex'; |
||||||
|
import { MetaTable } from '~/utils/globals'; |
||||||
|
|
||||||
|
const up = async (knex: Knex) => { |
||||||
|
await knex.schema.createTable(MetaTable.EXTENSIONS, (table) => { |
||||||
|
table.string('id', 20).primary(); |
||||||
|
|
||||||
|
table.string('base_id', 20).index(); |
||||||
|
|
||||||
|
table.string('fk_user_id', 20); |
||||||
|
|
||||||
|
table.string('extension_id'); |
||||||
|
|
||||||
|
table.string('title'); |
||||||
|
|
||||||
|
table.text('kv_store'); |
||||||
|
|
||||||
|
table.text('meta'); |
||||||
|
|
||||||
|
table.float('order'); |
||||||
|
|
||||||
|
table.timestamps(true, true); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
const down = async (knex: Knex) => { |
||||||
|
await knex.schema.dropTable(MetaTable.EXTENSIONS); |
||||||
|
}; |
||||||
|
|
||||||
|
export { up, down }; |
@ -0,0 +1,161 @@ |
|||||||
|
import { prepareForDb, prepareForResponse } from '~/utils/modelUtils'; |
||||||
|
import Noco from '~/Noco'; |
||||||
|
import { extractProps } from '~/helpers/extractProps'; |
||||||
|
import { |
||||||
|
CacheDelDirection, |
||||||
|
CacheGetType, |
||||||
|
CacheScope, |
||||||
|
MetaTable, |
||||||
|
} from '~/utils/globals'; |
||||||
|
import NocoCache from '~/cache/NocoCache'; |
||||||
|
|
||||||
|
export default class Extension { |
||||||
|
id?: string; |
||||||
|
base_id?: string; |
||||||
|
fk_user_id?: string; |
||||||
|
extension_id?: string; |
||||||
|
title?: string; |
||||||
|
kv_store?: any; |
||||||
|
meta?: any; |
||||||
|
order?: number; |
||||||
|
|
||||||
|
constructor(extension: Partial<Extension>) { |
||||||
|
Object.assign(this, extension); |
||||||
|
} |
||||||
|
|
||||||
|
public static async get(extensionId: string, ncMeta = Noco.ncMeta) { |
||||||
|
let extension = await NocoCache.get( |
||||||
|
`${CacheScope.EXTENSION}:${extensionId}`, |
||||||
|
CacheGetType.TYPE_OBJECT, |
||||||
|
); |
||||||
|
|
||||||
|
if (!extension) { |
||||||
|
extension = await ncMeta.metaGet2( |
||||||
|
null, |
||||||
|
null, |
||||||
|
MetaTable.EXTENSIONS, |
||||||
|
extensionId, |
||||||
|
); |
||||||
|
|
||||||
|
if (extension) { |
||||||
|
extension = prepareForResponse(extension, ['kv_store', 'meta']); |
||||||
|
NocoCache.set(`${CacheScope.EXTENSION}:${extensionId}`, extension); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return extension && new Extension(extension); |
||||||
|
} |
||||||
|
|
||||||
|
static async list(baseId: string, ncMeta = Noco.ncMeta) { |
||||||
|
const cachedList = await NocoCache.getList(CacheScope.EXTENSION, [baseId]); |
||||||
|
let { list: extensionList } = cachedList; |
||||||
|
const { isNoneList } = cachedList; |
||||||
|
if (!isNoneList && !extensionList.length) { |
||||||
|
extensionList = await ncMeta.metaList(null, null, MetaTable.EXTENSIONS, { |
||||||
|
condition: { |
||||||
|
base_id: baseId, |
||||||
|
}, |
||||||
|
orderBy: { |
||||||
|
created_at: 'asc', |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
if (extensionList) { |
||||||
|
extensionList = extensionList.map((extension) => |
||||||
|
prepareForResponse(extension, ['kv_store', 'meta']), |
||||||
|
); |
||||||
|
NocoCache.setList(CacheScope.EXTENSION, [baseId], extensionList); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return extensionList |
||||||
|
?.sort((a, b) => (a?.order ?? Infinity) - (b?.order ?? Infinity)) |
||||||
|
.map((extension) => new Extension(extension)); |
||||||
|
} |
||||||
|
|
||||||
|
public static async insert( |
||||||
|
extension: Partial<Extension>, |
||||||
|
ncMeta = Noco.ncMeta, |
||||||
|
) { |
||||||
|
const insertObj = extractProps(extension, [ |
||||||
|
'id', |
||||||
|
'base_id', |
||||||
|
'fk_user_id', |
||||||
|
'extension_id', |
||||||
|
'title', |
||||||
|
'kv_store', |
||||||
|
'meta', |
||||||
|
'order', |
||||||
|
]); |
||||||
|
|
||||||
|
if (insertObj.order === null || insertObj.order === undefined) { |
||||||
|
insertObj.order = await ncMeta.metaGetNextOrder(MetaTable.EXTENSIONS, { |
||||||
|
base_id: insertObj.base_id, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
const { id } = await ncMeta.metaInsert2( |
||||||
|
null, |
||||||
|
null, |
||||||
|
MetaTable.EXTENSIONS, |
||||||
|
prepareForDb(insertObj, ['kv_store', 'meta']), |
||||||
|
); |
||||||
|
|
||||||
|
return this.get(id, ncMeta).then(async (res) => { |
||||||
|
await NocoCache.appendToList( |
||||||
|
CacheScope.EXTENSION, |
||||||
|
[extension.base_id], |
||||||
|
`${CacheScope.EXTENSION}:${id}`, |
||||||
|
); |
||||||
|
return res; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
public static async update( |
||||||
|
extensionId: string, |
||||||
|
extension: Partial<Extension>, |
||||||
|
ncMeta = Noco.ncMeta, |
||||||
|
) { |
||||||
|
const updateObj = extractProps(extension, [ |
||||||
|
'base_id', |
||||||
|
'fk_user_id', |
||||||
|
'extension_id', |
||||||
|
'title', |
||||||
|
'kv_store', |
||||||
|
'meta', |
||||||
|
'order', |
||||||
|
]); |
||||||
|
|
||||||
|
// set meta
|
||||||
|
await ncMeta.metaUpdate( |
||||||
|
null, |
||||||
|
null, |
||||||
|
MetaTable.EXTENSIONS, |
||||||
|
prepareForDb(updateObj, ['kv_store', 'meta']), |
||||||
|
extensionId, |
||||||
|
); |
||||||
|
|
||||||
|
await NocoCache.update( |
||||||
|
`${CacheScope.EXTENSION}:${extensionId}`, |
||||||
|
prepareForResponse(updateObj, ['kv_store', 'meta']), |
||||||
|
); |
||||||
|
|
||||||
|
return this.get(extensionId, ncMeta); |
||||||
|
} |
||||||
|
|
||||||
|
static async delete(extensionId: any, ncMeta = Noco.ncMeta) { |
||||||
|
const res = await ncMeta.metaDelete( |
||||||
|
null, |
||||||
|
null, |
||||||
|
MetaTable.EXTENSIONS, |
||||||
|
extensionId, |
||||||
|
); |
||||||
|
|
||||||
|
await NocoCache.deepDel( |
||||||
|
`${CacheScope.EXTENSION}:${extensionId}`, |
||||||
|
CacheDelDirection.CHILD_TO_PARENT, |
||||||
|
); |
||||||
|
|
||||||
|
return res; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
import { Test } from '@nestjs/testing'; |
||||||
|
import { ExtensionsService } from './extensions.service'; |
||||||
|
import type { TestingModule } from '@nestjs/testing'; |
||||||
|
|
||||||
|
describe('ExtensionsService', () => { |
||||||
|
let service: ExtensionsService; |
||||||
|
|
||||||
|
beforeEach(async () => { |
||||||
|
const module: TestingModule = await Test.createTestingModule({ |
||||||
|
providers: [ExtensionsService], |
||||||
|
}).compile(); |
||||||
|
|
||||||
|
service = module.get<ExtensionsService>(ExtensionsService); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should be defined', () => { |
||||||
|
expect(service).toBeDefined(); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,74 @@ |
|||||||
|
import { Injectable } from '@nestjs/common'; |
||||||
|
import { AppEvents, type ExtensionReqType } from 'nocodb-sdk'; |
||||||
|
import type { NcRequest } from '~/interface/config'; |
||||||
|
import { AppHooksService } from '~/services/app-hooks/app-hooks.service'; |
||||||
|
import { validatePayload } from '~/helpers'; |
||||||
|
import { Extension } from '~/models'; |
||||||
|
|
||||||
|
@Injectable() |
||||||
|
export class ExtensionsService { |
||||||
|
constructor(private readonly appHooksService: AppHooksService) {} |
||||||
|
|
||||||
|
async extensionList(param: { baseId: string }) { |
||||||
|
return await Extension.list(param.baseId); |
||||||
|
} |
||||||
|
|
||||||
|
async extensionRead(param: { extensionId: string }) { |
||||||
|
return await Extension.get(param.extensionId); |
||||||
|
} |
||||||
|
|
||||||
|
async extensionCreate(param: { |
||||||
|
extension: ExtensionReqType; |
||||||
|
req: NcRequest; |
||||||
|
}) { |
||||||
|
validatePayload( |
||||||
|
'swagger.json#/components/schemas/ExtensionReq', |
||||||
|
param.extension, |
||||||
|
); |
||||||
|
|
||||||
|
const res = await Extension.insert({ |
||||||
|
...param.extension, |
||||||
|
fk_user_id: param.req.user.id, |
||||||
|
}); |
||||||
|
|
||||||
|
this.appHooksService.emit(AppEvents.EXTENSION_CREATE, { |
||||||
|
extensionId: res.id, |
||||||
|
extension: param.extension, |
||||||
|
req: param.req, |
||||||
|
}); |
||||||
|
|
||||||
|
return res; |
||||||
|
} |
||||||
|
|
||||||
|
async extensionUpdate(param: { |
||||||
|
extensionId: string; |
||||||
|
extension: ExtensionReqType; |
||||||
|
req: NcRequest; |
||||||
|
}) { |
||||||
|
validatePayload( |
||||||
|
'swagger.json#/components/schemas/ExtensionReq', |
||||||
|
param.extension, |
||||||
|
); |
||||||
|
|
||||||
|
const res = await Extension.update(param.extensionId, param.extension); |
||||||
|
|
||||||
|
this.appHooksService.emit(AppEvents.EXTENSION_UPDATE, { |
||||||
|
extensionId: param.extensionId, |
||||||
|
extension: param.extension, |
||||||
|
req: param.req, |
||||||
|
}); |
||||||
|
|
||||||
|
return res; |
||||||
|
} |
||||||
|
|
||||||
|
async extensionDelete(param: { extensionId: string; req: NcRequest }) { |
||||||
|
const res = await Extension.delete(param.extensionId); |
||||||
|
|
||||||
|
this.appHooksService.emit(AppEvents.EXTENSION_DELETE, { |
||||||
|
extensionId: param.extensionId, |
||||||
|
req: param.req, |
||||||
|
}); |
||||||
|
|
||||||
|
return res; |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue