diff --git a/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/FirestorePlugin.java b/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/FirestorePlugin.java index 16928b6dc3e2..b62436c5c36e 100644 --- a/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/FirestorePlugin.java +++ b/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/FirestorePlugin.java @@ -144,12 +144,14 @@ public Mono executeParameterized( if (smartSubstitutionObject instanceof Boolean) { smartJsonSubstitution = (Boolean) smartSubstitutionObject; } else if (smartSubstitutionObject instanceof String) { - // Older UI configuration used to set this value as a string which may/may not be castable to a boolean + // Older UI configuration used to set this value as a string which may/may not + // be castable to a boolean // directly. This is to ensure we are backward compatible smartJsonSubstitution = Boolean.parseBoolean((String) smartSubstitutionObject); } - // Smartly substitute in actionConfiguration.body and replace all the bindings with values. + // Smartly substitute in actionConfiguration.body and replace all the bindings + // with values. List> parameters = new ArrayList<>(); if (TRUE.equals(smartJsonSubstitution)) { String query = PluginUtils.getDataValueSafelyFromFormData( @@ -202,8 +204,8 @@ public Mono executeParameterized( requestParams.add(new RequestParamDTO(COMMAND, command, null, null, null)); requestParams.add(new RequestParamDTO(ACTION_CONFIGURATION_PATH, path, null, null, null)); - final PaginationField paginationField = - executeActionDTO == null ? null : executeActionDTO.getPaginationField(); + final PaginationField paginationField = executeActionDTO == null ? null + : executeActionDTO.getPaginationField(); Set hintMessages = new HashSet<>(); @@ -278,7 +280,8 @@ && isTimestampAndDeleteFieldValuePathEmpty(formData)) { try { /* - * - Update mapBody with FieldValue.xyz() values if the FieldValue paths are provided. + * - Update mapBody with FieldValue.xyz() values if the FieldValue paths are + * provided. */ insertFieldValues(mapBody, formData, method, requestParams); } catch (AppsmithPluginException e) { @@ -348,7 +351,8 @@ private boolean isSetOrUpdateOrCreateOrAddMethod(Method method) { } /* - * - Update mapBody with FieldValue.xyz() values if the FieldValue paths are provided. + * - Update mapBody with FieldValue.xyz() values if the FieldValue paths are + * provided. */ private void insertFieldValues( Map mapBody, @@ -358,7 +362,8 @@ private void insertFieldValues( throws AppsmithPluginException { /* - * - Check that FieldValue.delete() option is only available for UPDATE operation. + * - Check that FieldValue.delete() option is only available for UPDATE + * operation. */ if (!Method.UPDATE_DOCUMENT.equals(method) && !isBlank(PluginUtils.getDataValueSafelyFromFormData(formData, DELETE_KEY_PATH, STRING_TYPE))) { @@ -375,7 +380,8 @@ private void insertFieldValues( requestParams.add(new RequestParamDTO(DELETE_KEY_PATH, deletePaths, null, null, null)); List deletePathsList; try { - deletePathsList = objectMapper.readValue(deletePaths, new TypeReference>() {}); + deletePathsList = objectMapper.readValue(deletePaths, new TypeReference>() { + }); } catch (IOException e) { throw new AppsmithPluginException( AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, @@ -385,18 +391,24 @@ private void insertFieldValues( /* * - Update all the map body keys that need to be deleted. - * - This way of denoting a nested path via a "." (dot) notation can only be directly used for a update - * operation. e.g. {"key1.key2": FieldValue.delete()} - * - dot notation is used with FieldValue.delete() because otherwise it is not possible to delete - * nested fields. Ref: https://stackoverflow.com/questions/46677132/fieldvalue-delete-can-only-appear-at-the-top-level-of-your-update-data-fires/46677677 - * - dot notation is safe to use with delete FieldValue because this FieldValue only works with update - * operation. + * - This way of denoting a nested path via a "." (dot) notation can only be + * directly used for a update + * operation. e.g. {"key1.key2": FieldValue.delete()} + * - dot notation is used with FieldValue.delete() because otherwise it is not + * possible to delete + * nested fields. Ref: + * https://stackoverflow.com/questions/46677132/fieldvalue-delete-can-only- + * appear-at-the-top-level-of-your-update-data-fires/46677677 + * - dot notation is safe to use with delete FieldValue because this FieldValue + * only works with update + * operation. */ deletePathsList.stream().forEach(path -> mapBody.put(path, FieldValue.delete())); } /* - * - Check that FieldValue.serverTimestamp() option is not available for any GET or DELETE operations. + * - Check that FieldValue.serverTimestamp() option is not available for any GET + * or DELETE operations. */ if (isGetOrDeleteMethod(method) && !isBlank( @@ -410,13 +422,14 @@ private void insertFieldValues( * - Parse severTimestamp FieldValue path. */ if (!isBlank(PluginUtils.getDataValueSafelyFromFormData(formData, TIMESTAMP_VALUE_PATH, STRING_TYPE))) { - String timestampValuePaths = - PluginUtils.getDataValueSafelyFromFormData(formData, TIMESTAMP_VALUE_PATH, STRING_TYPE); + String timestampValuePaths = PluginUtils.getDataValueSafelyFromFormData(formData, TIMESTAMP_VALUE_PATH, + STRING_TYPE); requestParams.add(new RequestParamDTO(TIMESTAMP_VALUE_PATH, timestampValuePaths, null, null, null)); List timestampPathsStringList; // ["key1.key2", "key3.key4"] try { - timestampPathsStringList = - objectMapper.readValue(timestampValuePaths, new TypeReference>() {}); + timestampPathsStringList = objectMapper.readValue(timestampValuePaths, + new TypeReference>() { + }); } catch (IOException e) { throw new AppsmithPluginException( AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, @@ -426,10 +439,13 @@ private void insertFieldValues( /* * - Update all the map body keys that need to store timestamp. - * - Since serverTimestamp FieldValue can be used with non update operations like create and set, "." - * (dot) notation cannot be directly used to refer to nested paths. - * - We cannot use the dotted notation directly with timestamp FieldValue because during set/create - * actions, the dotted string is considered as a single key instead of nested path. + * - Since serverTimestamp FieldValue can be used with non update operations + * like create and set, "." + * (dot) notation cannot be directly used to refer to nested paths. + * - We cannot use the dotted notation directly with timestamp FieldValue + * because during set/create + * actions, the dotted string is considered as a single key instead of nested + * path. * - Convert ["key1.key2", "key3.key4"] to [["key1", "key2"], ["key3", "key4"]] */ List> timestampPathsArrayList = new ArrayList<>(); @@ -447,8 +463,9 @@ private boolean isGetOrDeleteMethod(Method method) { /* * - A common method that can be used for any FieldValue option. - * - It iterates over the map body and replaces the value of keys defined by pathsList with a FieldValue - * entity defined by fieldValueName. + * - It iterates over the map body and replaces the value of keys defined by + * pathsList with a FieldValue + * entity defined by fieldValueName. */ private void insertFieldValueByMethodName( Map mapBody, List> pathsList, String fieldValueName) { @@ -457,8 +474,9 @@ private void insertFieldValueByMethodName( .filter(singlePathList -> !CollectionUtils.isEmpty(singlePathList)) .forEach(singlePathList -> { /* - * - Unable to convert this for loop into a stream implementation. Please offer suggestions - * if possible. + * - Unable to convert this for loop into a stream implementation. Please offer + * suggestions + * if possible. */ HashMap targetKeyValuePair = (HashMap) mapBody; for (int i = 0; i < singlePathList.size() - 1; i++) { @@ -486,8 +504,10 @@ private void insertFieldValueByMethodName( singlePathList.get(singlePathList.size() - 1), /* * - As per Java documentation: If the underlying method is static, then the - * specified obj argument is ignored. It may be null. - * - Ref: https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Method.html#invoke-java.lang.Object-java.lang.Object...- + * specified obj argument is ignored. It may be null. + * - Ref: + * https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Method.html# + * invoke-java.lang.Object-java.lang.Object...- */ FieldValue.class.getMethod(fieldValueName).invoke(null)); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { @@ -510,8 +530,7 @@ public Mono handleDocumentLevelMethod( return Mono.just(method) // Get the actual Java method to be called. .flatMap(method1 -> { - final String methodName = - method1.toString().split("_")[0].toLowerCase(); + final String methodName = method1.toString().split("_")[0].toLowerCase(); try { switch (method1) { case GET_DOCUMENT: @@ -552,7 +571,8 @@ public Mono handleDocumentLevelMethod( } catch (IllegalAccessException | InvocationTargetException e) { /* - * - Printing the stack because e.getMessage() returns null for FieldValue errors. + * - Printing the stack because e.getMessage() returns null for FieldValue + * errors. */ e.printStackTrace(); return Mono.error(new AppsmithPluginException( @@ -624,11 +644,11 @@ private Mono methodGetCollection( List requestParams, Set hintMessages, ActionConfiguration actionConfiguration) { - final String limitString = - PluginUtils.getDataValueSafelyFromFormData(formData, LIMIT_DOCUMENTS, STRING_TYPE); + final String limitString = PluginUtils.getDataValueSafelyFromFormData(formData, LIMIT_DOCUMENTS, + STRING_TYPE); final int limit = StringUtils.isEmpty(limitString) ? 10 : Integer.parseInt(limitString); - final String orderByString = - PluginUtils.getDataValueSafelyFromFormData(formData, ORDER_BY, STRING_TYPE, ""); + final String orderByString = PluginUtils.getDataValueSafelyFromFormData(formData, ORDER_BY, STRING_TYPE, + ""); requestParams.add(new RequestParamDTO(ORDER_BY, orderByString, null, null, null)); final List orderings; @@ -696,8 +716,9 @@ private Mono methodGetCollection( return Mono.just(query1); } - Map>> childrenMap = - PluginUtils.getDataValueSafelyFromFormData(formData, WHERE, new TypeReference<>() {}); + Map>> childrenMap = PluginUtils + .getDataValueSafelyFromFormData(formData, WHERE, new TypeReference<>() { + }); final List> conditionList = childrenMap.get("children"); requestParams.add(new RequestParamDTO(WHERE, conditionList, null, null, null)); @@ -749,7 +770,8 @@ private Mono methodGetCollection( } return q; }) - // Apply limit, always provided, since without it, we can inadvertently end up processing too much + // Apply limit, always provided, since without it, we can inadvertently end up + // processing too much // data. .map(query1 -> { if (PaginationField.PREV.equals(paginationField) && !CollectionUtils.isEmpty(endBefore)) { @@ -784,8 +806,9 @@ private Mono methodGetCollection( } private boolean isWhereMethodUsed(Map formData) { - final Map> childrenMap = - getDataValueSafelyFromFormData(formData, WHERE, new TypeReference<>() {}); + final Map> childrenMap = getDataValueSafelyFromFormData(formData, WHERE, + new TypeReference<>() { + }); if (childrenMap == null || childrenMap.isEmpty()) { return false; @@ -798,8 +821,8 @@ private boolean isWhereMethodUsed(Map formData) { } // Check if all keys in the where clause are null. - boolean allValuesNull = - conditionList.stream().allMatch(condition -> isBlank((String) ((Map) condition).get("key"))); + boolean allValuesNull = conditionList.stream() + .allMatch(condition -> isBlank((String) ((Map) condition).get("key"))); if (allValuesNull) { return false; @@ -850,8 +873,7 @@ private Object resultToMap(Object objResult, boolean isRoot) throws AppsmithPlug Map resultMap = new HashMap<>(); resultMap.put("_ref", resultToMap(documentSnapshot.getReference())); if (documentSnapshot.getData() != null) { - for (final Map.Entry entry : - documentSnapshot.getData().entrySet()) { + for (final Map.Entry entry : documentSnapshot.getData().entrySet()) { resultMap.put(entry.getKey(), resultToMap(entry.getValue(), false)); } } @@ -912,29 +934,33 @@ public Mono datasourceCreate(DatasourceConfiguration datasourceConfig InputStream serviceAccount = new ByteArrayInputStream(clientJson.getBytes()); return Mono.fromSupplier(() -> { - log.debug(Thread.currentThread().getName() - + ": instantiating googlecredentials object from Firestore plugin."); - GoogleCredentials credentials; - try { - credentials = GoogleCredentials.fromStream(serviceAccount); - } catch (IOException e) { - throw Exceptions.propagate(new AppsmithPluginException( - AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, - FirestoreErrorMessages.DS_VALIDATION_FAILED_FOR_SERVICE_ACC_CREDENTIALS_ERROR_MSG, - e.getMessage())); - } + log.debug(Thread.currentThread().getName() + + ": instantiating googlecredentials object from Firestore plugin."); + GoogleCredentials credentials; + try { + credentials = GoogleCredentials.fromStream(serviceAccount); + } catch (IOException e) { + throw Exceptions.propagate(new AppsmithPluginException( + AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, + FirestoreErrorMessages.DS_VALIDATION_FAILED_FOR_SERVICE_ACC_CREDENTIALS_ERROR_MSG, + e.getMessage())); + } - return FirebaseOptions.builder() - .setDatabaseUrl(datasourceConfiguration.getUrl()) - .setProjectId(projectId) - .setCredentials(credentials) - .build(); - }) + return FirebaseOptions.builder() + .setDatabaseUrl(datasourceConfiguration.getUrl()) + .setProjectId(projectId) + .setCredentials(credentials) + .build(); + }) .onErrorMap(Exceptions::unwrap) .map(options -> { try { - return FirebaseApp.getInstance(projectId); + + return FirebaseApp.initializeApp(options, projectId); } catch (IllegalStateException e) { + + FirebaseApp existingApp = FirebaseApp.getInstance(projectId); + existingApp.delete(); return FirebaseApp.initializeApp(options, projectId); } }) @@ -965,8 +991,16 @@ public Mono testDatasource(DatasourceConfiguration datasou @Override public void datasourceDestroy(Firestore connection) { - // This is empty because there's no concept of destroying a Firestore connection here. - // When the datasource is updated, the FirebaseApp instance will delete & re-create the app + + try { + String projectId = connection.getOptions().getProjectId(); + if (projectId != null) { + FirebaseApp.getInstance(projectId).delete(); + } + } catch (IllegalStateException e) { + + log.debug("FirebaseApp already deleted or not found during datasource destroy"); + } } @Override @@ -1001,30 +1035,30 @@ public Mono getStructure( Firestore connection, DatasourceConfiguration datasourceConfiguration) { log.debug(Thread.currentThread().getName() + ": getStructure() called for Firestore plugin."); return Mono.fromSupplier(() -> { - log.debug(Thread.currentThread().getName() - + ": invoking connection.listCollections() from Firestore plugin."); - Iterable collectionReferences = connection.listCollections(); - - List tables = StreamSupport.stream( - collectionReferences.spliterator(), false) - .map(collectionReference -> { - String id = collectionReference.getId(); - final ArrayList templates = new ArrayList<>(); - return new DatasourceStructure.Table( - DatasourceStructure.TableType.COLLECTION, - null, - id, - new ArrayList<>(), - new ArrayList<>(), - templates); - }) - .collect(Collectors.toList()); - - DatasourceStructure structure = new DatasourceStructure(); - structure.setTables(tables); - - return structure; - }) + log.debug(Thread.currentThread().getName() + + ": invoking connection.listCollections() from Firestore plugin."); + Iterable collectionReferences = connection.listCollections(); + + List tables = StreamSupport.stream( + collectionReferences.spliterator(), false) + .map(collectionReference -> { + String id = collectionReference.getId(); + final ArrayList templates = new ArrayList<>(); + return new DatasourceStructure.Table( + DatasourceStructure.TableType.COLLECTION, + null, + id, + new ArrayList<>(), + new ArrayList<>(), + templates); + }) + .collect(Collectors.toList()); + + DatasourceStructure structure = new DatasourceStructure(); + structure.setTables(tables); + + return structure; + }) .subscribeOn(scheduler); }