Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add an api to add bulk import users #927

Closed
wants to merge 16 commits into from
12 changes: 11 additions & 1 deletion src/main/java/io/supertokens/inmemorydb/Start.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
import io.supertokens.pluginInterface.authRecipe.LoginMethod;
import io.supertokens.pluginInterface.authRecipe.sqlStorage.AuthRecipeSQLStorage;
import io.supertokens.pluginInterface.bulkimport.BulkImportStorage;
import io.supertokens.pluginInterface.bulkimport.BulkImportUser;
import io.supertokens.pluginInterface.dashboard.DashboardSearchTags;
import io.supertokens.pluginInterface.dashboard.DashboardSessionInfo;
import io.supertokens.pluginInterface.dashboard.DashboardUser;
Expand Down Expand Up @@ -102,7 +104,7 @@ public class Start
implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage, ThirdPartySQLStorage,
JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage,
UserIdMappingSQLStorage, MultitenancyStorage, MultitenancySQLStorage, TOTPSQLStorage, ActiveUsersStorage,
DashboardSQLStorage, AuthRecipeSQLStorage {
DashboardSQLStorage, AuthRecipeSQLStorage, BulkImportStorage {

private static final Object appenderLock = new Object();
private static final String APP_ID_KEY_NAME = "app_id";
Expand Down Expand Up @@ -2952,4 +2954,12 @@ public UserIdMapping[] getUserIdMapping_Transaction(TransactionConnection con, A
}
}

@Override
public void addBulkImportUsers(AppIdentifier appIdentifier, ArrayList<BulkImportUser> users) throws StorageQueryException {
try {
BulkImportQueries.insertBulkImportUsers(this, users);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,8 @@ public String getDashboardUsersTable() {
public String getDashboardSessionsTable() {
return "dashboard_user_sessions";
}

public String getBulkImportUsersTable() {
return "bulk_import_users";
}
}
Original file line number Diff line number Diff line change
@@ -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.inmemorydb.queries;

import io.supertokens.inmemorydb.config.Config;
import io.supertokens.pluginInterface.bulkimport.BulkImportUser;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;

import static io.supertokens.inmemorydb.PreparedStatementValueSetter.NO_OP_SETTER;
import static io.supertokens.inmemorydb.QueryExecutorTemplate.update;

import java.sql.SQLException;
import java.util.ArrayList;

import io.supertokens.inmemorydb.Start;

public class BulkImportQueries {
static String getQueryToCreateBulkImportUsersTable(Start start) {
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
return "CREATE TABLE IF NOT EXISTS " + Config.getConfig(start).getBulkImportUsersTable() + " ("
+ "id CHAR(36) PRIMARY KEY,"
+ "raw_data TEXT NOT NULL,"
+ "status VARCHAR(128) NOT NULL DEFAULT 'NEW',"
+ "error_msg TEXT,"
+ "created_at TIMESTAMP DEFAULT (strftime('%s', 'now')),"
+ "updated_at TIMESTAMP DEFAULT (strftime('%s', 'now'))"
+ " );";
}

public static String getQueryToCreateStatusUpdatedAtIndex(Start start) {
return "CREATE INDEX IF NOT EXISTS bulk_import_users_status_updated_at_index ON "
+ Config.getConfig(start).getBulkImportUsersTable() + " (status, updated_at)";
}

public static void insertBulkImportUsers(Start start, ArrayList<BulkImportUser> users)
throws SQLException, StorageQueryException {
StringBuilder queryBuilder = new StringBuilder(
"INSERT INTO " + Config.getConfig(start).getBulkImportUsersTable() + " (id, raw_data) VALUES ");
for (BulkImportUser user : users) {
queryBuilder.append("('")
.append(user.id)
.append("', '")
.append(user.toString())
.append("')");

if (user != users.get(users.size() - 1)) {
queryBuilder.append(",");
}
}
queryBuilder.append(";");
update(start, queryBuilder.toString(), NO_OP_SETTER);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,12 @@ public static void createTablesIfNotExists(Start start, Main main) throws SQLExc
update(start, TOTPQueries.getQueryToCreateUsedCodesExpiryTimeIndex(start), NO_OP_SETTER);
}

if (!doesTableExists(start, Config.getConfig(start).getBulkImportUsersTable())) {
getInstance(main).addState(CREATING_NEW_TABLE, null);
update(start, BulkImportQueries.getQueryToCreateBulkImportUsersTable(start), NO_OP_SETTER);
// index:
update(start, BulkImportQueries.getQueryToCreateStatusUpdatedAtIndex(start), NO_OP_SETTER);
}
}


Expand Down
3 changes: 3 additions & 0 deletions src/main/java/io/supertokens/webserver/Webserver.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.webserver.api.accountlinking.*;
import io.supertokens.webserver.api.bulkimport.AddBulkImportUsers;
import io.supertokens.webserver.api.core.*;
import io.supertokens.webserver.api.dashboard.*;
import io.supertokens.webserver.api.emailpassword.UserAPI;
Expand Down Expand Up @@ -259,6 +260,8 @@ private void setupRoutes() {

addAPI(new RequestStatsAPI(main));

addAPI(new AddBulkImportUsers(main));

StandardContext context = tomcatReference.getContext();
Tomcat tomcat = tomcatReference.getTomcat();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* 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.webserver.api.bulkimport;

import io.supertokens.Main;
import io.supertokens.multitenancy.Multitenancy;
import io.supertokens.pluginInterface.bulkimport.BulkImportStorage;
import io.supertokens.pluginInterface.bulkimport.BulkImportUser;
import io.supertokens.pluginInterface.bulkimport.exceptions.InvalidBulkImportDataException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage;
import io.supertokens.pluginInterface.multitenancy.TenantConfig;
import io.supertokens.webserver.InputParser;
import io.supertokens.webserver.WebserverAPI;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;

public class AddBulkImportUsers extends WebserverAPI {
private static final int MAX_USERS_TO_ADD = 10000;

public AddBulkImportUsers(Main main) {
super(main, "");
}

@Override
public String getPath() {
return "/bulk-import/add-users";
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
JsonObject input = InputParser.parseJsonObjectOrThrowError(req);
JsonArray users = InputParser.parseArrayOrThrowError(input, "users", false);

if (users.size() > MAX_USERS_TO_ADD) {
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
JsonObject errorResponseJson = new JsonObject();
errorResponseJson.addProperty("error", "You can only add 1000 users at a time.");
super.sendJsonResponse(400, errorResponseJson, resp);
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
return;
}

AppIdentifier appIdentifier = null;
try {
appIdentifier = getTenantIdentifierFromRequest(req).toAppIdentifier();
} catch (ServletException e) {
throw new ServletException(e);
}
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved

rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
TenantConfig[] allTenantConfigs = Multitenancy.getAllTenantsForApp(appIdentifier, main);
ArrayList<String> validTenantIds = new ArrayList<>();
Arrays.stream(allTenantConfigs)
.forEach(tenantConfig -> validTenantIds.add(tenantConfig.tenantIdentifier.getTenantId()));

JsonArray errorsJson = new JsonArray();
ArrayList<BulkImportUser> usersToAdd = new ArrayList<>();

for (int i = 0; i < users.size(); i++) {
try {
usersToAdd.add(new BulkImportUser(users.get(i).getAsJsonObject(), validTenantIds, null));
} catch (InvalidBulkImportDataException e) {
JsonObject errorObj = new JsonObject();

JsonArray errors = e.errors.stream()
.map(JsonPrimitive::new)
.collect(JsonArray::new, JsonArray::add, JsonArray::addAll);

errorObj.addProperty("index", i);
errorObj.add("errors", errors);

errorsJson.add(errorObj);
} catch (Exception e) {
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
JsonObject errorObj = new JsonObject();
errorObj.addProperty("index", i);
errorObj.addProperty("errors", "An unknown error occurred");
errorsJson.add(errorObj);
}
}

if (errorsJson.size() > 0) {
JsonObject errorResponseJson = new JsonObject();
errorResponseJson.addProperty("error",
"Data has missing or invalid fields. Please check the users field for more details.");
errorResponseJson.add("users", errorsJson);
super.sendJsonResponse(400, errorResponseJson, resp);
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
return;
}

try {
AppIdentifierWithStorage appIdentifierWithStorage = getAppIdentifierWithStorage(req);
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
BulkImportStorage storage = appIdentifierWithStorage.getBulkImportStorage();
storage.addBulkImportUsers(appIdentifierWithStorage, usersToAdd);
} catch (Exception e) {
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
throw new ServletException(e);
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
}

JsonObject result = new JsonObject();
result.addProperty("status", "OK");
super.sendJsonResponse(200, result, resp);

}
}
Loading
Loading