diff --git a/CHANGELOG.md b/CHANGELOG.md index c95b4107..c243891e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [6.2.0] - 2024-04-25 + +### Added + +- Adds support for Bulk Import +- Adds `BulkImportUser` class to represent a bulk import user +- Adds `BulkImportStorage` interface +- Adds `DuplicateUserIdException` class +- Adds `createBulkImportProxyStorageInstance` method in `Storage` class +- Adds `closeConnectionForBulkImportProxyStorage`, `commitTransactionForBulkImportProxyStorage`, and `rollbackTransactionForBulkImportProxyStorage` method in `SQLStorage` class + ## [6.1.0] - 2024-04-17 - Adds `addTenantIdentifier` to the storage interface. diff --git a/src/main/java/io/supertokens/pluginInterface/Storage.java b/src/main/java/io/supertokens/pluginInterface/Storage.java index 68369617..57d26025 100644 --- a/src/main/java/io/supertokens/pluginInterface/Storage.java +++ b/src/main/java/io/supertokens/pluginInterface/Storage.java @@ -33,6 +33,8 @@ public interface Storage { // if silent is true, do not log anything out on the console void constructor(String processId, boolean silent, boolean isTesting); + Storage createBulkImportProxyStorageInstance(); + void loadConfig(JsonObject jsonConfig, Set logLevels, TenantIdentifier tenantIdentifier) throws InvalidConfigException; // this returns a unique ID based on the db's connection URI and table prefix such that diff --git a/src/main/java/io/supertokens/pluginInterface/StorageUtils.java b/src/main/java/io/supertokens/pluginInterface/StorageUtils.java index 19bfb89a..9e6b4c99 100644 --- a/src/main/java/io/supertokens/pluginInterface/StorageUtils.java +++ b/src/main/java/io/supertokens/pluginInterface/StorageUtils.java @@ -17,6 +17,7 @@ package io.supertokens.pluginInterface; import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage; +import io.supertokens.pluginInterface.bulkimport.sqlStorage.BulkImportSQLStorage; import io.supertokens.pluginInterface.dashboard.sqlStorage.DashboardSQLStorage; import io.supertokens.pluginInterface.emailpassword.sqlStorage.EmailPasswordSQLStorage; import io.supertokens.pluginInterface.emailverification.sqlStorage.EmailVerificationSQLStorage; @@ -132,4 +133,12 @@ public static MultitenancyStorage getMultitenancyStorage(Storage storage) { } return (MultitenancyStorage) storage; } + + public static BulkImportSQLStorage getBulkImportStorage(Storage storage) { + if (storage.getType() != STORAGE_TYPE.SQL) { + // we only support SQL for now + throw new UnsupportedOperationException(""); + } + return (BulkImportSQLStorage) storage; + } } diff --git a/src/main/java/io/supertokens/pluginInterface/bulkimport/BulkImportStorage.java b/src/main/java/io/supertokens/pluginInterface/bulkimport/BulkImportStorage.java new file mode 100644 index 00000000..492d199f --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/bulkimport/BulkImportStorage.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.pluginInterface.bulkimport; + +import java.util.List; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.nonAuthRecipe.NonAuthRecipeStorage; + +public interface BulkImportStorage extends NonAuthRecipeStorage { + /** + * Add users to the bulk_import_users table + */ + void addBulkImportUsers(AppIdentifier appIdentifier, List users) + throws StorageQueryException, + TenantOrAppNotFoundException, + io.supertokens.pluginInterface.bulkimport.exceptions.DuplicateUserIdException; + + /** + * Get users from the bulk_import_users table + */ + List getBulkImportUsers(AppIdentifier appIdentifier, @Nonnull Integer limit, @Nullable BULK_IMPORT_USER_STATUS status, + @Nullable String bulkImportUserId, @Nullable Long createdAt) throws StorageQueryException; + + /** + * Delete users by id from the bulk_import_users table + */ + List deleteBulkImportUsers(AppIdentifier appIdentifier, @Nonnull String[] bulkImportUserIds) throws StorageQueryException; + + /** + * Returns the users from the bulk_import_users table for processing + */ + List getBulkImportUsersAndChangeStatusToProcessing(AppIdentifier appIdentifier, @Nonnull Integer limit) throws StorageQueryException; + + + /** + * Update the bulk_import_user's primary_user_id by bulk_import_user_id + */ + void updateBulkImportUserPrimaryUserId(AppIdentifier appIdentifier, @Nonnull String bulkImportUserId, @Nonnull String primaryUserId) throws StorageQueryException; + + /** + * Returns the count of users from the bulk_import_users table + */ + long getBulkImportUsersCount(AppIdentifier appIdentifier, @Nullable BULK_IMPORT_USER_STATUS status) throws StorageQueryException; + + public enum BULK_IMPORT_USER_STATUS { + NEW, PROCESSING, FAILED + } +} diff --git a/src/main/java/io/supertokens/pluginInterface/bulkimport/BulkImportUser.java b/src/main/java/io/supertokens/pluginInterface/bulkimport/BulkImportUser.java new file mode 100644 index 00000000..dff8ac57 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/bulkimport/BulkImportUser.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.pluginInterface.bulkimport; + +import java.util.List; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +import io.supertokens.pluginInterface.bulkimport.BulkImportStorage.BULK_IMPORT_USER_STATUS; + +public class BulkImportUser { + public String id; + public String externalUserId; + public JsonObject userMetadata; + public List userRoles; + public List totpDevices; + public List loginMethods; + + // Following fields come from the DB Record. + public BULK_IMPORT_USER_STATUS status; + public String primaryUserId; + public String errorMessage; + public Long createdAt; + public Long updatedAt; + + public BulkImportUser(String id, String externalUserId, JsonObject userMetadata, List userRoles, + List totpDevices, List loginMethods) { + this.id = id; + this.externalUserId = externalUserId; + this.userMetadata = userMetadata; + this.userRoles = userRoles; + this.totpDevices = totpDevices; + this.loginMethods = loginMethods; + } + + public static BulkImportUser forTesting_fromJson(JsonObject jsonObject) { + return new Gson().fromJson(jsonObject, BulkImportUser.class); + } + + // The bulk_import_users table stores users to be imported via a Cron Job. + // It has a `raw_data` column containing user data in JSON format. + + // The BulkImportUser class represents this `raw_data`, including additional fields like `status`, `createdAt`, and `updatedAt`. + // First, we validate all fields of `raw_data` using the BulkImportUser class, then store this data in the bulk_import_users table. + // This function retrieves the `raw_data` after removing the additional fields. + public String toRawDataForDbStorage() { + JsonObject jsonObject = new Gson().fromJson(new Gson().toJson(this), JsonObject.class); + jsonObject.remove("status"); + jsonObject.remove("createdAt"); + jsonObject.remove("updatedAt"); + return jsonObject.toString(); + } + + // The bulk_import_users table contains a `raw_data` column with user data in JSON format, along with other columns such as `id`, `status`, `primary_user_id`, and `error_msg` etc. + + // When creating an instance of the BulkImportUser class, the extra fields must be passed separately as they are not part of the `raw_data`. + // This function creates a BulkImportUser instance from a stored bulk_import_user entry. + public static BulkImportUser fromRawDataFromDbStorage(String id, String rawData, BULK_IMPORT_USER_STATUS status, String primaryUserId, String errorMessage, long createdAt, long updatedAt) { + BulkImportUser user = new Gson().fromJson(rawData, BulkImportUser.class); + user.id = id; + user.status = status; + user.primaryUserId = primaryUserId; + user.errorMessage = errorMessage; + user.createdAt = createdAt; + user.updatedAt = updatedAt; + return user; + } + + public JsonObject toJsonObject() { + return new Gson().fromJson(new Gson().toJson(this), JsonObject.class); + } + + public static class UserRole { + public String role; + public List tenantIds; + + public UserRole(String role, List tenantIds) { + this.role = role; + this.tenantIds = tenantIds; + } + } + + public static class TotpDevice { + public String secretKey; + public int period; + public int skew; + public String deviceName; + + public TotpDevice(String secretKey, int period, int skew, String deviceName) { + this.secretKey = secretKey; + this.period = period; + this.skew = skew; + this.deviceName = deviceName; + } + } + + public static class LoginMethod { + public List tenantIds; + public boolean isVerified; + public boolean isPrimary; + public long timeJoinedInMSSinceEpoch; + public String recipeId; + public String email; + public String passwordHash; + public String hashingAlgorithm; + public String plainTextPassword; + public String thirdPartyId; + public String thirdPartyUserId; + public String phoneNumber; + public String superTokensUserId; + public String externalUserId; + + public String getSuperTokenOrExternalUserId() { + return this.externalUserId != null ? this.externalUserId : this.superTokensUserId; + } + + public LoginMethod(List tenantIds, String recipeId, boolean isVerified, boolean isPrimary, + long timeJoinedInMSSinceEpoch, String email, String passwordHash, String hashingAlgorithm, String plainTextPassword, + String thirdPartyId, String thirdPartyUserId, String phoneNumber) { + this.tenantIds = tenantIds; + this.recipeId = recipeId; + this.isVerified = isVerified; + this.isPrimary = isPrimary; + this.timeJoinedInMSSinceEpoch = timeJoinedInMSSinceEpoch; + this.email = email; + this.passwordHash = passwordHash; + this.hashingAlgorithm = hashingAlgorithm; + this.plainTextPassword = plainTextPassword; + this.thirdPartyId = thirdPartyId; + this.thirdPartyUserId = thirdPartyUserId; + this.phoneNumber = phoneNumber; + } + } +} diff --git a/src/main/java/io/supertokens/pluginInterface/bulkimport/exceptions/DuplicateUserIdException.java b/src/main/java/io/supertokens/pluginInterface/bulkimport/exceptions/DuplicateUserIdException.java new file mode 100644 index 00000000..07070c15 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/bulkimport/exceptions/DuplicateUserIdException.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.pluginInterface.bulkimport.exceptions; + +public class DuplicateUserIdException extends Exception { + private static final long serialVersionUID = 6848053563771647272L; +} diff --git a/src/main/java/io/supertokens/pluginInterface/bulkimport/sqlStorage/BulkImportSQLStorage.java b/src/main/java/io/supertokens/pluginInterface/bulkimport/sqlStorage/BulkImportSQLStorage.java new file mode 100644 index 00000000..a7d1fff3 --- /dev/null +++ b/src/main/java/io/supertokens/pluginInterface/bulkimport/sqlStorage/BulkImportSQLStorage.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.pluginInterface.bulkimport.sqlStorage; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import io.supertokens.pluginInterface.bulkimport.BulkImportStorage; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.sqlStorage.SQLStorage; +import io.supertokens.pluginInterface.sqlStorage.TransactionConnection; + +public interface BulkImportSQLStorage extends BulkImportStorage, SQLStorage { + + /** + * Update the status of the users in the bulk_import_users table + */ + void updateBulkImportUserStatus_Transaction(AppIdentifier appIdentifier, + TransactionConnection con, @Nonnull String bulkImportUserId, @Nonnull BULK_IMPORT_USER_STATUS status, @Nullable String errorMessage) throws StorageQueryException; +} diff --git a/src/main/java/io/supertokens/pluginInterface/sqlStorage/SQLStorage.java b/src/main/java/io/supertokens/pluginInterface/sqlStorage/SQLStorage.java index d1351b30..740aa4b2 100644 --- a/src/main/java/io/supertokens/pluginInterface/sqlStorage/SQLStorage.java +++ b/src/main/java/io/supertokens/pluginInterface/sqlStorage/SQLStorage.java @@ -45,4 +45,12 @@ interface TransactionLogic { public enum TransactionIsolationLevel { SERIALIZABLE, REPEATABLE_READ, READ_COMMITTED, READ_UNCOMMITTED, NONE } + + /* BulkImportProxyStorage methods */ + + void closeConnectionForBulkImportProxyStorage() throws StorageQueryException; + + void commitTransactionForBulkImportProxyStorage() throws StorageQueryException; + + void rollbackTransactionForBulkImportProxyStorage() throws StorageQueryException; }