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