UNOMI-487 Add profile alias management to REST API and GraphQL API (#403)

* UNOMI-487 Add profile alias management to REST API and GraphQL API

* UNOMI-487 Add profile alias management to REST API and GraphQL API
diff --git a/api/src/main/java/org/apache/unomi/api/services/ProfileService.java b/api/src/main/java/org/apache/unomi/api/services/ProfileService.java
index b2698c4..e510919 100644
--- a/api/src/main/java/org/apache/unomi/api/services/ProfileService.java
+++ b/api/src/main/java/org/apache/unomi/api/services/ProfileService.java
@@ -108,9 +108,39 @@
      */
     Profile save(Profile profile);
 
+    /**
+     * Adds the alias to the profile.
+     *
+     * @param profileID the identifier of the profile
+     * @param alias     the alias which should be linked with of the profile
+     * @param clientID  the identifier of the client
+     */
     void addAliasToProfile(String profileID, String alias, String clientID);
 
     /**
+     * Removes the alias from the profile.
+     *
+     * @param profileID the identifier of the profile
+     * @param alias     the alias which should be unlinked from the profile
+     * @param clientID  the identifier of the client
+     */
+    ProfileAlias removeAliasFromProfile(String profileID, String alias, String clientID);
+
+    /**
+     * Find profile aliases which have the specified property with the specified value, ordered according to the specified {@code sortBy} String and paged: only
+     * {@code size} of them are retrieved, starting with the {@code offset}-th one.
+     *
+     * @param profileId the identifier of the profile
+     * @param offset    zero or a positive integer specifying the position of the first profile in the total ordered collection of matching profiles
+     * @param size      a positive integer specifying how many matching profiles should be retrieved or {@code -1} if all of them should be retrieved
+     * @param sortBy    an optional ({@code null} if no sorting is required) String of comma ({@code ,}) separated property names on which ordering should be performed, ordering elements according to  the property order in
+     *                  the String, considering each in turn and moving on to the next one in case of equality of all preceding ones. Each property name is optionally
+     *                  followed by a column ({@code :}) and an order specifier: {@code asc} or {@code desc}.
+     * @return a {@link PartialList} of matching profiles
+     */
+    PartialList<ProfileAlias> findProfileAliases(String profileId, int offset, int size, String sortBy);
+
+    /**
      * Merge the specified profile properties in an existing profile,or save new profile if it does not exist yet
      *
      * @param profile the profile to be saved
diff --git a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/commands/AddAliasToProfileCommand.java b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/commands/AddAliasToProfileCommand.java
new file mode 100644
index 0000000..4ba18d5
--- /dev/null
+++ b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/commands/AddAliasToProfileCommand.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); 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 org.apache.unomi.graphql.commands;
+
+import org.apache.unomi.api.ProfileAlias;
+import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.graphql.types.input.CDPProfileAliasInput;
+import org.apache.unomi.graphql.types.output.CDPProfileAlias;
+import org.apache.unomi.persistence.spi.PersistenceService;
+
+public class AddAliasToProfileCommand extends BaseCommand<CDPProfileAlias> {
+
+    private final CDPProfileAliasInput profileAliasInput;
+
+    public AddAliasToProfileCommand(Builder builder) {
+        super(builder);
+        this.profileAliasInput = builder.profileAliasInput;
+    }
+
+    @Override
+    public CDPProfileAlias execute() {
+        ProfileService profileService = serviceManager.getService(ProfileService.class);
+
+        profileService.addAliasToProfile(
+                profileAliasInput.getProfileID().getId(),
+                profileAliasInput.getAlias(),
+                profileAliasInput.getProfileID().getClient().getId());
+
+        PersistenceService persistenceService = serviceManager.getService(PersistenceService.class);
+        persistenceService.refreshIndex(ProfileAlias.class, null);
+
+        return new CDPProfileAlias(persistenceService.load(profileAliasInput.getAlias(), ProfileAlias.class));
+    }
+
+    public static Builder create() {
+        return new Builder();
+    }
+
+    public static class Builder extends BaseCommand.Builder<Builder> {
+
+        private CDPProfileAliasInput profileAliasInput;
+
+        public Builder profileAliasInput(CDPProfileAliasInput profileAliasInput) {
+            this.profileAliasInput = profileAliasInput;
+            return this;
+        }
+
+        public AddAliasToProfileCommand build() {
+            return new AddAliasToProfileCommand(this);
+        }
+    }
+}
diff --git a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/commands/RemoveAliasFromProfileCommand.java b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/commands/RemoveAliasFromProfileCommand.java
new file mode 100644
index 0000000..63aa065
--- /dev/null
+++ b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/commands/RemoveAliasFromProfileCommand.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); 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 org.apache.unomi.graphql.commands;
+
+import org.apache.unomi.api.ProfileAlias;
+import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.graphql.types.input.CDPProfileIDInput;
+import org.apache.unomi.graphql.types.output.CDPProfileAlias;
+
+public class RemoveAliasFromProfileCommand extends BaseCommand<CDPProfileAlias> {
+
+    private final String alias;
+
+    private final CDPProfileIDInput profileIDInput;
+
+    private RemoveAliasFromProfileCommand(Builder builder) {
+        super(builder);
+        this.alias = builder.alias;
+        this.profileIDInput = builder.profileIDInput;
+    }
+
+    @Override
+    public CDPProfileAlias execute() {
+        ProfileService profileService = serviceManager.getService(ProfileService.class);
+
+        ProfileAlias profileAlias = profileService.removeAliasFromProfile(profileIDInput.getId(), alias, profileIDInput.getClient().getId());
+
+        return profileAlias != null ? new CDPProfileAlias(profileAlias) : null;
+    }
+
+    public static Builder create() {
+        return new Builder();
+    }
+
+    public static class Builder extends BaseCommand.Builder<Builder> {
+
+        private String alias;
+
+        private CDPProfileIDInput profileIDInput;
+
+        public Builder setAlias(String alias) {
+            this.alias = alias;
+            return this;
+        }
+
+        public Builder setProfileIDInput(CDPProfileIDInput profileIDInput) {
+            this.profileIDInput = profileIDInput;
+            return this;
+        }
+
+        public RemoveAliasFromProfileCommand build() {
+            return new RemoveAliasFromProfileCommand(this);
+        }
+    }
+}
diff --git a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/condition/factories/ProfileAliasConditionFactory.java b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/condition/factories/ProfileAliasConditionFactory.java
new file mode 100644
index 0000000..28c6340
--- /dev/null
+++ b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/condition/factories/ProfileAliasConditionFactory.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); 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 org.apache.unomi.graphql.condition.factories;
+
+import graphql.schema.DataFetchingEnvironment;
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.graphql.types.input.CDPProfileAliasFilterInput;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class ProfileAliasConditionFactory extends ConditionFactory {
+
+    private static ProfileAliasConditionFactory instance;
+
+    public static synchronized ProfileAliasConditionFactory get(final DataFetchingEnvironment environment) {
+        if (instance == null) {
+            instance = new ProfileAliasConditionFactory(environment);
+        }
+
+        return instance;
+    }
+
+    private ProfileAliasConditionFactory(final DataFetchingEnvironment environment) {
+        super("profileAliasesPropertyCondition", environment);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Condition filterInputCondition(final CDPProfileAliasFilterInput filterInput, final Map<String, Object> filterInputAsMap) {
+        if (filterInput == null) {
+            return matchAllCondition();
+        }
+
+        final List<Condition> rootSubConditions = new ArrayList<>();
+
+        if (filterInput.getAlias_equals() != null) {
+            rootSubConditions.add(propertyCondition("itemId", filterInput.getAlias_equals()));
+        }
+
+        if (filterInput.getProfileID_equals() != null) {
+            rootSubConditions.add(propertyCondition("profileID.keyword", filterInput.getProfileID_equals()));
+        }
+
+        if (filterInput.getClientID_equals() != null) {
+            rootSubConditions.add(propertyCondition("clientID.keyword", filterInput.getClientID_equals()));
+        }
+
+        if (filterInputAsMap.get("and") != null) {
+            final List<Map<String, Object>> andFilterInputAsMap = (List<Map<String, Object>>) filterInputAsMap.get("and");
+
+            rootSubConditions.add(filtersToCondition(filterInput.getAnd(), andFilterInputAsMap, this::filterInputCondition, "and"));
+        }
+
+        if (filterInputAsMap.get("or") != null) {
+            final List<Map<String, Object>> orFilterInputAsMap = (List<Map<String, Object>>) filterInputAsMap.get("or");
+
+            rootSubConditions.add(filtersToCondition(filterInput.getOr(), orFilterInputAsMap, this::filterInputCondition, "or"));
+        }
+
+        return booleanCondition("and", rootSubConditions);
+    }
+}
diff --git a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/fetchers/profile/FindProfileAliasConnectionDataFetcher.java b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/fetchers/profile/FindProfileAliasConnectionDataFetcher.java
new file mode 100644
index 0000000..e6dea60
--- /dev/null
+++ b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/fetchers/profile/FindProfileAliasConnectionDataFetcher.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); 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 org.apache.unomi.graphql.fetchers.profile;
+
+import graphql.schema.DataFetchingEnvironment;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.ProfileAlias;
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.query.Query;
+import org.apache.unomi.graphql.condition.factories.ProfileAliasConditionFactory;
+import org.apache.unomi.graphql.fetchers.BaseConnectionDataFetcher;
+import org.apache.unomi.graphql.fetchers.ConnectionParams;
+import org.apache.unomi.graphql.services.ServiceManager;
+import org.apache.unomi.graphql.types.input.CDPOrderByInput;
+import org.apache.unomi.graphql.types.input.CDPProfileAliasFilterInput;
+import org.apache.unomi.graphql.types.output.CDPPageInfo;
+import org.apache.unomi.graphql.types.output.CDPProfileAliasConnection;
+import org.apache.unomi.graphql.types.output.CDPProfileAliasEdge;
+import org.apache.unomi.persistence.spi.PersistenceService;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class FindProfileAliasConnectionDataFetcher extends BaseConnectionDataFetcher<CDPProfileAliasConnection> {
+
+    private final CDPProfileAliasFilterInput filterInput;
+
+    private final List<CDPOrderByInput> orderByInput;
+
+    public FindProfileAliasConnectionDataFetcher(
+            final CDPProfileAliasFilterInput filterInput, final List<CDPOrderByInput> orderByInput) {
+        this.filterInput = filterInput;
+        this.orderByInput = orderByInput;
+    }
+
+    @Override
+    public CDPProfileAliasConnection get(DataFetchingEnvironment environment) throws Exception {
+        final ServiceManager serviceManager = environment.getContext();
+
+        final PersistenceService persistenceService = serviceManager.getService(PersistenceService.class);
+
+        final ConnectionParams params = parseConnectionParams(environment);
+
+        final Query query = buildQuery(createCondition(environment), orderByInput, params);
+
+        final PartialList<ProfileAlias> partialList = persistenceService.query(
+                query.getCondition(), query.getSortby(), ProfileAlias.class, query.getOffset(), query.getLimit());
+
+        final List<CDPProfileAliasEdge> edges = partialList.getList().stream().map(CDPProfileAliasEdge::new).collect(Collectors.toList());
+
+        return new CDPProfileAliasConnection(edges, new CDPPageInfo(), partialList.getTotalSize());
+    }
+
+    private Condition createCondition(final DataFetchingEnvironment environment) {
+        return ProfileAliasConditionFactory.get(environment).filterInputCondition(filterInput, environment.getArgument("filter"));
+    }
+}
diff --git a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/fetchers/profile/GetProfileAliasesDataFetcher.java b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/fetchers/profile/GetProfileAliasesDataFetcher.java
new file mode 100644
index 0000000..0b14d3d
--- /dev/null
+++ b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/fetchers/profile/GetProfileAliasesDataFetcher.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); 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 org.apache.unomi.graphql.fetchers.profile;
+
+import graphql.schema.DataFetcher;
+import graphql.schema.DataFetchingEnvironment;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.api.ProfileAlias;
+import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.graphql.services.ServiceManager;
+import org.apache.unomi.graphql.types.output.CDPProfileAlias;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class GetProfileAliasesDataFetcher implements DataFetcher<List<CDPProfileAlias>> {
+
+    private final String profileId;
+
+    public GetProfileAliasesDataFetcher(final String profileId) {
+        this.profileId = profileId;
+    }
+
+    @Override
+    public List<CDPProfileAlias> get(final DataFetchingEnvironment environment) throws Exception {
+        ServiceManager serviceManager = environment.getContext();
+
+        ProfileService profileService = serviceManager.getService(ProfileService.class);
+        PartialList<ProfileAlias> partialList = profileService.findProfileAliases(profileId, 0, 100, null);
+
+        return partialList.getList().stream().map(CDPProfileAlias::new).collect(Collectors.toList());
+    }
+}
diff --git a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/fetchers/profile/ProfileAliasDataFetcher.java b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/fetchers/profile/ProfileAliasDataFetcher.java
new file mode 100644
index 0000000..fd99e06
--- /dev/null
+++ b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/fetchers/profile/ProfileAliasDataFetcher.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); 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 org.apache.unomi.graphql.fetchers.profile;
+
+import graphql.schema.DataFetcher;
+import graphql.schema.DataFetchingEnvironment;
+import org.apache.unomi.api.ProfileAlias;
+import org.apache.unomi.graphql.services.ServiceManager;
+import org.apache.unomi.graphql.types.output.CDPProfileAlias;
+import org.apache.unomi.persistence.spi.PersistenceService;
+
+public class ProfileAliasDataFetcher implements DataFetcher<CDPProfileAlias> {
+
+    private final String alias;
+
+    public ProfileAliasDataFetcher(final String alias) {
+        this.alias = alias;
+    }
+
+    @Override
+    public CDPProfileAlias get(final DataFetchingEnvironment environment) throws Exception {
+        ServiceManager serviceManager = environment.getContext();
+        PersistenceService persistenceService = serviceManager.getService(PersistenceService.class);
+        ProfileAlias profileAlias = persistenceService.load(alias, ProfileAlias.class);
+        return profileAlias != null ? new CDPProfileAlias(profileAlias) : null;
+    }
+}
diff --git a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/input/CDPProfileAliasFilterInput.java b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/input/CDPProfileAliasFilterInput.java
new file mode 100644
index 0000000..ace6800
--- /dev/null
+++ b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/input/CDPProfileAliasFilterInput.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); 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 org.apache.unomi.graphql.types.input;
+
+import graphql.annotations.annotationTypes.GraphQLField;
+import graphql.annotations.annotationTypes.GraphQLName;
+
+import java.util.List;
+
+import static org.apache.unomi.graphql.types.input.CDPProfileAliasFilterInput.TYPE_NAME;
+
+@GraphQLName(TYPE_NAME)
+public class CDPProfileAliasFilterInput {
+
+    public static final String TYPE_NAME = "CDP_ProfileAliasFilterInput";
+
+    @GraphQLField
+    @GraphQLName("and")
+    private final List<CDPProfileAliasFilterInput> and;
+
+    @GraphQLField
+    @GraphQLName("or")
+    private final List<CDPProfileAliasFilterInput> or;
+
+    @GraphQLField
+    @GraphQLName("alias_equals")
+    private final String alias_equals;
+
+    @GraphQLField
+    @GraphQLName("profileID_equals")
+    private final String profileID_equals;
+
+    @GraphQLField
+    @GraphQLName("clientID_equals")
+    private final String clientID_equals;
+
+    public CDPProfileAliasFilterInput(
+            @GraphQLName("and") List<CDPProfileAliasFilterInput> and,
+            @GraphQLName("or") List<CDPProfileAliasFilterInput> or,
+            @GraphQLName("alias_equals") String alias_equals,
+            @GraphQLName("profileID_equals") String profileID_equals,
+            @GraphQLName("clientID_equals") String clientID_equals) {
+        this.and = and;
+        this.or = or;
+        this.alias_equals = alias_equals;
+        this.profileID_equals = profileID_equals;
+        this.clientID_equals = clientID_equals;
+    }
+
+    public List<CDPProfileAliasFilterInput> getAnd() {
+        return and;
+    }
+
+    public List<CDPProfileAliasFilterInput> getOr() {
+        return or;
+    }
+
+    public String getAlias_equals() {
+        return alias_equals;
+    }
+
+    public String getProfileID_equals() {
+        return profileID_equals;
+    }
+
+    public String getClientID_equals() {
+        return clientID_equals;
+    }
+}
diff --git a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/input/CDPProfileAliasInput.java b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/input/CDPProfileAliasInput.java
new file mode 100644
index 0000000..1351a8c
--- /dev/null
+++ b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/input/CDPProfileAliasInput.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); 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 org.apache.unomi.graphql.types.input;
+
+import graphql.annotations.annotationTypes.GraphQLField;
+import graphql.annotations.annotationTypes.GraphQLID;
+import graphql.annotations.annotationTypes.GraphQLName;
+import graphql.annotations.annotationTypes.GraphQLNonNull;
+
+import static org.apache.unomi.graphql.types.input.CDPProfileAliasInput.TYPE_NAME;
+
+@GraphQLName(TYPE_NAME)
+public class CDPProfileAliasInput {
+
+    public static final String TYPE_NAME = "CDP_ProfileAliasInput";
+
+    @GraphQLID
+    @GraphQLField
+    @GraphQLNonNull
+    private final String alias;
+
+    @GraphQLField
+    @GraphQLNonNull
+    private final CDPProfileIDInput profileID;
+
+    public CDPProfileAliasInput(@GraphQLID @GraphQLNonNull @GraphQLName("alias") String alias,
+                                @GraphQLNonNull @GraphQLName("profileID") CDPProfileIDInput profileID) {
+        this.alias = alias;
+        this.profileID = profileID;
+    }
+
+    public String getAlias() {
+        return alias;
+    }
+
+    public CDPProfileIDInput getProfileID() {
+        return profileID;
+    }
+}
diff --git a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/output/CDPMutation.java b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/output/CDPMutation.java
index ee1cdeb..26f88bd 100644
--- a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/output/CDPMutation.java
+++ b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/output/CDPMutation.java
@@ -21,6 +21,7 @@
 import graphql.annotations.annotationTypes.GraphQLName;
 import graphql.annotations.annotationTypes.GraphQLNonNull;
 import graphql.schema.DataFetchingEnvironment;
+import org.apache.unomi.graphql.commands.AddAliasToProfileCommand;
 import org.apache.unomi.graphql.commands.CreateOrUpdatePersonaCommand;
 import org.apache.unomi.graphql.commands.CreateOrUpdateProfilePropertiesCommand;
 import org.apache.unomi.graphql.commands.CreateOrUpdateSourceCommand;
@@ -33,6 +34,7 @@
 import org.apache.unomi.graphql.commands.DeleteTopicCommand;
 import org.apache.unomi.graphql.commands.DeleteViewCommand;
 import org.apache.unomi.graphql.commands.ProcessEventsCommand;
+import org.apache.unomi.graphql.commands.RemoveAliasFromProfileCommand;
 import org.apache.unomi.graphql.commands.list.AddProfileToListCommand;
 import org.apache.unomi.graphql.commands.list.CreateOrUpdateListCommand;
 import org.apache.unomi.graphql.commands.list.DeleteListCommand;
@@ -43,6 +45,7 @@
 import org.apache.unomi.graphql.types.input.CDPEventInput;
 import org.apache.unomi.graphql.types.input.CDPListInput;
 import org.apache.unomi.graphql.types.input.CDPPersonaInput;
+import org.apache.unomi.graphql.types.input.CDPProfileAliasInput;
 import org.apache.unomi.graphql.types.input.CDPProfileIDInput;
 import org.apache.unomi.graphql.types.input.CDPPropertyInput;
 import org.apache.unomi.graphql.types.input.CDPSegmentInput;
@@ -265,4 +268,28 @@
                 .execute();
     }
 
+    @GraphQLField
+    public CDPProfileAlias addAliasToProfile(
+            final @GraphQLNonNull @GraphQLName("profileAlias") CDPProfileAliasInput profileAliasInput,
+            final DataFetchingEnvironment environment) {
+        return AddAliasToProfileCommand.create()
+                .profileAliasInput(profileAliasInput)
+                .setEnvironment(environment)
+                .build()
+                .execute();
+    }
+
+    @GraphQLField
+    public CDPProfileAlias removeAliasFromProfile(
+            final @GraphQLID @GraphQLNonNull @GraphQLName("alias") String alias,
+            final @GraphQLName("profileID") CDPProfileIDInput profileIDInput,
+            final DataFetchingEnvironment environment
+    ) {
+        return RemoveAliasFromProfileCommand.create()
+                .setAlias(alias)
+                .setProfileIDInput(profileIDInput)
+                .setEnvironment(environment)
+                .build()
+                .execute();
+    }
 }
diff --git a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/output/CDPProfileAlias.java b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/output/CDPProfileAlias.java
new file mode 100644
index 0000000..cf2b3bb
--- /dev/null
+++ b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/output/CDPProfileAlias.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); 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 org.apache.unomi.graphql.types.output;
+
+import graphql.annotations.annotationTypes.GraphQLDescription;
+import graphql.annotations.annotationTypes.GraphQLField;
+import graphql.annotations.annotationTypes.GraphQLName;
+import org.apache.unomi.api.ProfileAlias;
+
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
+
+import static org.apache.unomi.graphql.types.output.CDPProfileAlias.TYPE_NAME;
+
+@GraphQLName(TYPE_NAME)
+@GraphQLDescription("Alias for Profile.")
+public class CDPProfileAlias {
+    public static final String TYPE_NAME = "CDP_ProfileAlias";
+
+    private final ProfileAlias profileAlias;
+
+    public CDPProfileAlias(final ProfileAlias profileAlias) {
+        this.profileAlias = profileAlias;
+    }
+
+    @GraphQLField
+    public String alias() {
+        return profileAlias != null ? profileAlias.getItemId() : null;
+    }
+
+    @GraphQLField
+    public CDPProfileID profileID() {
+        if (profileAlias != null) {
+            CDPProfileID profileID = new CDPProfileID(profileAlias.getProfileID());
+            if (profileAlias.getClientID() != null) {
+                profileID.setClient(new CDPClient(profileAlias.getClientID(), profileAlias.getClientID()));
+            }
+            return profileID;
+        }
+        return null;
+    }
+
+    @GraphQLField
+    public OffsetDateTime creationTime() {
+        return profileAlias != null ? profileAlias.getCreationTime().toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime() : null;
+    }
+
+    @GraphQLField
+    public OffsetDateTime modifiedTime() {
+        return profileAlias != null ? profileAlias.getModifiedTime().toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime() : null;
+    }
+}
diff --git a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/output/CDPProfileAliasConnection.java b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/output/CDPProfileAliasConnection.java
new file mode 100644
index 0000000..a6a9459
--- /dev/null
+++ b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/output/CDPProfileAliasConnection.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); 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 org.apache.unomi.graphql.types.output;
+
+import graphql.annotations.annotationTypes.GraphQLField;
+import graphql.annotations.annotationTypes.GraphQLName;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.apache.unomi.graphql.types.output.CDPProfileAliasConnection.TYPE_NAME;
+
+@GraphQLName(TYPE_NAME)
+public class CDPProfileAliasConnection {
+
+    public static final String TYPE_NAME = "CDP_ProfileAliasConnection";
+
+    private final List<CDPProfileAliasEdge> edges;
+
+    private final CDPPageInfo pageInfo;
+
+    private final Long totalCount;
+
+    public CDPProfileAliasConnection() {
+        this(new ArrayList<>(), new CDPPageInfo(), 0L);
+    }
+
+    public CDPProfileAliasConnection(
+            final List<CDPProfileAliasEdge> edges, final CDPPageInfo pageInfo, final Long totalCount) {
+        this.edges = edges;
+        this.pageInfo = pageInfo;
+        this.totalCount = totalCount;
+    }
+
+    @GraphQLField
+    @GraphQLName("edges")
+    public List<CDPProfileAliasEdge> edges() {
+        return edges;
+    }
+
+    @GraphQLField
+    @GraphQLName("pageInfo")
+    public CDPPageInfo pageInfo() {
+        return pageInfo;
+    }
+
+    @GraphQLField
+    @GraphQLName("totalCount")
+    public Long totalCount() {
+        return totalCount;
+    }
+}
diff --git a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/output/CDPProfileAliasEdge.java b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/output/CDPProfileAliasEdge.java
new file mode 100644
index 0000000..293b767
--- /dev/null
+++ b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/output/CDPProfileAliasEdge.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); 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 org.apache.unomi.graphql.types.output;
+
+import graphql.annotations.annotationTypes.GraphQLField;
+import graphql.annotations.annotationTypes.GraphQLName;
+import graphql.annotations.annotationTypes.GraphQLNonNull;
+import graphql.schema.DataFetchingEnvironment;
+import org.apache.unomi.api.ProfileAlias;
+
+import static org.apache.unomi.graphql.types.output.CDPProfileAliasEdge.TYPE_NAME;
+
+@GraphQLName(TYPE_NAME)
+public class CDPProfileAliasEdge {
+
+    public static final String TYPE_NAME = "CDP_ProfileAliasEdge";
+
+    private final ProfileAlias profileAlias;
+
+    public CDPProfileAliasEdge(final ProfileAlias profileAlias) {
+        this.profileAlias = profileAlias;
+    }
+
+    @GraphQLField
+    @GraphQLName("node")
+    public CDPProfileAlias node(final DataFetchingEnvironment environment) {
+        return new CDPProfileAlias(profileAlias);
+    }
+
+    @GraphQLNonNull
+    @GraphQLField
+    public String cursor() {
+        return profileAlias != null ? profileAlias.getItemId() : null;
+    }
+}
diff --git a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/output/CDPQuery.java b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/output/CDPQuery.java
index e723fcf..2b7ba0d 100644
--- a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/output/CDPQuery.java
+++ b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/output/CDPQuery.java
@@ -16,7 +16,11 @@
  */
 package org.apache.unomi.graphql.types.output;
 
-import graphql.annotations.annotationTypes.*;
+import graphql.annotations.annotationTypes.GraphQLDescription;
+import graphql.annotations.annotationTypes.GraphQLField;
+import graphql.annotations.annotationTypes.GraphQLID;
+import graphql.annotations.annotationTypes.GraphQLName;
+import graphql.annotations.annotationTypes.GraphQLNonNull;
 import graphql.schema.DataFetchingEnvironment;
 import org.apache.unomi.graphql.fetchers.FindTopicsConnectionDataFetcher;
 import org.apache.unomi.graphql.fetchers.SourceDataFetcher;
@@ -26,7 +30,10 @@
 import org.apache.unomi.graphql.fetchers.event.FindEventsConnectionDataFetcher;
 import org.apache.unomi.graphql.fetchers.list.GetListDataFetcher;
 import org.apache.unomi.graphql.fetchers.list.ListConnectionDataFetcher;
+import org.apache.unomi.graphql.fetchers.profile.FindProfileAliasConnectionDataFetcher;
 import org.apache.unomi.graphql.fetchers.profile.FindProfilesConnectionDataFetcher;
+import org.apache.unomi.graphql.fetchers.profile.GetProfileAliasesDataFetcher;
+import org.apache.unomi.graphql.fetchers.profile.ProfileAliasDataFetcher;
 import org.apache.unomi.graphql.fetchers.profile.ProfileDataFetcher;
 import org.apache.unomi.graphql.fetchers.profile.PropertiesConnectionDataFetcher;
 import org.apache.unomi.graphql.fetchers.segment.FindSegmentsConnectionDataFetcher;
@@ -35,6 +42,7 @@
 import org.apache.unomi.graphql.types.input.CDPEventFilterInput;
 import org.apache.unomi.graphql.types.input.CDPListFilterInput;
 import org.apache.unomi.graphql.types.input.CDPOrderByInput;
+import org.apache.unomi.graphql.types.input.CDPProfileAliasFilterInput;
 import org.apache.unomi.graphql.types.input.CDPProfileFilterInput;
 import org.apache.unomi.graphql.types.input.CDPProfileIDInput;
 import org.apache.unomi.graphql.types.input.CDPSegmentFilterInput;
@@ -174,4 +182,30 @@
         return new ListConnectionDataFetcher(filterInput, orderByInput).get(environment);
     }
 
