feat(server): modularized mocks + workspace mocks for Mike (#2534)
* modularized mocks foundation + updated workspaces gql * base queries done * mutations done * cleaner API for mock helpers * greatly improved mock definition DX
This commit is contained in:
Родитель
eefeef1ee4
Коммит
1e5dadacd3
|
@ -68,6 +68,7 @@ export type AdminQueries = {
|
|||
projectList: ProjectCollection;
|
||||
serverStatistics: ServerStatistics;
|
||||
userList: AdminUserList;
|
||||
workspaceList: WorkspaceCollection;
|
||||
};
|
||||
|
||||
|
||||
|
@ -94,6 +95,13 @@ export type AdminQueriesUserListArgs = {
|
|||
role?: InputMaybe<ServerRole>;
|
||||
};
|
||||
|
||||
|
||||
export type AdminQueriesWorkspaceListArgs = {
|
||||
cursor?: InputMaybe<Scalars['String']['input']>;
|
||||
limit?: Scalars['Int']['input'];
|
||||
query?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type AdminUserList = {
|
||||
__typename?: 'AdminUserList';
|
||||
cursor?: Maybe<Scalars['String']['output']>;
|
||||
|
@ -1305,6 +1313,7 @@ export type Mutation = {
|
|||
webhookDelete: Scalars['String']['output'];
|
||||
/** Updates an existing webhook */
|
||||
webhookUpdate: Scalars['String']['output'];
|
||||
workspaceMutations: WorkspaceMutations;
|
||||
};
|
||||
|
||||
|
||||
|
@ -1686,6 +1695,23 @@ export type PendingStreamCollaborator = {
|
|||
user?: Maybe<LimitedUser>;
|
||||
};
|
||||
|
||||
export type PendingWorkspaceCollaborator = {
|
||||
__typename?: 'PendingWorkspaceCollaborator';
|
||||
id: Scalars['ID']['output'];
|
||||
inviteId: Scalars['String']['output'];
|
||||
invitedBy: LimitedUser;
|
||||
/** Target workspace role */
|
||||
role: Scalars['String']['output'];
|
||||
/** E-mail address or name of the invited user */
|
||||
title: Scalars['String']['output'];
|
||||
/** Only available if the active user is the pending workspace collaborator */
|
||||
token?: Maybe<Scalars['String']['output']>;
|
||||
/** Set only if user is registered */
|
||||
user?: Maybe<LimitedUser>;
|
||||
workspaceId: Scalars['String']['output'];
|
||||
workspaceName: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type Project = {
|
||||
__typename?: 'Project';
|
||||
allowPublicComments: Scalars['Boolean']['output'];
|
||||
|
@ -1737,6 +1763,7 @@ export type Project = {
|
|||
viewerResources: Array<ViewerResourceGroup>;
|
||||
visibility: ProjectVisibility;
|
||||
webhooks: WebhookCollection;
|
||||
workspace?: Maybe<Workspace>;
|
||||
};
|
||||
|
||||
|
||||
|
@ -2383,6 +2410,14 @@ export type Query = {
|
|||
* The query looks for matches in name & email
|
||||
*/
|
||||
userSearch: UserSearchResultCollection;
|
||||
workspace: Workspace;
|
||||
/**
|
||||
* Look for an invitation to a workspace, for the current user (authed or not). If token
|
||||
* isn't specified, the server will look for any valid invite.
|
||||
*
|
||||
* If token is specified, it will return the corresponding invite even if it belongs to a different user.
|
||||
*/
|
||||
workspaceInvite?: Maybe<PendingWorkspaceCollaborator>;
|
||||
};
|
||||
|
||||
|
||||
|
@ -2508,6 +2543,17 @@ export type QueryUserSearchArgs = {
|
|||
query: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryWorkspaceArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryWorkspaceInviteArgs = {
|
||||
token?: InputMaybe<Scalars['String']['input']>;
|
||||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
/** Deprecated: Used by old stream-based mutations */
|
||||
export type ReplyCreateInput = {
|
||||
/** IDs of uploaded blobs that should be attached to this reply */
|
||||
|
@ -2610,6 +2656,7 @@ export type ServerInfo = {
|
|||
serverRoles: Array<ServerRoleItem>;
|
||||
termsOfService?: Maybe<Scalars['String']['output']>;
|
||||
version?: Maybe<Scalars['String']['output']>;
|
||||
workspaces: ServerWorkspacesInfo;
|
||||
};
|
||||
|
||||
export type ServerInfoUpdateInput = {
|
||||
|
@ -2678,6 +2725,15 @@ export type ServerStats = {
|
|||
userHistory?: Maybe<Array<Maybe<Scalars['JSONObject']['output']>>>;
|
||||
};
|
||||
|
||||
export type ServerWorkspacesInfo = {
|
||||
__typename?: 'ServerWorkspacesInfo';
|
||||
/**
|
||||
* This is a backend control variable for the workspaces feature set.
|
||||
* Since workspaces need a backend logic to be enabled, this is not enough as a feature flag.
|
||||
*/
|
||||
workspacesEnabled: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
export type SmartTextEditorValue = {
|
||||
__typename?: 'SmartTextEditorValue';
|
||||
/** File attachments, if any */
|
||||
|
@ -3298,6 +3354,10 @@ export type User = {
|
|||
* Note: Only count resolution is currently implemented
|
||||
*/
|
||||
versions: CountOnlyCollection;
|
||||
/** Get all invitations to workspaces that the active user has */
|
||||
workspaceInvites: Array<PendingWorkspaceCollaborator>;
|
||||
/** Get the workspaces for the user */
|
||||
workspaces: WorkspaceCollection;
|
||||
};
|
||||
|
||||
|
||||
|
@ -3385,6 +3445,17 @@ export type UserVersionsArgs = {
|
|||
limit?: Scalars['Int']['input'];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Full user type, should only be used in the context of admin operations or
|
||||
* when a user is reading/writing info about himself
|
||||
*/
|
||||
export type UserWorkspacesArgs = {
|
||||
cursor?: InputMaybe<Scalars['String']['input']>;
|
||||
filter?: InputMaybe<UserWorkspacesFilter>;
|
||||
limit?: Scalars['Int']['input'];
|
||||
};
|
||||
|
||||
export type UserAutomateInfo = {
|
||||
__typename?: 'UserAutomateInfo';
|
||||
availableGithubOrgs: Array<Scalars['String']['output']>;
|
||||
|
@ -3435,6 +3506,10 @@ export type UserUpdateInput = {
|
|||
name?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type UserWorkspacesFilter = {
|
||||
search?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type Version = {
|
||||
__typename?: 'Version';
|
||||
authorUser?: Maybe<LimitedUser>;
|
||||
|
@ -3647,6 +3722,153 @@ export type WebhookUpdateInput = {
|
|||
url?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type Workspace = {
|
||||
__typename?: 'Workspace';
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
description?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['ID']['output'];
|
||||
/** Only available to workspace owners */
|
||||
invitedTeam?: Maybe<Array<PendingWorkspaceCollaborator>>;
|
||||
name: Scalars['String']['output'];
|
||||
projects: ProjectCollection;
|
||||
/** Active user's role for this workspace. `null` if request is not authenticated, or the workspace is not explicitly shared with you. */
|
||||
role?: Maybe<Scalars['String']['output']>;
|
||||
team: Array<WorkspaceCollaborator>;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceProjectsArgs = {
|
||||
cursor?: InputMaybe<Scalars['String']['input']>;
|
||||
filter?: InputMaybe<UserProjectsFilter>;
|
||||
limit?: Scalars['Int']['input'];
|
||||
};
|
||||
|
||||
export type WorkspaceCollaborator = {
|
||||
__typename?: 'WorkspaceCollaborator';
|
||||
id: Scalars['ID']['output'];
|
||||
role: Scalars['String']['output'];
|
||||
user: LimitedUser;
|
||||
};
|
||||
|
||||
export type WorkspaceCollection = {
|
||||
__typename?: 'WorkspaceCollection';
|
||||
cursor?: Maybe<Scalars['String']['output']>;
|
||||
items: Array<Workspace>;
|
||||
totalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type WorkspaceCreateInput = {
|
||||
description?: InputMaybe<Scalars['String']['input']>;
|
||||
logoUrl?: InputMaybe<Scalars['String']['input']>;
|
||||
name: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type WorkspaceInviteCreateInput = {
|
||||
/** Either this or userId must be filled */
|
||||
email?: InputMaybe<Scalars['String']['input']>;
|
||||
/** Defaults to the member role, if not specified */
|
||||
role?: InputMaybe<WorkspaceRole>;
|
||||
/** Either this or email must be filled */
|
||||
userId?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type WorkspaceInviteMutations = {
|
||||
__typename?: 'WorkspaceInviteMutations';
|
||||
batchCreate: Workspace;
|
||||
cancel: Workspace;
|
||||
create: Workspace;
|
||||
use: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceInviteMutationsBatchCreateArgs = {
|
||||
input: Array<WorkspaceInviteCreateInput>;
|
||||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceInviteMutationsCancelArgs = {
|
||||
inviteId: Scalars['String']['input'];
|
||||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceInviteMutationsCreateArgs = {
|
||||
input: WorkspaceInviteCreateInput;
|
||||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceInviteMutationsUseArgs = {
|
||||
input: WorkspaceInviteUseInput;
|
||||
};
|
||||
|
||||
export type WorkspaceInviteUseInput = {
|
||||
accept: Scalars['Boolean']['input'];
|
||||
token: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type WorkspaceMutations = {
|
||||
__typename?: 'WorkspaceMutations';
|
||||
create: Workspace;
|
||||
delete: Workspace;
|
||||
deleteRole: Scalars['Boolean']['output'];
|
||||
invites: WorkspaceInviteMutations;
|
||||
update: Workspace;
|
||||
/** TODO: `@hasWorkspaceRole(role: WORKSPACE_ADMIN)` for role changes */
|
||||
updateRole: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceMutationsCreateArgs = {
|
||||
input: WorkspaceCreateInput;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceMutationsDeleteArgs = {
|
||||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceMutationsDeleteRoleArgs = {
|
||||
input: WorkspaceRoleDeleteInput;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceMutationsUpdateArgs = {
|
||||
input: WorkspaceUpdateInput;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceMutationsUpdateRoleArgs = {
|
||||
input: WorkspaceRoleUpdateInput;
|
||||
};
|
||||
|
||||
export enum WorkspaceRole {
|
||||
Admin = 'ADMIN',
|
||||
Guest = 'GUEST',
|
||||
Member = 'MEMBER'
|
||||
}
|
||||
|
||||
export type WorkspaceRoleDeleteInput = {
|
||||
userId: Scalars['String']['input'];
|
||||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type WorkspaceRoleUpdateInput = {
|
||||
role: WorkspaceRole;
|
||||
userId: Scalars['String']['input'];
|
||||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type WorkspaceUpdateInput = {
|
||||
description?: InputMaybe<Scalars['String']['input']>;
|
||||
id: Scalars['String']['input'];
|
||||
logoUrl?: InputMaybe<Scalars['String']['input']>;
|
||||
name?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type AuthRegisterPanelServerInfoFragment = { __typename?: 'ServerInfo', inviteOnly?: boolean | null };
|
||||
|
||||
export type RegisterPanelServerInviteQueryVariables = Exact<{
|
||||
|
|
|
@ -13,6 +13,10 @@ SESSION_SECRET="-> FILL IN <-"
|
|||
# Redis connection: default for local development environment
|
||||
REDIS_URL="redis://127.0.0.1:6379"
|
||||
|
||||
# Enable GraphQL API mocks for specific speckle modules by specifying them in a comma delimited list
|
||||
# Example: MOCKED_API_MODULES=core,automate
|
||||
MOCKED_API_MODULES=
|
||||
|
||||
############################################################
|
||||
# Frontend 2.0 settings
|
||||
# Settings for making the server work with Frontend 2.0
|
||||
|
|
|
@ -2,6 +2,14 @@ extend type Query {
|
|||
workspace(id: String!): Workspace!
|
||||
@hasServerRole(role: SERVER_USER)
|
||||
@hasScope(scope: "workspace:read")
|
||||
|
||||
"""
|
||||
Look for an invitation to a workspace, for the current user (authed or not). If token
|
||||
isn't specified, the server will look for any valid invite.
|
||||
|
||||
If token is specified, it will return the corresponding invite even if it belongs to a different user.
|
||||
"""
|
||||
workspaceInvite(workspaceId: String!, token: String): PendingWorkspaceCollaborator
|
||||
}
|
||||
|
||||
input WorkspaceCreateInput {
|
||||
|
@ -36,14 +44,14 @@ type WorkspaceMutations {
|
|||
create(input: WorkspaceCreateInput!): Workspace!
|
||||
@hasServerRole(role: SERVER_ADMIN)
|
||||
@hasScope(scope: "workspace:create")
|
||||
delete(workspaceId: String!): Workspace! @hasScope(scope: "workspace:delete")
|
||||
delete(workspaceId: String!): Boolean! @hasScope(scope: "workspace:delete")
|
||||
update(input: WorkspaceUpdateInput!): Workspace! @hasScope(scope: "workspace:update")
|
||||
"""
|
||||
TODO: `@hasWorkspaceRole(role: WORKSPACE_ADMIN)` for role changes
|
||||
"""
|
||||
updateRole(input: WorkspaceRoleUpdateInput!): Boolean!
|
||||
updateRole(input: WorkspaceRoleUpdateInput!): Workspace!
|
||||
@hasScope(scope: "workspace:update")
|
||||
deleteRole(input: WorkspaceRoleDeleteInput!): Boolean!
|
||||
deleteRole(input: WorkspaceRoleDeleteInput!): Workspace!
|
||||
@hasScope(scope: "workspace:update")
|
||||
invites: WorkspaceInviteMutations!
|
||||
}
|
||||
|
@ -64,7 +72,6 @@ input WorkspaceInviteCreateInput {
|
|||
}
|
||||
|
||||
input WorkspaceInviteUseInput {
|
||||
workspaceId: String!
|
||||
token: String!
|
||||
accept: Boolean!
|
||||
}
|
||||
|
@ -93,6 +100,9 @@ type Workspace {
|
|||
"""
|
||||
role: String
|
||||
team: [WorkspaceCollaborator!]!
|
||||
"""
|
||||
Only available to workspace owners
|
||||
"""
|
||||
invitedTeam: [PendingWorkspaceCollaborator!]
|
||||
projects(
|
||||
limit: Int! = 25
|
||||
|
@ -116,6 +126,9 @@ type PendingWorkspaceCollaborator {
|
|||
E-mail address or name of the invited user
|
||||
"""
|
||||
title: String!
|
||||
"""
|
||||
Target workspace role
|
||||
"""
|
||||
role: String!
|
||||
invitedBy: LimitedUser!
|
||||
"""
|
||||
|
@ -143,6 +156,14 @@ extend type User {
|
|||
cursor: String = null
|
||||
filter: UserWorkspacesFilter
|
||||
): WorkspaceCollection! @isOwner
|
||||
|
||||
"""
|
||||
Get all invitations to workspaces that the active user has
|
||||
"""
|
||||
workspaceInvites: [PendingWorkspaceCollaborator!]!
|
||||
@hasServerRole(role: SERVER_GUEST)
|
||||
@hasScope(scope: "workspace:read")
|
||||
@isOwner
|
||||
}
|
||||
|
||||
extend type Project {
|
||||
|
|
|
@ -0,0 +1,381 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
AutomationNotFoundError,
|
||||
FunctionNotFoundError
|
||||
} from '@/modules/automate/errors/management'
|
||||
import { functionTemplateRepos } from '@/modules/automate/helpers/executionEngine'
|
||||
import {
|
||||
AutomationRevisionTriggerDefinitionGraphQLReturn,
|
||||
AutomationRunTriggerGraphQLReturn
|
||||
} from '@/modules/automate/helpers/graphTypes'
|
||||
import { VersionCreationTriggerType } from '@/modules/automate/helpers/types'
|
||||
import { BranchCommits, Branches, Commits } from '@/modules/core/dbSchema'
|
||||
import { AutomateRunStatus } from '@/modules/core/graph/generated/graphql'
|
||||
import { SpeckleModuleMocksConfig } from '@/modules/shared/helpers/mocks'
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { Automate, isNullOrUndefined, SourceAppNames } from '@speckle/shared'
|
||||
import dayjs from 'dayjs'
|
||||
import { times } from 'lodash'
|
||||
|
||||
const { FF_AUTOMATE_MODULE_ENABLED } = getFeatureFlags()
|
||||
|
||||
const getRandomModelVersion = async (offset?: number) => {
|
||||
const versionQ = Commits.knex()
|
||||
.join(BranchCommits.name, BranchCommits.col.commitId, Commits.col.id)
|
||||
.first()
|
||||
if (offset) versionQ.offset(offset)
|
||||
const version = await versionQ
|
||||
|
||||
if (!version) {
|
||||
throw new Error("Couldn't find even one commit in the DB, please create some")
|
||||
}
|
||||
|
||||
const model = await Branches.knex()
|
||||
.join(BranchCommits.name, BranchCommits.col.branchId, Branches.col.id)
|
||||
.where(BranchCommits.col.commitId, version.id)
|
||||
.first()
|
||||
|
||||
if (!model) {
|
||||
throw new Error(
|
||||
`Couldn't find branch for first commit #${version.id}, please create one `
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
model,
|
||||
version
|
||||
}
|
||||
}
|
||||
|
||||
const mocks: SpeckleModuleMocksConfig = FF_AUTOMATE_MODULE_ENABLED
|
||||
? {
|
||||
resolvers: ({ store }) => ({
|
||||
AutomationRevisionTriggerDefinition: {
|
||||
__resolveType: () => 'VersionCreatedTriggerDefinition'
|
||||
},
|
||||
AutomationRunTrigger: {
|
||||
__resolveType: () => 'VersionCreatedTrigger'
|
||||
},
|
||||
VersionCreatedTriggerDefinition: {
|
||||
model: store.get('Model') as any
|
||||
},
|
||||
VersionCreatedTrigger: {
|
||||
model: store.get('Model') as any,
|
||||
version: store.get('Version') as any
|
||||
},
|
||||
Query: {
|
||||
automateFunctions: (_parent, args) => {
|
||||
const forceZero = false
|
||||
const count = forceZero ? 0 : faker.number.int({ min: 0, max: 20 })
|
||||
|
||||
const isFeatured = args.filter?.featuredFunctionsOnly
|
||||
|
||||
return {
|
||||
cursor: null,
|
||||
totalCount: count,
|
||||
items: times(count, () => store.get('AutomateFunction', { isFeatured }))
|
||||
} as any
|
||||
},
|
||||
automateFunction: (_parent, args) => {
|
||||
const id = args.id
|
||||
if (id === '404') {
|
||||
throw new FunctionNotFoundError()
|
||||
}
|
||||
|
||||
return store.get('AutomateFunction', { id }) as any
|
||||
}
|
||||
},
|
||||
Project: {
|
||||
automations: () => {
|
||||
const forceAutomations = false
|
||||
const forceNoAutomations = false
|
||||
|
||||
const limit = faker.number.int({ min: 0, max: 20 })
|
||||
let count
|
||||
if (forceNoAutomations) {
|
||||
count = 0
|
||||
} else {
|
||||
count = forceAutomations ? limit : faker.datatype.boolean() ? limit : 0
|
||||
}
|
||||
|
||||
return {
|
||||
cursor: null,
|
||||
totalCount: count,
|
||||
items: times(count, () => store.get('Automation'))
|
||||
} as any
|
||||
},
|
||||
automation: (_parent, args) => {
|
||||
if (args.id === '404') {
|
||||
throw new AutomationNotFoundError()
|
||||
}
|
||||
|
||||
return store.get('Automation', { id: args.id }) as any
|
||||
},
|
||||
blob: () => {
|
||||
return store.get('BlobMetadata') as any
|
||||
}
|
||||
},
|
||||
Model: {
|
||||
automationsStatus: async () => {
|
||||
const random = faker.datatype.boolean()
|
||||
return (random ? store.get('TriggeredAutomationsStatus') : null) as any
|
||||
}
|
||||
},
|
||||
Version: {
|
||||
automationsStatus: async () => {
|
||||
const random = faker.datatype.boolean()
|
||||
return (random ? store.get('TriggeredAutomationsStatus') : null) as any
|
||||
}
|
||||
},
|
||||
Automation: {
|
||||
creationPublicKeys: () => {
|
||||
// Random sized array of string keys
|
||||
return [...new Array(faker.number.int({ min: 0, max: 5 }))].map(() =>
|
||||
faker.string.uuid()
|
||||
)
|
||||
},
|
||||
runs: () => {
|
||||
const forceZero = false
|
||||
const count = forceZero ? 0 : faker.number.int({ min: 0, max: 20 })
|
||||
|
||||
return {
|
||||
cursor: null,
|
||||
totalCount: count,
|
||||
items: times(count, () => store.get('AutomateRun'))
|
||||
} as any
|
||||
},
|
||||
currentRevision: () => store.get('AutomationRevision') as any
|
||||
},
|
||||
AutomationRevision: {
|
||||
triggerDefinitions: async (parent) => {
|
||||
const rand = faker.number.int({ min: 0, max: 2 })
|
||||
const res = (
|
||||
await Promise.all([getRandomModelVersion(), getRandomModelVersion(1)])
|
||||
).slice(0, rand)
|
||||
|
||||
return res.map(
|
||||
(i): AutomationRevisionTriggerDefinitionGraphQLReturn => ({
|
||||
triggerType: VersionCreationTriggerType,
|
||||
triggeringId: i.model.id,
|
||||
automationRevisionId: parent.id
|
||||
})
|
||||
)
|
||||
},
|
||||
functions: () => [store.get('AutomateFunction') as any]
|
||||
},
|
||||
AutomationRevisionFunction: {
|
||||
parameters: () => ({}),
|
||||
release: () => store.get('AutomateFunctionRelease') as any
|
||||
},
|
||||
AutomateRun: {
|
||||
trigger: async (parent) => {
|
||||
const { version } = await getRandomModelVersion()
|
||||
|
||||
return <AutomationRunTriggerGraphQLReturn>{
|
||||
triggerType: VersionCreationTriggerType,
|
||||
triggeringId: version.id,
|
||||
automationRunId: parent.id
|
||||
}
|
||||
},
|
||||
automation: () => store.get('Automation') as any,
|
||||
status: () => faker.helpers.arrayElement(Object.values(AutomateRunStatus))
|
||||
},
|
||||
AutomateFunctionRun: {
|
||||
function: () => store.get('AutomateFunction') as any,
|
||||
status: () => faker.helpers.arrayElement(Object.values(AutomateRunStatus))
|
||||
},
|
||||
ProjectAutomationMutations: {
|
||||
create: (_parent, args) => {
|
||||
const {
|
||||
input: { name, enabled }
|
||||
} = args
|
||||
const automation = store.get('Automation') as any
|
||||
return {
|
||||
...automation,
|
||||
name,
|
||||
enabled
|
||||
}
|
||||
},
|
||||
update: (_parent, args) => {
|
||||
const {
|
||||
input: { id, name, enabled }
|
||||
} = args
|
||||
const automation = store.get('Automation') as any
|
||||
return {
|
||||
...automation,
|
||||
id,
|
||||
...(name?.length ? { name } : {}),
|
||||
...(isNullOrUndefined(enabled) ? {} : { enabled })
|
||||
}
|
||||
},
|
||||
trigger: () => faker.string.sample(10),
|
||||
createRevision: () => store.get('AutomationRevision') as any
|
||||
},
|
||||
UserAutomateInfo: {
|
||||
hasAutomateGithubApp: () => {
|
||||
return faker.datatype.boolean()
|
||||
},
|
||||
availableGithubOrgs: () => {
|
||||
// Random string array
|
||||
return [...new Array(faker.number.int({ min: 0, max: 5 }))].map(() =>
|
||||
faker.company.name()
|
||||
)
|
||||
}
|
||||
},
|
||||
AutomateFunction: {
|
||||
// creator: async (_parent, args, ctx) => {
|
||||
// const rand = faker.datatype.boolean()
|
||||
// const activeUser = ctx.userId
|
||||
// ? await ctx.loaders.users.getUser.load(ctx.userId)
|
||||
// : null
|
||||
|
||||
// return rand ? (store.get('LimitedUser') as any) : activeUser
|
||||
// }
|
||||
releases: () => store.get('AutomateFunctionReleaseCollection') as any,
|
||||
automationCount: () => faker.number.int({ min: 0, max: 99 })
|
||||
},
|
||||
AutomateFunctionRelease: {
|
||||
function: () => store.get('AutomateFunction') as any
|
||||
},
|
||||
TriggeredAutomationsStatus: {
|
||||
status: () => faker.helpers.arrayElement(Object.values(AutomateRunStatus))
|
||||
},
|
||||
AutomateMutations: {
|
||||
createFunction: () => store.get('AutomateFunction') as any,
|
||||
updateFunction: (_parent, args) => {
|
||||
const {
|
||||
input: { id, name, description, supportedSourceApps, tags }
|
||||
} = args
|
||||
const func = store.get('AutomateFunction', { id }) as any
|
||||
return {
|
||||
...func,
|
||||
id,
|
||||
...(name?.length ? { name } : {}),
|
||||
...(description?.length ? { description } : {}),
|
||||
...(supportedSourceApps?.length ? { supportedSourceApps } : {}),
|
||||
...(tags?.length ? { tags } : {})
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
mocks: {
|
||||
TriggeredAutomationsStatus: () => ({
|
||||
automationRuns: () => [
|
||||
...new Array(faker.datatype.number({ min: 1, max: 5 }))
|
||||
]
|
||||
}),
|
||||
AutomationRevision: () => ({
|
||||
functions: () => [undefined] // array of 1 always,
|
||||
}),
|
||||
Automation: () => ({
|
||||
name: () => faker.company.name(),
|
||||
enabled: () => faker.datatype.boolean()
|
||||
}),
|
||||
AutomateFunction: () => ({
|
||||
name: () => faker.commerce.productName(),
|
||||
isFeatured: () => faker.datatype.boolean(),
|
||||
logo: () => {
|
||||
const random = faker.datatype.boolean()
|
||||
return random
|
||||
? faker.image.imageUrl(undefined, undefined, undefined, true)
|
||||
: null
|
||||
},
|
||||
repoUrl: () =>
|
||||
'https://github.com/specklesystems/speckle-automate-code-compliance-window-safety',
|
||||
automationCount: () => faker.number.int({ min: 0, max: 99 }),
|
||||
description: () => {
|
||||
// Example markdown description
|
||||
return `# ${faker.commerce.productName()}\n${faker.lorem.paragraphs(
|
||||
1,
|
||||
'\n\n'
|
||||
)}\n## Features \n- ${faker.lorem.sentence()}\n - ${faker.lorem.sentence()}\n - ${faker.lorem.sentence()}`
|
||||
},
|
||||
supportedSourceApps: () => {
|
||||
const base = SourceAppNames
|
||||
|
||||
// Random assortment from base
|
||||
return base.filter(() => faker.datatype.boolean())
|
||||
},
|
||||
tags: () => {
|
||||
// Random string array
|
||||
return [...new Array(faker.number.int({ min: 0, max: 5 }))].map(() =>
|
||||
faker.lorem.word()
|
||||
)
|
||||
}
|
||||
}),
|
||||
AutomateFunctionRelease: () => ({
|
||||
versionTag: () => {
|
||||
// Fake semantic version
|
||||
return `${faker.number.int({
|
||||
min: 0,
|
||||
max: 9
|
||||
})}.${faker.number.int({
|
||||
min: 0,
|
||||
max: 9
|
||||
})}.${faker.number.int({ min: 0, max: 9 })}`
|
||||
},
|
||||
commitId: () => '0c259d384a4df3cce3f24667560e5124e68f202f',
|
||||
inputSchema: () => {
|
||||
// random fro 1 to 3
|
||||
const rand = faker.number.int({ min: 1, max: 3 })
|
||||
switch (rand) {
|
||||
case 1:
|
||||
return {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
$id: 'https://example.com/product.schema.json',
|
||||
title: 'Product',
|
||||
description: "A product from Acme's catalog",
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
desciption: 'Random name',
|
||||
type: 'string'
|
||||
},
|
||||
productId: {
|
||||
description: 'The unique identifier for a product',
|
||||
type: 'integer'
|
||||
}
|
||||
},
|
||||
required: ['productId']
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
}),
|
||||
AutomateRun: () => ({
|
||||
reason: () => faker.lorem.sentence(),
|
||||
id: () => faker.string.alphanumeric(20),
|
||||
createdAt: () =>
|
||||
faker.date
|
||||
.recent(undefined, dayjs().subtract(1, 'day').toDate())
|
||||
.toISOString(),
|
||||
updatedAt: () => faker.date.recent().toISOString(),
|
||||
functionRuns: () => [...new Array(faker.number.int({ min: 1, max: 5 }))],
|
||||
statusMessage: () => faker.lorem.sentence()
|
||||
}),
|
||||
AutomateFunctionRun: () => ({
|
||||
contextView: () => `/`,
|
||||
elapsed: () => faker.number.int({ min: 0, max: 600 }),
|
||||
statusMessage: () => faker.lorem.sentence(),
|
||||
results: (): Automate.AutomateTypes.ResultsSchema => {
|
||||
return {
|
||||
version: Automate.AutomateTypes.RESULTS_SCHEMA_VERSION,
|
||||
values: {
|
||||
objectResults: [],
|
||||
blobIds: [...new Array(faker.number.int({ min: 0, max: 5 }))].map(() =>
|
||||
faker.string.uuid()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
ServerAutomateInfo: () => ({
|
||||
availableFunctionTemplates: () => functionTemplateRepos
|
||||
})
|
||||
}
|
||||
}
|
||||
: {}
|
||||
export default mocks
|
|
@ -1714,6 +1714,7 @@ export type PendingWorkspaceCollaborator = {
|
|||
id: Scalars['ID']['output'];
|
||||
inviteId: Scalars['String']['output'];
|
||||
invitedBy: LimitedUser;
|
||||
/** Target workspace role */
|
||||
role: Scalars['String']['output'];
|
||||
/** E-mail address or name of the invited user */
|
||||
title: Scalars['String']['output'];
|
||||
|
@ -2424,6 +2425,13 @@ export type Query = {
|
|||
*/
|
||||
userSearch: UserSearchResultCollection;
|
||||
workspace: Workspace;
|
||||
/**
|
||||
* Look for an invitation to a workspace, for the current user (authed or not). If token
|
||||
* isn't specified, the server will look for any valid invite.
|
||||
*
|
||||
* If token is specified, it will return the corresponding invite even if it belongs to a different user.
|
||||
*/
|
||||
workspaceInvite?: Maybe<PendingWorkspaceCollaborator>;
|
||||
};
|
||||
|
||||
|
||||
|
@ -2554,6 +2562,12 @@ export type QueryWorkspaceArgs = {
|
|||
id: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryWorkspaceInviteArgs = {
|
||||
token?: InputMaybe<Scalars['String']['input']>;
|
||||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
/** Deprecated: Used by old stream-based mutations */
|
||||
export type ReplyCreateInput = {
|
||||
/** IDs of uploaded blobs that should be attached to this reply */
|
||||
|
@ -3354,6 +3368,8 @@ export type User = {
|
|||
* Note: Only count resolution is currently implemented
|
||||
*/
|
||||
versions: CountOnlyCollection;
|
||||
/** Get all invitations to workspaces that the active user has */
|
||||
workspaceInvites: Array<PendingWorkspaceCollaborator>;
|
||||
/** Get the workspaces for the user */
|
||||
workspaces: WorkspaceCollection;
|
||||
};
|
||||
|
@ -3725,6 +3741,7 @@ export type Workspace = {
|
|||
createdAt: Scalars['DateTime']['output'];
|
||||
description?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['ID']['output'];
|
||||
/** Only available to workspace owners */
|
||||
invitedTeam?: Maybe<Array<PendingWorkspaceCollaborator>>;
|
||||
name: Scalars['String']['output'];
|
||||
projects: ProjectCollection;
|
||||
|
@ -3804,18 +3821,17 @@ export type WorkspaceInviteMutationsUseArgs = {
|
|||
export type WorkspaceInviteUseInput = {
|
||||
accept: Scalars['Boolean']['input'];
|
||||
token: Scalars['String']['input'];
|
||||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type WorkspaceMutations = {
|
||||
__typename?: 'WorkspaceMutations';
|
||||
create: Workspace;
|
||||
delete: Workspace;
|
||||
deleteRole: Scalars['Boolean']['output'];
|
||||
delete: Scalars['Boolean']['output'];
|
||||
deleteRole: Workspace;
|
||||
invites: WorkspaceInviteMutations;
|
||||
update: Workspace;
|
||||
/** TODO: `@hasWorkspaceRole(role: WORKSPACE_ADMIN)` for role changes */
|
||||
updateRole: Scalars['Boolean']['output'];
|
||||
updateRole: Workspace;
|
||||
};
|
||||
|
||||
|
||||
|
@ -4130,7 +4146,7 @@ export type ResolversTypes = {
|
|||
UpdateAutomateFunctionInput: UpdateAutomateFunctionInput;
|
||||
UpdateModelInput: UpdateModelInput;
|
||||
UpdateVersionInput: UpdateVersionInput;
|
||||
User: ResolverTypeWrapper<Omit<User, 'automateInfo' | 'commits' | 'favoriteStreams' | 'projectAccessRequest' | 'projectInvites' | 'projects' | 'streams' | 'workspaces'> & { automateInfo: ResolversTypes['UserAutomateInfo'], commits?: Maybe<ResolversTypes['CommitCollection']>, favoriteStreams: ResolversTypes['StreamCollection'], projectAccessRequest?: Maybe<ResolversTypes['ProjectAccessRequest']>, projectInvites: Array<ResolversTypes['PendingStreamCollaborator']>, projects: ResolversTypes['ProjectCollection'], streams: ResolversTypes['StreamCollection'], workspaces: ResolversTypes['WorkspaceCollection'] }>;
|
||||
User: ResolverTypeWrapper<Omit<User, 'automateInfo' | 'commits' | 'favoriteStreams' | 'projectAccessRequest' | 'projectInvites' | 'projects' | 'streams' | 'workspaceInvites' | 'workspaces'> & { automateInfo: ResolversTypes['UserAutomateInfo'], commits?: Maybe<ResolversTypes['CommitCollection']>, favoriteStreams: ResolversTypes['StreamCollection'], projectAccessRequest?: Maybe<ResolversTypes['ProjectAccessRequest']>, projectInvites: Array<ResolversTypes['PendingStreamCollaborator']>, projects: ResolversTypes['ProjectCollection'], streams: ResolversTypes['StreamCollection'], workspaceInvites: Array<ResolversTypes['PendingWorkspaceCollaborator']>, workspaces: ResolversTypes['WorkspaceCollection'] }>;
|
||||
UserAutomateInfo: ResolverTypeWrapper<UserAutomateInfoGraphQLReturn>;
|
||||
UserDeleteInput: UserDeleteInput;
|
||||
UserProjectsFilter: UserProjectsFilter;
|
||||
|
@ -4347,7 +4363,7 @@ export type ResolversParentTypes = {
|
|||
UpdateAutomateFunctionInput: UpdateAutomateFunctionInput;
|
||||
UpdateModelInput: UpdateModelInput;
|
||||
UpdateVersionInput: UpdateVersionInput;
|
||||
User: Omit<User, 'automateInfo' | 'commits' | 'favoriteStreams' | 'projectAccessRequest' | 'projectInvites' | 'projects' | 'streams' | 'workspaces'> & { automateInfo: ResolversParentTypes['UserAutomateInfo'], commits?: Maybe<ResolversParentTypes['CommitCollection']>, favoriteStreams: ResolversParentTypes['StreamCollection'], projectAccessRequest?: Maybe<ResolversParentTypes['ProjectAccessRequest']>, projectInvites: Array<ResolversParentTypes['PendingStreamCollaborator']>, projects: ResolversParentTypes['ProjectCollection'], streams: ResolversParentTypes['StreamCollection'], workspaces: ResolversParentTypes['WorkspaceCollection'] };
|
||||
User: Omit<User, 'automateInfo' | 'commits' | 'favoriteStreams' | 'projectAccessRequest' | 'projectInvites' | 'projects' | 'streams' | 'workspaceInvites' | 'workspaces'> & { automateInfo: ResolversParentTypes['UserAutomateInfo'], commits?: Maybe<ResolversParentTypes['CommitCollection']>, favoriteStreams: ResolversParentTypes['StreamCollection'], projectAccessRequest?: Maybe<ResolversParentTypes['ProjectAccessRequest']>, projectInvites: Array<ResolversParentTypes['PendingStreamCollaborator']>, projects: ResolversParentTypes['ProjectCollection'], streams: ResolversParentTypes['StreamCollection'], workspaceInvites: Array<ResolversParentTypes['PendingWorkspaceCollaborator']>, workspaces: ResolversParentTypes['WorkspaceCollection'] };
|
||||
UserAutomateInfo: UserAutomateInfoGraphQLReturn;
|
||||
UserDeleteInput: UserDeleteInput;
|
||||
UserProjectsFilter: UserProjectsFilter;
|
||||
|
@ -5263,6 +5279,7 @@ export type QueryResolvers<ContextType = GraphQLContext, ParentType extends Reso
|
|||
userPwdStrength?: Resolver<ResolversTypes['PasswordStrengthCheckResults'], ParentType, ContextType, RequireFields<QueryUserPwdStrengthArgs, 'pwd'>>;
|
||||
userSearch?: Resolver<ResolversTypes['UserSearchResultCollection'], ParentType, ContextType, RequireFields<QueryUserSearchArgs, 'archived' | 'emailOnly' | 'limit' | 'query'>>;
|
||||
workspace?: Resolver<ResolversTypes['Workspace'], ParentType, ContextType, RequireFields<QueryWorkspaceArgs, 'id'>>;
|
||||
workspaceInvite?: Resolver<Maybe<ResolversTypes['PendingWorkspaceCollaborator']>, ParentType, ContextType, RequireFields<QueryWorkspaceInviteArgs, 'workspaceId'>>;
|
||||
};
|
||||
|
||||
export type ResourceIdentifierResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['ResourceIdentifier'] = ResolversParentTypes['ResourceIdentifier']> = {
|
||||
|
@ -5541,6 +5558,7 @@ export type UserResolvers<ContextType = GraphQLContext, ParentType extends Resol
|
|||
totalOwnedStreamsFavorites?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
verified?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
|
||||
versions?: Resolver<ResolversTypes['CountOnlyCollection'], ParentType, ContextType, RequireFields<UserVersionsArgs, 'authoredOnly' | 'limit'>>;
|
||||
workspaceInvites?: Resolver<Array<ResolversTypes['PendingWorkspaceCollaborator']>, ParentType, ContextType>;
|
||||
workspaces?: Resolver<ResolversTypes['WorkspaceCollection'], ParentType, ContextType, RequireFields<UserWorkspacesArgs, 'cursor' | 'limit'>>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
@ -5708,11 +5726,11 @@ export type WorkspaceInviteMutationsResolvers<ContextType = GraphQLContext, Pare
|
|||
|
||||
export type WorkspaceMutationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspaceMutations'] = ResolversParentTypes['WorkspaceMutations']> = {
|
||||
create?: Resolver<ResolversTypes['Workspace'], ParentType, ContextType, RequireFields<WorkspaceMutationsCreateArgs, 'input'>>;
|
||||
delete?: Resolver<ResolversTypes['Workspace'], ParentType, ContextType, RequireFields<WorkspaceMutationsDeleteArgs, 'workspaceId'>>;
|
||||
deleteRole?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<WorkspaceMutationsDeleteRoleArgs, 'input'>>;
|
||||
delete?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<WorkspaceMutationsDeleteArgs, 'workspaceId'>>;
|
||||
deleteRole?: Resolver<ResolversTypes['Workspace'], ParentType, ContextType, RequireFields<WorkspaceMutationsDeleteRoleArgs, 'input'>>;
|
||||
invites?: Resolver<ResolversTypes['WorkspaceInviteMutations'], ParentType, ContextType>;
|
||||
update?: Resolver<ResolversTypes['Workspace'], ParentType, ContextType, RequireFields<WorkspaceMutationsUpdateArgs, 'input'>>;
|
||||
updateRole?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<WorkspaceMutationsUpdateRoleArgs, 'input'>>;
|
||||
updateRole?: Resolver<ResolversTypes['Workspace'], ParentType, ContextType, RequireFields<WorkspaceMutationsUpdateRoleArgs, 'input'>>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { SpeckleModuleMocksConfig } from '@/modules/shared/helpers/mocks'
|
||||
import { faker } from '@faker-js/faker'
|
||||
|
||||
// TODO: Some of these might make better sense in the base config, adjust as needed
|
||||
const mocks: SpeckleModuleMocksConfig = {
|
||||
resolvers: ({ store }) => ({
|
||||
Project: {
|
||||
blob: () => {
|
||||
return store.get('BlobMetadata') as any
|
||||
}
|
||||
}
|
||||
}),
|
||||
mocks: {
|
||||
BlobMetadata: () => ({
|
||||
fileName: () => faker.system.fileName(),
|
||||
fileType: () => faker.system.mimeType(),
|
||||
fileSize: () => faker.number.int({ min: 1, max: 1000 })
|
||||
}),
|
||||
Model: () => ({
|
||||
id: () => faker.string.uuid(),
|
||||
name: () => faker.commerce.productName(),
|
||||
previewUrl: () => faker.image.imageUrl()
|
||||
}),
|
||||
Version: () => ({
|
||||
id: () => faker.string.alphanumeric(10)
|
||||
})
|
||||
}
|
||||
}
|
||||
export default mocks
|
|
@ -1703,6 +1703,7 @@ export type PendingWorkspaceCollaborator = {
|
|||
id: Scalars['ID']['output'];
|
||||
inviteId: Scalars['String']['output'];
|
||||
invitedBy: LimitedUser;
|
||||
/** Target workspace role */
|
||||
role: Scalars['String']['output'];
|
||||
/** E-mail address or name of the invited user */
|
||||
title: Scalars['String']['output'];
|
||||
|
@ -2413,6 +2414,13 @@ export type Query = {
|
|||
*/
|
||||
userSearch: UserSearchResultCollection;
|
||||
workspace: Workspace;
|
||||
/**
|
||||
* Look for an invitation to a workspace, for the current user (authed or not). If token
|
||||
* isn't specified, the server will look for any valid invite.
|
||||
*
|
||||
* If token is specified, it will return the corresponding invite even if it belongs to a different user.
|
||||
*/
|
||||
workspaceInvite?: Maybe<PendingWorkspaceCollaborator>;
|
||||
};
|
||||
|
||||
|
||||
|
@ -2543,6 +2551,12 @@ export type QueryWorkspaceArgs = {
|
|||
id: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryWorkspaceInviteArgs = {
|
||||
token?: InputMaybe<Scalars['String']['input']>;
|
||||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
/** Deprecated: Used by old stream-based mutations */
|
||||
export type ReplyCreateInput = {
|
||||
/** IDs of uploaded blobs that should be attached to this reply */
|
||||
|
@ -3343,6 +3357,8 @@ export type User = {
|
|||
* Note: Only count resolution is currently implemented
|
||||
*/
|
||||
versions: CountOnlyCollection;
|
||||
/** Get all invitations to workspaces that the active user has */
|
||||
workspaceInvites: Array<PendingWorkspaceCollaborator>;
|
||||
/** Get the workspaces for the user */
|
||||
workspaces: WorkspaceCollection;
|
||||
};
|
||||
|
@ -3714,6 +3730,7 @@ export type Workspace = {
|
|||
createdAt: Scalars['DateTime']['output'];
|
||||
description?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['ID']['output'];
|
||||
/** Only available to workspace owners */
|
||||
invitedTeam?: Maybe<Array<PendingWorkspaceCollaborator>>;
|
||||
name: Scalars['String']['output'];
|
||||
projects: ProjectCollection;
|
||||
|
@ -3793,18 +3810,17 @@ export type WorkspaceInviteMutationsUseArgs = {
|
|||
export type WorkspaceInviteUseInput = {
|
||||
accept: Scalars['Boolean']['input'];
|
||||
token: Scalars['String']['input'];
|
||||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type WorkspaceMutations = {
|
||||
__typename?: 'WorkspaceMutations';
|
||||
create: Workspace;
|
||||
delete: Workspace;
|
||||
deleteRole: Scalars['Boolean']['output'];
|
||||
delete: Scalars['Boolean']['output'];
|
||||
deleteRole: Workspace;
|
||||
invites: WorkspaceInviteMutations;
|
||||
update: Workspace;
|
||||
/** TODO: `@hasWorkspaceRole(role: WORKSPACE_ADMIN)` for role changes */
|
||||
updateRole: Scalars['Boolean']['output'];
|
||||
updateRole: Workspace;
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
'use strict'
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { appRoot, packageRoot } = require('@/bootstrap')
|
||||
const { values, merge, camelCase } = require('lodash')
|
||||
const { values, merge, camelCase, intersection } = require('lodash')
|
||||
const baseTypeDefs = require('@/modules/core/graph/schema/baseTypeDefs')
|
||||
const { scalarResolvers } = require('./core/graph/scalars')
|
||||
const { makeExecutableSchema } = require('@graphql-tools/schema')
|
||||
const { moduleLogger } = require('@/logging/logging')
|
||||
const { addMocksToSchema } = require('@graphql-tools/mock')
|
||||
const { getFeatureFlags } = require('@/modules/shared/helpers/envHelper')
|
||||
const { isNonNullable } = require('@speckle/shared')
|
||||
|
||||
/**
|
||||
* Cached speckle module requires
|
||||
|
@ -208,3 +208,38 @@ exports.graphSchema = (mocksConfig) => {
|
|||
|
||||
return schema
|
||||
}
|
||||
|
||||
/**
|
||||
* Load GQL mock configs from speckle modules
|
||||
* @param {string[]} moduleWhitelist
|
||||
* @returns {Record<string, import('@/modules/shared/helpers/mocks').SpeckleModuleMocksConfig>}
|
||||
*/
|
||||
exports.moduleMockConfigs = (moduleWhitelist) => {
|
||||
const enabledModuleNames = intersection(getEnabledModuleNames(), moduleWhitelist)
|
||||
|
||||
// Config default exports keyed by module name
|
||||
const mockConfigs = {}
|
||||
if (!enabledModuleNames.length) return mockConfigs
|
||||
|
||||
// load code modules from /modules
|
||||
const codeModuleDirs = fs.readdirSync(`${appRoot}/modules`)
|
||||
codeModuleDirs.forEach((moduleName) => {
|
||||
const fullPath = path.join(`${appRoot}/modules`, moduleName)
|
||||
if (!enabledModuleNames.includes(moduleName)) return
|
||||
|
||||
// load mock config
|
||||
const mocksFolderPath = path.join(fullPath, 'graph', 'mocks')
|
||||
if (fs.existsSync(mocksFolderPath)) {
|
||||
// We only take the first mocks.ts file we find (for now)
|
||||
const mainConfig = values(autoloadFromDirectory(mocksFolderPath))
|
||||
.map((l) => l.default)
|
||||
.filter(isNonNullable)[0]
|
||||
|
||||
if (mainConfig && Object.values(mainConfig).length) {
|
||||
mockConfigs[moduleName] = mainConfig
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return mockConfigs
|
||||
}
|
||||
|
|
|
@ -1,52 +1,77 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { ProjectVisibility, Resolvers } from '@/modules/core/graph/generated/graphql'
|
||||
import {
|
||||
AutomateRunStatus,
|
||||
LimitedUser,
|
||||
Resolvers
|
||||
} from '@/modules/core/graph/generated/graphql'
|
||||
import { isTestEnv } from '@/modules/shared/helpers/envHelper'
|
||||
import { Automate, Roles, SourceAppNames, isNullOrUndefined } from '@speckle/shared'
|
||||
import { times } from 'lodash'
|
||||
mockedApiModules,
|
||||
isProdEnv,
|
||||
isTestEnv
|
||||
} from '@/modules/shared/helpers/envHelper'
|
||||
import { has, reduce } from 'lodash'
|
||||
import { IMockStore, IMocks } from '@graphql-tools/mock'
|
||||
import dayjs from 'dayjs'
|
||||
import { BranchCommits, Branches, Commits } from '@/modules/core/dbSchema'
|
||||
|
||||
import { moduleMockConfigs } from '@/modules'
|
||||
import { isNonNullable, Roles, SourceAppNames } from '@speckle/shared'
|
||||
import {
|
||||
AutomationNotFoundError,
|
||||
FunctionNotFoundError
|
||||
} from '@/modules/automate/errors/management'
|
||||
import { functionTemplateRepos } from '@/modules/automate/helpers/executionEngine'
|
||||
import {
|
||||
AutomationRevisionTriggerDefinitionGraphQLReturn,
|
||||
AutomationRunTriggerGraphQLReturn
|
||||
} from '@/modules/automate/helpers/graphTypes'
|
||||
import { VersionCreationTriggerType } from '@/modules/automate/helpers/types'
|
||||
getRandomDbRecords,
|
||||
mockStoreHelpers,
|
||||
SpeckleModuleMocksConfig
|
||||
} from '@/modules/shared/helpers/mocks'
|
||||
import { Streams } from '@/modules/core/dbSchema'
|
||||
|
||||
const getRandomModelVersion = async (offset?: number) => {
|
||||
const versionQ = Commits.knex()
|
||||
.join(BranchCommits.name, BranchCommits.col.commitId, Commits.col.id)
|
||||
.first()
|
||||
if (offset) versionQ.offset(offset)
|
||||
const version = await versionQ
|
||||
|
||||
if (!version) {
|
||||
throw new Error("Couldn't find even one commit in the DB, please create some")
|
||||
}
|
||||
|
||||
const model = await Branches.knex()
|
||||
.join(BranchCommits.name, BranchCommits.col.branchId, Branches.col.id)
|
||||
.where(BranchCommits.col.commitId, version.id)
|
||||
.first()
|
||||
|
||||
if (!model) {
|
||||
throw new Error(
|
||||
`Couldn't find branch for first commit #${version.id}, please create one `
|
||||
)
|
||||
}
|
||||
/**
|
||||
* Base config that always needs to be loaded, cause it sets up core primitives
|
||||
*/
|
||||
const buildBaseConfig = async (): Promise<SpeckleModuleMocksConfig> => {
|
||||
// Async import so that we only import this when envs actually have mocks on
|
||||
const faker = (await import('@faker-js/faker')).faker
|
||||
|
||||
return {
|
||||
model,
|
||||
version
|
||||
resolvers: ({ helpers: { getFieldValue } }) => ({
|
||||
LimitedUser: {
|
||||
role: (parent) =>
|
||||
getFieldValue(
|
||||
{ type: 'LimitedUser', id: getFieldValue(parent, 'id') },
|
||||
'role'
|
||||
)
|
||||
},
|
||||
ProjectCollection: {
|
||||
items: async (parent) => {
|
||||
// In case a real project collection was built, we skip mocking it
|
||||
if (has(parent, 'items')) return parent.items
|
||||
|
||||
const count = getFieldValue(parent, 'totalCount')
|
||||
|
||||
// To avoid having to mock projects fully, we pull real ones from the DB
|
||||
return await getRandomDbRecords({ tableName: Streams.name, min: count })
|
||||
}
|
||||
}
|
||||
}),
|
||||
mocks: {
|
||||
// Primitives
|
||||
JSONObject: () => ({}),
|
||||
ID: () => faker.string.uuid(),
|
||||
DateTime: () => faker.date.recent().toISOString(),
|
||||
Boolean: () => faker.datatype.boolean(),
|
||||
// Base objects
|
||||
LimitedUser: () => ({
|
||||
id: faker.string.uuid(),
|
||||
name: faker.person.fullName(),
|
||||
avatar: faker.image.avatar(),
|
||||
bio: faker.lorem.sentence(),
|
||||
company: faker.company.name(),
|
||||
verified: faker.datatype.boolean(),
|
||||
role: Roles.Server.User
|
||||
}),
|
||||
Project: () => ({
|
||||
id: faker.string.uuid(),
|
||||
name: faker.commerce.productName(),
|
||||
description: faker.lorem.sentence(),
|
||||
visibility: faker.helpers.arrayElement(Object.values(ProjectVisibility)),
|
||||
role: faker.helpers.arrayElement(Object.values(Roles.Stream)),
|
||||
sourceApps: faker.helpers.arrayElements(SourceAppNames, { min: 0, max: 5 })
|
||||
}),
|
||||
ProjectCollection: () => ({
|
||||
totalCount: faker.number.int({ min: 0, max: 10 })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,364 +84,54 @@ export async function buildMocksConfig(): Promise<{
|
|||
mockEntireSchema: boolean
|
||||
resolvers?: Resolvers | ((store: IMockStore) => Resolvers)
|
||||
}> {
|
||||
return { mocks: false, mockEntireSchema: false }
|
||||
const mockableModuleList = mockedApiModules()
|
||||
const enable = mockableModuleList.length && !isTestEnv() && !isProdEnv()
|
||||
if (!enable) {
|
||||
return { mocks: false, mockEntireSchema: false }
|
||||
}
|
||||
|
||||
// TODO: Disable before merging!
|
||||
if (isTestEnv()) return { mocks: false, mockEntireSchema: false }
|
||||
const configs = moduleMockConfigs(mockableModuleList)
|
||||
if (!Object.keys(configs).length) {
|
||||
return { mocks: false, mockEntireSchema: false }
|
||||
}
|
||||
|
||||
// const isDebugEnv = isDevEnv()
|
||||
// if (!isDebugEnv) return { mocks: false, mockEntireSchema: false } // we def don't want this on in prod
|
||||
const allConfigs = { base: await buildBaseConfig(), ...configs }
|
||||
|
||||
// feel free to define mocks for your dev env below
|
||||
const { faker } = await import('@faker-js/faker')
|
||||
// Merge configs into one
|
||||
const mocks: IMocks = reduce(
|
||||
allConfigs,
|
||||
(acc, config) => {
|
||||
return { ...acc, ...(config.mocks || {}) }
|
||||
},
|
||||
{} as IMocks
|
||||
)
|
||||
const resolvers: (store: IMockStore) => Resolvers = (store) => {
|
||||
const allResolversBuilders = Object.values(allConfigs)
|
||||
.map((c) => c.resolvers)
|
||||
.filter(isNonNullable)
|
||||
const allResolvers = allResolversBuilders.map((builder) =>
|
||||
builder({ store, helpers: mockStoreHelpers(store) })
|
||||
)
|
||||
|
||||
// Deep merge all resolvers
|
||||
const resolvers = allResolvers.reduce((acc, resolvers) => {
|
||||
for (const [typeName, typeResolvers] of Object.entries(resolvers)) {
|
||||
if (!acc[typeName]) {
|
||||
acc[typeName] = {}
|
||||
}
|
||||
|
||||
Object.assign(acc[typeName], typeResolvers)
|
||||
}
|
||||
return acc
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
}, {} as Record<string, any>)
|
||||
|
||||
return resolvers as Resolvers
|
||||
}
|
||||
|
||||
return {
|
||||
resolvers: (store) => ({
|
||||
AutomationRevisionTriggerDefinition: {
|
||||
__resolveType: () => 'VersionCreatedTriggerDefinition'
|
||||
},
|
||||
AutomationRunTrigger: {
|
||||
__resolveType: () => 'VersionCreatedTrigger'
|
||||
},
|
||||
VersionCreatedTriggerDefinition: {
|
||||
model: store.get('Model') as any
|
||||
},
|
||||
VersionCreatedTrigger: {
|
||||
model: store.get('Model') as any,
|
||||
version: store.get('Version') as any
|
||||
},
|
||||
Query: {
|
||||
automateFunctions: (_parent, args) => {
|
||||
const forceZero = false
|
||||
const count = forceZero ? 0 : faker.datatype.number({ min: 0, max: 20 })
|
||||
|
||||
const isFeatured = args.filter?.featuredFunctionsOnly
|
||||
|
||||
return {
|
||||
cursor: null,
|
||||
totalCount: count,
|
||||
items: times(count, () => store.get('AutomateFunction', { isFeatured }))
|
||||
} as any
|
||||
},
|
||||
automateFunction: (_parent, args) => {
|
||||
const id = args.id
|
||||
if (id === '404') {
|
||||
throw new FunctionNotFoundError()
|
||||
}
|
||||
|
||||
return store.get('AutomateFunction', { id }) as any
|
||||
}
|
||||
},
|
||||
Project: {
|
||||
automations: () => {
|
||||
const forceAutomations = false
|
||||
const forceNoAutomations = false
|
||||
|
||||
const limit = faker.datatype.number({ min: 0, max: 20 })
|
||||
let count
|
||||
if (forceNoAutomations) {
|
||||
count = 0
|
||||
} else {
|
||||
count = forceAutomations ? limit : faker.datatype.boolean() ? limit : 0
|
||||
}
|
||||
|
||||
return {
|
||||
cursor: null,
|
||||
totalCount: count,
|
||||
items: times(count, () => store.get('Automation'))
|
||||
} as any
|
||||
},
|
||||
automation: (_parent, args) => {
|
||||
if (args.id === '404') {
|
||||
throw new AutomationNotFoundError()
|
||||
}
|
||||
|
||||
return store.get('Automation', { id: args.id }) as any
|
||||
},
|
||||
blob: () => {
|
||||
return store.get('BlobMetadata') as any
|
||||
}
|
||||
},
|
||||
Model: {
|
||||
automationsStatus: async () => {
|
||||
const random = faker.datatype.boolean()
|
||||
return (random ? store.get('TriggeredAutomationsStatus') : null) as any
|
||||
}
|
||||
},
|
||||
Version: {
|
||||
automationsStatus: async () => {
|
||||
const random = faker.datatype.boolean()
|
||||
return (random ? store.get('TriggeredAutomationsStatus') : null) as any
|
||||
}
|
||||
},
|
||||
Automation: {
|
||||
creationPublicKeys: () => {
|
||||
// Random sized array of string keys
|
||||
return [...new Array(faker.datatype.number({ min: 0, max: 5 }))].map(() =>
|
||||
faker.datatype.uuid()
|
||||
)
|
||||
},
|
||||
runs: () => {
|
||||
const forceZero = false
|
||||
const count = forceZero ? 0 : faker.datatype.number({ min: 0, max: 20 })
|
||||
|
||||
return {
|
||||
cursor: null,
|
||||
totalCount: count,
|
||||
items: times(count, () => store.get('AutomateRun'))
|
||||
} as any
|
||||
},
|
||||
currentRevision: () => store.get('AutomationRevision') as any
|
||||
},
|
||||
AutomationRevision: {
|
||||
triggerDefinitions: async (parent) => {
|
||||
const rand = faker.datatype.number({ min: 0, max: 2 })
|
||||
const res = (
|
||||
await Promise.all([getRandomModelVersion(), getRandomModelVersion(1)])
|
||||
).slice(0, rand)
|
||||
|
||||
return res.map(
|
||||
(i): AutomationRevisionTriggerDefinitionGraphQLReturn => ({
|
||||
triggerType: VersionCreationTriggerType,
|
||||
triggeringId: i.model.id,
|
||||
automationRevisionId: parent.id
|
||||
})
|
||||
)
|
||||
},
|
||||
functions: () => [store.get('AutomateFunction') as any]
|
||||
},
|
||||
AutomationRevisionFunction: {
|
||||
parameters: () => ({}),
|
||||
release: () => store.get('AutomateFunctionRelease') as any
|
||||
},
|
||||
AutomateRun: {
|
||||
trigger: async (parent) => {
|
||||
const { version } = await getRandomModelVersion()
|
||||
|
||||
return <AutomationRunTriggerGraphQLReturn>{
|
||||
triggerType: VersionCreationTriggerType,
|
||||
triggeringId: version.id,
|
||||
automationRunId: parent.id
|
||||
}
|
||||
},
|
||||
automation: () => store.get('Automation') as any,
|
||||
status: () => faker.helpers.arrayElement(Object.values(AutomateRunStatus))
|
||||
},
|
||||
AutomateFunctionRun: {
|
||||
function: () => store.get('AutomateFunction') as any,
|
||||
status: () => faker.helpers.arrayElement(Object.values(AutomateRunStatus))
|
||||
},
|
||||
ProjectAutomationMutations: {
|
||||
create: (_parent, args) => {
|
||||
const {
|
||||
input: { name, enabled }
|
||||
} = args
|
||||
const automation = store.get('Automation') as any
|
||||
return {
|
||||
...automation,
|
||||
name,
|
||||
enabled
|
||||
}
|
||||
},
|
||||
update: (_parent, args) => {
|
||||
const {
|
||||
input: { id, name, enabled }
|
||||
} = args
|
||||
const automation = store.get('Automation') as any
|
||||
return {
|
||||
...automation,
|
||||
id,
|
||||
...(name?.length ? { name } : {}),
|
||||
...(isNullOrUndefined(enabled) ? {} : { enabled })
|
||||
}
|
||||
},
|
||||
trigger: () => faker.datatype.string(10),
|
||||
createRevision: () => store.get('AutomationRevision') as any
|
||||
},
|
||||
UserAutomateInfo: {
|
||||
hasAutomateGithubApp: () => {
|
||||
return faker.datatype.boolean()
|
||||
},
|
||||
availableGithubOrgs: () => {
|
||||
// Random string array
|
||||
return [...new Array(faker.datatype.number({ min: 0, max: 5 }))].map(() =>
|
||||
faker.company.companyName()
|
||||
)
|
||||
}
|
||||
},
|
||||
AutomateFunction: {
|
||||
// creator: async (_parent, args, ctx) => {
|
||||
// const rand = faker.datatype.boolean()
|
||||
// const activeUser = ctx.userId
|
||||
// ? await ctx.loaders.users.getUser.load(ctx.userId)
|
||||
// : null
|
||||
|
||||
// return rand ? (store.get('LimitedUser') as any) : activeUser
|
||||
// }
|
||||
releases: () => store.get('AutomateFunctionReleaseCollection') as any,
|
||||
automationCount: () => faker.datatype.number({ min: 0, max: 99 })
|
||||
},
|
||||
AutomateFunctionRelease: {
|
||||
function: () => store.get('AutomateFunction') as any
|
||||
},
|
||||
TriggeredAutomationsStatus: {
|
||||
status: () => faker.helpers.arrayElement(Object.values(AutomateRunStatus))
|
||||
},
|
||||
AutomateMutations: {
|
||||
createFunction: () => store.get('AutomateFunction') as any,
|
||||
updateFunction: (_parent, args) => {
|
||||
const {
|
||||
input: { id, name, description, supportedSourceApps, tags }
|
||||
} = args
|
||||
const func = store.get('AutomateFunction', { id }) as any
|
||||
return {
|
||||
...func,
|
||||
id,
|
||||
...(name?.length ? { name } : {}),
|
||||
...(description?.length ? { description } : {}),
|
||||
...(supportedSourceApps?.length ? { supportedSourceApps } : {}),
|
||||
...(tags?.length ? { tags } : {})
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
mocks: {
|
||||
BlobMetadata: () => ({
|
||||
fileName: () => faker.system.fileName(),
|
||||
fileType: () => faker.system.mimeType(),
|
||||
fileSize: () => faker.datatype.number({ min: 1, max: 1000 })
|
||||
}),
|
||||
TriggeredAutomationsStatus: () => ({
|
||||
automationRuns: () => [...new Array(faker.datatype.number({ min: 1, max: 5 }))]
|
||||
}),
|
||||
AutomationRevision: () => ({
|
||||
functions: () => [undefined] // array of 1 always,
|
||||
}),
|
||||
Automation: () => ({
|
||||
name: () => faker.company.companyName(),
|
||||
enabled: () => faker.datatype.boolean()
|
||||
}),
|
||||
AutomateFunction: () => ({
|
||||
name: () => faker.commerce.productName(),
|
||||
isFeatured: () => faker.datatype.boolean(),
|
||||
logo: () => {
|
||||
const random = faker.datatype.boolean()
|
||||
return random
|
||||
? faker.image.imageUrl(undefined, undefined, undefined, true)
|
||||
: null
|
||||
},
|
||||
repoUrl: () =>
|
||||
'https://github.com/specklesystems/speckle-automate-code-compliance-window-safety',
|
||||
automationCount: () => faker.datatype.number({ min: 0, max: 99 }),
|
||||
description: () => {
|
||||
// Example markdown description
|
||||
return `# ${faker.commerce.productName()}\n${faker.lorem.paragraphs(
|
||||
1,
|
||||
'\n\n'
|
||||
)}\n## Features \n- ${faker.lorem.sentence()}\n - ${faker.lorem.sentence()}\n - ${faker.lorem.sentence()}`
|
||||
},
|
||||
supportedSourceApps: () => {
|
||||
const base = SourceAppNames
|
||||
|
||||
// Random assortment from base
|
||||
return base.filter(faker.datatype.boolean)
|
||||
},
|
||||
tags: () => {
|
||||
// Random string array
|
||||
return [...new Array(faker.datatype.number({ min: 0, max: 5 }))].map(() =>
|
||||
faker.lorem.word()
|
||||
)
|
||||
}
|
||||
}),
|
||||
AutomateFunctionRelease: () => ({
|
||||
versionTag: () => {
|
||||
// Fake semantic version
|
||||
return `${faker.datatype.number({ min: 0, max: 9 })}.${faker.datatype.number({
|
||||
min: 0,
|
||||
max: 9
|
||||
})}.${faker.datatype.number({ min: 0, max: 9 })}`
|
||||
},
|
||||
commitId: () => '0c259d384a4df3cce3f24667560e5124e68f202f',
|
||||
inputSchema: () => {
|
||||
// random fro 1 to 3
|
||||
const rand = faker.datatype.number({ min: 1, max: 3 })
|
||||
switch (rand) {
|
||||
case 1:
|
||||
return {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
$id: 'https://example.com/product.schema.json',
|
||||
title: 'Product',
|
||||
description: "A product from Acme's catalog",
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
desciption: 'Random name',
|
||||
type: 'string'
|
||||
},
|
||||
productId: {
|
||||
description: 'The unique identifier for a product',
|
||||
type: 'integer'
|
||||
}
|
||||
},
|
||||
required: ['productId']
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
}),
|
||||
AutomateRun: () => ({
|
||||
reason: () => faker.lorem.sentence(),
|
||||
id: () => faker.random.alphaNumeric(20),
|
||||
createdAt: () =>
|
||||
faker.date
|
||||
.recent(undefined, dayjs().subtract(1, 'day').toDate())
|
||||
.toISOString(),
|
||||
updatedAt: () => faker.date.recent().toISOString(),
|
||||
functionRuns: () => [...new Array(faker.datatype.number({ min: 1, max: 5 }))],
|
||||
statusMessage: () => faker.lorem.sentence()
|
||||
}),
|
||||
AutomateFunctionRun: () => ({
|
||||
contextView: () => `/`,
|
||||
elapsed: () => faker.datatype.number({ min: 0, max: 600 }),
|
||||
statusMessage: () => faker.lorem.sentence(),
|
||||
results: (): Automate.AutomateTypes.ResultsSchema => {
|
||||
return {
|
||||
version: Automate.AutomateTypes.RESULTS_SCHEMA_VERSION,
|
||||
values: {
|
||||
objectResults: [],
|
||||
blobIds: [...new Array(faker.datatype.number({ min: 0, max: 5 }))].map(
|
||||
() => faker.datatype.uuid()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
LimitedUser: () =>
|
||||
({
|
||||
id: faker.datatype.uuid(),
|
||||
name: faker.name.findName(),
|
||||
avatar: faker.image.avatar(),
|
||||
bio: faker.lorem.sentence(),
|
||||
company: faker.company.companyName(),
|
||||
verified: faker.datatype.boolean(),
|
||||
role: Roles.Server.User
|
||||
} as LimitedUser),
|
||||
JSONObject: () => ({}),
|
||||
ID: () => faker.datatype.uuid(),
|
||||
DateTime: () => faker.date.recent().toISOString(),
|
||||
Model: () => ({
|
||||
id: () => faker.datatype.uuid(),
|
||||
name: () => faker.commerce.productName(),
|
||||
previewUrl: () => faker.image.imageUrl()
|
||||
}),
|
||||
Version: () => ({
|
||||
id: () => faker.random.alphaNumeric(10)
|
||||
}),
|
||||
ServerAutomateInfo: () => ({
|
||||
availableFunctionTemplates: () => functionTemplateRepos
|
||||
})
|
||||
},
|
||||
mocks,
|
||||
resolvers,
|
||||
mockEntireSchema: false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -231,6 +231,14 @@ export function ignoreMissingMigrations() {
|
|||
return getBooleanFromEnv('IGNORE_MISSING_MIRATIONS')
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to enable GQL API mocks
|
||||
*/
|
||||
export const mockedApiModules = () => {
|
||||
const base = process.env.MOCKED_API_MODULES
|
||||
return (base || '').split(',').map((x) => x.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* URL of a project on any FE2 speckle server that will be pulled in and used as the onboarding stream
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,206 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { db } from '@/db/knex'
|
||||
import { ResolverFn, Resolvers } from '@/modules/core/graph/generated/graphql'
|
||||
import { IMockStore, IMocks, isRef, Ref } from '@graphql-tools/mock'
|
||||
import { GraphQLResolveInfo } from 'graphql'
|
||||
import { get, has, isArray, isObjectLike, random } from 'lodash'
|
||||
|
||||
export type SpeckleModuleMocksConfig = {
|
||||
resolvers?: (params: {
|
||||
store: IMockStore
|
||||
helpers: ReturnType<typeof mockStoreHelpers>
|
||||
}) => Resolvers
|
||||
mocks?: IMocks
|
||||
}
|
||||
|
||||
type SimpleRef = { type: string; id: string }
|
||||
const isSimpleRef = (obj: any): obj is SimpleRef =>
|
||||
isObjectLike(obj) && Object.keys(obj).length === 2 && 'type' in obj && 'id' in obj
|
||||
|
||||
export const mockStoreHelpers = (store: IMockStore) => {
|
||||
/**
|
||||
* We have to use an internal api, but there is no other way to check
|
||||
* for the existence of a field in the mock store.
|
||||
*/
|
||||
const hasField = (type: string, key: string, field: string) => {
|
||||
const internalStore = get(store, 'store') as {
|
||||
[type: string]: {
|
||||
[key: string]: {
|
||||
[field: string]: unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return has(internalStore, [type, key, field])
|
||||
}
|
||||
|
||||
const addMockRefValues = (ref: Ref, values: Record<string, any>) => {
|
||||
store.set(ref, values)
|
||||
return ref
|
||||
}
|
||||
|
||||
const getMockRef = <T = any>(
|
||||
type: string,
|
||||
options?: {
|
||||
/**
|
||||
* The id of the object to get. If object w/ this ID already exists in the mock store,
|
||||
* it will be retrieved from there. Otherwise, a new object will be created.
|
||||
*/
|
||||
id?: string
|
||||
/**
|
||||
* Additional field values that should be set on the object
|
||||
*/
|
||||
values?: Record<string, any>
|
||||
}
|
||||
) => {
|
||||
const { id, values } = options || {}
|
||||
const ret = values
|
||||
? store.get(type, {
|
||||
...values,
|
||||
...(id ? { id } : {})
|
||||
})
|
||||
: store.get(type, id)
|
||||
return ret as T
|
||||
}
|
||||
|
||||
const getFieldValue = <T = any>(
|
||||
refOrObj: Record<string, unknown> | Ref | SimpleRef,
|
||||
field: string
|
||||
) => {
|
||||
if (isRef(refOrObj)) return store.get(refOrObj, field) as T
|
||||
if (isSimpleRef(refOrObj)) return store.get(refOrObj.type, refOrObj.id, field) as T
|
||||
return refOrObj[field] as T
|
||||
}
|
||||
|
||||
type AnyResolverFn = ResolverFn<any, any, any, any>
|
||||
|
||||
const resolveFromMockParent = (
|
||||
options?: Partial<{
|
||||
/**
|
||||
* Allows you to map any refs found (whether they're in arrays or not) to something else,
|
||||
* e.g. the same mock, but with different arg values
|
||||
*/
|
||||
mapRefs: (
|
||||
mockRef: Ref,
|
||||
resolverArgs: { parent: any; args: any; ctx: any; info: GraphQLResolveInfo }
|
||||
) => any
|
||||
}>
|
||||
) => {
|
||||
const { mapRefs } = options || {}
|
||||
|
||||
const resolver: AnyResolverFn = (parent, args, ctx, info) => {
|
||||
const resolverArgs = { parent, args, ctx, info }
|
||||
const val = getFieldValue(parent, info.fieldName)
|
||||
if (!mapRefs) return val
|
||||
|
||||
if (isArray(val)) {
|
||||
return val.map((v) => (isRef(v) ? mapRefs(v, resolverArgs) : v))
|
||||
} else {
|
||||
return isRef(val) ? mapRefs(val, resolverArgs) : val
|
||||
}
|
||||
}
|
||||
|
||||
return resolver
|
||||
}
|
||||
|
||||
const resolveAndCache = (resolver: AnyResolverFn) => {
|
||||
const wrapperResolver: AnyResolverFn = (parent, args, ctx, info) => {
|
||||
let cached: any
|
||||
|
||||
if (!isRef(parent) && !has(parent, 'id')) {
|
||||
throw new Error(
|
||||
'resolveAndCache depends on resolver parent being a mock ref or an object with an ID field'
|
||||
)
|
||||
}
|
||||
|
||||
if (isRef(parent)) {
|
||||
if (hasField(parent.$ref.typeName, parent.$ref.key, info.fieldName)) {
|
||||
cached = store.get(parent, info.fieldName)
|
||||
}
|
||||
} else {
|
||||
if (hasField(info.parentType.name, parent.id, info.fieldName)) {
|
||||
cached = store.get(info.parentType.name, parent.id, info.fieldName)
|
||||
}
|
||||
}
|
||||
|
||||
if (cached) return cached
|
||||
|
||||
const val = resolver(parent, args, ctx, info)
|
||||
if (isRef(parent)) {
|
||||
store.set(parent, info.fieldName, val)
|
||||
} else {
|
||||
store.set(info.parentType.name, parent.id, info.fieldName, val)
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
return wrapperResolver
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Get mock reference. It can be returned from resolvers and converted to the actual mock
|
||||
* when outputted to response.
|
||||
*/
|
||||
getMockRef,
|
||||
/**
|
||||
* Get value from a mock reference or plain object.
|
||||
*
|
||||
* Useful when you need to access something from parent, where you don't know if it's
|
||||
* gonna be a mock reference or the actual object.
|
||||
* Also useful for just getting arbitrary field values from mock refs.
|
||||
*/
|
||||
getFieldValue,
|
||||
/**
|
||||
* Invoke this in place of a resolver definition to just tell Apollo to take the value from
|
||||
* the mock in `parent`.
|
||||
*
|
||||
* This is useful when there's a real resolver that blocks access to the mock, so you
|
||||
* need to create a mock resolver that just returns the value from the parent.
|
||||
*/
|
||||
resolveFromMockParent,
|
||||
/**
|
||||
* Update specific values in mock
|
||||
*/
|
||||
addMockRefValues,
|
||||
/**
|
||||
* Wraps your resolver with a caching mechanism that caches the value in the MockStore
|
||||
* for this specific parent object
|
||||
*
|
||||
* Useful when parent object is not a MockRef, but you want to mock out and cache some of its values.
|
||||
* Or it may be a MockRef, but for some reason you can't just define the field in the mock definition
|
||||
* and need a resolver
|
||||
*/
|
||||
resolveAndCache
|
||||
}
|
||||
}
|
||||
|
||||
export const getRandomDbRecords = async <T extends {} = any>(params: {
|
||||
tableName: string
|
||||
min: number
|
||||
max?: number
|
||||
}) => {
|
||||
const { tableName, min, max } = params
|
||||
if (max && max < min) {
|
||||
throw new Error('Max cannot be less than min')
|
||||
}
|
||||
|
||||
const finalCount = max ? random(min, max) : min
|
||||
|
||||
// Query could be slow on large datasets, but for test/dev envs it should be fine
|
||||
const res = await db(tableName)
|
||||
.select<T>('*')
|
||||
.orderByRaw('RANDOM()')
|
||||
.limit(finalCount)
|
||||
return res as T[]
|
||||
}
|
||||
|
||||
/**
|
||||
* For defining lists of size X in mock fields. If 2nd arg is specified, the size will be random
|
||||
* between the two numbers.
|
||||
*/
|
||||
export const listMock = (min: number, max?: number) =>
|
||||
[...new Array(max ? random(min, max) : min)] as unknown[]
|
|
@ -0,0 +1,229 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
import { listMock, SpeckleModuleMocksConfig } from '@/modules/shared/helpers/mocks'
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { omit, times } from 'lodash'
|
||||
import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace'
|
||||
|
||||
const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
|
||||
|
||||
const workspaceName = () =>
|
||||
`${faker.person.firstName()} ${faker.commerce.productName()}`
|
||||
|
||||
const config: SpeckleModuleMocksConfig = FF_WORKSPACES_MODULE_ENABLED
|
||||
? {
|
||||
resolvers: ({
|
||||
helpers: {
|
||||
getFieldValue,
|
||||
getMockRef,
|
||||
resolveFromMockParent,
|
||||
addMockRefValues,
|
||||
resolveAndCache
|
||||
}
|
||||
}) => {
|
||||
return {
|
||||
WorkspaceMutations: {
|
||||
create: (_parent, args) => {
|
||||
if (args.input.name === 'error') {
|
||||
throw new Error('Fake workspace create error')
|
||||
}
|
||||
|
||||
return getMockRef('Workspace', { values: omit(args.input, ['logoUrl']) })
|
||||
},
|
||||
delete: () => {
|
||||
const val = faker.datatype.boolean()
|
||||
if (!val) {
|
||||
throw new Error('Fake workspace delete error')
|
||||
}
|
||||
|
||||
return val
|
||||
},
|
||||
update: (_parent, args) => {
|
||||
if (args.input.name === 'error') {
|
||||
throw new Error('Fake workspace update error')
|
||||
}
|
||||
|
||||
return getMockRef('Workspace', { values: omit(args.input, ['logoUrl']) })
|
||||
},
|
||||
updateRole: (_parent, args) => {
|
||||
const val = faker.datatype.boolean()
|
||||
|
||||
if (val) {
|
||||
throw new Error('Fake update role error')
|
||||
}
|
||||
|
||||
return getMockRef('Workspace', {
|
||||
id: args.input.workspaceId
|
||||
})
|
||||
},
|
||||
deleteRole: (_parent, args) => {
|
||||
const val = faker.datatype.boolean()
|
||||
|
||||
if (val) {
|
||||
throw new Error('Fake delete role error')
|
||||
}
|
||||
|
||||
return getMockRef('Workspace', {
|
||||
id: args.input.workspaceId
|
||||
})
|
||||
}
|
||||
},
|
||||
WorkspaceInviteMutations: {
|
||||
create: (_parent, args) => {
|
||||
const val = faker.datatype.boolean()
|
||||
|
||||
if (val) {
|
||||
throw new Error('Fake invite create error')
|
||||
}
|
||||
|
||||
return getMockRef('Workspace', {
|
||||
id: args.workspaceId
|
||||
})
|
||||
},
|
||||
batchCreate: (_parent, args) => {
|
||||
const val = faker.datatype.boolean()
|
||||
|
||||
if (val) {
|
||||
throw new Error('Fake batch create invite error')
|
||||
}
|
||||
|
||||
return getMockRef('Workspace', {
|
||||
id: args.workspaceId
|
||||
})
|
||||
},
|
||||
use: () => {
|
||||
const val = faker.datatype.boolean()
|
||||
if (!val) {
|
||||
throw new Error('Fake use invite error')
|
||||
}
|
||||
|
||||
return val
|
||||
},
|
||||
cancel: (_parent, args) => {
|
||||
const val = faker.datatype.boolean()
|
||||
|
||||
if (val) {
|
||||
throw new Error('Fake cancel invite error')
|
||||
}
|
||||
|
||||
return getMockRef('Workspace', {
|
||||
id: args.workspaceId
|
||||
})
|
||||
}
|
||||
},
|
||||
Query: {
|
||||
workspace: (_parent, args) => {
|
||||
if (args.id === '404') {
|
||||
throw new WorkspaceNotFoundError('Workspace not found')
|
||||
}
|
||||
|
||||
return getMockRef('Workspace', {
|
||||
id: args.id
|
||||
})
|
||||
},
|
||||
workspaceInvite: (_parent, args) => {
|
||||
const getResult = () => getMockRef('PendingWorkspaceCollaborator')
|
||||
if (args.token) {
|
||||
return getResult()
|
||||
}
|
||||
|
||||
return faker.datatype.boolean() ? getResult() : null
|
||||
}
|
||||
},
|
||||
User: {
|
||||
workspaces: resolveAndCache((_parent, args) =>
|
||||
getMockRef('WorkspaceCollection', {
|
||||
values: {
|
||||
cursor: args.cursor ? null : undefined
|
||||
}
|
||||
})
|
||||
),
|
||||
workspaceInvites: resolveAndCache(() =>
|
||||
times(faker.number.int({ min: 0, max: 2 }), () =>
|
||||
getMockRef('PendingWorkspaceCollaborator')
|
||||
)
|
||||
)
|
||||
},
|
||||
Workspace: {
|
||||
role: resolveFromMockParent(),
|
||||
team: resolveFromMockParent(),
|
||||
invitedTeam: resolveFromMockParent({
|
||||
mapRefs: (mock, { parent }) =>
|
||||
addMockRefValues(mock, {
|
||||
workspaceId: getFieldValue(parent, 'id'),
|
||||
workspaceName: getFieldValue(parent, 'name')
|
||||
})
|
||||
}),
|
||||
projects: resolveAndCache((_parent, args) =>
|
||||
getMockRef('ProjectCollection', {
|
||||
values: {
|
||||
cursor: args.cursor ? null : undefined
|
||||
}
|
||||
})
|
||||
)
|
||||
},
|
||||
WorkspaceCollaborator: {
|
||||
role: resolveFromMockParent()
|
||||
},
|
||||
PendingWorkspaceCollaborator: {
|
||||
user: resolveAndCache((parent) => {
|
||||
const title = getFieldValue<string>(parent, 'title')
|
||||
const isEmail = title.includes('@')
|
||||
if (isEmail) return null
|
||||
|
||||
return getMockRef('LimitedUser', { values: { name: title } })
|
||||
}),
|
||||
invitedBy: resolveAndCache(() => getMockRef('LimitedUser'))
|
||||
},
|
||||
Project: {
|
||||
workspace: resolveAndCache(() => {
|
||||
return faker.datatype.boolean() ? getMockRef('Workspace') : null
|
||||
})
|
||||
},
|
||||
AdminQueries: {
|
||||
workspaceList: resolveAndCache((_parent, args) =>
|
||||
getMockRef('WorkspaceCollection', {
|
||||
values: {
|
||||
cursor: args.cursor ? null : undefined
|
||||
}
|
||||
})
|
||||
)
|
||||
},
|
||||
WorkspaceCollection: {
|
||||
items: resolveAndCache((parent) => {
|
||||
const count = getFieldValue(parent, 'totalCount')
|
||||
|
||||
return times(count, () => getMockRef('Workspace'))
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
mocks: {
|
||||
Workspace: () => ({
|
||||
name: workspaceName(),
|
||||
description: faker.lorem.sentence(),
|
||||
role: faker.helpers.arrayElement(Object.values(Roles.Workspace)),
|
||||
team: listMock(1, 5),
|
||||
invitedTeam: listMock(1, 5)
|
||||
}),
|
||||
WorkspaceCollaborator: () => ({
|
||||
role: () => faker.helpers.arrayElement(Object.values(Roles.Server))
|
||||
}),
|
||||
PendingWorkspaceCollaborator: () => ({
|
||||
inviteId: faker.string.uuid(),
|
||||
workspaceId: faker.string.uuid(),
|
||||
workspaceName: workspaceName(),
|
||||
title: faker.datatype.boolean()
|
||||
? faker.internet.email()
|
||||
: faker.person.fullName(),
|
||||
role: faker.helpers.arrayElement(Object.values(Roles.Workspace)),
|
||||
token: faker.string.alphanumeric(32)
|
||||
}),
|
||||
WorkspaceCollection: () => ({
|
||||
totalCount: faker.number.int({ min: 0, max: 10 })
|
||||
})
|
||||
}
|
||||
}
|
||||
: {}
|
||||
export default config
|
|
@ -39,7 +39,6 @@
|
|||
"@apollo/client": "^3.7.0",
|
||||
"@aws-sdk/client-s3": "^3.276.0",
|
||||
"@aws-sdk/lib-storage": "^3.100.0",
|
||||
"@faker-js/faker": "^7.1.0",
|
||||
"@godaddy/terminus": "^4.9.0",
|
||||
"@graphql-tools/schema": "^10.0.4",
|
||||
"@mailchimp/mailchimp_marketing": "^3.0.80",
|
||||
|
@ -117,6 +116,7 @@
|
|||
"devDependencies": {
|
||||
"@apollo/rover": "^0.23.0",
|
||||
"@bull-board/express": "^4.2.2",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@graphql-codegen/cli": "^5.0.2",
|
||||
"@graphql-codegen/typed-document-node": "^5.0.7",
|
||||
"@graphql-codegen/typescript": "^4.0.7",
|
||||
|
|
|
@ -1704,6 +1704,7 @@ export type PendingWorkspaceCollaborator = {
|
|||
id: Scalars['ID']['output'];
|
||||
inviteId: Scalars['String']['output'];
|
||||
invitedBy: LimitedUser;
|
||||
/** Target workspace role */
|
||||
role: Scalars['String']['output'];
|
||||
/** E-mail address or name of the invited user */
|
||||
title: Scalars['String']['output'];
|
||||
|
@ -2414,6 +2415,13 @@ export type Query = {
|
|||
*/
|
||||
userSearch: UserSearchResultCollection;
|
||||
workspace: Workspace;
|
||||
/**
|
||||
* Look for an invitation to a workspace, for the current user (authed or not). If token
|
||||
* isn't specified, the server will look for any valid invite.
|
||||
*
|
||||
* If token is specified, it will return the corresponding invite even if it belongs to a different user.
|
||||
*/
|
||||
workspaceInvite?: Maybe<PendingWorkspaceCollaborator>;
|
||||
};
|
||||
|
||||
|
||||
|
@ -2544,6 +2552,12 @@ export type QueryWorkspaceArgs = {
|
|||
id: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryWorkspaceInviteArgs = {
|
||||
token?: InputMaybe<Scalars['String']['input']>;
|
||||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
/** Deprecated: Used by old stream-based mutations */
|
||||
export type ReplyCreateInput = {
|
||||
/** IDs of uploaded blobs that should be attached to this reply */
|
||||
|
@ -3344,6 +3358,8 @@ export type User = {
|
|||
* Note: Only count resolution is currently implemented
|
||||
*/
|
||||
versions: CountOnlyCollection;
|
||||
/** Get all invitations to workspaces that the active user has */
|
||||
workspaceInvites: Array<PendingWorkspaceCollaborator>;
|
||||
/** Get the workspaces for the user */
|
||||
workspaces: WorkspaceCollection;
|
||||
};
|
||||
|
@ -3715,6 +3731,7 @@ export type Workspace = {
|
|||
createdAt: Scalars['DateTime']['output'];
|
||||
description?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['ID']['output'];
|
||||
/** Only available to workspace owners */
|
||||
invitedTeam?: Maybe<Array<PendingWorkspaceCollaborator>>;
|
||||
name: Scalars['String']['output'];
|
||||
projects: ProjectCollection;
|
||||
|
@ -3794,18 +3811,17 @@ export type WorkspaceInviteMutationsUseArgs = {
|
|||
export type WorkspaceInviteUseInput = {
|
||||
accept: Scalars['Boolean']['input'];
|
||||
token: Scalars['String']['input'];
|
||||
workspaceId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type WorkspaceMutations = {
|
||||
__typename?: 'WorkspaceMutations';
|
||||
create: Workspace;
|
||||
delete: Workspace;
|
||||
deleteRole: Scalars['Boolean']['output'];
|
||||
delete: Scalars['Boolean']['output'];
|
||||
deleteRole: Workspace;
|
||||
invites: WorkspaceInviteMutations;
|
||||
update: Workspace;
|
||||
/** TODO: `@hasWorkspaceRole(role: WORKSPACE_ADMIN)` for role changes */
|
||||
updateRole: Scalars['Boolean']['output'];
|
||||
updateRole: Workspace;
|
||||
};
|
||||
|
||||
|
||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -10047,10 +10047,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@faker-js/faker@npm:^7.1.0":
|
||||
version: 7.1.0
|
||||
resolution: "@faker-js/faker@npm:7.1.0"
|
||||
checksum: 10/62161e9ac25ab55db7d5fc2ea6177b94be07107275ef11137b88645cd2d41845f5d301077f84ee4663977bf9d42776bb244a23595b791deb9afa4c91366d5d2f
|
||||
"@faker-js/faker@npm:^8.4.1":
|
||||
version: 8.4.1
|
||||
resolution: "@faker-js/faker@npm:8.4.1"
|
||||
checksum: 10/5983c2ea64f26055ad6648de748878e11ebe2fb751e3c7435ae141cdffabc2dccfe4c4f49da69a3d2add71e21b415c683ac5fba196fab0d5ed6779fbec436c80
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -15550,7 +15550,7 @@ __metadata:
|
|||
"@aws-sdk/client-s3": "npm:^3.276.0"
|
||||
"@aws-sdk/lib-storage": "npm:^3.100.0"
|
||||
"@bull-board/express": "npm:^4.2.2"
|
||||
"@faker-js/faker": "npm:^7.1.0"
|
||||
"@faker-js/faker": "npm:^8.4.1"
|
||||
"@godaddy/terminus": "npm:^4.9.0"
|
||||
"@graphql-codegen/cli": "npm:^5.0.2"
|
||||
"@graphql-codegen/typed-document-node": "npm:^5.0.7"
|
||||
|
|
Загрузка…
Ссылка в новой задаче