Stop rejecting application install when APP_VERSION is wrong (#20443)

as title

allows to install https://github.com/JordanChoo/twenty-multi-pipeline
locally
This commit is contained in:
martmull
2026-05-11 17:17:17 +02:00
committed by GitHub
parent 6c18bacb93
commit 7c053716ae
10 changed files with 574 additions and 539 deletions
@@ -1970,76 +1970,6 @@ type RotateClientSecret {
clientSecret: String!
}
type ResendEmailVerificationToken {
success: Boolean!
}
type DeleteSso {
identityProviderId: UUID!
}
type EditSso {
id: UUID!
type: IdentityProviderType!
issuer: String!
name: String!
status: SSOIdentityProviderStatus!
}
type WorkspaceNameAndId {
displayName: String
id: UUID!
}
type FindAvailableSSOIDP {
type: IdentityProviderType!
id: UUID!
issuer: String!
name: String!
status: SSOIdentityProviderStatus!
workspace: WorkspaceNameAndId!
}
type SetupSso {
id: UUID!
type: IdentityProviderType!
issuer: String!
name: String!
status: SSOIdentityProviderStatus!
}
type SSOConnection {
type: IdentityProviderType!
id: UUID!
issuer: String!
name: String!
status: SSOIdentityProviderStatus!
}
type AvailableWorkspace {
id: UUID!
displayName: String
loginToken: String
personalInviteToken: String
inviteHash: String
workspaceUrls: WorkspaceUrls!
logo: String
sso: [SSOConnection!]!
}
type AvailableWorkspaces {
availableWorkspacesForSignIn: [AvailableWorkspace!]!
availableWorkspacesForSignUp: [AvailableWorkspace!]!
}
type DeletedWorkspaceMember {
id: UUID!
name: FullName!
userEmail: String!
avatarUrl: String
userWorkspaceId: UUID
}
type Relation {
type: RelationType!
sourceObjectMetadata: Object!
@@ -2161,6 +2091,76 @@ type FieldConnection {
edges: [FieldEdge!]!
}
type ResendEmailVerificationToken {
success: Boolean!
}
type DeleteSso {
identityProviderId: UUID!
}
type EditSso {
id: UUID!
type: IdentityProviderType!
issuer: String!
name: String!
status: SSOIdentityProviderStatus!
}
type WorkspaceNameAndId {
displayName: String
id: UUID!
}
type FindAvailableSSOIDP {
type: IdentityProviderType!
id: UUID!
issuer: String!
name: String!
status: SSOIdentityProviderStatus!
workspace: WorkspaceNameAndId!
}
type SetupSso {
id: UUID!
type: IdentityProviderType!
issuer: String!
name: String!
status: SSOIdentityProviderStatus!
}
type SSOConnection {
type: IdentityProviderType!
id: UUID!
issuer: String!
name: String!
status: SSOIdentityProviderStatus!
}
type AvailableWorkspace {
id: UUID!
displayName: String
loginToken: String
personalInviteToken: String
inviteHash: String
workspaceUrls: WorkspaceUrls!
logo: String
sso: [SSOConnection!]!
}
type AvailableWorkspaces {
availableWorkspacesForSignIn: [AvailableWorkspace!]!
availableWorkspacesForSignUp: [AvailableWorkspace!]!
}
type DeletedWorkspaceMember {
id: UUID!
name: FullName!
userEmail: String!
avatarUrl: String
userWorkspaceId: UUID
}
type BillingEntitlement {
key: BillingEntitlementKey!
value: Boolean!
@@ -1604,86 +1604,6 @@ export interface RotateClientSecret {
__typename: 'RotateClientSecret'
}
export interface ResendEmailVerificationToken {
success: Scalars['Boolean']
__typename: 'ResendEmailVerificationToken'
}
export interface DeleteSso {
identityProviderId: Scalars['UUID']
__typename: 'DeleteSso'
}
export interface EditSso {
id: Scalars['UUID']
type: IdentityProviderType
issuer: Scalars['String']
name: Scalars['String']
status: SSOIdentityProviderStatus
__typename: 'EditSso'
}
export interface WorkspaceNameAndId {
displayName?: Scalars['String']
id: Scalars['UUID']
__typename: 'WorkspaceNameAndId'
}
export interface FindAvailableSSOIDP {
type: IdentityProviderType
id: Scalars['UUID']
issuer: Scalars['String']
name: Scalars['String']
status: SSOIdentityProviderStatus
workspace: WorkspaceNameAndId
__typename: 'FindAvailableSSOIDP'
}
export interface SetupSso {
id: Scalars['UUID']
type: IdentityProviderType
issuer: Scalars['String']
name: Scalars['String']
status: SSOIdentityProviderStatus
__typename: 'SetupSso'
}
export interface SSOConnection {
type: IdentityProviderType
id: Scalars['UUID']
issuer: Scalars['String']
name: Scalars['String']
status: SSOIdentityProviderStatus
__typename: 'SSOConnection'
}
export interface AvailableWorkspace {
id: Scalars['UUID']
displayName?: Scalars['String']
loginToken?: Scalars['String']
personalInviteToken?: Scalars['String']
inviteHash?: Scalars['String']
workspaceUrls: WorkspaceUrls
logo?: Scalars['String']
sso: SSOConnection[]
__typename: 'AvailableWorkspace'
}
export interface AvailableWorkspaces {
availableWorkspacesForSignIn: AvailableWorkspace[]
availableWorkspacesForSignUp: AvailableWorkspace[]
__typename: 'AvailableWorkspaces'
}
export interface DeletedWorkspaceMember {
id: Scalars['UUID']
name: FullName
userEmail: Scalars['String']
avatarUrl?: Scalars['String']
userWorkspaceId?: Scalars['UUID']
__typename: 'DeletedWorkspaceMember'
}
export interface Relation {
type: RelationType
sourceObjectMetadata: Object
@@ -1803,6 +1723,86 @@ export interface FieldConnection {
__typename: 'FieldConnection'
}
export interface ResendEmailVerificationToken {
success: Scalars['Boolean']
__typename: 'ResendEmailVerificationToken'
}
export interface DeleteSso {
identityProviderId: Scalars['UUID']
__typename: 'DeleteSso'
}
export interface EditSso {
id: Scalars['UUID']
type: IdentityProviderType
issuer: Scalars['String']
name: Scalars['String']
status: SSOIdentityProviderStatus
__typename: 'EditSso'
}
export interface WorkspaceNameAndId {
displayName?: Scalars['String']
id: Scalars['UUID']
__typename: 'WorkspaceNameAndId'
}
export interface FindAvailableSSOIDP {
type: IdentityProviderType
id: Scalars['UUID']
issuer: Scalars['String']
name: Scalars['String']
status: SSOIdentityProviderStatus
workspace: WorkspaceNameAndId
__typename: 'FindAvailableSSOIDP'
}
export interface SetupSso {
id: Scalars['UUID']
type: IdentityProviderType
issuer: Scalars['String']
name: Scalars['String']
status: SSOIdentityProviderStatus
__typename: 'SetupSso'
}
export interface SSOConnection {
type: IdentityProviderType
id: Scalars['UUID']
issuer: Scalars['String']
name: Scalars['String']
status: SSOIdentityProviderStatus
__typename: 'SSOConnection'
}
export interface AvailableWorkspace {
id: Scalars['UUID']
displayName?: Scalars['String']
loginToken?: Scalars['String']
personalInviteToken?: Scalars['String']
inviteHash?: Scalars['String']
workspaceUrls: WorkspaceUrls
logo?: Scalars['String']
sso: SSOConnection[]
__typename: 'AvailableWorkspace'
}
export interface AvailableWorkspaces {
availableWorkspacesForSignIn: AvailableWorkspace[]
availableWorkspacesForSignUp: AvailableWorkspace[]
__typename: 'AvailableWorkspaces'
}
export interface DeletedWorkspaceMember {
id: Scalars['UUID']
name: FullName
userEmail: Scalars['String']
avatarUrl?: Scalars['String']
userWorkspaceId?: Scalars['UUID']
__typename: 'DeletedWorkspaceMember'
}
export interface BillingEntitlement {
key: BillingEntitlementKey
value: Scalars['Boolean']
@@ -4564,96 +4564,6 @@ export interface RotateClientSecretGenqlSelection{
__scalar?: boolean | number
}
export interface ResendEmailVerificationTokenGenqlSelection{
success?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface DeleteSsoGenqlSelection{
identityProviderId?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface EditSsoGenqlSelection{
id?: boolean | number
type?: boolean | number
issuer?: boolean | number
name?: boolean | number
status?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface WorkspaceNameAndIdGenqlSelection{
displayName?: boolean | number
id?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface FindAvailableSSOIDPGenqlSelection{
type?: boolean | number
id?: boolean | number
issuer?: boolean | number
name?: boolean | number
status?: boolean | number
workspace?: WorkspaceNameAndIdGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface SetupSsoGenqlSelection{
id?: boolean | number
type?: boolean | number
issuer?: boolean | number
name?: boolean | number
status?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface SSOConnectionGenqlSelection{
type?: boolean | number
id?: boolean | number
issuer?: boolean | number
name?: boolean | number
status?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface AvailableWorkspaceGenqlSelection{
id?: boolean | number
displayName?: boolean | number
loginToken?: boolean | number
personalInviteToken?: boolean | number
inviteHash?: boolean | number
workspaceUrls?: WorkspaceUrlsGenqlSelection
logo?: boolean | number
sso?: SSOConnectionGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface AvailableWorkspacesGenqlSelection{
availableWorkspacesForSignIn?: AvailableWorkspaceGenqlSelection
availableWorkspacesForSignUp?: AvailableWorkspaceGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface DeletedWorkspaceMemberGenqlSelection{
id?: boolean | number
name?: FullNameGenqlSelection
userEmail?: boolean | number
avatarUrl?: boolean | number
userWorkspaceId?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface RelationGenqlSelection{
type?: boolean | number
sourceObjectMetadata?: ObjectGenqlSelection
@@ -4783,6 +4693,96 @@ export interface FieldConnectionGenqlSelection{
__scalar?: boolean | number
}
export interface ResendEmailVerificationTokenGenqlSelection{
success?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface DeleteSsoGenqlSelection{
identityProviderId?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface EditSsoGenqlSelection{
id?: boolean | number
type?: boolean | number
issuer?: boolean | number
name?: boolean | number
status?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface WorkspaceNameAndIdGenqlSelection{
displayName?: boolean | number
id?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface FindAvailableSSOIDPGenqlSelection{
type?: boolean | number
id?: boolean | number
issuer?: boolean | number
name?: boolean | number
status?: boolean | number
workspace?: WorkspaceNameAndIdGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface SetupSsoGenqlSelection{
id?: boolean | number
type?: boolean | number
issuer?: boolean | number
name?: boolean | number
status?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface SSOConnectionGenqlSelection{
type?: boolean | number
id?: boolean | number
issuer?: boolean | number
name?: boolean | number
status?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface AvailableWorkspaceGenqlSelection{
id?: boolean | number
displayName?: boolean | number
loginToken?: boolean | number
personalInviteToken?: boolean | number
inviteHash?: boolean | number
workspaceUrls?: WorkspaceUrlsGenqlSelection
logo?: boolean | number
sso?: SSOConnectionGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface AvailableWorkspacesGenqlSelection{
availableWorkspacesForSignIn?: AvailableWorkspaceGenqlSelection
availableWorkspacesForSignUp?: AvailableWorkspaceGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface DeletedWorkspaceMemberGenqlSelection{
id?: boolean | number
name?: FullNameGenqlSelection
userEmail?: boolean | number
avatarUrl?: boolean | number
userWorkspaceId?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface BillingEntitlementGenqlSelection{
key?: boolean | number
value?: boolean | number
@@ -7398,86 +7398,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const ResendEmailVerificationToken_possibleTypes: string[] = ['ResendEmailVerificationToken']
export const isResendEmailVerificationToken = (obj?: { __typename?: any } | null): obj is ResendEmailVerificationToken => {
if (!obj?.__typename) throw new Error('__typename is missing in "isResendEmailVerificationToken"')
return ResendEmailVerificationToken_possibleTypes.includes(obj.__typename)
}
const DeleteSso_possibleTypes: string[] = ['DeleteSso']
export const isDeleteSso = (obj?: { __typename?: any } | null): obj is DeleteSso => {
if (!obj?.__typename) throw new Error('__typename is missing in "isDeleteSso"')
return DeleteSso_possibleTypes.includes(obj.__typename)
}
const EditSso_possibleTypes: string[] = ['EditSso']
export const isEditSso = (obj?: { __typename?: any } | null): obj is EditSso => {
if (!obj?.__typename) throw new Error('__typename is missing in "isEditSso"')
return EditSso_possibleTypes.includes(obj.__typename)
}
const WorkspaceNameAndId_possibleTypes: string[] = ['WorkspaceNameAndId']
export const isWorkspaceNameAndId = (obj?: { __typename?: any } | null): obj is WorkspaceNameAndId => {
if (!obj?.__typename) throw new Error('__typename is missing in "isWorkspaceNameAndId"')
return WorkspaceNameAndId_possibleTypes.includes(obj.__typename)
}
const FindAvailableSSOIDP_possibleTypes: string[] = ['FindAvailableSSOIDP']
export const isFindAvailableSSOIDP = (obj?: { __typename?: any } | null): obj is FindAvailableSSOIDP => {
if (!obj?.__typename) throw new Error('__typename is missing in "isFindAvailableSSOIDP"')
return FindAvailableSSOIDP_possibleTypes.includes(obj.__typename)
}
const SetupSso_possibleTypes: string[] = ['SetupSso']
export const isSetupSso = (obj?: { __typename?: any } | null): obj is SetupSso => {
if (!obj?.__typename) throw new Error('__typename is missing in "isSetupSso"')
return SetupSso_possibleTypes.includes(obj.__typename)
}
const SSOConnection_possibleTypes: string[] = ['SSOConnection']
export const isSSOConnection = (obj?: { __typename?: any } | null): obj is SSOConnection => {
if (!obj?.__typename) throw new Error('__typename is missing in "isSSOConnection"')
return SSOConnection_possibleTypes.includes(obj.__typename)
}
const AvailableWorkspace_possibleTypes: string[] = ['AvailableWorkspace']
export const isAvailableWorkspace = (obj?: { __typename?: any } | null): obj is AvailableWorkspace => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAvailableWorkspace"')
return AvailableWorkspace_possibleTypes.includes(obj.__typename)
}
const AvailableWorkspaces_possibleTypes: string[] = ['AvailableWorkspaces']
export const isAvailableWorkspaces = (obj?: { __typename?: any } | null): obj is AvailableWorkspaces => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAvailableWorkspaces"')
return AvailableWorkspaces_possibleTypes.includes(obj.__typename)
}
const DeletedWorkspaceMember_possibleTypes: string[] = ['DeletedWorkspaceMember']
export const isDeletedWorkspaceMember = (obj?: { __typename?: any } | null): obj is DeletedWorkspaceMember => {
if (!obj?.__typename) throw new Error('__typename is missing in "isDeletedWorkspaceMember"')
return DeletedWorkspaceMember_possibleTypes.includes(obj.__typename)
}
const Relation_possibleTypes: string[] = ['Relation']
export const isRelation = (obj?: { __typename?: any } | null): obj is Relation => {
if (!obj?.__typename) throw new Error('__typename is missing in "isRelation"')
@@ -7590,6 +7510,86 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const ResendEmailVerificationToken_possibleTypes: string[] = ['ResendEmailVerificationToken']
export const isResendEmailVerificationToken = (obj?: { __typename?: any } | null): obj is ResendEmailVerificationToken => {
if (!obj?.__typename) throw new Error('__typename is missing in "isResendEmailVerificationToken"')
return ResendEmailVerificationToken_possibleTypes.includes(obj.__typename)
}
const DeleteSso_possibleTypes: string[] = ['DeleteSso']
export const isDeleteSso = (obj?: { __typename?: any } | null): obj is DeleteSso => {
if (!obj?.__typename) throw new Error('__typename is missing in "isDeleteSso"')
return DeleteSso_possibleTypes.includes(obj.__typename)
}
const EditSso_possibleTypes: string[] = ['EditSso']
export const isEditSso = (obj?: { __typename?: any } | null): obj is EditSso => {
if (!obj?.__typename) throw new Error('__typename is missing in "isEditSso"')
return EditSso_possibleTypes.includes(obj.__typename)
}
const WorkspaceNameAndId_possibleTypes: string[] = ['WorkspaceNameAndId']
export const isWorkspaceNameAndId = (obj?: { __typename?: any } | null): obj is WorkspaceNameAndId => {
if (!obj?.__typename) throw new Error('__typename is missing in "isWorkspaceNameAndId"')
return WorkspaceNameAndId_possibleTypes.includes(obj.__typename)
}
const FindAvailableSSOIDP_possibleTypes: string[] = ['FindAvailableSSOIDP']
export const isFindAvailableSSOIDP = (obj?: { __typename?: any } | null): obj is FindAvailableSSOIDP => {
if (!obj?.__typename) throw new Error('__typename is missing in "isFindAvailableSSOIDP"')
return FindAvailableSSOIDP_possibleTypes.includes(obj.__typename)
}
const SetupSso_possibleTypes: string[] = ['SetupSso']
export const isSetupSso = (obj?: { __typename?: any } | null): obj is SetupSso => {
if (!obj?.__typename) throw new Error('__typename is missing in "isSetupSso"')
return SetupSso_possibleTypes.includes(obj.__typename)
}
const SSOConnection_possibleTypes: string[] = ['SSOConnection']
export const isSSOConnection = (obj?: { __typename?: any } | null): obj is SSOConnection => {
if (!obj?.__typename) throw new Error('__typename is missing in "isSSOConnection"')
return SSOConnection_possibleTypes.includes(obj.__typename)
}
const AvailableWorkspace_possibleTypes: string[] = ['AvailableWorkspace']
export const isAvailableWorkspace = (obj?: { __typename?: any } | null): obj is AvailableWorkspace => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAvailableWorkspace"')
return AvailableWorkspace_possibleTypes.includes(obj.__typename)
}
const AvailableWorkspaces_possibleTypes: string[] = ['AvailableWorkspaces']
export const isAvailableWorkspaces = (obj?: { __typename?: any } | null): obj is AvailableWorkspaces => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAvailableWorkspaces"')
return AvailableWorkspaces_possibleTypes.includes(obj.__typename)
}
const DeletedWorkspaceMember_possibleTypes: string[] = ['DeletedWorkspaceMember']
export const isDeletedWorkspaceMember = (obj?: { __typename?: any } | null): obj is DeletedWorkspaceMember => {
if (!obj?.__typename) throw new Error('__typename is missing in "isDeletedWorkspaceMember"')
return DeletedWorkspaceMember_possibleTypes.includes(obj.__typename)
}
const BillingEntitlement_possibleTypes: string[] = ['BillingEntitlement']
export const isBillingEntitlement = (obj?: { __typename?: any } | null): obj is BillingEntitlement => {
if (!obj?.__typename) throw new Error('__typename is missing in "isBillingEntitlement"')
@@ -56,7 +56,7 @@ export default {
181,
184,
187,
210,
200,
225,
261,
262,
@@ -946,10 +946,10 @@ export default {
3
],
"relation": [
209
199
],
"morphRelations": [
209
199
],
"object": [
55
@@ -1008,7 +1008,7 @@ export default {
45
],
"objectMetadata": [
217,
207,
{
"paging": [
48,
@@ -1021,7 +1021,7 @@ export default {
}
],
"indexFieldMetadatas": [
215,
205,
{
"paging": [
48,
@@ -1266,7 +1266,7 @@ export default {
46
],
"fields": [
222,
212,
{
"paging": [
48,
@@ -1279,7 +1279,7 @@ export default {
}
],
"indexMetadatas": [
220,
210,
{
"paging": [
48,
@@ -1976,7 +1976,7 @@ export default {
20
],
"deletedWorkspaceMembers": [
208
223
],
"hasPassword": [
6
@@ -1988,7 +1988,7 @@ export default {
17
],
"availableWorkspaces": [
207
222
],
"__typename": [
1
@@ -3857,6 +3857,176 @@ export default {
1
]
},
"Relation": {
"type": [
200
],
"sourceObjectMetadata": [
55
],
"targetObjectMetadata": [
55
],
"sourceFieldMetadata": [
43
],
"targetFieldMetadata": [
43
],
"__typename": [
1
]
},
"RelationType": {},
"IndexEdge": {
"node": [
46
],
"cursor": [
49
],
"__typename": [
1
]
},
"PageInfo": {
"hasNextPage": [
6
],
"hasPreviousPage": [
6
],
"startCursor": [
49
],
"endCursor": [
49
],
"__typename": [
1
]
},
"IndexConnection": {
"pageInfo": [
202
],
"edges": [
201
],
"__typename": [
1
]
},
"IndexFieldEdge": {
"node": [
45
],
"cursor": [
49
],
"__typename": [
1
]
},
"IndexIndexFieldMetadatasConnection": {
"pageInfo": [
202
],
"edges": [
204
],
"__typename": [
1
]
},
"ObjectEdge": {
"node": [
55
],
"cursor": [
49
],
"__typename": [
1
]
},
"IndexObjectMetadataConnection": {
"pageInfo": [
202
],
"edges": [
206
],
"__typename": [
1
]
},
"ObjectRecordCount": {
"objectNamePlural": [
1
],
"totalCount": [
21
],
"__typename": [
1
]
},
"ObjectConnection": {
"pageInfo": [
202
],
"edges": [
206
],
"__typename": [
1
]
},
"ObjectIndexMetadatasConnection": {
"pageInfo": [
202
],
"edges": [
201
],
"__typename": [
1
]
},
"FieldEdge": {
"node": [
43
],
"cursor": [
49
],
"__typename": [
1
]
},
"ObjectFieldsConnection": {
"pageInfo": [
202
],
"edges": [
211
],
"__typename": [
1
]
},
"FieldConnection": {
"pageInfo": [
202
],
"edges": [
211
],
"__typename": [
1
]
},
"ResendEmailVerificationToken": {
"success": [
6
@@ -3921,7 +4091,7 @@ export default {
174
],
"workspace": [
202
217
],
"__typename": [
1
@@ -3990,7 +4160,7 @@ export default {
1
],
"sso": [
205
220
],
"__typename": [
1
@@ -3998,10 +4168,10 @@ export default {
},
"AvailableWorkspaces": {
"availableWorkspacesForSignIn": [
206
221
],
"availableWorkspacesForSignUp": [
206
221
],
"__typename": [
1
@@ -4027,176 +4197,6 @@ export default {
1
]
},
"Relation": {
"type": [
210
],
"sourceObjectMetadata": [
55
],
"targetObjectMetadata": [
55
],
"sourceFieldMetadata": [
43
],
"targetFieldMetadata": [
43
],
"__typename": [
1
]
},
"RelationType": {},
"IndexEdge": {
"node": [
46
],
"cursor": [
49
],
"__typename": [
1
]
},
"PageInfo": {
"hasNextPage": [
6
],
"hasPreviousPage": [
6
],
"startCursor": [
49
],
"endCursor": [
49
],
"__typename": [
1
]
},
"IndexConnection": {
"pageInfo": [
212
],
"edges": [
211
],
"__typename": [
1
]
},
"IndexFieldEdge": {
"node": [
45
],
"cursor": [
49
],
"__typename": [
1
]
},
"IndexIndexFieldMetadatasConnection": {
"pageInfo": [
212
],
"edges": [
214
],
"__typename": [
1
]
},
"ObjectEdge": {
"node": [
55
],
"cursor": [
49
],
"__typename": [
1
]
},
"IndexObjectMetadataConnection": {
"pageInfo": [
212
],
"edges": [
216
],
"__typename": [
1
]
},
"ObjectRecordCount": {
"objectNamePlural": [
1
],
"totalCount": [
21
],
"__typename": [
1
]
},
"ObjectConnection": {
"pageInfo": [
212
],
"edges": [
216
],
"__typename": [
1
]
},
"ObjectIndexMetadatasConnection": {
"pageInfo": [
212
],
"edges": [
211
],
"__typename": [
1
]
},
"FieldEdge": {
"node": [
43
],
"cursor": [
49
],
"__typename": [
1
]
},
"ObjectFieldsConnection": {
"pageInfo": [
212
],
"edges": [
221
],
"__typename": [
1
]
},
"FieldConnection": {
"pageInfo": [
212
],
"edges": [
221
],
"__typename": [
1
]
},
"BillingEntitlement": {
"key": [
225
@@ -4310,7 +4310,7 @@ export default {
234
],
"availableWorkspaces": [
207
222
],
"__typename": [
1
@@ -6071,7 +6071,7 @@ export default {
}
],
"objectRecordCounts": [
218
208
],
"object": [
55,
@@ -6083,7 +6083,7 @@ export default {
}
],
"objects": [
219,
209,
{
"paging": [
48,
@@ -6105,7 +6105,7 @@ export default {
}
],
"indexMetadatas": [
213,
203,
{
"paging": [
48,
@@ -6154,7 +6154,7 @@ export default {
}
],
"fields": [
223,
213,
{
"paging": [
48,
@@ -6425,7 +6425,7 @@ export default {
}
],
"getSSOIdentityProviders": [
203
218
],
"eventLogs": [
289,
@@ -8393,7 +8393,7 @@ export default {
}
],
"resendEmailVerificationToken": [
199,
214,
{
"email": [
1,
@@ -8460,7 +8460,7 @@ export default {
}
],
"createOIDCIdentityProvider": [
204,
219,
{
"input": [
455,
@@ -8469,7 +8469,7 @@ export default {
}
],
"createSAMLIdentityProvider": [
204,
219,
{
"input": [
456,
@@ -8478,7 +8478,7 @@ export default {
}
],
"deleteSSOIdentityProvider": [
200,
215,
{
"input": [
457,
@@ -8487,7 +8487,7 @@ export default {
}
],
"editSSOIdentityProvider": [
201,
216,
{
"input": [
458,
@@ -137,7 +137,7 @@ export class ApplicationInstallService {
resolvedPackage.packageJson.engines?.['twenty'];
const versionValidation =
this.applicationVersionValidationService.validateServerCompatibility(
await this.applicationVersionValidationService.validateServerCompatibility(
requiredServerVersion,
);
@@ -7,11 +7,13 @@ import { ApplicationEntity } from 'src/engine/core-modules/application/applicati
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
import { UpgradeModule } from 'src/engine/core-modules/upgrade/upgrade.module';
@Module({
imports: [
FileStorageModule,
TwentyConfigModule,
UpgradeModule,
TypeOrmModule.forFeature([FileEntity, ApplicationEntity]),
],
providers: [
@@ -2,8 +2,7 @@ import { Injectable } from '@nestjs/common';
import semver from 'semver';
import { isDefined } from 'twenty-shared/utils';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { UpgradeMigrationService } from 'src/engine/core-modules/upgrade/services/upgrade-migration.service';
export type VersionValidationFailureReason =
| 'INVALID_REQUIRED_VERSION'
@@ -20,11 +19,13 @@ export type VersionValidationResult =
@Injectable()
export class ApplicationVersionValidationService {
constructor(private readonly twentyConfigService: TwentyConfigService) {}
constructor(
private readonly upgradeMigrationService: UpgradeMigrationService,
) {}
validateServerCompatibility(
async validateServerCompatibility(
requiredServerVersion: string | undefined,
): VersionValidationResult {
): Promise<VersionValidationResult> {
if (!isDefined(requiredServerVersion)) {
return { compatible: true };
}
@@ -37,21 +38,25 @@ export class ApplicationVersionValidationService {
};
}
const serverVersion = this.twentyConfigService.get('APP_VERSION');
const inferredServerVersion =
await this.upgradeMigrationService.getInferredVersion();
if (!isDefined(serverVersion) || !isDefined(semver.valid(serverVersion))) {
if (
!isDefined(inferredServerVersion) ||
!isDefined(semver.valid(inferredServerVersion))
) {
return {
compatible: false,
reason: 'INVALID_SERVER_VERSION',
message: `Cannot verify server compatibility: APP_VERSION "${serverVersion ?? 'undefined'}" is not a valid semver version. Self-hosted instances must set a valid APP_VERSION.`,
message: `Cannot verify server compatibility: inferred server version "${inferredServerVersion ?? 'undefined'}" is not a valid semver version.`,
};
}
if (!semver.satisfies(serverVersion, requiredServerVersion)) {
if (!semver.satisfies(inferredServerVersion, requiredServerVersion)) {
return {
compatible: false,
reason: 'INCOMPATIBLE',
message: `App requires Twenty server ${requiredServerVersion} but this server is ${serverVersion}.`,
message: `App requires Twenty server ${requiredServerVersion} but this server is ${inferredServerVersion}.`,
};
}
@@ -96,7 +96,7 @@ export class ApplicationTarballService {
const requiredServerVersion = packageJson?.engines?.twenty;
const versionValidation =
this.applicationVersionValidationService.validateServerCompatibility(
await this.applicationVersionValidationService.validateServerCompatibility(
requiredServerVersion,
);
@@ -50,6 +50,7 @@ const buildWorkspaceCacheGetMock = (
describe('UpgradeStatusService', () => {
let service: UpgradeStatusService;
let getLastAttemptedInstanceCommand: jest.Mock;
let getInferredVersion: jest.Mock;
let getWorkspaceLastAttemptedCommandName: jest.Mock;
let workspaceFind: jest.Mock;
let coreEntityCacheGet: jest.Mock;
@@ -68,6 +69,12 @@ describe('UpgradeStatusService', () => {
beforeEach(async () => {
getLastAttemptedInstanceCommand = jest.fn();
getInferredVersion = jest.fn(async (name?: string) => {
if (!name) return null;
const idx = name.indexOf('_');
return idx === -1 ? null : name.substring(0, idx);
});
getWorkspaceLastAttemptedCommandName = jest.fn();
workspaceFind = jest.fn().mockResolvedValue([]);
coreEntityCacheGet = jest.fn().mockResolvedValue(null);
@@ -84,6 +91,7 @@ describe('UpgradeStatusService', () => {
provide: UpgradeMigrationService,
useValue: {
getLastAttemptedInstanceCommand,
getInferredVersion,
getWorkspaceLastAttemptedCommandName,
},
},
@@ -9,6 +9,7 @@ import {
UpgradeMigrationStatus,
} from 'src/engine/core-modules/upgrade/upgrade-migration.entity';
import { formatUpgradeErrorForStorage } from 'src/engine/core-modules/upgrade/utils/format-upgrade-error-for-storage.util';
import { extractVersionFromCommandName } from 'src/engine/core-modules/upgrade/utils/extract-version-from-command-name.util';
export type WorkspaceLastAttemptedCommand = {
workspaceId: string;
@@ -27,6 +28,18 @@ export class UpgradeMigrationService {
private readonly upgradeMigrationRepository: Repository<UpgradeMigrationEntity>,
) {}
async getInferredVersion(commandName?: string): Promise<string | null> {
if (isDefined(commandName)) {
return extractVersionFromCommandName(commandName);
}
const migration = await this.getLastAttemptedInstanceCommand();
return isDefined(migration)
? extractVersionFromCommandName(migration.name)
: null;
}
async isLastAttemptCompleted({
name,
workspaceId,
@@ -10,7 +10,7 @@ import { UpgradeMigrationService } from 'src/engine/core-modules/upgrade/service
import { UpgradeSequenceReaderService } from 'src/engine/core-modules/upgrade/services/upgrade-sequence-reader.service';
import { UpgradeStatusCacheService } from 'src/engine/core-modules/upgrade/services/upgrade-status-cache.service';
import { type UpgradeMigrationStatus } from 'src/engine/core-modules/upgrade/upgrade-migration.entity';
import { extractVersionFromCommandName } from 'src/engine/core-modules/upgrade/utils/extract-version-from-command-name.util';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { In, Repository } from 'typeorm';
@@ -91,7 +91,10 @@ export class UpgradeStatusService {
step.kind === 'fast-instance' || step.kind === 'slow-instance',
);
return this.buildCursorStatus(migration, lastInstanceStep?.name ?? null);
return await this.buildCursorStatus(
migration,
lastInstanceStep?.name ?? null,
);
}
async getWorkspaceStatuses(
@@ -122,14 +125,16 @@ export class UpgradeStatusService {
const lastStepName =
sequence.length > 0 ? sequence[sequence.length - 1].name : null;
return workspaces.map((workspace) => ({
...this.buildCursorStatus(
cursors.get(workspace.id) ?? null,
lastStepName,
),
workspaceId: workspace.id,
displayName: workspace.displayName ?? null,
}));
return Promise.all(
workspaces.map(async (workspace) => ({
...(await this.buildCursorStatus(
cursors.get(workspace.id) ?? null,
lastStepName,
)),
workspaceId: workspace.id,
displayName: workspace.displayName ?? null,
})),
);
}
async getInstanceAndAllWorkspacesStatus(): Promise<InstanceAndAllWorkspacesUpgradeStatus> {
@@ -209,10 +214,10 @@ export class UpgradeStatusService {
await this.upgradeStatusCacheService.invalidate();
}
private buildCursorStatus(
private async buildCursorStatus(
migration: LatestUpgradeCommand | null,
lastExpectedCommandName: string | null,
): InstanceUpgradeStatus {
): Promise<InstanceUpgradeStatus> {
if (!migration) {
return {
inferredVersion: null,
@@ -224,7 +229,9 @@ export class UpgradeStatusService {
const health = deriveHealth(migration, lastExpectedCommandName);
return {
inferredVersion: extractVersionFromCommandName(migration.name),
inferredVersion: await this.upgradeMigrationService.getInferredVersion(
migration.name,
),
health,
latestCommand: {
name: migration.name,