+    @GraphQLField
+    public CDPProfileAlias getProfileAlias(
+            final @GraphQLID @GraphQLNonNull @GraphQLName("alias") String alias,
+            final DataFetchingEnvironment environment) throws Exception {
+        return new ProfileAliasDataFetcher(alias).get(environment);
+    }
+
+    @GraphQLField
+    public List<CDPProfileAlias> getProfileAliases(
+            final @GraphQLID @GraphQLName("profileID") @GraphQLNonNull String profileID,
+            final DataFetchingEnvironment environment) throws Exception {
+        return new GetProfileAliasesDataFetcher(profileID).get(environment);
+    }
+
+    @GraphQLField
+    public CDPProfileAliasConnection findProfileAliases(
+            final @GraphQLName("filter") CDPProfileAliasFilterInput filterInput,
+            final @GraphQLName("orderBy") List<CDPOrderByInput> orderByInput,
+            final @GraphQLName("first") Integer first,
+            final @GraphQLName("after") String after,
+            final @GraphQLName("last") Integer last,
+            final @GraphQLName("before") String before,
+            final DataFetchingEnvironment environment
+    ) throws Exception {
+        return new FindProfileAliasConnectionDataFetcher(filterInput, orderByInput).get(environment);
+    }
 }
diff --git a/itests/src/test/java/org/apache/unomi/itests/AllITs.java b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
index b198a93..70230bf 100644
--- a/itests/src/test/java/org/apache/unomi/itests/AllITs.java
+++ b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
@@ -59,7 +59,8 @@
         GraphQLProfilePropertiesIT.class,
         GraphQLSegmentIT.class,
         GraphQLWebSocketIT.class,
