Skip to content

Commit

Permalink
feat: improve querying for CredentialDefinitions (#567)
Browse files Browse the repository at this point in the history
  • Loading branch information
paullatzelsperger authored Feb 12, 2025
1 parent 9498f34 commit 4499bc4
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@
import static java.lang.String.format;

public class BaseSqlDialectStatements implements CredentialDefinitionStoreStatements {

protected final PostgresqlOperatorTranslator operatorTranslator;

public BaseSqlDialectStatements(PostgresqlOperatorTranslator operatorTranslator) {
this.operatorTranslator = operatorTranslator;
}

@Override
public String getInsertTemplate() {
return executeStatement()
Expand All @@ -30,7 +37,7 @@ public String getInsertTemplate() {
.jsonColumn(getAttestationsColumn())
.jsonColumn(getRulesColumn())
.jsonColumn(getMappingsColumn())
.column(getJsonSchemaColumn())
.jsonColumn(getJsonSchemaColumn())
.column(getJsonSchemaUrlColumn())
.column(getValidityColumn())
.column(getDataModelColumn())
Expand All @@ -46,7 +53,7 @@ public String getUpdateTemplate() {
.jsonColumn(getAttestationsColumn())
.jsonColumn(getRulesColumn())
.jsonColumn(getMappingsColumn())
.column(getJsonSchemaColumn())
.jsonColumn(getJsonSchemaColumn())
.column(getJsonSchemaUrlColumn())
.column(getValidityColumn())
.column(getDataModelColumn())
Expand Down Expand Up @@ -74,7 +81,7 @@ public String getFindCredentialTypeTemplate() {
@Override
public SqlQueryStatement createQuery(QuerySpec querySpec) {
var select = getSelectStatement();
return new SqlQueryStatement(select, querySpec, new CredentialDefinitionMapping(this), new PostgresqlOperatorTranslator());
return new SqlQueryStatement(select, querySpec, new CredentialDefinitionMapping(this), operatorTranslator);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public StoreResult<Void> create(CredentialDefinition credentialDefinition) {
toJson(credentialDefinition.getAttestations()),
toJson(credentialDefinition.getRules()),
toJson(credentialDefinition.getMappings()),
credentialDefinition.getJsonSchema(),
toJson(credentialDefinition.getJsonSchema()),
credentialDefinition.getJsonSchemaUrl(),
credentialDefinition.getValidity(),
credentialDefinition.getDataModel().name(),
Expand Down Expand Up @@ -141,7 +141,7 @@ public StoreResult<Void> update(CredentialDefinition credentialDefinition) {
toJson(credentialDefinition.getAttestations()),
toJson(credentialDefinition.getRules()),
toJson(credentialDefinition.getMappings()),
credentialDefinition.getJsonSchema(),
toJson(credentialDefinition.getJsonSchema()),
credentialDefinition.getJsonSchemaUrl(),
credentialDefinition.getValidity(),
credentialDefinition.getDataModel().name(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@
package org.eclipse.edc.issuerservice.store.sql.attestationdefinition.schema.postgres;

import org.eclipse.edc.issuerservice.store.sql.attestationdefinition.CredentialDefinitionStoreStatements;
import org.eclipse.edc.sql.translation.JsonArrayTranslator;
import org.eclipse.edc.sql.translation.JsonFieldTranslator;
import org.eclipse.edc.sql.translation.TranslationMapping;

import static org.eclipse.edc.issuerservice.store.sql.attestationdefinition.schema.postgres.PostgresDialectStatements.MAPPING_ALIAS;
import static org.eclipse.edc.issuerservice.store.sql.attestationdefinition.schema.postgres.PostgresDialectStatements.RULES_ALIAS;


/**
* Provides a mapping from the canonical format to SQL column names for a {@code CredentialDefinition}
Expand All @@ -27,12 +32,26 @@ public class CredentialDefinitionMapping extends TranslationMapping {
public static final String FIELD_CREDENTIAL_TYPE = "credentialType";
public static final String FIELD_CREATE_TIMESTAMP = "createdAt";
public static final String FIELD_LASTMODIFIED_TIMESTAMP = "lastModified";
public static final String FIELD_JSON_SCHEMA = "jsonSchema";
public static final String FIELD_JSON_SCHEMA_URL = "jsonSchemaUrl";
public static final String FIELD_VALIDITY = "validity";
public static final String FIELD_DATAMODEL = "dataModel";
public static final String FIELD_ATTESTATIONS = "attestations";
public static final String FIELD_RULES = "rules";
public static final String FIELD_MAPPINGS = "mappings";


public CredentialDefinitionMapping(CredentialDefinitionStoreStatements statements) {
add(FIELD_ID, statements.getIdColumn());
add(FIELD_CREDENTIAL_TYPE, statements.getCredentialTypeColumn());
add(FIELD_CREATE_TIMESTAMP, statements.getCreateTimestampColumn());
add(FIELD_LASTMODIFIED_TIMESTAMP, statements.getLastModifiedTimestampColumn());
add(FIELD_JSON_SCHEMA, new JsonFieldTranslator(statements.getJsonSchemaColumn()));
add(FIELD_JSON_SCHEMA_URL, statements.getJsonSchemaUrlColumn());
add(FIELD_VALIDITY, statements.getValidityColumn());
add(FIELD_DATAMODEL, statements.getDataModelColumn());
add(FIELD_ATTESTATIONS, new JsonArrayTranslator());
add(FIELD_RULES, new JsonFieldTranslator(RULES_ALIAS));
add(FIELD_MAPPINGS, new JsonFieldTranslator(MAPPING_ALIAS));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,40 @@
package org.eclipse.edc.issuerservice.store.sql.attestationdefinition.schema.postgres;

import org.eclipse.edc.issuerservice.store.sql.attestationdefinition.BaseSqlDialectStatements;
import org.eclipse.edc.spi.query.QuerySpec;
import org.eclipse.edc.sql.dialect.PostgresDialect;
import org.eclipse.edc.sql.translation.PostgresqlOperatorTranslator;
import org.eclipse.edc.sql.translation.SqlQueryStatement;

import static org.eclipse.edc.sql.dialect.PostgresDialect.getSelectFromJsonArrayTemplate;

/**
* Postgres-specific specialization for creating queries based on Postgres JSON operators
*/
public class PostgresDialectStatements extends BaseSqlDialectStatements {

static final String RULES_ALIAS = "a_rules";
static final String MAPPING_ALIAS = "a_mappings";

public PostgresDialectStatements() {
super(new PostgresqlOperatorTranslator());
}


@Override
public String getFormatAsJsonOperator() {
return PostgresDialect.getJsonCastOperator();
}

@Override
public SqlQueryStatement createQuery(QuerySpec querySpec) {
if (querySpec.containsAnyLeftOperand("rules")) {
var select = getSelectFromJsonArrayTemplate(getSelectStatement(), getRulesColumn(), RULES_ALIAS);
return new SqlQueryStatement(select, querySpec, new CredentialDefinitionMapping(this), operatorTranslator);
} else if (querySpec.containsAnyLeftOperand("mappings")) {
var select = getSelectFromJsonArrayTemplate(getSelectStatement(), getMappingsColumn(), MAPPING_ALIAS);
return new SqlQueryStatement(select, querySpec, new CredentialDefinitionMapping(this), operatorTranslator);
}
return super.createQuery(querySpec);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@
-- only intended for and tested with Postgres!
CREATE TABLE IF NOT EXISTS credential_definitions
(
id VARCHAR NOT NULL ,
credential_type VARCHAR NOT NULL UNIQUE ,
attestations JSON NOT NULL ,
rules JSON NOT NULL ,
mappings JSON NOT NULL ,
json_schema VARCHAR NOT NULL ,
json_schema_url VARCHAR NOT NULL ,
validity BIGINT NOT NULL ,
data_model VARCHAR NOT NULL ,
created_date BIGINT NOT NULL , -- POSIX timestamp of the creation of the PC
last_modified_date BIGINT , -- POSIX timestamp of the last modified date
id VARCHAR NOT NULL,
credential_type VARCHAR NOT NULL UNIQUE,
attestations JSON NOT NULL DEFAULT '[]',
rules JSON NOT NULL DEFAULT '[]',
mappings JSON NOT NULL DEFAULT '[]',
json_schema JSON,
json_schema_url VARCHAR,
validity BIGINT NOT NULL,
data_model VARCHAR NOT NULL,
created_date BIGINT NOT NULL, -- POSIX timestamp of the creation of the PC
last_modified_date BIGINT, -- POSIX timestamp of the last modified date
PRIMARY KEY (id)
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,28 @@

package org.eclipse.edc.issuerservice.store.sql.attestationdefinition;

import org.assertj.core.api.Assertions;
import org.eclipse.edc.issuerservice.spi.issuance.credentialdefinition.store.CredentialDefinitionStore;
import org.eclipse.edc.issuerservice.spi.issuance.credentialdefinition.store.CredentialDefinitionStoreTestBase;
import org.eclipse.edc.issuerservice.spi.issuance.model.CredentialRuleDefinition;
import org.eclipse.edc.issuerservice.store.sql.attestationdefinition.schema.postgres.PostgresDialectStatements;
import org.eclipse.edc.json.JacksonTypeManager;
import org.eclipse.edc.junit.annotations.PostgresqlIntegrationTest;
import org.eclipse.edc.junit.testfixtures.TestUtils;
import org.eclipse.edc.spi.query.Criterion;
import org.eclipse.edc.spi.query.QuerySpec;
import org.eclipse.edc.sql.QueryExecutor;
import org.eclipse.edc.sql.testfixtures.PostgresqlStoreSetupExtension;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import java.time.Clock;
import java.util.Map;
import java.util.Set;

import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat;

@PostgresqlIntegrationTest
@ExtendWith(PostgresqlStoreSetupExtension.class)
Expand All @@ -50,6 +59,71 @@ void tearDown(PostgresqlStoreSetupExtension extension) {
extension.runQuery("DROP TABLE " + statements.getCredentialDefinitionTable() + " CASCADE");
}

// queries that introspect JSON are not supported in the in-mem variant
@Test
void byJsonSchema_introspectingJson() {
var def = createCredentialDefinitionBuilder("id", "Membership")
.jsonSchemaUrl(null)
.jsonSchema("""
{
"$id": "https://example.com/person.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Person",
"type": "object",
"properties": {
"firstName": {
"type": "string",
"description": "The person's first name."
},
"age": {
"description": "Age in years which must be equal to or greater than zero.",
"type": "integer",
"minimum": 0
}
}
}
""")
.build();

var query = QuerySpec.Builder.newInstance()
.filter(new Criterion("jsonSchema.title", "=", "Person"))
.build();

getStore().create(def);
var res = getStore().query(query);
assertThat(res).isSucceeded();
Assertions.assertThat(res.getContent()).hasSize(1)
.allSatisfy(cd -> Assertions.assertThat(cd).usingRecursiveComparison().isEqualTo(def));
}

@Test
void byRuleConfiguration() {
var def1 = createCredentialDefinitionBuilder("id1", "Membership")
.rule(new CredentialRuleDefinition("rule-type-1", Map.of("ruleConfigKey", "ruleConfigValue")))
.attestations(Set.of("att1", "att2"))
.build();
var def2 = createCredentialDefinitionBuilder("id2", "Iso9001Cert")
.rule(new CredentialRuleDefinition("rule-type-2", Map.of("ruleConfigKey", "ruleConfigValue", "ruleConfigKey2", "ruleConfigValue2")))
.rule(new CredentialRuleDefinition("rule-type-2", Map.of("anotherRuleConfigKey", "anotherRuleConfigValue")))
.attestations(Set.of("att2", "att3"))
.build();

var r = getStore().create(def1).compose(v -> getStore().create(def2));
assertThat(r).isSucceeded();

var query = QuerySpec.Builder.newInstance()
.filter(new Criterion("rules.configuration.ruleConfigKey", "=", "ruleConfigValue"))
.build();

var result = getStore().query(query);
assertThat(result).isSucceeded();

Assertions.assertThat(result.getContent())
.hasSize(2)
.usingRecursiveFieldByFieldElementComparator()
.containsExactlyInAnyOrder(def1, def2);
}

@Override
protected CredentialDefinitionStore getStore() {
return store;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,11 @@ public CredentialDefinition build() {
definition.id = UUID.randomUUID().toString();
}
requireNonNull(definition.credentialType, "credentialType");
requireNonNull(definition.jsonSchema, "jsonSchema");
requireNonNull(definition.jsonSchemaUrl, "jsonSchemaUrl");

if (definition.jsonSchema == null && definition.jsonSchemaUrl == null) {
throw new IllegalStateException("Either jsonSchema or jsonSchemaUrl must be non-null");
}

return definition;
}

Expand Down
Loading

0 comments on commit 4499bc4

Please sign in to comment.