@elyx-code/editor-ui 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +109 -0
- package/src/App.tsx +31 -0
- package/src/Router.tsx +115 -0
- package/src/__mocks__/defaultModuleMock.ts +1 -0
- package/src/__mocks__/fileMock.ts +1 -0
- package/src/__mocks__/styleMock.ts +1 -0
- package/src/assets/Clock-11.1s-18px.svg +16 -0
- package/src/assets/Clock-11.1s-28px.svg +16 -0
- package/src/assets/authentication.svg +1 -0
- package/src/assets/canvas-backdrop-0.png +0 -0
- package/src/assets/canvas-backdrop-1.png +0 -0
- package/src/assets/canvas-backdrop-2.png +0 -0
- package/src/assets/canvas-backdrop-3.png +0 -0
- package/src/assets/canvas-backdrop-4.png +0 -0
- package/src/assets/canvas-backdrop-5.png +0 -0
- package/src/assets/canvas-backdrop.png +0 -0
- package/src/assets/checkmark-animation.gif +0 -0
- package/src/assets/checkmark-animation.mp4 +0 -0
- package/src/assets/code-formatting/format-black.svg +6 -0
- package/src/assets/code-formatting/format-dark-grey.svg +6 -0
- package/src/assets/code-formatting/format-light-grey.svg +6 -0
- package/src/assets/code-formatting/format-white.svg +6 -0
- package/src/assets/code-formatting/inline-black.svg +5 -0
- package/src/assets/code-formatting/inline-dark-grey.svg +5 -0
- package/src/assets/code-formatting/inline-light-grey.svg +5 -0
- package/src/assets/code-formatting/inline-white.svg +5 -0
- package/src/assets/contained-logo-full-word.png +0 -0
- package/src/assets/cron-job-color.png +0 -0
- package/src/assets/cron-job.png +0 -0
- package/src/assets/database-table-color.png +0 -0
- package/src/assets/database-table.png +0 -0
- package/src/assets/datatype-icons/black/any.svg +1 -0
- package/src/assets/datatype-icons/black/binary.svg +1 -0
- package/src/assets/datatype-icons/black/boolean.svg +3 -0
- package/src/assets/datatype-icons/black/date-time.svg +3 -0
- package/src/assets/datatype-icons/black/definition-entity.svg +6 -0
- package/src/assets/datatype-icons/black/key-file.svg +1 -0
- package/src/assets/datatype-icons/black/list.svg +3 -0
- package/src/assets/datatype-icons/black/null.svg +3 -0
- package/src/assets/datatype-icons/black/number.svg +13 -0
- package/src/assets/datatype-icons/black/project.svg +12 -0
- package/src/assets/datatype-icons/black/sql-program.svg +2 -0
- package/src/assets/datatype-icons/black/text.svg +3 -0
- package/src/assets/datatype-icons/black/unknown.svg +3 -0
- package/src/assets/datatype-icons/black/uuid.svg +4 -0
- package/src/assets/datatype-icons/black/void.svg +1 -0
- package/src/assets/datatype-icons/dark-grey/any.svg +1 -0
- package/src/assets/datatype-icons/dark-grey/boolean.svg +3 -0
- package/src/assets/datatype-icons/dark-grey/date-time.svg +3 -0
- package/src/assets/datatype-icons/dark-grey/definition-entity.svg +6 -0
- package/src/assets/datatype-icons/dark-grey/list.svg +3 -0
- package/src/assets/datatype-icons/dark-grey/null.svg +3 -0
- package/src/assets/datatype-icons/dark-grey/number.svg +13 -0
- package/src/assets/datatype-icons/dark-grey/project.svg +12 -0
- package/src/assets/datatype-icons/dark-grey/sql-program.svg +2 -0
- package/src/assets/datatype-icons/dark-grey/text.svg +3 -0
- package/src/assets/datatype-icons/dark-grey/unknown.svg +3 -0
- package/src/assets/datatype-icons/dark-grey/uuid.svg +4 -0
- package/src/assets/datatype-icons/dark-grey/void.svg +1 -0
- package/src/assets/datatype-icons/light-grey/any.svg +1 -0
- package/src/assets/datatype-icons/light-grey/boolean.svg +3 -0
- package/src/assets/datatype-icons/light-grey/date-time.svg +3 -0
- package/src/assets/datatype-icons/light-grey/definition-entity.svg +6 -0
- package/src/assets/datatype-icons/light-grey/list.svg +3 -0
- package/src/assets/datatype-icons/light-grey/null.svg +3 -0
- package/src/assets/datatype-icons/light-grey/number.svg +13 -0
- package/src/assets/datatype-icons/light-grey/project.svg +12 -0
- package/src/assets/datatype-icons/light-grey/sql-program.svg +2 -0
- package/src/assets/datatype-icons/light-grey/text.svg +3 -0
- package/src/assets/datatype-icons/light-grey/unknown.svg +3 -0
- package/src/assets/datatype-icons/light-grey/uuid.svg +4 -0
- package/src/assets/datatype-icons/light-grey/void.svg +1 -0
- package/src/assets/edit.png +0 -0
- package/src/assets/execution.svg +13 -0
- package/src/assets/favicon.svg +14 -0
- package/src/assets/file-search.svg +1 -0
- package/src/assets/http-endpoint.png +0 -0
- package/src/assets/image-input-placeholder.png +0 -0
- package/src/assets/logo-full-word-white.png +0 -0
- package/src/assets/logo-full-word.png +0 -0
- package/src/assets/password.svg +85 -0
- package/src/assets/pencil.png +0 -0
- package/src/assets/publish-project-rich-icon-2.svg +1 -0
- package/src/assets/publish-project-rich-icon.svg +1 -0
- package/src/assets/relational-database.png +0 -0
- package/src/assets/resources.svg +3 -0
- package/src/assets/resume-icon-14px.png +0 -0
- package/src/assets/server.png +0 -0
- package/src/assets/small-status/checkmark.svg +4 -0
- package/src/assets/small-status/error.svg +4 -0
- package/src/assets/small-status/loading.svg +4 -0
- package/src/assets/small-status/skipped.svg +11 -0
- package/src/assets/sql-connection-config.svg +1 -0
- package/src/assets/sql-row-transformer.svg +1 -0
- package/src/assets/ssl-certificate-config.svg +1 -0
- package/src/assets/sync.svg +1 -0
- package/src/assets/testing-logic-icon.svg +1 -0
- package/src/assets/versions.svg +25 -0
- package/src/assets/visual-programming-icon.svg +1 -0
- package/src/assets/warning-sign-24px.png +0 -0
- package/src/auth/index.ts +318 -0
- package/src/components/DialogLoader.tsx +94 -0
- package/src/components/EntityDialogHeader.tsx +110 -0
- package/src/components/EntityDialogSectionHeader.tsx +214 -0
- package/src/components/GalleryAddExternalIntegrationInfoDialog.tsx +87 -0
- package/src/components/GenerateProjectStartingLogicPromptDialog.tsx +281 -0
- package/src/components/LegacyRouteRedirector.tsx +55 -0
- package/src/components/ProPlanChip.tsx +23 -0
- package/src/components/ReportBugDialog.tsx +412 -0
- package/src/components/RequestIntegrationAccessDialog.tsx +261 -0
- package/src/components/UseTemplateProjectDialog.tsx +193 -0
- package/src/components/WorkspaceLayout.tsx +152 -0
- package/src/components/animated-svg/AnimatedCheckmark.tsx +41 -0
- package/src/components/animated-svg/AnimatedCrossmark.tsx +51 -0
- package/src/components/animated-svg/AnimatedEmailSending.tsx +38 -0
- package/src/components/animated-svg/AnimatedLoading.tsx +72 -0
- package/src/components/animated-svg/animated-svg.css +239 -0
- package/src/components/canvas/Canvas.tsx +16 -0
- package/src/components/canvas/CreateEntityMenu.tsx +2020 -0
- package/src/components/canvas/canvas.css +10 -0
- package/src/components/canvas/create-entity-menu.css +579 -0
- package/src/components/canvas-search/CanvasSearch.tsx +501 -0
- package/src/components/canvas-search/canvas-search.css +126 -0
- package/src/components/canvas-settings-menu/CanvasSettingsMenuButton.tsx +515 -0
- package/src/components/canvas-settings-menu/canvas-settings-menu.css +96 -0
- package/src/components/circular-image-upload/CircularImageUpload.tsx +113 -0
- package/src/components/circular-image-upload/circular-image-upload.css +69 -0
- package/src/components/costs/CostsDialog.tsx +459 -0
- package/src/components/data-type/DataTypeBuilder.tsx +3127 -0
- package/src/components/data-type/data-type-builder.css +45 -0
- package/src/components/dialogs/BetaAcknowledgeDialog.tsx +43 -0
- package/src/components/dialogs/ComplexDataDialog.tsx +458 -0
- package/src/components/dialogs/CronBuilderDialog.tsx +2145 -0
- package/src/components/dialogs/ExternalIntegrationConnections.tsx +565 -0
- package/src/components/dialogs/JsonEditorDialog.tsx +1392 -0
- package/src/components/dialogs/StringEditorDialog.tsx +268 -0
- package/src/components/dialogs/argument-declaration/ArgumentDeclaration.tsx +1167 -0
- package/src/components/dialogs/argument-declaration/ArgumentDeclarationDialogContent.tsx +128 -0
- package/src/components/dialogs/beta-dialog.css +165 -0
- package/src/components/dialogs/condition/Condition.tsx +431 -0
- package/src/components/dialogs/condition/ConditionDialogContent.tsx +126 -0
- package/src/components/dialogs/definition-entity/DefinitionEntityDialogContent.tsx +973 -0
- package/src/components/dialogs/function-call/FunctionCall.tsx +442 -0
- package/src/components/dialogs/function-call/FunctionCallDialogContent.tsx +126 -0
- package/src/components/dialogs/function-declaration/FunctionDeclaration.tsx +926 -0
- package/src/components/dialogs/function-declaration/FunctionDeclarationDialogContent.tsx +124 -0
- package/src/components/dialogs/generating-project-starting-logic-overlay/GeneratingProjectStartingLogicOverlay.tsx +176 -0
- package/src/components/dialogs/generating-project-starting-logic-overlay/generating-project-starting-logic-overlay.css +13 -0
- package/src/components/dialogs/global-event/GlobalEvent.tsx +475 -0
- package/src/components/dialogs/global-event/GlobalEventDialogContent.tsx +126 -0
- package/src/components/dialogs/help/HelpDialog.tsx +217 -0
- package/src/components/dialogs/help/HelpDilalogHomeContent.tsx +178 -0
- package/src/components/dialogs/help/help-dialog.css +116 -0
- package/src/components/dialogs/help/help-icon/HelpIconButton.tsx +41 -0
- package/src/components/dialogs/help/help-icon/help-icon.css +9 -0
- package/src/components/dialogs/input-map/InputMap.tsx +635 -0
- package/src/components/dialogs/input-map/InputMapDialogContent.tsx +126 -0
- package/src/components/dialogs/json-editor-dialog.css +4 -0
- package/src/components/dialogs/loop/Loop.tsx +650 -0
- package/src/components/dialogs/loop/LoopDialogContent.tsx +122 -0
- package/src/components/dialogs/operation/Operation.tsx +440 -0
- package/src/components/dialogs/operation/OperationDialogContent.tsx +126 -0
- package/src/components/dialogs/output-map/OutputMap.tsx +536 -0
- package/src/components/dialogs/output-map/OutputMapDialogContent.tsx +126 -0
- package/src/components/dialogs/property/Property.tsx +1490 -0
- package/src/components/dialogs/property/PropertyDialogContent.tsx +106 -0
- package/src/components/dialogs/search-statement/ColumnSelector.tsx +334 -0
- package/src/components/dialogs/search-statement/ConditionBuilder.tsx +750 -0
- package/src/components/dialogs/search-statement/DataAggregationSection.tsx +621 -0
- package/src/components/dialogs/search-statement/DataSourceSelection.tsx +734 -0
- package/src/components/dialogs/search-statement/EntityMetadataSection.tsx +135 -0
- package/src/components/dialogs/search-statement/FilterConditionsSection.tsx +151 -0
- package/src/components/dialogs/search-statement/InlineInputMap.tsx +153 -0
- package/src/components/dialogs/search-statement/LiteralValue.tsx +616 -0
- package/src/components/dialogs/search-statement/MainSourceAndInputsSection.tsx +271 -0
- package/src/components/dialogs/search-statement/NestedSearchStatementBuilder.tsx +170 -0
- package/src/components/dialogs/search-statement/OutputFormatSection.tsx +1779 -0
- package/src/components/dialogs/search-statement/ResultsSection.tsx +344 -0
- package/src/components/dialogs/search-statement/SearchStatementBuilder.tsx +251 -0
- package/src/components/dialogs/search-statement/SearchStatementDialogContent.tsx +398 -0
- package/src/components/dialogs/search-statement/ValueSelector.tsx +766 -0
- package/src/components/dialogs/search-statement/search-statement-context.tsx +1630 -0
- package/src/components/dialogs/search-statement/search-statement-dialog.css +56 -0
- package/src/components/dialogs/search-statement/test.sql +111 -0
- package/src/components/dialogs/value-descriptor/ValueDescriptor.tsx +824 -0
- package/src/components/dialogs/value-descriptor/ValueDescriptorDialogContent.tsx +124 -0
- package/src/components/dialogs/variable-declaration/VariableDeclaration.tsx +836 -0
- package/src/components/dialogs/variable-declaration/VariableDeclarationDialogContent.tsx +106 -0
- package/src/components/dialogs/variable-instance/VariableInstance.tsx +443 -0
- package/src/components/dialogs/variable-instance/VariableInstanceDialogContent.tsx +124 -0
- package/src/components/draggable-entity-card/ArgumentDeclaration.tsx +736 -0
- package/src/components/draggable-entity-card/CollapseEntityButton.tsx +170 -0
- package/src/components/draggable-entity-card/ConditionCard.tsx +1062 -0
- package/src/components/draggable-entity-card/ConnectionDeleteButton.tsx +309 -0
- package/src/components/draggable-entity-card/DataTypeIcon.tsx +624 -0
- package/src/components/draggable-entity-card/DraggableEntityCard.tsx +617 -0
- package/src/components/draggable-entity-card/ErrorMapProperty.tsx +464 -0
- package/src/components/draggable-entity-card/EventCard.tsx +700 -0
- package/src/components/draggable-entity-card/ExecutionInProgressValue.tsx +327 -0
- package/src/components/draggable-entity-card/FunctionDeclarationCard.tsx +819 -0
- package/src/components/draggable-entity-card/InputMapProperty.tsx +1067 -0
- package/src/components/draggable-entity-card/InternalCall.tsx +978 -0
- package/src/components/draggable-entity-card/InternalCallExecutionNode.tsx +643 -0
- package/src/components/draggable-entity-card/LogicScopeCallerNode.tsx +262 -0
- package/src/components/draggable-entity-card/LoopCard.tsx +791 -0
- package/src/components/draggable-entity-card/MainValueInput.tsx +523 -0
- package/src/components/draggable-entity-card/MainValueOutput.tsx +458 -0
- package/src/components/draggable-entity-card/MethodDeclaration.tsx +1088 -0
- package/src/components/draggable-entity-card/NestedCondition.tsx +1025 -0
- package/src/components/draggable-entity-card/OutputMapProperty.tsx +843 -0
- package/src/components/draggable-entity-card/PassthroughEntityCard.tsx +1247 -0
- package/src/components/draggable-entity-card/ReturnedError.tsx +549 -0
- package/src/components/draggable-entity-card/SmallSuccessFailureNodes.tsx +523 -0
- package/src/components/draggable-entity-card/SuccessFailureNodes.tsx +509 -0
- package/src/components/draggable-entity-card/TestEntityButton.tsx +946 -0
- package/src/components/draggable-entity-card/TestMenu.tsx +523 -0
- package/src/components/draggable-entity-card/TestMenuValidationDropdown.tsx +84 -0
- package/src/components/draggable-entity-card/UnreachableMarker.tsx +114 -0
- package/src/components/draggable-entity-card/VariableCard.tsx +1577 -0
- package/src/components/draggable-entity-card/VariableScopeMarker.tsx +117 -0
- package/src/components/draggable-entity-card/collapse-entity-button.css +44 -0
- package/src/components/draggable-entity-card/definition-entity/DefinitionEntityCard.tsx +1181 -0
- package/src/components/draggable-entity-card/definition-entity/DefinitionEntityIcon.tsx +36 -0
- package/src/components/draggable-entity-card/definition-entity/DefinitionEntityProperty.tsx +478 -0
- package/src/components/draggable-entity-card/definition-entity/DynamicFooterActions.tsx +112 -0
- package/src/components/draggable-entity-card/definition-entity/actions/external-integration-connection/ExportCredentialsFooterAction.tsx +461 -0
- package/src/components/draggable-entity-card/definition-entity/actions/external-integration-connection/RestablishConnectionFooterAction.tsx +199 -0
- package/src/components/draggable-entity-card/definition-entity/actions/external-integration-connection/restablish-connection-footer-action.css +85 -0
- package/src/components/draggable-entity-card/definition-entity/actions/google-drive/GoogleDriveFilePickerAPIFooterAction.tsx +277 -0
- package/src/components/draggable-entity-card/definition-entity/actions/google-drive/google-drive-file-picker-api-footer-action.css +107 -0
- package/src/components/draggable-entity-card/definition-entity/actions/persisted-entity/DatabaseFooterAction.tsx +452 -0
- package/src/components/draggable-entity-card/definition-entity/actions/persisted-entity/database-footer-action.css +86 -0
- package/src/components/draggable-entity-card/definition-entity/definition-entity-card.css +17 -0
- package/src/components/draggable-entity-card/draggable-entity-card.css +1140 -0
- package/src/components/draggable-entity-card/entity-locked-icon/EntityLockedIcon.tsx +133 -0
- package/src/components/draggable-entity-card/entity-locked-icon/entity-locked.css +8 -0
- package/src/components/draggable-entity-card/expand-properties-icon-button/ExpandPropertiesIconButton.tsx +84 -0
- package/src/components/draggable-entity-card/expand-properties-icon-button/expand-properties-icon-button.css +21 -0
- package/src/components/draggable-entity-card/implement-entity-icon/ImplementEntityIcon.tsx +74 -0
- package/src/components/draggable-entity-card/implement-entity-icon/implement-entity-icon.css +13 -0
- package/src/components/draggable-entity-card/logic-error/LogicErrorIconMenu.tsx +424 -0
- package/src/components/draggable-entity-card/logic-error/logic-error.css +23 -0
- package/src/components/draggable-entity-card/new-card-input-button/NewCardInputButton.tsx +193 -0
- package/src/components/draggable-entity-card/new-card-input-button/NewDynamicInputButton.tsx +214 -0
- package/src/components/draggable-entity-card/new-card-input-button/new-card-input-button.css +71 -0
- package/src/components/draggable-entity-card/new-card-output-button/NewCardOutputButton.tsx +192 -0
- package/src/components/draggable-entity-card/new-card-output-button/new-card-output-button.css +71 -0
- package/src/components/draggable-entity-card/termination-statement/TerminationStatementCard.tsx +1543 -0
- package/src/components/draggable-entity-card/termination-statement/termination-statement-card.css +17 -0
- package/src/components/draggable-entity-card/test-entity-button.css +55 -0
- package/src/components/draggable-entity-card/test-menu.css +181 -0
- package/src/components/draggable-entity-card/unreachable-marker.css +43 -0
- package/src/components/draggable-entity-card/variable-scope-marker.css +22 -0
- package/src/components/dynamic-value/DynamicValue.tsx +2395 -0
- package/src/components/dynamic-value/DynamicValueEntry.tsx +1957 -0
- package/src/components/dynamic-value/dynamic-value.css +230 -0
- package/src/components/editor/ElyxMonacoEditor.tsx +38 -0
- package/src/components/entity-error/EntityErrorListItem.tsx +47 -0
- package/src/components/entity-error/entity-error.css +198 -0
- package/src/components/entity-icon/EntityIcon.tsx +292 -0
- package/src/components/entity-icon/entity-icon.css +39 -0
- package/src/components/gallery-card/CreateNewProject.tsx +222 -0
- package/src/components/gallery-card/GalleryCard.tsx +171 -0
- package/src/components/gallery-card/MarketplaceCard.tsx +87 -0
- package/src/components/gallery-card/ProjectDuplicationCard.tsx +575 -0
- package/src/components/gallery-card/gallery-card.css +25 -0
- package/src/components/notifications/NotificationsIconButton.tsx +124 -0
- package/src/components/notifications/NotificationsPanel.tsx +385 -0
- package/src/components/notifications/notifications.css +189 -0
- package/src/components/online-users/LocalOnlineUsers.tsx +175 -0
- package/src/components/online-users/PageOnlineUsers.tsx +297 -0
- package/src/components/online-users/online-users.css +72 -0
- package/src/components/page-backdrop/PageBackdrop.tsx +8 -0
- package/src/components/page-backdrop/page-backdrop.css +7 -0
- package/src/components/project-configuration/DeleteProjectConfirmationDialog.tsx +134 -0
- package/src/components/project-configuration/ProjectConfigurationDialog.tsx +972 -0
- package/src/components/project-configuration/ProjectDataForm.tsx +121 -0
- package/src/components/project-configuration/UnpublishProjectConfirmationDialog.tsx +162 -0
- package/src/components/project-configuration/project-configuration-content.css +209 -0
- package/src/components/project-name/ProjectName.tsx +2025 -0
- package/src/components/project-name/project-name.css +599 -0
- package/src/components/publishing/Publication.tsx +133 -0
- package/src/components/publishing/history/PublicationHistoryContent.tsx +414 -0
- package/src/components/publishing/history/PublicationHistoryDialog.tsx +234 -0
- package/src/components/publishing/preview/PublicationPreviewDialog.tsx +1158 -0
- package/src/components/publishing/preview/PublishingPriceForecast.tsx +160 -0
- package/src/components/publishing/preview/PublishingResourcesDetails.tsx +91 -0
- package/src/components/publishing/publication-sequence/PublishingSequenceContent.tsx +375 -0
- package/src/components/publishing/publication-sequence/PublishingSequenceDialog.tsx +344 -0
- package/src/components/publishing/publishing-dialog.css +142 -0
- package/src/components/publishing/utils.ts +227 -0
- package/src/components/resources/ResourcesDialog.tsx +591 -0
- package/src/components/resources/UpgradeBanner.tsx +102 -0
- package/src/components/resources/codebase/CodebaseDetails.tsx +156 -0
- package/src/components/resources/cron-job/CronJobsList.tsx +532 -0
- package/src/components/resources/functions/FunctionsList.tsx +454 -0
- package/src/components/resources/http-api/HttpAPI.tsx +566 -0
- package/src/components/resources/http-api/HttpAPIClientModule.tsx +37 -0
- package/src/components/resources/logs/LogsViewer.tsx +768 -0
- package/src/components/resources/query.ts +74 -0
- package/src/components/resources/relational-database/DatabaseTable.tsx +905 -0
- package/src/components/resources/relational-database/RelationalDatabase.tsx +83 -0
- package/src/components/resources/relational-database/RelationalDatabaseSecrets.tsx +361 -0
- package/src/components/resources/resources-dialog.css +74 -0
- package/src/components/test-relational-database/DatabaseTable.tsx +913 -0
- package/src/components/test-relational-database/TestDatabaseDialogContent.tsx +670 -0
- package/src/components/test-relational-database/query.ts +74 -0
- package/src/components/toolbar/ToolBar.tsx +236 -0
- package/src/components/toolbar/toolbar.css +78 -0
- package/src/components/transaction-history/TransactionHistoryDialog.tsx +268 -0
- package/src/components/user/CurrentUserAvatar.tsx +65 -0
- package/src/components/user/UserChip.tsx +62 -0
- package/src/components/user/user.css +39 -0
- package/src/components/user-profile/ChangePasswordForm.tsx +67 -0
- package/src/components/user-profile/OwnUserProfileContent.tsx +665 -0
- package/src/components/user-profile/PublicUserProfileContent.tsx +99 -0
- package/src/components/user-profile/UserDataForm.tsx +75 -0
- package/src/components/user-profile/UserProfileDialog.tsx +110 -0
- package/src/components/user-profile/user-profile-content.css +25 -0
- package/src/config.ts +130 -0
- package/src/globals.d.ts +13 -0
- package/src/index.html +27 -0
- package/src/index.tsx +23 -0
- package/src/lib/badge/Badge.tsx +35 -0
- package/src/lib/badge/badge.css +32 -0
- package/src/lib/button/Button.tsx +129 -0
- package/src/lib/button/button.css +145 -0
- package/src/lib/canvas/canvas-undo-redo.ts +263 -0
- package/src/lib/canvas/defs.ts +170 -0
- package/src/lib/canvas/index.test.ts +189 -0
- package/src/lib/canvas/index.ts +6999 -0
- package/src/lib/canvas/utils.ts +59 -0
- package/src/lib/card/Card.tsx +62 -0
- package/src/lib/card/LoadingCard.tsx +82 -0
- package/src/lib/card/card.css +259 -0
- package/src/lib/chip/Chip.tsx +79 -0
- package/src/lib/chip/chip.css +0 -0
- package/src/lib/dialog/Dialog.tsx +122 -0
- package/src/lib/dialog/SmallDialog.tsx +61 -0
- package/src/lib/dialog/dialog.css +40 -0
- package/src/lib/display-data-structure/index.tsx +21 -0
- package/src/lib/dropdown/CanvasDropdownMenuCard.tsx +68 -0
- package/src/lib/dropdown/CanvasDropdownMenuCardOption.tsx +136 -0
- package/src/lib/dropdown/DropdownButton.tsx +104 -0
- package/src/lib/dropdown/DropdownMenuCard.tsx +324 -0
- package/src/lib/dropdown/DropdownMenuPopup.tsx +27 -0
- package/src/lib/dropdown/dropdown-button.css +76 -0
- package/src/lib/dropdown/dropdown-menu.css +151 -0
- package/src/lib/json-editor/RawJsonEditor.tsx +137 -0
- package/src/lib/json-editor/json-editor.css +35 -0
- package/src/lib/loader/Loader.tsx +120 -0
- package/src/lib/loader/loader.css +38 -0
- package/src/lib/pagination/Pagination.tsx +64 -0
- package/src/lib/popup/CanvasPopupBaseComponent.tsx +103 -0
- package/src/lib/popup/Popup.tsx +243 -0
- package/src/lib/popup/popup.css +16 -0
- package/src/lib/table/RowForm.tsx +301 -0
- package/src/lib/table/Table.tsx +1069 -0
- package/src/lib/table/table.css +249 -0
- package/src/lib/table/types.ts +108 -0
- package/src/lib/text-area/TextArea.tsx +183 -0
- package/src/lib/text-area/text-area.css +156 -0
- package/src/lib/text-field/TextField.tsx +218 -0
- package/src/lib/text-field/index.ts +8 -0
- package/src/lib/text-field/text-field.css +201 -0
- package/src/lib/tooltip/Tooltip.tsx +24 -0
- package/src/lib/tooltip/tooltip.css +17 -0
- package/src/localization/index.ts +47 -0
- package/src/main.css +343 -0
- package/src/pages/Auth.tsx +848 -0
- package/src/pages/Editor.tsx +883 -0
- package/src/pages/ErrorPage.tsx +179 -0
- package/src/pages/Gallery.tsx +1693 -0
- package/src/pages/NewPaymentMethodCallback.tsx +53 -0
- package/src/pages/NotFoundPage.tsx +126 -0
- package/src/pages/PricingPlans.tsx +155 -0
- package/src/pages/auth.css +304 -0
- package/src/pages/gallery.css +421 -0
- package/src/payments/index.ts +187 -0
- package/src/popup-notification/index.ts +90 -0
- package/src/services/database/index.ts +1 -0
- package/src/services/database/utils.ts +1301 -0
- package/src/services/editor/CanvasElement.tsx +2934 -0
- package/src/services/editor/CanvasElementConnectionDeleteButton.ts +204 -0
- package/src/services/editor/CanvasPopup.tsx +749 -0
- package/src/services/editor/EditorService.ts +8157 -0
- package/src/services/editor/area.ts +1312 -0
- package/src/services/editor/connections.ts +1019 -0
- package/src/services/editor/create/condition.ts +25 -0
- package/src/services/editor/create/definition-entity.ts +29 -0
- package/src/services/editor/create/function-call.ts +25 -0
- package/src/services/editor/create/global-event.ts +33 -0
- package/src/services/editor/create/loop.ts +25 -0
- package/src/services/editor/create/operation.ts +30 -0
- package/src/services/editor/create/utils.ts +140 -0
- package/src/services/editor/create/variable-declaration.ts +135 -0
- package/src/services/editor/create/variable-instance.ts +100 -0
- package/src/services/editor/editor-ui-extensions-context.ts +43 -0
- package/src/services/editor/entities-metadata.json +9310 -0
- package/src/services/editor/icons.ts +1093 -0
- package/src/services/editor/index.ts +1 -0
- package/src/services/editor/layout.ts +102 -0
- package/src/services/editor/modules/built-in-function-implementations/base.ts +14 -0
- package/src/services/editor/modules/built-in-function-implementations/create-persisted-entity/index.ts +56 -0
- package/src/services/editor/modules/built-in-function-implementations/delete-persisted-entity/index.ts +55 -0
- package/src/services/editor/modules/built-in-function-implementations/index.ts +4 -0
- package/src/services/editor/modules/built-in-function-implementations/update-persisted-entity/index.ts +56 -0
- package/src/services/editor/modules/operations-implementations/external-integrations/google-drive/get-files.ts +183 -0
- package/src/services/editor/modules/operations-implementations/external-integrations/google-drive/list-drives.ts +124 -0
- package/src/services/editor/modules/operations-implementations/external-integrations/google-drive/list-root-folders.ts +125 -0
- package/src/services/editor/modules/operations-implementations/external-integrations/google-drive/smart-fetch-document.ts +702 -0
- package/src/services/editor/modules/operations-implementations/external-integrations/google-drive/upload-document.ts +535 -0
- package/src/services/editor/modules/operations-implementations/external-integrations/google-gemini/generate-content.ts +193 -0
- package/src/services/editor/modules/operations-implementations/external-integrations/google-mail/get-emails.ts +586 -0
- package/src/services/editor/modules/operations-implementations/external-integrations/google-mail/send-email.ts +386 -0
- package/src/services/editor/modules/operations-implementations/external-integrations/index.ts +12 -0
- package/src/services/editor/modules/operations-implementations/external-integrations/slack/channels.ts +240 -0
- package/src/services/editor/modules/operations-implementations/external-integrations/slack/messages.ts +210 -0
- package/src/services/editor/modules/operations-implementations/external-integrations/slack/replies.ts +200 -0
- package/src/services/editor/modules/operations-implementations/external-integrations/slack/send-message.ts +177 -0
- package/src/services/editor/modules/operations-implementations/index.ts +1 -0
- package/src/services/editor/modules/search-node-implementation/index.ts +42 -0
- package/src/services/editor/modules/sql-migrations-generation.tsx +1054 -0
- package/src/services/editor/publication/publication.ts +578 -0
- package/src/services/editor/ui.ts +1348 -0
- package/src/services/editor/utils.ts +5868 -0
- package/src/services/editor/value-store.ts +619 -0
- package/src/services/execution/built-in-function-implementations.ts +422 -0
- package/src/services/execution/index.ts +4747 -0
- package/src/services/execution/logic.ts +121 -0
- package/src/services/execution/test-instance.tsx +2296 -0
- package/src/services/execution/utils.ts +33 -0
- package/src/services/execution/value-resolution.test.ts +424 -0
- package/src/services/execution/value-resolution.ts +4087 -0
- package/src/services/integrations/ExternalIntegrationsService.ts +439 -0
- package/src/services/integrations/api.ts +175 -0
- package/src/services/local-relational-database/idb_helper.ts +66 -0
- package/src/services/local-relational-database/index.ts +3308 -0
- package/src/services/local-relational-database/utils.ts +403 -0
- package/src/services/notifications/index.ts +525 -0
- package/src/services/user/index.ts +144 -0
- package/src/setupTests.ts +1 -0
- package/src/socket/socket.ts +248 -0
- package/src/socket/utils.ts +10 -0
- package/src/store/workspace.ts +12 -0
- package/src/theme.ts +19 -0
- package/src/utils/DOM.ts +39 -0
- package/src/utils/date.ts +169 -0
- package/src/utils/index.ts +158 -0
- package/src/utils/react.tsx +679 -0
- package/src/utils/testing.ts +103 -0
|
@@ -0,0 +1,3308 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BUILT_IN_BASE_ENTITY_IDS,
|
|
3
|
+
BaseEntityNames,
|
|
4
|
+
EntityType,
|
|
5
|
+
ProjectStateEvents,
|
|
6
|
+
SearchState,
|
|
7
|
+
checkHasBaseEntity,
|
|
8
|
+
checkIsEmptyValue,
|
|
9
|
+
getColumnProperties,
|
|
10
|
+
getDatabaseEntity,
|
|
11
|
+
lowercaseFirstLetter,
|
|
12
|
+
resolvePersistedDefinitionEntityDatabaseEntity,
|
|
13
|
+
searchStatementDefs,
|
|
14
|
+
PrimitiveTypes,
|
|
15
|
+
InputMapState,
|
|
16
|
+
DefinitionEntityState,
|
|
17
|
+
PropertyState,
|
|
18
|
+
resolveEntityName,
|
|
19
|
+
toCamelCase,
|
|
20
|
+
toPascalCase,
|
|
21
|
+
SQLAST as SQLASTLib,
|
|
22
|
+
searchStatementState as searchStatementStateLib,
|
|
23
|
+
searchStatementUtils
|
|
24
|
+
} from '@elyx-code/project-logic-tree';
|
|
25
|
+
import {
|
|
26
|
+
Connection as BaseConnection,
|
|
27
|
+
IDataBase,
|
|
28
|
+
IInsertQuery,
|
|
29
|
+
IOrderQuery,
|
|
30
|
+
IRemoveQuery,
|
|
31
|
+
ISelectQuery,
|
|
32
|
+
ITable,
|
|
33
|
+
IUpdateQuery
|
|
34
|
+
} from 'jsstore';
|
|
35
|
+
import {
|
|
36
|
+
persistedDefinitionEntityToDBTableConfig,
|
|
37
|
+
resolveForeignKeyInfo
|
|
38
|
+
} from './utils';
|
|
39
|
+
import { initLocalDBConnection, initLocalIndexedDatabase } from './idb_helper';
|
|
40
|
+
import {
|
|
41
|
+
fromOrderByClauseToJsStore,
|
|
42
|
+
fromWhereClauseToJsStoreWhereObject,
|
|
43
|
+
getJoinClausesFromSelectStatement,
|
|
44
|
+
getAllSelectedColumnNamesFromSelectClause
|
|
45
|
+
} from '../database';
|
|
46
|
+
import { EditorService } from '../editor';
|
|
47
|
+
import { IDynamicValue } from '../editor/value-store';
|
|
48
|
+
|
|
49
|
+
const { ColumnRef, WhereStatement, DataSource, AggregationStatement } =
|
|
50
|
+
searchStatementStateLib;
|
|
51
|
+
|
|
52
|
+
const {
|
|
53
|
+
SearchStatementNodeType,
|
|
54
|
+
AggregationStatementType,
|
|
55
|
+
SortStatementDirection,
|
|
56
|
+
WhereStatementOperator,
|
|
57
|
+
SQLFunctionCategory,
|
|
58
|
+
DataSourceType
|
|
59
|
+
} = searchStatementDefs;
|
|
60
|
+
|
|
61
|
+
type AggregationStatement = searchStatementStateLib.AggregationStatement;
|
|
62
|
+
type ColumnRef = searchStatementStateLib.ColumnRef;
|
|
63
|
+
type DataSource = searchStatementStateLib.DataSource;
|
|
64
|
+
type SearchStatementState = searchStatementStateLib.SearchStatementState;
|
|
65
|
+
type WhereStatement = searchStatementStateLib.WhereStatement;
|
|
66
|
+
type SearchNode = searchStatementStateLib.SearchNode;
|
|
67
|
+
type FunctionCall = searchStatementStateLib.FunctionCall;
|
|
68
|
+
|
|
69
|
+
type Program = SQLASTLib.Program;
|
|
70
|
+
type SelectStmt = SQLASTLib.SelectStmt;
|
|
71
|
+
type LimitClause = SQLASTLib.LimitClause;
|
|
72
|
+
type WhereStatementOperator = searchStatementDefs.WhereStatementOperator;
|
|
73
|
+
type SortStatementDirection = searchStatementDefs.SortStatementDirection;
|
|
74
|
+
type AggregationStatementType = searchStatementDefs.AggregationStatementType;
|
|
75
|
+
|
|
76
|
+
export async function describeExisting(
|
|
77
|
+
dbIdentifier: string
|
|
78
|
+
): Promise<IDataBase | null> {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const request = indexedDB.open(dbIdentifier);
|
|
81
|
+
|
|
82
|
+
request.onsuccess = (event) => {
|
|
83
|
+
const db = (event.target as any).result;
|
|
84
|
+
// console.log('Object store names:', [...db.objectStoreNames]);
|
|
85
|
+
|
|
86
|
+
const finalDef: IDataBase = {
|
|
87
|
+
name: dbIdentifier,
|
|
88
|
+
tables: [] as ITable[],
|
|
89
|
+
version: db.version
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Iterate over each object store to inspect its properties
|
|
93
|
+
for (const storeName of db.objectStoreNames) {
|
|
94
|
+
const transaction = db.transaction(storeName, 'readonly');
|
|
95
|
+
const objectStore = transaction.objectStore(storeName);
|
|
96
|
+
// console.log(`Object store: ${storeName}`);
|
|
97
|
+
// console.log(' Key path:', objectStore.keyPath);
|
|
98
|
+
// console.log(' Auto increment:', objectStore.autoIncrement);
|
|
99
|
+
// console.log(' Indexes:', [...objectStore.indexNames]);
|
|
100
|
+
|
|
101
|
+
const columns: ITable['columns'] = {};
|
|
102
|
+
|
|
103
|
+
// Iterate over each index to inspect its properties
|
|
104
|
+
for (const indexName of objectStore.indexNames) {
|
|
105
|
+
const index = objectStore.index(indexName);
|
|
106
|
+
// console.log(` Index: ${indexName}`);
|
|
107
|
+
// console.log(' Key path:', index.keyPath);
|
|
108
|
+
// console.log(' Unique:', index.unique);
|
|
109
|
+
|
|
110
|
+
columns[indexName] = {
|
|
111
|
+
primaryKey: index.keyPath === objectStore.keyPath,
|
|
112
|
+
autoIncrement: index.autoIncrement,
|
|
113
|
+
unique: index.unique,
|
|
114
|
+
notNull: false,
|
|
115
|
+
dataType: 'string',
|
|
116
|
+
default: null,
|
|
117
|
+
multiEntry: false,
|
|
118
|
+
enableSearch: false,
|
|
119
|
+
keyPath: index.keyPath
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
finalDef.tables.push({
|
|
124
|
+
name: storeName,
|
|
125
|
+
columns: columns
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
db.close();
|
|
129
|
+
resolve(finalDef);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
request.onerror = (event) => {
|
|
133
|
+
console.error(
|
|
134
|
+
'Error opening database:',
|
|
135
|
+
(event.target as any).error
|
|
136
|
+
);
|
|
137
|
+
resolve(null);
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const toLower = SQLASTLib.cstVisitor({
|
|
143
|
+
keyword: (kw) => {
|
|
144
|
+
kw.text = kw.text.toLowerCase();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
export interface Connection extends BaseConnection {
|
|
149
|
+
$sql: {
|
|
150
|
+
run: (sql: string) => Promise<any>;
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface ISelectQueryResult {
|
|
155
|
+
columns: searchStatementUtils.IColumnMapping[];
|
|
156
|
+
rows: {
|
|
157
|
+
[columnName: string]: any;
|
|
158
|
+
}[];
|
|
159
|
+
tableName: string | null;
|
|
160
|
+
entity: DefinitionEntityState | null;
|
|
161
|
+
// Total number of rows in the table
|
|
162
|
+
total: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface ISelectQueryResultV2 {
|
|
166
|
+
columns: (ColumnRef | FunctionCall)[];
|
|
167
|
+
rows: {
|
|
168
|
+
[columnName: string]: any;
|
|
169
|
+
}[];
|
|
170
|
+
tableName: string | null;
|
|
171
|
+
entity: DefinitionEntityState | null;
|
|
172
|
+
// Total number of rows in the table
|
|
173
|
+
total: number;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface IDataBaseDetails {
|
|
177
|
+
items: Table[];
|
|
178
|
+
// Data for the initial query
|
|
179
|
+
data: ISelectQueryResult;
|
|
180
|
+
// Total number of rows in the database
|
|
181
|
+
total: number;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// This entity puts the table name and property entity that represents it in the canvas under one object
|
|
185
|
+
export class Column implements searchStatementUtils.IColumn {
|
|
186
|
+
columnName: string;
|
|
187
|
+
property: PropertyState;
|
|
188
|
+
|
|
189
|
+
// Table that this column class instance is stored in
|
|
190
|
+
table: Table;
|
|
191
|
+
// Table in which this column can actually be found in the latest version of the database
|
|
192
|
+
actualTable: Table;
|
|
193
|
+
|
|
194
|
+
constructor(columnName: string, property: PropertyState, table: Table) {
|
|
195
|
+
this.table = table;
|
|
196
|
+
this.columnName = columnName;
|
|
197
|
+
this.property = property;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
get primaryKey(): boolean {
|
|
201
|
+
const builtInPrimaryKeyPropertyId =
|
|
202
|
+
BUILT_IN_BASE_ENTITY_IDS[EntityType.BuiltInBaseEntity][
|
|
203
|
+
BaseEntityNames.PERSISTED_ENTITY
|
|
204
|
+
].properties.primaryKey.id;
|
|
205
|
+
|
|
206
|
+
const isPrimaryKey =
|
|
207
|
+
this.property.id === builtInPrimaryKeyPropertyId ||
|
|
208
|
+
!!this.property.implements.find(
|
|
209
|
+
(impl) => impl.id === builtInPrimaryKeyPropertyId
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
return isPrimaryKey;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// This entity puts the table name and entity that represents it in the canvas under one object
|
|
217
|
+
export class Table implements searchStatementUtils.ITable {
|
|
218
|
+
tableName: string;
|
|
219
|
+
entity: DefinitionEntityState;
|
|
220
|
+
columns: Column[] = [];
|
|
221
|
+
|
|
222
|
+
// Total number of rows in the table
|
|
223
|
+
total: number = 0;
|
|
224
|
+
|
|
225
|
+
// Database
|
|
226
|
+
database: Database;
|
|
227
|
+
|
|
228
|
+
constructor(
|
|
229
|
+
tableName: string,
|
|
230
|
+
entity: DefinitionEntityState,
|
|
231
|
+
database: Database
|
|
232
|
+
) {
|
|
233
|
+
this.database = database;
|
|
234
|
+
this.tableName = tableName;
|
|
235
|
+
this.entity = entity;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async init(): Promise<void> {
|
|
239
|
+
await this.updateTotal();
|
|
240
|
+
|
|
241
|
+
const columnProperties = getColumnProperties(this.entity);
|
|
242
|
+
|
|
243
|
+
// Loop over properties and add one key for each in the 'columns' property of the table
|
|
244
|
+
columnProperties.forEach((property) => {
|
|
245
|
+
const columnName =
|
|
246
|
+
property.codeName ||
|
|
247
|
+
lowercaseFirstLetter(
|
|
248
|
+
toCamelCase(
|
|
249
|
+
resolveEntityName(property, this.entity.project)
|
|
250
|
+
)
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const existingColumn = this.columns.find(
|
|
254
|
+
(column) => column.columnName === columnName
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
if (existingColumn) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.columns.push(new Column(columnName, property, this));
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Gets the total number of rows in the table and sets it to the 'total' property
|
|
266
|
+
async updateTotal(): Promise<number> {
|
|
267
|
+
const oldTotal = this.total;
|
|
268
|
+
|
|
269
|
+
const rows = await this.database.select({
|
|
270
|
+
from: this.tableName
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
this.total = rows.length;
|
|
274
|
+
|
|
275
|
+
const difference = this.total - oldTotal;
|
|
276
|
+
|
|
277
|
+
// Add the difference to the total of the parent database
|
|
278
|
+
this.database.total += difference;
|
|
279
|
+
|
|
280
|
+
return this.total;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function areValuesEqual(left: any, right: any): boolean {
|
|
285
|
+
if (left === right) {
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
if (
|
|
289
|
+
left === null ||
|
|
290
|
+
left === undefined ||
|
|
291
|
+
right === null ||
|
|
292
|
+
right === undefined
|
|
293
|
+
) {
|
|
294
|
+
return left === right;
|
|
295
|
+
}
|
|
296
|
+
// Numeric coercion for comparison
|
|
297
|
+
if (typeof left === 'number' && typeof right === 'string') {
|
|
298
|
+
const rightNum = Number(right);
|
|
299
|
+
return !isNaN(rightNum) && right !== '' && left === rightNum;
|
|
300
|
+
}
|
|
301
|
+
if (typeof left === 'string' && typeof right === 'number') {
|
|
302
|
+
const leftNum = Number(left);
|
|
303
|
+
return !isNaN(leftNum) && left !== '' && leftNum === right;
|
|
304
|
+
}
|
|
305
|
+
// Boolean-string coercion
|
|
306
|
+
if (typeof left === 'boolean' && typeof right === 'string') {
|
|
307
|
+
return String(left) === right;
|
|
308
|
+
}
|
|
309
|
+
if (typeof left === 'string' && typeof right === 'boolean') {
|
|
310
|
+
return left === String(right);
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function coerceToComparable(left: any, right: any): [any, any] {
|
|
316
|
+
if (typeof left === 'number' && typeof right === 'string') {
|
|
317
|
+
const rightNum = Number(right);
|
|
318
|
+
if (!isNaN(rightNum) && right !== '') {
|
|
319
|
+
return [left, rightNum];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (typeof left === 'string' && typeof right === 'number') {
|
|
323
|
+
const leftNum = Number(left);
|
|
324
|
+
if (!isNaN(leftNum) && left !== '') {
|
|
325
|
+
return [leftNum, right];
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return [left, right];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function sanitizeValueForColumn(value: any, column: Column): any {
|
|
332
|
+
if (checkIsEmptyValue(value)) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
const dataType = column.property.dataType;
|
|
336
|
+
if (dataType?.entity?.type === EntityType.PrimitiveEntity) {
|
|
337
|
+
switch (dataType.entity.name) {
|
|
338
|
+
case PrimitiveTypes.Number: {
|
|
339
|
+
const num = Number(value);
|
|
340
|
+
return isNaN(num) ? null : num;
|
|
341
|
+
}
|
|
342
|
+
case PrimitiveTypes.Boolean:
|
|
343
|
+
return value === 'true' || value === true;
|
|
344
|
+
}
|
|
345
|
+
} else if (dataType?.entity?.type === EntityType.DefinitionEntity) {
|
|
346
|
+
const builtInPrimaryKeyPropertyId =
|
|
347
|
+
BUILT_IN_BASE_ENTITY_IDS[EntityType.BuiltInBaseEntity][
|
|
348
|
+
BaseEntityNames.PERSISTED_ENTITY
|
|
349
|
+
].properties.primaryKey.id;
|
|
350
|
+
|
|
351
|
+
const referencedProperties = getColumnProperties(
|
|
352
|
+
dataType.entity as DefinitionEntityState
|
|
353
|
+
);
|
|
354
|
+
const primaryKeyProperty = referencedProperties.find((prop) => {
|
|
355
|
+
return (
|
|
356
|
+
prop.id === builtInPrimaryKeyPropertyId ||
|
|
357
|
+
!!prop.implements.find(
|
|
358
|
+
(impl) => impl.id === builtInPrimaryKeyPropertyId
|
|
359
|
+
)
|
|
360
|
+
);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
if (primaryKeyProperty) {
|
|
364
|
+
const pkDataType = primaryKeyProperty.dataType;
|
|
365
|
+
if (
|
|
366
|
+
pkDataType?.entity?.type === EntityType.PrimitiveEntity &&
|
|
367
|
+
pkDataType.entity.name === PrimitiveTypes.Number
|
|
368
|
+
) {
|
|
369
|
+
const num = Number(value);
|
|
370
|
+
return isNaN(num) ? null : num;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return value;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function migrateAndCompactData(
|
|
378
|
+
dbName: string,
|
|
379
|
+
tablesToMigrate: {
|
|
380
|
+
tableName: string;
|
|
381
|
+
desiredColumnsMeta: { [colName: string]: any };
|
|
382
|
+
renames: { [old: string]: string };
|
|
383
|
+
}[]
|
|
384
|
+
): Promise<void> {
|
|
385
|
+
return new Promise((resolve) => {
|
|
386
|
+
const request = indexedDB.open(dbName);
|
|
387
|
+
request.onerror = (event) => {
|
|
388
|
+
console.error(
|
|
389
|
+
'Failed to open DB for data migration:',
|
|
390
|
+
(event.target as any).error
|
|
391
|
+
);
|
|
392
|
+
resolve();
|
|
393
|
+
};
|
|
394
|
+
request.onsuccess = (event) => {
|
|
395
|
+
const db = request.result;
|
|
396
|
+
const tableNames = tablesToMigrate.map((t) => t.tableName);
|
|
397
|
+
|
|
398
|
+
// If any table names are not in db.objectStoreNames, filter them out to prevent transaction errors
|
|
399
|
+
const validTableNames = tableNames.filter((name) =>
|
|
400
|
+
db.objectStoreNames.contains(name)
|
|
401
|
+
);
|
|
402
|
+
if (validTableNames.length === 0) {
|
|
403
|
+
db.close();
|
|
404
|
+
resolve();
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
const transaction = db.transaction(
|
|
410
|
+
validTableNames,
|
|
411
|
+
'readwrite'
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
tablesToMigrate.forEach((tableInfo) => {
|
|
415
|
+
if (!db.objectStoreNames.contains(tableInfo.tableName)) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const store = transaction.objectStore(tableInfo.tableName);
|
|
419
|
+
const cursorRequest = store.openCursor();
|
|
420
|
+
|
|
421
|
+
cursorRequest.onsuccess = (e) => {
|
|
422
|
+
const cursor = (e.target as any).result;
|
|
423
|
+
if (cursor) {
|
|
424
|
+
const row = cursor.value;
|
|
425
|
+
let modified = false;
|
|
426
|
+
|
|
427
|
+
// 1. Process renames
|
|
428
|
+
Object.entries(tableInfo.renames).forEach(
|
|
429
|
+
([oldCol, newCol]) => {
|
|
430
|
+
if (row[oldCol] !== undefined) {
|
|
431
|
+
row[newCol] = row[oldCol];
|
|
432
|
+
delete row[oldCol];
|
|
433
|
+
modified = true;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
// 2. Compact: delete any fields not in the desired columns list
|
|
439
|
+
Object.keys(row).forEach((key) => {
|
|
440
|
+
if (!tableInfo.desiredColumnsMeta[key]) {
|
|
441
|
+
delete row[key];
|
|
442
|
+
modified = true;
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// 2.5 Type Casting (e.g. String to Number for foreign keys)
|
|
447
|
+
Object.entries(
|
|
448
|
+
tableInfo.desiredColumnsMeta
|
|
449
|
+
).forEach(([colName, colMeta]) => {
|
|
450
|
+
if (colMeta.dataType === 'number') {
|
|
451
|
+
if (typeof row[colName] === 'string') {
|
|
452
|
+
const parsed = Number(row[colName]);
|
|
453
|
+
if (!isNaN(parsed)) {
|
|
454
|
+
row[colName] = parsed;
|
|
455
|
+
modified = true;
|
|
456
|
+
} else if (row[colName] === '') {
|
|
457
|
+
row[colName] = null;
|
|
458
|
+
modified = true;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// 3. Drop rows that are incompatible with the new shape
|
|
465
|
+
let isCompatible = true;
|
|
466
|
+
Object.entries(
|
|
467
|
+
tableInfo.desiredColumnsMeta
|
|
468
|
+
).forEach(([colName, colMeta]) => {
|
|
469
|
+
if (
|
|
470
|
+
colMeta.notNull &&
|
|
471
|
+
(colMeta.default === undefined ||
|
|
472
|
+
colMeta.default === null)
|
|
473
|
+
) {
|
|
474
|
+
if (
|
|
475
|
+
row[colName] === undefined ||
|
|
476
|
+
row[colName] === null
|
|
477
|
+
) {
|
|
478
|
+
isCompatible = false;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
if (!isCompatible) {
|
|
484
|
+
cursor.delete();
|
|
485
|
+
} else if (modified) {
|
|
486
|
+
cursor.update(row);
|
|
487
|
+
}
|
|
488
|
+
cursor.continue();
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
transaction.oncomplete = () => {
|
|
494
|
+
db.close();
|
|
495
|
+
resolve();
|
|
496
|
+
};
|
|
497
|
+
transaction.onerror = (e) => {
|
|
498
|
+
console.error(
|
|
499
|
+
'Transaction error in migrateAndCompactData:',
|
|
500
|
+
(e.target as any).error
|
|
501
|
+
);
|
|
502
|
+
db.close();
|
|
503
|
+
resolve();
|
|
504
|
+
};
|
|
505
|
+
} catch (err) {
|
|
506
|
+
console.error(
|
|
507
|
+
'Error during migrateAndCompactData transaction creation:',
|
|
508
|
+
err
|
|
509
|
+
);
|
|
510
|
+
db.close();
|
|
511
|
+
resolve();
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
type PrimitiveRawResults =
|
|
518
|
+
| number
|
|
519
|
+
| string
|
|
520
|
+
| boolean
|
|
521
|
+
| null
|
|
522
|
+
| Date
|
|
523
|
+
| (number | string | boolean | null | Date)[];
|
|
524
|
+
|
|
525
|
+
type RawResults = PrimitiveRawResults | PrimitiveRawResults[] | RawResults[];
|
|
526
|
+
|
|
527
|
+
// This entity puts the connection to a real local database and the entity that owns it under one object
|
|
528
|
+
export class Database {
|
|
529
|
+
connection:
|
|
530
|
+
| (Connection & {
|
|
531
|
+
$sql: {
|
|
532
|
+
run: (sql: string) => Promise<any>;
|
|
533
|
+
};
|
|
534
|
+
} & {
|
|
535
|
+
// Missing while not fully initialized
|
|
536
|
+
database?: IDataBase;
|
|
537
|
+
})
|
|
538
|
+
| null = null;
|
|
539
|
+
|
|
540
|
+
entity: DefinitionEntityState;
|
|
541
|
+
tables: Table[] = [];
|
|
542
|
+
|
|
543
|
+
// Total number of rows in all tables
|
|
544
|
+
total: number = 0;
|
|
545
|
+
|
|
546
|
+
store: LocalRelationalDatabasesStore;
|
|
547
|
+
|
|
548
|
+
onGoingQueryCache: {
|
|
549
|
+
[query: string]: ISelectQueryResult;
|
|
550
|
+
} = {};
|
|
551
|
+
|
|
552
|
+
constructor(
|
|
553
|
+
entity: DefinitionEntityState,
|
|
554
|
+
store: LocalRelationalDatabasesStore
|
|
555
|
+
) {
|
|
556
|
+
this.store = store;
|
|
557
|
+
this.entity = entity;
|
|
558
|
+
|
|
559
|
+
this.processSelectQuery = this.processSelectQuery.bind(this);
|
|
560
|
+
this.init = this.init.bind(this);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async destroy() {
|
|
564
|
+
try {
|
|
565
|
+
// Clear local storage mappings for this database's tables
|
|
566
|
+
const attachedTableEntities: DefinitionEntityState[] =
|
|
567
|
+
this.store.persistedEntities.filter((persistedEntity) => {
|
|
568
|
+
const databaseEntity = getDatabaseEntity(persistedEntity);
|
|
569
|
+
return (
|
|
570
|
+
!!databaseEntity &&
|
|
571
|
+
databaseEntity?.id === this.entity?.id
|
|
572
|
+
);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
attachedTableEntities.forEach((tableEntity) => {
|
|
576
|
+
const mappingKey = `elyx__local_db_column_mappings__${tableEntity.project.id}__${tableEntity.id}`;
|
|
577
|
+
try {
|
|
578
|
+
localStorage.removeItem(mappingKey);
|
|
579
|
+
} catch (e) {
|
|
580
|
+
// Ignore
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
// If the database is not initialized, it will throw an error
|
|
585
|
+
// But it might actually exist in storage
|
|
586
|
+
// So we need to catch the error, re-initialize the connection and drop the database
|
|
587
|
+
await this.connection.dropDb();
|
|
588
|
+
} catch (e) {
|
|
589
|
+
// Ignore the error
|
|
590
|
+
console.log('Error dropping database:', e);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
await this.disconnect();
|
|
594
|
+
|
|
595
|
+
// Remove the database from the store
|
|
596
|
+
this.store.databases = this.store.databases.filter(
|
|
597
|
+
(db) => db.entity.id !== this.entity.id
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
static toDbName(entity: DefinitionEntityState): string {
|
|
602
|
+
const pascalName = toPascalCase(entity.name);
|
|
603
|
+
return `elyx__project--${entity.project.id}__${pascalName}__indexdb`;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
get definition(): IDataBase {
|
|
607
|
+
const attachedTableEntities: DefinitionEntityState[] =
|
|
608
|
+
this.store.persistedEntities.filter((persistedEntity) => {
|
|
609
|
+
// Some persisted entities aren't directly extending the base built in abstract persisted entity
|
|
610
|
+
// But rather, extend another definition entity that eventually down the extension inheritance extends a base built in persisted entity
|
|
611
|
+
// For that reason, we first find the base persisted entity
|
|
612
|
+
const databaseEntity = getDatabaseEntity(persistedEntity);
|
|
613
|
+
|
|
614
|
+
return (
|
|
615
|
+
!!databaseEntity && databaseEntity?.id === this.entity?.id
|
|
616
|
+
);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
const tables = attachedTableEntities.map(
|
|
620
|
+
persistedDefinitionEntityToDBTableConfig
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
const dbName = Database.toDbName(this.entity);
|
|
624
|
+
|
|
625
|
+
const dataBaseDef: IDataBase = {
|
|
626
|
+
name: dbName,
|
|
627
|
+
tables: tables.map((table) => table.definition)
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
return dataBaseDef;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
async init(): Promise<Database> {
|
|
634
|
+
// Find the persisted entities that belong to this db
|
|
635
|
+
// Persisted entities have a property called 'database' that points to the db entity
|
|
636
|
+
const attachedTableEntities: DefinitionEntityState[] =
|
|
637
|
+
this.store.persistedEntities.filter((persistedEntity) => {
|
|
638
|
+
// Some persisted entities aren't directly extending the base built in abstract persisted entity
|
|
639
|
+
// But rather, extend another definition entity that eventually down the extension inheritance extends a base built in persisted entity
|
|
640
|
+
// For that reason, we first find the base persisted entity
|
|
641
|
+
const databaseEntity = getDatabaseEntity(persistedEntity);
|
|
642
|
+
|
|
643
|
+
return (
|
|
644
|
+
!!databaseEntity && databaseEntity?.id === this.entity?.id
|
|
645
|
+
);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// Initialize the db with one table for each attached entity
|
|
649
|
+
const tables = attachedTableEntities.map(
|
|
650
|
+
persistedDefinitionEntityToDBTableConfig
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
const tablesToMigrate: {
|
|
654
|
+
tableName: string;
|
|
655
|
+
desiredColumnsMeta: { [colName: string]: any };
|
|
656
|
+
renames: { [old: string]: string };
|
|
657
|
+
}[] = [];
|
|
658
|
+
|
|
659
|
+
tables.forEach((table) => {
|
|
660
|
+
const mappingKey = `elyx__local_db_column_mappings__${table.entity.project.id}__${table.entity.id}`;
|
|
661
|
+
let savedMappings: { [propertyId: string]: string } = {};
|
|
662
|
+
try {
|
|
663
|
+
const rawMappings = localStorage.getItem(mappingKey);
|
|
664
|
+
if (rawMappings) {
|
|
665
|
+
savedMappings = JSON.parse(rawMappings);
|
|
666
|
+
}
|
|
667
|
+
} catch (e) {
|
|
668
|
+
console.error(
|
|
669
|
+
'Failed to parse column mappings from localStorage:',
|
|
670
|
+
e
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const currentProperties = getColumnProperties(table.entity);
|
|
675
|
+
const renames: { [oldCol: string]: string } = {};
|
|
676
|
+
|
|
677
|
+
currentProperties.forEach((prop) => {
|
|
678
|
+
const oldColumnName = savedMappings[prop.id];
|
|
679
|
+
const newColumnName =
|
|
680
|
+
prop.codeName ||
|
|
681
|
+
lowercaseFirstLetter(
|
|
682
|
+
toCamelCase(
|
|
683
|
+
resolveEntityName(prop, table.entity.project)
|
|
684
|
+
)
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
if (oldColumnName && oldColumnName !== newColumnName) {
|
|
688
|
+
renames[oldColumnName] = newColumnName;
|
|
689
|
+
}
|
|
690
|
+
savedMappings[prop.id] = newColumnName;
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
try {
|
|
694
|
+
localStorage.setItem(mappingKey, JSON.stringify(savedMappings));
|
|
695
|
+
} catch (e) {
|
|
696
|
+
console.error(
|
|
697
|
+
'Failed to save column mappings to localStorage:',
|
|
698
|
+
e
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
tablesToMigrate.push({
|
|
703
|
+
tableName: table.definition.name,
|
|
704
|
+
desiredColumnsMeta: table.definition.columns,
|
|
705
|
+
renames
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
const dbName = Database.toDbName(this.entity);
|
|
710
|
+
|
|
711
|
+
const existingDbDef = await describeExisting(dbName);
|
|
712
|
+
|
|
713
|
+
const sanitizedExistingDBDef = !!existingDbDef
|
|
714
|
+
? {
|
|
715
|
+
...existingDbDef,
|
|
716
|
+
tables:
|
|
717
|
+
existingDbDef?.tables.filter(
|
|
718
|
+
(table) => table.name !== 'JsStore_Meta'
|
|
719
|
+
) || []
|
|
720
|
+
}
|
|
721
|
+
: null;
|
|
722
|
+
|
|
723
|
+
const existingVersion = sanitizedExistingDBDef?.version || 1;
|
|
724
|
+
|
|
725
|
+
const tablesToRecreate: {
|
|
726
|
+
oldTableName: string;
|
|
727
|
+
newTableName: string;
|
|
728
|
+
oldPrimaryKey: string;
|
|
729
|
+
newPrimaryKey: string;
|
|
730
|
+
rows: any[];
|
|
731
|
+
}[] = [];
|
|
732
|
+
|
|
733
|
+
let connection = this.connection || (await initLocalDBConnection());
|
|
734
|
+
|
|
735
|
+
if (sanitizedExistingDBDef) {
|
|
736
|
+
for (const table of tables) {
|
|
737
|
+
const existingTable = sanitizedExistingDBDef.tables.find(
|
|
738
|
+
(t) => t.name === table.definition.name
|
|
739
|
+
);
|
|
740
|
+
if (existingTable) {
|
|
741
|
+
const desiredPrimaryKeyCol = Object.entries(
|
|
742
|
+
table.definition.columns
|
|
743
|
+
).find(([_, colMeta]) => colMeta.primaryKey);
|
|
744
|
+
const desiredPrimaryKeyName = desiredPrimaryKeyCol?.[0];
|
|
745
|
+
|
|
746
|
+
const existingPrimaryKeyCol = Object.entries(
|
|
747
|
+
existingTable.columns
|
|
748
|
+
).find(([_, colMeta]) => colMeta.primaryKey);
|
|
749
|
+
const existingPrimaryKeyName = existingPrimaryKeyCol?.[0];
|
|
750
|
+
|
|
751
|
+
if (
|
|
752
|
+
existingPrimaryKeyName &&
|
|
753
|
+
desiredPrimaryKeyName &&
|
|
754
|
+
existingPrimaryKeyName !== desiredPrimaryKeyName
|
|
755
|
+
) {
|
|
756
|
+
let rows: any[] = [];
|
|
757
|
+
try {
|
|
758
|
+
rows = await new Promise<any[]>(
|
|
759
|
+
(resolve, reject) => {
|
|
760
|
+
const request = indexedDB.open(dbName);
|
|
761
|
+
request.onsuccess = (event) => {
|
|
762
|
+
const db = (event.target as any).result;
|
|
763
|
+
try {
|
|
764
|
+
if (
|
|
765
|
+
!db.objectStoreNames.contains(
|
|
766
|
+
table.definition.name
|
|
767
|
+
)
|
|
768
|
+
) {
|
|
769
|
+
db.close();
|
|
770
|
+
resolve([]);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const transaction = db.transaction(
|
|
774
|
+
table.definition.name,
|
|
775
|
+
'readonly'
|
|
776
|
+
);
|
|
777
|
+
const store =
|
|
778
|
+
transaction.objectStore(
|
|
779
|
+
table.definition.name
|
|
780
|
+
);
|
|
781
|
+
const getAllReq = store.getAll();
|
|
782
|
+
getAllReq.onsuccess = () => {
|
|
783
|
+
const result =
|
|
784
|
+
getAllReq.result || [];
|
|
785
|
+
db.close();
|
|
786
|
+
resolve(result);
|
|
787
|
+
};
|
|
788
|
+
getAllReq.onerror = () => {
|
|
789
|
+
db.close();
|
|
790
|
+
reject(getAllReq.error);
|
|
791
|
+
};
|
|
792
|
+
} catch (err) {
|
|
793
|
+
db.close();
|
|
794
|
+
reject(err);
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
request.onerror = (event) => {
|
|
798
|
+
reject((event.target as any).error);
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
);
|
|
802
|
+
} catch (e) {
|
|
803
|
+
console.error(
|
|
804
|
+
`Failed to fetch rows for recreating table ${table.definition.name}:`,
|
|
805
|
+
e
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
tablesToRecreate.push({
|
|
810
|
+
oldTableName: table.definition.name,
|
|
811
|
+
newTableName: table.definition.name,
|
|
812
|
+
oldPrimaryKey: existingPrimaryKeyName,
|
|
813
|
+
newPrimaryKey: desiredPrimaryKeyName,
|
|
814
|
+
rows
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// The target definition is the one that we want to have
|
|
822
|
+
// We will populate it, from the difference between the existing definition and the target definition
|
|
823
|
+
const finalDef: IDataBase = {
|
|
824
|
+
name: dbName,
|
|
825
|
+
tables: [] as ITable[]
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
// If anything has changed, increment the version
|
|
829
|
+
let hasChanged: boolean = false;
|
|
830
|
+
let nextVersion = existingVersion;
|
|
831
|
+
|
|
832
|
+
const increaseVersion = () => {
|
|
833
|
+
if (!hasChanged) {
|
|
834
|
+
hasChanged = true;
|
|
835
|
+
nextVersion++;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return nextVersion;
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
const tablesToAdd: ITable[] = [];
|
|
842
|
+
const tablesToRemove: ITable[] = [];
|
|
843
|
+
const tablesToModify: ITable[] = [];
|
|
844
|
+
|
|
845
|
+
// Loop over the existing definition tables
|
|
846
|
+
sanitizedExistingDBDef?.tables?.forEach((table) => {
|
|
847
|
+
const desiredTable = tables.find(
|
|
848
|
+
(t) => t.definition.name === table.name
|
|
849
|
+
);
|
|
850
|
+
|
|
851
|
+
if (!desiredTable) {
|
|
852
|
+
increaseVersion();
|
|
853
|
+
tablesToRemove.push(table);
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
const columnsToDrop: { tableName: string; columnName: string }[] = [];
|
|
858
|
+
|
|
859
|
+
tables.forEach((table) => {
|
|
860
|
+
const existingTable = sanitizedExistingDBDef?.tables?.find(
|
|
861
|
+
(t) => t.name === table.definition.name
|
|
862
|
+
);
|
|
863
|
+
if (existingTable) {
|
|
864
|
+
Object.keys(existingTable.columns).forEach(
|
|
865
|
+
(existingColumnName) => {
|
|
866
|
+
if (!table.definition.columns[existingColumnName]) {
|
|
867
|
+
columnsToDrop.push({
|
|
868
|
+
tableName: table.definition.name,
|
|
869
|
+
columnName: existingColumnName
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
if (
|
|
878
|
+
tablesToRecreate.length > 0 ||
|
|
879
|
+
tablesToRemove.length > 0 ||
|
|
880
|
+
columnsToDrop.length > 0
|
|
881
|
+
) {
|
|
882
|
+
const deleteVersion = existingVersion + 1;
|
|
883
|
+
nextVersion = deleteVersion + 1;
|
|
884
|
+
hasChanged = true;
|
|
885
|
+
|
|
886
|
+
// Natively delete the object stores and indexes
|
|
887
|
+
await new Promise<void>((resolveDelete) => {
|
|
888
|
+
if (connection) {
|
|
889
|
+
connection.terminate();
|
|
890
|
+
}
|
|
891
|
+
const request = indexedDB.open(dbName, deleteVersion);
|
|
892
|
+
request.onupgradeneeded = (event) => {
|
|
893
|
+
const db = request.result;
|
|
894
|
+
const transaction = request.transaction;
|
|
895
|
+
|
|
896
|
+
tablesToRecreate.forEach((t) => {
|
|
897
|
+
if (db.objectStoreNames.contains(t.oldTableName)) {
|
|
898
|
+
db.deleteObjectStore(t.oldTableName);
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
tablesToRemove.forEach((t) => {
|
|
902
|
+
if (db.objectStoreNames.contains(t.name)) {
|
|
903
|
+
db.deleteObjectStore(t.name);
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
if (transaction) {
|
|
908
|
+
columnsToDrop.forEach((c) => {
|
|
909
|
+
if (db.objectStoreNames.contains(c.tableName)) {
|
|
910
|
+
const store = transaction.objectStore(
|
|
911
|
+
c.tableName
|
|
912
|
+
);
|
|
913
|
+
if (store.indexNames.contains(c.columnName)) {
|
|
914
|
+
store.deleteIndex(c.columnName);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
request.onsuccess = (event) => {
|
|
921
|
+
const db = request.result;
|
|
922
|
+
db.close();
|
|
923
|
+
resolveDelete();
|
|
924
|
+
};
|
|
925
|
+
request.onerror = () => {
|
|
926
|
+
resolveDelete();
|
|
927
|
+
};
|
|
928
|
+
request.onblocked = () => {
|
|
929
|
+
console.warn(
|
|
930
|
+
'IndexedDB open blocked during table recreation'
|
|
931
|
+
);
|
|
932
|
+
resolveDelete();
|
|
933
|
+
};
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
// Re-initialize connection
|
|
937
|
+
connection = await initLocalDBConnection();
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/*
|
|
941
|
+
Compare both definitions
|
|
942
|
+
Loop over the desired definition tables
|
|
943
|
+
For each table, check if all columns exist, if not, add the new column definition as 'alter -> add'
|
|
944
|
+
For each unrecognized column, add the column definition as 'alter -> drop'
|
|
945
|
+
For each colmn present in both, check if the data-type and other metadata matches, if not, add the new column definition as 'alter -> modify'
|
|
946
|
+
*/
|
|
947
|
+
|
|
948
|
+
// When a data-type changes, we need to migrate the data
|
|
949
|
+
// So we store here a list of migrations needed as a from -> to pair of property version instances
|
|
950
|
+
// The from is the last PropertyState version that had a data-type like the one in the database
|
|
951
|
+
// The to is the new PropertyState version that is being added to the table
|
|
952
|
+
const migrationRequests: {
|
|
953
|
+
from: PropertyState;
|
|
954
|
+
to: PropertyState;
|
|
955
|
+
}[] = [];
|
|
956
|
+
|
|
957
|
+
// Loop over the desired definition tables
|
|
958
|
+
tables.forEach((table) => {
|
|
959
|
+
const existingTable = sanitizedExistingDBDef.tables.find(
|
|
960
|
+
(t) => t.name === table.definition.name
|
|
961
|
+
);
|
|
962
|
+
|
|
963
|
+
const isRecreating = tablesToRecreate.some(
|
|
964
|
+
(t) => t.oldTableName === table.definition.name
|
|
965
|
+
);
|
|
966
|
+
|
|
967
|
+
if (!existingTable || isRecreating) {
|
|
968
|
+
increaseVersion();
|
|
969
|
+
tablesToAdd.push(table.definition);
|
|
970
|
+
finalDef.tables.push(table.definition);
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
let finalTable: ITable = {
|
|
975
|
+
name: table.definition.name,
|
|
976
|
+
columns: table.definition.columns
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
// Loop over the desired definition columns
|
|
980
|
+
Object.entries(table.definition.columns).forEach(
|
|
981
|
+
([targetColumnName, targetColMeta]) => {
|
|
982
|
+
const columnEntries = Object.entries(existingTable.columns);
|
|
983
|
+
|
|
984
|
+
const result = columnEntries.find(
|
|
985
|
+
([cName, cMeta]) => cName === targetColumnName
|
|
986
|
+
);
|
|
987
|
+
|
|
988
|
+
const existingColumnName = result?.[0];
|
|
989
|
+
const existingColMeta = result?.[1];
|
|
990
|
+
|
|
991
|
+
if (!existingColumnName) {
|
|
992
|
+
increaseVersion();
|
|
993
|
+
tablesToModify.push(table.definition);
|
|
994
|
+
finalTable = {
|
|
995
|
+
...finalTable,
|
|
996
|
+
alter: {
|
|
997
|
+
...(finalTable.alter || {}),
|
|
998
|
+
[nextVersion]: {
|
|
999
|
+
...(finalTable.alter?.[nextVersion] || {}),
|
|
1000
|
+
add: {
|
|
1001
|
+
...(finalTable.alter?.[nextVersion]
|
|
1002
|
+
?.add || {}),
|
|
1003
|
+
[targetColumnName]: targetColMeta
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// Check if the data-type and other metadata matches
|
|
1012
|
+
if (existingColMeta.dataType !== targetColMeta.dataType) {
|
|
1013
|
+
increaseVersion();
|
|
1014
|
+
tablesToModify.push(table.definition);
|
|
1015
|
+
finalTable = {
|
|
1016
|
+
...finalTable,
|
|
1017
|
+
alter: {
|
|
1018
|
+
...(finalTable.alter || {}),
|
|
1019
|
+
[nextVersion]: {
|
|
1020
|
+
...(finalTable.alter?.[nextVersion] || {}),
|
|
1021
|
+
modify: {
|
|
1022
|
+
...(finalTable.alter?.[nextVersion]
|
|
1023
|
+
?.modify || {}),
|
|
1024
|
+
[targetColumnName]: targetColMeta
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
// TODO: Here subscribe to 'migrationRequests'
|
|
1031
|
+
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
);
|
|
1036
|
+
|
|
1037
|
+
// Loop over the existing definition columns
|
|
1038
|
+
Object.entries(existingTable.columns).forEach(
|
|
1039
|
+
([existingColumnName, existingColMeta]) => {
|
|
1040
|
+
const targetColMeta =
|
|
1041
|
+
table.definition.columns[existingColumnName];
|
|
1042
|
+
|
|
1043
|
+
if (!targetColMeta) {
|
|
1044
|
+
increaseVersion();
|
|
1045
|
+
tablesToModify.push(table.definition);
|
|
1046
|
+
finalTable = {
|
|
1047
|
+
...finalTable,
|
|
1048
|
+
alter: {
|
|
1049
|
+
...(finalTable.alter || {}),
|
|
1050
|
+
[nextVersion]: {
|
|
1051
|
+
...(finalTable.alter?.[nextVersion] || {}),
|
|
1052
|
+
drop: {
|
|
1053
|
+
...(finalTable.alter?.[nextVersion]
|
|
1054
|
+
?.drop || {}),
|
|
1055
|
+
[existingColumnName]: null
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
);
|
|
1064
|
+
|
|
1065
|
+
finalDef.tables.push(finalTable);
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
finalDef.version = nextVersion;
|
|
1069
|
+
|
|
1070
|
+
// If a database exists, it will not add the new tables, it will just initialize the database connection
|
|
1071
|
+
const created = await initLocalIndexedDatabase(connection, finalDef);
|
|
1072
|
+
|
|
1073
|
+
this.connection = connection;
|
|
1074
|
+
|
|
1075
|
+
// Perform IndexedDB schema and data migration and compaction
|
|
1076
|
+
await migrateAndCompactData(dbName, tablesToMigrate);
|
|
1077
|
+
|
|
1078
|
+
// Restore recreated tables data
|
|
1079
|
+
if (tablesToRecreate.length > 0) {
|
|
1080
|
+
for (const t of tablesToRecreate) {
|
|
1081
|
+
if (t.rows.length > 0) {
|
|
1082
|
+
const mappedRows = t.rows.map((row) => {
|
|
1083
|
+
const newRow = { ...row };
|
|
1084
|
+
if (row[t.oldPrimaryKey] !== undefined) {
|
|
1085
|
+
newRow[t.newPrimaryKey] = row[t.oldPrimaryKey];
|
|
1086
|
+
delete newRow[t.oldPrimaryKey];
|
|
1087
|
+
}
|
|
1088
|
+
return newRow;
|
|
1089
|
+
});
|
|
1090
|
+
try {
|
|
1091
|
+
await connection.insert({
|
|
1092
|
+
into: t.newTableName,
|
|
1093
|
+
values: mappedRows
|
|
1094
|
+
});
|
|
1095
|
+
} catch (e) {
|
|
1096
|
+
console.error(
|
|
1097
|
+
`Failed to restore rows for table ${t.newTableName}:`,
|
|
1098
|
+
e
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// Init tables
|
|
1106
|
+
await Promise.all(
|
|
1107
|
+
tables.map(async (table) => {
|
|
1108
|
+
const existingTableInstance = this.tables.find(
|
|
1109
|
+
(t) => t.tableName === table.definition.name
|
|
1110
|
+
);
|
|
1111
|
+
const tableInstance =
|
|
1112
|
+
existingTableInstance ||
|
|
1113
|
+
new Table(table.definition.name, table.entity, this);
|
|
1114
|
+
|
|
1115
|
+
await tableInstance.init();
|
|
1116
|
+
|
|
1117
|
+
this.tables.push(tableInstance);
|
|
1118
|
+
})
|
|
1119
|
+
);
|
|
1120
|
+
|
|
1121
|
+
this.store.project.emit('local-databases-updated', this);
|
|
1122
|
+
|
|
1123
|
+
return this;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
async disconnect(): Promise<void> {
|
|
1127
|
+
await this.connection.terminate();
|
|
1128
|
+
this.connection = null;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
async describe(initialQuery: any): Promise<IDataBaseDetails> {
|
|
1132
|
+
// @ts-ignore Because it is a protected property
|
|
1133
|
+
const databaseStructureDef = this.connection.database;
|
|
1134
|
+
|
|
1135
|
+
let preselectedTableDef: ITable | null = null;
|
|
1136
|
+
let preselectedTable: Table | null = null;
|
|
1137
|
+
let preselectedTableEntity: DefinitionEntityState | null = null;
|
|
1138
|
+
|
|
1139
|
+
if (initialQuery.tableName) {
|
|
1140
|
+
preselectedTableDef = databaseStructureDef.tables.find(
|
|
1141
|
+
(table) => table.name === initialQuery.tableName
|
|
1142
|
+
);
|
|
1143
|
+
|
|
1144
|
+
preselectedTable = this.tables.find(
|
|
1145
|
+
(t) => t.tableName === initialQuery.tableName
|
|
1146
|
+
);
|
|
1147
|
+
|
|
1148
|
+
preselectedTableEntity = preselectedTable?.entity;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
if (!preselectedTableDef) {
|
|
1152
|
+
return {
|
|
1153
|
+
items: databaseStructureDef.tables.map((t) => {
|
|
1154
|
+
const table = this.tables.find(
|
|
1155
|
+
(ta) => ta.tableName === t.name
|
|
1156
|
+
);
|
|
1157
|
+
|
|
1158
|
+
return table;
|
|
1159
|
+
}),
|
|
1160
|
+
data: null,
|
|
1161
|
+
total: this.total
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
const rows: {
|
|
1166
|
+
[columnName: string]: any;
|
|
1167
|
+
}[] = initialQuery.tableName
|
|
1168
|
+
? await this.connection.select({
|
|
1169
|
+
from: initialQuery.tableName,
|
|
1170
|
+
limit: initialQuery.limit
|
|
1171
|
+
})
|
|
1172
|
+
: [];
|
|
1173
|
+
|
|
1174
|
+
const initialColumns: searchStatementUtils.IColumnMapping[] = (
|
|
1175
|
+
preselectedTable?.columns || []
|
|
1176
|
+
).map((column) => ({
|
|
1177
|
+
column: column,
|
|
1178
|
+
as: column.columnName
|
|
1179
|
+
}));
|
|
1180
|
+
|
|
1181
|
+
return {
|
|
1182
|
+
items: databaseStructureDef.tables.map((tabl) => {
|
|
1183
|
+
const table = this.tables.find(
|
|
1184
|
+
(t) => t.tableName === tabl.name
|
|
1185
|
+
);
|
|
1186
|
+
|
|
1187
|
+
return table;
|
|
1188
|
+
}),
|
|
1189
|
+
data: initialQuery.tableName
|
|
1190
|
+
? {
|
|
1191
|
+
tableName: preselectedTableDef.name,
|
|
1192
|
+
entity: preselectedTableEntity,
|
|
1193
|
+
columns: initialColumns,
|
|
1194
|
+
rows: rows,
|
|
1195
|
+
total: preselectedTable.total
|
|
1196
|
+
}
|
|
1197
|
+
: null,
|
|
1198
|
+
total: this.total
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Given an entity id that represents a table in the database
|
|
1203
|
+
// Return the table object
|
|
1204
|
+
getTable(entityIdOrTableName: string): Table {
|
|
1205
|
+
return this.tables.find(
|
|
1206
|
+
(table) =>
|
|
1207
|
+
table.entity.id === entityIdOrTableName ||
|
|
1208
|
+
table.tableName === entityIdOrTableName
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
async checkForeignKeyConstraintsForUpdate(
|
|
1213
|
+
tableName: string,
|
|
1214
|
+
setValues: any
|
|
1215
|
+
) {
|
|
1216
|
+
const table = this.getTable(tableName);
|
|
1217
|
+
if (!table) return;
|
|
1218
|
+
|
|
1219
|
+
for (const column of table.columns) {
|
|
1220
|
+
const foreignKeyValue = setValues[column.columnName];
|
|
1221
|
+
if (foreignKeyValue === undefined) {
|
|
1222
|
+
continue; // Not being updated
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
const dataType = column.property?.dataType;
|
|
1226
|
+
const fkInfo = resolveForeignKeyInfo(dataType);
|
|
1227
|
+
if (fkInfo) {
|
|
1228
|
+
const referencedEntity = fkInfo.entity;
|
|
1229
|
+
const referencedDb =
|
|
1230
|
+
this.store.getDatabaseForPersistedEntity(referencedEntity);
|
|
1231
|
+
if (!referencedDb) continue;
|
|
1232
|
+
|
|
1233
|
+
const referencedTableName = toPascalCase(
|
|
1234
|
+
resolveEntityName(
|
|
1235
|
+
referencedEntity,
|
|
1236
|
+
this.store.project.logic
|
|
1237
|
+
)
|
|
1238
|
+
);
|
|
1239
|
+
const referencedTable =
|
|
1240
|
+
referencedDb.getTable(referencedTableName);
|
|
1241
|
+
if (!referencedTable) continue;
|
|
1242
|
+
|
|
1243
|
+
if (foreignKeyValue == null) {
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// Find the primary key name of the referenced table
|
|
1248
|
+
const referencedPrimaryKeyName = fkInfo.foreignKey.key;
|
|
1249
|
+
|
|
1250
|
+
// Query the referenced table
|
|
1251
|
+
const referencedRows = await referencedDb.connection.select({
|
|
1252
|
+
from: referencedTableName,
|
|
1253
|
+
where: {
|
|
1254
|
+
[referencedPrimaryKeyName]: foreignKeyValue
|
|
1255
|
+
},
|
|
1256
|
+
limit: 1
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
if (referencedRows.length === 0) {
|
|
1260
|
+
throw new Error(
|
|
1261
|
+
`SequelizeDatabaseError: Foreign key constraint failed: Referenced record in table "${referencedTableName}" does not exist`
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
async checkForeignKeyConstraintsForInsert(
|
|
1269
|
+
tableName: string,
|
|
1270
|
+
values: any[]
|
|
1271
|
+
) {
|
|
1272
|
+
for (const row of values) {
|
|
1273
|
+
await this.checkForeignKeyConstraintsForUpdate(tableName, row);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
async insert(query: IInsertQuery): Promise<any> {
|
|
1278
|
+
const table = this.getTable(query.into);
|
|
1279
|
+
if (table && query.values) {
|
|
1280
|
+
query.values = query.values.map((row) => {
|
|
1281
|
+
const sanitizedRow: any = { ...row };
|
|
1282
|
+
table.columns.forEach((column) => {
|
|
1283
|
+
const value = row[column.columnName];
|
|
1284
|
+
if (value !== undefined) {
|
|
1285
|
+
sanitizedRow[column.columnName] =
|
|
1286
|
+
sanitizeValueForColumn(value, column);
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
return sanitizedRow;
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
await this.checkForeignKeyConstraintsForInsert(
|
|
1293
|
+
query.into,
|
|
1294
|
+
query.values
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
const result = await this.connection.insert(query);
|
|
1299
|
+
|
|
1300
|
+
// Update the total number of rows in the table
|
|
1301
|
+
if (table) {
|
|
1302
|
+
table.total += query.values.length;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
this.store.project.emit('local-databases-updated', this);
|
|
1306
|
+
|
|
1307
|
+
return result;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
async update(query: IUpdateQuery): Promise<any> {
|
|
1311
|
+
const table = this.getTable(query.in);
|
|
1312
|
+
if (table && query.set) {
|
|
1313
|
+
const sanitizedSet: any = { ...query.set };
|
|
1314
|
+
table.columns.forEach((column) => {
|
|
1315
|
+
const value = query.set[column.columnName];
|
|
1316
|
+
if (value !== undefined) {
|
|
1317
|
+
sanitizedSet[column.columnName] = sanitizeValueForColumn(
|
|
1318
|
+
value,
|
|
1319
|
+
column
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
query.set = sanitizedSet;
|
|
1324
|
+
|
|
1325
|
+
await this.checkForeignKeyConstraintsForUpdate(query.in, query.set);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
const result = await this.connection.update(query);
|
|
1329
|
+
|
|
1330
|
+
this.store.project.emit('local-databases-updated', this);
|
|
1331
|
+
|
|
1332
|
+
return result;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
async remove(query: IRemoveQuery): Promise<any> {
|
|
1336
|
+
// Find rows being targeted for deletion
|
|
1337
|
+
const rowsToDelete = await this.connection.select({
|
|
1338
|
+
from: query.from,
|
|
1339
|
+
where: query.where
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
if (rowsToDelete.length > 0) {
|
|
1343
|
+
const table = this.getTable(query.from);
|
|
1344
|
+
const primaryKeyName =
|
|
1345
|
+
table.columns.find((col) => col.primaryKey)?.columnName || 'id';
|
|
1346
|
+
const primaryKeyValues = rowsToDelete
|
|
1347
|
+
.map((row) => (row as any)[primaryKeyName])
|
|
1348
|
+
.filter((val) => val != null);
|
|
1349
|
+
|
|
1350
|
+
if (primaryKeyValues.length > 0) {
|
|
1351
|
+
// Iterate over all tables in this database
|
|
1352
|
+
for (const otherTable of this.tables) {
|
|
1353
|
+
for (const column of otherTable.columns) {
|
|
1354
|
+
const dataType = column.property?.dataType;
|
|
1355
|
+
if (
|
|
1356
|
+
dataType?.entity?.type ===
|
|
1357
|
+
EntityType.DefinitionEntity &&
|
|
1358
|
+
dataType.entity.id === table.entity.id
|
|
1359
|
+
) {
|
|
1360
|
+
// This column references table.entity!
|
|
1361
|
+
// Check if there are any rows in otherTable where this column holds one of primaryKeyValues
|
|
1362
|
+
const referencingRows =
|
|
1363
|
+
await this.connection.select({
|
|
1364
|
+
from: otherTable.tableName,
|
|
1365
|
+
where: {
|
|
1366
|
+
[column.columnName]: {
|
|
1367
|
+
in: primaryKeyValues
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
const otherTablePrimaryKeyName =
|
|
1373
|
+
otherTable.columns.find((col) => col.primaryKey)
|
|
1374
|
+
?.columnName || 'id';
|
|
1375
|
+
const actualReferencingRows =
|
|
1376
|
+
referencingRows.filter((row) => {
|
|
1377
|
+
const rowId = (row as any)[
|
|
1378
|
+
otherTablePrimaryKeyName
|
|
1379
|
+
];
|
|
1380
|
+
// If this referencing row is also among the ones being deleted, it's fine!
|
|
1381
|
+
if (otherTable.tableName === query.from) {
|
|
1382
|
+
return !primaryKeyValues.includes(
|
|
1383
|
+
rowId
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
return true;
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
if (actualReferencingRows.length > 0) {
|
|
1390
|
+
throw new Error(
|
|
1391
|
+
`SequelizeForeignKeyConstraintError: update or delete on table "${query.from}" violates foreign key constraint on table "${otherTable.tableName}"`
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
await this.connection.remove(query);
|
|
1401
|
+
|
|
1402
|
+
// Recalculate the total number of rows in the table
|
|
1403
|
+
const table = this.getTable(query.from);
|
|
1404
|
+
|
|
1405
|
+
await table.updateTotal();
|
|
1406
|
+
|
|
1407
|
+
this.store.project.emit('local-databases-updated', this);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
async clear(table: string): Promise<any> {
|
|
1411
|
+
await this.connection.clear(table);
|
|
1412
|
+
|
|
1413
|
+
// Recalculate the total number of rows in the table
|
|
1414
|
+
const tableInstance = this.getTable(table);
|
|
1415
|
+
|
|
1416
|
+
await tableInstance.updateTotal();
|
|
1417
|
+
|
|
1418
|
+
this.store.project.emit('local-databases-updated', this);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
select(query: ISelectQuery): Promise<
|
|
1422
|
+
{
|
|
1423
|
+
[columnName: string]: any;
|
|
1424
|
+
}[]
|
|
1425
|
+
> {
|
|
1426
|
+
return this.connection.select(query);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/*
|
|
1430
|
+
Jsstore doesn't have feature parity with SQL
|
|
1431
|
+
So we process the 'where' clause separately
|
|
1432
|
+
This means, the first query is executed in jsstore without any 'where' clause filters
|
|
1433
|
+
Then, we filter the results in memory
|
|
1434
|
+
|
|
1435
|
+
To achieve the filtering of the results according to the potentially complex and nested 'where' clause
|
|
1436
|
+
We divide the 'where' clause into smaller parts, each representing a single 'where' condition
|
|
1437
|
+
Then merge the results base on "and" and "or" conditions, where if a row passes all "and" conditions, it is included
|
|
1438
|
+
And if a row passes any "or" condition, it is included
|
|
1439
|
+
*/
|
|
1440
|
+
async processFiltering(
|
|
1441
|
+
selectStatement: SelectStmt,
|
|
1442
|
+
rows: {
|
|
1443
|
+
[columnName: string]: any;
|
|
1444
|
+
}[]
|
|
1445
|
+
): Promise<
|
|
1446
|
+
{
|
|
1447
|
+
[columnName: string]: any;
|
|
1448
|
+
}[]
|
|
1449
|
+
> {
|
|
1450
|
+
const whereClause =
|
|
1451
|
+
searchStatementUtils.getWhereClauseFromSelectStatement(
|
|
1452
|
+
selectStatement
|
|
1453
|
+
);
|
|
1454
|
+
|
|
1455
|
+
if (!whereClause) {
|
|
1456
|
+
return rows;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
return rows;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
evaluateNode(
|
|
1463
|
+
node: SearchNode,
|
|
1464
|
+
dataSources: { source: DataSource; data: any }[] = []
|
|
1465
|
+
): any {
|
|
1466
|
+
if (!node) return null;
|
|
1467
|
+
|
|
1468
|
+
if (node.type === SearchStatementNodeType.LiteralValue) {
|
|
1469
|
+
return (node as any).value;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
if (node.type === SearchStatementNodeType.ColumnRef) {
|
|
1473
|
+
const col = node as ColumnRef;
|
|
1474
|
+
const ds = dataSources.find((d) => d.source?.as === col.parent?.as);
|
|
1475
|
+
if (ds && ds.data) {
|
|
1476
|
+
let ogColumnName = '';
|
|
1477
|
+
if (col.source?.type === EntityType.Property) {
|
|
1478
|
+
ogColumnName =
|
|
1479
|
+
(col.source as PropertyState)?.codeName ||
|
|
1480
|
+
lowercaseFirstLetter(
|
|
1481
|
+
toCamelCase(
|
|
1482
|
+
resolveEntityName(
|
|
1483
|
+
col.source as PropertyState,
|
|
1484
|
+
(col.source as PropertyState).project
|
|
1485
|
+
)
|
|
1486
|
+
)
|
|
1487
|
+
);
|
|
1488
|
+
} else if (
|
|
1489
|
+
col.source?.type === SearchStatementNodeType.LiteralValue
|
|
1490
|
+
) {
|
|
1491
|
+
ogColumnName = (col.source as any).value?.toString() || '';
|
|
1492
|
+
} else if (
|
|
1493
|
+
col.source?.type === SearchStatementNodeType.ColumnRef
|
|
1494
|
+
) {
|
|
1495
|
+
ogColumnName = (col.source as ColumnRef).as;
|
|
1496
|
+
} else {
|
|
1497
|
+
ogColumnName = col.as;
|
|
1498
|
+
}
|
|
1499
|
+
return ds.data[ogColumnName];
|
|
1500
|
+
}
|
|
1501
|
+
return null;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
if (node.type === SearchStatementNodeType.FunctionCall) {
|
|
1505
|
+
const func = node as FunctionCall;
|
|
1506
|
+
const args = func.arguments.map((arg) =>
|
|
1507
|
+
this.evaluateNode(arg as any, dataSources)
|
|
1508
|
+
);
|
|
1509
|
+
const funcName = func.functionName?.toLowerCase();
|
|
1510
|
+
|
|
1511
|
+
switch (funcName) {
|
|
1512
|
+
case 'upper':
|
|
1513
|
+
return typeof args[0] === 'string'
|
|
1514
|
+
? args[0].toUpperCase()
|
|
1515
|
+
: args[0];
|
|
1516
|
+
case 'lower':
|
|
1517
|
+
return typeof args[0] === 'string'
|
|
1518
|
+
? args[0].toLowerCase()
|
|
1519
|
+
: args[0];
|
|
1520
|
+
case 'length':
|
|
1521
|
+
return typeof args[0] === 'string' ? args[0].length : 0;
|
|
1522
|
+
case 'concat':
|
|
1523
|
+
return args.map((a) => a ?? '').join('');
|
|
1524
|
+
case 'coalesce':
|
|
1525
|
+
return (
|
|
1526
|
+
args.find((a) => a !== null && a !== undefined) ?? null
|
|
1527
|
+
);
|
|
1528
|
+
case 'round': {
|
|
1529
|
+
const num = Number(args[0]);
|
|
1530
|
+
const decimals =
|
|
1531
|
+
args[1] !== undefined ? Number(args[1]) : 0;
|
|
1532
|
+
if (isNaN(num)) return null;
|
|
1533
|
+
const factor = Math.pow(10, decimals);
|
|
1534
|
+
return Math.round(num * factor) / factor;
|
|
1535
|
+
}
|
|
1536
|
+
case 'now':
|
|
1537
|
+
return new Date();
|
|
1538
|
+
case 'json_build_object': {
|
|
1539
|
+
const obj: any = {};
|
|
1540
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
1541
|
+
if (args[i] !== undefined && args[i] !== null) {
|
|
1542
|
+
obj[String(args[i])] = args[i + 1];
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
return obj;
|
|
1546
|
+
}
|
|
1547
|
+
case 'row_to_json':
|
|
1548
|
+
return args[0];
|
|
1549
|
+
default:
|
|
1550
|
+
return null;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
return null;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
evaluateAggregateNode(
|
|
1557
|
+
func: FunctionCall,
|
|
1558
|
+
rows: any[],
|
|
1559
|
+
statement: SearchStatementState
|
|
1560
|
+
): any {
|
|
1561
|
+
const funcName = func.functionName?.toLowerCase();
|
|
1562
|
+
const args = func.arguments;
|
|
1563
|
+
|
|
1564
|
+
if (funcName === 'count') {
|
|
1565
|
+
if (args[0]?.type === SearchStatementNodeType.AllColumnsSelector)
|
|
1566
|
+
return rows.length;
|
|
1567
|
+
let count = 0;
|
|
1568
|
+
rows.forEach((row) => {
|
|
1569
|
+
const dataSources = [
|
|
1570
|
+
{ source: statement.from, data: row },
|
|
1571
|
+
...statement.aggregations.map((a) => ({
|
|
1572
|
+
source: a.dataSource,
|
|
1573
|
+
data: row
|
|
1574
|
+
}))
|
|
1575
|
+
];
|
|
1576
|
+
const val = this.evaluateNode(
|
|
1577
|
+
args[0] as any,
|
|
1578
|
+
dataSources as any
|
|
1579
|
+
);
|
|
1580
|
+
if (val !== null && val !== undefined) count++;
|
|
1581
|
+
});
|
|
1582
|
+
return count;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
if (['sum', 'max', 'min', 'json_agg'].includes(funcName)) {
|
|
1586
|
+
let sum = 0,
|
|
1587
|
+
max: number | null = null,
|
|
1588
|
+
min: number | null = null;
|
|
1589
|
+
const arr: any[] = [];
|
|
1590
|
+
|
|
1591
|
+
rows.forEach((row) => {
|
|
1592
|
+
const dataSources = [
|
|
1593
|
+
{ source: statement.from, data: row },
|
|
1594
|
+
...statement.aggregations.map((a) => ({
|
|
1595
|
+
source: a.dataSource,
|
|
1596
|
+
data: row
|
|
1597
|
+
}))
|
|
1598
|
+
];
|
|
1599
|
+
const val = this.evaluateNode(
|
|
1600
|
+
args[0] as any,
|
|
1601
|
+
dataSources as any
|
|
1602
|
+
);
|
|
1603
|
+
|
|
1604
|
+
if (
|
|
1605
|
+
funcName === 'sum' &&
|
|
1606
|
+
typeof val === 'number' &&
|
|
1607
|
+
!isNaN(val)
|
|
1608
|
+
)
|
|
1609
|
+
sum += val;
|
|
1610
|
+
if (funcName === 'max' && val !== null && val !== undefined)
|
|
1611
|
+
max = max === null || val > max ? val : max;
|
|
1612
|
+
if (funcName === 'min' && val !== null && val !== undefined)
|
|
1613
|
+
min = min === null || val < min ? val : min;
|
|
1614
|
+
if (funcName === 'json_agg') arr.push(val);
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
if (funcName === 'sum') return sum;
|
|
1618
|
+
if (funcName === 'max') return max;
|
|
1619
|
+
if (funcName === 'min') return min;
|
|
1620
|
+
if (funcName === 'json_agg') return arr;
|
|
1621
|
+
}
|
|
1622
|
+
return null;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
checkCondition(
|
|
1626
|
+
condition: WhereStatement,
|
|
1627
|
+
dataSources: {
|
|
1628
|
+
source: DataSource;
|
|
1629
|
+
data: {
|
|
1630
|
+
[columnName: string]: RawResults;
|
|
1631
|
+
};
|
|
1632
|
+
}[]
|
|
1633
|
+
): boolean {
|
|
1634
|
+
if (!condition?.isValid) {
|
|
1635
|
+
return true;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
if (condition.operator && condition.left && condition.right) {
|
|
1639
|
+
let leftValue = this.evaluateNode(condition.left, dataSources);
|
|
1640
|
+
let rightValue = this.evaluateNode(condition.right, dataSources);
|
|
1641
|
+
|
|
1642
|
+
if (leftValue === undefined) {
|
|
1643
|
+
leftValue = null;
|
|
1644
|
+
}
|
|
1645
|
+
if (rightValue === undefined) {
|
|
1646
|
+
rightValue = null;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
switch (condition.operator) {
|
|
1650
|
+
case WhereStatementOperator.Equal: {
|
|
1651
|
+
return areValuesEqual(leftValue, rightValue);
|
|
1652
|
+
}
|
|
1653
|
+
case WhereStatementOperator.NotEqual: {
|
|
1654
|
+
return !areValuesEqual(leftValue, rightValue);
|
|
1655
|
+
}
|
|
1656
|
+
case WhereStatementOperator.Like: {
|
|
1657
|
+
if (leftValue === null || rightValue === null) {
|
|
1658
|
+
return false;
|
|
1659
|
+
}
|
|
1660
|
+
const regex = new RegExp(
|
|
1661
|
+
`^${rightValue.toString().replace(/%/g, '.*').replace(/_/g, '.')}$`
|
|
1662
|
+
);
|
|
1663
|
+
return regex.test(leftValue.toString());
|
|
1664
|
+
}
|
|
1665
|
+
case WhereStatementOperator.NotLike: {
|
|
1666
|
+
if (leftValue === null || rightValue === null) {
|
|
1667
|
+
return false;
|
|
1668
|
+
}
|
|
1669
|
+
const regex = new RegExp(
|
|
1670
|
+
`^${rightValue.toString().replace(/%/g, '.*').replace(/_/g, '.')}$`
|
|
1671
|
+
);
|
|
1672
|
+
return !regex.test(leftValue.toString());
|
|
1673
|
+
}
|
|
1674
|
+
case WhereStatementOperator.In: {
|
|
1675
|
+
if (!Array.isArray(rightValue))
|
|
1676
|
+
throw new Error(
|
|
1677
|
+
'Right value of "IN" operator must be an array'
|
|
1678
|
+
);
|
|
1679
|
+
return rightValue.some((val) =>
|
|
1680
|
+
areValuesEqual(leftValue, val)
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
1683
|
+
case WhereStatementOperator.Between: {
|
|
1684
|
+
if (!Array.isArray(rightValue) || rightValue.length !== 2)
|
|
1685
|
+
throw new Error(
|
|
1686
|
+
'Right value of "BETWEEN" operator must be an array of two elements'
|
|
1687
|
+
);
|
|
1688
|
+
if (
|
|
1689
|
+
leftValue === null ||
|
|
1690
|
+
rightValue[0] === null ||
|
|
1691
|
+
rightValue[1] === null
|
|
1692
|
+
) {
|
|
1693
|
+
return false;
|
|
1694
|
+
}
|
|
1695
|
+
const [l, r0] = coerceToComparable(
|
|
1696
|
+
leftValue,
|
|
1697
|
+
rightValue[0]
|
|
1698
|
+
);
|
|
1699
|
+
const [, r1] = coerceToComparable(l, rightValue[1]);
|
|
1700
|
+
return l >= r0 && l <= r1;
|
|
1701
|
+
}
|
|
1702
|
+
case WhereStatementOperator.BiggerThan: {
|
|
1703
|
+
if (leftValue === null || rightValue === null) {
|
|
1704
|
+
return false;
|
|
1705
|
+
}
|
|
1706
|
+
const [l, r] = coerceToComparable(leftValue, rightValue);
|
|
1707
|
+
return l > r;
|
|
1708
|
+
}
|
|
1709
|
+
case WhereStatementOperator.SmallerThan: {
|
|
1710
|
+
if (leftValue === null || rightValue === null) {
|
|
1711
|
+
return false;
|
|
1712
|
+
}
|
|
1713
|
+
const [l, r] = coerceToComparable(leftValue, rightValue);
|
|
1714
|
+
return l < r;
|
|
1715
|
+
}
|
|
1716
|
+
case WhereStatementOperator.BiggerThanOrEqualTo: {
|
|
1717
|
+
if (leftValue === null || rightValue === null) {
|
|
1718
|
+
return false;
|
|
1719
|
+
}
|
|
1720
|
+
const [l, r] = coerceToComparable(leftValue, rightValue);
|
|
1721
|
+
return l >= r;
|
|
1722
|
+
}
|
|
1723
|
+
case WhereStatementOperator.SmallerThanOrEqualTo: {
|
|
1724
|
+
if (leftValue === null || rightValue === null) {
|
|
1725
|
+
return false;
|
|
1726
|
+
}
|
|
1727
|
+
const [l, r] = coerceToComparable(leftValue, rightValue);
|
|
1728
|
+
return l <= r;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
let andGroupMatches = true;
|
|
1734
|
+
|
|
1735
|
+
if (!!condition.and.length) {
|
|
1736
|
+
andGroupMatches = condition.and.every((andCondition) =>
|
|
1737
|
+
this.checkCondition(andCondition, dataSources)
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
let orGroupMatches = true;
|
|
1742
|
+
|
|
1743
|
+
if (!!condition.or.length) {
|
|
1744
|
+
orGroupMatches = condition.or.some((orCondition) =>
|
|
1745
|
+
this.checkCondition(orCondition, dataSources)
|
|
1746
|
+
);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
return andGroupMatches && orGroupMatches;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
async applyJoinResults(
|
|
1753
|
+
statement: SearchStatementState,
|
|
1754
|
+
mainResults: {
|
|
1755
|
+
[columnName: string]: RawResults;
|
|
1756
|
+
}[],
|
|
1757
|
+
joins: {
|
|
1758
|
+
aggregation: AggregationStatement;
|
|
1759
|
+
results: {
|
|
1760
|
+
[columnName: string]: RawResults;
|
|
1761
|
+
}[];
|
|
1762
|
+
}[]
|
|
1763
|
+
): Promise<
|
|
1764
|
+
{
|
|
1765
|
+
[columnName: string]: RawResults;
|
|
1766
|
+
}[]
|
|
1767
|
+
> {
|
|
1768
|
+
let joinedResults = mainResults;
|
|
1769
|
+
|
|
1770
|
+
joinedResults = joins.reduce((acc, join) => {
|
|
1771
|
+
const newAcc = [...acc];
|
|
1772
|
+
|
|
1773
|
+
if (join.aggregation.joinType === AggregationStatementType.Inner) {
|
|
1774
|
+
// Inner join
|
|
1775
|
+
// Loop over each row in the main results
|
|
1776
|
+
// And join the results from the join results
|
|
1777
|
+
// If the join condition is met
|
|
1778
|
+
newAcc.forEach((mainRow) => {
|
|
1779
|
+
join.results.forEach((joinRow) => {
|
|
1780
|
+
// Check if the join condition is met
|
|
1781
|
+
// If it is, merge the results
|
|
1782
|
+
// If not, ignore the join result
|
|
1783
|
+
if (
|
|
1784
|
+
this.checkCondition(join.aggregation.on, [
|
|
1785
|
+
{
|
|
1786
|
+
source: statement.from,
|
|
1787
|
+
data: mainRow
|
|
1788
|
+
},
|
|
1789
|
+
{
|
|
1790
|
+
source: join.aggregation.dataSource,
|
|
1791
|
+
data: joinRow
|
|
1792
|
+
}
|
|
1793
|
+
])
|
|
1794
|
+
) {
|
|
1795
|
+
newAcc.push({ ...mainRow, ...joinRow });
|
|
1796
|
+
}
|
|
1797
|
+
});
|
|
1798
|
+
});
|
|
1799
|
+
} else if (
|
|
1800
|
+
join.aggregation.joinType === AggregationStatementType.Left
|
|
1801
|
+
) {
|
|
1802
|
+
// Left join
|
|
1803
|
+
// Loop over each row in the main results
|
|
1804
|
+
// And join the results from the join results
|
|
1805
|
+
// If the join condition is met
|
|
1806
|
+
newAcc.forEach((mainRow) => {
|
|
1807
|
+
let matchFound = false;
|
|
1808
|
+
|
|
1809
|
+
join.results.forEach((joinRow) => {
|
|
1810
|
+
// Check if the join condition is met
|
|
1811
|
+
// If it is, merge the results
|
|
1812
|
+
// If not, ignore the join result
|
|
1813
|
+
if (
|
|
1814
|
+
this.checkCondition(join.aggregation.on, [
|
|
1815
|
+
{
|
|
1816
|
+
source: statement.from,
|
|
1817
|
+
data: mainRow
|
|
1818
|
+
},
|
|
1819
|
+
{
|
|
1820
|
+
source: join.aggregation.dataSource,
|
|
1821
|
+
data: joinRow
|
|
1822
|
+
}
|
|
1823
|
+
])
|
|
1824
|
+
) {
|
|
1825
|
+
matchFound = true;
|
|
1826
|
+
newAcc.push({ ...mainRow, ...joinRow });
|
|
1827
|
+
}
|
|
1828
|
+
});
|
|
1829
|
+
|
|
1830
|
+
if (!matchFound) {
|
|
1831
|
+
const joinColumns = Object.keys(
|
|
1832
|
+
join.results[0] || {}
|
|
1833
|
+
).reduce(
|
|
1834
|
+
(a, key) => {
|
|
1835
|
+
a[key] = null;
|
|
1836
|
+
return a;
|
|
1837
|
+
},
|
|
1838
|
+
{} as {
|
|
1839
|
+
[columnName: string]:
|
|
1840
|
+
| number
|
|
1841
|
+
| string
|
|
1842
|
+
| boolean
|
|
1843
|
+
| null
|
|
1844
|
+
| Date;
|
|
1845
|
+
}
|
|
1846
|
+
);
|
|
1847
|
+
|
|
1848
|
+
newAcc.push({ ...mainRow, ...joinColumns });
|
|
1849
|
+
}
|
|
1850
|
+
});
|
|
1851
|
+
} else if (
|
|
1852
|
+
join.aggregation.joinType === AggregationStatementType.Right
|
|
1853
|
+
) {
|
|
1854
|
+
// Right join
|
|
1855
|
+
// Loop over each row in the main results
|
|
1856
|
+
// And join the results from the join results
|
|
1857
|
+
// If the join condition is met
|
|
1858
|
+
join.results.forEach((joinRow) => {
|
|
1859
|
+
let matchFound = false;
|
|
1860
|
+
// Check if the join condition is met
|
|
1861
|
+
// If it is, merge the results
|
|
1862
|
+
// If not, ignore the join result
|
|
1863
|
+
newAcc.forEach((mainRow) => {
|
|
1864
|
+
if (
|
|
1865
|
+
this.checkCondition(join.aggregation.on, [
|
|
1866
|
+
{
|
|
1867
|
+
source: statement.from,
|
|
1868
|
+
data: mainRow
|
|
1869
|
+
},
|
|
1870
|
+
{
|
|
1871
|
+
source: join.aggregation.dataSource,
|
|
1872
|
+
data: joinRow
|
|
1873
|
+
}
|
|
1874
|
+
])
|
|
1875
|
+
) {
|
|
1876
|
+
matchFound = true;
|
|
1877
|
+
newAcc.push({ ...mainRow, ...joinRow });
|
|
1878
|
+
}
|
|
1879
|
+
});
|
|
1880
|
+
|
|
1881
|
+
if (!matchFound) {
|
|
1882
|
+
const mainColumns = Object.keys(newAcc[0] || {}).reduce(
|
|
1883
|
+
(a, key) => {
|
|
1884
|
+
a[key] = null;
|
|
1885
|
+
return a;
|
|
1886
|
+
},
|
|
1887
|
+
{} as {
|
|
1888
|
+
[columnName: string]:
|
|
1889
|
+
| number
|
|
1890
|
+
| string
|
|
1891
|
+
| boolean
|
|
1892
|
+
| null
|
|
1893
|
+
| Date;
|
|
1894
|
+
}
|
|
1895
|
+
);
|
|
1896
|
+
|
|
1897
|
+
newAcc.push({ ...mainColumns, ...joinRow });
|
|
1898
|
+
}
|
|
1899
|
+
});
|
|
1900
|
+
} else if (
|
|
1901
|
+
join.aggregation.joinType === AggregationStatementType.Full
|
|
1902
|
+
) {
|
|
1903
|
+
// Full join
|
|
1904
|
+
// Loop over each row in the main results
|
|
1905
|
+
// And join the results from the join results
|
|
1906
|
+
// If the join condition is met
|
|
1907
|
+
const matchedRows = new Set();
|
|
1908
|
+
|
|
1909
|
+
newAcc.forEach((mainRow) => {
|
|
1910
|
+
let matchFound = false;
|
|
1911
|
+
|
|
1912
|
+
join.results.forEach((joinRow) => {
|
|
1913
|
+
if (
|
|
1914
|
+
this.checkCondition(join.aggregation.on, [
|
|
1915
|
+
{
|
|
1916
|
+
source: statement.from,
|
|
1917
|
+
data: mainRow
|
|
1918
|
+
},
|
|
1919
|
+
{
|
|
1920
|
+
source: join.aggregation.dataSource,
|
|
1921
|
+
data: joinRow
|
|
1922
|
+
}
|
|
1923
|
+
])
|
|
1924
|
+
) {
|
|
1925
|
+
matchFound = true;
|
|
1926
|
+
newAcc.push({ ...mainRow, ...joinRow });
|
|
1927
|
+
matchedRows.add(joinRow);
|
|
1928
|
+
}
|
|
1929
|
+
});
|
|
1930
|
+
|
|
1931
|
+
if (!matchFound) {
|
|
1932
|
+
const joinColumns = Object.keys(
|
|
1933
|
+
join.results[0] || {}
|
|
1934
|
+
).reduce(
|
|
1935
|
+
(a, key) => {
|
|
1936
|
+
a[key] = null;
|
|
1937
|
+
return a;
|
|
1938
|
+
},
|
|
1939
|
+
{} as {
|
|
1940
|
+
[columnName: string]:
|
|
1941
|
+
| number
|
|
1942
|
+
| string
|
|
1943
|
+
| boolean
|
|
1944
|
+
| null
|
|
1945
|
+
| Date;
|
|
1946
|
+
}
|
|
1947
|
+
);
|
|
1948
|
+
|
|
1949
|
+
newAcc.push({ ...mainRow, ...joinColumns });
|
|
1950
|
+
}
|
|
1951
|
+
});
|
|
1952
|
+
|
|
1953
|
+
join.results.forEach((joinRow) => {
|
|
1954
|
+
if (!matchedRows.has(joinRow)) {
|
|
1955
|
+
const mainColumns = Object.keys(newAcc[0] || {}).reduce(
|
|
1956
|
+
(a, key) => {
|
|
1957
|
+
a[key] = null;
|
|
1958
|
+
return a;
|
|
1959
|
+
},
|
|
1960
|
+
{} as {
|
|
1961
|
+
[columnName: string]:
|
|
1962
|
+
| number
|
|
1963
|
+
| string
|
|
1964
|
+
| boolean
|
|
1965
|
+
| null
|
|
1966
|
+
| Date;
|
|
1967
|
+
}
|
|
1968
|
+
);
|
|
1969
|
+
|
|
1970
|
+
newAcc.push({ ...mainColumns, ...joinRow });
|
|
1971
|
+
}
|
|
1972
|
+
});
|
|
1973
|
+
} else if (
|
|
1974
|
+
join.aggregation.joinType === AggregationStatementType.Cross
|
|
1975
|
+
) {
|
|
1976
|
+
// Cross join
|
|
1977
|
+
// Loop over each row in the main results
|
|
1978
|
+
// And join the results from the join results
|
|
1979
|
+
// If the join condition is met
|
|
1980
|
+
newAcc.forEach((mainRow) => {
|
|
1981
|
+
join.results.forEach((joinRow) => {
|
|
1982
|
+
// Check if the join condition is met
|
|
1983
|
+
// If it is, merge the results
|
|
1984
|
+
// If not, ignore the join result
|
|
1985
|
+
newAcc.push({ ...mainRow, ...joinRow });
|
|
1986
|
+
});
|
|
1987
|
+
});
|
|
1988
|
+
} else if (
|
|
1989
|
+
join.aggregation.joinType === AggregationStatementType.Self
|
|
1990
|
+
) {
|
|
1991
|
+
// Self join
|
|
1992
|
+
// Loop over each row in the main results
|
|
1993
|
+
// And join the results from the join results
|
|
1994
|
+
// If the join condition is met
|
|
1995
|
+
newAcc.forEach((mainRow) => {
|
|
1996
|
+
join.results.forEach((joinRow) => {
|
|
1997
|
+
// Check if the join condition is met
|
|
1998
|
+
// If it is, merge the results
|
|
1999
|
+
// If not, ignore the join result
|
|
2000
|
+
if (
|
|
2001
|
+
this.checkCondition(join.aggregation.on, [
|
|
2002
|
+
{
|
|
2003
|
+
source: statement.from,
|
|
2004
|
+
data: mainRow
|
|
2005
|
+
},
|
|
2006
|
+
{
|
|
2007
|
+
source: join.aggregation.dataSource,
|
|
2008
|
+
data: joinRow
|
|
2009
|
+
}
|
|
2010
|
+
])
|
|
2011
|
+
) {
|
|
2012
|
+
newAcc.push({ ...mainRow, ...joinRow });
|
|
2013
|
+
}
|
|
2014
|
+
});
|
|
2015
|
+
});
|
|
2016
|
+
} else if (
|
|
2017
|
+
join.aggregation.joinType === AggregationStatementType.Natural
|
|
2018
|
+
) {
|
|
2019
|
+
// Get the keys that are common to both mainArray and joinArray rows
|
|
2020
|
+
const commonKeys =
|
|
2021
|
+
newAcc.length && join.results.length
|
|
2022
|
+
? Object.keys(newAcc[0]).filter(
|
|
2023
|
+
(key) => key in join.results[0]
|
|
2024
|
+
)
|
|
2025
|
+
: [];
|
|
2026
|
+
|
|
2027
|
+
// Check if two rows have matching values for all common keys
|
|
2028
|
+
function hasMatchingCommonKeys(
|
|
2029
|
+
row1: {
|
|
2030
|
+
[columnName: string]: RawResults;
|
|
2031
|
+
},
|
|
2032
|
+
row2: {
|
|
2033
|
+
[columnName: string]: RawResults;
|
|
2034
|
+
}
|
|
2035
|
+
) {
|
|
2036
|
+
return commonKeys.every((key) => row1[key] === row2[key]);
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
// Natural join
|
|
2040
|
+
// Loop over each row in the main results
|
|
2041
|
+
// And join the results from the join results
|
|
2042
|
+
// If the join condition is met
|
|
2043
|
+
newAcc.forEach((mainRow) => {
|
|
2044
|
+
join.results.forEach((joinRow) => {
|
|
2045
|
+
// Check if the join condition is met
|
|
2046
|
+
// If it is, merge the results
|
|
2047
|
+
// If not, ignore the join result
|
|
2048
|
+
if (hasMatchingCommonKeys(mainRow, joinRow)) {
|
|
2049
|
+
newAcc.push({ ...mainRow, ...joinRow });
|
|
2050
|
+
}
|
|
2051
|
+
});
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
return newAcc;
|
|
2056
|
+
}, joinedResults);
|
|
2057
|
+
|
|
2058
|
+
return joinedResults;
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
async applyWhereToResults(
|
|
2062
|
+
statement: SearchStatementState,
|
|
2063
|
+
results: {
|
|
2064
|
+
[columnName: string]: RawResults;
|
|
2065
|
+
}[]
|
|
2066
|
+
): Promise<
|
|
2067
|
+
{
|
|
2068
|
+
[columnName: string]: RawResults;
|
|
2069
|
+
}[]
|
|
2070
|
+
> {
|
|
2071
|
+
const whereClause = statement.where;
|
|
2072
|
+
|
|
2073
|
+
if (!whereClause) {
|
|
2074
|
+
return results;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
const filteredResults = results.filter((result) => {
|
|
2078
|
+
const allSources = [
|
|
2079
|
+
{
|
|
2080
|
+
source: statement.from,
|
|
2081
|
+
data: result
|
|
2082
|
+
},
|
|
2083
|
+
...statement.aggregations.map((aggregation) => ({
|
|
2084
|
+
source: aggregation.dataSource,
|
|
2085
|
+
data: result
|
|
2086
|
+
}))
|
|
2087
|
+
];
|
|
2088
|
+
|
|
2089
|
+
return this.checkCondition(whereClause, allSources);
|
|
2090
|
+
});
|
|
2091
|
+
|
|
2092
|
+
return filteredResults;
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
mapResults(
|
|
2096
|
+
statement: SearchStatementState,
|
|
2097
|
+
rows: { [columnName: string]: RawResults }[]
|
|
2098
|
+
): { [columnName: string]: RawResults }[] {
|
|
2099
|
+
// 1. Check for aggregates (Giant Bucket Rule)
|
|
2100
|
+
const hasAggregates = statement.validSelections.some(
|
|
2101
|
+
(selection) =>
|
|
2102
|
+
selection.type === SearchStatementNodeType.FunctionCall &&
|
|
2103
|
+
(selection as FunctionCall).hasFunctionOfCategory(
|
|
2104
|
+
SQLFunctionCategory.Aggregate
|
|
2105
|
+
)
|
|
2106
|
+
);
|
|
2107
|
+
|
|
2108
|
+
if (hasAggregates) {
|
|
2109
|
+
const mappedResult: any = {};
|
|
2110
|
+
statement.validSelections.forEach((selection) => {
|
|
2111
|
+
// 1. Guard against AllColumnsSelector (it doesn't have an .as property)
|
|
2112
|
+
if (
|
|
2113
|
+
selection.type ===
|
|
2114
|
+
SearchStatementNodeType.AllColumnsSelector
|
|
2115
|
+
)
|
|
2116
|
+
return;
|
|
2117
|
+
|
|
2118
|
+
if (selection.type === SearchStatementNodeType.FunctionCall) {
|
|
2119
|
+
// 2. Cast to FunctionCall
|
|
2120
|
+
mappedResult[(selection as FunctionCall).as] =
|
|
2121
|
+
this.evaluateAggregateNode(
|
|
2122
|
+
selection as FunctionCall,
|
|
2123
|
+
rows,
|
|
2124
|
+
statement
|
|
2125
|
+
);
|
|
2126
|
+
} else {
|
|
2127
|
+
// 3. Cast to ColumnRef
|
|
2128
|
+
mappedResult[(selection as ColumnRef).as] =
|
|
2129
|
+
rows.length > 0
|
|
2130
|
+
? this.evaluateNode(selection, [
|
|
2131
|
+
{ source: statement.from, data: rows[0] }
|
|
2132
|
+
])
|
|
2133
|
+
: null;
|
|
2134
|
+
}
|
|
2135
|
+
});
|
|
2136
|
+
return [mappedResult]; // Return exactly 1 row containing the aggregations
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
// 2. Standard Scalar Mapping
|
|
2140
|
+
const mappedResults = rows.map((result) => {
|
|
2141
|
+
const mappedResult: any = {};
|
|
2142
|
+
const dataSources = [
|
|
2143
|
+
{ source: statement.from, data: result },
|
|
2144
|
+
...statement.aggregations.map((a) => ({
|
|
2145
|
+
source: a.dataSource,
|
|
2146
|
+
data: result
|
|
2147
|
+
}))
|
|
2148
|
+
];
|
|
2149
|
+
|
|
2150
|
+
const selectAll = statement.validSelections.find(
|
|
2151
|
+
(selection) =>
|
|
2152
|
+
selection.type ===
|
|
2153
|
+
SearchStatementNodeType.AllColumnsSelector
|
|
2154
|
+
);
|
|
2155
|
+
|
|
2156
|
+
if (selectAll) {
|
|
2157
|
+
statement.columns.forEach((columnRef) => {
|
|
2158
|
+
mappedResult[columnRef.as] = this.evaluateNode(
|
|
2159
|
+
columnRef,
|
|
2160
|
+
dataSources
|
|
2161
|
+
);
|
|
2162
|
+
});
|
|
2163
|
+
return mappedResult;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
statement.validSelections.forEach((selection) => {
|
|
2167
|
+
if (!selection.isValid) return;
|
|
2168
|
+
|
|
2169
|
+
// 1. Tell TypeScript to skip AllColumnsSelector here too
|
|
2170
|
+
if (
|
|
2171
|
+
selection.type ===
|
|
2172
|
+
SearchStatementNodeType.AllColumnsSelector
|
|
2173
|
+
)
|
|
2174
|
+
return;
|
|
2175
|
+
|
|
2176
|
+
// 2. Safely cast to access the .as property
|
|
2177
|
+
const selectionWithAlias = selection as
|
|
2178
|
+
| ColumnRef
|
|
2179
|
+
| FunctionCall;
|
|
2180
|
+
|
|
2181
|
+
// Evaluate the node (Scalar Function or ColumnRef) directly from the row context!
|
|
2182
|
+
mappedResult[selectionWithAlias.as] = this.evaluateNode(
|
|
2183
|
+
selection,
|
|
2184
|
+
dataSources
|
|
2185
|
+
);
|
|
2186
|
+
});
|
|
2187
|
+
|
|
2188
|
+
return mappedResult;
|
|
2189
|
+
});
|
|
2190
|
+
|
|
2191
|
+
return mappedResults;
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
selectionsToColumns(statement: SearchStatementState): ColumnRef[] {
|
|
2195
|
+
const selectAll = statement.validSelections.find(
|
|
2196
|
+
(selection) =>
|
|
2197
|
+
selection.type === SearchStatementNodeType.AllColumnsSelector
|
|
2198
|
+
);
|
|
2199
|
+
|
|
2200
|
+
if (selectAll) {
|
|
2201
|
+
const allColumns = statement.columns;
|
|
2202
|
+
|
|
2203
|
+
return allColumns;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
return statement.columns;
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
// This function orders the rows
|
|
2210
|
+
// Please note: we use jsstore for sorting simply because it has the logic built in
|
|
2211
|
+
// But we don't use it as a normal select from the database, we pass it the rows we already have
|
|
2212
|
+
async applySort(
|
|
2213
|
+
statement: SearchStatementState,
|
|
2214
|
+
rows: {
|
|
2215
|
+
[columnName: string]: RawResults;
|
|
2216
|
+
}[]
|
|
2217
|
+
): Promise<
|
|
2218
|
+
{
|
|
2219
|
+
[columnName: string]: RawResults;
|
|
2220
|
+
}[]
|
|
2221
|
+
> {
|
|
2222
|
+
const sorts = statement.validSortings;
|
|
2223
|
+
|
|
2224
|
+
if (!sorts.length) {
|
|
2225
|
+
return rows;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
const jsStoreSorts: IOrderQuery[] = sorts.map((sort) => {
|
|
2229
|
+
const column = sort.column;
|
|
2230
|
+
|
|
2231
|
+
const jsStoreSort: IOrderQuery = {
|
|
2232
|
+
by: column.as,
|
|
2233
|
+
type:
|
|
2234
|
+
sort.direction === SortStatementDirection.Ascending
|
|
2235
|
+
? 'asc'
|
|
2236
|
+
: 'desc'
|
|
2237
|
+
};
|
|
2238
|
+
|
|
2239
|
+
return jsStoreSort;
|
|
2240
|
+
});
|
|
2241
|
+
|
|
2242
|
+
// @ts-ignore It asks for properties that it doesn't need, like "meta.primaryKey"
|
|
2243
|
+
const sortedRows = await this.connection.select({
|
|
2244
|
+
store: rows,
|
|
2245
|
+
order: jsStoreSorts
|
|
2246
|
+
});
|
|
2247
|
+
|
|
2248
|
+
return sortedRows as {
|
|
2249
|
+
[columnName: string]: RawResults;
|
|
2250
|
+
}[];
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
applyLimitAndOffset(
|
|
2254
|
+
statement: SearchStatementState,
|
|
2255
|
+
rows: {
|
|
2256
|
+
[columnName: string]: RawResults;
|
|
2257
|
+
}[]
|
|
2258
|
+
): {
|
|
2259
|
+
[columnName: string]: RawResults;
|
|
2260
|
+
}[] {
|
|
2261
|
+
let offsetResults = rows;
|
|
2262
|
+
|
|
2263
|
+
if (
|
|
2264
|
+
!!statement.offset &&
|
|
2265
|
+
statement.offset.type === SearchStatementNodeType.LiteralValue &&
|
|
2266
|
+
typeof statement.offset.value === 'number'
|
|
2267
|
+
) {
|
|
2268
|
+
const offset = statement.offset.value;
|
|
2269
|
+
|
|
2270
|
+
offsetResults = rows.slice(offset);
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
if (
|
|
2274
|
+
!!statement.limit &&
|
|
2275
|
+
statement.limit.type === SearchStatementNodeType.LiteralValue &&
|
|
2276
|
+
typeof statement.limit.value === 'number'
|
|
2277
|
+
) {
|
|
2278
|
+
const limit = statement.limit.value;
|
|
2279
|
+
|
|
2280
|
+
offsetResults = offsetResults.slice(0, limit);
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
return offsetResults;
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
async executeSearchStatementAsQuery(
|
|
2287
|
+
statement: SearchStatementState
|
|
2288
|
+
): Promise<ISelectQueryResultV2 | null> {
|
|
2289
|
+
if (!statement) {
|
|
2290
|
+
return null;
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
if (!statement.from) {
|
|
2294
|
+
// Check for literal values
|
|
2295
|
+
let row: {
|
|
2296
|
+
[columnName: string]: RawResults;
|
|
2297
|
+
} = {};
|
|
2298
|
+
|
|
2299
|
+
statement.validSelectedColumns.forEach((column) => {
|
|
2300
|
+
row[column.as] = column.literalValue?.flattenRawValue();
|
|
2301
|
+
});
|
|
2302
|
+
|
|
2303
|
+
const literalSelectionResult: ISelectQueryResultV2 = {
|
|
2304
|
+
columns: statement.validSelectedColumns,
|
|
2305
|
+
rows: [row],
|
|
2306
|
+
tableName: null,
|
|
2307
|
+
entity: null,
|
|
2308
|
+
total: 1
|
|
2309
|
+
};
|
|
2310
|
+
|
|
2311
|
+
return literalSelectionResult;
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
let mainResults: {
|
|
2315
|
+
[columnName: string]: RawResults;
|
|
2316
|
+
}[] = [];
|
|
2317
|
+
|
|
2318
|
+
let mainEntity: DefinitionEntityState | null = null;
|
|
2319
|
+
|
|
2320
|
+
if (
|
|
2321
|
+
statement.from.source.type ===
|
|
2322
|
+
SearchStatementNodeType.NestedSearchStatement
|
|
2323
|
+
) {
|
|
2324
|
+
const subQueryResult = await this.executeSearchStatementAsQuery(
|
|
2325
|
+
// @ts-ignore
|
|
2326
|
+
statement.from.source.statement
|
|
2327
|
+
);
|
|
2328
|
+
|
|
2329
|
+
mainResults = subQueryResult.rows;
|
|
2330
|
+
} else if (statement.from.source.type === EntityType.DefinitionEntity) {
|
|
2331
|
+
const mainSourceResults = await this.select({
|
|
2332
|
+
from: toPascalCase(statement.from.source.name)
|
|
2333
|
+
});
|
|
2334
|
+
|
|
2335
|
+
mainResults = mainSourceResults;
|
|
2336
|
+
mainEntity = statement.from.source;
|
|
2337
|
+
} else if (
|
|
2338
|
+
statement.from.sourceType === DataSourceType.Function &&
|
|
2339
|
+
statement.from.functionCall
|
|
2340
|
+
) {
|
|
2341
|
+
// --- NEW: EMULATE GENERATE_SERIES AND UNNEST ---
|
|
2342
|
+
const func = statement.from.functionCall;
|
|
2343
|
+
const funcName = func.functionName.toLowerCase();
|
|
2344
|
+
if (funcName === 'generate_series') {
|
|
2345
|
+
const start = Number(
|
|
2346
|
+
this.evaluateNode(func.arguments[0] as any, [])
|
|
2347
|
+
);
|
|
2348
|
+
const stop = Number(
|
|
2349
|
+
this.evaluateNode(func.arguments[1] as any, [])
|
|
2350
|
+
);
|
|
2351
|
+
const step = func.arguments[2]
|
|
2352
|
+
? Number(this.evaluateNode(func.arguments[2] as any, []))
|
|
2353
|
+
: 1;
|
|
2354
|
+
for (let i = start; i <= stop; i += step) {
|
|
2355
|
+
mainResults.push({ [statement.from.as]: i });
|
|
2356
|
+
}
|
|
2357
|
+
} else if (funcName === 'unnest') {
|
|
2358
|
+
const arr = this.evaluateNode(func.arguments[0] as any, []);
|
|
2359
|
+
if (Array.isArray(arr)) {
|
|
2360
|
+
mainResults = arr.map((item) => ({
|
|
2361
|
+
[statement.from.as]: item
|
|
2362
|
+
}));
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
const aggregationResults: {
|
|
2368
|
+
aggregation: AggregationStatement;
|
|
2369
|
+
results: {
|
|
2370
|
+
[columnName: string]: number | string | boolean | null | Date;
|
|
2371
|
+
}[];
|
|
2372
|
+
}[] = !!statement.from
|
|
2373
|
+
? await Promise.all(
|
|
2374
|
+
statement.aggregations.map(async (aggregation) => {
|
|
2375
|
+
if (
|
|
2376
|
+
aggregation.dataSource?.source?.type ===
|
|
2377
|
+
SearchStatementNodeType.NestedSearchStatement
|
|
2378
|
+
) {
|
|
2379
|
+
const results =
|
|
2380
|
+
await this.executeSearchStatementAsQuery(
|
|
2381
|
+
// @ts-ignore
|
|
2382
|
+
aggregation.dataSource?.source?.statement
|
|
2383
|
+
);
|
|
2384
|
+
|
|
2385
|
+
return {
|
|
2386
|
+
aggregation,
|
|
2387
|
+
results:
|
|
2388
|
+
(results?.rows as {
|
|
2389
|
+
[columnName: string]:
|
|
2390
|
+
| number
|
|
2391
|
+
| string
|
|
2392
|
+
| boolean
|
|
2393
|
+
| null
|
|
2394
|
+
| Date;
|
|
2395
|
+
}[]) || []
|
|
2396
|
+
};
|
|
2397
|
+
} else if (
|
|
2398
|
+
aggregation.dataSource?.source?.type ===
|
|
2399
|
+
EntityType.DefinitionEntity
|
|
2400
|
+
) {
|
|
2401
|
+
const results = await this.select({
|
|
2402
|
+
from: toPascalCase(
|
|
2403
|
+
aggregation.dataSource?.source?.name
|
|
2404
|
+
)
|
|
2405
|
+
});
|
|
2406
|
+
|
|
2407
|
+
return {
|
|
2408
|
+
aggregation,
|
|
2409
|
+
results: results as {
|
|
2410
|
+
[columnName: string]:
|
|
2411
|
+
| number
|
|
2412
|
+
| string
|
|
2413
|
+
| boolean
|
|
2414
|
+
| null
|
|
2415
|
+
| Date;
|
|
2416
|
+
}[]
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
return {
|
|
2421
|
+
aggregation,
|
|
2422
|
+
results: []
|
|
2423
|
+
};
|
|
2424
|
+
})
|
|
2425
|
+
)
|
|
2426
|
+
: [];
|
|
2427
|
+
|
|
2428
|
+
const joinedResults = await this.applyJoinResults(
|
|
2429
|
+
statement,
|
|
2430
|
+
mainResults,
|
|
2431
|
+
aggregationResults
|
|
2432
|
+
);
|
|
2433
|
+
|
|
2434
|
+
const filteredResults = !!statement.from
|
|
2435
|
+
? await this.applyWhereToResults(statement, joinedResults)
|
|
2436
|
+
: joinedResults;
|
|
2437
|
+
|
|
2438
|
+
const mappedResults = this.mapResults(statement, filteredResults);
|
|
2439
|
+
|
|
2440
|
+
// Check if this is a pure table or there are aggregations or aliases
|
|
2441
|
+
if (!statement.isUntouchedEntity) {
|
|
2442
|
+
mainEntity = null;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
const sortedResults = await this.applySort(statement, mappedResults);
|
|
2446
|
+
|
|
2447
|
+
// Apply limit and offset
|
|
2448
|
+
const offsetResults = this.applyLimitAndOffset(
|
|
2449
|
+
statement,
|
|
2450
|
+
sortedResults
|
|
2451
|
+
);
|
|
2452
|
+
|
|
2453
|
+
const result = {
|
|
2454
|
+
columns: statement.validSelectedColumns,
|
|
2455
|
+
rows: offsetResults,
|
|
2456
|
+
tableName: !!mainEntity ? toPascalCase(mainEntity.name) : null,
|
|
2457
|
+
entity: mainEntity,
|
|
2458
|
+
total: offsetResults.length
|
|
2459
|
+
};
|
|
2460
|
+
|
|
2461
|
+
return result;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
async runStatement(
|
|
2465
|
+
statement: SearchStatementState,
|
|
2466
|
+
inputs: IDynamicValue[] = []
|
|
2467
|
+
): Promise<ISelectQueryResultV2 | null> {
|
|
2468
|
+
// console.log('[runStatement] Running statement: ', statement);
|
|
2469
|
+
console.log(
|
|
2470
|
+
`[runStatement] Inputs:\n ${inputs
|
|
2471
|
+
.map(
|
|
2472
|
+
(d) =>
|
|
2473
|
+
`${d.valueOwner?.type} (${d.valueOwner?.id}) "${resolveEntityName(d.valueOwner as any, this.store.project?.logic)}": ${JSON.stringify(d.value?.value, null, 2)}`
|
|
2474
|
+
)
|
|
2475
|
+
.join('\n ')}`
|
|
2476
|
+
);
|
|
2477
|
+
|
|
2478
|
+
if (!statement) {
|
|
2479
|
+
return null;
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
const queryString = searchStatementUtils.sanitizeQuery(
|
|
2483
|
+
statement.toQuery()
|
|
2484
|
+
);
|
|
2485
|
+
|
|
2486
|
+
if (!queryString) {
|
|
2487
|
+
return null;
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
let workingStatement = statement;
|
|
2491
|
+
|
|
2492
|
+
if (!!inputs?.length) {
|
|
2493
|
+
const resolvedInputs = inputs.map((i) => {
|
|
2494
|
+
if (i.valueOwner?.type === EntityType.ValueDescriptor) {
|
|
2495
|
+
// Exchange the value-descriptor for its input-map counterpart
|
|
2496
|
+
const parentInputs = ((i.valueOwner?.parent as SearchState)
|
|
2497
|
+
?.inputs || []) as InputMapState[];
|
|
2498
|
+
|
|
2499
|
+
const foundInputMap = parentInputs.find(
|
|
2500
|
+
(p) => p.declaration?.id === i.valueOwner?.id
|
|
2501
|
+
);
|
|
2502
|
+
|
|
2503
|
+
return {
|
|
2504
|
+
...i,
|
|
2505
|
+
valueOwner: foundInputMap
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
return i;
|
|
2510
|
+
});
|
|
2511
|
+
|
|
2512
|
+
const query = this.resolveRawInterpolationValues(
|
|
2513
|
+
queryString,
|
|
2514
|
+
resolvedInputs
|
|
2515
|
+
);
|
|
2516
|
+
|
|
2517
|
+
console.log('Query:\n', query);
|
|
2518
|
+
|
|
2519
|
+
const sanitizedAST = SQLASTLib.parse(query, {
|
|
2520
|
+
dialect: 'sqlite',
|
|
2521
|
+
// These are optional:
|
|
2522
|
+
includeSpaces: true // Adds spaces/tabs
|
|
2523
|
+
// includeNewlines: true, // Adds newlines
|
|
2524
|
+
// includeComments: true, // Adds comments
|
|
2525
|
+
// includeRange: true, // Adds source code location data
|
|
2526
|
+
});
|
|
2527
|
+
|
|
2528
|
+
const newStatementConfig =
|
|
2529
|
+
searchStatementUtils.hydrateSearchStatementState(
|
|
2530
|
+
sanitizedAST,
|
|
2531
|
+
this.store.project.logic
|
|
2532
|
+
);
|
|
2533
|
+
workingStatement = newStatementConfig.state;
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
const result =
|
|
2537
|
+
await this.executeSearchStatementAsQuery(workingStatement);
|
|
2538
|
+
|
|
2539
|
+
this.onGoingQueryCache = {};
|
|
2540
|
+
|
|
2541
|
+
return result;
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
async processSelectQuery(
|
|
2545
|
+
selectStatement: SelectStmt
|
|
2546
|
+
): Promise<ISelectQueryResult> {
|
|
2547
|
+
if (!selectStatement) {
|
|
2548
|
+
return null;
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
// @ts-ignore
|
|
2552
|
+
if (this.onGoingQueryCache[selectStatement.id]) {
|
|
2553
|
+
// @ts-ignore
|
|
2554
|
+
return this.onGoingQueryCache[selectStatement.id];
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
let queryBase!:
|
|
2558
|
+
| {
|
|
2559
|
+
store: {
|
|
2560
|
+
[key: string]: number | string | boolean | null | Date;
|
|
2561
|
+
}[];
|
|
2562
|
+
meta: {
|
|
2563
|
+
primaryKey: string;
|
|
2564
|
+
};
|
|
2565
|
+
}
|
|
2566
|
+
| {
|
|
2567
|
+
from: string;
|
|
2568
|
+
};
|
|
2569
|
+
|
|
2570
|
+
const mainSource =
|
|
2571
|
+
searchStatementUtils.getMainSourceFromSelectStatement(
|
|
2572
|
+
selectStatement
|
|
2573
|
+
);
|
|
2574
|
+
const selectClause =
|
|
2575
|
+
searchStatementUtils.getSelectClauseFromSelectStatement(
|
|
2576
|
+
selectStatement
|
|
2577
|
+
);
|
|
2578
|
+
const ownTableMappings: searchStatementUtils.IShallowColumnMapping[] =
|
|
2579
|
+
searchStatementUtils.fromSelectClauseColumnsToColumnMappings(
|
|
2580
|
+
selectClause,
|
|
2581
|
+
(mainSource as any)?.name || ''
|
|
2582
|
+
// (mainSource as any).name === 'Product'
|
|
2583
|
+
);
|
|
2584
|
+
|
|
2585
|
+
const otherNewMappingsInTopSelect: searchStatementUtils.IShallowColumnMapping[] =
|
|
2586
|
+
searchStatementUtils
|
|
2587
|
+
.fromSelectClauseColumnsToColumnMappings(selectClause)
|
|
2588
|
+
.filter(
|
|
2589
|
+
(mapping) =>
|
|
2590
|
+
!ownTableMappings.find(
|
|
2591
|
+
(ownMapping) => ownMapping.from === mapping.from
|
|
2592
|
+
)
|
|
2593
|
+
);
|
|
2594
|
+
|
|
2595
|
+
let ownColumns: searchStatementUtils.IColumnMapping[] = [];
|
|
2596
|
+
let mainEntity: DefinitionEntityState = null;
|
|
2597
|
+
|
|
2598
|
+
if (mainSource?.type === 'table') {
|
|
2599
|
+
queryBase = {
|
|
2600
|
+
from: mainSource.name
|
|
2601
|
+
};
|
|
2602
|
+
|
|
2603
|
+
const table = this.getTable(mainSource.name);
|
|
2604
|
+
|
|
2605
|
+
mainEntity = table.entity;
|
|
2606
|
+
|
|
2607
|
+
ownColumns = table.columns.map((column) => {
|
|
2608
|
+
const mapping = ownTableMappings.find(
|
|
2609
|
+
(mapping) => mapping.from === column.columnName
|
|
2610
|
+
);
|
|
2611
|
+
|
|
2612
|
+
return {
|
|
2613
|
+
column: column,
|
|
2614
|
+
as: mapping?.to || column.columnName
|
|
2615
|
+
};
|
|
2616
|
+
});
|
|
2617
|
+
} else if (mainSource?.type === 'query' && !!mainSource.subquery) {
|
|
2618
|
+
// Execute the 'from' subquery and get the result
|
|
2619
|
+
const subQueryResult = await this.processSelectQuery(
|
|
2620
|
+
mainSource.subquery
|
|
2621
|
+
);
|
|
2622
|
+
mainEntity = subQueryResult.entity;
|
|
2623
|
+
|
|
2624
|
+
queryBase = {
|
|
2625
|
+
store: subQueryResult.rows,
|
|
2626
|
+
meta: {
|
|
2627
|
+
primaryKey: subQueryResult.columns.find(
|
|
2628
|
+
(column) => column.column.primaryKey
|
|
2629
|
+
).column.columnName
|
|
2630
|
+
}
|
|
2631
|
+
};
|
|
2632
|
+
|
|
2633
|
+
ownColumns = subQueryResult.columns.map((column) => {
|
|
2634
|
+
const mapping = ownTableMappings.find(
|
|
2635
|
+
(mapping) => mapping.from === column.as
|
|
2636
|
+
);
|
|
2637
|
+
|
|
2638
|
+
return {
|
|
2639
|
+
column: column.column,
|
|
2640
|
+
as: mapping?.to || column.as
|
|
2641
|
+
};
|
|
2642
|
+
});
|
|
2643
|
+
} else {
|
|
2644
|
+
// Not supported
|
|
2645
|
+
return {
|
|
2646
|
+
columns: [],
|
|
2647
|
+
rows: [],
|
|
2648
|
+
tableName: null,
|
|
2649
|
+
entity: null,
|
|
2650
|
+
total: 0
|
|
2651
|
+
};
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
let selectQuery: ISelectQuery = queryBase as ISelectQuery;
|
|
2655
|
+
|
|
2656
|
+
const offsetClause: LimitClause | null =
|
|
2657
|
+
searchStatementUtils.getLimitClauseFromSelectStatement(
|
|
2658
|
+
selectStatement
|
|
2659
|
+
);
|
|
2660
|
+
|
|
2661
|
+
const rawOffset: number =
|
|
2662
|
+
searchStatementUtils.getRawOffetFromLimitClause(
|
|
2663
|
+
offsetClause
|
|
2664
|
+
) as number;
|
|
2665
|
+
const rawLimit: number | null =
|
|
2666
|
+
searchStatementUtils.getRawLimitFromLimitClause(offsetClause) as
|
|
2667
|
+
| number
|
|
2668
|
+
| null;
|
|
2669
|
+
|
|
2670
|
+
const orderByClause =
|
|
2671
|
+
searchStatementUtils.getOrderByClauseFromSelectStatement(
|
|
2672
|
+
selectStatement
|
|
2673
|
+
);
|
|
2674
|
+
|
|
2675
|
+
const joinClauses = await getJoinClausesFromSelectStatement(
|
|
2676
|
+
selectStatement,
|
|
2677
|
+
{
|
|
2678
|
+
onNestedSelect: this.processSelectQuery,
|
|
2679
|
+
onDescribeTable: async (tableName) => this.getTable(tableName)
|
|
2680
|
+
}
|
|
2681
|
+
);
|
|
2682
|
+
|
|
2683
|
+
if (rawLimit !== null) {
|
|
2684
|
+
selectQuery.limit = rawLimit;
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
if (rawOffset) {
|
|
2688
|
+
selectQuery.skip = rawOffset;
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
if (orderByClause) {
|
|
2692
|
+
selectQuery.order = await fromOrderByClauseToJsStore(
|
|
2693
|
+
orderByClause,
|
|
2694
|
+
selectStatement,
|
|
2695
|
+
{
|
|
2696
|
+
onNestedSelect: this.processSelectQuery,
|
|
2697
|
+
onDescribeTable: async (tableName) =>
|
|
2698
|
+
this.getTable(tableName)
|
|
2699
|
+
}
|
|
2700
|
+
);
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
if (!!joinClauses.joins.length) {
|
|
2704
|
+
selectQuery.join = joinClauses.joins;
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
const allSelectedColumnNames =
|
|
2708
|
+
getAllSelectedColumnNamesFromSelectClause(selectClause);
|
|
2709
|
+
|
|
2710
|
+
const builtInPrimaryKeyPropertyId =
|
|
2711
|
+
BUILT_IN_BASE_ENTITY_IDS[EntityType.BuiltInBaseEntity][
|
|
2712
|
+
BaseEntityNames.PERSISTED_ENTITY
|
|
2713
|
+
].properties.primaryKey.id;
|
|
2714
|
+
|
|
2715
|
+
let primaryKeyName = mainEntity.properties.find(
|
|
2716
|
+
(property) =>
|
|
2717
|
+
property.id === builtInPrimaryKeyPropertyId ||
|
|
2718
|
+
property.implements.find(
|
|
2719
|
+
(impl) => impl.id === builtInPrimaryKeyPropertyId
|
|
2720
|
+
)
|
|
2721
|
+
)?.name;
|
|
2722
|
+
|
|
2723
|
+
const allColumns: searchStatementUtils.IColumnMapping[] = [
|
|
2724
|
+
...ownColumns,
|
|
2725
|
+
...joinClauses.columnMappings
|
|
2726
|
+
// Filter out duplicates
|
|
2727
|
+
].filter((column, index, self) => {
|
|
2728
|
+
return self.findIndex((c) => c.as === column.as) === index;
|
|
2729
|
+
});
|
|
2730
|
+
|
|
2731
|
+
// We need to filter and rename any columns that have changed or aren't selected in t he querys
|
|
2732
|
+
const resultingQueryColumns: searchStatementUtils.IColumnMapping[] =
|
|
2733
|
+
allColumns.reduce((acc, columnMap) => {
|
|
2734
|
+
const alreadyExists = acc.find((c) => c.as === columnMap.as);
|
|
2735
|
+
|
|
2736
|
+
if (alreadyExists) {
|
|
2737
|
+
return acc;
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
const newlyMapped = otherNewMappingsInTopSelect.find(
|
|
2741
|
+
(mapping) => mapping.from === columnMap.as
|
|
2742
|
+
);
|
|
2743
|
+
|
|
2744
|
+
columnMap.as = newlyMapped?.to || columnMap.as;
|
|
2745
|
+
|
|
2746
|
+
const selected =
|
|
2747
|
+
allSelectedColumnNames.includes(
|
|
2748
|
+
columnMap.column.columnName
|
|
2749
|
+
) || allSelectedColumnNames.includes(columnMap?.as);
|
|
2750
|
+
|
|
2751
|
+
if (columnMap?.column.columnName === primaryKeyName) {
|
|
2752
|
+
primaryKeyName = columnMap.as;
|
|
2753
|
+
} else if (!primaryKeyName && columnMap.column.primaryKey) {
|
|
2754
|
+
primaryKeyName = columnMap.as;
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
if (selected) {
|
|
2758
|
+
return [...acc, columnMap];
|
|
2759
|
+
} else {
|
|
2760
|
+
return acc;
|
|
2761
|
+
}
|
|
2762
|
+
}, [] as searchStatementUtils.IColumnMapping[]);
|
|
2763
|
+
|
|
2764
|
+
const whereClause =
|
|
2765
|
+
searchStatementUtils.getWhereClauseFromSelectStatement(
|
|
2766
|
+
selectStatement
|
|
2767
|
+
);
|
|
2768
|
+
|
|
2769
|
+
if (whereClause) {
|
|
2770
|
+
const resolvedWhereQuery = fromWhereClauseToJsStoreWhereObject(
|
|
2771
|
+
whereClause,
|
|
2772
|
+
allColumns
|
|
2773
|
+
);
|
|
2774
|
+
|
|
2775
|
+
if (resolvedWhereQuery && Object.keys(resolvedWhereQuery).length) {
|
|
2776
|
+
selectQuery.where = resolvedWhereQuery;
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
const rows: {
|
|
2781
|
+
[columnName: string]: any;
|
|
2782
|
+
}[] = await this.connection.select(selectQuery);
|
|
2783
|
+
|
|
2784
|
+
const filteredRows = await this.processFiltering(selectStatement, rows);
|
|
2785
|
+
|
|
2786
|
+
const count = filteredRows.length;
|
|
2787
|
+
|
|
2788
|
+
const mappedRows = filteredRows.map((row) => {
|
|
2789
|
+
const newRow: { [columnName: string]: any } = {
|
|
2790
|
+
...row
|
|
2791
|
+
};
|
|
2792
|
+
|
|
2793
|
+
Object.keys(row).forEach((columnName) => {
|
|
2794
|
+
const columnMapping = resultingQueryColumns.find((mapping) => {
|
|
2795
|
+
// Split the column name by the dot
|
|
2796
|
+
const columnNameParts =
|
|
2797
|
+
mapping.column.columnName.split('.');
|
|
2798
|
+
const columnNamePart =
|
|
2799
|
+
columnNameParts[columnNameParts.length - 1];
|
|
2800
|
+
|
|
2801
|
+
if (
|
|
2802
|
+
mapping.column.columnName === columnName ||
|
|
2803
|
+
columnNamePart === columnName
|
|
2804
|
+
) {
|
|
2805
|
+
return true;
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
return false;
|
|
2809
|
+
});
|
|
2810
|
+
|
|
2811
|
+
if (columnMapping) {
|
|
2812
|
+
newRow[columnMapping.as] = row[columnName];
|
|
2813
|
+
} else {
|
|
2814
|
+
newRow[columnName] = row[columnName];
|
|
2815
|
+
}
|
|
2816
|
+
});
|
|
2817
|
+
|
|
2818
|
+
// Remove any property of the row that isn't present in the 'allSelectedColumnNames'
|
|
2819
|
+
Object.keys(newRow).forEach((columnName) => {
|
|
2820
|
+
if (!allSelectedColumnNames.includes(columnName)) {
|
|
2821
|
+
delete newRow[columnName];
|
|
2822
|
+
}
|
|
2823
|
+
});
|
|
2824
|
+
|
|
2825
|
+
return newRow;
|
|
2826
|
+
});
|
|
2827
|
+
|
|
2828
|
+
const isPureTable =
|
|
2829
|
+
mainSource.type === 'table' &&
|
|
2830
|
+
!(selectQuery.join as [])?.length &&
|
|
2831
|
+
resultingQueryColumns.every(
|
|
2832
|
+
(column) =>
|
|
2833
|
+
column.as === column.column.columnName &&
|
|
2834
|
+
column.column.table.tableName === mainSource.name
|
|
2835
|
+
);
|
|
2836
|
+
|
|
2837
|
+
const tableName = isPureTable ? mainSource.name : null;
|
|
2838
|
+
|
|
2839
|
+
const entity =
|
|
2840
|
+
mainSource.type === 'table'
|
|
2841
|
+
? this.getTable(tableName)?.entity || null
|
|
2842
|
+
: null;
|
|
2843
|
+
|
|
2844
|
+
const result: ISelectQueryResult = {
|
|
2845
|
+
columns: resultingQueryColumns,
|
|
2846
|
+
rows: mappedRows,
|
|
2847
|
+
tableName: tableName,
|
|
2848
|
+
entity: entity,
|
|
2849
|
+
total: count
|
|
2850
|
+
};
|
|
2851
|
+
|
|
2852
|
+
// Cache the result
|
|
2853
|
+
// @ts-ignore
|
|
2854
|
+
this.onGoingQueryCache[selectStatement.id] = result;
|
|
2855
|
+
|
|
2856
|
+
return result;
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
resolveRawInterpolationValues(
|
|
2860
|
+
query: string,
|
|
2861
|
+
inputs: IDynamicValue[]
|
|
2862
|
+
): string {
|
|
2863
|
+
let queryString = query;
|
|
2864
|
+
|
|
2865
|
+
inputs.forEach((value) => {
|
|
2866
|
+
let valuetoReplaceWithQuotes = `'{{::${value.valueOwner.id}}}'`;
|
|
2867
|
+
let valuetoReplaceWithoutQuotes = `{{::${value.valueOwner.id}}}`;
|
|
2868
|
+
|
|
2869
|
+
let stringifiedValue = '';
|
|
2870
|
+
|
|
2871
|
+
// Use the literal's flattened raw value when available so Array
|
|
2872
|
+
// literals (whose `value` field carries nested
|
|
2873
|
+
// SearchStatementLiteralValue objects) collapse down to plain
|
|
2874
|
+
// primitives / nested arrays before we serialise them. Falling
|
|
2875
|
+
// back to value.value.value preserves behaviour for inputs whose
|
|
2876
|
+
// .value isn't a SearchStatementLiteralValue instance.
|
|
2877
|
+
const innerWrapper: any = value?.value;
|
|
2878
|
+
const flattened =
|
|
2879
|
+
innerWrapper &&
|
|
2880
|
+
typeof innerWrapper.flattenRawValue === 'function'
|
|
2881
|
+
? innerWrapper.flattenRawValue()
|
|
2882
|
+
: innerWrapper?.value;
|
|
2883
|
+
|
|
2884
|
+
const literalValue = flattened;
|
|
2885
|
+
|
|
2886
|
+
if (
|
|
2887
|
+
!value ||
|
|
2888
|
+
!innerWrapper ||
|
|
2889
|
+
(literalValue == null &&
|
|
2890
|
+
literalValue !== false &&
|
|
2891
|
+
literalValue !== 0)
|
|
2892
|
+
) {
|
|
2893
|
+
stringifiedValue = 'NULL';
|
|
2894
|
+
} else if (typeof literalValue === 'string') {
|
|
2895
|
+
stringifiedValue = `'${literalValue}'`;
|
|
2896
|
+
} else if (typeof literalValue === 'boolean') {
|
|
2897
|
+
stringifiedValue = `${literalValue}`;
|
|
2898
|
+
} else if (typeof literalValue === 'number') {
|
|
2899
|
+
stringifiedValue = `${literalValue}`;
|
|
2900
|
+
} else if (Array.isArray(literalValue)) {
|
|
2901
|
+
// Arrays interpolate as JSON-string arrays — same shape produced
|
|
2902
|
+
// by SearchStatementLiteralValue.toQuery() for Array literals,
|
|
2903
|
+
// so they round-trip through unnest()/array-typed function args
|
|
2904
|
+
// without further coercion.
|
|
2905
|
+
stringifiedValue = `'${JSON.stringify(literalValue)}'`;
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
// @ts-ignore
|
|
2909
|
+
queryString = queryString.replaceAll(
|
|
2910
|
+
valuetoReplaceWithQuotes,
|
|
2911
|
+
stringifiedValue
|
|
2912
|
+
);
|
|
2913
|
+
|
|
2914
|
+
// @ts-ignore
|
|
2915
|
+
queryString = queryString.replaceAll(
|
|
2916
|
+
valuetoReplaceWithoutQuotes,
|
|
2917
|
+
stringifiedValue
|
|
2918
|
+
);
|
|
2919
|
+
});
|
|
2920
|
+
|
|
2921
|
+
return queryString;
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
async run(
|
|
2925
|
+
sql: Program,
|
|
2926
|
+
inputs: IDynamicValue[] = []
|
|
2927
|
+
): Promise<ISelectQueryResult> {
|
|
2928
|
+
toLower(sql);
|
|
2929
|
+
|
|
2930
|
+
let queryString = this.resolveRawInterpolationValues(
|
|
2931
|
+
SQLASTLib.show(sql),
|
|
2932
|
+
inputs
|
|
2933
|
+
);
|
|
2934
|
+
|
|
2935
|
+
const sanitizedAST = SQLASTLib.parse(queryString, {
|
|
2936
|
+
dialect: 'sqlite',
|
|
2937
|
+
// These are optional:
|
|
2938
|
+
includeSpaces: true // Adds spaces/tabs
|
|
2939
|
+
// includeNewlines: true, // Adds newlines
|
|
2940
|
+
// includeComments: true, // Adds comments
|
|
2941
|
+
// includeRange: true, // Adds source code location data
|
|
2942
|
+
});
|
|
2943
|
+
|
|
2944
|
+
const selectStatement =
|
|
2945
|
+
searchStatementUtils.getSelectStatementFromProgram(sanitizedAST);
|
|
2946
|
+
|
|
2947
|
+
let result: ISelectQueryResult = null;
|
|
2948
|
+
|
|
2949
|
+
if (!!selectStatement) {
|
|
2950
|
+
result = await this.processSelectQuery(selectStatement);
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
this.onGoingQueryCache = {};
|
|
2954
|
+
|
|
2955
|
+
return result;
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
/*
|
|
2960
|
+
This class is responsible for managing the local indexed databases for the project
|
|
2961
|
+
and is the main interface for interacting with them
|
|
2962
|
+
|
|
2963
|
+
The constructor takes a project state
|
|
2964
|
+
Then searches for any instances of custom entities that extend the base 'relational database' entity
|
|
2965
|
+
For each database entity, it initializes (creates or reuses) a local indexed database
|
|
2966
|
+
|
|
2967
|
+
The class holds a list of active local dbs as well as utilities to interact with them
|
|
2968
|
+
*/
|
|
2969
|
+
export class LocalRelationalDatabasesStore {
|
|
2970
|
+
project: EditorService;
|
|
2971
|
+
|
|
2972
|
+
databases: Database[] = [];
|
|
2973
|
+
|
|
2974
|
+
constructor(project: EditorService) {
|
|
2975
|
+
this.project = project;
|
|
2976
|
+
|
|
2977
|
+
this.initDb = this.initDb.bind(this);
|
|
2978
|
+
this.init = this.init.bind(this);
|
|
2979
|
+
this.destroy = this.destroy.bind(this);
|
|
2980
|
+
this.addDatabase = this.addDatabase.bind(this);
|
|
2981
|
+
this.getDatabase = this.getDatabase.bind(this);
|
|
2982
|
+
this.getDatabaseForPersistedEntity =
|
|
2983
|
+
this.getDatabaseForPersistedEntity.bind(this);
|
|
2984
|
+
this.onPersistedEntityAdded = this.onPersistedEntityAdded.bind(this);
|
|
2985
|
+
this.onPersistedEntityRemoved =
|
|
2986
|
+
this.onPersistedEntityRemoved.bind(this);
|
|
2987
|
+
this.onPersistedEntityUpdated =
|
|
2988
|
+
this.onPersistedEntityUpdated.bind(this);
|
|
2989
|
+
this.onDatabaseEntityAdded = this.onDatabaseEntityAdded.bind(this);
|
|
2990
|
+
this.onDatabaseEntityRemoved = this.onDatabaseEntityRemoved.bind(this);
|
|
2991
|
+
this.onDatabaseEntityUpdated = this.onDatabaseEntityUpdated.bind(this);
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
// These are the user declared entities that represent a relational db
|
|
2995
|
+
get databaseEntities(): DefinitionEntityState[] {
|
|
2996
|
+
return this.project.logic.entities.filter((entity) =>
|
|
2997
|
+
checkHasBaseEntity(entity, BaseEntityNames.RELATIONAL_DATABASE)
|
|
2998
|
+
);
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
// These are the user declared entities that are stored in the relational db as a table
|
|
3002
|
+
get persistedEntities(): DefinitionEntityState[] {
|
|
3003
|
+
return this.project.logic.entities.filter((entity) =>
|
|
3004
|
+
checkHasBaseEntity(entity, BaseEntityNames.PERSISTED_ENTITY)
|
|
3005
|
+
);
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
onPersistedEntityAdded(entity: DefinitionEntityState) {
|
|
3009
|
+
if (checkHasBaseEntity(entity, BaseEntityNames.PERSISTED_ENTITY)) {
|
|
3010
|
+
if (!this.persistedEntities.find((e) => e.id === entity.id)) {
|
|
3011
|
+
this.persistedEntities.push(entity);
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
// Find and then sync the parent db
|
|
3015
|
+
const db = this.getDatabaseForPersistedEntity(entity);
|
|
3016
|
+
|
|
3017
|
+
if (db) {
|
|
3018
|
+
this.initDb(db.entity);
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
onPersistedEntityRemoved(entity: DefinitionEntityState) {
|
|
3024
|
+
if (checkHasBaseEntity(entity, BaseEntityNames.PERSISTED_ENTITY)) {
|
|
3025
|
+
// Find and then sync the parent db
|
|
3026
|
+
const db = this.getDatabaseForPersistedEntity(entity);
|
|
3027
|
+
|
|
3028
|
+
if (db) {
|
|
3029
|
+
this.initDb(db.entity);
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
onPersistedEntityUpdated(entity: DefinitionEntityState) {
|
|
3035
|
+
if (checkHasBaseEntity(entity, BaseEntityNames.PERSISTED_ENTITY)) {
|
|
3036
|
+
// Find and then sync the parent db
|
|
3037
|
+
const db = this.getDatabaseForPersistedEntity(entity);
|
|
3038
|
+
|
|
3039
|
+
if (db) {
|
|
3040
|
+
this.initDb(db.entity);
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
onDatabaseEntityAdded(entity: DefinitionEntityState) {
|
|
3046
|
+
if (checkHasBaseEntity(entity, BaseEntityNames.RELATIONAL_DATABASE)) {
|
|
3047
|
+
if (!this.databaseEntities.find((e) => e.id === entity.id)) {
|
|
3048
|
+
this.databaseEntities.push(entity);
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
// Improve syncing of the db
|
|
3052
|
+
this.initDb(entity);
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
onDatabaseEntityRemoved(entity: DefinitionEntityState) {
|
|
3057
|
+
if (checkHasBaseEntity(entity, BaseEntityNames.RELATIONAL_DATABASE)) {
|
|
3058
|
+
const db = this.getDatabase(entity.id);
|
|
3059
|
+
|
|
3060
|
+
if (db) {
|
|
3061
|
+
this.databases = this.databases.filter(
|
|
3062
|
+
(db) => db.entity.id !== entity.id
|
|
3063
|
+
);
|
|
3064
|
+
db.destroy();
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
onDatabaseEntityUpdated(entity: DefinitionEntityState) {
|
|
3070
|
+
// Find the db and reinitialize it
|
|
3071
|
+
// if the name is what has changed, delete the old db and create a new one
|
|
3072
|
+
const db = this.getDatabase(entity.id);
|
|
3073
|
+
|
|
3074
|
+
if (db) {
|
|
3075
|
+
const dbName = Database.toDbName(entity);
|
|
3076
|
+
|
|
3077
|
+
if (db.connection?.database?.name !== dbName) {
|
|
3078
|
+
db.destroy();
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
this.initDb(entity);
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
async init(): Promise<void> {
|
|
3086
|
+
await Promise.all(this.databaseEntities.map(this.initDb));
|
|
3087
|
+
|
|
3088
|
+
// Listen for changes to the project state
|
|
3089
|
+
// And modify the databases accordingly
|
|
3090
|
+
this.project.logic.on(
|
|
3091
|
+
ProjectStateEvents.PERSISTED_DEFINITION_ENTITY_ADDED,
|
|
3092
|
+
this.onPersistedEntityAdded
|
|
3093
|
+
);
|
|
3094
|
+
this.project.logic.on(
|
|
3095
|
+
ProjectStateEvents.PERSISTED_DEFINITION_ENTITY_REMOVED,
|
|
3096
|
+
this.onPersistedEntityRemoved
|
|
3097
|
+
);
|
|
3098
|
+
this.project.logic.on(
|
|
3099
|
+
ProjectStateEvents.PERSISTED_DEFINITION_ENTITY_UPDATED,
|
|
3100
|
+
this.onPersistedEntityUpdated
|
|
3101
|
+
);
|
|
3102
|
+
|
|
3103
|
+
this.project.logic.on(
|
|
3104
|
+
ProjectStateEvents.RELATIONAL_DATABASE_DEFINITION_ENTITY_ADDED,
|
|
3105
|
+
this.onDatabaseEntityAdded
|
|
3106
|
+
);
|
|
3107
|
+
this.project.logic.on(
|
|
3108
|
+
ProjectStateEvents.RELATIONAL_DATABASE_DEFINITION_ENTITY_REMOVED,
|
|
3109
|
+
this.onDatabaseEntityRemoved
|
|
3110
|
+
);
|
|
3111
|
+
this.project.logic.on(
|
|
3112
|
+
ProjectStateEvents.RELATIONAL_DATABASE_DEFINITION_ENTITY_UPDATED,
|
|
3113
|
+
this.onDatabaseEntityUpdated
|
|
3114
|
+
);
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
async destroy(): Promise<void> {
|
|
3118
|
+
await Promise.all(this.databases.map((db) => db.disconnect()));
|
|
3119
|
+
|
|
3120
|
+
this.databases = [];
|
|
3121
|
+
this.project = null;
|
|
3122
|
+
|
|
3123
|
+
this.project?.logic?.off(
|
|
3124
|
+
ProjectStateEvents.PERSISTED_DEFINITION_ENTITY_ADDED,
|
|
3125
|
+
this.onPersistedEntityAdded
|
|
3126
|
+
);
|
|
3127
|
+
this.project?.logic?.off(
|
|
3128
|
+
ProjectStateEvents.PERSISTED_DEFINITION_ENTITY_REMOVED,
|
|
3129
|
+
this.onPersistedEntityRemoved
|
|
3130
|
+
);
|
|
3131
|
+
this.project?.logic?.off(
|
|
3132
|
+
ProjectStateEvents.PERSISTED_DEFINITION_ENTITY_UPDATED,
|
|
3133
|
+
this.onPersistedEntityUpdated
|
|
3134
|
+
);
|
|
3135
|
+
this.project?.logic?.off(
|
|
3136
|
+
ProjectStateEvents.RELATIONAL_DATABASE_DEFINITION_ENTITY_ADDED,
|
|
3137
|
+
this.onDatabaseEntityAdded
|
|
3138
|
+
);
|
|
3139
|
+
this.project?.logic?.off(
|
|
3140
|
+
ProjectStateEvents.RELATIONAL_DATABASE_DEFINITION_ENTITY_REMOVED,
|
|
3141
|
+
this.onDatabaseEntityRemoved
|
|
3142
|
+
);
|
|
3143
|
+
this.project?.logic?.off(
|
|
3144
|
+
ProjectStateEvents.RELATIONAL_DATABASE_DEFINITION_ENTITY_UPDATED,
|
|
3145
|
+
this.onDatabaseEntityUpdated
|
|
3146
|
+
);
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
async initDb(entity: DefinitionEntityState): Promise<Database> {
|
|
3150
|
+
const database =
|
|
3151
|
+
this.getDatabase(entity.id) || new Database(entity, this);
|
|
3152
|
+
|
|
3153
|
+
try {
|
|
3154
|
+
await database.init();
|
|
3155
|
+
} catch (e) {
|
|
3156
|
+
console.error(
|
|
3157
|
+
`Error initializing local db for ${resolveEntityName(
|
|
3158
|
+
entity,
|
|
3159
|
+
this.project.logic
|
|
3160
|
+
)}: `,
|
|
3161
|
+
e
|
|
3162
|
+
);
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
const savedDatabase = this.addDatabase(database);
|
|
3166
|
+
|
|
3167
|
+
return savedDatabase;
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
addDatabase(database: Database): Database {
|
|
3171
|
+
let existingDatabase: Database = this.databases.find(
|
|
3172
|
+
(db) =>
|
|
3173
|
+
db === database ||
|
|
3174
|
+
db.connection === database.connection ||
|
|
3175
|
+
db.connection?.database?.name ===
|
|
3176
|
+
database.connection?.database?.name ||
|
|
3177
|
+
db.entity.id === database.entity.id
|
|
3178
|
+
);
|
|
3179
|
+
|
|
3180
|
+
// If it doesn't yet exist, add it to the list of active dbs
|
|
3181
|
+
if (!existingDatabase) {
|
|
3182
|
+
this.databases.push(database);
|
|
3183
|
+
|
|
3184
|
+
return database;
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
return existingDatabase;
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
// Given a definition entity that extends the built in persisted entity
|
|
3191
|
+
// It returns the database instance that owns the table for this entity
|
|
3192
|
+
getDatabaseForPersistedEntity(entity: DefinitionEntityState): Database {
|
|
3193
|
+
const databaseEntity =
|
|
3194
|
+
resolvePersistedDefinitionEntityDatabaseEntity(entity);
|
|
3195
|
+
|
|
3196
|
+
if (!databaseEntity) {
|
|
3197
|
+
return null;
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
const database = this.databases.find((db) => {
|
|
3201
|
+
return db.entity.id === databaseEntity.id;
|
|
3202
|
+
});
|
|
3203
|
+
|
|
3204
|
+
return database;
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
getDatabase(entityId: string): Database {
|
|
3208
|
+
return this.databases.find((db) => db.entity.id === entityId);
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
export async function browserSearchImplementation(
|
|
3213
|
+
entity: SearchState,
|
|
3214
|
+
inputs: IDynamicValue[],
|
|
3215
|
+
project: EditorService
|
|
3216
|
+
): Promise<{
|
|
3217
|
+
// @ts-ignore
|
|
3218
|
+
error: string | null;
|
|
3219
|
+
[dataPropertyName: string]:
|
|
3220
|
+
| {
|
|
3221
|
+
[columnName: string]: any;
|
|
3222
|
+
}
|
|
3223
|
+
| null
|
|
3224
|
+
| {
|
|
3225
|
+
[columnName: string]: any;
|
|
3226
|
+
}[];
|
|
3227
|
+
}> {
|
|
3228
|
+
const initialAst = searchStatementUtils.sanitizeQuery(entity.query)
|
|
3229
|
+
? SQLASTLib.parse(searchStatementUtils.sanitizeQuery(entity.query), {
|
|
3230
|
+
dialect: 'sqlite'
|
|
3231
|
+
})
|
|
3232
|
+
: null;
|
|
3233
|
+
|
|
3234
|
+
const hydratedStateConfig =
|
|
3235
|
+
searchStatementUtils.hydrateSearchStatementState(
|
|
3236
|
+
initialAst,
|
|
3237
|
+
project.logic,
|
|
3238
|
+
entity
|
|
3239
|
+
);
|
|
3240
|
+
|
|
3241
|
+
if (!!hydratedStateConfig.errors.length) {
|
|
3242
|
+
// Do nothing
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
const searchStatementState = hydratedStateConfig.state;
|
|
3246
|
+
|
|
3247
|
+
// Based on the main data source, we resolve the database client
|
|
3248
|
+
const resolveDatabaseClient = (): Database | null => {
|
|
3249
|
+
if (!!searchStatementState?.from) {
|
|
3250
|
+
const mainDataSourceEntity =
|
|
3251
|
+
searchStatementState?.mainPersistedEntity || null;
|
|
3252
|
+
|
|
3253
|
+
if (!mainDataSourceEntity) {
|
|
3254
|
+
return null;
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
const database =
|
|
3258
|
+
project?.localDatabaseStore?.getDatabaseForPersistedEntity(
|
|
3259
|
+
mainDataSourceEntity
|
|
3260
|
+
);
|
|
3261
|
+
|
|
3262
|
+
return database;
|
|
3263
|
+
} else {
|
|
3264
|
+
// There is no need for any table to be present, so we just need ANY client, to query against
|
|
3265
|
+
return project?.localDatabaseStore?.databases[0] || null;
|
|
3266
|
+
}
|
|
3267
|
+
};
|
|
3268
|
+
|
|
3269
|
+
const dbClient = resolveDatabaseClient();
|
|
3270
|
+
|
|
3271
|
+
const newResult: ISelectQueryResultV2 = (await dbClient?.runStatement(
|
|
3272
|
+
searchStatementState,
|
|
3273
|
+
inputs
|
|
3274
|
+
)) as ISelectQueryResultV2;
|
|
3275
|
+
|
|
3276
|
+
const newValue = searchStatementState.asList
|
|
3277
|
+
? newResult?.rows
|
|
3278
|
+
: newResult?.rows[0] || null;
|
|
3279
|
+
|
|
3280
|
+
const resultDT = entity.getDataType(null);
|
|
3281
|
+
if (
|
|
3282
|
+
resultDT?.entity.type === EntityType.DefinitionEntity &&
|
|
3283
|
+
!!resultDT?.entity.properties[0]
|
|
3284
|
+
) {
|
|
3285
|
+
const foundOutputMap = entity.outputs.find(
|
|
3286
|
+
(output) =>
|
|
3287
|
+
output.declaration?.id ===
|
|
3288
|
+
(resultDT.entity as DefinitionEntityState)?.properties[0].id
|
|
3289
|
+
);
|
|
3290
|
+
|
|
3291
|
+
const dataPropertyName = foundOutputMap
|
|
3292
|
+
? foundOutputMap.codeName ||
|
|
3293
|
+
foundOutputMap.declaration?.codeName ||
|
|
3294
|
+
toCamelCase(resolveEntityName(foundOutputMap, entity.project))
|
|
3295
|
+
: toCamelCase(resolveEntityName(resultDT.entity, entity.project)) ||
|
|
3296
|
+
'data';
|
|
3297
|
+
|
|
3298
|
+
return {
|
|
3299
|
+
[dataPropertyName]: newValue,
|
|
3300
|
+
error: null
|
|
3301
|
+
};
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
return {
|
|
3305
|
+
data: newValue,
|
|
3306
|
+
error: null
|
|
3307
|
+
};
|
|
3308
|
+
}
|