Fix/metadata object and settings post merge (#2269)

* WIP

* WIP2

* Seed views standard objects

* Migrate views to the new data model

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Charles Bochet
2023-10-28 12:25:43 +02:00
committed by GitHub
parent afd4b7c634
commit b591023eb3
30 changed files with 609 additions and 208 deletions

View File

@ -10,7 +10,6 @@ import { TableContext } from '@/ui/data/data-table/contexts/TableContext';
import { useUpsertDataTableItem } from '@/ui/data/data-table/hooks/useUpsertDataTableItem';
import { TableOptionsDropdown } from '@/ui/data/data-table/options/components/TableOptionsDropdown';
import { ViewBar } from '@/views/components/ViewBar';
import { ViewBarEffect } from '@/views/components/ViewBarEffect';
import { useViewFields } from '@/views/hooks/internal/useViewFields';
import { useView } from '@/views/hooks/useView';
import { ViewScope } from '@/views/scopes/ViewScope';
@ -78,12 +77,7 @@ export const CompanyTable = () => {
`;
return (
<ViewScope
viewScopeId={tableViewScopeId}
onViewFieldsChange={() => {}}
onViewSortsChange={() => {}}
onViewFiltersChange={() => {}}
>
<ViewScope viewScopeId={tableViewScopeId}>
<StyledContainer>
<TableContext.Provider
value={{
@ -93,14 +87,11 @@ export const CompanyTable = () => {
},
}}
>
<ViewBarEffect />
<ViewBar
optionsDropdownButton={<TableOptionsDropdown onImport={onImport} />}
optionsDropdownScopeId="table-dropdown-option"
/>
<CompanyTableEffect />
<DataTableEffect
getRequestResultKey="companies"
useGetRequest={useGetCompaniesQuery}

View File

@ -1,10 +1,4 @@
import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useRecoilCallback } from 'recoil';
import { TableRecoilScopeContext } from '@/ui/data/data-table/states/recoil-scope-contexts/TableRecoilScopeContext';
import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId';
import { currentViewIdScopedState } from '@/views/states/currentViewIdScopedState';
import { useFindManyObjects } from '../hooks/useFindManyObjects';
import { useSetObjectDataTableData } from '../hooks/useSetDataTableData';
@ -15,6 +9,7 @@ export type ObjectDataTableEffectProps = Pick<
'objectNamePlural'
>;
// TODO: merge in a single effect component
export const ObjectDataTableEffect = ({
objectNamePlural,
}: ObjectDataTableEffectProps) => {
@ -32,32 +27,5 @@ export const ObjectDataTableEffect = ({
}
}, [objects, setDataTableData, loading]);
const [searchParams] = useSearchParams();
const tableRecoilScopeId = useRecoilScopeId(TableRecoilScopeContext);
const handleViewSelect = useRecoilCallback(
({ set, snapshot }) =>
(viewId: string) => {
const currentView = snapshot
.getLoadable(
currentViewIdScopedState({ scopeId: tableRecoilScopeId }),
)
.getValue();
if (currentView === viewId) {
return;
}
set(currentViewIdScopedState({ scopeId: tableRecoilScopeId }), viewId);
},
[tableRecoilScopeId],
);
useEffect(() => {
const viewId = searchParams.get('view');
if (viewId) {
handleViewSelect(viewId);
}
}, [handleViewSelect, searchParams, objectNamePlural]);
return <></>;
};

View File

@ -1,10 +1,29 @@
import styled from '@emotion/styled';
import { useRecoilCallback } from 'recoil';
import { DataTable } from '@/ui/data/data-table/components/DataTable';
import { TableContext } from '@/ui/data/data-table/contexts/TableContext';
import { TableOptionsDropdown } from '@/ui/data/data-table/options/components/TableOptionsDropdown';
import { tableColumnsScopedState } from '@/ui/data/data-table/states/tableColumnsScopedState';
import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
import { ViewBar } from '@/views/components/ViewBar';
import { useViewFields } from '@/views/hooks/internal/useViewFields';
import { useView } from '@/views/hooks/useView';
import { ViewScope } from '@/views/scopes/ViewScope';
import { useUpdateOneObject } from '../hooks/useUpdateOneObject';
import { MetadataObjectIdentifier } from '../types/MetadataObjectIdentifier';
import { ObjectDataTableEffect } from './ObjectDataTableEffect';
import { ObjectTableEffect } from './ObjectTableEffect';
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
overflow: auto;
`;
export type ObjectTableProps = Pick<
MetadataObjectIdentifier,
@ -16,6 +35,11 @@ export const ObjectTable = ({ objectNamePlural }: ObjectTableProps) => {
objectNamePlural,
});
const viewScopeId = objectNamePlural ?? '';
const { persistViewFields } = useViewFields(viewScopeId);
const { setCurrentViewFields } = useView({ viewScopeId: viewScopeId });
const updateEntity = ({
variables,
}: {
@ -32,17 +56,38 @@ export const ObjectTable = ({ objectNamePlural }: ObjectTableProps) => {
});
};
const updateTableColumns = useRecoilCallback(
({ set, snapshot }) =>
(viewFields: ColumnDefinition<FieldMetadata>[]) => {
set(tableColumnsScopedState(viewScopeId), viewFields);
},
);
return (
<TableContext.Provider
value={{
onColumnsChange: () => {
//
},
<ViewScope
viewScopeId={viewScopeId}
onViewFieldsChange={(viewFields) => {
// updateTableColumns(viewFields);
}}
>
<ObjectDataTableEffect objectNamePlural={objectNamePlural} />
<DataTable updateEntityMutation={updateEntity} />
</TableContext.Provider>
<StyledContainer>
<TableContext.Provider
value={{
onColumnsChange: (columns) => {
// setCurrentViewFields?.(columns);
// persistViewFields(columns);
},
}}
>
<ViewBar
optionsDropdownButton={<TableOptionsDropdown />}
optionsDropdownScopeId="table-dropdown-option"
/>
<ObjectTableEffect />
<ObjectDataTableEffect objectNamePlural={objectNamePlural} />
<DataTable updateEntityMutation={updateEntity} />
</TableContext.Provider>
</StyledContainer>
</ViewScope>
);
};

View File

@ -0,0 +1,86 @@
import { useEffect } from 'react';
import { useSetRecoilState } from 'recoil';
import { availableTableColumnsScopedState } from '@/ui/data/data-table/states/availableTableColumnsScopedState';
import { useView } from '@/views/hooks/useView';
import { ViewType } from '~/generated/graphql';
import { useMetadataObjectInContext } from '../hooks/useMetadataObjectInContext';
export const ObjectTableEffect = () => {
console.log('ObjectTableEffect');
const {
setAvailableSorts,
setAvailableFilters,
setAvailableFields,
setViewType,
setViewObjectId,
} = useView();
// const [, setTableColumns] = useRecoilScopedState(
// tableColumnsScopedState,
// TableRecoilScopeContext,
// );
// const [, setTableSorts] = useRecoilScopedState(
// tableSortsScopedState,
// TableRecoilScopeContext,
// );
// const [, setTableFilters] = useRecoilScopedState(
// tableFiltersScopedState,
// TableRecoilScopeContext,
// );
const { columnDefinitions, objectNamePlural } = useMetadataObjectInContext();
const setAvailableTableColumns = useSetRecoilState(
availableTableColumnsScopedState(objectNamePlural ?? ''),
);
useEffect(() => {
setAvailableSorts?.([]); // TODO: extract from metadata fields
setAvailableFilters?.([]); // TODO: extract from metadata fields
setAvailableFields?.(columnDefinitions);
setViewObjectId?.(objectNamePlural);
setViewType?.(ViewType.Table);
setAvailableTableColumns(columnDefinitions);
}, [
setAvailableFields,
setAvailableFilters,
setAvailableSorts,
setAvailableTableColumns,
setViewObjectId,
setViewType,
columnDefinitions,
objectNamePlural,
]);
// useEffect(() => {
// if (currentViewFields) {
// setTableColumns([...currentViewFields].sort((a, b) => a.index - b.index));
// }
// }, [currentViewFields, setTableColumns]);
// useEffect(() => {
// if (currentViewSorts) {
// setTableSorts(currentViewSorts);
// }
// }, [currentViewFields, currentViewSorts, setTableColumns, setTableSorts]);
// useEffect(() => {
// if (currentViewFilters) {
// setTableFilters(currentViewFilters);
// }
// }, [
// currentViewFields,
// currentViewFilters,
// setTableColumns,
// setTableFilters,
// setTableSorts,
// ]);
return <></>;
};

View File

@ -38,6 +38,10 @@ export const useFindManyMetadataObjects = () => {
},
);
},
onCompleted: (data) => {
// eslint-disable-next-line no-console
//console.log('useFindManyMetadataObjects data : ', data);
},
},
);