-        JSONSchemaIT.class
+        JSONSchemaIT.class,
+        GraphQLProfileAliasesIT.class
 })
 public class AllITs {
 }
diff --git a/itests/src/test/java/org/apache/unomi/itests/ProfileServiceIT.java b/itests/src/test/java/org/apache/unomi/itests/ProfileServiceIT.java
index e9bf221..b91aec2 100644
--- a/itests/src/test/java/org/apache/unomi/itests/ProfileServiceIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/ProfileServiceIT.java
@@ -29,6 +29,8 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.fail;
+
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.Before;
@@ -44,6 +46,7 @@
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.UUID;
 import java.util.stream.IntStream;
 
 import javax.inject.Inject;
@@ -179,7 +182,8 @@
 
     @Test
     public void testLoadProfileByAlias() throws Exception {
-        String profileID = "profileID_testLoadProfileByAlias";
+        String profileID = UUID.randomUUID().toString();
+
         try {
             Profile profile = new Profile();
             profile.setItemId(profileID);
@@ -189,7 +193,7 @@
 
             IntStream.range(1, 3).forEach(index -> {
                 final String profileAlias = profileID + "_alias_" + index;
-                profileService.addAliasToProfile(profileID, profileAlias, "clientID");
+                profileService.addAliasToProfile(profileID, profileAlias, "clientID" + index);
             });
 
             refreshPersistence();
@@ -205,13 +209,16 @@
             storedProfile = profileService.load(profileID + "_alias_2");
             assertNotNull(storedProfile);
             assertEquals(profileID, storedProfile.getItemId());
-        } finally {
-            profileService.delete(profileID, false);
 
+            PartialList<ProfileAlias> aliasList = profileService.findProfileAliases(profileID, 0, 10, null);
+            assertEquals(2, aliasList.size());
+        } finally {
             IntStream.range(1, 3).forEach(index -> {
                 final String profileAlias = profileID + "_alias_" + index;
-                persistenceService.remove(profileAlias, ProfileAlias.class);
+                profileService.removeAliasFromProfile(profileID, profileAlias, "clientID" + index);
             });
+
+            profileService.delete(profileID, false);
         }
     }
 
