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

View File

@ -1,10 +1,4 @@
import { useEffect } from 'react'; 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 { useFindManyObjects } from '../hooks/useFindManyObjects';
import { useSetObjectDataTableData } from '../hooks/useSetDataTableData'; import { useSetObjectDataTableData } from '../hooks/useSetDataTableData';
@ -15,6 +9,7 @@ export type ObjectDataTableEffectProps = Pick<
'objectNamePlural' 'objectNamePlural'
>; >;
// TODO: merge in a single effect component
export const ObjectDataTableEffect = ({ export const ObjectDataTableEffect = ({
objectNamePlural, objectNamePlural,
}: ObjectDataTableEffectProps) => { }: ObjectDataTableEffectProps) => {
@ -32,32 +27,5 @@ export const ObjectDataTableEffect = ({
} }
}, [objects, setDataTableData, loading]); }, [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 <></>; 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 { DataTable } from '@/ui/data/data-table/components/DataTable';
import { TableContext } from '@/ui/data/data-table/contexts/TableContext'; 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 { useUpdateOneObject } from '../hooks/useUpdateOneObject';
import { MetadataObjectIdentifier } from '../types/MetadataObjectIdentifier'; import { MetadataObjectIdentifier } from '../types/MetadataObjectIdentifier';
import { ObjectDataTableEffect } from './ObjectDataTableEffect'; 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< export type ObjectTableProps = Pick<
MetadataObjectIdentifier, MetadataObjectIdentifier,
@ -16,6 +35,11 @@ export const ObjectTable = ({ objectNamePlural }: ObjectTableProps) => {
objectNamePlural, objectNamePlural,
}); });
const viewScopeId = objectNamePlural ?? '';
const { persistViewFields } = useViewFields(viewScopeId);
const { setCurrentViewFields } = useView({ viewScopeId: viewScopeId });
const updateEntity = ({ const updateEntity = ({
variables, variables,
}: { }: {
@ -32,17 +56,38 @@ export const ObjectTable = ({ objectNamePlural }: ObjectTableProps) => {
}); });
}; };
const updateTableColumns = useRecoilCallback(
({ set, snapshot }) =>
(viewFields: ColumnDefinition<FieldMetadata>[]) => {
set(tableColumnsScopedState(viewScopeId), viewFields);
},
);
return ( return (
<TableContext.Provider <ViewScope
value={{ viewScopeId={viewScopeId}
onColumnsChange: () => { onViewFieldsChange={(viewFields) => {
// // updateTableColumns(viewFields);
},
}} }}
> >
<ObjectDataTableEffect objectNamePlural={objectNamePlural} /> <StyledContainer>
<TableContext.Provider
<DataTable updateEntityMutation={updateEntity} /> value={{
</TableContext.Provider> 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>, ObjectType extends { id: string } & Record<string, any>,
>({ >({
objectNamePlural, objectNamePlural,
}: Pick<MetadataObjectIdentifier, 'objectNamePlural'>) => { filter,
orderBy,
onCompleted,
}: Pick<MetadataObjectIdentifier, 'objectNamePlural'> & {
filter?: any;
orderBy?: any;
onCompleted?: (data: any) => void;
}) => {
const { foundMetadataObject, objectNotFoundInMetadata, findManyQuery } = const { foundMetadataObject, objectNotFoundInMetadata, findManyQuery } =
useFindOneMetadataObject({ useFindOneMetadataObject({
objectNamePlural, objectNamePlural,
@ -27,6 +34,12 @@ export const useFindManyObjects = <
findManyQuery, findManyQuery,
{ {
skip: !foundMetadataObject, skip: !foundMetadataObject,
variables: {
filter: filter ?? {},
orderBy: orderBy ?? {},
},
onCompleted: (data) =>
objectNamePlural && onCompleted?.(data[objectNamePlural]),
onError: (error) => { onError: (error) => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error( console.error(

View File

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

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

View File

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

View File

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

View File

@ -17,29 +17,32 @@ export const useViews = (scopeId: string) => {
const [updateViewMutation] = useUpdateViewMutation(); const [updateViewMutation] = useUpdateViewMutation();
const [deleteViewMutation] = useDeleteViewMutation(); const [deleteViewMutation] = useDeleteViewMutation();
const createView = useRecoilCallback(({ snapshot }) => async (view: View) => { const createView = useRecoilCallback(
const viewObjectId = await snapshot ({ snapshot }) =>
.getLoadable(viewObjectIdScopeState({ scopeId })) async (view: Pick<View, 'id' | 'name'>) => {
.getValue(); const viewObjectId = await snapshot
.getLoadable(viewObjectIdScopeState({ scopeId }))
.getValue();
const viewType = await snapshot const viewType = await snapshot
.getLoadable(viewTypeScopedState({ scopeId })) .getLoadable(viewTypeScopedState({ scopeId }))
.getValue(); .getValue();
if (!viewObjectId || !viewType) { if (!viewObjectId || !viewType) {
return; return;
} }
await createViewMutation({ await createViewMutation({
variables: { variables: {
data: { data: {
...view, ...view,
objectId: viewObjectId, objectId: viewObjectId,
type: viewType, type: viewType,
}, },
},
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
});
}, },
refetchQueries: [getOperationName(GET_VIEWS) ?? ''], );
});
});
const updateView = async (view: View) => { const updateView = async (view: View) => {
await updateViewMutation({ 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-gql-select": "node scripts/generate-model-select-map.js",
"prisma:generate-nest-graphql": "npx prisma generate --generator nestgraphql", "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: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:seed": "npx prisma db seed",
"prisma:migrate": "npx prisma migrate deploy", "prisma:migrate": "npx prisma migrate deploy",
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js", "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js",
"typeorm:migrate": "yarn typeorm migration:run -d ./src/metadata/metadata.datasource.ts", "typeorm:migrate": "yarn typeorm migration:run -d ./src/metadata/metadata.datasource.ts",
"database:init": "yarn database:setup && yarn database:seed", "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:truncate": "npx ts-node ./scripts/truncate-db.ts",
"database:migrate": "yarn typeorm:migrate && yarn prisma:migrate", "database:migrate": "yarn typeorm:migrate && yarn prisma:migrate",
"database:generate": "yarn prisma:generate", "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", "database:reset": "yarn database:truncate && yarn database:init",
"command": "node dist/src/command" "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' 'b37b2163-7f63-47a9-b1b3-6c7290ca9fb1', 'workspace_twenty_7icsva0r6s00mpcp6cwg4w4rd', 'postgres', 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419'
) ON CONFLICT DO NOTHING`, ) 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 { SyncTenantMetadataCommand } from './sync-tenant-metadata.command';
import { RunTenantMigrationsCommand } from './run-tenant-migrations.command'; import { RunTenantMigrationsCommand } from './run-tenant-migrations.command';
import { DataSeedTenantCommand } from './data-seed-tenant.command';
@Module({ @Module({
imports: [ imports: [
@ -19,6 +20,10 @@ import { RunTenantMigrationsCommand } from './run-tenant-migrations.command';
DataSourceMetadataModule, DataSourceMetadataModule,
TenantInitialisationModule, TenantInitialisationModule,
], ],
providers: [RunTenantMigrationsCommand, SyncTenantMetadataCommand], providers: [
RunTenantMigrationsCommand,
SyncTenantMetadataCommand,
DataSeedTenantCommand,
],
}) })
export class MetadataCommandModule {} export class MetadataCommandModule {}

View File

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

View File

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

View File

@ -1,7 +1,11 @@
import companyObject from './companies/companies.metadata.json'; import companyObject from './companies/companies.metadata.json';
import personObject from './people/people.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 = { export const standardObjectsMetadata = {
companyV2: companyObject, companyV2: companyObject,
personV2: personObject, personV2: personObject,
viewV2: viewObject,
viewFieldV2: viewFieldObject,
}; };

View File

@ -1,5 +1,9 @@
import companySeeds from './companies/companies.seeds.json'; 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 = { export const standardObjectsSeeds = {
companyV2: companySeeds, 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, dataSourceMetadata: DataSourceMetadata,
workspaceId: string, workspaceId: string,
) { ) {
@ -117,11 +117,7 @@ export class TenantInitialisationService {
continue; continue;
} }
const fields = standardObjectsMetadata[object.nameSingular].fields; const columns = Object.keys(seedData[0]);
const columns = fields.map((field: FieldMetadata) =>
Object.values(field.targetColumnMap),
);
await workspaceDataSource await workspaceDataSource
?.createQueryBuilder() ?.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 { addCompanyTable } from './migrations/1697618009-addCompanyTable';
import { addPeopleTable } from './migrations/1697618010-addPeopleTable'; 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 // TODO: read the folder and return all migrations
export const standardMigrations = { export const standardMigrations = {
'1697618009-addCompanyTable': addCompanyTable, '1697618009-addCompanyTable': addCompanyTable,
'1697618010-addPeopleTable': addPeopleTable, '1697618010-addPeopleTable': addPeopleTable,
'1697618011-addViewTable': addViewTable,
'1697618012-addViewFieldTable': addViewFieldTable,
}; };