View File

@ -15,7 +15,14 @@ export const useFindManyObjects = <
ObjectType extends { id: string } & Record<string, any>,
>({
objectNamePlural,
}: Pick<MetadataObjectIdentifier, 'objectNamePlural'>) => {
filter,
orderBy,
onCompleted,
}: Pick<MetadataObjectIdentifier, 'objectNamePlural'> & {
filter?: any;
orderBy?: any;
onCompleted?: (data: any) => void;
}) => {
const { foundMetadataObject, objectNotFoundInMetadata, findManyQuery } =
useFindOneMetadataObject({
objectNamePlural,
@ -27,6 +34,12 @@ export const useFindManyObjects = <
findManyQuery,
{
skip: !foundMetadataObject,
variables: {
filter: filter ?? {},
orderBy: orderBy ?? {},
},
onCompleted: (data) =>
objectNamePlural && onCompleted?.(data[objectNamePlural]),
onError: (error) => {
// eslint-disable-next-line no-console
console.error(

View File

@ -1,8 +1,5 @@
import { PaginatedObjectTypeResults } from './PaginatedObjectTypeResults';
export type PaginatedObjectType<ObjectType extends { id: string }> = {
[objectNamePlural: string]: {
edges: {
node: ObjectType;
cursor: string;
}[];
};
[objectNamePlural: string]: PaginatedObjectTypeResults<ObjectType>;
};

View File

@ -0,0 +1,6 @@
export type PaginatedObjectTypeResults<ObjectType extends { id: string }> = {
edges: {
node: ObjectType;
cursor: string;
}[];
};

View File

@ -1,5 +1,7 @@
import { gql } from '@apollo/client';
import { capitalize } from '~/utils/string/capitalize';
import { MetadataObject } from '../types/MetadataObject';
import { mapFieldMetadataToGraphQLQuery } from './mapFieldMetadataToGraphQLQuery';
@ -12,8 +14,10 @@ export const generateFindManyCustomObjectsQuery = ({
_fromCursor?: string;
}) => {
return gql`
query FindMany${metadataObject.namePlural} {
${metadataObject.namePlural}{
query FindMany${metadataObject.namePlural}($filter: ${capitalize(
metadataObject.nameSingular,
)}FilterInput, $orderBy: ${capitalize(metadataObject.nameSingular)}OrderBy) {
${metadataObject.namePlural}(filter: $filter, orderBy: $orderBy){
edges {
node {
id

View File

@ -14,6 +14,7 @@ import { ViewsHotkeyScope } from '../types/ViewsHotkeyScope';
import { UpdateViewButtonGroup } from './UpdateViewButtonGroup';
import { ViewBarDetails } from './ViewBarDetails';
import { ViewBarEffect } from './ViewBarEffect';
import { ViewsDropdownButton } from './ViewsDropdownButton';
export type ViewBarProps = {
@ -43,6 +44,7 @@ export const ViewBar = ({
availableSorts={availableSorts}
onSortAdd={upsertViewSort}
>
<ViewBarEffect />
<TopBar
className={className}
leftComponent={

View File

@ -2,16 +2,15 @@ import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useRecoilCallback } from 'recoil';
import { useFindManyObjects } from '@/metadata/hooks/useFindManyObjects';
import { PaginatedObjectTypeResults } from '@/metadata/types/PaginatedObjectTypeResults';
import { ColumnDefinition } from '@/ui/data/data-table/types/ColumnDefinition';
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
import { Filter } from '@/ui/data/filter/types/Filter';
import { Sort } from '@/ui/data/sort/types/Sort';
import {
SortOrder,
useGetViewFieldsQuery,
useGetViewFiltersQuery,
useGetViewSortsQuery,
useGetViewsQuery,
} from '~/generated/graphql';
import { assertNotNull } from '~/utils/assert';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
@ -25,6 +24,8 @@ import { savedViewFieldsScopedFamilyState } from '../states/savedViewFieldsScope
import { savedViewFiltersScopedFamilyState } from '../states/savedViewFiltersScopedFamilyState';
import { savedViewSortsScopedFamilyState } from '../states/savedViewSortsScopedFamilyState';
import { viewsScopedState } from '../states/viewsScopedState';
import { View } from '../types/View';
import { ViewField } from '../types/ViewField';
export const ViewBarEffect = () => {
const {
@ -45,80 +46,78 @@ export const ViewBarEffect = () => {
const { viewType, viewObjectId } = useViewInternalStates(viewScopeId);
useGetViewFieldsQuery({
skip: !currentViewId,
variables: {
orderBy: { index: SortOrder.Asc },
where: {
viewId: { equals: currentViewId },
},
},
onCompleted: useRecoilCallback(({ snapshot }) => async (data) => {
const availableFields = snapshot
.getLoadable(availableFieldsScopedState({ scopeId: viewScopeId }))
.getValue();
useFindManyObjects({
objectNamePlural: 'viewFieldsV2',
filter: { viewId: { eq: currentViewId } },
onCompleted: useRecoilCallback(
({ snapshot }) =>
async (data: PaginatedObjectTypeResults<ViewField>) => {
const availableFields = snapshot
.getLoadable(availableFieldsScopedState({ scopeId: viewScopeId }))
.getValue();
if (!availableFields || !currentViewId) {
return;
}
if (!availableFields || !currentViewId) {
return;
}
const savedViewFields = snapshot
.getLoadable(
savedViewFieldsScopedFamilyState({
scopeId: viewScopeId,
familyKey: currentViewId,
}),
)
.getValue();
const savedViewFields = snapshot
.getLoadable(
savedViewFieldsScopedFamilyState({
scopeId: viewScopeId,
familyKey: currentViewId,
}),
)
.getValue();
const queriedViewFields = data.viewFields
.map<ColumnDefinition<FieldMetadata> | null>((viewField) => {
const columnDefinition = availableFields.find(
({ key }) => viewField.key === key,
);
const queriedViewFields = data.edges
.map<ColumnDefinition<FieldMetadata> | null>((viewField) => {
const columnDefinition = availableFields.find(
({ key }) => viewField.node.fieldId === key,
);
return columnDefinition
? {
...columnDefinition,
key: viewField.key,
name: viewField.name,
index: viewField.index,
size: viewField.size ?? columnDefinition.size,
isVisible: viewField.isVisible,
}
: null;
})
.filter<ColumnDefinition<FieldMetadata>>(assertNotNull);
return columnDefinition
? {
...columnDefinition,
key: viewField.node.fieldId,
name: viewField.node.fieldId,
index: viewField.node.position,
size: viewField.node.size ?? columnDefinition.size,
isVisible: viewField.node.isVisible,
}
: null;
})
.filter<ColumnDefinition<FieldMetadata>>(assertNotNull);
if (!isDeeplyEqual(savedViewFields, queriedViewFields)) {
setCurrentViewFields?.(queriedViewFields);
setSavedViewFields?.(queriedViewFields);
}
}),
if (!isDeeplyEqual(savedViewFields, queriedViewFields)) {
setCurrentViewFields?.(queriedViewFields);
setSavedViewFields?.(queriedViewFields);
}
},
),
});
useGetViewsQuery({
variables: {
where: {
objectId: { equals: viewObjectId },
type: { equals: viewType },
},
},
onCompleted: useRecoilCallback(({ snapshot }) => async (data) => {
const nextViews = data.views.map((view) => ({
id: view.id,
name: view.name,
}));
const views = snapshot
.getLoadable(viewsScopedState({ scopeId: viewScopeId }))
.getValue();
useFindManyObjects({
objectNamePlural: 'viewsV2',
filter: { type: { eq: viewType }, objectId: { eq: viewObjectId } },
onCompleted: useRecoilCallback(
({ snapshot }) =>
async (data: PaginatedObjectTypeResults<View>) => {
const nextViews = data.edges.map((view) => ({
id: view.node.id,
name: view.node.name,
objectId: view.node.objectId,
}));
const views = snapshot
.getLoadable(viewsScopedState({ scopeId: viewScopeId }))
.getValue();
if (!isDeeplyEqual(views, nextViews)) setViews(nextViews);
if (!isDeeplyEqual(views, nextViews)) setViews(nextViews);
if (!nextViews.length) return;
if (!nextViews.length) return;
if (!currentViewId) return changeView(nextViews[0].id);
}),
if (!currentViewId) return setCurrentViewId(nextViews[0].id);
},
),
});
useGetViewSortsQuery({

View File

@ -17,29 +17,32 @@ export const useViews = (scopeId: string) => {
const [updateViewMutation] = useUpdateViewMutation();
const [deleteViewMutation] = useDeleteViewMutation();
const createView = useRecoilCallback(({ snapshot }) => async (view: View) => {
const viewObjectId = await snapshot
.getLoadable(viewObjectIdScopeState({ scopeId }))
.getValue();
const createView = useRecoilCallback(
({ snapshot }) =>
async (view: Pick<View, 'id' | 'name'>) => {
const viewObjectId = await snapshot
.getLoadable(viewObjectIdScopeState({ scopeId }))
.getValue();
const viewType = await snapshot
.getLoadable(viewTypeScopedState({ scopeId }))
.getValue();
const viewType = await snapshot
.getLoadable(viewTypeScopedState({ scopeId }))
.getValue();
if (!viewObjectId || !viewType) {
return;
}
await createViewMutation({
variables: {
data: {
...view,
objectId: viewObjectId,
type: viewType,
},
if (!viewObjectId || !viewType) {
return;
}
await createViewMutation({
variables: {
data: {
...view,
objectId: viewObjectId,
type: viewType,
},
},
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
});
},
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
});
});
);
const updateView = async (view: View) => {
await updateViewMutation({

View File

@ -1 +1 @@
export type View = { id: string; name: string };
export type View = { id: string; name: string; objectId: string };

View File

@ -0,0 +1,9 @@
export type ViewField = {
id: string;
objectId: string;
fieldId: string;
viewId: string;
position: number;
isVisible: boolean;
size: number;
};

View File

@ -24,17 +24,16 @@
"prisma:generate-gql-select": "node scripts/generate-model-select-map.js",
"prisma:generate-nest-graphql": "npx prisma generate --generator nestgraphql",
"prisma:generate": "yarn prisma:generate-client && yarn prisma:generate-gql-select && yarn prisma:generate-nest-graphql",
"prisma:reset": "npx prisma migrate reset && yarn prisma:generate",
"prisma:seed": "npx prisma db seed",
"prisma:migrate": "npx prisma migrate deploy",
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js",
"typeorm:migrate": "yarn typeorm migration:run -d ./src/metadata/metadata.datasource.ts",
"database:init": "yarn database:setup && yarn database:seed",
"database:setup": "npx ts-node ./scripts/setup-db.ts && yarn database:migrate",
"database:setup": "npx ts-node ./scripts/setup-db.ts && yarn database:migrate && yarn database:generate",
"database:truncate": "npx ts-node ./scripts/truncate-db.ts",
"database:migrate": "yarn typeorm:migrate && yarn prisma:migrate",
"database:generate": "yarn prisma:generate",
"database:seed": "yarn prisma:seed",
"database:seed": "yarn prisma:seed && yarn command tenant:sync-metadata -w twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419 && yarn command tenant:migrate -w twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419 && yarn command tenant:data-seed -w twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419",
"database:reset": "yarn database:truncate && yarn database:init",
"command": "node dist/src/command"
},

View File

@ -12,51 +12,4 @@ export const seedMetadata = async (prisma: PrismaClient) => {
'b37b2163-7f63-47a9-b1b3-6c7290ca9fb1', 'workspace_twenty_7icsva0r6s00mpcp6cwg4w4rd', 'postgres', 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419'
) ON CONFLICT DO NOTHING`,
);
await prisma.$queryRawUnsafe(`CREATE TABLE IF NOT EXISTS
workspace_twenty_7icsva0r6s00mpcp6cwg4w4rd.company(
"id" TEXT PRIMARY KEY,
"name" TEXT NOT NULL,
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
"deletedAt" TIMESTAMP WITH TIME ZONE,
"domainName" TEXT NOT NULL,
"address" TEXT NOT NULL,
"employees" INTEGER NOT NULL
);
`);
await prisma.$queryRawUnsafe(`INSERT INTO workspace_twenty_7icsva0r6s00mpcp6cwg4w4rd.company(
"id", "name", "domainName", "address", "employees"
)
VALUES (
'89bb825c-171e-4bcc-9cf7-43448d6fb278', 'Airbnb', 'airbnb.com', 'San Francisco', 5000
), (
'04b2e9f5-0713-40a5-8216-82802401d33e', 'Qonto', 'qonto.com', 'San Francisco', 800
), (
'118995f3-5d81-46d6-bf83-f7fd33ea6102', 'Facebook', 'facebook.com', 'San Francisco', 8000
), (
'460b6fb1-ed89-413a-b31a-962986e67bb4', 'Microsoft', 'microsoft.com', 'San Francisco', 800
), (
'fe256b39-3ec3-4fe3-8997-b76aa0bfa408', 'Linkedin', 'linkedin.com', 'San Francisco', 400
) ON CONFLICT DO NOTHING`);
await prisma.$queryRawUnsafe(`INSERT INTO metadata.object_metadata(
id, name_singular, name_plural, label_singular, label_plural, description, icon, target_table_name, is_custom, is_active, workspace_id, data_source_id
)
VALUES (
'ba391617-ee08-432f-9438-2e17df5ac279', 'companyV2', 'companiesV2', 'Company', 'Companies', 'Companies', 'IconBuilding', 'company', false, true, 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', 'b37b2163-7f63-47a9-b1b3-6c7290ca9fb1'
) ON CONFLICT DO NOTHING`);
await prisma.$queryRawUnsafe(`INSERT INTO metadata.field_metadata(
id, object_id, type, name, label, target_column_map, description, icon, enums, is_custom, is_active, is_nullable, workspace_id
)
VALUES (
'22f5906d-153f-448c-b254-28adce721dcd', 'ba391617-ee08-432f-9438-2e17df5ac279', 'text', 'name', 'Name', '{"value": "name"}', 'Name', 'IconUser', NULL, false, true, false, 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419'
), (
'19bfab29-1cbb-4ce2-9117-8540ac45a0f1', 'ba391617-ee08-432f-9438-2e17df5ac279', 'text', 'domainName', 'Domain Name', '{"value": "domainName"}', 'Domain Name', 'IconExternalLink', NULL, false, true, true, 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419'
), (
'70130f27-9497-4b44-a04c-1a0fb9a4829c', 'ba391617-ee08-432f-9438-2e17df5ac279', 'text', 'address', 'Address', '{"value": "address"}', 'Address', 'IconMap', NULL, false, true, true, 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419'
), (
'2a63c30e-8e80-475b-b5d7-9dda17adc537', 'ba391617-ee08-432f-9438-2e17df5ac279', 'number', 'employees', 'Employees', '{"value": "employees"}', 'Employees', 'IconUsers', NULL, false, true, true, 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419'
) ON CONFLICT DO NOTHING`);
};

View File

@ -0,0 +1,47 @@
import { Command, CommandRunner, Option } from 'nest-commander';
import { TenantInitialisationService } from '../tenant-initialisation/tenant-initialisation.service';
import { DataSourceMetadataService } from '../data-source-metadata/data-source-metadata.service';
// TODO: implement dry-run
interface DataSeedTenantOptions {
workspaceId: string;
}
@Command({
name: 'tenant:data-seed',
description: 'Seed tenant with initial data',
})
export class DataSeedTenantCommand extends CommandRunner {
constructor(
private readonly dataSourceMetadataService: DataSourceMetadataService,
private readonly tenantInitialisationService: TenantInitialisationService,
) {
super();
}
async run(
_passedParam: string[],
options: DataSeedTenantOptions,
): Promise<void> {
const dataSourceMetadata =
await this.dataSourceMetadataService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
options.workspaceId,
);
// TODO: run in a dedicated job + run queries in a transaction.
await this.tenantInitialisationService.prefillWorkspaceWithStandardObjects(
dataSourceMetadata,
options.workspaceId,
);
}
// TODO: workspaceId should be optional and we should run migrations for all workspaces
@Option({
flags: '-w, --workspace-id [workspace_id]',
description: 'workspace id',
required: true,
})
parseWorkspaceId(value: string): string {
return value;
}
}

View File

@ -9,6 +9,7 @@ import { DataSourceMetadataModule } from 'src/metadata/data-source-metadata/data
import { SyncTenantMetadataCommand } from './sync-tenant-metadata.command';
import { RunTenantMigrationsCommand } from './run-tenant-migrations.command';
import { DataSeedTenantCommand } from './data-seed-tenant.command';
@Module({
imports: [
@ -19,6 +20,10 @@ import { RunTenantMigrationsCommand } from './run-tenant-migrations.command';
DataSourceMetadataModule,
TenantInitialisationModule,
],
providers: [RunTenantMigrationsCommand, SyncTenantMetadataCommand],
providers: [
RunTenantMigrationsCommand,
SyncTenantMetadataCommand,
DataSeedTenantCommand,
],
})
export class MetadataCommandModule {}

View File

@ -1,7 +1,6 @@
import { Command, CommandRunner, Option } from 'nest-commander';
import { ObjectMetadataService } from 'src/metadata/object-metadata/services/object-metadata.service';
import { FieldMetadataService } from 'src/metadata/field-metadata/services/field-metadata.service';
import { TenantInitialisationService } from 'src/metadata/tenant-initialisation/tenant-initialisation.service';
import { DataSourceMetadataService } from 'src/metadata/data-source-metadata/data-source-metadata.service';
@ -17,7 +16,6 @@ interface RunTenantMigrationsOptions {
export class SyncTenantMetadataCommand extends CommandRunner {
constructor(
private readonly objectMetadataService: ObjectMetadataService,
private readonly fieldMetadataService: FieldMetadataService,
private readonly dataSourceMetadataService: DataSourceMetadataService,
private readonly tenantInitialisationService: TenantInitialisationService,
) {

View File

@ -1,4 +1,5 @@
{
"id": "1a8487a0-480c-434e-b4c7-e22408b97047",
"nameSingular": "companyV2",
"namePlural": "companiesV2",
"labelSingular": "Company",

View File

@ -1,7 +1,11 @@
import companyObject from './companies/companies.metadata.json';
import personObject from './people/people.metadata.json';
import viewObject from './views/views.metadata.json';
import viewFieldObject from './view-fields/view-fields.metadata.json';
export const standardObjectsMetadata = {
companyV2: companyObject,
personV2: personObject,
viewV2: viewObject,
viewFieldV2: viewFieldObject,
};

View File

@ -1,5 +1,9 @@
import companySeeds from './companies/companies.seeds.json';
import viewSeeds from './views/views.seeds.json';
import viewFieldSeeds from './view-fields/view-fields.seeds.json';
export const standardObjectsSeeds = {
companyV2: companySeeds,
viewV2: viewSeeds,
viewFieldV2: viewFieldSeeds,
};

View File

@ -0,0 +1,77 @@
{
"nameSingular": "viewFieldV2",
"namePlural": "viewFieldsV2",
"labelSingular": "View Field",
"labelPlural": "View Fields",
"targetTableName": "viewField",
"description": "(System) View Fields",
"icon": "arrows-sort",
"fields": [
{
"type": "text",
"name": "objectId",
"label": "Object Id",
"targetColumnMap": {
"value": "objectId"
},
"description": "View Field target object",
"icon": null,
"isNullable": false
},
{
"type": "text",
"name": "fieldId",
"label": "Field Id",
"targetColumnMap": {
"value": "fieldId"
},
"description": "View Field target field",
"icon": null,
"isNullable": false
},
{
"type": "text",
"name": "viewId",
"label": "View Id",
"targetColumnMap": {
"value": "viewId"
},
"description": "View Field related view",
"icon": null,
"isNullable": false
},
{
"type": "boolean",
"name": "isVisible",
"label": "Visible",
"targetColumnMap": {
"value": "isVisible"
},
"description": "View Field visibility",
"icon": null,
"isNullable": false
},
{
"type": "number",
"name": "size",
"label": "Size",
"targetColumnMap": {
"value": "size"
},
"description": "View Field size",
"icon": null,
"isNullable": false
},
{
"type": "number",
"name": "position",
"label": "Position",
"targetColumnMap": {
"value": "position"
},
"description": "View Field position",
"icon": null,
"isNullable": false
}
]
}

View File

@ -0,0 +1,43 @@
[
{
"objectId": "1a8487a0-480c-434e-b4c7-e22408b97047",
"fieldId": "name",
"viewId": "10bec73c-0aea-4cc4-a3b2-8c2186f29b43",
"position": 0,
"isVisible": true,
"size": 180
},
{
"objectId": "company",
"fieldId": "name",
"viewId": "37a8a866-eb17-4e76-9382-03143a2f6a80",
"position": 0,
"isVisible": true,
"size": 180
},
{
"objectId": "company",
"fieldId": "domainName",
"viewId": "37a8a866-eb17-4e76-9382-03143a2f6a80",
"position": 1,
"isVisible": true,
"size": 100
},
{
"objectId": "company",
"fieldId": "accountOwner",
"viewId": "37a8a866-eb17-4e76-9382-03143a2f6a80",
"position": 2,
"isVisible": true,
"size": 150
},
{
"objectId": "company",
"fieldId": "createdAt",
"viewId": "37a8a866-eb17-4e76-9382-03143a2f6a80",
"position": 3,
"isVisible": true,
"size": 150
}
]

View File

@ -0,0 +1,44 @@
{
"nameSingular": "viewV2",
"namePlural": "viewsV2",
"labelSingular": "View",
"labelPlural": "Views",
"targetTableName": "view",
"description": "(System) Views",
"icon": "layout-collage",
"fields": [
{
"type": "text",
"name": "name",
"label": "Name",
"targetColumnMap": {
"value": "name"
},
"description": "View name",
"icon": null,
"isNullable": false
},
{
"type": "text",
"name": "objectId",
"label": "Object Id",
"targetColumnMap": {
"value": "objectId"
},
"description": "View target object",
"icon": null,
"isNullable": false
},
{
"type": "text",
"name": "type",
"label": "Type",
"targetColumnMap": {
"value": "type"
},
"description": "View type",
"icon": null,
"isNullable": false
}
]
}

View File

@ -0,0 +1,26 @@
[
{
"id": "37a8a866-eb17-4e76-9382-03143a2f6a80",
"name": "All companies",
"objectId": "company",
"type": "Table"
},
{
"id": "6095799e-b48f-4e00-b071-10818083593a",
"name": "All companies",
"objectId": "person",
"type": "Table"
},
{
"id": "e26f66b7-f890-4a5c-b4d2-ec09987b5308",
"name": "All Opportunities",
"objectId": "company",
"type": "Pipeline"
},
{
"id": "10bec73c-0aea-4cc4-a3b2-8c2186f29b43",
"name": "All Companies (V2)",
"objectId": "1a8487a0-480c-434e-b4c7-e22408b97047",
"type": "Table"
}
]

View File

@ -98,7 +98,7 @@ export class TenantInitialisationService {
);
}
private async prefillWorkspaceWithStandardObjects(
public async prefillWorkspaceWithStandardObjects(
dataSourceMetadata: DataSourceMetadata,
workspaceId: string,
) {
@ -117,11 +117,7 @@ export class TenantInitialisationService {
continue;
}
const fields = standardObjectsMetadata[object.nameSingular].fields;
const columns = fields.map((field: FieldMetadata) =>
Object.values(field.targetColumnMap),
);
const columns = Object.keys(seedData[0]);
await workspaceDataSource
?.createQueryBuilder()

View File

@ -0,0 +1,29 @@
import { TenantMigrationTableAction } from 'src/metadata/tenant-migration/tenant-migration.entity';
export const addViewTable: TenantMigrationTableAction[] = [
{
name: 'view',
action: 'create',
},
{
name: 'view',
action: 'alter',
columns: [
{
name: 'name',
type: 'varchar',
action: 'create',
},
{
name: 'objectId',
type: 'varchar',
action: 'create',
},
{
name: 'type',
type: 'varchar',
action: 'create',
},
],
},
];

View File

@ -0,0 +1,44 @@
import { TenantMigrationTableAction } from 'src/metadata/tenant-migration/tenant-migration.entity';
export const addViewFieldTable: TenantMigrationTableAction[] = [
{
name: 'viewField',
action: 'create',
},
{
name: 'viewField',
action: 'alter',
columns: [
{
name: 'objectId',
type: 'varchar',
action: 'create',
},
{
name: 'fieldId',
type: 'varchar',
action: 'create',
},
{
name: 'viewId',
type: 'varchar',
action: 'create',
},
{
name: 'position',
type: 'integer',
action: 'create',
},
{
name: 'isVisible',
type: 'boolean',
action: 'create',
},
{
name: 'size',
type: 'integer',
action: 'create',
},
],
},
];

View File

@ -1,8 +1,12 @@
import { addCompanyTable } from './migrations/1697618009-addCompanyTable';
import { addPeopleTable } from './migrations/1697618010-addPeopleTable';
import { addViewTable } from './migrations/1697618011-addViewTable';
import { addViewFieldTable } from './migrations/1697618012-addViewFieldTable';
// TODO: read the folder and return all migrations
export const standardMigrations = {
'1697618009-addCompanyTable': addCompanyTable,
'1697618010-addPeopleTable': addPeopleTable,
'1697618011-addViewTable': addViewTable,
'1697618012-addViewFieldTable': addViewFieldTable,
};