diff --git a/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLProfileAliasesIT.java b/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLProfileAliasesIT.java
new file mode 100644
index 0000000..6cd6275
--- /dev/null
+++ b/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLProfileAliasesIT.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); 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 org.apache.unomi.itests.graphql;
+
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.unomi.api.Profile;
+import org.apache.unomi.api.ProfileAlias;
+import org.apache.unomi.api.services.ProfileService;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.ops4j.pax.exam.util.Filter;
+
+import javax.inject.Inject;
+
+public class GraphQLProfileAliasesIT extends BaseGraphQLIT {
+
+    @Inject
+    @Filter(timeout = 600000)
+    protected ProfileService profileService;
+
+    @Before
+    public void setUp() throws InterruptedException {
+        removeItems(ProfileAlias.class);
+        removeItems(Profile.class);
+    }
+
+    @Test
+    public void lifecycle() throws Exception {
+        final Profile profile = new Profile("f6c1c5a0-eff3-42b7-b375-44da5d01f2a6");
+        profileService.save(profile);
+
+        // test adding an alias to a profile
+        try (CloseableHttpResponse response = post("graphql/profileAlias/addAliasToProfile.json")) {
+            final ResponseContext context = ResponseContext.parse(response.getEntity());
+
+            Assert.assertNotNull(context.getValue("data.cdp.addAliasToProfile"));
+            Assert.assertEquals("myAlias", context.getValue("data.cdp.addAliasToProfile.alias"));
+            Assert.assertNotNull(context.getValue("data.cdp.addAliasToProfile.profileID"));
+            Assert.assertEquals("f6c1c5a0-eff3-42b7-b375-44da5d01f2a6",
+                    context.getValue("data.cdp.addAliasToProfile.profileID.id"));
+            Assert.assertNotNull(context.getValue("data.cdp.addAliasToProfile.profileID.client"));
+            Assert.assertEquals("facebook", context.getValue("data.cdp.addAliasToProfile.profileID.client.ID"));
+        }
+
+        // test fetching a profile by an alias
+        try (CloseableHttpResponse response = post("graphql/profileAlias/getProfileAlias.json")) {
+            final ResponseContext context = ResponseContext.parse(response.getEntity());
+
+            Assert.assertNotNull(context.getValue("data.cdp.getProfileAlias"));
+            Assert.assertEquals("myAlias", context.getValue("data.cdp.getProfileAlias.alias"));
+            Assert.assertNotNull(context.getValue("data.cdp.getProfileAlias.profileID"));
+            Assert.assertEquals("f6c1c5a0-eff3-42b7-b375-44da5d01f2a6",
+                    context.getValue("data.cdp.getProfileAlias.profileID.id"));
+        }
+
+        // test fetching a profile's aliases
+        try (CloseableHttpResponse response = post("graphql/profileAlias/getProfileAliases.json")) {
+            final ResponseContext context = ResponseContext.parse(response.getEntity());
+
+            Assert.assertNotNull(context.getValue("data.cdp.getProfileAliases"));
+            Assert.assertEquals("myAlias", context.getValue("data.cdp.getProfileAliases[0].alias"));
+            Assert.assertNotNull(context.getValue("data.cdp.getProfileAliases[0].profileID"));
+            Assert.assertEquals("f6c1c5a0-eff3-42b7-b375-44da5d01f2a6",
+                    context.getValue("data.cdp.getProfileAliases[0].profileID.id"));
+        }
+
+        // test filtering a profile aliases by criteria
+        try (CloseableHttpResponse response = post("graphql/profileAlias/findProfileAliases.json")) {
+            final ResponseContext context = ResponseContext.parse(response.getEntity());
+
+            Assert.assertNotNull(context.getValue("data.cdp.findProfileAliases"));
+            Assert.assertEquals("myAlias", context.getValue("data.cdp.findProfileAliases.edges[0].node.alias"));
+            Assert.assertNotNull(context.getValue("data.cdp.findProfileAliases.edges[0].node.profileID"));
+            Assert.assertEquals("f6c1c5a0-eff3-42b7-b375-44da5d01f2a6",
+                    context.getValue("data.cdp.findProfileAliases.edges[0].node.profileID.id"));
+        }
+
+        // test removing an alias from a profile
+        try (CloseableHttpResponse response = post("graphql/profileAlias/removeAliasFromProfile.json")) {
+            final ResponseContext context = ResponseContext.parse(response.getEntity());
+
+            Assert.assertNotNull(context.getValue("data.cdp.removeAliasFromProfile"));
+            Assert.assertEquals("myAlias", context.getValue("data.cdp.removeAliasFromProfile.alias"));
+            Assert.assertNotNull(context.getValue("data.cdp.removeAliasFromProfile.profileID"));
+            Assert.assertEquals("f6c1c5a0-eff3-42b7-b375-44da5d01f2a6",
+                    context.getValue("data.cdp.removeAliasFromProfile.profileID.id"));
+        }
+    }
+}
diff --git a/itests/src/test/resources/graphql/profileAlias/addAliasToProfile.json b/itests/src/test/resources/graphql/profileAlias/addAliasToProfile.json
new file mode 100644
index 0000000..81a2f7e
--- /dev/null
+++ b/itests/src/test/resources/graphql/profileAlias/addAliasToProfile.json
@@ -0,0 +1,16 @@
+{
+  "operationName": "addAliasToProfile",
+  "variables": {
+    "profileAlias": {
+      "alias": "myAlias",
+      "profileID": {
+        "id": "f6c1c5a0-eff3-42b7-b375-44da5d01f2a6",
+        "client": {
+          "id": "facebook",
+          "title": "Facebook"
+        }
+      }
+    }
+  },
+  "query": "mutation addAliasToProfile($profileAlias: CDP_ProfileAliasInput!) {\n  cdp {\n    addAliasToProfile(profileAlias: $profileAlias) {\n      alias\n      profileID {\n        client {\n          ID\n          title\n        }\n        id\n      }\n    }\n  }\n}\n"
+}
diff --git a/itests/src/test/resources/graphql/profileAlias/findProfileAliases.json b/itests/src/test/resources/graphql/profileAlias/findProfileAliases.json
new file mode 100644
index 0000000..5e4abdd
--- /dev/null
+++ b/itests/src/test/resources/graphql/profileAlias/findProfileAliases.json
@@ -0,0 +1,8 @@
+{
+  "operationName": null,
+  "variables": {
+    "alias": "myAlias",
+    "clientID": "facebook"
+  },
+  "query": "query ($alias: String, $clientID: String) {\n  cdp {\n    findProfileAliases(filter: {and: [{alias_equals: $alias}, {clientID_equals: $clientID}]}) {\n      totalCount\n      edges {\n        cursor\n        node {\n          alias\n          profileID {\n            id\n            client {\n              ID\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
+}
diff --git a/itests/src/test/resources/graphql/profileAlias/getProfileAlias.json b/itests/src/test/resources/graphql/profileAlias/getProfileAlias.json
new file mode 100644
index 0000000..8c97290
--- /dev/null
+++ b/itests/src/test/resources/graphql/profileAlias/getProfileAlias.json
@@ -0,0 +1,5 @@
+{
+  "operationName": null,
+  "variables": {},
+  "query": "{\n  cdp {\n    getProfileAlias(alias: \"myAlias\") {\n      alias\n      creationTime\n      modifiedTime\n      profileID {\n        id\n      }\n    }\n  }\n}\n"
+}
diff --git a/itests/src/test/resources/graphql/profileAlias/getProfileAliases.json b/itests/src/test/resources/graphql/profileAlias/getProfileAliases.json
new file mode 100644
index 0000000..32f4a78
--- /dev/null
+++ b/itests/src/test/resources/graphql/profileAlias/getProfileAliases.json
@@ -0,0 +1,5 @@
+{
+  "operationName": null,
+  "variables": {},
+  "query": "{\n  cdp {\n    getProfileAliases(profileID: \"f6c1c5a0-eff3-42b7-b375-44da5d01f2a6\") {\n      alias\n      creationTime\n      modifiedTime\n      profileID {\n        client {\n          ID\n          title\n        }\n        id\n      }\n    }\n  }\n}\n"
+}
diff --git a/itests/src/test/resources/graphql/profileAlias/removeAliasFromProfile.json b/itests/src/test/resources/graphql/profileAlias/removeAliasFromProfile.json
new file mode 100644
index 0000000..915dba2
--- /dev/null
+++ b/itests/src/test/resources/graphql/profileAlias/removeAliasFromProfile.json
@@ -0,0 +1,13 @@
+{
+  "operationName": "removeAliasFromProfile",
+  "variables": {
+    "alias": "myAlias",
+    "profileID": {
+      "id": "f6c1c5a0-eff3-42b7-b375-44da5d01f2a6",
+      "client": {
+        "id": "facebook"
+      }
+    }
+  },
+  "query": "mutation removeAliasFromProfile($alias: ID!, $profileID: CDP_ProfileIDInput!) {\n  cdp {\n    removeAliasFromProfile(alias: $alias, profileID: $profileID) {\n      alias\n      creationTime\n      modifiedTime\n      profileID {\n        id\n        client {\n          ID\n          title\n        }\n      }\n    }\n  }\n}\n"
+}
diff --git a/persistence-elasticsearch/core/src/main/resources/META-INF/cxs/mappings/profileAlias.json b/persistence-elasticsearch/core/src/main/resources/META-INF/cxs/mappings/profileAlias.json
new file mode 100644
index 0000000..6d2f54d
--- /dev/null
+++ b/persistence-elasticsearch/core/src/main/resources/META-INF/cxs/mappings/profileAlias.json
@@ -0,0 +1,28 @@
+{
+  "dynamic_templates": [
+    {
+      "all": {
+        "match": "*",
+        "match_mapping_type": "string",
+        "mapping": {
+          "type": "text",
+          "analyzer": "folding",
+          "fields": {
+            "keyword": {
+              "type": "keyword",
+              "ignore_above": 256
+            }
+          }
+        }
+      }
+    }
+  ],
+  "properties": {
+    "creationTime": {
+      "type": "date"
+    },
+    "modifiedTime": {
+      "type": "date"
+    }
+  }
+}
diff --git a/rest/src/main/java/org/apache/unomi/rest/endpoints/ProfileServiceEndPoint.java b/rest/src/main/java/org/apache/unomi/rest/endpoints/ProfileServiceEndPoint.java
index db6aa93..86b50b7 100644
--- a/rest/src/main/java/org/apache/unomi/rest/endpoints/ProfileServiceEndPoint.java
+++ b/rest/src/main/java/org/apache/unomi/rest/endpoints/ProfileServiceEndPoint.java
@@ -622,4 +622,31 @@
     public PartialList<Session> searchSession(Query query) {
         return profileService.searchSessions(query);
     }
