Skip to content

Commit

Permalink
feat: adds userIdMapping support (#49)
Browse files Browse the repository at this point in the history
* feat: adds userIdMapping support

* completes remaining checklist items

* fixes CHANGELOG.md
  • Loading branch information
jscyo authored Jul 25, 2022
1 parent f7feec4 commit 4e66593
Show file tree
Hide file tree
Showing 7 changed files with 301 additions and 6 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.18.0] - 2022-07-25

- Adds support for UserIdMapping recipe

## [1.17.0] - 2022-06-07

- Compatibility with plugin interface 2.15 - returns only non expired session handles for a user
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ plugins {
id 'java-library'
}

version = "1.17.0"
version = "1.18.0"

repositories {
mavenCentral()
Expand Down
2 changes: 1 addition & 1 deletion pluginInterfaceSupported.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"_comment": "contains a list of plugin interfaces branch names that this core supports",
"versions": [
"2.15"
"2.16"
]
}
104 changes: 102 additions & 2 deletions src/main/java/io/supertokens/storage/postgresql/Start.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.supertokens.pluginInterface.KeyValueInfo;
import io.supertokens.pluginInterface.RECIPE_ID;
import io.supertokens.pluginInterface.STORAGE_TYPE;
import io.supertokens.pluginInterface.authRecipe.AuthRecipeStorage;
import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
import io.supertokens.pluginInterface.emailpassword.PasswordResetTokenInfo;
import io.supertokens.pluginInterface.emailpassword.UserInfo;
Expand All @@ -48,6 +49,10 @@
import io.supertokens.pluginInterface.sqlStorage.TransactionConnection;
import io.supertokens.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException;
import io.supertokens.pluginInterface.thirdparty.sqlStorage.ThirdPartySQLStorage;
import io.supertokens.pluginInterface.useridmapping.UserIdMapping;
import io.supertokens.pluginInterface.useridmapping.UserIdMappingStorage;
import io.supertokens.pluginInterface.useridmapping.exception.UnknownSuperTokensUserIdException;
import io.supertokens.pluginInterface.useridmapping.exception.UserIdMappingAlreadyExistsException;
import io.supertokens.pluginInterface.usermetadata.sqlStorage.UserMetadataSQLStorage;
import io.supertokens.pluginInterface.userroles.exception.DuplicateUserRoleMappingException;
import io.supertokens.pluginInterface.userroles.exception.UnknownRoleException;
Expand All @@ -69,8 +74,9 @@
import java.sql.SQLTransactionRollbackException;
import java.util.List;

