Twenty standard and workspace custom applications 1/3 (#15625)
# Introduction related to https://github.com/twentyhq/core-team-issues/issues/1833 In this PR we're starting the sync-metadata and standardIds deprecation by introducing `twenty-standard` application that will regroup every standard object such as company and opportunities. But also the `custom-workspace-application` which is an app created at the same time as a workspace and that will regroup everything configure within the workspace ( custom objects fields etc ) ## What's done On both new workspace and seeded workspace creation: - Creating a custom workspace app - Creating a twenty standard app - Refactored the seed core schema and workspace creation to be run within a transaction in order to handle circular dependency foreignkey requirements ( which is deferred for app toward workspace ) - Updated workspace entity to have a custom workspace relation ( nullable for the moment until we implem an upgrade command to handle retro comp ) - Integration testing on user, workspace creation deletion and expected default apps creation - ~~Soft deleted user on `deleteUser`~~ Done by marie and rebased on it ## What's next - Update seeder to propagate the `twenty-standard` workspace `applicationId` to every standard synchronized entities ( cheap and fast iteration through the about to be deprecated sync-metadata as an easy way to synchronize standards metadata entities ). - Update seeder to propagate the `custom-workspace-application` workspace `applicationId` to anything custom ( `pets` and `rockets` ) - Prepend `custom-workspace-application` `applicationId` to every metadata API operations ( create a specific cache etc ) - Upgrade command on all existing workspace to create a custom app and associate its applicationId to any existing custom entities - Make `universalIdentifier` and `applicationId` required for any syncable entity
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
type ActivateWorkspaceUtilArgs = {
|
||||
accessToken: string;
|
||||
displayName: string;
|
||||
expectToFail?: boolean;
|
||||
};
|
||||
|
||||
export const activateWorkspace = async ({
|
||||
accessToken,
|
||||
displayName,
|
||||
expectToFail,
|
||||
}: ActivateWorkspaceUtilArgs): CommonResponseBody<{
|
||||
activateWorkspace: WorkspaceEntity;
|
||||
}> => {
|
||||
const mutation = gql`
|
||||
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
|
||||
activateWorkspace(data: $input) {
|
||||
id
|
||||
displayName
|
||||
activationStatus
|
||||
subdomain
|
||||
inviteHash
|
||||
logo
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await makeGraphqlAPIRequest(
|
||||
{
|
||||
query: mutation,
|
||||
variables: {
|
||||
input: {
|
||||
displayName,
|
||||
},
|
||||
},
|
||||
},
|
||||
accessToken,
|
||||
);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Activate workspace should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Activate workspace has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
|
||||
import { type UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
|
||||
type DeleteUserUtilArgs = {
|
||||
accessToken: string;
|
||||
expectToFail?: boolean;
|
||||
};
|
||||
|
||||
export const deleteUser = async ({
|
||||
accessToken,
|
||||
expectToFail,
|
||||
}: DeleteUserUtilArgs): CommonResponseBody<{
|
||||
deleteUser: UserEntity;
|
||||
}> => {
|
||||
const mutation = gql`
|
||||
mutation DeleteUser {
|
||||
deleteUser {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
deletedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await makeGraphqlAPIRequest(
|
||||
{
|
||||
query: mutation,
|
||||
variables: {},
|
||||
},
|
||||
accessToken,
|
||||
);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Delete user should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Delete user has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { type ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
|
||||
|
||||
export const APPLICATION_GQL_FIELDS = `
|
||||
id
|
||||
name
|
||||
description
|
||||
version
|
||||
universalIdentifier
|
||||
`;
|
||||
|
||||
export const findManyApplications = async ({
|
||||
gqlFields = APPLICATION_GQL_FIELDS,
|
||||
expectToFail,
|
||||
accessToken,
|
||||
}: {
|
||||
gqlFields?: string;
|
||||
expectToFail?: boolean;
|
||||
accessToken?: string;
|
||||
}): CommonResponseBody<{
|
||||
findManyApplications: ApplicationDTO[];
|
||||
}> => {
|
||||
const response = await makeGraphqlAPIRequest(
|
||||
{
|
||||
query: gql`
|
||||
query FindManyApplications {
|
||||
findManyApplications {
|
||||
${gqlFields}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {},
|
||||
},
|
||||
accessToken,
|
||||
);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Application search should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Application search has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
|
||||
import { type AuthTokens } from 'src/engine/core-modules/auth/dto/auth-tokens.dto';
|
||||
|
||||
type GetAuthTokensFromLoginTokenUtilArgs = {
|
||||
loginToken: string;
|
||||
origin?: string;
|
||||
expectToFail?: boolean;
|
||||
};
|
||||
|
||||
export const getAuthTokensFromLoginToken = async ({
|
||||
loginToken,
|
||||
origin = 'http://localhost:3001',
|
||||
expectToFail,
|
||||
}: GetAuthTokensFromLoginTokenUtilArgs): CommonResponseBody<{
|
||||
getAuthTokensFromLoginToken: AuthTokens;
|
||||
}> => {
|
||||
const mutation = gql`
|
||||
mutation GetAuthTokensFromLoginToken(
|
||||
$loginToken: String!
|
||||
$origin: String!
|
||||
) {
|
||||
getAuthTokensFromLoginToken(loginToken: $loginToken, origin: $origin) {
|
||||
tokens {
|
||||
accessOrWorkspaceAgnosticToken {
|
||||
token
|
||||
expiresAt
|
||||
}
|
||||
refreshToken {
|
||||
token
|
||||
expiresAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await makeGraphqlAPIRequest(
|
||||
{
|
||||
query: mutation,
|
||||
variables: {
|
||||
loginToken,
|
||||
origin,
|
||||
},
|
||||
},
|
||||
undefined, // Public endpoint - no authentication required
|
||||
);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage:
|
||||
'Get auth tokens from login token should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage:
|
||||
'Get auth tokens from login token has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
|
||||
import { type UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
|
||||
type CurrentUserUtilArgs = {
|
||||
accessToken: string;
|
||||
expectToFail?: boolean;
|
||||
};
|
||||
|
||||
export const getCurrentUser = async ({
|
||||
accessToken,
|
||||
expectToFail,
|
||||
}: CurrentUserUtilArgs): CommonResponseBody<{
|
||||
currentUser: UserEntity;
|
||||
}> => {
|
||||
const query = gql`
|
||||
query CurrentUser {
|
||||
currentUser {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
defaultAvatarUrl
|
||||
isEmailVerified
|
||||
disabled
|
||||
canImpersonate
|
||||
canAccessFullAdminPanel
|
||||
locale
|
||||
createdAt
|
||||
updatedAt
|
||||
deletedAt
|
||||
currentWorkspace {
|
||||
id
|
||||
displayName
|
||||
subdomain
|
||||
activationStatus
|
||||
logo
|
||||
workspaceCustomApplicationId
|
||||
}
|
||||
currentUserWorkspace {
|
||||
id
|
||||
userId
|
||||
workspaceId
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await makeGraphqlAPIRequest(
|
||||
{
|
||||
query,
|
||||
variables: {},
|
||||
},
|
||||
accessToken,
|
||||
);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Get current user should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Get current user has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
+15
-8
@@ -1,19 +1,26 @@
|
||||
import { type ASTNode, print } from 'graphql';
|
||||
import request from 'supertest';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type GraphqlOperation = {
|
||||
query: ASTNode;
|
||||
variables?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export const makeGraphqlAPIRequest = (graphqlOperation: GraphqlOperation) => {
|
||||
export const makeGraphqlAPIRequest = (
|
||||
graphqlOperation: GraphqlOperation,
|
||||
token: string | undefined = APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
) => {
|
||||
const client = request(`http://localhost:${APP_PORT}`);
|
||||
|
||||
return client
|
||||
.post('/graphql')
|
||||
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
|
||||
.send({
|
||||
query: print(graphqlOperation.query),
|
||||
variables: graphqlOperation.variables || {},
|
||||
});
|
||||
const clientInstance = client.post('/graphql');
|
||||
|
||||
if (isDefined(token)) {
|
||||
clientInstance.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
|
||||
return clientInstance.send({
|
||||
query: print(graphqlOperation.query),
|
||||
variables: graphqlOperation.variables || {},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { searchFactory } from 'test/integration/graphql/utils/search-factory.util';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
|
||||
import { type SearchArgs } from 'src/engine/core-modules/search/dtos/search-args';
|
||||
import { type SearchResultConnectionDTO } from 'src/engine/core-modules/search/dtos/search-result-connection.dto';
|
||||
|
||||
type SearchOperationArgs = SearchArgs & {
|
||||
expectToFail?: boolean;
|
||||
accessToken?: string;
|
||||
};
|
||||
|
||||
export const search = async ({
|
||||
searchInput,
|
||||
limit,
|
||||
after,
|
||||
includedObjectNameSingulars,
|
||||
excludedObjectNameSingulars,
|
||||
filter,
|
||||
expectToFail,
|
||||
accessToken,
|
||||
}: SearchOperationArgs): CommonResponseBody<{
|
||||
search: SearchResultConnectionDTO;
|
||||
}> => {
|
||||
const graphqlOperation = searchFactory({
|
||||
searchInput,
|
||||
limit,
|
||||
after,
|
||||
includedObjectNameSingulars,
|
||||
excludedObjectNameSingulars,
|
||||
filter,
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation, accessToken);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Search should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Search has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
|
||||
import { type SignUpOutput } from 'src/engine/core-modules/auth/dto/sign-up.output';
|
||||
|
||||
type SignUpOnNewWorkspaceUtilArgs = {
|
||||
accessToken: string;
|
||||
expectToFail?: boolean;
|
||||
};
|
||||
|
||||
export const signUpInNewWorkspace = async ({
|
||||
accessToken,
|
||||
expectToFail,
|
||||
}: SignUpOnNewWorkspaceUtilArgs): CommonResponseBody<{
|
||||
signUpInNewWorkspace: SignUpOutput;
|
||||
}> => {
|
||||
const mutation = gql`
|
||||
mutation SignUpInNewWorkspace {
|
||||
signUpInNewWorkspace {
|
||||
loginToken {
|
||||
token
|
||||
expiresAt
|
||||
}
|
||||
workspace {
|
||||
id
|
||||
workspaceUrls {
|
||||
customUrl
|
||||
subdomainUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await makeGraphqlAPIRequest(
|
||||
{
|
||||
query: mutation,
|
||||
variables: {},
|
||||
},
|
||||
accessToken,
|
||||
);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Sign up on new workspace should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Sign up on new workspace has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
|
||||
export const signUpOperationFactory = ({
|
||||
email,
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
|
||||
import { type AvailableWorkspacesAndAccessTokensOutput } from 'src/engine/core-modules/auth/dto/available-workspaces-and-access-tokens.output';
|
||||
import { type UserCredentialsInput } from 'src/engine/core-modules/auth/dto/user-credentials.input';
|
||||
|
||||
type SignUpUtilArgs = {
|
||||
input: UserCredentialsInput;
|
||||
expectToFail?: boolean;
|
||||
};
|
||||
|
||||
export const signUp = async ({
|
||||
input,
|
||||
expectToFail,
|
||||
}: SignUpUtilArgs): CommonResponseBody<{
|
||||
signUp: AvailableWorkspacesAndAccessTokensOutput;
|
||||
}> => {
|
||||
const mutation = gql`
|
||||
mutation SignUp(
|
||||
$email: String!
|
||||
$password: String!
|
||||
$captchaToken: String
|
||||
) {
|
||||
signUp(email: $email, password: $password, captchaToken: $captchaToken) {
|
||||
tokens {
|
||||
accessOrWorkspaceAgnosticToken {
|
||||
token
|
||||
expiresAt
|
||||
}
|
||||
refreshToken {
|
||||
token
|
||||
expiresAt
|
||||
}
|
||||
}
|
||||
availableWorkspaces {
|
||||
availableWorkspacesForSignIn {
|
||||
id
|
||||
displayName
|
||||
logo
|
||||
workspaceUrls {
|
||||
customUrl
|
||||
subdomainUrl
|
||||
}
|
||||
}
|
||||
availableWorkspacesForSignUp {
|
||||
id
|
||||
displayName
|
||||
logo
|
||||
workspaceUrls {
|
||||
customUrl
|
||||
subdomainUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await makeGraphqlAPIRequest(
|
||||
{
|
||||
query: mutation,
|
||||
variables: {
|
||||
...input,
|
||||
},
|
||||
},
|
||||
undefined, // Public endpoint - no authentication required
|
||||
);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Sign up should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Sign up has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
Reference in New Issue
Block a user