+
+    @POST
+    @Path("/{profileId}/aliases/{aliasId}")
+    public void addAliasToProfile(final @PathParam("profileId") String profileId,
+                                  final @PathParam("aliasId") String aliasId,
+                                  final @HeaderParam("X-Unomi-ClientId") String headerClientID) {
+        String clientId = headerClientID != null ? headerClientID : "defaultClientId";
+        profileService.addAliasToProfile(profileId, aliasId, clientId);
+    }
+
+    @DELETE
+    @Path("/{profileId}/aliases/{aliasId}")
+    public void removeAliasFromProfile(final @PathParam("profileId") String profileId,
+                                       final @PathParam("aliasId") String aliasId,
+                                       final @HeaderParam("X-Unomi-ClientId") String headerClientID) {
+        String clientId = headerClientID != null ? headerClientID : "defaultClientId";
+        profileService.removeAliasFromProfile(profileId, aliasId, clientId);
+    }
+
+    @GET
+    @Path("/{profileId}/aliases")
+    public PartialList<ProfileAlias> listAliasesByProfileId(final @PathParam("profileId") String profileId,
+                                                            @QueryParam("offset") @DefaultValue("0") int offset,
+                                                            @QueryParam("size") @DefaultValue("50") int size,
+                                                            @QueryParam("sort") String sortBy) {
+        return profileService.findProfileAliases(profileId, offset, size, sortBy);
+    }
 }
