Closed jpike88 closed 1 year ago
Got it again, this is a dump of the output window in vscode:
[cli-stdout] data 84
[cli] exit 0
Connecting to "/var/folders/0v/w61lqyb97l37zrp46x8s24900000gn/T/rome-socket-11.0.0-nightly.fab5440" ...
[Info - 11:11:33 PM] Server initialized with PID: 50396
[Error - 5:08:22 AM] Request textDocument/codeAction failed.
Message: failed to access range Range { start: Position { line: 2411, character: 38 }, end: Position { line: 2411, character: 43 } } in document file:///Users/joshpike/Projects/aquipa/server/src/components/api/assessment/assessmentUtil.ts
Code: -32603
failed to access range Range { start: Position { line: 2411, character: 38 }, end: Position { line: 2411, character: 43 } } in document file:///Users/joshpike/Projects/aquipa/server/src/components/api/assessment/assessmentUtil.ts
Caused by:
position Position { line: 2411, character: 43 } is out of range
[Error - 6:13:10 PM] Client Rome: connection to server is erroring. Shutting down server.
[Error - 6:13:10 PM] Client Rome: connection to server is erroring. Shutting down server.
[Error - 6:13:10 PM] Connection to server got closed. Server will not be restarted.
[Error - 6:13:10 PM] Stopping server failed
Message: Pending response rejected since connection got disposed
Code: -32097
[Error - 6:13:10 PM] Stopping server failed
Message: Pending response rejected since connection got disposed
Code: -32097
[Error - 6:13:10 PM] Stopping server failed
Message: Pending response rejected since connection got disposed
Code: -32097
[cli-stderr] end
[cli-stdout] end
[cli-stdout] close
[cli] close 0
[cli-stderr] close
[cli-stdout] data 84
[cli] exit 0
Connecting to "/var/folders/0v/w61lqyb97l37zrp46x8s24900000gn/T/rome-socket-11.0.0-nightly.fab5440" ...
[Info - 6:13:23 PM] Server initialized with PID: 57455
[Error - 6:13:23 PM] Request textDocument/codeAction failed.
Message: the file does not exist in the workspace
Code: -32603
the file does not exist in the workspace
Clicking each time in the code window causes the dialog to appear again and again, same error about a file not existing in the workspace.
Could you please update the issue template with the info coming from rome rage
?
CLI:
Version: 11.0.0-nightly.fab5440
Color support: true
Platform:
CPU Architecture: aarch64
OS: macos
Environment:
ROME_LOG_DIR: unset
NO_COLOR: unset
TERM: "xterm-256color"
JS_RUNTIME_VERSION: "v16.14.2"
JS_RUNTIME_NAME: "node"
NODE_PACKAGE_MANAGER: "npm/9.1.1"
Rome Configuration:
Status: loaded
Formatter disabled: true
Linter disabled: false
Workspace:
Open Documents: 0
Discovering running Rome servers...
Running Rome Server: ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βΉ The client isn't connected to any server but rage discovered this running Rome server.
Server:
Version: 11.0.0-nightly.fab5440
Name: rome_lsp
CPU Architecture: aarch64
OS: macos
Workspace:
Open Documents: 0
Other Active Server Workspaces:
Workspace:
Open Documents: 1
Client Name: Visual Studio Code
Client Version: 1.74.3
Rome Server Log:
β Please review the content of the log file before sharing it publicly as it may contain sensitive information:
* Path names that may reveal your name, a project name, or the name of your employer.
* Source code
βrome_cli::commands::daemon::Running Server{pid=57455}
ββ48ms ERROR tower_lsp::transport failed to encode message: failed to encode response: Socket is not connected (os error 57)
βββrome_lsp::server::initialize{root_uri=file:///Users/joshpike/Projects/aquipa, capabilities=ClientCapabilities { workspace: Some(WorkspaceClientCapabilities { apply_edit: Some(true), workspace_edit: Some(WorkspaceEditClientCapabilities { document_changes: Some(true), resource_operations: Some([Create, Rename, Delete]), failure_handling: Some(TextOnlyTransactional), normalizes_line_endings: Some(true), change_annotation_support: Some(ChangeAnnotationWorkspaceEditClientCapabilities { groups_on_label: Some(true) }) }), did_change_configuration: Some(DynamicRegistrationClientCapabilities { dynamic_registration: Some(true) }), did_change_watched_files: Some(DynamicRegistrationClientCapabilities { dynamic_registration: Some(true) }), symbol: Some(WorkspaceSymbolClientCapabilities { dynamic_registration: Some(true), symbol_kind: Some(SymbolKindCapability { value_set: Some([File, Module, Namespace, Package, Class, Method, Property, Field, Constructor, Enum, Interface, Function, Variable, Constant, String, Number, Boolean, Array, Object, Key, Null, EnumMember, Struct, Event, Operator, TypeParameter]) }), tag_support: Some(TagSupport { value_set: [Deprecated] }) }), execute_command: Some(DynamicRegistrationClientCapabilities { dynamic_registration: Some(true) }), workspace_folders: Some(true), configuration: Some(true), semantic_tokens: Some(SemanticTokensWorkspaceClientCapabilities { refresh_support: Some(true) }), code_lens: Some(CodeLensWorkspaceClientCapabilities { refresh_support: Some(true) }), file_operations: Some(WorkspaceFileOperationsClientCapabilities { dynamic_registration: Some(true), did_create: Some(true), will_create: Some(true), did_rename: Some(true), will_rename: Some(true), did_delete: Some(true), will_delete: Some(true) }) }), text_document: Some(TextDocumentClientCapabilities { synchronization: Some(TextDocumentSyncClientCapabilities { dynamic_registration: Some(true), will_save: Some(true), will_save_wait_until: Some(true), did_save: Some(true) }), completion: Some(CompletionClientCapabilities { dynamic_registration: Some(true), completion_item: Some(CompletionItemCapability { snippet_support: Some(true), commit_characters_support: Some(true), documentation_format: Some([Markdown, PlainText]), deprecated_support: Some(true), preselect_support: Some(true), tag_support: Some(TagSupport { value_set: [Deprecated] }), insert_replace_support: Some(true), resolve_support: Some(CompletionItemCapabilityResolveSupport { properties: ["documentation", "detail", "additionalTextEdits"] }), insert_text_mode_support: Some(InsertTextModeSupport { value_set: [AsIs, AdjustIndentation] }) }), completion_item_kind: Some(CompletionItemKindCapability { value_set: Some([Text, Method, Function, Constructor, Field, Variable, Class, Interface, Module, Property, Unit, Value, Enum, Keyword, Snippet, Color, File, Reference, Folder, EnumMember, Constant, Struct, Event, Operator, TypeParameter]) }), context_support: Some(true) }), hover: Some(HoverClientCapabilities { dynamic_registration: Some(true), content_format: Some([Markdown, PlainText]) }), signature_help: Some(SignatureHelpClientCapabilities { dynamic_registration: Some(true), signature_information: Some(SignatureInformationSettings { documentation_format: Some([Markdown, PlainText]), parameter_information: Some(ParameterInformationSettings { label_offset_support: Some(true) }), active_parameter_support: Some(true) }), context_support: Some(true) }), references: Some(DynamicRegistrationClientCapabilities { dynamic_registration: Some(true) }), document_highlight: Some(DynamicRegistrationClientCapabilities { dynamic_registration: Some(true) }), document_symbol: Some(DocumentSymbolClientCapabilities { dynamic_registration: Some(true), symbol_kind: Some(SymbolKindCapability { value_set: Some([File, Module, Namespace, Package, Class, Method, Property, Field, Constructor, Enum, Interface, Function, Variable, Constant, String, Number, Boolean, Array, Object, Key, Null, EnumMember, Struct, Event, Operator, TypeParameter]) }), hierarchical_document_symbol_support: Some(true), tag_support: Some(TagSupport { value_set: [Deprecated] }) }), formatting: Some(DynamicRegistrationClientCapabilities { dynamic_registration: Some(true) }), range_formatting: Some(DynamicRegistrationClientCapabilities { dynamic_registration: Some(true) }), on_type_formatting: Some(DynamicRegistrationClientCapabilities { dynamic_registration: Some(true) }), declaration: Some(GotoCapability { dynamic_registration: Some(true), link_support: Some(true) }), definition: Some(GotoCapability { dynamic_registration: Some(true), link_support: Some(true) }), type_definition: Some(GotoCapability { dynamic_registration: Some(true), link_support: Some(true) }), implementation: Some(GotoCapability { dynamic_registration: Some(true), link_support: Some(true) }), code_action: Some(CodeActionClientCapabilities { dynamic_registration: Some(true), code_action_literal_support: Some(CodeActionLiteralSupport { code_action_kind: CodeActionKindLiteralSupport { value_set: ["", "quickfix", "refactor", "refactor.extract", "refactor.inline", "refactor.rewrite", "source", "source.organizeImports"] } }), is_preferred_support: Some(true), disabled_support: Some(true), data_support: Some(true), resolve_support: Some(CodeActionCapabilityResolveSupport { properties: ["edit"] }), honors_change_annotations: Some(false) }), code_lens: Some(DynamicRegistrationClientCapabilities { dynamic_registration: Some(true) }), document_link: Some(DocumentLinkClientCapabilities { dynamic_registration: Some(true), tooltip_support: Some(true) }), color_provider: Some(DynamicRegistrationClientCapabilities { dynamic_registration: Some(true) }), rename: Some(RenameClientCapabilities { dynamic_registration: Some(true), prepare_support: Some(true), prepare_support_default_behavior: Some(Identifier), honors_change_annotations: Some(true) }), publish_diagnostics: Some(PublishDiagnosticsClientCapabilities { related_information: Some(true), tag_support: Some(TagSupport { value_set: [Unnecessary, Deprecated] }), version_support: Some(false), code_description_support: Some(true), data_support: Some(true) }), folding_range: Some(FoldingRangeClientCapabilities { dynamic_registration: Some(true), range_limit: Some(5000), line_folding_only: Some(true) }), selection_range: Some(SelectionRangeClientCapabilities { dynamic_registration: Some(true) }), linked_editing_range: Some(DynamicRegistrationClientCapabilities { dynamic_registration: Some(true) }), call_hierarchy: Some(DynamicRegistrationClientCapabilities { dynamic_registration: Some(true) }), semantic_tokens: Some(SemanticTokensClientCapabilities { dynamic_registration: Some(true), requests: SemanticTokensClientCapabilitiesRequests { range: Some(true), full: Some(Delta { delta: Some(true) }) }, token_types: [SemanticTokenType("namespace"), SemanticTokenType("type"), SemanticTokenType("class"), SemanticTokenType("enum"), SemanticTokenType("interface"), SemanticTokenType("struct"), SemanticTokenType("typeParameter"), SemanticTokenType("parameter"), SemanticTokenType("variable"), SemanticTokenType("property"), SemanticTokenType("enumMember"), SemanticTokenType("event"), SemanticTokenType("function"), SemanticTokenType("method"), SemanticTokenType("macro"), SemanticTokenType("keyword"), SemanticTokenType("modifier"), SemanticTokenType("comment"), SemanticTokenType("string"), SemanticTokenType("number"), SemanticTokenType("regexp"), SemanticTokenType("operator"), SemanticTokenType("decorator")], token_modifiers: [SemanticTokenModifier("declaration"), SemanticTokenModifier("definition"), SemanticTokenModifier("readonly"), SemanticTokenModifier("static"), SemanticTokenModifier("deprecated"), SemanticTokenModifier("abstract"), SemanticTokenModifier("async"), SemanticTokenModifier("modification"), SemanticTokenModifier("documentation"), SemanticTokenModifier("defaultLibrary")], formats: [TokenFormat("relative")], overlapping_token_support: Some(false), multiline_token_support: Some(false) }), moniker: None }), window: Some(WindowClientCapabilities { work_done_progress: Some(true), show_message: Some(ShowMessageRequestClientCapabilities { message_action_item: Some(MessageActionItemCapabilities { additional_properties_support: Some(true) }) }), show_document: Some(ShowDocumentClientCapabilities { support: true }) }), general: Some(GeneralClientCapabilities { regular_expressions: Some(RegularExpressionsClientCapabilities { engine: "ECMAScript", version: Some("ES2020") }), markdown: Some(MarkdownClientCapabilities { parser: "marked", version: Some("1.1.0") }) }), experimental: None }, client_info=ClientInfo { name: "Visual Studio Code", version: Some("1.74.3") }, root_path="/Users/joshpike/Projects/aquipa", workspace_folders=[WorkspaceFolder { uri: Url { scheme: "file", cannot_be_a_base: false, username: "", password: None, host: None, port: None, path: "/Users/joshpike/Projects/aquipa", query: None, fragment: None }, name: "aquipa" }]}
β ββ0ms INFO rome_lsp::server Starting Rome Language Server...
β ββ0ms WARN rome_lsp::server The Rome Server was initialized with the deprecated `root_path` parameter: this is not supported, use `root_uri` instead
β ββ0ms WARN rome_lsp::server The Rome Server was initialized with the `workspace_folders` parameter: this is unsupported at the moment, use `root_uri` instead
βββ
βββrome_lsp::server::initialized{params=InitializedParams}
β ββ0ms INFO rome_lsp::server Attempting to load the configuration from 'rome.json' file
β βββrome_lsp::session::load_client_configuration{}
β β ββ2ms INFO rome_lsp::session Loaded client configuration: Object {
β β β "lspBin": Null,
β β β "rename": Null,
β β β }
β βββ
β βββrome_lsp::session::load_workspace_settings{}
β β ββ0ms INFO rome_service::configuration Attempting to load the configuration file at path "/Users/joshpike/Projects/aquipa/rome.json"
β β βββrome_fs::fs::os::OsFileSystem::open_with_options{path="/Users/joshpike/Projects/aquipa/rome.json", options=OpenOptions { read: true, write: false, truncate: false, create: false, create_new: false }}
β β βββ
β β βββrome_fs::fs::os::OsFile::read_to_string{}
β β βββ
β β ββ0ms INFO rome_lsp::session Loaded workspace settings: Configuration {
β β β schema: None,
β β β files: None,
β β β formatter: Some(
β β β FormatterConfiguration {
β β β enabled: false,
β β β format_with_errors: false,
β β β indent_style: Tab,
β β β indent_size: 2,
β β β line_width: LineWidth(
β β β 80,
β β β ),
β β β ignore: None,
β β β },
β β β ),
β β β linter: Some(
β β β LinterConfiguration {
β β β enabled: true,
β β β rules: Some(
β β β Rules {
β β β recommended: None,
β β β a11y: None,
β β β complexity: Some(
β β β Complexity {
β β β recommended: None,
β β β rules: {
β β β "noExtraBooleanCast": Plain(
β β β Off,
β β β ),
β β β "useSimplifiedLogicExpression": Plain(
β β β Off,
β β β ),
β β β "useOptionalChain": Plain(
β β β Off,
β β β ),
β β β },
β β β },
β β β ),
β β β correctness: Some(
β β β Correctness {
β β β recommended: None,
β β β rules: {
β β β "noUnusedVariables": Plain(
β β β Off,
β β β ),
β β β "noUnreachable": Plain(
β β β Error,
β β β ),
β β β },
β β β },
β β β ),
β β β nursery: Some(
β β β Nursery {
β β β recommended: None,
β β β rules: {
β β β "noEmptyInterface": Plain(
β β β Error,
β β β ),
β β β "noVar": Plain(
β β β Error,
β β β ),
β β β "useConst": Plain(
β β β Error,
β β β ),
β β β "noInvalidConstructorSuper": Plain(
β β β Error,
β β β ),
β β β "noUnsafeFinally": Plain(
β β β Error,
β β β ),
β β β "noAssignInExpressions": Plain(
β β β Error,
β β β ),
β β β "noBannedTypes": Plain(
β β β Off,
β β β ),
β β β "useCamelCase": Plain(
β β β Off,
β β β ),
β β β "useEnumInitializers": Plain(
β β β Off,
β β β ),
β β β "useDefaultParameterLast": Plain(
β β β Off,
β β β ),
β β β },
β β β },
β β β ),
β β β performance: Some(
β β β Performance {
β β β recommended: None,
β β β rules: {
β β β "noDelete": Plain(
β β β Off,
β β β ),
β β β },
β β β },
β β β ),
β β β security: None,
β β β style: Some(
β β β Style {
β β β recommended: None,
β β β rules: {
β β β "useShorthandArrayType": Plain(
β β β Off,
β β β ),
β β β "useSingleVarDeclarator": Plain(
β β β Error,
β β β ),
β β β "useTemplate": Plain(
β β β Off,
β β β ),
β β β "noUnusedTemplateLiteral": Plain(
β β β Error,
β β β ),
β β β },
β β β },
β β β ),
β β β suspicious: Some(
β β β Suspicious {
β β β recommended: None,
β β β rules: {
β β β "noDoubleEquals": Plain(
β β β Error,
β β β ),
β β β "noDebugger": Plain(
β β β Error,
β β β ),
β β β "noShadowRestrictedNames": Plain(
β β β Error,
β β β ),
β β β "noExplicitAny": Plain(
β β β Off,
β β β ),
β β β "useValidTypeof": Plain(
β β β Error,
β β β ),
β β β "noAsyncPromiseExecutor": Plain(
β β β Off,
β β β ),
β β β },
β β β },
β β β ),
β β β },
β β β ),
β β β ignore: None,
β β β },
β β β ),
β β β javascript: None,
β β β }
β β βββrome_service::workspace::server::update_settings{params=UpdateSettingsParams { configuration: Configuration { schema: None, files: None, formatter: Some(FormatterConfiguration { enabled: false, format_with_errors: false, indent_style: Tab, indent_size: 2, line_width: LineWidth(80), ignore: None }), linter: Some(LinterConfiguration { enabled: true, rules: Some(Rules { recommended: None, a11y: None, complexity: Some(Complexity { recommended: None, rules: {"noExtraBooleanCast": Plain(Off), "useSimplifiedLogicExpression": Plain(Off), "useOptionalChain": Plain(Off)} }), correctness: Some(Correctness { recommended: None, rules: {"noUnusedVariables": Plain(Off), "noUnreachable": Plain(Error)} }), nursery: Some(Nursery { recommended: None, rules: {"noEmptyInterface": Plain(Error), "noVar": Plain(Error), "useConst": Plain(Error), "noInvalidConstructorSuper": Plain(Error), "noUnsafeFinally": Plain(Error), "noAssignInExpressions": Plain(Error), "noBannedTypes": Plain(Off), "useCamelCase": Plain(Off), "useEnumInitializers": Plain(Off), "useDefaultParameterLast": Plain(Off)} }), performance: Some(Performance { recommended: None, rules: {"noDelete": Plain(Off)} }), security: None, style: Some(Style { recommended: None, rules: {"useShorthandArrayType": Plain(Off), "useSingleVarDeclarator": Plain(Error), "useTemplate": Plain(Off), "noUnusedTemplateLiteral": Plain(Error)} }), suspicious: Some(Suspicious { recommended: None, rules: {"noDoubleEquals": Plain(Error), "noDebugger": Plain(Error), "noShadowRestrictedNames": Plain(Error), "noExplicitAny": Plain(Off), "useValidTypeof": Plain(Error), "noAsyncPromiseExecutor": Plain(Off)} }) }), ignore: None }), javascript: None } }}
β β β βββrome_service::settings::merge_with_configuration{configuration=Configuration { schema: None, files: None, formatter: Some(FormatterConfiguration { enabled: false, format_with_errors: false, indent_style: Tab, indent_size: 2, line_width: LineWidth(80), ignore: None }), linter: Some(LinterConfiguration { enabled: true, rules: Some(Rules { recommended: None, a11y: None, complexity: Some(Complexity { recommended: None, rules: {"noExtraBooleanCast": Plain(Off), "useSimplifiedLogicExpression": Plain(Off), "useOptionalChain": Plain(Off)} }), correctness: Some(Correctness { recommended: None, rules: {"noUnusedVariables": Plain(Off), "noUnreachable": Plain(Error)} }), nursery: Some(Nursery { recommended: None, rules: {"noEmptyInterface": Plain(Error), "noVar": Plain(Error), "useConst": Plain(Error), "noInvalidConstructorSuper": Plain(Error), "noUnsafeFinally": Plain(Error), "noAssignInExpressions": Plain(Error), "noBannedTypes": Plain(Off), "useCamelCase": Plain(Off), "useEnumInitializers": Plain(Off), "useDefaultParameterLast": Plain(Off)} }), performance: Some(Performance { recommended: None, rules: {"noDelete": Plain(Off)} }), security: None, style: Some(Style { recommended: None, rules: {"useShorthandArrayType": Plain(Off), "useSingleVarDeclarator": Plain(Error), "useTemplate": Plain(Off), "noUnusedTemplateLiteral": Plain(Error)} }), suspicious: Some(Suspicious { recommended: None, rules: {"noDoubleEquals": Plain(Error), "noDebugger": Plain(Error), "noShadowRestrictedNames": Plain(Error), "noExplicitAny": Plain(Off), "useValidTypeof": Plain(Error), "noAsyncPromiseExecutor": Plain(Off)} }) }), ignore: None }), javascript: None }}
β β β βββ
β β βββ
β βββ
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/sql.ts, range=Range { start: Position { line: 199, character: 13 }, end: Position { line: 199, character: 13 } }, only=None, diagnostics=[]}
β ββ0ms ERROR rome_lsp::handlers::analysis error=the file does not exist in the workspace
βββ
ββ1060ms ERROR rome_lsp::utils Error: the file does not exist in the workspace
βββ
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/sql.ts, range=Range { start: Position { line: 203, character: 33 }, end: Position { line: 203, character: 33 } }, only=None, diagnostics=[]}
β ββ0ms ERROR rome_lsp::handlers::analysis error=the file does not exist in the workspace
βββ
ββ57204ms ERROR rome_lsp::utils Error: the file does not exist in the workspace
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/sql.ts, range=Range { start: Position { line: 209, character: 47 }, end: Position { line: 209, character: 47 } }, only=None, diagnostics=[]}
β ββ0ms ERROR rome_lsp::handlers::analysis error=the file does not exist in the workspace
βββ
ββ60364ms ERROR rome_lsp::utils Error: the file does not exist in the workspace
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/sql.ts, range=Range { start: Position { line: 205, character: 40 }, end: Position { line: 205, character: 40 } }, only=None, diagnostics=[]}
β ββ0ms ERROR rome_lsp::handlers::analysis error=the file does not exist in the workspace
βββ
ββ64716ms ERROR rome_lsp::utils Error: the file does not exist in the workspace
βββrome_lsp::handlers::text_document::did_open{params=DidOpenTextDocumentParams { text_document: TextDocumentItem { uri: Url { scheme: "file", cannot_be_a_base: false, username: "", password: None, host: None, port: None, path: "/Users/joshpike/Projects/aquipa/server/src/components/api/assessment/assessmentUtil.ts", query: None, fragment: None }, language_id: "typescript", version: 49, text: "import {\n\tUserAndBusinessID,\n\tcloseCase,\n\tcreateCase,\n} from '@server/api/case/caseUtil';\nimport {\n\tcomment,\n\tgetBaseEventLogQuery,\n} from '@server/api/conversation/conversationUtil';\nimport {\n\tassignFormToUser,\n\tprocessSubmittedFormData,\n} from '@server/api/form/formUtil';\nimport {\n\tcheckDeadline,\n\teditMaintenanceUsageParams,\n\tgetDueSoonMaintenanceDeadlines,\n\tmaintenanceOverdue,\n} from '@server/api/maintenance/maintenanceUtil';\nimport { Junction } from '@server/api/plant/algebraUtil';\nimport { Operator } from '@server/api/plant/plantQueryUtil';\nimport {\n\tapplyPlantAssessmentSelectorsToQuery,\n\tgetAssessmentRowForPlant,\n\tgetOwnerBusinessIDOfPlant,\n\ttagOutPlant,\n} from '@server/api/plant/plantUtil';\nimport { getSpecificationsForPlant } from '@server/api/plant/specificationUtil';\nimport { getClientGroupIDsFromSchemaIDs } from '@server/api/schema/schemaUtil';\nimport { createTask } from '@server/api/task/taskUtil';\nimport {\n\tgetClientBusinessIDsForClientGroupIDs,\n\tgetClientGroupIDsForPlantAssessment,\n\tgetCompactUser,\n\tgetDefaultUsersQuery,\n} from '@server/api/user/userUtil';\nimport core from '@server/core';\nimport { Event } from '@server/events/eventDefinitions';\nimport { getEventForDisplay } from '@server/events/eventUtil';\nimport redis from '@server/redis';\nimport sql from '@server/sql';\nimport {\n\tSchemaSelectors,\n\tconvertStringDurationToSeconds,\n\tgetUserTeamObjectFromIDs,\n} from '@server/util';\nimport { FormType } from '@shared/Template';\nimport { Assessment } from '@shared/models/Assessment';\nimport { Business, ClientGroupMap } from '@shared/models/Business';\nimport { CaseReason, CaseRow, CaseStatus } from '@shared/models/Case';\nimport { ConversationType } from '@shared/models/Conversation';\nimport { EventForDisplay } from '@shared/models/Event';\nimport { FormData } from '@shared/models/FormInterimData';\nimport { KeyDocument } from '@shared/models/KeyDocument';\nimport {\n\tMaintenanceSequenceStep,\n\tMaintenanceSequenceStepInsertionRow,\n\tMaintenanceUsageParams,\n\tNewMaintenanceSchedule,\n} from '@shared/models/Maintenance';\nimport { NonConformances } from '@shared/models/NonConformance';\nimport { OperatingCondition } from '@shared/models/OperatingCondition';\nimport {\n\tPlant,\n\tPlantAssessment,\n\tPlantAssessmentStepInstance,\n\tPlantAssessmentStepInstanceInsertion,\n\tPlantSchema,\n} from '@shared/models/Plant';\nimport { Schema, UserSchemaAssessmentPermissions } from '@shared/models/Schema';\nimport { SpecificationChange } from '@shared/models/SpecificationChange';\nimport { TaskReason } from '@shared/models/Task';\nimport { CompactUser, User, UserTeamObject } from '@shared/models/User';\n\nexport async function calculateDateFromMonthsEval(plantID, evalString) {\n\t// this means that nextDueDate needs to be automatically calculated.\n\t// if the assessment is NOT recurring, then it doesn't need to fire.\n\n\tconst specifications = await getSpecificationsForPlant(plantID);\n\t// this means that the assessment proceeds straight through. increment date server side.\n\tconst currentDate = new Date();\n\tconst currentYear = currentDate.getFullYear();\n\n\tconst evalObjects = { currentYear };\n\tfor (const specification of specifications) {\n\t\tevalObjects[specification.id.toString()] = specification.getRawValue();\n\t}\n\n\tconst results: string[] = evalString.match(/\\${[\\w\\W]+?}/g);\n\t(results || []).map((varMatch: string) => {\n\t\tevalString = evalString.replace(\n\t\t\tvarMatch,\n\t\t\tevalObjects[varMatch.substr(2).slice(0, -1)]\n\t\t);\n\t});\n\tconst maxMonthExpiry = core.exprEval.Parser.evaluate(evalString);\n\n\treturn core.moment().add(maxMonthExpiry, 'M').startOf('day').unix();\n}\n\nexport async function getAssessmentsForSchemaIDs(\n\tschemaIDs: number[]\n): Promise<Assessment[]> {\n\tconst assessments = await sql\n\t\t.knex('assessment')\n\t\t.join(\n\t\t\t'schema_assessment_map',\n\t\t\t'schema_assessment_map.assessmentID',\n\t\t\t'assessment.id'\n\t\t)\n\t\t.whereIn('schema_assessment_map.schemaID', schemaIDs)\n\t\t.where('schema_assessment_map.active', true)\n\t\t.where('assessment.active', true)\n\t\t.groupBy('assessment.id')\n\t\t.select('assessment.*');\n\tfor (const assessment of assessments) {\n\t\tconst rows = await sql\n\t\t\t.knex('schema_assessment_map')\n\t\t\t.where('schema_assessment_map.assessmentID', assessment.id)\n\t\t\t.where('active', true);\n\t\tassessment.schemaIDs = rows.map((row) => row.schemaID);\n\t\tassessment.schemas = await sql\n\t\t\t.knex(sql.Table.SchemaAssessmentMap)\n\t\t\t.where('schema_assessment_map.assessmentID', assessment.id)\n\t\t\t.join('schema', 'schema_assessment_map.schemaID', 'schema.id')\n\t\t\t.groupBy('schema.id')\n\t\t\t.select(SchemaSelectors);\n\t}\n\treturn assessments;\n}\n\nexport async function changeAssessmentDate(\n\tplantID: Plant['id'],\n\tassessmentID: number,\n\toriginatingUserID: User['id'],\n\tnextDueDate: number = null,\n\tpushDateForward = false,\n\tforceNullDatesForNonRecurringAssessments = false\n) {\n\tawait redis.delete(redis.CacheDataType.Plant, plantID);\n\tconst plantAssessment: PlantAssessment = await getAssessmentRowForPlant(\n\t\tplantID,\n\t\t[],\n\t\tassessmentID,\n\t\ttrue,\n\t\tnull\n\t);\n\n\t// we may trigger this from either the delegate assessors dialog,\n\t// or have this be fired from an auto-approval of an assessment.\n\n\t// if from a dialog, nextDueDate -> means that date is NOT included in the call.\n\t// therefore it should be ignored.\n\tif (nextDueDate !== null) {\n\t\t// this means we want to set the date manually.\n\t\t// this is an override. this can be fired for either a recurring, or non-recurring assessment.\n\t\t// don't need to do anything, as the date has been decided on.\n\t} else {\n\t\t// the due date is null.\n\t\t// if the assessment is recurring, and if the pushDateForward flag is overridden, we want to auto-calculate the next date.\n\n\t\tif (plantAssessment.isRecurring && pushDateForward) {\n\t\t\tnextDueDate = await calculateDateFromMonthsEval(\n\t\t\t\tplantID,\n\t\t\t\tplantAssessment.maxExpiryMonthsEval\n\t\t\t);\n\t\t}\n\t}\n\n\t// we only want to set a due date if nextDueDate has a value.\n\t// at some point, we'll probably want a way for a non-recurring assessment to be 'green', which means that nextDueDate would be null anyway.\n\tif (nextDueDate !== null || forceNullDatesForNonRecurringAssessments) {\n\t\tawait sql.updateRowsSafe(\n\t\t\tsql.Table.PlantAssessment,\n\t\t\t{\n\t\t\t\tid: plantAssessment.id,\n\t\t\t},\n\t\t\t{\n\t\t\t\tisInitialAssessment: false,\n\t\t\t}\n\t\t);\n\n\t\t// assessments only have one step, we don't need to specify what step to update\n\t\tawait sql.updateRowsSafe(\n\t\t\tsql.Table.PlantAssessmentStepMap,\n\t\t\t{\n\t\t\t\tplantAssessmentID: plantAssessment.id,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdateDue: nextDueDate,\n\t\t\t\toverdue: nextDueDate <= core.moment().unix(),\n\t\t\t}\n\t\t);\n\n\t\tnew Event.AssessmentDueDateModified({\n\t\t\tassessmentID: plantAssessment.assessmentID,\n\t\t\tplantID: plantAssessment.plantID,\n\t\t\tcreatorID: originatingUserID,\n\t\t\tisSuperadmin: false,\n\t\t\tfromDate: plantAssessment.dateDue,\n\t\t\ttoDate: nextDueDate,\n\t\t});\n\t}\n}\n\nexport async function getDefaultAssigneesForAssessment(\n\tassessmentIDs: Array<Assessment['id']>,\n\tschemaID: number,\n\tbusinessID: string\n): Promise<UserTeamObject> {\n\tconst rows = await sql\n\t\t.knex(sql.Table.AssessmentDefaultAssigneeMap)\n\t\t.where('schemaID', schemaID)\n\t\t.whereIn('assessmentID', assessmentIDs)\n\t\t// ensure i only fetch my own business user\n\t\t.leftJoin('user', 'user.id', 'assessment_defaultAssignee_map.userID')\n\t\t.leftJoin(\n\t\t\t'permissions',\n\t\t\t'permissions.id',\n\t\t\t'assessment_defaultAssignee_map.teamID'\n\t\t)\n\t\t.where((q) => {\n\t\t\tvoid q\n\t\t\t\t.where((qq) => {\n\t\t\t\t\tvoid qq\n\t\t\t\t\t\t.whereNotNull('assessment_defaultAssignee_map.userID')\n\t\t\t\t\t\t.where('user.businessID', businessID);\n\t\t\t\t})\n\t\t\t\t.orWhere((qq) => {\n\t\t\t\t\tvoid qq\n\t\t\t\t\t\t.whereNotNull('assessment_defaultAssignee_map.teamID')\n\t\t\t\t\t\t.where('permissions.businessID', businessID);\n\t\t\t\t});\n\t\t});\n\n\tconst assignees = await getUserTeamObjectFromIDs(rows);\n\n\treturn assignees;\n}\n\nexport async function processAssessmentApproval(\n\tsubmittingAssessorUser: User,\n\tformData: FormData\n): Promise<void> {\n\tconst formID = formData.hash;\n\tconst isSuperadmin = submittingAssessorUser.isSuperadmin;\n\n\tconst {\n\t\tassessmentID,\n\t\tplant: { id },\n\t} = formData.formVars;\n\n\tconst schemaIDs = submittingAssessorUser.schemaAssessmentsWhereReviewer\n\t\t.filter((entry) => entry.assessmentID === assessmentID)\n\t\t.map((entry) => entry.schemaID);\n\tawait sql\n\t\t.knex(sql.Table.PlantAssessmentSchemaStatusMap)\n\t\t.where('assessmentID', assessmentID)\n\t\t.where('plantID', id)\n\t\t.whereIn('schemaID', schemaIDs)\n\t\t.update({\n\t\t\tassessmentApprovalReviewFormID: formID,\n\t\t});\n\n\tnew Event.AssessmentSubmittedPassed({\n\t\tplantID: id,\n\t\tassessmentID,\n\t\tcreatorID: submittingAssessorUser.id,\n\t\tisSuperadmin,\n\t\tformID,\n\t});\n}\nexport async function processMaintenanceAssessment(\n\tsubmittingAssessorUser: User,\n\tformData: FormData\n): Promise<string> {\n\tconst formID = formData.hash;\n\tconst isSuperadmin = submittingAssessorUser.isSuperadmin;\n\n\t// form is an assessment\n\tconst {\n\t\tplantAssessmentID,\n\t\tassessmentID,\n\t\tschemaIDs,\n\t\tplant: { id },\n\t\tnonConformances, // object containing non-applicable and applicable NCs in separate arrays\n\t\tdatePerformed,\n\t} = formData.formVars;\n\tconst operatingConditions = formData.formVars.operatingConditions;\n\t// make sure that when submitting an assessment we removed old legacy data\n\tdelete (formData.formVars as any)?.operatingRestrictions;\n\tawait redis.delete(redis.CacheDataType.Plant, id);\n\tconst assessmentRow: Assessment = await sql.getRow<any>(\n\t\tsql.Table.Assessment,\n\t\t{\n\t\t\tid: assessmentID,\n\t\t}\n\t);\n\tconst plantAssessmentRow: PlantAssessment = await sql.getRow<any>(\n\t\tsql.Table.PlantAssessment,\n\t\t{\n\t\t\tid: plantAssessmentID,\n\t\t}\n\t);\n\n\tconst usageLogElement = Object.keys(formData.data)\n\t\t.map((elementKey) => formData.data[elementKey])\n\t\t.find((e) => e.metadata.isUsageLog);\n\tlet usageParams: MaintenanceUsageParams;\n\tif (usageLogElement) {\n\t\t// also mark them in the conversation\n\t\tusageParams = {\n\t\t\tfuelUsed: usageLogElement.values.fuel\n\t\t\t\t? parseFloat(usageLogElement.values.fuel)\n\t\t\t\t: null,\n\t\t\thoursLogged: usageLogElement.values.hours\n\t\t\t\t? parseFloat(usageLogElement.values.hours)\n\t\t\t\t: null,\n\t\t\todometerReading: usageLogElement.values.odometer\n\t\t\t\t? parseFloat(usageLogElement.values.odometer)\n\t\t\t\t: null,\n\t\t};\n\t}\n\n\tconst maintenanceDetailsElement = Object.keys(formData.data)\n\t\t.map((elementKey) => formData.data[elementKey])\n\t\t.find((e) => e.metadata.isMaintenanceElement);\n\n\tif (maintenanceDetailsElement) {\n\t\t// we need to update JOB CARD NUMBER AND WORK ORDER NUMBER\n\t\t// leave out for now, they will probablt get reworked anyway\n\t}\n\n\t// iterate over fields and process checklist item and hazard fields\n\tawait processSubmittedFormData(formData, submittingAssessorUser);\n\n\t// assessment fails if at least once applicable non-conformance\n\tconst assessmentFailed = !!nonConformances?.applicable.length;\n\tconst isAwaitingreview =\n\t\tassessmentRow?.requiresReview && plantAssessmentRow?.businessID\n\t\t\t? true\n\t\t\t: false;\n\n\tconst updateObj = {\n\t\treviewFormID: !assessmentFailed ? formID : null,\n\t\t// for schema assessments this flag is on the plant_assessment_schema_map table\n\t\tawaitingReview: isAwaitingreview,\n\t\tlastFailedFormID: assessmentFailed ? formID : null,\n\t\tsavedFormID: null,\n\t\tdateLastSubmitted: core.moment().unix(),\n\t\tdatePerformed,\n\t};\n\n\tawait sql.updateRows(\n\t\tsql.Table.PlantAssessment,\n\t\t{ id: plantAssessmentID },\n\t\tupdateObj\n\t);\n\n\tif (usageParams) {\n\t\t// make sure this happens after we rescheduled the maintenance so we don't accidentally tag out the plant becasue the code fired when teh maintenance was still the old one\n\t\tawait editMaintenanceUsageParams(\n\t\t\tplantAssessmentID,\n\t\t\tusageParams,\n\t\t\tsubmittingAssessorUser,\n\t\t\tid\n\t\t);\n\t}\n\n\tconst isRejectedAssessmentResubmission = await sql.getRow<any>(\n\t\tsql.Table.Conversation,\n\t\t{\n\t\t\tstatus: 'open',\n\t\t\ttype: 'case',\n\t\t\tplantID: id,\n\t\t\tassessmentID,\n\t\t\treason: CaseReason.AssessmentReviewedRejected,\n\t\t}\n\t);\n\n\t// mark it in the table\n\tawait sql.updateRows(\n\t\tsql.Table.PlantAssessment,\n\t\t{\n\t\t\tplantID: id,\n\t\t\tassessmentID,\n\t\t},\n\t\t{\n\t\t\tisRejectedAssessmentResubmission: isRejectedAssessmentResubmission\n\t\t\t\t? true\n\t\t\t\t: false,\n\t\t}\n\t);\n\tconst isMaintenance = !!plantAssessmentRow.businessID;\n\n\tif (isMaintenance) {\n\t\t// business assessment, close existing cases and untag out the plant\n\t\tawait markAssessmentInCase(\n\t\t\tsubmittingAssessorUser,\n\t\t\tid,\n\t\t\tassessmentID,\n\t\t\tnull,\n\t\t\tsubmittingAssessorUser.id,\n\t\t\tformID,\n\t\t\tCaseReason.AssessmentFailed,\n\t\t\tassessmentFailed,\n\t\t\toperatingConditions,\n\t\t\tnonConformances,\n\t\t\tnull,\n\t\t\tnull\n\t\t);\n\n\t\t// close the case that was opened when the plant got marked for tagged out because exceeding a maintenance deadline\n\t\tconst tagOutCase = await sql.getRow<CaseRow>(sql.Table.Conversation, {\n\t\t\tstatus: CaseStatus.Open,\n\t\t\ttype: ConversationType.Case,\n\t\t\treason: CaseReason.TaggedOut,\n\t\t\tplantID: plantAssessmentRow.plantID,\n\t\t\tassessmentID: assessmentID || null,\n\t\t\tschemaID: null,\n\t\t});\n\t\tif (tagOutCase) {\n\t\t\tawait closeCase(tagOutCase.id, submittingAssessorUser);\n\t\t}\n\t}\n\n\tfor (const schemaID of schemaIDs) {\n\t\t// creates a task for the client to review\n\t\tconst createsTaskForReview = await sql\n\t\t\t.knex(sql.Table.SchemaAssessmentMap)\n\t\t\t.where('schemaID', schemaID)\n\t\t\t.where('assessmentID', assessmentID)\n\t\t\t.where('createTaskForReview', true)\n\t\t\t.first();\n\n\t\tif (createsTaskForReview) {\n\t\t\tconst clientBusinessID = (\n\t\t\t\tawait sql\n\t\t\t\t\t.knex(sql.Table.SchemaClientGroupMap)\n\t\t\t\t\t.join(\n\t\t\t\t\t\t'business_clientGroup_map',\n\t\t\t\t\t\t'business_clientGroup_map.clientGroupID',\n\t\t\t\t\t\t'schema_clientGroup_map.clientGroupID'\n\t\t\t\t\t)\n\t\t\t\t\t.where('schemaID', schemaID)\n\t\t\t\t\t.where('isClient', true)\n\t\t\t\t\t.where('business_clientGroup_map.active', true)\n\t\t\t\t\t.where('schema_clientGroup_map.active', true)\n\t\t\t\t\t.select('businessID')\n\t\t\t\t\t.first()\n\t\t\t)?.businessID;\n\t\t\t// create a task for the client to review\n\t\t\tawait createTask(\n\t\t\t\tplantAssessmentRow.plantID,\n\t\t\t\tTaskReason.AssessmentSubmitted,\n\t\t\t\t`${plantAssessmentRow.title} submission for review`,\n\t\t\t\t'0',\n\t\t\t\t[],\n\t\t\t\tformID,\n\t\t\t\tnull,\n\t\t\t\tnull,\n\t\t\t\tschemaID,\n\t\t\t\tclientBusinessID,\n\t\t\t\tnull,\n\t\t\t\tnull,\n\t\t\t\tnull,\n\t\t\t\t[],\n\t\t\t\tnull,\n\t\t\t\tnull,\n\t\t\t\tnull,\n\t\t\t\tplantAssessmentRow.assessmentID\n\t\t\t);\n\t\t}\n\t\tconst schemaHasFailedAssessmentCase = await sql.getRow<any>(\n\t\t\tsql.Table.Conversation,\n\t\t\t{\n\t\t\t\tstatus: 'open',\n\t\t\t\ttype: 'case',\n\t\t\t\tplantID: plantAssessmentRow.plantID,\n\t\t\t\tschemaID,\n\t\t\t\treason: CaseReason.AssessmentFailed,\n\t\t\t}\n\t\t);\n\n\t\tawait markAssessmentInCase(\n\t\t\tsubmittingAssessorUser,\n\t\t\tid,\n\t\t\tassessmentID,\n\t\t\tschemaID,\n\t\t\tsubmittingAssessorUser.id,\n\t\t\tformID,\n\t\t\tCaseReason.AssessmentFailed,\n\t\t\tassessmentFailed,\n\t\t\toperatingConditions,\n\t\t\tnonConformances,\n\t\t\tnull,\n\t\t\tnull\n\t\t);\n\n\t\tif (schemaHasFailedAssessmentCase) {\n\t\t\tconst assessmentIsSharedBetweenClients =\n\t\t\t\tawait isAssessmentSharedBetweenClients(assessmentID, id);\n\t\t\t// we ensure requiresApprovalForSchema is turned on for the schema who rejected the assessment only if it's a shared assessment between clients\n\t\t\tawait sql.updateRows(\n\t\t\t\tsql.Table.PlantAssessmentSchemaStatusMap,\n\t\t\t\t{ assessmentID, plantID: id, schemaID },\n\t\t\t\t{\n\t\t\t\t\tfailed: assessmentFailed,\n\t\t\t\t\tawaitingReview: !assessmentFailed,\n\t\t\t\t\t// the approval review is reset on new submit\n\t\t\t\t\tassessmentApprovalReviewFormID: null,\n\t\t\t\t\trequiresApprovalForSchema:\n\t\t\t\t\t\tplantAssessmentRow.requiresReview &&\n\t\t\t\t\t\tassessmentIsSharedBetweenClients\n\t\t\t\t\t\t\t? true\n\t\t\t\t\t\t\t: false,\n\t\t\t\t\trejected: false,\n\t\t\t\t}\n\t\t\t);\n\t\t} else {\n\t\t\t// for the schema who didnt reject the assessment we leave requiresApprovalForSchema as it already is\n\t\t\tawait sql.updateRows(\n\t\t\t\tsql.Table.PlantAssessmentSchemaStatusMap,\n\t\t\t\t{ assessmentID, plantID: id, schemaID },\n\t\t\t\t{\n\t\t\t\t\tfailed: assessmentFailed,\n\t\t\t\t\t// the approval review is reset on new submit\n\t\t\t\t\tassessmentApprovalReviewFormID: null,\n\t\t\t\t\tawaitingReview: !assessmentFailed,\n\t\t\t\t\trejected: false,\n\t\t\t\t}\n\t\t\t);\n\t\t}\n\t}\n\n\tif (!assessmentFailed) {\n\t\t// only update the value of isInitialAssessment if it's not failed\n\t\tawait sql.updateRows(\n\t\t\tsql.Table.PlantAssessment,\n\t\t\t{ assessmentID, plantID: id },\n\t\t\t{\n\t\t\t\tisInitialAssessment: false,\n\t\t\t}\n\t\t);\n\t}\n\n\t// note: for now this functionality is disabled for internal maintenances\n\tif (assessmentID && !isMaintenance) {\n\t\tawait assignDefaultUsersForAssessmentReview(\n\t\t\tid,\n\t\t\tformID,\n\t\t\tassessmentID,\n\t\t\tassessmentFailed\n\t\t);\n\t}\n\n\t// maintenances are rescheduled via client side api call at the moment\n\tconst isRecurring = assessmentRow?.isRecurring;\n\n\tconst requiresReview = assessmentRow.requiresReview;\n\n\tif (assessmentFailed) {\n\t\tnew Event.AssessmentSubmittedFailed({\n\t\t\tplantID: id,\n\t\t\tassessmentID,\n\t\t\tcreatorID: submittingAssessorUser.id,\n\t\t\tisSuperadmin,\n\t\t\tformID,\n\t\t});\n\t} else {\n\t\tif (isMaintenance) {\n\t\t\tnew Event.MaintenanceSubmitted({\n\t\t\t\tplantID: id,\n\t\t\t\tcreatorID: submittingAssessorUser.id,\n\t\t\t\tisSuperadmin,\n\t\t\t\tformID,\n\t\t\t\tfuel: usageLogElement?.values?.fuel || null,\n\t\t\t\thours:\n\t\t\t\t\tusageLogElement && !!usageLogElement.values.hours\n\t\t\t\t\t\t? usageLogElement.values.hours\n\t\t\t\t\t\t: null,\n\t\t\t\todometer: usageLogElement?.values?.odometer || null,\n\t\t\t\tdatePerformed,\n\t\t\t});\n\t\t} else {\n\t\t\tnew Event.AssessmentSubmittedPassed({\n\t\t\t\tplantID: id,\n\t\t\t\tassessmentID,\n\t\t\t\tcreatorID: submittingAssessorUser.id,\n\t\t\t\tisSuperadmin,\n\t\t\t\tformID,\n\t\t\t});\n\t\t}\n\n\t\tif (!requiresReview) {\n\t\t\tif (!isMaintenance) {\n\t\t\t\tif (isRecurring) {\n\t\t\t\t\t// auto approve, so push forward the expiry date.\n\t\t\t\t\tawait changeAssessmentDate(\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tassessmentID,\n\t\t\t\t\t\tsubmittingAssessorUser.id,\n\t\t\t\t\t\tnull,\n\t\t\t\t\t\ttrue\n\t\t\t\t\t);\n\t\t\t\t} else if (!isRecurring) {\n\t\t\t\t\t// set dateDue as null, the assessment has been completed and it's non recurring\n\t\t\t\t\tawait sql.updateRows(\n\t\t\t\t\t\tsql.Table.PlantAssessmentStepMap,\n\t\t\t\t\t\t{ plantAssessmentID },\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tdateDue: null,\n\t\t\t\t\t\t}\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst replacementObj = {\n\t\t\t\treviewFormID: null,\n\t\t\t\tlastPassedFormID: formID,\n\t\t\t\tsavedFormID: null,\n\t\t\t\tlastFailedFormID: null,\n\t\t\t\tnextNotificationScheduled: null,\n\t\t\t};\n\n\t\t\tawait sql.updateRows(\n\t\t\t\tsql.Table.PlantAssessment,\n\t\t\t\t{ assessmentID, plantID: id },\n\t\t\t\treplacementObj\n\t\t\t);\n\n\t\t\tfor (const schemaID of schemaIDs) {\n\t\t\t\tawait sql.updateRows(\n\t\t\t\t\tsql.Table.PlantAssessmentSchemaStatusMap,\n\t\t\t\t\t{ assessmentID, plantID: id, schemaID },\n\t\t\t\t\t{\n\t\t\t\t\t\tawaitingReview: false,\n\t\t\t\t\t\tfailed: false,\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (!isMaintenance) {\n\t\t\t\tnew Event.AssessmentAutoApproval({\n\t\t\t\t\tplantID: id,\n\t\t\t\t\tassessmentID,\n\t\t\t\t\tcreatorID: '0',\n\t\t\t\t\tisSuperadmin: submittingAssessorUser.isSuperadmin,\n\t\t\t\t\tformID,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\treturn formID;\n}\n\nasync function assignDefaultUsersForAssessmentReview(\n\tplantID: Plant['id'],\n\tformID: string,\n\tassessmentID: PlantAssessment['assessmentID'],\n\tassessmentFailed: boolean\n) {\n\t// NOTE: Only get the explicit default assignees, don't pull in the reviewers!\n\tconst schemaIDs = (\n\t\tawait sql.getRows(sql.Table.SchemaAssessmentMap, {\n\t\t\tassessmentID,\n\t\t\tactive: true,\n\t\t})\n\t).map((row) => row.schemaID);\n\n\t// we will firstly get all default assignes for the given parameters.\n\tconst userBusinessIDs = await getDefaultUsersQuery(\n\t\tsql.Table.AssessmentDefaultAssigneeMap,\n\t\t[\n\t\t\t{\n\t\t\t\tjunction: 'AND',\n\t\t\t\tparam: 'assessment_defaultAssignee_map.assessmentID',\n\t\t\t\top: '=',\n\t\t\t\tvalue: assessmentID,\n\t\t\t},\n\t\t\t{\n\t\t\t\tjunction: 'AND',\n\t\t\t\tparam: 'assessment_defaultAssignee_map.schemaID',\n\t\t\t\top: 'in',\n\t\t\t\tvalue: schemaIDs,\n\t\t\t},\n\t\t]\n\t);\n\n\t// we will then MASK this selection against the allowed businesses.\n\t// default assignees for an assessment must fit the following scope:\n\t// clients which are CLIENTS of clientgroups => schema => assessments\n\t// assessor business assigned to that assessment\n\t// supplier that owns the plant.\n\n\tconst allowedBusinessIDs: Array<Business['id']> = [];\n\n\tconst assessmentRow = await getAssessmentRowForPlant(\n\t\tplantID,\n\t\tschemaIDs,\n\t\tassessmentID,\n\t\ttrue,\n\t\tnull\n\t);\n\t// assessor business ID (if there is one)\n\tif (assessmentRow.assessorID) {\n\t\tallowedBusinessIDs.push(assessmentRow.assessorID);\n\t}\n\n\t// plant owner businessID\n\tconst ownerBusinessID = await getOwnerBusinessIDOfPlant(plantID);\n\tallowedBusinessIDs.push(ownerBusinessID);\n\n\t// get clients of the clientgroups which are linked to schema, that link to this assessment.\n\tconst clientGroupIDs = await getClientGroupIDsForPlantAssessment(\n\t\tplantID,\n\t\tassessmentID\n\t);\n\tconst clientBusinessIDs = await getClientBusinessIDsForClientGroupIDs(\n\t\tclientGroupIDs\n\t);\n\tallowedBusinessIDs.push(...clientBusinessIDs);\n\n\tconst userIDs: Array<User['id']> = [...userBusinessIDs]\n\t\t.filter((userBusinessIDCombo) =>\n\t\t\tallowedBusinessIDs.includes(userBusinessIDCombo.businessID)\n\t\t)\n\t\t.map((userBusinessIDCombo) => userBusinessIDCombo.userID);\n\n\tconst uniqueUserIDs = Array.from(new Set(userIDs));\n\n\tawait Promise.all(\n\t\tuniqueUserIDs.map((userID) =>\n\t\t\tassignFormToUser(\n\t\t\t\tformID,\n\t\t\t\tFormType.Assessment,\n\t\t\t\tuserID,\n\t\t\t\tfalse,\n\t\t\t\tnull,\n\t\t\t\tassessmentFailed\n\t\t\t)\n\t\t)\n\t);\n}\n\nexport async function markAssessmentInCase(\n\toriginatingUser: User,\n\tplantID: Plant['id'],\n\tassessmentID: PlantAssessment['assessmentID'],\n\tschemaID: PlantSchema['schemaID'],\n\tassessorUserID: User['id'],\n\tformID: string,\n\tcaseType: CaseReason,\n\tfailed: boolean,\n\toperatingConditions: OperatingCondition[],\n\tnonConformances: NonConformances,\n\treason: string,\n\tinitialComment: string,\n\tisMaintenance?: boolean\n) {\n\t// if an assessment has failed, append to an existing assessment case or create a new case entirely\n\t// if an assessment has passed, close any existing assessment case for the plant\n\n\tlet caseObj: CaseRow = await sql.getRowUnsafe(sql.Table.Conversation, {\n\t\tstatus: 'open',\n\t\ttype: 'case',\n\t\tplantID,\n\t\tassessmentID: assessmentID || null,\n\t\tschemaID: schemaID || null,\n\t});\n\n\tif (caseObj && caseObj.reason !== caseType) {\n\t\t// case exists, but is wrong type; close it before continuing (and potentially creating a new case)\n\t\tawait closeCase(caseObj.id, originatingUser);\n\t\tcaseObj = null; // this case was not relevant to our assessment this.result\n\t}\n\tlet caseID = caseObj && caseObj.id;\n\tif (failed && !caseObj) {\n\t\t// create a new case\n\t\tconst subscriberIDs: Array<User['id']> = Array.from(\n\t\t\tnew Set([originatingUser.id, assessorUserID])\n\t\t); // no duplicates\n\t\tcaseID = await createCase(\n\t\t\tcaseType,\n\t\t\toriginatingUser.id,\n\t\t\tplantID,\n\t\t\tsubscriberIDs,\n\t\t\tassessmentID,\n\t\t\tschemaID,\n\t\t\tnull,\n\t\t\tnull,\n\t\t\treason,\n\t\t\t// don't pass initial comment here as we want the comment for assessment review rejected to be added BEFORE in order for the correct case specific banners to show\n\t\t\t// conversationService see isLastSubmittedAssessment\n\t\t\tnull,\n\t\t\tnull,\n\t\t\tnull,\n\t\t\tisMaintenance\n\t\t);\n\t}\n\tif (\n\t\tfailed ||\n\t\t(caseObj && caseObj.reason === CaseReason.AssessmentReviewedRejected)\n\t) {\n\t\t// this is going be a fail, or a pass with a rejected caseObject\n\t\t// add an assessment-specific comment\n\n\t\t// if the assessment was connected to two schemas, here we will end up with two identical comments being added to the case, check before adding\n\t\tconst existingComment = await sql.getRow<any>(\n\t\t\tsql.Table.ConversationComment,\n\t\t\t{\n\t\t\t\tconversationID: caseID,\n\t\t\t\tcreatorID: false,\n\t\t\t}\n\t\t);\n\n\t\tif (!existingComment) {\n\t\t\tawait comment(caseID, '0', {\n\t\t\t\tassessmentID,\n\t\t\t\tformID,\n\t\t\t\toperatingConditions,\n\t\t\t\tnonConformances,\n\t\t\t\tfiles: [],\n\t\t\t\ttext: '',\n\t\t\t});\n\t\t} else {\n\t\t\t// update the comment\n\t\t\t// set new formID in the comment\n\t\t\t// this will be fire when we are submitting a new failed assessment from a case of a failed assessment\n\n\t\t\tconst data = JSON.parse(existingComment.data);\n\t\t\tdata.formID = formID;\n\n\t\t\tawait sql.updateRows(\n\t\t\t\tsql.Table.ConversationComment,\n\t\t\t\t{ id: existingComment.id },\n\t\t\t\t{\n\t\t\t\t\tdata: JSON.stringify(data),\n\t\t\t\t\tdateLastModified: core.moment().unix(),\n\t\t\t\t}\n\t\t\t);\n\t\t}\n\t} else if (caseObj) {\n\t\t// this is going be a pass.\n\t\t// the assessment has passed, and there is a failed-assessment case still open -- close it!\n\t\tawait closeCase(caseID, originatingUser);\n\t}\n\n\tif (initialComment) {\n\t\t// create the comment after the system has commented\n\t\tconst commentObj = {\n\t\t\tcreatorID: originatingUser.id,\n\t\t\tdata: {\n\t\t\t\ttext: initialComment,\n\t\t\t\tfiles: [],\n\t\t\t},\n\t\t};\n\t\tawait comment(caseID, originatingUser.id, commentObj.data);\n\t}\n\n\treturn caseID;\n}\n\nexport async function runMaintenanceAssessmentNotificationChecks() {\n\t// this check sends out notifications regarding upcoming assessments\n\tconst now = core.moment().unix();\n\tconst oneWeek = 604800;\n\tconst threeWeeks = oneWeek * 3;\n\tconst twoWeeks = oneWeek * 2;\n\tconst oneMonth = 2628000;\n\tconst twoMonths = oneMonth * 2;\n\n\tconst baseQuery = sql\n\t\t.knex(sql.Table.PlantAssessment)\n\t\t.leftJoin(\n\t\t\t'plant_assessment_step_map',\n\t\t\t'plant_assessment_step_map.plantAssessmentID',\n\t\t\t'plant_assessment_map.id'\n\t\t)\n\t\t.join(sql.Table.Plant, 'plant.id', sql.Table.PlantAssessment + '.plantID')\n\t\t.join(\n\t\t\tsql.Table.Assessment,\n\t\t\t'assessment.id',\n\t\t\tsql.Table.PlantAssessment + '.assessmentID'\n\t\t)\n\t\t// requires left join because internal maintenances won't have a schema attached\n\t\t.leftJoin(\n\t\t\tsql.Table.PlantAssessmentSchemaStatusMap,\n\t\t\t'plant_assessment_schema_status_map.assessmentID',\n\t\t\t'plant_assessment_map.assessmentID'\n\t\t)\n\t\t.join(sql.Table.Business, 'business.id', 'plant.businessID')\n\t\t.leftJoin(\n\t\t\t'business_maintenance_configuration',\n\t\t\t'business_maintenance_configuration.businessID',\n\t\t\t'business.id'\n\t\t)\n\t\t.join(\n\t\t\tsql.Table.PlantSchemaMap,\n\t\t\t'plant.id',\n\t\t\tsql.Table.PlantSchemaMap + '.plantID'\n\t\t)\n\t\t.join(\n\t\t\tsql.Table.SchemaClientGroupMap,\n\t\t\t'plant_schema_map.schemaID',\n\t\t\tsql.Table.SchemaClientGroupMap + '.schemaID'\n\t\t)\n\t\t.leftJoin(\n\t\t\t'schema_assessment_map',\n\t\t\t'schema_assessment_map.assessmentID',\n\t\t\tsql.Table.PlantAssessment + '.assessmentID'\n\t\t)\n\t\t.where('plant.active', true)\n\t\t.where('assessment.active', true)\n\t\t.where('business.active', true)\n\t\t.where('plant_schema_map.active', true)\n\t\t.where('plant_schema_map.awaitingApplicationReview', false)\n\t\t.where('schema_assessment_map.active', true)\n\t\t.where('plant_assessment_map.active', true)\n\t\t.where('plant_assessment_step_map.completed', false)\n\t\t.where((qq) => {\n\t\t\t// internal maintenances don't have a schema\n\t\t\tvoid qq\n\t\t\t\t.whereNull('plant_assessment_schema_status_map.plantID')\n\t\t\t\t.orWhere('plant_assessment_schema_status_map.awaitingReview', false);\n\t\t})\n\t\t.where((qq) => {\n\t\t\t// internal maintenances don't have a schema\n\t\t\tvoid qq\n\t\t\t\t.whereNull('schema_assessment_map.schemaID')\n\t\t\t\t.orWhere(\n\t\t\t\t\t'plant_schema_map.schemaID',\n\t\t\t\t\t'=',\n\t\t\t\t\tsql.knex.ref('schema_assessment_map.schemaID')\n\t\t\t\t);\n\t\t})\n\t\t.distinctOn('plant_assessment_map.id', 'maintenanceSequenceStepID');\n\n\tvoid applyPlantAssessmentSelectorsToQuery(baseQuery);\n\n\tvoid baseQuery.select(\n\t\t'plant_assessment_step_map.plantAssessmentID',\n\t\t'plant_assessment_step_map.maintenanceSequenceStepID',\n\t\t'plant_assessment_step_map.hoursDue',\n\t\t'plant_assessment_step_map.fuelDue',\n\t\t'plant_assessment_step_map.odometerDue',\n\t\t'plant_assessment_step_map.dateDue'\n\t);\n\n\t// only select assessments that are worth checking.\n\t// plant must be active.\n\t// assessments must be joined to an active plant_schema_map row, connected via the schema_assessment_map.\n\t// an assessment is active only if those criteria are met.\n\n\t// assessment expiry notification should be cascading checks to ensure no accidental double tapping occurs.\n\t// 4 weeks before expiry up until expiry\n\n\tconst overdueBeforeRows = await baseQuery\n\t\t.clone()\n\t\t.where('nextNotificationScheduled', '<', now)\n\t\t.where((q) => {\n\t\t\t// notify the user when the assessment is coming up soon based on date, odometer or hours\n\t\t\tvoid q\n\t\t\t\t.where((qq) => {\n\t\t\t\t\tvoid qq\n\t\t\t\t\t\t.whereNotNull('plant_assessment_step_map.dateDue')\n\t\t\t\t\t\t.whereBetween('plant_assessment_step_map.dateDue', [\n\t\t\t\t\t\t\tnow,\n\t\t\t\t\t\t\tnow + oneWeek * 4,\n\t\t\t\t\t\t]);\n\t\t\t\t})\n\t\t\t\t// check for odometer due coming soon\n\t\t\t\t.orWhere((qqq) => {\n\t\t\t\t\t// user has configured odometer deadlines, check those\n\t\t\t\t\tvoid qqq\n\t\t\t\t\t\t.whereNotNull('plant_assessment_step_map.odometerDue')\n\t\t\t\t\t\t.whereNotNull(\n\t\t\t\t\t\t\t'business_maintenance_configuration.dueSoonOdometerConfiguration'\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.whereRaw(\n\t\t\t\t\t\t\t'\"plant_assessment_step_map\".\"odometerDue\" < \"plant\".\"lastOdometerReading\" + \"business_maintenance_configuration\".\"dueSoonOdometerConfiguration\"'\n\t\t\t\t\t\t);\n\t\t\t\t})\n\t\t\t\t// check for hours due coming soon\n\t\t\t\t.orWhere((qqq) => {\n\t\t\t\t\t// user has configured hours deadlines,check those\n\t\t\t\t\tvoid qqq\n\t\t\t\t\t\t.whereNotNull('plant_assessment_step_map.hoursDue')\n\t\t\t\t\t\t.whereNotNull(\n\t\t\t\t\t\t\t'business_maintenance_configuration.dueSoonHoursConfiguration'\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.whereRaw(\n\t\t\t\t\t\t\t'\"plant_assessment_step_map\".\"hoursDue\" < \"plant\".\"totalHoursLogged\" + \"business_maintenance_configuration\".\"dueSoonHoursConfiguration\"'\n\t\t\t\t\t\t);\n\t\t\t\t});\n\t\t});\n\n\t// split\n\tfor (const assRow of overdueBeforeRows) {\n\t\t// generate event of type assessment-overdue-before\n\n\t\t// increment the nextNotificationScheduled one week forward relative to the dateDue\n\t\t// ensure that it is rounded properly\n\t\tlet newDate = now;\n\n\t\tif (assRow.nextNotificationScheduled < assRow.dateDue - twoMonths) {\n\t\t\tnewDate = assRow.dateDue - oneMonth;\n\t\t} else if (assRow.nextNotificationScheduled < assRow.dateDue - oneMonth) {\n\t\t\tnewDate = assRow.dateDue - threeWeeks;\n\t\t} else if (assRow.nextNotificationScheduled < assRow.dateDue - threeWeeks) {\n\t\t\tnewDate = assRow.dateDue - twoWeeks;\n\t\t} else if (assRow.nextNotificationScheduled < assRow.dateDue - twoWeeks) {\n\t\t\tnewDate = assRow.dateDue - oneWeek;\n\t\t} else if (assRow.nextNotificationScheduled < assRow.dateDue - oneWeek) {\n\t\t\tnewDate = assRow.dateDue;\n\t\t} else {\n\t\t\t// do nothin\n\t\t}\n\n\t\twhile (newDate <= now) {\n\t\t\tnewDate = newDate + oneWeek;\n\t\t}\n\n\t\tconsole.info(\n\t\t\t// eslint-disable-next-line @typescript-eslint/restrict-plus-operands\n\t\t\t'assessment ' +\n\t\t\t\tassRow.assessmentID.toString() +\n\t\t\t\t' being sent a warning in advance... incremented to date ' +\n\t\t\t\tnewDate.toString()\n\t\t);\n\n\t\tawait sql.updateRows(\n\t\t\tsql.Table.PlantAssessment,\n\t\t\t{ assessmentID: assRow.assessmentID, plantID: assRow.plantID },\n\t\t\t{ nextNotificationScheduled: newDate, lastNotificationSent: now }\n\t\t);\n\n\t\tif (assRow.businessID) {\n\t\t\tconst { kmDeadlines, hrDeadlines, daysDeadlines } =\n\t\t\t\tawait getDueSoonMaintenanceDeadlines(assRow.plantID);\n\t\t\tawait checkDeadline(\n\t\t\t\tkmDeadlines,\n\t\t\t\tassRow,\n\t\t\t\t'distance',\n\t\t\t\tassRow.lastOdometerReading,\n\t\t\t\tassRow.odometerDue\n\t\t\t);\n\t\t\tawait checkDeadline(\n\t\t\t\thrDeadlines,\n\t\t\t\tassRow,\n\t\t\t\t'hours',\n\t\t\t\tassRow.totalHoursLogged,\n\t\t\t\tassRow.hoursDue\n\t\t\t);\n\t\t\tawait checkDeadline(daysDeadlines, assRow, 'date', now, assRow.dateDue);\n\t\t} else {\n\t\t\tnew Event.AssessmentOverdueBefore({\n\t\t\t\tassessmentID: assRow.assessmentID,\n\t\t\t\tplantID: assRow.plantID,\n\t\t\t\tisSuperadmin: false,\n\t\t\t});\n\t\t}\n\t}\n\n\t// first time overdue notifications begin\n\tconst firstTimeOverdueRows: PlantAssessment[] = await baseQuery\n\t\t.clone()\n\n\t\t.where((q) => {\n\t\t\tvoid q\n\t\t\t\t.where((qq) => {\n\t\t\t\t\t// check for overdue odometer\n\t\t\t\t\tvoid qq\n\t\t\t\t\t\t.where('plant_assessment_step_map.overdue', false)\n\t\t\t\t\t\t.whereNotNull('plant_assessment_step_map.odometerDue')\n\t\t\t\t\t\t.where(\n\t\t\t\t\t\t\t'plant_assessment_step_map.odometerDue',\n\t\t\t\t\t\t\t'<',\n\t\t\t\t\t\t\tsql.knex.ref('plant.lastOdometerReading')\n\t\t\t\t\t\t);\n\t\t\t\t})\n\t\t\t\t.orWhere((qq) => {\n\t\t\t\t\t// check for overdue hours\n\t\t\t\t\tvoid qq\n\t\t\t\t\t\t.where('plant_assessment_step_map.overdue', false)\n\t\t\t\t\t\t.whereNotNull('plant_assessment_step_map.hoursDue')\n\t\t\t\t\t\t.where(\n\t\t\t\t\t\t\t'plant_assessment_step_map.hoursDue',\n\t\t\t\t\t\t\t'<',\n\t\t\t\t\t\t\tsql.knex.ref('plant.totalHoursLogged')\n\t\t\t\t\t\t);\n\t\t\t\t})\n\n\t\t\t\t.orWhere((qq) => {\n\t\t\t\t\tvoid qq\n\t\t\t\t\t\t.where(\n\t\t\t\t\t\t\t'nextNotificationScheduled',\n\t\t\t\t\t\t\t'=',\n\t\t\t\t\t\t\tsql.knex.ref('plant_assessment_step_map.dateDue')\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.whereNotNull('plant_assessment_step_map.dateDue')\n\t\t\t\t\t\t.where('plant_assessment_step_map.dateDue', '<=', now);\n\t\t\t\t});\n\t\t});\n\n\tfor (const assRow of firstTimeOverdueRows) {\n\t\tconsole.info(\n\t\t\t'assessment ' +\n\t\t\t\tassRow.assessmentID.toString() +\n\t\t\t\t' being sent a warning because it is overdue...'\n\t\t);\n\t\t// generate event of type assessment-overdue\n\t\t// increment the nextNotificationScheduled one week forward relative to the dateDue\n\t\t// ensure that it is rounded properly\n\t\tconst newDate = assRow.dateDue + oneWeek;\n\t\tawait sql.updateRows(\n\t\t\tsql.Table.PlantAssessment,\n\t\t\t{ assessmentID: assRow.assessmentID, plantID: assRow.plantID },\n\t\t\t{ nextNotificationScheduled: newDate, lastNotificationSent: now }\n\t\t);\n\n\t\tawait sql.updateRows(\n\t\t\tsql.Table.PlantAssessmentStepMap,\n\t\t\t{\n\t\t\t\tplantAssessmentID: (assRow as any).plantAssessmentID,\n\t\t\t\tmaintenanceSequenceStepID:\n\t\t\t\t\t(assRow as any).maintenanceSequenceStepID || null,\n\t\t\t},\n\t\t\t{\n\t\t\t\toverdue: true,\n\t\t\t}\n\t\t);\n\n\t\tif (assRow.businessID) {\n\t\t\tnew Event.MaintenanceOverdue({\n\t\t\t\tassessmentID: assRow.assessmentID,\n\t\t\t\tplantID: assRow.plantID,\n\t\t\t\tisSuperadmin: false,\n\t\t\t});\n\t\t} else {\n\t\t\tnew Event.AssessmentOverdue({\n\t\t\t\tassessmentID: assRow.assessmentID,\n\t\t\t\tplantID: assRow.plantID,\n\t\t\t\tisSuperadmin: false,\n\t\t\t});\n\t\t}\n\t\tawait redis.delete(redis.CacheDataType.Plant, assRow.plantID);\n\t}\n\t// first time overdue notifications end\n\n\t// after first time overdue notifications begin\n\tconst afterOverDueRows: PlantAssessment[] = await baseQuery\n\t\t.clone()\n\t\t.where('plant_assessment_step_map.overdue', true)\n\t\t.where('nextNotificationScheduled', '<=', now);\n\n\tfor (const assRow of afterOverDueRows) {\n\t\t// generate event of type assessment-overdue-after\n\t\tlet newDate = assRow.nextNotificationScheduled + oneWeek;\n\t\twhile (newDate <= now) {\n\t\t\tnewDate = newDate + oneWeek;\n\t\t}\n\t\tlet logStr =\n\t\t\t'assessment ' +\n\t\t\tassRow.assessmentID.toString() +\n\t\t\t' being sent a warning because it is already overdue...';\n\t\tlogStr += 'next schedule put forward one week to ' + newDate.toString();\n\t\tconsole.info(logStr);\n\t\tawait sql.updateRows(\n\t\t\tsql.Table.PlantAssessment,\n\t\t\t{ assessmentID: assRow.assessmentID, plantID: assRow.plantID },\n\t\t\t{ nextNotificationScheduled: newDate, lastNotificationSent: now }\n\t\t);\n\t\tif (assRow.businessID) {\n\t\t\tnew Event.MaintenanceOverdue({\n\t\t\t\tplantID: assRow.plantID,\n\t\t\t\tassessmentID: assRow.assessmentID,\n\t\t\t\tisSuperadmin: false,\n\t\t\t});\n\t\t} else {\n\t\t\tnew Event.AssessmentOverdueAfter({\n\t\t\t\tplantID: assRow.plantID,\n\t\t\t\tassessmentID: assRow.assessmentID,\n\t\t\t\tisSuperadmin: false,\n\t\t\t});\n\t\t}\n\t}\n}\n\nexport async function getPossibleClientGroupIDsForAssessmentUnsafe(\n\tassessmentID: number\n): Promise<number[]> {\n\tconst clientGroupIDs = [];\n\tconst rows = await sql\n\t\t.knex('schema_assessment_map')\n\t\t.leftJoin(\n\t\t\t'schema_clientGroup_map',\n\t\t\t'schema_assessment_map.schemaID',\n\t\t\t'schema_clientGroup_map.schemaID'\n\t\t)\n\t\t.where('schema_assessment_map.assessmentID', assessmentID);\n\trows.map((row) => clientGroupIDs.push(row.clientGroupID));\n\treturn clientGroupIDs;\n}\n\nexport function getAssessorsBaseQuery(\n\tclientGroups: ClientGroupMap,\n\tassessmentID: number\n) {\n\treturn sql\n\t\t.knex('assessor_schema_map')\n\t\t.join(sql.Table.User, 'assessor_schema_map.userID', 'user.id')\n\t\t.join(\n\t\t\t'schema_assessment_map',\n\t\t\t'schema_assessment_map.schemaID',\n\t\t\t'assessor_schema_map.schemaID'\n\t\t)\n\t\t.join(\n\t\t\t'schema_clientGroup_map',\n\t\t\t'schema_clientGroup_map.schemaID',\n\t\t\t'schema_assessment_map.schemaID'\n\t\t)\n\t\t.join(sql.Table.Business, 'business.id', 'user.businessID')\n\t\t.where('schema_assessment_map.assessmentID', assessmentID)\n\t\t.whereIn('schema_clientGroup_map.clientGroupID', Object.keys(clientGroups));\n}\n\nexport async function getAssessorsForSchemaAssessment(\n\tassessmentID: number,\n\tschemaID: number\n): Promise<CompactUser[]> {\n\tconst userIDs = (\n\t\tawait sql\n\t\t\t.knex(sql.Table.AssessorSchemaMap)\n\t\t\t.leftJoin('user', 'user.id', 'userID')\n\t\t\t.where('schemaID', schemaID)\n\t\t\t.where('assessmentID', assessmentID)\n\t\t\t.where('user.active', true)\n\t\t\t.select('userID')\n\t).map((row) => row?.userID);\n\n\tconst users = [];\n\tfor (const userID of userIDs) {\n\t\tconst user = await getCompactUser(userID);\n\t\tusers.push(user);\n\t}\n\treturn users;\n}\n\nexport async function getAssessmentsForBusiness(\n\tbusinessID: string\n): Promise<Assessment[]> {\n\tconst rows = await sql\n\t\t.knex(sql.Table.Business)\n\t\t.join(\n\t\t\t'business_clientGroup_map',\n\t\t\t'business_clientGroup_map.businessID',\n\t\t\t'business.id'\n\t\t)\n\t\t.join(\n\t\t\t'schema_clientGroup_map',\n\t\t\t'business_clientGroup_map.clientGroupID',\n\t\t\t'schema_clientGroup_map.clientGroupID'\n\t\t)\n\t\t.join(\n\t\t\t'schema_assessment_map',\n\t\t\t'schema_assessment_map.schemaID',\n\t\t\t'schema_clientGroup_map.schemaID'\n\t\t)\n\t\t.join('assessment', 'schema_assessment_map.assessmentID', 'assessment.id')\n\t\t.where('business.id', businessID)\n\t\t.groupBy('assessment.id')\n\t\t.select('assessment.*');\n\treturn rows;\n}\n\nexport async function getAssessmentsForAllSuppliers(\n\tcgIDs: number[]\n): Promise<Assessment[]> {\n\t// get all the assessments linked to my suppliers\n\t// used to have visibility on all assessments in history\n\tconst rows = await sql\n\t\t.knex(sql.Table.Business)\n\t\t.join(\n\t\t\t'business_clientGroup_map',\n\t\t\t'business_clientGroup_map.businessID',\n\t\t\t'business.id'\n\t\t)\n\t\t.join(\n\t\t\t'schema_clientGroup_map',\n\t\t\t'business_clientGroup_map.clientGroupID',\n\t\t\t'schema_clientGroup_map.clientGroupID'\n\t\t)\n\t\t.join(\n\t\t\t'schema_assessment_map',\n\t\t\t'schema_assessment_map.schemaID',\n\t\t\t'schema_clientGroup_map.schemaID'\n\t\t)\n\t\t.join('assessment', 'schema_assessment_map.assessmentID', 'assessment.id')\n\t\t.whereIn(\n\t\t\t'business.id',\n\t\t\t// get supplier IDs\n\t\t\tsql\n\t\t\t\t.knex('clientGroup')\n\t\t\t\t.leftJoin(\n\t\t\t\t\t'business_clientGroup_map',\n\t\t\t\t\t'business_clientGroup_map.clientGroupID',\n\t\t\t\t\t'clientGroup.id'\n\t\t\t\t)\n\t\t\t\t.whereIn('clientGroupID', cgIDs)\n\t\t\t\t.groupBy('business_clientGroup_map.businessID')\n\t\t\t\t.select('business_clientGroup_map.businessID')\n\t\t)\n\t\t.groupBy('assessment.id')\n\t\t.select('assessment.*');\n\treturn rows;\n}\n\nexport async function isAssessmentSharedAcrossSchemas(\n\tassessmentID: number\n): Promise<boolean> {\n\tif (\n\t\t!(await sql.rowExists<Assessment>(sql.Table.Assessment, {\n\t\t\tid: assessmentID,\n\t\t\trequiresReassessmentForAdditionalSchemas: true,\n\t\t}))\n\t) {\n\t\treturn false;\n\t}\n\tconst rows = await sql.getRows(sql.Table.SchemaAssessmentMap, {\n\t\tassessmentID,\n\t\tactive: true,\n\t});\n\treturn !!rows && rows.length > 1;\n}\n\nexport async function isSelfAssessmentAllowed(\n\tassessmentID: number,\n\tbusinessID: string\n) {\n\tconst row = await sql.getRowUnsafe(sql.Table.BusinessSelfAssessmentMap, {\n\t\tassessmentID,\n\t\tbusinessID,\n\t});\n\treturn !!row;\n}\n\nexport async function getAssessmentsForSchemaID(\n\tschemaID: number\n): Promise<Assessment[]> {\n\tconst assessments = await sql\n\t\t.knex('schema_assessment_map')\n\t\t.leftJoin(\n\t\t\t'assessment',\n\t\t\t'schema_assessment_map.assessmentID',\n\t\t\t'assessment.id'\n\t\t)\n\t\t.where('schema_assessment_map.schemaID', schemaID)\n\t\t.where('schema_assessment_map.active', true);\n\treturn assessments;\n}\n\nexport async function getApprovedReviewersForAssessment(\n\tassessmentID: number,\n\tpermissionToCheck: UserSchemaAssessmentPermissions,\n\tbusinessID: string,\n\tschemaIDs: number[]\n): Promise<UserTeamObject> {\n\tconst q = sql\n\t\t.knex(sql.Table.UserSchemaAssessmentPermissions)\n\t\t.where('assessmentID', assessmentID)\n\t\t.where(permissionToCheck, true)\n\t\t// ensure i only fetch my own business user\n\t\t.leftJoin('user', 'user.id', 'user_schema_assessment_permissions.userID')\n\t\t.leftJoin(\n\t\t\t'permissions',\n\t\t\t'permissions.id',\n\t\t\t'user_schema_assessment_permissions.teamID'\n\t\t)\n\t\t.where((q) => {\n\t\t\tvoid q\n\t\t\t\t.where((qq) => {\n\t\t\t\t\tvoid qq.whereNotNull('user_schema_assessment_permissions.userID');\n\t\t\t\t\tif (businessID) {\n\t\t\t\t\t\tvoid qq.where('user.businessID', businessID);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.orWhere((qq) => {\n\t\t\t\t\tvoid qq.whereNotNull('user_schema_assessment_permissions.teamID');\n\t\t\t\t\tif (businessID) {\n\t\t\t\t\t\tvoid qq.where('permissions.businessID', businessID);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n\n\tif (schemaIDs && schemaIDs.length) {\n\t\tvoid q.whereIn('schemaID', schemaIDs);\n\t}\n\tconst rows = await q;\n\n\tconst approvedReviewers = await getUserTeamObjectFromIDs(rows);\n\n\treturn approvedReviewers;\n}\n\nexport async function getApprovedReviewerUserIDsForAssessment(\n\tschemaIDs: number[],\n\tassessmentID: number,\n\tpermissionToCheck: UserSchemaAssessmentPermissions\n): Promise<UserAndBusinessID[]> {\n\t// get all the userIDs that are approved reviewers of this permission\n\tconst whereArgs: Array<{\n\t\tjunction: Junction;\n\t\tparam: string;\n\t\top: Operator;\n\t\tvalue;\n\t}> = [\n\t\t{\n\t\t\tjunction: 'AND',\n\t\t\tparam: permissionToCheck,\n\t\t\top: '=',\n\t\t\tvalue: true,\n\t\t},\n\t];\n\tif (assessmentID) {\n\t\twhereArgs.push({\n\t\t\tjunction: 'AND',\n\t\t\tparam: 'user_schema_assessment_permissions.assessmentID',\n\t\t\top: '=',\n\t\t\tvalue: assessmentID,\n\t\t});\n\t}\n\tif (schemaIDs) {\n\t\twhereArgs.push({\n\t\t\tjunction: 'AND',\n\t\t\tparam: 'user_schema_assessment_permissions.schemaID',\n\t\t\top: 'in',\n\t\t\tvalue: schemaIDs,\n\t\t});\n\t}\n\n\treturn getDefaultUsersQuery(\n\t\tsql.Table.UserSchemaAssessmentPermissions,\n\t\twhereArgs\n\t);\n}\n\nexport async function getUserTeamObjectFromPermissionForSchemaAssessment(\n\tschemaID: number,\n\tassessmentID: number,\n\tbusinessID: string,\n\tpermissionToCheck: UserSchemaAssessmentPermissions\n): Promise<UserTeamObject> {\n\tconst approvedReviewers = {\n\t\tusers: [],\n\t\tteams: [],\n\t};\n\tconst rows = await sql\n\t\t.knex(sql.Table.UserSchemaAssessmentPermissions)\n\t\t.where('schemaID', schemaID)\n\t\t.where('assessmentID', assessmentID)\n\t\t.where(permissionToCheck, true)\n\t\t// ensure i only fetch my own business user\n\t\t.leftJoin('user', 'user.id', 'user_schema_assessment_permissions.userID')\n\t\t.leftJoin(\n\t\t\t'permissions',\n\t\t\t'permissions.id',\n\t\t\t'user_schema_assessment_permissions.teamID'\n\t\t)\n\t\t.where((q) => {\n\t\t\tvoid q\n\t\t\t\t.where((qq) => {\n\t\t\t\t\tvoid qq\n\t\t\t\t\t\t.whereNotNull('user_schema_assessment_permissions.userID')\n\t\t\t\t\t\t.where('user.businessID', businessID);\n\t\t\t\t})\n\t\t\t\t.orWhere((qq) => {\n\t\t\t\t\tvoid qq\n\t\t\t\t\t\t.whereNotNull('user_schema_assessment_permissions.teamID')\n\t\t\t\t\t\t.where('permissions.businessID', businessID);\n\t\t\t\t});\n\t\t});\n\n\tfor (const row of rows) {\n\t\tif (row.userID) {\n\t\t\tapprovedReviewers.users.push(await getCompactUser(row.userID));\n\t\t} else if (row.teamID) {\n\t\t\tconst team = await sql.getRow<any>(sql.Table.Permissions, {\n\t\t\t\tid: row.teamID,\n\t\t\t});\n\t\t\tapprovedReviewers.teams.push({\n\t\t\t\tbusinessID: team.businessID,\n\t\t\t\trole: team.role,\n\t\t\t\tid: team.id,\n\t\t\t});\n\t\t}\n\t}\n\n\treturn approvedReviewers;\n}\n\nexport async function getApprovedReviewersForMultipleSchemaAssessments(\n\tschemaIDs: number[],\n\tassessmentIDs: number[],\n\tbusinessID: string,\n\tpermissionToCheck: UserSchemaAssessmentPermissions\n): Promise<UserTeamObject> {\n\tconst approvedReviewers = {\n\t\tusers: [],\n\t\tteams: [],\n\t};\n\tconst rows = await sql\n\t\t.knex(sql.Table.UserSchemaAssessmentPermissions)\n\t\t.whereIn('schemaID', schemaIDs)\n\t\t.whereIn('assessmentID', assessmentIDs)\n\t\t.where(permissionToCheck, true)\n\t\t// ensure i only fetch my own business user\n\t\t.leftJoin('user', 'user.id', 'user_schema_assessment_permissions.userID')\n\t\t.leftJoin(\n\t\t\t'permissions',\n\t\t\t'permissions.id',\n\t\t\t'user_schema_assessment_permissions.teamID'\n\t\t)\n\t\t.where((q) => {\n\t\t\tvoid q\n\t\t\t\t.where((qq) => {\n\t\t\t\t\tvoid qq\n\t\t\t\t\t\t.whereNotNull('user_schema_assessment_permissions.userID')\n\t\t\t\t\t\t.where('user.businessID', businessID);\n\t\t\t\t})\n\t\t\t\t.orWhere((qq) => {\n\t\t\t\t\tvoid qq\n\t\t\t\t\t\t.whereNotNull('user_schema_assessment_permissions.teamID')\n\t\t\t\t\t\t.where('permissions.businessID', businessID);\n\t\t\t\t});\n\t\t});\n\n\tfor (const row of rows) {\n\t\t// avoid duplicates\n\t\tif (row.userID) {\n\t\t\tif (\n\t\t\t\t!approvedReviewers.users.length ||\n\t\t\t\t!approvedReviewers.users.find((u) => u.userID === row.userID)\n\t\t\t) {\n\t\t\t\tapprovedReviewers.users.push(await getCompactUser(row.userID));\n\t\t\t}\n\t\t} else if (row.teamID) {\n\t\t\tif (\n\t\t\t\t!approvedReviewers.teams.length ||\n\t\t\t\t!approvedReviewers.teams.find((t) => t.teamID === row.teamID)\n\t\t\t) {\n\t\t\t\tconst team = await sql.getRow<any>(sql.Table.Permissions, {\n\t\t\t\t\tid: row.teamID,\n\t\t\t\t});\n\t\t\t\tapprovedReviewers.teams.push({\n\t\t\t\t\tbusinessID: team.businessID,\n\t\t\t\t\trole: team.role,\n\t\t\t\t\tid: team.id,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\treturn approvedReviewers;\n}\n\nexport async function getReviewerUserIDsForAssessmentID(\n\tplantID: Plant['id'],\n\tschemaID: Schema['id'],\n\tassessmentID: Assessment['id']\n): Promise<string[]> {\n\t// get the approved reviewers, can either be a user or a team\n\tconst approvedReviewersUserIDs =\n\t\tawait getApprovedReviewerUserIDsForAssessment(\n\t\t\t[schemaID],\n\t\t\tassessmentID,\n\t\t\tUserSchemaAssessmentPermissions.CanReviewAssessmentAndCase\n\t\t);\n\n\t// we will then MASK this selection against the allowed businesses.\n\t// default assignees for an application must fit the following scope:\n\t// clients which are CLIENTS of clientgroups => schema\n\t// supplier that owns the plant.\n\n\tconst allowedBusinessIDs: Array<Business['id']> = [];\n\n\t// plant owner businessID\n\tconst ownerBusinessID = await getOwnerBusinessIDOfPlant(plantID);\n\tallowedBusinessIDs.push(ownerBusinessID);\n\n\t// get clients of the clientgroups which are linked to this schema\n\tconst clientGroupIDs = await getClientGroupIDsFromSchemaIDs(\n\t\t[schemaID],\n\t\townerBusinessID\n\t);\n\tconst clientBusinessIDs = await getClientBusinessIDsForClientGroupIDs(\n\t\tclientGroupIDs\n\t);\n\tallowedBusinessIDs.push(...clientBusinessIDs);\n\n\tconst userIDs: Array<User['id']> = approvedReviewersUserIDs\n\t\t.filter((userBusinessIDCombo) =>\n\t\t\tallowedBusinessIDs.includes(userBusinessIDCombo.businessID)\n\t\t)\n\t\t.map((userBusinessIDCombo) => userBusinessIDCombo.userID);\n\n\tconst uniqueUserIDs = Array.from(new Set(userIDs));\n\treturn uniqueUserIDs;\n}\nexport async function amIAssessmentAdministrator(\n\tassessmentID: number,\n\tbusinessID: string\n): Promise<boolean> {\n\tconst assessmentRow = await sql.getRow<any>(sql.Table.Assessment, {\n\t\tid: assessmentID,\n\t});\n\treturn assessmentRow.administratorBusinessID === businessID;\n}\n\nexport async function assessmentBelongToClient(\n\tassessmentID: number,\n\tbusinessID\n): Promise<boolean> {\n\tconst row = await sql\n\t\t.knex(sql.Table.BusinessClientGroupMap)\n\t\t.join(\n\t\t\t'schema_clientGroup_map',\n\t\t\t'schema_clientGroup_map.clientGroupID',\n\t\t\t'business_clientGroup_map.clientGroupID'\n\t\t)\n\t\t.join(\n\t\t\t'schema_assessment_map',\n\t\t\t'schema_assessment_map.schemaID',\n\t\t\t'schema_clientGroup_map.schemaID'\n\t\t)\n\t\t.where('business_clientGroup_map.businessID', businessID)\n\t\t.where('schema_assessment_map.assessmentID', assessmentID)\n\t\t.where('schema_clientGroup_map.active', true)\n\t\t.where('business_clientGroup_map.active', true)\n\t\t.where('business_clientGroup_map.isClient', true)\n\t\t.where('schema_assessment_map.active', true)\n\t\t.first();\n\treturn !!row;\n}\n\nexport async function isApprovedReviewerForAssessment(\n\tassessmentID: number,\n\tuserID: string,\n\tpermissionToCheck: UserSchemaAssessmentPermissions\n): Promise<boolean> {\n\tconst row = await sql\n\t\t.knex(sql.Table.UserSchemaAssessmentPermissions)\n\t\t.where('user_schema_assessment_permissions.assessmentID', assessmentID)\n\t\t.where((q) => {\n\t\t\tvoid q\n\t\t\t\t.where('user_schema_assessment_permissions.userID', userID)\n\t\t\t\t.orWhere('user_team_map.userID', userID);\n\t\t})\n\t\t.where(permissionToCheck, true)\n\t\t.leftJoin(\n\t\t\t'user_team_map',\n\t\t\t'user_team_map.teamID',\n\t\t\t'user_schema_assessment_permissions.teamID'\n\t\t)\n\t\t.select(\n\t\t\t'user_schema_assessment_permissions.userID',\n\t\t\t'user_team_map.userID as uID'\n\t\t)\n\t\t.groupBy('user_schema_assessment_permissions.userID', 'uID');\n\n\treturn !!row && !!row.length;\n}\n\nexport async function checkJointApprovalForAssessment(\n\tassessmentID: number,\n\tplantID: Plant['id'],\n\tuser: User,\n\tapprovalOnBehalfOf: {\n\t\tuserIDs: string[];\n\t\tteamIDs: number[];\n\t},\n\tschemaIDs: number[]\n): Promise<boolean> {\n\t// count how many approval this is\n\t// eg: user is approving for 2 teams at the same time.\n\t// check if the assessment requires joint approval\n\tconst jointApprovers = await getApprovedReviewersForAssessment(\n\t\tassessmentID,\n\t\tUserSchemaAssessmentPermissions.IsJointApprover,\n\t\tuser.businessID,\n\t\tschemaIDs\n\t);\n\tconst assessment: Assessment = await sql.getRow<any>(sql.Table.Assessment, {\n\t\tid: assessmentID,\n\t});\n\tif (\n\t\t!!assessment.requiresOwnerReview ||\n\t\t(!!jointApprovers &&\n\t\t\t(!!jointApprovers.users.length || !!jointApprovers.teams.length))\n\t) {\n\t\tconst approvalSteps =\n\t\t\tapprovalOnBehalfOf.userIDs.length + approvalOnBehalfOf.teamIDs.length;\n\n\t\tconst completedSteps = await sql.getRows(\n\t\t\tsql.Table.PlantAssessmentJointApprovalsMap,\n\t\t\t{\n\t\t\t\tplantID,\n\t\t\t\tassessmentID,\n\t\t\t}\n\t\t);\n\t\tlet userCanStillApprove = true;\n\t\tif (completedSteps && completedSteps.length) {\n\t\t\t// ensure this user is still allowed to approve\n\t\t\tfor (const userID of approvalOnBehalfOf.userIDs) {\n\t\t\t\tif (\n\t\t\t\t\tcompletedSteps.find(\n\t\t\t\t\t\t(step) => step.userID === userID && step.teamID === null\n\t\t\t\t\t)\n\t\t\t\t) {\n\t\t\t\t\t// remove this userID as it has already approved\n\t\t\t\t\tapprovalOnBehalfOf.userIDs.splice(\n\t\t\t\t\t\tapprovalOnBehalfOf.userIDs.indexOf(userID, 1)\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// ensure an approval is still required for these teamIDs\n\t\t\tfor (const teamID of approvalOnBehalfOf.teamIDs) {\n\t\t\t\tif (completedSteps.find((step) => step.teamID === teamID)) {\n\t\t\t\t\t// remove this teamID as it has already approved\n\t\t\t\t\tapprovalOnBehalfOf.teamIDs.splice(\n\t\t\t\t\t\tapprovalOnBehalfOf.teamIDs.indexOf(teamID, 1)\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\t!approvalOnBehalfOf.userIDs.length &&\n\t\t\t\t!approvalOnBehalfOf.teamIDs.length\n\t\t\t) {\n\t\t\t\tuserCanStillApprove = false;\n\t\t\t}\n\t\t}\n\t\tconst isFinalApproval = await checkIsFinalApproval(\n\t\t\tassessmentID,\n\t\t\tuser,\n\t\t\tplantID,\n\t\t\tapprovalSteps,\n\t\t\tschemaIDs\n\t\t);\n\t\tif (!userCanStillApprove) {\n\t\t\tcore.reportError(new Error('attempted to approve again!'), null);\n\t\t} else {\n\t\t\tif (isFinalApproval) {\n\t\t\t\t// this is the last approval required, we are done, flush the table\n\t\t\t\tawait sql\n\t\t\t\t\t.knex(sql.Table.PlantAssessmentJointApprovalsMap)\n\t\t\t\t\t.where('plantID', plantID)\n\t\t\t\t\t.where('assessmentID', assessmentID)\n\t\t\t\t\t.delete();\n\t\t\t\treturn true;\n\t\t\t} else {\n\t\t\t\tconst now = core.moment().unix();\n\t\t\t\tfor (const userID of approvalOnBehalfOf.userIDs) {\n\t\t\t\t\t// approving as themselves\n\t\t\t\t\tawait sql.insertRow(sql.Table.PlantAssessmentJointApprovalsMap, {\n\t\t\t\t\t\tassessmentID,\n\t\t\t\t\t\tplantID,\n\t\t\t\t\t\tapproverID: userID,\n\t\t\t\t\t\tdateCreated: now,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tfor (const teamID of approvalOnBehalfOf.teamIDs) {\n\t\t\t\t\t// approving as part of a team\n\t\t\t\t\tawait sql.insertRow(sql.Table.PlantAssessmentJointApprovalsMap, {\n\t\t\t\t\t\tassessmentID,\n\t\t\t\t\t\tplantID,\n\t\t\t\t\t\tapproverID: user.id,\n\t\t\t\t\t\tapproverTeamID: teamID,\n\t\t\t\t\t\tdateCreated: now,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// not a joint approval, proceed\n\t\treturn true;\n\t}\n}\n\nexport async function markSchemaAssessmentJointApprovalsOnRejection(\n\tassessmentID: number,\n\tplantID: Plant['id'],\n\tschemaID\n): Promise<void> {\n\t// put it back into approval for this schema\n\tawait sql.updateRowsSafe(\n\t\tsql.Table.PlantAssessmentSchemaStatusMap,\n\t\t{\n\t\t\tassessmentID,\n\t\t\tplantID,\n\t\t\tschemaID,\n\t\t},\n\t\t{\n\t\t\trequiresApprovalForSchema: true,\n\t\t}\n\t);\n\tawait redis.delete(redis.CacheDataType.Plant, plantID);\n}\n\nexport async function isAssessmentSharedBetweenClients(\n\tassessmentID: number,\n\tplantID?: Plant['id']\n): Promise<boolean> {\n\t// check if the assessment is shared across schemas that belong to different clients:\n\t// eg: MTM/VLine but not JHG/MTS\n\tconst baseQuery = sql\n\t\t.knex(sql.Table.SchemaAssessmentMap)\n\t\t.join(\n\t\t\t'schema_clientGroup_map',\n\t\t\t'schema_clientGroup_map.schemaID',\n\t\t\t'schema_assessment_map.schemaID'\n\t\t)\n\t\t.where('schema_assessment_map.assessmentID', assessmentID)\n\t\t.where('schema_clientGroup_map.active', true)\n\t\t.where('schema_assessment_map.active', true)\n\t\t.select('schema_clientGroup_map.clientGroupID')\n\t\t.groupBy('schema_clientGroup_map.clientGroupID');\n\n\tif (plantID) {\n\t\t// ensure that this plant is actually connected to more than one client who owns this assessment\n\t\tvoid baseQuery\n\t\t\t.join('plant_assessment_schema_status_map', function () {\n\t\t\t\tthis.on(\n\t\t\t\t\t'plant_assessment_schema_status_map.schemaID',\n\t\t\t\t\t'schema_clientGroup_map.schemaID'\n\t\t\t\t).on(\n\t\t\t\t\t'plant_assessment_schema_status_map.assessmentID',\n\t\t\t\t\t'schema_assessment_map.assessmentID'\n\t\t\t\t);\n\t\t\t})\n\t\t\t.where('plant_assessment_schema_status_map.plantID', plantID);\n\t}\n\tconst rows = await baseQuery;\n\t// more than 1 client 'owns' this assessment\n\treturn rows && rows.length > 1;\n}\n\nexport async function checkIsFinalApproval(\n\tassessmentID: number,\n\tuser: User,\n\tplantID: Plant['id'],\n\tapprovalStepsPerformedInCurrentApproval: number,\n\tschemaIDs: number[]\n): Promise<boolean> {\n\t// this functions simply counts how many approvals have been done and checks if including the current one(s) incoming it will be a final approval\n\n\tconst jointApprovers = await getApprovedReviewersForAssessment(\n\t\tassessmentID,\n\t\tUserSchemaAssessmentPermissions.IsJointApprover,\n\t\tuser.businessID,\n\t\tschemaIDs\n\t);\n\n\tconst assessment: Assessment = await sql.getRow<any>(sql.Table.Assessment, {\n\t\tid: assessmentID,\n\t});\n\tlet jointApproversMinCount = 0;\n\tif (\n\t\t!!assessment.requiresOwnerReview ||\n\t\t(!!jointApprovers &&\n\t\t\t(!!jointApprovers.users.length || !!jointApprovers.teams.length))\n\t) {\n\t\t// count how many user + teams (one user per group is required, not all of them ) are needed\n\t\tif (jointApprovers) {\n\t\t\tjointApproversMinCount =\n\t\t\t\tjointApproversMinCount +\n\t\t\t\tjointApprovers.users.length +\n\t\t\t\tjointApprovers.teams.length;\n\t\t}\n\t\tif (!!assessment.requiresOwnerReview) {\n\t\t\t// jointApproversMinCount only counts the client users, add one more approval if the owner is required too\n\t\t\tif (!jointApproversMinCount) {\n\t\t\t\t// in case it's required for the owner to approve BUT no client users are specifically assigned as joint approvers, the approval will still need to go through a client regardless, thus 1(owner) + 1(client) = 2\n\t\t\t\tjointApproversMinCount = 2;\n\t\t\t} else {\n\t\t\t\tjointApproversMinCount += 1;\n\t\t\t}\n\t\t}\n\t\tconst completedSteps = await sql.getRows(\n\t\t\tsql.Table.PlantAssessmentJointApprovalsMap,\n\t\t\t{\n\t\t\t\tassessmentID,\n\t\t\t\tplantID,\n\t\t\t}\n\t\t);\n\t\t// completing this one will be mean the end of the joint approval?\n\t\t// normally the approvalStepsInCurrentApproval is 1, but the user may be in two teams and approving for both at the same time, which means it counts as two approvals\n\t\treturn (\n\t\t\tcompletedSteps.length ===\n\t\t\tjointApproversMinCount - approvalStepsPerformedInCurrentApproval\n\t\t);\n\t}\n\treturn true;\n}\n\nexport async function disconnectAssessmentForPlant(\n\tplantID: Plant['id'],\n\tassessmentID: Assessment['id'],\n\toriginatingUser: User\n): Promise<void> {\n\tconst existingPlantAssessment = await getAssessmentRowForPlant(\n\t\tplantID,\n\t\tnull,\n\t\tassessmentID,\n\t\ttrue\n\t);\n\tawait sql.updateRows(\n\t\tsql.Table.PlantAssessment,\n\t\t{\n\t\t\tplantID,\n\t\t\tassessmentID,\n\t\t\tactive: true,\n\t\t},\n\t\t{ active: false, isInitialAssessment: false }\n\t);\n\tif (existingPlantAssessment) {\n\t\tnew Event.AssessmentDueDateModified({\n\t\t\tassessmentID,\n\t\t\tplantID,\n\t\t\tcreatorID: originatingUser.id,\n\t\t\tisSuperadmin: false,\n\t\t\tfromDate: existingPlantAssessment.nextDueDate,\n\t\t\ttoDate: null,\n\t\t});\n\t\t// assessment disconnected\n\t\tnew Event.AssessmentRequiredChanged({\n\t\t\tassessmentID,\n\t\t\tplantID,\n\t\t\tcreatorID: null,\n\t\t\tisSuperadmin: false,\n\t\t\tfromValue: true,\n\t\t\ttoValue: false,\n\t\t});\n\t}\n}\n\nexport async function getAssessmentsFromSpecifications(\n\tplantID: Plant['id'],\n\tmodifiedSpecifications: SpecificationChange['specificationDiff'],\n\tschemaIDs: Array<Schema['id']>,\n\tcheckForAssessmentsToRemove = false\n): Promise<Assessment[]> {\n\t// get assessments that are enabled by the presence of a spec\n\tconst modifiedSpecificationIDs =\n\t\tmodifiedSpecifications?.map((sp) => sp.id) || [];\n\n\tconst allSpecificationIDs = [];\n\tallSpecificationIDs.push(...modifiedSpecificationIDs);\n\n\tlet existingSpecifications;\n\tif (!checkForAssessmentsToRemove) {\n\t\t// if we want to check for assessment to remove, we will simply see what assessments were turned on by the oldValue of the specs\n\t\texistingSpecifications = await sql\n\t\t\t.knex(sql.Table.PlantSpecification)\n\t\t\t.where('plantID', plantID)\n\t\t\t.whereNotIn('specificationID', modifiedSpecificationIDs)\n\t\t\t.select('specificationID', 'plant_specification_map.value');\n\t\tconst existingSpecIDs = existingSpecifications?.map(\n\t\t\t(sp) => sp.specificationID\n\t\t);\n\t\tallSpecificationIDs.push(...existingSpecIDs);\n\t}\n\tconst possibleAssessmentRows = await sql\n\t\t.knex(sql.Table.SpecificationAssessmentMap)\n\t\t.join(\n\t\t\t'assessment',\n\t\t\t'assessment.id',\n\t\t\t'specification_assessment_map.assessmentID'\n\t\t)\n\t\t.join(\n\t\t\t'schema_assessment_map',\n\t\t\t'schema_assessment_map.assessmentID',\n\t\t\t'specification_assessment_map.assessmentID'\n\t\t)\n\t\t// ensure we don't pull in assessments that have nothing to do with these schemas\n\t\t.where((q) => {\n\t\t\t// connected to that assessment already or being connected now\n\t\t\tvoid q\n\t\t\t\t.whereIn('schema_assessment_map.schemaID', schemaIDs)\n\t\t\t\t.orWhereExists(\n\t\t\t\t\tsql\n\t\t\t\t\t\t.knex(sql.Table.PlantSchemaMap)\n\t\t\t\t\t\t.where('plant_schema_map.plantID', plantID)\n\t\t\t\t\t\t.where(\n\t\t\t\t\t\t\t'plant_schema_map.schemaID',\n\t\t\t\t\t\t\t'=',\n\t\t\t\t\t\t\tsql.knex.ref('schema_assessment_map.schemaID')\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.where('schema_assessment_map.active', true)\n\t\t\t\t);\n\t\t})\n\t\t.where('schema_assessment_map.active', true)\n\t\t.whereNotNull('activateWhenValue')\n\t\t.whereIn('specificationID', allSpecificationIDs)\n\t\t.select('assessment.*', 'specification_assessment_map.*');\n\tconst assessments = [];\n\n\tfor (const assessment of possibleAssessmentRows) {\n\t\t// simple conversion that can be extended and more thorough in order to compare the value in the db with this\n\t\tconst specActivateWhenValue =\n\t\t\tassessment.activateWhenValue === 'true'\n\t\t\t\t? true\n\t\t\t\t: assessment.activateWhenValue === 'false'\n\t\t\t\t? false\n\t\t\t\t: assessment.activateWhenValue;\n\t\tif (checkForAssessmentsToRemove) {\n\t\t\t// perform a check based on the oldValue\n\t\t\tconst specification = modifiedSpecifications?.find(\n\t\t\t\t(spec) => spec.id === assessment.specificationID\n\t\t\t);\n\t\t\t// remove the to string !!!\n\t\t\tif (\n\t\t\t\tspecActivateWhenValue === specification?.oldVal &&\n\t\t\t\tassessment.activateWhenValue !== specification?.newVal\n\t\t\t) {\n\t\t\t\t// these are not allowed anymore\n\t\t\t\tassessments.push(assessment);\n\t\t\t}\n\t\t} else {\n\t\t\tconst specification = modifiedSpecifications?.find(\n\t\t\t\t(spec) => spec.id === assessment.specificationID\n\t\t\t);\n\t\t\tconst existingSpecification = existingSpecifications?.find(\n\t\t\t\t(spec) => spec.id === assessment.specificationID\n\t\t\t);\n\t\t\tif (\n\t\t\t\tspecActivateWhenValue === specification?.newVal ||\n\t\t\t\tspecActivateWhenValue === existingSpecification?.value\n\t\t\t) {\n\t\t\t\tassessments.push(assessment);\n\t\t\t}\n\t\t}\n\t}\n\treturn assessments;\n}\nexport async function getAssessmentsFromKeyDocuments(\n\tplantID: Plant['id'],\n\tdocumentIDs: Array<KeyDocument['id']>,\n\tschemaIDs: Array<Schema['id']>\n): Promise<Assessment[]> {\n\t// usage note: ensure that the key document is connected to only one schema because the assessment has no knowledge of a schema so it won't know if it needs to be activated for schema 1 or 2,it will turn on regardless\n\n\t// get the assessment from the new key documents that we are adding on the plant\n\tconst possibleAssessmentsFromKeyDocuments = documentIDs?.length\n\t\t? await sql\n\t\t\t\t.knex(sql.Table.AssessmentKeyDocumentsMap)\n\t\t\t\t.join(\n\t\t\t\t\t'schema_key_documents_map',\n\t\t\t\t\t'schema_key_documents_map.documentID',\n\t\t\t\t\t'assessment_key_documents_map.documentID'\n\t\t\t\t)\n\t\t\t\t.join(\n\t\t\t\t\t'assessment',\n\t\t\t\t\t'assessment.id',\n\t\t\t\t\t'assessment_key_documents_map.assessmentID'\n\t\t\t\t)\n\t\t\t\t.whereIn('assessment_key_documents_map.documentID', documentIDs)\n\t\t\t\t// make sure that the document is connected to the schemas that are connected or connecting to the plant\n\t\t\t\t.whereIn('schema_key_documents_map.schemaID', schemaIDs)\n\t\t\t\t.where('assessment_key_documents_map.active', true)\n\t\t\t\t.select('assessment.*')\n\t\t: [];\n\t// get the assessment from the key documents already on the plant, ignored if we are just checking for delete documents\n\tconst possibleAssessmentsFromPlantKeyDocuments = !plantID\n\t\t? []\n\t\t: await sql\n\t\t\t\t.knex(sql.Table.PlantKeyDocumentsMap)\n\t\t\t\t.join(\n\t\t\t\t\t'assessment_key_documents_map',\n\t\t\t\t\t'assessment_key_documents_map.documentID',\n\t\t\t\t\t'plant_key_documents_map.documentID'\n\t\t\t\t)\n\t\t\t\t.join(\n\t\t\t\t\t'schema_key_documents_map',\n\t\t\t\t\t'schema_key_documents_map.documentID',\n\t\t\t\t\t'assessment_key_documents_map.documentID'\n\t\t\t\t)\n\t\t\t\t.whereIn('assessment_key_documents_map.documentID', documentIDs)\n\t\t\t\t.where('plant_key_documents_map.plantID', plantID)\n\t\t\t\t.where('plant_key_documents_map.active', true)\n\t\t\t\t.join(\n\t\t\t\t\t'assessment',\n\t\t\t\t\t'assessment.id',\n\t\t\t\t\t'assessment_key_documents_map.assessmentID'\n\t\t\t\t)\n\t\t\t\t.where('assessment_key_documents_map.active', true)\n\t\t\t\t.select('assessment.*');\n\tconst assessmentSet = new Set();\n\tfor (const ass of [\n\t\t...possibleAssessmentsFromKeyDocuments,\n\t\t...possibleAssessmentsFromPlantKeyDocuments,\n\t]) {\n\t\tassessmentSet.add(ass);\n\t}\n\treturn Array.from(assessmentSet) as Assessment[];\n}\n\nexport async function getNextAssessmentIdentifier(businessID: Business['id']) {\n\tlet counter: number;\n\tconst businessIntegerID: string = (\n\t\tawait sql.getRow<any>(\n\t\t\tsql.Table.Business,\n\t\t\t{\n\t\t\t\tid: businessID,\n\t\t\t},\n\t\t\t['integerIdentifier']\n\t\t)\n\t)?.integerIdentifier;\n\tawait sql.knexMaster.transaction(async (t) => {\n\t\t// increment the counter\n\t\tawait sql\n\t\t\t.knexMaster('business')\n\t\t\t.transacting(t)\n\t\t\t.where('business.integerIdentifier', businessIntegerID)\n\t\t\t.increment('assessmentCount', 1);\n\t\t// get the new counter\n\t\tconst { assessmentCount } = await sql\n\t\t\t.knexMaster('business')\n\t\t\t.transacting(t)\n\t\t\t.where('business.integerIdentifier', businessIntegerID)\n\t\t\t.first();\n\t\tcounter = assessmentCount;\n\t});\n\tif (businessIntegerID == null || businessIntegerID == null) {\n\t\tcore.reportError(new Error(\"Couldn't create identifier!\"));\n\t\treturn '';\n\t}\n\treturn businessIntegerID.toString() + 'S' + counter.toString();\n}\n\nexport function getNextDeadlineForInitialScheduleOfMaintenanceStep(\n\tstep: MaintenanceSequenceStepInsertionRow,\n\tplant,\n\tpreviousMaintenanceStep: {\n\t\tstepID: number;\n\t\tdatePerformed: number;\n\t\todometerReading: number;\n\t\tusageHours: number;\n\t}\n): {\n\todometerDue: number;\n\tdateDue: number;\n\thoursDue: number;\n} {\n\tconst today = core.moment().unix();\n\n\t// this are configured when scheduling the first time\n\tconst initialDateForSequencing = today;\n\tconst initialUsageHoursForSequencing: number = plant.totalHoursLogged || 0;\n\tconst initialOdometerForSequencing: number = plant.lastOdometerReading || 0;\n\t// at the moment they default to 0\n\tlet odometerDue = null;\n\tlet hoursDue = null;\n\tlet dateDue = null;\n\n\t// if a previousMaintenanceStep is passed, we use whatever value the user input in the field or default to 0, meaning that they explicitly wanted the step to start counting at zero, regardless if it's gonna be overdue\n\n\t// if no previousMaintenanceStep, we start from zero we push forward the deadlines until we find the appropriate next one\n\t// eg: current is 55km, next one will have to be 60km (repeat every 10 starting from 0) and not 65km ( dont add 10 onto 55)\n\tif (step.offsetDate) {\n\t\tconst offsetInUnixSeconds = convertStringDurationToSeconds(step.offsetDate);\n\n\t\t// if no date was added to the previousMaintenanceStep, just calculate the next one as we normally would\n\t\tif (previousMaintenanceStep?.datePerformed) {\n\t\t\t// the user is in advanced edit mode, use whatever value they input or default to 0, disregard the current usage parameter on the plant\n\t\t\tdateDue = previousMaintenanceStep.datePerformed + offsetInUnixSeconds;\n\t\t} else {\n\t\t\tlet nextDueDate = 0;\n\t\t\t// use end of day to ensure we don't have issues caused by difference in minutes within the day in the while loop\n\t\t\tconst currentDateDue = core\n\t\t\t\t.moment(initialDateForSequencing * 1000)\n\t\t\t\t.endOf('day')\n\t\t\t\t.unix();\n\t\t\t// iterate from the starting point until we find the next deadline\n\t\t\t// example: starting point is 300. deadline offset is 200. previous submitted one was 900.\n\t\t\t// calculation will be 300 - 500 - 700 - 900 - 1100 - stop 1100 is the next in line\n\t\t\twhile (nextDueDate <= currentDateDue) {\n\t\t\t\tnextDueDate = nextDueDate + offsetInUnixSeconds;\n\t\t\t}\n\t\t\tdateDue = nextDueDate;\n\t\t}\n\t}\n\n\tif (step.offsetHours) {\n\t\tif (previousMaintenanceStep?.stepID) {\n\t\t\t// the user is in advanced edit mode, use whatever value they input or default to 0, disregard the current usage parameter on the plant\n\t\t\thoursDue = (previousMaintenanceStep.usageHours || 0) + step.offsetHours;\n\t\t} else {\n\t\t\tlet nextDueHours = 0;\n\t\t\tconst currentHoursDue = initialUsageHoursForSequencing;\n\t\t\twhile (nextDueHours <= currentHoursDue) {\n\t\t\t\tnextDueHours = nextDueHours + step.offsetHours;\n\t\t\t}\n\t\t\thoursDue = nextDueHours;\n\t\t}\n\t}\n\n\tif (step.offsetOdometer) {\n\t\tif (previousMaintenanceStep?.stepID) {\n\t\t\t// the user is in advanced edit mode, use whatever value they input or default to 0, disregard the current usage parameter on the plant\n\t\t\todometerDue =\n\t\t\t\t(previousMaintenanceStep.odometerReading || 0) + step.offsetOdometer;\n\t\t} else {\n\t\t\t// start counting from zero and get the next one coming up\n\t\t\tlet nextOdometerDue = 0;\n\t\t\tconst currentOdometerDue = initialOdometerForSequencing;\n\t\t\twhile (nextOdometerDue <= currentOdometerDue) {\n\t\t\t\tnextOdometerDue = nextOdometerDue + step.offsetOdometer;\n\t\t\t}\n\t\t\todometerDue = nextOdometerDue;\n\t\t}\n\t}\n\n\treturn {\n\t\tdateDue,\n\t\todometerDue,\n\t\thoursDue,\n\t};\n}\nexport async function scheduleMaintenanceForPlant(\n\tmaintenanceObj: NewMaintenanceSchedule,\n\tuser: User,\n\tskipEvent = false\n): Promise<{\n\tassessmentID: PlantAssessment['id'];\n}> {\n\tawait redis.delete(redis.CacheDataType.Plant, maintenanceObj.plantID);\n\n\tconst now = core.moment().unix();\n\tconst plantRow = await sql.getRow<any>(\n\t\tsql.Table.Plant,\n\t\t{\n\t\t\tid: maintenanceObj.plantID,\n\t\t\tactive: true,\n\t\t},\n\t\t['lastOdometerReading', 'totalFuelUsed', 'totalHoursLogged']\n\t);\n\tif (!plantRow) {\n\t\treturn;\n\t}\n\n\tlet plantAssessmentID = core.uuidv4();\n\tconst assessmentID = maintenanceObj.assessmentID;\n\n\tconst maintenanceSteps: MaintenanceSequenceStep[] =\n\t\t(await sql.getRows(sql.Table.MaintenanceSequenceStep, {\n\t\t\tassessmentID: maintenanceObj.assessmentID,\n\t\t\tactive: true,\n\t\t})) || [];\n\n\t// check if the plant already had a previously deleted maintenance plan for this assessmentID\n\tconst existingInactiveMaintenancePlan = await sql.getRow(\n\t\tsql.Table.PlantAssessment,\n\t\t{\n\t\t\tplantID: maintenanceObj.plantID,\n\t\t\tassessmentID,\n\t\t\tactive: false,\n\t\t}\n\t);\n\tif (existingInactiveMaintenancePlan) {\n\t\t// reuse the existing assessment ID as we are activating it\n\t\tplantAssessmentID = existingInactiveMaintenancePlan.id as string;\n\t\tawait sql.updateRows(\n\t\t\tsql.Table.PlantAssessment,\n\t\t\t{\n\t\t\t\tid: existingInactiveMaintenancePlan.id,\n\t\t\t},\n\t\t\t{\n\t\t\t\tactive: true,\n\t\t\t\tbusinessID: user.businessID,\n\t\t\t\tdateCreated: core.moment().unix(),\n\t\t\t\ttitle: maintenanceObj.title,\n\t\t\t\tdescription: maintenanceObj.description,\n\t\t\t\t// make sure no banner for initial assessment shows\n\t\t\t\tisInitialAssessment: false,\n\t\t\t}\n\t\t);\n\n\t\t// delete the previous exisitng steps before inserting the new ones\n\t\tawait sql.deleteRow(sql.Table.PlantAssessmentStepMap, {\n\t\t\tplantAssessmentID: existingInactiveMaintenancePlan.id,\n\t\t});\n\t} else {\n\t\tawait sql.insertRow(sql.Table.PlantAssessment, {\n\t\t\tplantID: maintenanceObj.plantID,\n\t\t\tid: plantAssessmentID,\n\t\t\tassessmentID,\n\t\t\tcreatorID: user.id,\n\t\t\tbusinessID: user.businessID,\n\t\t\tdateCreated: core.moment().unix(),\n\t\t\ttitle: maintenanceObj.title,\n\t\t\tdescription: maintenanceObj.description,\n\t\t\tidentifier: await getNextAssessmentIdentifier(user.businessID),\n\t\t\t// make sure no banner for initial assessment shows\n\t\t\tisInitialAssessment: false,\n\t\t});\n\t}\n\n\tconst maintenanceStepInstances: PlantAssessmentStepInstanceInsertion[] = [];\n\t// create one step instance for each step configuration\n\tfor (const step of maintenanceSteps) {\n\t\tconst offsetInUnixSeconds = convertStringDurationToSeconds(step.offsetDate);\n\n\t\tconst previousMaintenanceStep =\n\t\t\tmaintenanceObj.previousMaintenanceSteps.find((s) => s.stepID === step.id);\n\n\t\tconst { odometerDue, dateDue, hoursDue } =\n\t\t\tgetNextDeadlineForInitialScheduleOfMaintenanceStep(\n\t\t\t\tstep,\n\t\t\t\tplantRow,\n\t\t\t\tpreviousMaintenanceStep\n\t\t\t);\n\n\t\tconst stepObj: PlantAssessmentStepInstanceInsertion = {\n\t\t\t// ensure it's marked as non complete\n\t\t\tplantAssessmentID,\n\t\t\tcompleted: false,\n\t\t\tmaintenanceSequenceStepID: step.id,\n\t\t\toverdue: false,\n\t\t\todometerDue,\n\t\t\tfuelDue: null,\n\t\t\thoursDue,\n\t\t\tdateDue,\n\t\t\todometerDueWithTolerance:\n\t\t\t\todometerDue && step.tolerance\n\t\t\t\t\t? odometerDue + (step.offsetOdometer * step.tolerance) / 100\n\t\t\t\t\t: null,\n\t\t\thoursDueWithTolerance:\n\t\t\t\thoursDue && step.tolerance\n\t\t\t\t\t? hoursDue + (step.offsetHours * step.tolerance) / 100\n\t\t\t\t\t: null,\n\t\t\tdateDueWithTolerance:\n\t\t\t\tdateDue && step.tolerance\n\t\t\t\t\t? dateDue + (offsetInUnixSeconds * step.tolerance) / 100\n\t\t\t\t\t: null,\n\t\t};\n\n\t\tconst overDueParam = maintenanceOverdue(\n\t\t\t{\n\t\t\t\todometer: plantRow.lastOdometerReading,\n\t\t\t\thours: plantRow.totalHoursLogged,\n\t\t\t\tfuel: plantRow.totalFuelUsed,\n\t\t\t\tdate: now,\n\t\t\t},\n\t\t\t// check the highest between the tolerance and the standard value\n\t\t\t{\n\t\t\t\thours: stepObj.hoursDueWithTolerance || stepObj.hoursDue,\n\t\t\t\tfuel: null,\n\t\t\t\todometer: stepObj.odometerDueWithTolerance || stepObj.odometerDue,\n\t\t\t\tdate: stepObj.dateDueWithTolerance || stepObj.dateDue,\n\t\t\t}\n\t\t);\n\n\t\tstepObj.overdue = !!overDueParam;\n\n\t\tmaintenanceStepInstances.push(stepObj);\n\t}\n\n\t// schedule one instance for each step\n\tfor (const stepInstance of maintenanceStepInstances) {\n\t\tawait sql.insertRow(sql.Table.PlantAssessmentStepMap, {\n\t\t\tplantAssessmentID,\n\t\t\tmaintenanceSequenceStepID: stepInstance.maintenanceSequenceStepID,\n\t\t\tcompleted: stepInstance.completed,\n\t\t\tfuelDue: stepInstance.fuelDue,\n\t\t\thoursDue: stepInstance.hoursDue,\n\t\t\todometerDue: stepInstance.odometerDue,\n\t\t\tdateDue: stepInstance.dateDue,\n\t\t\toverdue: stepInstance.overdue,\n\t\t\thoursDueWithTolerance: stepInstance.hoursDueWithTolerance,\n\t\t\todometerDueWithTolerance: stepInstance.odometerDueWithTolerance,\n\t\t\tdateDueWithTolerance: stepInstance.dateDueWithTolerance,\n\t\t\tfuelDueWithTolerance: stepInstance.fuelDueWithTolerance,\n\t\t});\n\t}\n\n\tif (!skipEvent) {\n\t\tnew Event.MaintenanceScheduled({\n\t\t\tplantID: maintenanceObj.plantID,\n\t\t\tcreatorID: user.id,\n\t\t\tplantAssessmentID,\n\t\t\tisSuperadmin: user.isSuperadmin,\n\t\t});\n\t\t// run a check on this plant and mark it overdue/tagged out if necesasry\n\t\tawait tagOutPlantExceedingTolerances(maintenanceObj.plantID);\n\t}\n\n\treturn {\n\t\tassessmentID: plantAssessmentID,\n\t};\n}\n\nexport async function getMaintenanceSequenceSteps(\n\tsequenceID: number\n): Promise<MaintenanceSequenceStep[]> {\n\tconst steps: MaintenanceSequenceStep[] = await sql\n\t\t.knex(sql.Table.MaintenanceSequenceStep)\n\t\t.where('active', true)\n\t\t.where('assessmentID', sequenceID)\n\t\t.orderBy('id', 'asc');\n\n\tfor (const step of steps) {\n\t\t// todo: rmeove after migraiton done\n\t\t(step as any).templateIDs = (\n\t\t\tawait sql.getRows(sql.Table.MaintenanceSequenceStepTemplateMap, {\n\t\t\t\tmaintenanceSequenceStepID: step.id,\n\t\t\t})\n\t\t)?.map((row) => row.templateID);\n\t\tstep.taskTemplateIDs = (\n\t\t\tawait sql\n\t\t\t\t.knex(sql.Table.TaskTemplate)\n\t\t\t\t.join(\n\t\t\t\t\tsql.Table.TaskTemplateStepMap,\n\t\t\t\t\tsql.Table.TaskTemplateStepMap + '.taskTemplateID',\n\t\t\t\t\tsql.Table.TaskTemplate + '.id'\n\t\t\t\t)\n\t\t\t\t.where(sql.Table.TaskTemplateStepMap + '.stepID', step.id)\n\t\t\t\t.select(sql.Table.TaskTemplate + '.id')\n\t\t).map((row) => row.id);\n\t}\n\n\treturn steps;\n}\n\nexport async function updateScheduledMaintenance(\n\tplantID: Plant['id'],\n\tmaintenanceObj: NewMaintenanceSchedule,\n\tuser: User\n) {\n\tawait redis.delete(redis.CacheDataType.Plant, plantID);\n\n\tif (!plantID) throw new Error('Plant ID not provided.');\n\tconst plantRow = await sql.getRow<any>(\n\t\tsql.Table.Plant,\n\t\t{\n\t\t\tactive: true,\n\t\t\tid: plantID,\n\t\t},\n\t\t['lastOdometerReading', 'totalFuelUsed', 'totalHoursLogged']\n\t);\n\tconst today = core.moment().unix();\n\n\t// this will be configurable when scheduling\n\tconst initialDateForSequencing = today;\n\tconst initialUsageHoursForSequencing: number = plantRow.totalHoursLogged || 0;\n\tconst initialOdometerForSequencing: number =\n\t\tplantRow.lastOdometerReading || 0;\n\tlet maintenanceSteps: MaintenanceSequenceStep[];\n\tif (maintenanceObj.assessmentID) {\n\t\t// if no assessmentID it means that it's a one off maintenance so there are no steps\n\t\t// get all the possible steps for this maintenance\n\t\tmaintenanceSteps =\n\t\t\t(await sql.getRows(sql.Table.MaintenanceSequenceStep, {\n\t\t\t\tassessmentID: maintenanceObj.assessmentID,\n\t\t\t\tactive: true,\n\t\t\t})) || [];\n\t}\n\tconst assessmentUpdateObj = {\n\t\tdescription: maintenanceObj.description,\n\t\ttitle: maintenanceObj.title,\n\t\tawaitingReview: false,\n\t};\n\n\tconst maintenanceStepInstances: PlantAssessmentStepInstanceInsertion[] = [];\n\n\tif (maintenanceSteps?.length) {\n\t\t// todo: this is actually not allowed at the moment for maintenance with sequences\n\t\t// create one step instance for each step configuration\n\t\tfor (const step of maintenanceSteps) {\n\t\t\tconst offsetInUnixSeconds = convertStringDurationToSeconds(\n\t\t\t\tstep.offsetDate\n\t\t\t);\n\n\t\t\t// todo: account for toleance here when we eventually open up editing of sequenced maintenances\n\t\t\tconst overDueParam = maintenanceOverdue(\n\t\t\t\t{\n\t\t\t\t\todometer: plantRow.lastOdometerReading,\n\t\t\t\t\thours: plantRow.totalHoursLogged,\n\t\t\t\t\tfuel: plantRow.totalFuelUsed,\n\t\t\t\t\tdate: today,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\thours: maintenanceObj.hoursDue,\n\t\t\t\t\tfuel: maintenanceObj.fuelDue,\n\t\t\t\t\todometer: maintenanceObj.odometerDue,\n\t\t\t\t\tdate: maintenanceObj.dateDue,\n\t\t\t\t}\n\t\t\t);\n\n\t\t\tmaintenanceStepInstances.push({\n\t\t\t\t// ensure it's marked as non complete\n\t\t\t\tplantAssessmentID: maintenanceObj.id,\n\t\t\t\tcompleted: false,\n\t\t\t\todometerDue: step.offsetOdometer\n\t\t\t\t\t? step.offsetOdometer + initialOdometerForSequencing\n\t\t\t\t\t: null,\n\t\t\t\t// unused\n\t\t\t\tfuelDue: null,\n\t\t\t\thoursDue: step.offsetHours\n\t\t\t\t\t? step.offsetHours + initialUsageHoursForSequencing\n\t\t\t\t\t: null,\n\t\t\t\tdateDue: step.offsetDate\n\t\t\t\t\t? offsetInUnixSeconds + initialDateForSequencing\n\t\t\t\t\t: null,\n\t\t\t\toverdue: !!overDueParam,\n\t\t\t\tmaintenanceSequenceStepID: step.id,\n\t\t\t});\n\t\t}\n\t} else {\n\t\t// no step, one off\n\t\t// recalculate if it should still be marked as overdue\n\t\tconst overDueParam = maintenanceOverdue(\n\t\t\t{\n\t\t\t\todometer: plantRow.lastOdometerReading,\n\t\t\t\thours: plantRow.totalHoursLogged,\n\t\t\t\tfuel: plantRow.totalFuelUsed,\n\t\t\t\tdate: today,\n\t\t\t},\n\t\t\t{\n\t\t\t\thours: maintenanceObj.hoursDue,\n\t\t\t\tfuel: maintenanceObj.fuelDue,\n\t\t\t\todometer: maintenanceObj.odometerDue,\n\t\t\t\tdate: maintenanceObj.dateDue,\n\t\t\t}\n\t\t);\n\n\t\tif (!!overDueParam) {\n\t\t\t// close the case that was opened when the plant got marked for tagged out because exceeding a maintenance deadline\n\t\t\tconst tagOutCase = await sql.getRow<CaseRow>(sql.Table.Conversation, {\n\t\t\t\tstatus: CaseStatus.Open,\n\t\t\t\ttype: ConversationType.Case,\n\t\t\t\treason: CaseReason.TaggedOut,\n\t\t\t\tplantID: maintenanceObj.plantID,\n\t\t\t\tassessmentID: maintenanceObj.assessmentID || null,\n\t\t\t\tschemaID: null,\n\t\t\t});\n\t\t\tif (tagOutCase) {\n\t\t\t\tawait closeCase(tagOutCase.id, user);\n\t\t\t}\n\t\t}\n\t\tmaintenanceStepInstances.push({\n\t\t\t// ensure it's marked as non complete\n\t\t\tplantAssessmentID: maintenanceObj.id,\n\t\t\tcompleted: false,\n\t\t\todometerDue: maintenanceObj.odometerDue,\n\t\t\t// unused fuel\n\t\t\tfuelDue: maintenanceObj.fuelDue,\n\t\t\thoursDue: maintenanceObj.hoursDue,\n\t\t\tdateDue: maintenanceObj.dateDue,\n\t\t\toverdue: !!overDueParam,\n\t\t\tmaintenanceSequenceStepID: null,\n\t\t});\n\t}\n\n\t// update the plant assessment instance\n\tawait sql.updateRows(\n\t\tsql.Table.PlantAssessment,\n\t\t{\n\t\t\tplantID,\n\t\t\tid: maintenanceObj.id,\n\t\t},\n\t\tassessmentUpdateObj\n\t);\n\n\t// update all the step instances\n\tfor (const stepInstance of maintenanceStepInstances) {\n\t\tawait sql.updateRows(\n\t\t\tsql.Table.PlantAssessmentStepMap,\n\t\t\t{\n\t\t\t\tplantAssessmentID: stepInstance.plantAssessmentID,\n\t\t\t\tmaintenanceSequenceStepID: stepInstance.maintenanceSequenceStepID,\n\t\t\t},\n\t\t\t{\n\t\t\t\tcompleted: stepInstance.completed,\n\t\t\t\tfuelDue: stepInstance.fuelDue,\n\t\t\t\thoursDue: stepInstance.hoursDue,\n\t\t\t\todometerDue: stepInstance.odometerDue,\n\t\t\t\tdateDue: stepInstance.dateDue,\n\t\t\t\toverdue: stepInstance.overdue,\n\t\t\t}\n\t\t);\n\t}\n}\n\nexport async function getAssessmentStepInstances(\n\tplantAssessmentID\n): Promise<PlantAssessmentStepInstance[]> {\n\tconst stepInstances: PlantAssessmentStepInstance[] = await sql\n\t\t.knex(sql.Table.PlantAssessmentStepMap)\n\t\t.where('plantAssessmentID', plantAssessmentID)\n\t\t.leftJoin(\n\t\t\t'maintenance_sequence_step',\n\t\t\t'maintenance_sequence_step.id',\n\t\t\t'plant_assessment_step_map.maintenanceSequenceStepID'\n\t\t)\n\t\t.select(\n\t\t\t'plant_assessment_step_map.*',\n\t\t\t'maintenance_sequence_step.name',\n\t\t\t'maintenance_sequence_step.offsetOdometer',\n\t\t\t'maintenance_sequence_step.offsetFuel',\n\t\t\t'maintenance_sequence_step.offsetHours',\n\t\t\t'maintenance_sequence_step.offsetDate'\n\t\t);\n\n\tif (!stepInstances?.length) {\n\t\treturn [];\n\t}\n\n\tconst plantRow = await sql\n\t\t.knex(sql.Table.PlantAssessment)\n\t\t.join('plant', 'plant_assessment_map.plantID', 'plant.id')\n\t\t.where('plant_assessment_map.id', plantAssessmentID)\n\t\t.select('lastOdometerReading', 'totalFuelUsed', 'totalHoursLogged')\n\t\t.first();\n\n\tfor (const step of stepInstances) {\n\t\t// todo: remove after migration is done\n\t\t(step as any).templateIDs = (\n\t\t\tawait sql\n\t\t\t\t.knex(sql.Table.MaintenanceSequenceStepTemplateMap)\n\t\t\t\t.where('maintenanceSequenceStepID', step.maintenanceSequenceStepID)\n\t\t)?.map((row) => row.templateID);\n\n\t\tstep.exceedsTolerance = false;\n\n\t\tif (step.dateDueWithTolerance) {\n\t\t\tif (core.moment().unix() > step.dateDueWithTolerance) {\n\t\t\t\tstep.exceedsTolerance = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\n\t\tif (step.hoursDueWithTolerance) {\n\t\t\tif (plantRow.totalHoursLogged > step.hoursDueWithTolerance) {\n\t\t\t\tstep.exceedsTolerance = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\n\t\tif (step.odometerDueWithTolerance) {\n\t\t\tif (plantRow.lastOdometerReading > step.odometerDueWithTolerance) {\n\t\t\t\tstep.exceedsTolerance = true;\n\t\t\t}\n\t\t}\n\t}\n\treturn stepInstances;\n}\n\nexport async function isAssessmentAMaintenancePlan(\n\tplantAssessmentID: PlantAssessment['id'],\n\tassessmentID: Assessment['id']\n): Promise<boolean> {\n\tif (plantAssessmentID) {\n\t\treturn !!(await sql\n\t\t\t.knex(sql.Table.PlantAssessment)\n\t\t\t.where('plant_assessment_map.id', plantAssessmentID)\n\t\t\t.join(\n\t\t\t\t'maintenance_sequence_step',\n\t\t\t\t'maintenance_sequence_step.assessmentID',\n\t\t\t\t'plant_assessment_map.assessmentID'\n\t\t\t)\n\t\t\t.where('maintenance_sequence_step.active', true)\n\t\t\t.first());\n\t} else {\n\t\treturn !!(await sql\n\t\t\t.knex(sql.Table.Assessment)\n\t\t\t.where('assessment.id', assessmentID)\n\t\t\t.join(\n\t\t\t\t'maintenance_sequence_step',\n\t\t\t\t'maintenance_sequence_step.assessmentID',\n\t\t\t\t'assessment.id'\n\t\t\t)\n\t\t\t.where('maintenance_sequence_step.active', true)\n\t\t\t.first());\n\t}\n}\n\nexport async function tagOutPlantExceedingTolerances(plantID: Plant['id']) {\n\t// tag out the plant if it exceeds tolerances\n\tconst now = core.moment().unix();\n\n\tawait sql.knexMaster.transaction(async (trx) => {\n\t\tconst baseQuery = sql\n\t\t\t.knexMaster('plant_assessment_map')\n\t\t\t.transacting(trx)\n\t\t\t.whereNotNull('plant_assessment_map.businessID')\n\t\t\t.join('plant', 'plant_assessment_map.plantID', 'plant.id')\n\t\t\t.join('assessment', 'plant_assessment_map.assessmentID', 'assessment.id')\n\t\t\t.leftJoin(\n\t\t\t\t'plant_assessment_step_map',\n\t\t\t\t'plant_assessment_step_map.plantAssessmentID',\n\t\t\t\t'plant_assessment_map.id'\n\t\t\t)\n\t\t\t.select(\n\t\t\t\t'plant_assessment_map.plantID',\n\t\t\t\t'assessment.tagOutOnExceedParameter',\n\t\t\t\t'plant_assessment_map.assessmentID',\n\t\t\t\t'plant_assessment_step_map.*',\n\t\t\t\t'plant.lastOdometerReading',\n\t\t\t\t'plant.totalHoursLogged',\n\t\t\t\t'plant.totalFuelUsed'\n\t\t\t)\n\t\t\t.where('plant_assessment_step_map.completed', false)\n\t\t\t.where('plant.taggedOut', false)\n\t\t\t.where('plant_assessment_map.active', true)\n\t\t\t.where('plant.active', true);\n\t\tif (plantID) {\n\t\t\tvoid baseQuery.where('plant.id', plantID);\n\t\t}\n\t\tconst plantAssessmentRows: PlantAssessmentStepInstance[] =\n\t\t\t(await baseQuery) || [];\n\n\t\tfor (const row of plantAssessmentRows) {\n\t\t\tconst overDueParam = maintenanceOverdue(\n\t\t\t\t{\n\t\t\t\t\todometer: (row as any).lastOdometerReading,\n\t\t\t\t\thours: (row as any).totalHoursLogged,\n\t\t\t\t\tfuel: (row as any).totalFuelUsed,\n\t\t\t\t\tdate: now,\n\t\t\t\t},\n\t\t\t\t// check the highest between the tolerance and the standard value\n\t\t\t\t{\n\t\t\t\t\thours: row.hoursDueWithTolerance || row.hoursDue,\n\t\t\t\t\tfuel: row.fuelDueWithTolerance || row.fuelDue,\n\t\t\t\t\todometer: row.odometerDueWithTolerance || row.odometerDue,\n\t\t\t\t\tdate: row.dateDueWithTolerance || row.dateDue,\n\t\t\t\t}\n\t\t\t);\n\t\t\tif (overDueParam) {\n\t\t\t\t// maintenance due, mark it in database\n\n\t\t\t\tawait sql.updateRows(\n\t\t\t\t\tsql.Table.PlantAssessmentStepMap,\n\t\t\t\t\t{\n\t\t\t\t\t\tplantAssessmentID: row.plantAssessmentID,\n\t\t\t\t\t\tmaintenanceSequenceStepID: row.maintenanceSequenceStepID || null,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\toverdue: true,\n\t\t\t\t\t}\n\t\t\t\t);\n\n\t\t\t\tif ((row as any).tagOutOnExceedParameter) {\n\t\t\t\t\tawait tagOutPlant(\n\t\t\t\t\t\trow.plantID,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tid: 0,\n\t\t\t\t\t\t\tisSuperadmin: true,\n\t\t\t\t\t\t} as any,\n\t\t\t\t\t\t'Plant Usage Exceeds Maintenance Tolerance',\n\t\t\t\t\t\tnull,\n\t\t\t\t\t\t(row as any).assessmentID\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tawait redis.delete(redis.CacheDataType.Plant, (row as any).plantID);\n\t\t\t}\n\t\t}\n\t});\n}\n\nexport async function getPlantAssessmentHistory(\n\tuser: User,\n\tplantAssessmentID: PlantAssessment['id']\n): Promise<EventForDisplay[]> {\n\tconst q = getBaseEventLogQuery(user).where(\n\t\t'event_log.plantAssessmentID',\n\t\tplantAssessmentID\n\t);\n\tconst events = await q.select('event_log.*');\n\n\tfor (const e of events) {\n\t\tawait getEventForDisplay(\n\t\t\te,\n\t\t\tuser.language,\n\t\t\tuser.whiteLabelKey,\n\t\t\tuser.measurementSystem,\n\t\t\tuser.timezone\n\t\t);\n\t}\n\n\treturn events;\n}\n" } }}
β βββrome_lsp::session::update_diagnostics{url=file:///Users/joshpike/Projects/aquipa/server/src/components/api/assessment/assessmentUtil.ts}
β β βββrome_js_parser::parse::parse{file_id=FileId(1)}
β β βββ
β βββ
βββ
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/api/assessment/assessmentUtil.ts, range=Range { start: Position { line: 0, character: 0 }, end: Position { line: 0, character: 0 } }, only=None, diagnostics=[]}
β ββ3ms DEBUG rome_lsp::handlers::analysis Suggested actions:
β β []
βββ
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/api/assessment/assessmentUtil.ts, range=Range { start: Position { line: 2408, character: 20 }, end: Position { line: 2408, character: 20 } }, only=None, diagnostics=[]}
β ββ3ms DEBUG rome_rowan::ast::batch pushing change...
β ββ3ms DEBUG rome_rowan::ast::batch pushing change...
β ββ3ms DEBUG rome_rowan::ast::batch pushing change...
β ββ3ms DEBUG rome_rowan::ast::batch changes [CommitChange { parent_depth: 7, parent: Some(JS_FOR_OF_STATEMENT@70148..70754), parent_range: Some((70148, 70754)), new_node_slot: 5, new_node: Some(Node(JS_AWAIT_EXPRESSION@0..138)) }, CommitChange { parent_depth: 6, parent: Some(JS_STATEMENT_LIST@69966..70770), parent_range: Some((69966, 70770)), new_node_slot: 0, new_node: None }, CommitChange { parent_depth: 7, parent: Some(JS_RETURN_STATEMENT@70754..70770), parent_range: Some((70754, 70770)), new_node_slot: 1, new_node: Some(Node(JS_AWAIT_EXPRESSION@0..138)) }]
β ββ10ms DEBUG rome_lsp::handlers::analysis Suggested actions:
β β [CodeAction(CodeAction { title: "Inline variable", kind: Some(CodeActionKind("refactor.inline.rome.correctness.inlineVariable")), diagnostics: None, edit: Some(WorkspaceEdit { changes: Some({Url { scheme: "file", cannot_be_a_base: false, username: "", password: None, host: None, port: None, path: "/Users/joshpike/Projects/aquipa/server/src/components/api/assessment/assessmentUtil.ts", query: None, fragment: None }: [TextEdit { range: Range { start: Position { line: 2408, character: 0 }, end: Position { line: 2408, character: 0 } }, new_text: "\n" }, TextEdit { range: Range { start: Position { line: 2408, character: 1 }, end: Position { line: 2408, character: 1 } }, new_text: "for (" }, TextEdit { range: Range { start: Position { line: 2408, character: 7 }, end: Position { line: 2408, character: 13 } }, new_text: "step" }, TextEdit { range: Range { start: Position { line: 2408, character: 14 }, end: Position { line: 2408, character: 41 } }, new_text: "of" }, TextEdit { range: Range { start: Position { line: 2412, character: 23 }, end: Position { line: 2414, character: 25 } }, new_text: "" }, TextEdit { range: Range { start: Position { line: 2434, character: 8 }, end: Position { line: 2434, character: 13 } }, new_text: "await sql\n\t\t.knex(sql.Table.MaintenanceSequenceStep)\n\t\t.where('active', true)\n\t\t.where('assessmentID', sequenceID)\n\t\t.orderBy('id', 'asc')" }]}), document_changes: None, change_annotations: None }), command: None, is_preferred: Some(true), disabled: None, data: None })]
βββ
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/api/assessment/assessmentUtil.ts, range=Range { start: Position { line: 2417, character: 45 }, end: Position { line: 2417, character: 45 } }, only=None, diagnostics=[]}
β ββ7ms DEBUG rome_lsp::handlers::analysis Suggested actions:
β β []
βββ
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/sql.ts, range=Range { start: Position { line: 0, character: 0 }, end: Position { line: 0, character: 0 } }, only=None, diagnostics=[]}
β ββ0ms ERROR rome_lsp::handlers::analysis error=the file does not exist in the workspace
βββ
ββ73761ms ERROR rome_lsp::utils Error: the file does not exist in the workspace
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/sql.ts, range=Range { start: Position { line: 6, character: 38 }, end: Position { line: 6, character: 38 } }, only=None, diagnostics=[]}
β ββ0ms ERROR rome_lsp::handlers::analysis error=the file does not exist in the workspace
βββ
ββ74332ms ERROR rome_lsp::utils Error: the file does not exist in the workspace
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/api/assessment/assessmentUtil.ts, range=Range { start: Position { line: 0, character: 0 }, end: Position { line: 0, character: 0 } }, only=None, diagnostics=[]}
β ββ3ms DEBUG rome_lsp::handlers::analysis Suggested actions:
β β []
βββ
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/api/assessment/assessmentUtil.ts, range=Range { start: Position { line: 2417, character: 45 }, end: Position { line: 2417, character: 45 } }, only=None, diagnostics=[]}
β ββ3ms DEBUG rome_lsp::handlers::analysis Suggested actions:
β β []
βββ
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/sql.ts, range=Range { start: Position { line: 0, character: 0 }, end: Position { line: 0, character: 0 } }, only=None, diagnostics=[]}
β ββ0ms ERROR rome_lsp::handlers::analysis error=the file does not exist in the workspace
βββ
ββ75605ms ERROR rome_lsp::utils Error: the file does not exist in the workspace
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/sql.ts, range=Range { start: Position { line: 6, character: 38 }, end: Position { line: 6, character: 38 } }, only=None, diagnostics=[]}
β ββ0ms ERROR rome_lsp::handlers::analysis error=the file does not exist in the workspace
βββ
ββ75834ms ERROR rome_lsp::utils Error: the file does not exist in the workspace
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/api/assessment/assessmentUtil.ts, range=Range { start: Position { line: 0, character: 0 }, end: Position { line: 0, character: 0 } }, only=None, diagnostics=[]}
β ββ3ms DEBUG rome_lsp::handlers::analysis Suggested actions:
β β []
βββ
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/api/assessment/assessmentUtil.ts, range=Range { start: Position { line: 2417, character: 45 }, end: Position { line: 2417, character: 45 } }, only=None, diagnostics=[]}
β ββ3ms DEBUG rome_lsp::handlers::analysis Suggested actions:
β β []
βββ
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/sql.ts, range=Range { start: Position { line: 0, character: 0 }, end: Position { line: 0, character: 0 } }, only=None, diagnostics=[]}
β ββ0ms ERROR rome_lsp::handlers::analysis error=the file does not exist in the workspace
βββ
ββ76933ms ERROR rome_lsp::utils Error: the file does not exist in the workspace
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/sql.ts, range=Range { start: Position { line: 6, character: 38 }, end: Position { line: 6, character: 38 } }, only=None, diagnostics=[]}
β ββ0ms ERROR rome_lsp::handlers::analysis error=the file does not exist in the workspace
βββ
ββ77166ms ERROR rome_lsp::utils Error: the file does not exist in the workspace
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/sql.ts, range=Range { start: Position { line: 702, character: 3 }, end: Position { line: 702, character: 3 } }, only=None, diagnostics=[]}
β ββ0ms ERROR rome_lsp::handlers::analysis error=the file does not exist in the workspace
βββ
ββ88259ms ERROR rome_lsp::utils Error: the file does not exist in the workspace
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/sql.ts, range=Range { start: Position { line: 0, character: 0 }, end: Position { line: 728, character: 0 } }, only=Some([CodeActionKind("source.organizeImports.rome")]), diagnostics=[Diagnostic { range: Range { start: Position { line: 337, character: 29 }, end: Position { line: 337, character: 36 } }, severity: Some(Hint), code: Some(Number(7044)), code_description: None, source: Some("ts"), message: "Parameter 'builder' implicitly has an 'any' type, but a better type may be inferred from usage.", related_information: None, tags: None, data: None }, Diagnostic { range: Range { start: Position { line: 357, character: 5 }, end: Position { line: 357, character: 8 } }, severity: Some(Hint), code: Some(Number(7043)), code_description: None, source: Some("ts"), message: "Variable 'sql' implicitly has an 'any' type, but a better type may be inferred from usage.", related_information: None, tags: None, data: None }, Diagnostic { range: Range { start: Position { line: 373, character: 30 }, end: Position { line: 373, character: 36 } }, severity: Some(Hint), code: Some(Number(7044)), code_description: None, source: Some("ts"), message: "Parameter 'object' implicitly has an 'any' type, but a better type may be inferred from usage.", related_information: None, tags: None, data: None }, Diagnostic { range: Range { start: Position { line: 391, character: 14 }, end: Position { line: 391, character: 16 } }, severity: Some(Hint), code: Some(Number(7044)), code_description: None, source: Some("ts"), message: "Parameter 'ms' implicitly has an 'any' type, but a better type may be inferred from usage.", related_information: None, tags: None, data: None }, Diagnostic { range: Range { start: Position { line: 394, character: 32 }, end: Position { line: 394, character: 37 } }, severity: Some(Hint), code: Some(Number(7044)), code_description: None, source: Some("ts"), message: "Parameter 'array' implicitly has an 'any' type, but a better type may be inferred from usage.", related_information: None, tags: None, data: None }, Diagnostic { range: Range { start: Position { line: 685, character: 16 }, end: Position { line: 685, character: 19 } }, severity: Some(Hint), code: Some(Number(7044)), code_description: None, source: Some("ts"), message: "Parameter 'err' implicitly has an 'any' type, but a better type may be inferred from usage.", related_information: None, tags: None, data: None }]}
β ββ0ms ERROR rome_lsp::handlers::analysis error=the file does not exist in the workspace
βββ
ββ91399ms ERROR rome_lsp::utils Error: the file does not exist in the workspace
ββ91494ms WARN tower_lsp Got a textDocument/didSave notification, but it is not implemented
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/sql.ts, range=Range { start: Position { line: 10, character: 28 }, end: Position { line: 10, character: 28 } }, only=None, diagnostics=[]}
β ββ0ms ERROR rome_lsp::handlers::analysis error=the file does not exist in the workspace
βββ
ββ92917ms ERROR rome_lsp::utils Error: the file does not exist in the workspace
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/sql.ts, range=Range { start: Position { line: 17, character: 37 }, end: Position { line: 17, character: 37 } }, only=None, diagnostics=[]}
β ββ0ms ERROR rome_lsp::handlers::analysis error=the file does not exist in the workspace
βββ
ββ95749ms ERROR rome_lsp::utils Error: the file does not exist in the workspace
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/sql.ts, range=Range { start: Position { line: 13, character: 21 }, end: Position { line: 13, character: 21 } }, only=None, diagnostics=[]}
β ββ0ms ERROR rome_lsp::handlers::analysis error=the file does not exist in the workspace
βββ
ββ96823ms ERROR rome_lsp::utils Error: the file does not exist in the workspace
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/sql.ts, range=Range { start: Position { line: 6, character: 38 }, end: Position { line: 6, character: 38 } }, only=None, diagnostics=[]}
β ββ0ms ERROR rome_lsp::handlers::analysis error=the file does not exist in the workspace
βββ
ββ98215ms ERROR rome_lsp::utils Error: the file does not exist in the workspace
βββrome_lsp::handlers::text_document::did_change{url=file:///Users/joshpike/Projects/aquipa/server/src/components/sql.ts, version=20}
β ββ0ms ERROR rome_lsp::handlers::text_document error=the file does not exist in the workspace
βββ
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/sql.ts, range=Range { start: Position { line: 199, character: 11 }, end: Position { line: 199, character: 11 } }, only=None, diagnostics=[]}
β ββ0ms ERROR rome_lsp::handlers::analysis error=the file does not exist in the workspace
βββ
ββ101832ms ERROR rome_lsp::utils Error: the file does not exist in the workspace
βββrome_lsp::handlers::text_document::did_change{url=file:///Users/joshpike/Projects/aquipa/server/src/components/sql.ts, version=21}
β ββ0ms ERROR rome_lsp::handlers::text_document error=the file does not exist in the workspace
βββ
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/sql.ts, range=Range { start: Position { line: 199, character: 13 }, end: Position { line: 199, character: 13 } }, only=None, diagnostics=[]}
β ββ0ms ERROR rome_lsp::handlers::analysis error=the file does not exist in the workspace
βββ
ββ102489ms ERROR rome_lsp::utils Error: the file does not exist in the workspace
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/api/assessment/assessmentUtil.ts, range=Range { start: Position { line: 0, character: 0 }, end: Position { line: 0, character: 0 } }, only=None, diagnostics=[]}
β ββ3ms DEBUG rome_lsp::handlers::analysis Suggested actions:
β β []
βββ
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/api/assessment/assessmentUtil.ts, range=Range { start: Position { line: 2417, character: 45 }, end: Position { line: 2417, character: 45 } }, only=None, diagnostics=[]}
β ββ3ms DEBUG rome_lsp::handlers::analysis Suggested actions:
β β []
βββ
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/api/assessment/assessmentUtil.ts, range=Range { start: Position { line: 2417, character: 68 }, end: Position { line: 2417, character: 68 } }, only=None, diagnostics=[]}
β ββ8ms DEBUG rome_lsp::handlers::analysis Suggested actions:
β β []
βββ
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/api/assessment/assessmentUtil.ts, range=Range { start: Position { line: 2414, character: 28 }, end: Position { line: 2414, character: 28 } }, only=None, diagnostics=[]}
β ββ6ms DEBUG rome_lsp::handlers::analysis Suggested actions:
β β []
βββ
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/api/application/application.ts, range=Range { start: Position { line: 0, character: 0 }, end: Position { line: 0, character: 0 } }, only=None, diagnostics=[]}
β ββ0ms ERROR rome_lsp::handlers::analysis error=the file does not exist in the workspace
βββ
ββ106878ms ERROR rome_lsp::utils Error: the file does not exist in the workspace
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/api/application/application.ts, range=Range { start: Position { line: 81, character: 19 }, end: Position { line: 81, character: 19 } }, only=None, diagnostics=[]}
β ββ0ms ERROR rome_lsp::handlers::analysis error=the file does not exist in the workspace
βββ
ββ107109ms ERROR rome_lsp::utils Error: the file does not exist in the workspace
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/api/application/application.ts, range=Range { start: Position { line: 83, character: 38 }, end: Position { line: 83, character: 38 } }, only=None, diagnostics=[]}
β ββ0ms ERROR rome_lsp::handlers::analysis error=the file does not exist in the workspace
βββ
ββ108594ms ERROR rome_lsp::utils Error: the file does not exist in the workspace
βββrome_lsp::handlers::analysis::code_actions{uri=file:///Users/joshpike/Projects/aquipa/server/src/components/api/application/application.ts, range=Range { start: Position { line: 82, character: 18 }, end: Position { line: 82, character: 18 } }, only=None, diagnostics=[]}
β ββ0ms ERROR rome_lsp::handlers::analysis error=the file does not exist in the workspace
βββ
ββ109595ms ERROR rome_lsp::utils Error: the file does not exist in the workspace
βββrome_lsp::server::initialize{capabilities=ClientCapabilities { workspace: None, text_document: None, window: None, general: None, experimental: None }, client_info=ClientInfo { name: "rome_service", version: Some("11.0.0-nightly.fab5440") }}
β ββ0ms INFO rome_lsp::server Starting Rome Language Server...
βββ
βββrome_lsp::server::rome/rage{params=RageParams}
βββ
ββ175157ms ERROR tower_lsp::transport failed to encode message: failed to encode response: Socket is not connected (os error 57)
It also goes nuts in a similar way when I'm editing a JSON file
It seems documents are out of sync. I think we should provide more logging inside the LSP
same here, the error is quite annoying
Almost fixedππΌ
It is still happening to me. Rome 12.0.0. I have emoji in the file that is crashing tho it is not regular.
It is still happening to me. Rome 12.0.0.
I have emoji in the file that is crashing tho it is not regular.
Could you provide 'rome rage' output?
Let's open a new issue at this point, instead of writing on closed issues.
Each user might have different settings
It is still happening to me. Rome 12.0.0. I have emoji in the file that is crashing tho it is not regular.
Same here, no emoji π¦
Environment information
What happened?
After a while, I get a VSCode message about Rome codeAction request failing (I forgot what it said, but not much), then when I click go to output, I see this:
Expected result
the warning box shouldn't show
Code of Conduct