public class Start implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage,
ThirdPartySQLStorage, JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage {
public class Start
implements SessionSQLStorage, EmailPasswordSQLStorage, EmailVerificationSQLStorage, ThirdPartySQLStorage,
JWTRecipeSQLStorage, PasswordlessSQLStorage, UserMetadataSQLStorage, UserRolesSQLStorage, UserIdMappingStorage {

private static final Object appenderLock = new Object();
public static boolean silent = false;
Expand Down Expand Up @@ -1026,6 +1032,15 @@ public AuthRecipeUserInfo[] getUsers(@NotNull Integer limit, @NotNull String tim
}
}

@Override
public boolean doesUserIdExist(String userId) throws StorageQueryException {
try {
return GeneralQueries.doesUserIdExist(this, userId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}

@Override
public List<JWTSigningKeyInfo> getJWTSigningKeys_Transaction(TransactionConnection con)
throws StorageQueryException {
Expand Down Expand Up @@ -1617,4 +1632,89 @@ public boolean doesRoleExist_Transaction(TransactionConnection con, String role)
}
}

@Override
public void createUserIdMapping(String superTokensUserId, String externalUserId,
@Nullable String externalUserIdInfo)
throws StorageQueryException, UnknownSuperTokensUserIdException, UserIdMappingAlreadyExistsException {
try {
UserIdMappingQueries.createUserIdMapping(this, superTokensUserId, externalUserId, externalUserIdInfo);
} catch (SQLException e) {
if (e instanceof PSQLException) {
PostgreSQLConfig config = Config.getConfig(this);
ServerErrorMessage serverErrorMessage = ((PSQLException) e).getServerErrorMessage();
if (isForeignKeyConstraintError(serverErrorMessage, config.getUserIdMappingTable(),
"supertokens_user_id")) {
throw new UnknownSuperTokensUserIdException();
}

if (isPrimaryKeyError(serverErrorMessage, config.getUserIdMappingTable())) {
throw new UserIdMappingAlreadyExistsException(true, true);
}

if (isUniqueConstraintError(serverErrorMessage, config.getUserIdMappingTable(),
"supertokens_user_id")) {
throw new UserIdMappingAlreadyExistsException(true, false);
}

if (isUniqueConstraintError(serverErrorMessage, config.getUserIdMappingTable(), "external_user_id")) {
throw new UserIdMappingAlreadyExistsException(false, true);
}
}
throw new StorageQueryException(e);
}

}

@Override
public boolean deleteUserIdMapping(String userId, boolean isSuperTokensUserId) throws StorageQueryException {
try {
if (isSuperTokensUserId) {
return UserIdMappingQueries.deleteUserIdMappingWithSuperTokensUserId(this, userId);
}

return UserIdMappingQueries.deleteUserIdMappingWithExternalUserId(this, userId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}

@Override
public UserIdMapping getUserIdMapping(String userId, boolean isSuperTokensUserId) throws StorageQueryException {

try {
if (isSuperTokensUserId) {
return UserIdMappingQueries.getuseraIdMappingWithSuperTokensUserId(this, userId);
}

return UserIdMappingQueries.getUserIdMappingWithExternalUserId(this, userId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}

@Override
public UserIdMapping[] getUserIdMapping(String userId) throws StorageQueryException {
try {
return UserIdMappingQueries.getUserIdMappingWithEitherSuperTokensUserIdOrExternalUserId(this, userId);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}

@Override
public boolean updateOrDeleteExternalUserIdInfo(String userId, boolean isSuperTokensUserId,
@Nullable String externalUserIdInfo) throws StorageQueryException {

try {
if (isSuperTokensUserId) {
return UserIdMappingQueries.updateOrDeleteExternalUserIdInfoWithSuperTokensUserId(this, userId,
externalUserIdInfo);
}

return UserIdMappingQueries.updateOrDeleteExternalUserIdInfoWithExternalUserId(this, userId,
externalUserIdInfo);
} catch (SQLException e) {
throw new StorageQueryException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,10 @@ public String getUserRolesTable() {
return addSchemaAndPrefixToTableName("user_roles");
}

public String getUserIdMappingTable() {
return addSchemaAndPrefixToTableName("userid_mapping");
}

private String addSchemaAndPrefixToTableName(String tableName) {
String name = tableName;
if (!postgresql_table_names_prefix.trim().equals("")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,11 @@ public static void createTablesIfNotExists(Start start) throws SQLException, Sto
update(start, UserRolesQueries.getQueryToCreateUserRolesRoleIndex(start), NO_OP_SETTER);
}

if (!doesTableExists(start, Config.getConfig(start).getUserIdMappingTable())) {
getInstance(start).addState(CREATING_NEW_TABLE, null);
update(start, UserIdMappingQueries.getQueryToCreateUserIdMappingTable(start), NO_OP_SETTER);
}

} catch (Exception e) {
if (e.getMessage().contains("schema") && e.getMessage().contains("does not exist")
&& numberOfRetries < 1) {
Expand Down Expand Up @@ -249,8 +254,9 @@ public static void deleteAllTables(Start start) throws SQLException, StorageQuer

{
String DROP_QUERY = "DROP TABLE IF EXISTS " + getConfig(start).getKeyValueTable() + ","
+ getConfig(start).getUsersTable() + "," + getConfig(start).getAccessTokenSigningKeysTable() + ","
+ getConfig(start).getSessionInfoTable() + "," + getConfig(start).getEmailPasswordUsersTable() + ","
+ getConfig(start).getUserIdMappingTable() + "," + getConfig(start).getUsersTable() + ","
+ getConfig(start).getAccessTokenSigningKeysTable() + "," + getConfig(start).getSessionInfoTable()
+ "," + getConfig(start).getEmailPasswordUsersTable() + ","
+ getConfig(start).getPasswordResetTokensTable() + ","
+ getConfig(start).getEmailVerificationTokensTable() + ","
+ getConfig(start).getEmailVerificationTable() + "," + getConfig(start).getThirdPartyUsersTable()
Expand Down Expand Up @@ -341,6 +347,13 @@ public static long getUsersCount(Start start, RECIPE_ID[] includeRecipeIds)
});
}

public static boolean doesUserIdExist(Start start, String userId) throws SQLException, StorageQueryException {

String QUERY = "SELECT 1 FROM " + getConfig(start).getUsersTable() + " WHERE user_id = ?";
return execute(start, QUERY, pst -> pst.setString(1, userId), ResultSet::next);

}

public static AuthRecipeUserInfo[] getUsers(Start start, @NotNull Integer limit, @NotNull String timeJoinedOrder,
@Nullable RECIPE_ID[] includeRecipeIds, @Nullable String userId, @Nullable Long timeJoined)
throws SQLException, StorageQueryException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* Copyright (c) 2022, 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.storage.postgresql.queries;

import io.supertokens.pluginInterface.RowMapper;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.useridmapping.UserIdMapping;
import io.supertokens.storage.postgresql.Start;
import io.supertokens.storage.postgresql.config.Config;
import io.supertokens.storage.postgresql.utils.Utils;

import javax.annotation.Nullable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;

import static io.supertokens.storage.postgresql.QueryExecutorTemplate.execute;
import static io.supertokens.storage.postgresql.QueryExecutorTemplate.update;

public class UserIdMappingQueries {

public static String getQueryToCreateUserIdMappingTable(Start start) {
String schema = Config.getConfig(start).getTableSchema();
String userIdMappingTable = Config.getConfig(start).getUserIdMappingTable();
// @formatter:off
return "CREATE TABLE IF NOT EXISTS " + userIdMappingTable + " ("
+ "supertokens_user_id CHAR(36) NOT NULL "
+ "CONSTRAINT " + Utils.getConstraintName(schema, userIdMappingTable, "supertokens_user_id", "key") + " UNIQUE,"
+ "external_user_id VARCHAR(128) NOT NULL"
+ " CONSTRAINT " + Utils.getConstraintName(schema, userIdMappingTable, "external_user_id", "key") + " UNIQUE,"
+ "external_user_id_info TEXT,"
+ " CONSTRAINT " + Utils.getConstraintName(schema, userIdMappingTable, null, "pkey") +
" PRIMARY KEY(supertokens_user_id, external_user_id),"
+ ("CONSTRAINT " + Utils.getConstraintName(schema, userIdMappingTable, "supertokens_user_id", "fkey") +
" FOREIGN KEY (supertokens_user_id)"
+ " REFERENCES " + Config.getConfig(start).getUsersTable() + "(user_id)"
+ " ON DELETE CASCADE);");
// @formatter:on
}

public static void createUserIdMapping(Start start, String superTokensUserId, String externalUserId,
String externalUserIdInfo) throws SQLException, StorageQueryException {
String QUERY = "INSERT INTO " + Config.getConfig(start).getUserIdMappingTable()
+ " (supertokens_user_id, external_user_id, external_user_id_info)" + " VALUES(?, ?, ?)";

update(start, QUERY, pst -> {
pst.setString(1, superTokensUserId);
pst.setString(2, externalUserId);
pst.setString(3, externalUserIdInfo);
});
}

public static UserIdMapping getuseraIdMappingWithSuperTokensUserId(Start start, String userId)
throws SQLException, StorageQueryException {
String QUERY = "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable()
+ " WHERE supertokens_user_id = ?";
return execute(start, QUERY, pst -> pst.setString(1, userId), result -> {
if (result.next()) {
return UserIdMappingRowMapper.getInstance().mapOrThrow(result);
}
return null;
});
}

public static UserIdMapping getUserIdMappingWithExternalUserId(Start start, String userId)
throws SQLException, StorageQueryException {
String QUERY = "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable()
+ " WHERE external_user_id = ?";

return execute(start, QUERY, pst -> pst.setString(1, userId), result -> {
if (result.next()) {
return UserIdMappingRowMapper.getInstance().mapOrThrow(result);
}
return null;
});
}

public static UserIdMapping[] getUserIdMappingWithEitherSuperTokensUserIdOrExternalUserId(Start start,
String userId) throws SQLException, StorageQueryException {
String QUERY = "SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable()
+ " WHERE supertokens_user_id = ? OR external_user_id = ? ";

return execute(start, QUERY, pst -> {
pst.setString(1, userId);
pst.setString(2, userId);
}, result -> {
ArrayList<UserIdMapping> userIdMappingArray = new ArrayList<>();
while (result.next()) {
userIdMappingArray.add(UserIdMappingRowMapper.getInstance().mapOrThrow(result));
}
return userIdMappingArray.toArray(UserIdMapping[]::new);
});

}

public static boolean deleteUserIdMappingWithSuperTokensUserId(Start start, String userId)
throws SQLException, StorageQueryException {
String QUERY = "DELETE FROM " + Config.getConfig(start).getUserIdMappingTable()
+ " WHERE supertokens_user_id = ?";

// store the number of rows updated
int rowUpdatedCount = update(start, QUERY, pst -> pst.setString(1, userId));

return rowUpdatedCount > 0;
}

public static boolean deleteUserIdMappingWithExternalUserId(Start start, String userId)
throws SQLException, StorageQueryException {
String QUERY = "DELETE FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE external_user_id = ?";

// store the number of rows updated
int rowUpdatedCount = update(start, QUERY, pst -> pst.setString(1, userId));

return rowUpdatedCount > 0;
}

public static boolean updateOrDeleteExternalUserIdInfoWithSuperTokensUserId(Start start, String userId,
@Nullable String externalUserIdInfo) throws SQLException, StorageQueryException {
String QUERY = "UPDATE " + Config.getConfig(start).getUserIdMappingTable()
+ " SET external_user_id_info = ? WHERE supertokens_user_id = ?";

int rowUpdated = update(start, QUERY, pst -> {
pst.setString(1, externalUserIdInfo);
pst.setString(2, userId);
});

return rowUpdated > 0;
}

public static boolean updateOrDeleteExternalUserIdInfoWithExternalUserId(Start start, String userId,
@Nullable String externalUserIdInfo) throws SQLException, StorageQueryException {
String QUERY = "UPDATE " + Config.getConfig(start).getUserIdMappingTable()
+ " SET external_user_id_info = ? WHERE external_user_id = ?";

int rowUpdated = update(start, QUERY, pst -> {
pst.setString(1, externalUserIdInfo);
pst.setString(2, userId);
});

return rowUpdated > 0;
}

private static class UserIdMappingRowMapper implements RowMapper<UserIdMapping, ResultSet> {
private static final UserIdMappingRowMapper INSTANCE = new UserIdMappingRowMapper();

private UserIdMappingRowMapper() {
}

private static UserIdMappingRowMapper getInstance() {
return INSTANCE;
}

@Override
public UserIdMapping map(ResultSet rs) throws Exception {
return new UserIdMapping(rs.getString("supertokens_user_id"), rs.getString("external_user_id"),
rs.getString("external_user_id_info"));
}
}

}

0 comments on commit 4e66593

Please sign in to comment.