diff --git a/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java b/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java
index 148a98d..4903074 100644
--- a/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java
+++ b/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java
@@ -581,6 +581,51 @@
         }
     }
 
+    @Override
+    public ProfileAlias removeAliasFromProfile(String profileID, String alias, String clientID) {
+        Condition profileIDCondition = new Condition(definitionsService.getConditionType("profileAliasesPropertyCondition"));
+        profileIDCondition.setParameter("propertyName", "profileID.keyword");
+        profileIDCondition.setParameter("comparisonOperator", "equals");
+        profileIDCondition.setParameter("propertyValue", profileID);
+
+        Condition clientIDCondition = new Condition(definitionsService.getConditionType("profileAliasesPropertyCondition"));
+        clientIDCondition.setParameter("propertyName", "clientID.keyword");
+        clientIDCondition.setParameter("comparisonOperator", "equals");
+        clientIDCondition.setParameter("propertyValue", clientID);
+
+        Condition aliasCondition = new Condition(definitionsService.getConditionType("profileAliasesPropertyCondition"));
+        aliasCondition.setParameter("propertyName", "itemId");
+        aliasCondition.setParameter("comparisonOperator", "equals");
+        aliasCondition.setParameter("propertyValue", alias);
+
+        List<Condition> conditions = new ArrayList<>();
+        conditions.add(profileIDCondition);
+        conditions.add(clientIDCondition);
+        conditions.add(aliasCondition);
+
+        Condition condition = new Condition(definitionsService.getConditionType("booleanCondition"));
+        condition.setParameter("operator", "and");
+        condition.setParameter("subConditions", conditions);
+
+        List<ProfileAlias> profileAliases = persistenceService.query(condition, null, ProfileAlias.class);
+
+        if (profileAliases.size() == 1 && persistenceService.removeByQuery(condition, ProfileAlias.class)) {
+            return profileAliases.get(0);
+        }
+
+        return null;
+    }
+
+    @Override
+    public PartialList<ProfileAlias> findProfileAliases(String profileId, int offset, int size, String sortBy) {
+        Condition condition = new Condition(definitionsService.getConditionType("profileAliasesPropertyCondition"));
+        condition.setParameter("propertyName", "profileID.keyword");
+        condition.setParameter("comparisonOperator", "equals");
+        condition.setParameter("propertyValue", profileId);
+
+        return persistenceService.query(condition, sortBy, ProfileAlias.class, offset, size);
+    }
+
     private Profile save(Profile profile, boolean forceRefresh) {
         if (profile.getItemId() == null) {
             return null;