Developers

The InCountry platform provides developers the ability to configure and manage data localization. With a plethora of tools, InCountry integrates with almost any type of app.

InCountry’s developer toolset includes an SDK, an API, a proxy, and a line of out-of-the-box integrations for SaaS with coverage of over 90 countries.

InCountry Platform

Diagram Developers

InCountry tools

InCountry Platform provides a variety of products for data residency services, as follows:

InCountry Portal

.

InCountry Portal is an initial point to get started within the InCountry Platform. It provides the signup option to create an organization account, and further control your data stores. Here you can also manage accounts of other employees within your organization who get access to these data stores and perform specific operations.

InCountry Border

.

InCountry Border is a data-communication component that integrates between your application’s frontend and backend parts. It proxies requests with regulated data from the frontend to the backend and writes it to the InCountry Platform while returning the tokenized data to the backend. It allows you to redact and unredact data on the fly with minimal effort.

InCountry REST API

.

InCountry REST API is a set of RESTful methods that allow you to query regulated data in the InCountry Platform without integrating InCountry SDK into your application. It simplifies data communication and lets you quickly build applications with data localization capabilities.

InCountry SDK

.

InCountry SDK is a collection of software development tools for the following programming languages:

  • Python
  • Java
  • Node.js
  • .Net (coming soon)

You can integrate InCountry SDK into your application or system to develop complex data handling scenarios and use all the InCountry Platform advantages for data localization. With InCountry SDK you can implement comprehensive integrations and process regulated data the way you need. Additionally, with InCountry SDK you can manage encryption keys yourself and use custom encryption methods if needed.

SaaS Integrations (including Salesforce)

.

The InCountry Platform provides a variety of native integrations with the top-performing SaaS solutions, including Salesforce and ServiceNow. You can use the out-of-box integrations to handle regulated data from SaaS applications. Take advantage of the existing integrations and packages, the customize them according to your data and needs.

With the InCountry for Salesforce integration, you can redact regulated data in Salesforce when accessing it outside of the origin country, restrict access to it for users from specific countries, or replicate it to other countries without violating the local compliance regulations.

Features of InCountry Products

The following section contains the list of features the InCountry toolset provides:

InCountry Portal

.
  • Initial signup of new users and account recovery
  • Management of the organization and its members
  • Access management to the organization account
  • Creation of storage environments and access credentials
  • Recommendation module for storing regulated data for each country

InCountry Border

.
  • Seamless integration between your application’s frontend and backend
  • Redaction and unredaction policies for regulated data types
  • On-the-fly data redaction/unredaction
  • User-based data control
  • Tokenization of regulated data with preserving data formats
  • Encryption of regulated data in InCountry Platform

InCountry REST API

.
  • Quick creation of the application prototype
  • All the CRUD operations at hand
  • Encryption of regulated data in InCountry Platform
  • On-demand data retrieval for data bandwidth economy
  • Serverless functions for processing, validating and aggregating data

InCountry SDK

.
  • Availability for the three programming languages
  • Implementation of complex data scenarios within your application
  • Data encryption the way you need
  • Robust performance and quick data access
  • Data migration and key rotation
  • Custom encryption methods
  • Data writing/reading in batches
  • Secure data communication with TLS 1.2.

SaaS Integrations (including Salesforce)

.
  • Instant integration with SaaS applications, including Salesforce
  • Minimal time to data residency compliance
  • Serverless functions with JavaScript
  • Data encryption
  • Multiple data handling modes: redaction, restriction, replication

Choosing the best solution for your needs

Building a new application

.

For building a new application, first of all, identify the level of integration and the data communication flow between your application and the InCountry Platform. In addition to this, also identify which data regulation model you want to use and what countries your application will service.

Once all these points are identified, you can proceed to choose the InCountry tool, as follows:

1. If your application has only frontend and backend with a135792 simple data communication data flow, the easiest solution for you will be its integration with InCountry Border. You will have just to redirect your data requests from the frontend to InCountry Proxy and further route them to the backend.

2. If you want to implement a more autonomous application, you need to use InCountry REST API. You will have to authorize data requests with the issued certificate on the client-side of your application.

3. If you need to build an application with comprehensive business logic, data localization in multiple countries, and further data aggregation from them, you need to choose the InCountry SDK. It comes in a variety of programming languages including Python, Java, and Node.js. It provides all the essential development tools for implementing robust and comprehensive solutions for handling regulated data, encrypting it, and fetching it on demand. With the InCountry SDK, you get more capabilities to manage, handle, and store data the way you want.

For custom and newly built internal applications, please check our SDK.

Integrating data residency into the existing application

.

For integrating data residency into your existing application, use InCountry Border. It provides a quick way for integration within your data communication flow between the frontend and backend sides. You will need to re-route data requests from the application’s frontend to InCountry Border which will perform data redaction and unredaction. After performing these data manipulation InCountry Border will further proxy the data request with regulated and tokenized data to your application’s backend.

This product can perform on-the-fly data redaction and “unredaction”, including encryption of the regulated data fields and their tokenization for preserving the existing data format patterns established in your application’s backend. This requires minimal adjustments within your application and no changes at all within your data model.

Integrating multiple systems

.

For integrating multiple systems and handling regulated data between them through the InCountry Platform, use InCountry REST API. This way you can write regulated data from one system into the InCountry Platform, and read it from the other system by performing common requests through InCountry REST API. Tokenization of regulated data will be performed by the InCountry Platform, so you do not need to adjust anything on your systems except the storage of the record keys for differentiating records.

In the same way, you can integrate more systems, for example, you can have a CRM system and an ERP system that store their own layers of data which you want to combine and analyze in the data aggregation system of yours. InCountry REST API will allow you to do this with minimal adjustments in the data communication flow.

REST API can integrate multiple systems and perform data queries with both regulated and non-regulated data between systems. This way you can combine data for showing it within your systems.

Creating data backup

.

For creating data backup of records with regulated data, use InCountry REST API. It allows you to perform a bulk import of records into InCountry Platform. You can store these records for prolonged time with having the on-demand access to it when needed.

Data residency for SaaS solutions

.

If you use any SaaS solution, for example Salesforce, use our native SaaS integration solutions for data residency and secured data retention in InCountry Platform. Quick deployment and minimal setup are the key advantages of ready-to-use solutions. Serverless functions will allow you to enhance and variate data communication operations for your regulated data.

At the moment InCountry Platform provides the following out-of-box integrations:

  • Salesforce
  • ServiceNow
  • Mambu
  • Segment
  • Twilio

Data regulations Models

Data localization rules can affect the location of storage, viewing and processing of regulated data which in aggerate map into these three InCountry models

InCountry Data Protection Models
.ReplicatedRestrictedReducted
StorageIn and out countryIn countryIn country
ViewingIn and out countryIn and out countryIn country
ProcessingIn and out countryOut of countryIn country

InCountry platform can be configured in three models, depending on the regulatory, compliance, and customer needs.

ModelDescription

Replicated

Regulated data is first saved in the origin country, then the data can be replicated to other countries. In this case, regulated data is saved to the InCountry Platform which locates in the origin country, and then it is replicated to your system or data store in another country.

Restricted

Regulated data is stored within the InCountry Platform in the origin country, but can leave it in some cases when it is read.

All data requests for regulated data are performed directly to the InCountry Platform within the origin country when you request this data outside of the origin country. Regulated data is not saved within your system.

Reducted

Regulated data is stored within the origin country and cannot leave it. Access to this data is regulated depending on the current user’s location, so the end user will either see regulated data (if the access country matches the origin country of data) or redacted data (if the access country differs from the origin country of data).

All data requests for regulated data are performed directly to the InCountry Platform within the origin country. Regulated data is not saved within your system.

Next Steps

Below you can find the links to the ready-to-use program samples that you can use for building applications on your own.

1.
Authorization

InCountry Platform provides different ways to authorize data requests depending on the product you are using.

2.
OAuth 2.0 authorization

InCountry Platform uses the OAuth 2.0 protocol for authentication and authorization. InCountry Platform supports common OAuth 2.0 scenarios such as those for web server and client-side. The OAuth 2.0 authorization mechanism guarantees the security of your account and provides full control over your user sessions.

To get started with the InCountry Platform, create a new account on InCountry Portal. Here you need to create a new environment and client. For each client, you will get the client id and client secret which you can further use to obtain a token from the InCountry Platform.

3.
Getting client id and client secret

1. Sign up or sign in to InCountry Portal.

2. Create a new environment.

3. Create a new client.

4. InCountry Portal will issue the client id and client secret for the environment.

5. Use the issued client and client secret to generate a token with SDK.

4.
Authorization with a certificate

InCountry REST API is using a certificate to authorize data requests to InCountry Platform. The InCountry team will provide the certificate to you per your request.

Quick Recipes

Below you can find the list of quick code recipes on how to implement this or that solution with InCountry Platform.

Create Storage

Creating a storage instance

.

To get started with InCountry Platform, you need to create a storage instance:

  • Python SDK
  • Node.js SDK
  • Java SDK
Python SDK

To access your data in InCountry Platform with Python SDK, you need to create an instance of the Storage class.

class Storage:
    def __init__(
        self,
        environment_id: Optional[str] = None,   # Required to be passed in, or as the INC_API_KEY environment variable
        api_key: Optional[str] = None,          # Required when using API key authorization, or as an environment variable
        client_id: Optional[str] = None,        # Required when using oAuth authorization, can be also set through the INC_CLIENT_ID variable
        client_secret: Optional[str] = None,    # Required when using oAuth authorization, can be also set through the INC_CLIENT_SECRET variable
        endpoint: Optional[str] = None,         # Optional. Defines API URL. Can also be set up through the INC_ENDPOINT environment variable
        encrypt: Optional[bool] = True,         # Optional. If False, encryption is not applied
        debug: Optional[bool] = False,          # Optional. If True enables the additional debug logging
        options: Optional[Dict[str, Any]] = {}, # Optional. It is used to fine-tune some configurations
        custom_encryption_configs: Optional[List[dict]] = None, # Optional. List of custom encryption configurations
        secret_key_accessor: Optional[SecretKeyAccessor] = None, # Instance of the SecretKeyAccessor class. It is used to fetch the encryption secret
    ):
        ...

WARNING

API Key authorization is being deprecated. The backward compatibility is preserved for the api_key parameter but you no longer can access API keys (neither old nor new) from your dashboard.


The client_id, client_secret, and environment_id variables can be fetched from your dashboard on Incountry Portal.

The endpoint variable defines API URL and is used to override the default one.

You can disable the encryption (not recommended). Set the encrypt property to false if you want to disable it. Avoid using it when using for production data.

The options variable allows you to tweak some SDK configurations

{
    "http_options": {
        "timeout": int,         # In seconds. Should be greater than 0
    },
    "auth_endpoints": dict,     # A custom endpoints regional map that should be used for fetching oAuth tokens

    "countries_endpoint": str,  # If your PoPAPI configuration relies on a custom PoPAPI server
                                # (rather than the default one) use the `countriesEndpoint` option
                                # to specify the endpoint responsible for fetching the list of supported countries

    "endpoint_mask": str,       # Defines API base hostname part to use.
                                # If set, all requests will be sent to https://${country}${endpointMask} host
                                # instead of the default one (https://${country}-mt-01.api.incountry.io)
}

Below you can find the example of how to create a storage instance

from incountry import Storage, SecretKeyAccessor

storage = Storage(
    api_key="<api_key>",
    environment_id="<env_id>",
    debug=True,
    secret_key_accessor=SecretKeyAccessor(lambda: "password"),
    options={
        "http_options": {
            "timeout": 5
        },
        "countries_endpoint": "https://private-pop.incountry.io/countries",
        "endpoint_mask" ".private-pop.incountry.io",
    }
)

oAuth Authentication

The SDK also supports oAuth authentication credentials instead of plain API key authorization. oAuth authentication flow is mutually exclusive with API key authentication - you need to provide either an API key or oAuth credentials.

Below you can find the example of how to create a storage instance with oAuth credentials (and also provide a custom oAuth endpoint):

from incountry import Storage, SecretKeyAccessor

storage = Storage(
    client_id="<client_id>",
    client_secret="<client_secret>",
    environment_id="<env_id>",
    debug=True,
    secret_key_accessor=SecretKeyAccessor(lambda: "password"),
    options={
        "auth_endpoints": {
            "default": "https://auth-server-default.com",
            "emea": "https://auth-server-emea.com",
            "apac": "https://auth-server-apac.com",
            "amer": "https://auth-server-amer.com",
        }
    }
)

Encryption key/secret

The secret_key_accessor variable is used to pass a key or secret used for data encryption.

Note: even though SDK uses PBKDF2 to generate a cryptographically strong encryption key, you must ensure that you provide a secret/password which follows the modern security best practices and standards.

The SecretKeyAccessor class constructor allows you to pass a function that should return either a string representing your secret or a dictionary (we call it secrets_data object):

{
  "secrets": [{
       "secret": str,
       "version": int, # Should be an integer greater than or equal to 0
       "isKey": bool,  # Should be True only for user-defined encryption keys
    }
  }, ....],
  "currentVersion": int,
}

The secrets_data variable allows you to specify multiple keys/secrets which the SDK will use for data decryption based on the version of the key or secret used for encryption. Meanwhile SDK will encrypt data by using only the key/secret pair which matches the currentVersion parameter provided in the secrets_data object.

This enables the flexibility required to support Key Rotation policies when secret/key must change with time. The SDK will encrypt data by using the current secret/key while maintaining the ability to decrypt data records that were encrypted with old key/secret. The SDK also provides a method for data migration which allows you to re-encrypt data with the newest key/secret. For details please see the migrate method.

The SDK allows you to use custom encryption keys, instead of secrets. Please note that a user-defined encryption key should be a 32-character 'utf8' encoded string as required by AES-256 cryptographic algorithm.

Below you can find several examples of how you can use SecretKeyAccessor module.

# Get a secret from a variable
from incountry import SecretKeyAccessor

password = "password"
secret_key_accessor = SecretKeyAccessor(lambda: password)

# Get secrets via an HTTP request
from incountry import SecretKeyAccessor
import requests as req

def get_secrets_data():
    url = "<your_secret_url>"
    r = req.get(url)
    return r.json() # assuming response is a `secrets_data` object

secret_key_accessor = SecretKeyAccessor(get_secrets_data)
Node.js SDK

To access your data in InCountry Platform by using Node.js SDK, you need to create an instance of the Storage class using the createStorage async factory method.

type StorageOptions = {
  apiKey?: string;          // Required when using API key authorization, or as the INC_API_KEY environment variable 
  environmentId?: string;   // Required to be passed in, or as the INC_ENVIRONMENT_ID environment variable 

  oauth?: {
    clientId?: string;      // Required when using oAuth authorization, can be also set through the INC_CLIENT_ID environment variable
    clientSecret?: string;  // Required when using oAuth authorization, can be also set through INC_CLIENT_SECRET environment variable
    authEndpoints?: {       // Custom endpoints regional map to use for fetching oAuth tokens
      default: string;
      [key: string]: string;
    };
  };

  endpoint?: string;        // Defines API URL
  encrypt?: boolean;        // If false, encryption is not used. Defaults to true.

  logger?: Logger;
  getSecrets?: Function;    // Used to fetch an encryption secret
  normalizeKeys?: boolean;
  countriesCache?: CountriesCache;
  hashSearchKeys?: boolean; // Set to false to enable partial match search among record's text fields `key1, key2, ..., key10`. Defaults to true.

  /**
   * Defines API base hostname part to use.
   * If set, all requests will be sent to https://${country}${endpointMask} host instead of the default
   * one (https://${country}-mt-01.api.incountry.io)
   */
  endpointMask?: string;

  /**
   * If your PoPAPI configuration relies on a custom PoPAPI server (rather than the default one)
   * use the `countriesEndpoint` option to specify the endpoint responsible for fetching the list of supported countries.
   */
  countriesEndpoint?: string;

  httpOptions?: {
    timeout?: NonNegativeInt; // Timeout in milliseconds.
  };
};

async function createStorage(
  options: StorageOptions,
  customEncryptionConfigs?: CustomEncryptionConfig[]
): Promise<Storage> {
  /* ... */
}

const { createStorage } = require('incountry');
const storage = await createStorage({
  apiKey: 'API_KEY',
  environmentId: 'ENVIRONMENT_ID',
  oauth: {
    clientId: '',
    clientSecret: '',
    authEndpoints: {
      default: 'https://auth',
    },
  },
  endpoint: 'INC_URL',
  encrypt: true,
  getSecrets: () => '',
  endpointMask: '',
  countriesEndpoint: '',
  httpOptions: {
    timeout: 5000,
  },
});

WARNING

API Key authorization is being deprecated. The backward compatibility is preserved for the apiKey parameter but you no longer can access API keys (neither old nor new) from your dashboard.


The oauth.clientId, oauth.clientSecret and environmentId variables can be fetched from your dashboard on InCountry Portal.

Otherwise you can create an instance of the Storage class and run all asynchronous checks by yourself (or skip their running at your own risk!).

const { Storage } = require('incountry');
const storage = new Storage({
  apiKey: 'API_KEY',
  environmentId: 'ENVIRONMENT_ID',
  endpoint: 'INC_URL',
  encrypt: true,
  getSecrets: () => '',
});

await storage.validate();

The validate method fetches the secret using GetSecretsCallback and validates it. If custom encryption configurations were provided they would also be checked with all the matching secrets.

oAuth Authentication

The SDK also supports oAuth authentication credentials instead of plain API key authorization. oAuth authentication flow is mutually exclusive with API key authentication - you will need to provide either API key or oAuth credentials.

Below you can find the example of how to create a storage instance with oAuth credentials (and also provide a custom oAuth endpoint):

const { Storage } = require('incountry');
const storage = new Storage({
  environmentId: 'ENVIRONMENT_ID',
  endpoint: 'INC_URL',
  encrypt: true,
  getSecrets: () => '',
  oauth: {
    clientId: 'CLIENT_ID',
    clientSecret: 'CLIENT_SECRET',
    authEndpoints: {
      "default": "https://auth-server-default.com",
      "emea": "https://auth-server-emea.com",
      "apac": "https://auth-server-apac.com",
      "amer": "https://auth-server-amer.com",
    },
  },
});

Encryption key/secret

The GetSecretsCallback function is used to pass a key or secret used for encryption.

Note: even though SDK uses PBKDF2 to generate a cryptographically strong encryption key, you must ensure that you provide a secret/password which follows the modern security best practices and standards.

The GetSecretsCallback function returns either a string representing your secret or an object (we call it SecretsData) or a Promise which is resolved to that string or object:

type SecretOrKey = {
  secret: string;
  version: NonNegativeInt;
  isKey?: boolean;
  isForCustomEncryption?: boolean;
};

type SecretsData = {
  currentVersion: NonNegativeInt;
  secrets: Array<SecretOrKey>;
};

/// SecretsData example
{
  secrets: [
    {
      secret: 'aaa',
      version: 0
    },
    {
      secret: 'base64...IHN0cmluZw==', // Should be a base64-encoded key (32 byte key)
      version: 1,
      isKey: true
    },
    {
      secret: 'ccc',
      version: 2,
      isForCustomEncryption: true
    }
  ],
  currentVersion: 1
};

The GetSecretsCallback function allows you to specify multiple keys/secrets which the SDK will use for decryption based on the version of the key or secret used for encryption. Meanwhile SDK will encrypt data only by using a key/secret pair which matches the currentVersion parameter provided in the SecretsData object.

This enables the flexibility required to support Key Rotation policies when secret/key pairs must be changed with time. The SDK will encrypt data by using the current secret/key pair while maintaining the ability to decrypt data records that were encrypted with old key/secret pairs. The SDK also provides a method for data migration which allows to re-encrypt data with the newest key/secret. For details please see the migrate method.

The SDK allows you to use custom encryption keys, instead of secrets. Please note that a user-defined encryption key should be a 32-character 'utf8' encoded string as required by AES-256 cryptographic algorithm.

Below you can find several examples of how you can use the GetSecretsCallback function.

type GetSecretsCallback = () => string | SecretsData | Promise<string> | Promise<SecretsData>;

// Synchronous
const getSecretsSync = () => 'longAndStrongPassword';

// Asynchronous
const getSecretsAsync = async () => {
  const secretsData = await getSecretsDataFromSomewhere();
  return secretsData;
};

// Using promises syntax
const getSecretsPromise = () =>
  new Promise(resolve => {
    getPasswordFromSomewhere(secretsData => {
      resolve(secretsData);
    });
  });
Java SDK

Use the StorageImpl class to access your data in InCountry Platofrm using Java SDK.

public class StorageImpl implements Storage {
  /**
   * creating Storage instance
   *
   * @param environmentID     Required to be passed in, or as the INC_API_KEY environment variable
                              with {@link #getInstance()}
   * @param apiKey            Required to be passed in, or as the INC_ENVIRONMENT_ID environment variable
                              with {@link #getInstance()}
   * @param endpoint          Optional. Defines API URL.
   *                          Default endpoint will be used if this parameter is null
   * @param secretKeyAccessor Instance of SecretKeyAccessor class. Used to fetch encryption secret
   * @return instance of Storage
   * @throws StorageClientException if configuration validation finishes with errors
   * @throws StorageServerException if server connection fails or server response error is returned
   */
  public static Storage getInstance(String environmentID, String apiKey, String endpoint,
                            SecretKeyAccessor secretKeyAccessor) throws StorageServerException {...}
//...
}

The environmentID and apiKey parameters (or clientId and clientSecret instead of apiKey) can be fetched from your dashboard on the Incountry Portal.

You can disable encryption (not recommended) by passing the null value to the secretKeyAccessor parameter.

Below you can find the example of how to create a new storage instance:

SecretKeyAccessor secretKeyAccessor = () -> SecretsDataGenerator.fromPassword("<password>");
String endPoint = "https://us-mt-01.api.incountry.io";
String envId = "<env_id>";
String apiKey = "<api_key>";
Storage storage = StorageImpl.getInstance(envId, apiKey, endPoint, secretKeyAccessor);

oAuth Authentication

SDK also supports oAuth authentication credentials instead of plain API key authorization. oAuth authentication flow is mutually exclusive with API key authentication - you will need to provide either API key or oAuth credentials.

Below you can find the example of how to create a new storage instance with oAuth credentials (and also provide a custom oAuth endpoint):

Map<String, String> authEndpointsMap = new HashMap<>();
authEndpointsMap.put("emea", "https://auth-server-emea.com");
authEndpointsMap.put("apac", "https://auth-server-apac.com");
authEndpointsMap.put("amer", "https://auth-server-amer.com");

StorageConfig config = new StorageConfig()
   //can be also set via environment variable INC_CLIENT_ID with {@link #getInstance()}
   .setClientId(CLIENT_ID)
   //can be also set via environment variable INC_CLIENT_SECRET with {@link #getInstance()}
   .setClientSecret(SECRET)
   .setAuthEndpoints(authEndpointsMap)
   .setDefaultAuthEndpoint("https://auth-server-default.com")
   .setEndpointMask(ENDPOINT_MASK)
   .setEnvId(ENV_ID);
Storage storage = StorageImpl.getInstance(config);

Note: the endpointMask parameter is used for switching from the default InCountry host list (api.incountry.io) to a different one. For example the endpointMask==-private.incountry.io setting will further redirect requests to https://{COUNTRY_CODE}-private.incountry.io If your PoPAPI configuration relies on a custom PoPAPI server (rather than the default one) use the countriesEndpoint option to specify the endpoint responsible for fetching the list of supported countries.

StorageConfig config = new StorageConfig()
   .setCountriesEndpoint(countriesEndpoint)
   //...
Storage storage = StorageImpl.getInstance(config);

Encryption key/secret

SDK provides the SecretKeyAccessor interface which allows you to pass your own secrets/keys to the SDK.

/**
 * Secrets accessor. Method {@link SecretKeyAccessor#getSecretsData()} is invoked on each encryption/decryption.
 */
public interface SecretKeyAccessor {

    /**
     * get your container with secrets
     *
     * @return SecretsData
     * @throws StorageClientException when something goes wrong during getting secrets
     */
    SecretsData getSecretsData() throws StorageClientException;
}


public class SecretsData {
    /**
     * creates a container with secrets
     *
     * @param secrets non-empty list of secrets. One of the secrets must have
     *        same version as currentVersion in SecretsData
     * @param currentVersion Should be a non-negative integer
     * @throws StorageClientException when parameter validation fails
     */
     public SecretsData(List<SecretKey> secrets, int currentVersion)
                throws StorageClientException {...}
    //...
}


public class SecretKey {
    /**
    * Creates a secret key
    *
    * @param secret  secret/key
    * @param version secret version, should be a non-negative integer
    * @param isKey   should be True only for user-defined encryption keys
    * @throws StorageClientException when parameter validation fails
    */
    public SecretKey(String secret, int version, boolean isKey)
              throws StorageClientException {...}
    //...
}

You can implement SecretKeyAccessor interface and pass secrets/keys in multiple ways:

  1. As a constant SecretsData object:

    SecretsData secretsData = new SecretsData(secretsList, currentVersion);
    SecretKeyAccessor accessor = () -> secretsData;
  2. As a function that dynamically fetches secrets:

    SecretKeyAccessor accessor = () -> loadSecretsData();
    
    private SecretsData loadSecretsData()  {
       String url = "<your_secret_url>";
       String responseJson = loadFromUrl(url).asJson();
       return SecretsDataGenerator.fromJson(responseJson);
    }

You can also use the SecretsDataGenerator class for creating SecretsData instances:

  1. from a String password:

    SecretsData secretsData = SecretsDataGenerator.fromPassword("<password>");
  2. from a JSON string representing SecretsData object:

    SecretsData secretsData = SecretsDataGenerator.fromJson(jsonString);
    {
    "secrets": [
        {
        "secret": "secret0",
        "version": 0,
        "isKey": false
        },
        {
        "secret": "secret1",
        "version": 1,
        "isKey": false
        }
    ],
    "currentVersion": 1
    }

The SecretsData object allows you to specify multiple keys/secrets which SDK will use for data decryption based on the version of the key or secret used for data encryption.

Writing data to storage

Writing data to storage

.

  • Python SDK
  • Node.js SDK
  • Java SDK
  • REST API
Python SDK

Use the write method to create/replace a record (by record_key).

def write(self, country: str, record_key: str, **record_data: Union[str, int]) -> Dict[str, TRecord]:
    ...


# write returns created record dict on success
{
    "record": Dict
}

Below you can find the example of how you can use the write method.

write_result = storage.write(
    country="us",
    record_key="user_1",
    body="some PII data",
    profile_key="customer",
    range_key1=10000,
    key1="english",
    key2="rolls-royce",
)

# write_result would be as follows
write_result = {
    "record": {
        "record_key": "user_1",
        "body": "some PII data",
        "profile_key": "customer",
        "range_key1": 10000,
        "key1": "english",
        "key2": "rolls-royce",
    }
}

For the list of possible record_data kwargs, see the section below.

List of available record fields

v3.0.0 release introduced a series of new fields available for data storage. Below you can find the full list of all the fields available for storage in InCountry Platform along with their types and storage methods. Each field is either encrypted, hashed or stored as follows:

# String fields, hashed
record_key
key1
key2
key3
key4
key5
key6
key7
key8
key9
key10
profile_key
service_key1
service_key2

# String fields, encrypted
body
precommit_body

# Int fields, plain
range_key1
range_key2
range_key3
range_key4
range_key5
range_key6
range_key7
range_key8
range_key9
range_key10

Batches

Use the batch_write method to create/replace multiple records at once.

def batch_write(self, country: str, records: List[TRecord]) -> Dict[str, List[TRecord]]:
    ...


# batch_write returns the following dict of created records
{
    "records": List
}

Below you can find the example of how to use this method.

batch_result = storage.batch_write(
    country="us",
    records=[
        {"record_key": "key1", "body": "body1", ...},
        {"record_key": "key2", "body": "body2", ...},
    ],
)

# batch_result would be as follows
batch_result = {
    "records": [
        {"record_key": "key1", "body": "body1", ...},
        {"record_key": "key2", "body": "body2", ...},
    ]
}
Node.js SDK

Use the write method create/replace a record (by recordKey) .

List of available record fields

v3.0.0 release introduced a series of new fields available for data storage. Below you can find the full list of all the fields available for storage in InCountry Platform along with their types and storage methods. Each field is either encrypted, hashed or stored as follows:

String fields, hashed:
recordKey
profileKey
serviceKey1
serviceKey2
String fields, hashed if Storage options "hashSearchKeys" is set to true (by default it is):

WARNING If the hashSearchKeys option is set to false the following string fields will have length limitation of 256 characters at most.

key1
key2
key3
key4
key5
key6
key7
key8
key9
key10
String fields, encrypted:
body
precommitBody
Int fields, plain:
rangeKey1
rangeKey2
rangeKey3
rangeKey4
rangeKey5
rangeKey6
rangeKey7
rangeKey8
rangeKey9
rangeKey10
type StorageRecordData = {
  recordKey: string;
  profileKey?: string | null;
  key1?: string | null;  // If `hashSearchKeys` is set to `false` key1 has length limit 256
  key2?: string | null;  // If `hashSearchKeys` is set to `false` key2 has length limit 256
  key3?: string | null;  // If `hashSearchKeys` is set to `false` key3 has length limit 256
  key4?: string | null;  // If `hashSearchKeys` is set to `false` key4 has length limit 256
  key5?: string | null;  // If `hashSearchKeys` is set to `false` key5 has length limit 256
  key6?: string | null;  // If `hashSearchKeys` is set to `false` key6 has length limit 256
  key7?: string | null;  // If `hashSearchKeys` is set to `false` key7 has length limit 256
  key8?: string | null;  // If `hashSearchKeys` is set to `false` key8 has length limit 256
  key9?: string | null;  // If `hashSearchKeys` is set to `false` key9 has length limit 256
  key10?: string | null; // If `hashSearchKeys` is set to `false` key10 has length limit 256
  serviceKey1?: string | null;
  serviceKey2?: string | null;
  body?: string | null;
  precommitBody?: string | null;
  rangeKey1?: t.Int | null;
  rangeKey2?: t.Int | null;
  rangeKey3?: t.Int | null;
  rangeKey4?: t.Int | null;
  rangeKey5?: t.Int | null;
  rangeKey6?: t.Int | null;
  rangeKey7?: t.Int | null;
  rangeKey8?: t.Int | null;
  rangeKey9?: t.Int | null;
  rangeKey10?: t.Int | null;
};

type WriteResult = {
  record: StorageRecordData;
};

async write(
  countryCode: string,
  recordData: StorageRecordData,
  requestOptions: RequestOptions = {},
): Promise<WriteResult> {
  /* ... */
}

Below you can find the example of how you can use the write method.

const recordData = {
  recordKey: '<key>',
  body: '<body>',
  profileKey: '<profile_key>',
  rangeKey1: 0,
  key2: '<key2>',
  key3: '<key3>'
}

const writeResult = await storage.write(countryCode, recordData);

Batches

You can use the batchWrite method to create/replace multiple records at once.

type BatchWriteResult = {
  records: Array<StorageRecordData>;
};

async batchWrite(
  countryCode: string,
  records: Array<StorageRecordData>,
  requestOptions: RequestOptions = {},
): Promise<BatchWriteResult> {
  /* ... */
}

Example of usage:

batchResult = await storage.batchWrite(countryCode, recordDataArr);
Java SDK

Use the write method to create a new record.

public interface Storage {
    /**
     * Write data to a remote storage
     *
     * @param country country identifier
     * @param record  object which encapsulates data which must is written to the storage
     * @return recorded record
     * @throws StorageClientException if validation finishes with errors
     * @throws StorageServerException if server connection fails or a server response error occurs
     * @throws StorageCryptoException if encryption fails
     */
    Record write(String country, Record record)
          throws StorageClientException, StorageServerException, StorageCryptoException;
    //...
}

Below you can find the example of how to initialize a record object:

public class Record {
    /**
     * Full constructor
     *
     * @param key        Required, record key
     * @param body       Optional, data to be stored and encrypted
     * @param profileKey Optional, profile key
     * @param rangeKey   Optional, range key
     * @param key2       Optional, key2
     * @param key3       Optional, key3
     */
    public Record(String key, String body, String profileKey, Long rangeKey, String key2, String key3)
    //...
}

Below you can find the example of how to use the write method:

key = "user_1";
body = "some PII data";
profileKey = "customer";
rangeKey = 10000l;
key2 = "english";
key3 = "insurance";
Record record = new Record(key, body, profileKey, rangeKey, key2, key3);
storage.write("us", record);

Encryption

InCountry Platform uses the client-side encryption for your data. Note that only body is encrypted. Some other fields are hashed. Below you can find the example of how data is transformed and stored in InCountry Platform:

public class Record {
    private String key;          // hashed
    private String body;         // encrypted
    private String profileKey;   // hashed
    private Long rangeKey;       // plain
    private String key2;         // hashed
    private String key3;         // hashed
    //...
}

Batches

Use the batchWrite method to write multiple records to the storage within a single request.

public interface Storage {
     /**
      * Write multiple records at once in remote storage
      *
      * @param country country identifier
      * @param records record list
      * @return BatchRecord object which contains a list of recorded records
      * @throws StorageClientException if validation finishes with errors
      * @throws StorageServerException if server connection fails or a server response error occurs
      * @throws StorageCryptoException if record encryption fails
      */
     BatchRecord batchWrite(String country, List<Record> records)
          throws StorageClientException, StorageServerException, StorageCryptoException;
     //...
}

Below you can find the example of how you can use the batchWrite method:

List<Record> list = new ArrayList<>();
list.add(new Record(firstKey, firstBody, firstProfileKey, firstRangeKey, firstKey2, firstKey3));
list.add(new Record(secondKey, secondBody, secondProfileKey, secondRangeKey, secondKey2, secondKey3));
storage.batchWrite("us", list);
REST API

Create a record

POST /api/records

:::note Alternatively, this request can be used to update the record. Be careful, as it rewrites the data record with new values you provide. :::

Headers:

HeaderValue
Content-Typeapplication/json
Content-LengthCalculated automatically when a request is sent
HostCalculated automatically when a request is sent
User-AgentSpecify the user agent used
Accept/
Accept-Encodinggzip, deflate, br
Connectionkeep-alive

Request Body:

ParametersTypeDescription
record_keystringIt is used for storing the primary key of the record (identifier). The value from this field is also propagated to the key field when REST API creates a new record.
keyNstringCan be used for storing regulated and non-regulated data. Your record can have up to 10 keys.
profile_keystringAdditional data value
service_key1 service_key2stringService data value.
range_key1integerNumerical value. Your record can have up to 10 range keys.
bodystringAny text string that you want to store.
precommit_bodystringAny text string that you want to store. It can be used for storing the intermediate value of the body field.
countryISO country code (lowercase)Country which a new record is written to

Example:

{
  "record_key": "a98787ss87",
  "profile_key": "pk1237",
  "key1": "a1237",
  "key2": "John",
  "key3": "Doe",
  ...
  "key10": "Visa",
  "service_key1": "478",
  "service_key2": "57",
  "body": "This is a contact from Tradeshow",
  "precommit_body": "Consent received on 12/1/2020",
  "range_key1": 42,
  "range_key2": 50,
  ...
  "range_key10": 980,
  "country": "sg"
}

Responses:

STATUS 201 - application/json Returns information and a record key for the newly created record.

Example:

{
    "key": "a98787ss87",
    "profile_key": "pk1237",
    "range_key": 42,
    "record_key": "a98787ss87",
    "body": "This is a contact from Tradeshow",
    "precommit_body": "Consent received on 12/1/2020",
    "key1": "a1237",
    "key2": "John",
    "key3": "Doe",
    ...
    "key10": "Visa",
    "service_key1": "478",
    "service_key2": "57",
    "range_key1": 42,
    "range_key2": 50,
    ...
    "range_key10": 980,
    "country": "sg"
}

STATUS 400 - application/json Bad request when it has not passed validation. The error details will be in the response.

STATUS 401 - This error status is returned when the request is unauthorized.

Reading stored data

Reading stored data

.

  • Python SDK
  • Node.js SDK
  • Java SDK
  • REST API
Python SDK

You can read a stored record by its record_key by using the read method. It accepts an object with two fields - country and record_key.

def read(self, country: str, record_key: str) -> Dict[str, TRecord]:
    ...


# The read method returns the record dictionary if the record is found
{
    "record": Dict
}

Date fields

You can use the created_at and updated_at fields to access date-related information about records. The created_at field stores a date when a record was initially created in the target country. The updated_at field stores a date of the latest write operation for the given recordKey.

You can use the read method, as follows:

read_result = storage.read(country="us", record_key="user1")

# read_result would be as follows
read_result = {
    "record": {
        "record_key": "user_1",
        "body": "some PII data",
        "profile_key": "customer",
        "range_key1": 10000,
        "key1": "english",
        "key2": "rolls-royce",
        "created_at": datetime.datetime(...),
        "updated_at": datetime.datetime(...),
    }
}
Node.js SDK

You can read the stored data records by its recordKey by using the read method. It accepts an object with the two fields - country and recordKey. It returns a Promise which is resolved to { record } or is rejected if there are no records for the passed recordKey.

Date fields

You can use the createdAt and updatedAt fields to access date-related information about records. The createdAt field stores the date when the record was initially created in the target country. The updatedAt field stores the date of the latest write operation for the given recordKey.

type StorageRecord = {
  recordKey: string;
  body: string | null;
  profileKey: string | null;
  precommitBody: string | null;
  key1?: string | null;  // If `hashSearchKeys` is set to `false` key1 has length limit 256
  key2?: string | null;  // If `hashSearchKeys` is set to `false` key2 has length limit 256
  key3?: string | null;  // If `hashSearchKeys` is set to `false` key3 has length limit 256
  key4?: string | null;  // If `hashSearchKeys` is set to `false` key4 has length limit 256
  key5?: string | null;  // If `hashSearchKeys` is set to `false` key5 has length limit 256
  key6?: string | null;  // If `hashSearchKeys` is set to `false` key6 has length limit 256
  key7?: string | null;  // If `hashSearchKeys` is set to `false` key7 has length limit 256
  key8?: string | null;  // If `hashSearchKeys` is set to `false` key8 has length limit 256
  key9?: string | null;  // If `hashSearchKeys` is set to `false` key9 has length limit 256
  key10?: string | null; // If `hashSearchKeys` is set to `false` key10 has length limit 256
  serviceKey1: string | null;
  serviceKey2: string | null;
  rangeKey1: t.Int | null;
  rangeKey2: t.Int | null;
  rangeKey3: t.Int | null;
  rangeKey4: t.Int | null;
  rangeKey5: t.Int | null;
  rangeKey6: t.Int | null;
  rangeKey7: t.Int | null;
  rangeKey8: t.Int | null;
  rangeKey9: t.Int | null;
  rangeKey10: t.Int | null;
  createdAt: Date;
  updatedAt: Date;
}

type ReadResult = {
  record: StorageRecord;
};

async read(
  countryCode: string,
  recordKey: string,
  requestOptions: RequestOptions = {},
): Promise<ReadResult> {
  /* ... */
}

Example of usage:

const readResult = await storage.read(countryCode, recordKey);
Java SDK

You can read the stored records by key by using the read method.

public interface Storage {
   /**
    * Read data from remote storage
    *
    * @param country   country identifier
    * @param key record unique identifier
    * @return Record object which contains required data
    * @throws StorageClientException if validation finishes with errors
    * @throws StorageServerException if server connection fails or a server response error occurs
    * @throws StorageCryptoException if decryption fails
    */
    Record read(String country, String key)
        throws StorageClientException, StorageServerException, StorageCryptoException;
    //...
}

Below you can find the example of how you may use read method:

String key = "user_1";
Record record = storage.read("us", key);
String decryptedBody = record.getBody();

The Record object includes the following properties: key, body, key2, key3, profileKey, rangeKey.

These properties can be accessed using getters, for example:

String key2 = record.getKey2();
String body = record.getBody();
REST API

Find a record

POST /api/records/find

Headers:

HeaderValue
Content-Typeapplication/json
Content-LengthCalculated automatically when a request is sent
HostCalculated automatically when a request is sent
User-AgentSpecify the user agent used
Accept/
Accept-Encodinggzip, deflate, br
Connectionkeep-alive

Request Body:

ParametersTypeExampleDescription
countryISO country code (lowercase)sg (equals to Singapore)Country which a new record is looked up in. You can use different countries.
filterobjectSee the example.Specify the criteria for filtration within the filter parameter. You can look up for records by any of record fields. For the full list of supported fields, please see the Available record fields section.
keyNstringSee the example.Value from the key fields of your record.
search_keysstringabcDefines the search query string for lookup. The search keys should be provided within the filter object.
range_keyNarray[1, 5]Defines the range of keys for lookup. The range keys should be provided within the filter object.
optionsobjectSee the example.Specify the criteria for returning the search results:limit, offset
limitinteger10Sets the maximal number of search results.
offsetinteger50Specifies the offset (pagination) of records from which the record lookup is performed.

Example:

{
  "country": "sg",
  "filter": {
    "key1": ["k1234", "k1235"],
    "key2": ["John", "Jane"]
  },
  "options": {
    "limit": 10,
    "offset": 50
  }
}

or

{
  "country": "sg",
  "filter": {
    "search_keys": "abc"
  }
}

or

{
  "country": "sg",
  "filter": {
    "search_keys": "abc",
    "range_key1": [1, 2]
  }
}

Responses:

STATUS 200 - application/json Returns information about the found record or records.

This status is also returned when no records matching the search criteria are found.

Example:

{
    "data": [
        {
            "key": "k1235",
            "profile_key": "pk1235",
            "range_key": 42,
            "record_key": "k1235",
            "body": null,
            "precommit_body": null,
            "key1": null,
            "key2": "Jane",
            "key3": "Doe",
            "key4": null,
            "key5": null,
            "key6": null,
            "key7": null,
            "key8": null,
            "key9": null,
            "key10": null,
            "service_key1": null,
            "service_key2": null,
            "range_key1": 42,
            "range_key2": null,
            "range_key3": null,
            "range_key4": null,
            "range_key5": null,
            "range_key6": null,
            "range_key7": null,
            "range_key8": null,
            "range_key9": null,
            "range_key10": null,
            "version": 0,
            "created_at": "2020-11-21T12:07:43.000Z",
            "updated_at": "2020-11-21T12:07:43.000Z"
        },
        {
            "key": "k1234",
            "profile_key": "pk1234",
            "range_key": 42,
            "record_key": "k1234",
            "body": null,
            "precommit_body": null,
            "key1": null,
            "key2": "John",
            "key3": "Doe",
            "key4": null,
            "key5": null,
            "key6": null,
            "key7": null,
            "key8": null,
            "key9": null,
            "key10": null,
            "service_key1": null,
            "service_key2": null,
            "range_key1": 42,
            "range_key2": null,
            "range_key3": null,
            "range_key4": null,
            "range_key5": null,
            "range_key6": null,
            "range_key7": null,
            "range_key8": null,
            "range_key9": null,
            "range_key10": null,
            "version": 0,
            "created_at": "2020-11-21T11:55:49.000Z",
            "updated_at": "2020-11-21T12:05:28.000Z"
        }
    ],
    "meta": {
        "count": 2,
        "limit": 100,
        "offset": 0,
        "total": 2
    }
}

STATUS 400 - application/json Bad request when it has not passed validation. The error details will be in the response.

STATUS 401 - This error is returned when the request is unauthorized.

Looking up for records

Looking up for records

.

  • Python SDK
  • Node.js SDK
  • Java SDK
  • REST API
Python SDK

You can look up for records by keys or version using the find method.

def find(
        self,
        country: str,
        limit: Optional[int] = FIND_LIMIT,
        offset: Optional[int] = 0,
        **filters: Union[TIntFilter, TStringFilter],
    ) -> Dict[str, Any]:
    ...

Note: SDK returns 100 records at most.

The returned object looks like the following:

{
    "data": List,
    "errors": List, # optional
    "meta": {
        "limit": int,
        "offset": int,
        "total": int,  # total records matching filter, ignoring limit
    }
}

You can use the following options to look up for records by hashed string keys from the list above:

# single value
key1="value1" # records with key1 equal to "value1"

# list of values
key2=["value1", "value2"] # records with key2 equal to "value1" or "value2"

# dict with $not operator
key3={"$not": "value1"} # records with key3 not equal "value1"
key4={"$not": ["value1", "value2"]} # records with key4 equal to neither "value1" or "value2"

You can use the following options to look up for records by int keys from the list above:

# single value
range_key1=1 # records with range_key1 equal to 1

# list of values
range_key2=[1, 2] # records with range_key2 equal to 1 or 2

# dictionary with comparison operators
range_key3={"$gt": 1} # records with range_key3 greater than 1
range_key4={"$gte": 1} # records with range_key4 greater than or equal to 1
range_key5={"$lt": 1} # records with range_key5 less than 1
range_key6={"$lte": 1} # records with range_key6 less than or equal to 1

# you can combine different comparison operators
range_key7={"$gt": 1, "$lte": 10} # records with range_key7 greater than 1 and less than or equal to 10

# you cannot combine similar comparison operators - e.g. $gt and $gte, $lt and $lte

You can use the following option to look up for records by version (encryption key version):

# single value
version=1 # records with version equal to 1

# list of values
version=[1, 2] # records with version equal to 1 or 2

# dictionary with $not operator
version={"$not": 1} # records with version not equal 1
version={"$not": [1, 2]} # records with version equal to neither 1 or 2

Below you can find the example of how you can use the find method:

find_result = storage.find(country="us", limit=10, offset=10, key1="value1", key2=["value2", "value3"])

# find_result would be as follows
find_result = {
    "data": [
        {
            "record_key": "<record_key>",
            "body": "<body>",
            "key1": "value1",
            "key2": "value2",
            "created_at": datetime.datetime(...),
            "updated_at": datetime.datetime(...),
            ...
        }
    ],
    "meta": {
        "limit": 10,
        "offset": 10,
        "total": 100,
    }
}

Find one record matching a filter

If you need to find only one of the records matching a specific filter, you can use the find_one method.

def find_one(
        self, country: str, offset: Optional[int] = 0, **filters: Union[TIntFilter, TStringFilter],
    ) -> Union[None, Dict[str, Dict]]:
    ...


# If a record is not found, the find_one method returns `None`. Otherwise it returns a record dictionary.
{
    "record": Dict
}

Below you can find the example of how to use the find_one method:

find_one_result = storage.find_one(country="us", key1="english", key2=["rolls-royce", "bmw"])

# find_one_result would be as follows
find_one_result = {
    "record": {
        "record_key": "user_1",
        "body": "some PII data",
        "profile_key": "customer",
        "range_key1": 10000,
        "key1": "english",
        "key2": "rolls-royce",
    }
}
Node.js SDK

You can look up for data records either by using exact match search operators or partial text match operators in almost any combinations.

Exact match search

The following exact match search criteria are available:

  • single value:
// Matches all records where record.key1 = 'abc' AND record.rangeKey = 1
{ key1: 'abc', rangeKey1: 1 }
  • multiple values as an array:
// Matches all records where (record.key2 = 'def' OR record.key2 = 'jkl') AND (record.rangeKey = 1 OR record.rangeKey = 2)
{ key2: ['def', 'jkl'], rangeKey1: [1, 2] }
// Matches all records where record.key3 <> 'abc'
{ key3: { $not: 'abc' } }

// Matches all records where record.key3 <> 'abc' AND record.key3 <> 'def'
{ key3: { $not: ['abc', 'def'] } }

// Matches all records where record.version <> 1
{ version: { $not: 1 }}
// Matches all records where record.rangeKey >= 5 AND record.rangeKey <= 100
{ rangeKey1: { $gte: 5, $lte: 100 } }
Partial text match search

You can also look up for data records by partial match using the searchKeys operator which performs partial match search (similar to the LIKE SQL operator) within records text fields key1, key2, ..., key10.

// Matches all records where record.key1 LIKE 'abc' OR record.key2 LIKE 'abc' OR ... OR record.key10 LIKE 'abc'
{ searchKeys: 'abc' }

Please note: The searchKeys operator cannot be used in combination with any of key1, key2, ..., key10 keys and works only in combination with the non-hashing Storage mode (hashSearchKeys param for Storage).

// Matches all records where (record.key1 LIKE 'abc' OR record.key2 LIKE 'abc' OR ... OR record.key10 LIKE 'abc') AND (record.rangeKey = 1 OR record.rangeKey = 2)
{ searchKeys: 'abc', rangeKey1: [1, 2] }

// Causes validation error (StorageClientError)
{ searchKeys: 'abc', key1: 'def' }

Search options

The options parameter defines the limit - number of records that are returned, and the offset- the starting index used for record pagination. You can use these parameters to implement pagination. Note: The SDK returns 100 records at most.

type FilterStringValue = string | string[];
type FilterStringQuery = FilterStringValue | { $not?: FilterStringValue };

type FilterNumberValue = number | number[];
type FilterNumberQuery =
  FilterNumberValue |
  {
    $not?: FilterNumberValue;
    $gt?: number;
    $gte?: number;
    $lt?: number;
    $lte?: number;
  };

type FindFilter = Record<string, FilterStringQuery | FilterNumberQuery>;

type FindOptions = {
  limit?: number;
  offset?: number;
};

type FindResult = {
  meta: {
    total: number;
    count: number;
    limit: number;
    offset: number;
  };
  records: Array<StorageRecord>;
  errors?: Array<{ error: StorageCryptoError; rawData: ApiRecord }>;
};

async find(
  countryCode: string,
  filter: FindFilter = {},
  options: FindOptions = {},
  requestOptions: RequestOptions = {},
): Promise<FindResult> {
  /* ... */
}

Example of usage

const filter = {
  key1: 'abc',
  key2: ['def', 'jkl'],
  profileKey: 'test2',
  rangeKey1: { $gte: 5, $lte: 100 },
  rangeKey2: { $not: [0, 1] },
}

const options = {
  limit: 100,
  offset: 0,
};

const findResult = await storage.find(countryCode, filter, options);

The returned findResult object looks like the following:

{
  records: [{/* StorageRecord */}],
  errors: [],
  meta: {
    limit: 100,
    offset: 0,
    total: 24
  }
}

Find one record matching a filter

If you need to find only one of the records matching a specific filter, you can use the find_one method. If a record is not found, it returns null.

type FindOneResult = {
  record: StorageRecord | null;
};

async findOne(
  countryCode: string,
  filter: FindFilter = {},
  options: FindOptions = {},
  requestOptions: RequestOptions = {},
): Promise<FindOneResult> {
  /* ... */
}

Example of usage:

const findOneResult = await storage.findOne(countryCode, filter);
Java SDK

You can search for data records by random keys using the find method.

public interface Storage {
   /**
    * Find records in remote storage according to filters
    *
    * @param country country identifier
    * @param builder object representing find filters and search options
    * @return BatchRecord object which contains required records
    * @throws StorageClientException if validation finishes with errors
    * @throws StorageServerException if server connection fails or a server response error occurs
    * @throws StorageCryptoException if decryption fails
    */
    BatchRecord find(String country, FindFilterBuilder builder)
         throws StorageClientException, StorageServerException, StorageCryptoException;
    //...
}

You can use the FindFilterBuilder class to refine your find request.

Below you can find the example of how to use the find method along with the FindFilterBuilder class:

FindFilterBuilder builder = FindFilterBuilder.create()
                  .key2Eq("someKey")
                  .key3Eq("firstValue","secondValue")
                  .rangeKeyBetween(123l, 456l);

BatchRecord findResult = storage.find("us", builder);
if (findResult.getCount() > 0) {
    Record record = findResult.getRecords().get(0);
    //...
}

The request will return records that are filtered according to the following pseudo-sql:

key2 = 'someKey' AND key3 in ('firstValue' , 'secondValue') AND (123 < = `rangeKey` < = 456)

All conditions added with the FindFilterBuilder class are joined using the logical operator AND. You cannot add multiple conditions for the same key, elsewise only the last condition will be used.

The SDK returns 100 records at most. Use the limit and offset parameters to iterate through the list of records.

FindFilterBuilder builder = FindFilterBuilder.create()
                  //...
                  .limitAndOffset(20, 80);
BatchRecord records = storage.find("us", builder);

Next predicate types are available for each string key field of the Record class through individual methods of the FindFilterBuilder class:

EQUALS         (FindFilterBuilder::keyEq)
               (FindFilterBuilder::key2Eq)
               (FindFilterBuilder::key3Eq)
               (FindFilterBuilder::profileKeyEq)
NOT_EQUALS     (FindFilterBuilder::keyNotEq)
               (FindFilterBuilder::key2NotEq)
               (FindFilterBuilder::key3NotEq)
               (FindFilterBuilder::profileKeyNotEq)

You can use the following builder methods for record filtering by the numerical rangeKey field:

EQUALS              (FindFilterBuilder::rangeKeyEq)
IN                  (FindFilterBuilder::rangeKeyIn)
GREATER             (FindFilterBuilder::rangeKeyGT)
GREATER OR EQUALS   (FindFilterBuilder::rangeKeyGTE)
LESS                (FindFilterBuilder::rangeKeyLT)
LESS OR EQUALS      (FindFilterBuilder::rangeKeyLTE)
BETWEEN             (FindFilterBuilder::rangeKeyBetween)

The find method returns the BatchRecord object which contains a list of Record and some metadata:

class BatchRecord {
    private int count;
    private int limit;
    private int offset;
    private int total;
    private List<Record> records;
    //...
}

These fields can be accessed using getters, for example:

int limit = records.getTotal();

The BatchRecord.getErrors() method allows you to get a list of the RecordException objects with detailed information about records that were not correctly processed during execution of the find request.

Find one record matching a filter

If you need to find the first record matching filter, you can use findOne method:

public interface Storage {
   /**
    * Find only one first record in remote storage according to filters
    *
    * @param country country identifier
    * @param builder object representing find filters
    * @return founded record or null
    * @throws StorageClientException if validation finishes with errors
    * @throws StorageServerException if server connection fails or a server response error occurs
    * @throws StorageCryptoException if decryption fails
    */
    Record findOne(String country, FindFilterBuilder builder)
           throws StorageClientException, StorageServerException, StorageCryptoException;
    //...
}

It works the same way as the find method but returns the first record or null if no records found.

Below you can find the example of how the findOne method can be used:

FindFilterBuilder builder = FindFilterBuilder.create()
                .key2Eq("someKey")
                .key3Eq("firstValue", "secondValue")
                .rangeKeyBetween(123l, 456l);

Record record = storage.findOne("us", builder);
//...
REST API

Find a record

POST /api/records/find

Headers:

HeaderValue
Content-Typeapplication/json
Content-LengthCalculated automatically when a request is sent
HostCalculated automatically when a request is sent
User-AgentSpecify the user agent used
Accept/
Accept-Encodinggzip, deflate, br
Connectionkeep-alive

Request Body:

ParametersTypeExampleDescription
countryISO country code (lowercase)sg (equals to Singapore)Country which a new record is looked up in. You can use different countries.
filterobjectSee the example.Specify the criteria for filtration within the filter parameter. You can look up for records by any of record fields. For the full list of supported fields, please see the Available record fields section.
keyNstringSee the example.Value from the key fields of your record.
search_keysstringabcDefines the search query string for lookup. The search keys should be provided within the filter object.
range_keyNarray[1, 5]Defines the range of keys for lookup. The range keys should be provided within the filter object.
optionsobjectSee the example.Specify the criteria for returning the search results:limit, offset
limitinteger10Sets the maximal number of search results.
offsetinteger50Specifies the offset (pagination) of records from which the record lookup is performed.

Example:

{
  "country": "sg",
  "filter": {
    "key1": ["k1234", "k1235"],
    "key2": ["John", "Jane"]
  },
  "options": {
    "limit": 10,
    "offset": 50
  }
}

or

{
  "country": "sg",
  "filter": {
    "search_keys": "abc"
  }
}

or

{
  "country": "sg",
  "filter": {
    "search_keys": "abc",
    "range_key1": [1, 2]
  }
}

Responses:

STATUS 200 - application/json Returns information about the found record or records.

This status is also returned when no records matching the search criteria are found.

Example:

{
    "data": [
        {
            "key": "k1235",
            "profile_key": "pk1235",
            "range_key": 42,
            "record_key": "k1235",
            "body": null,
            "precommit_body": null,
            "key1": null,
            "key2": "Jane",
            "key3": "Doe",
            "key4": null,
            "key5": null,
            "key6": null,
            "key7": null,
            "key8": null,
            "key9": null,
            "key10": null,
            "service_key1": null,
            "service_key2": null,
            "range_key1": 42,
            "range_key2": null,
            "range_key3": null,
            "range_key4": null,
            "range_key5": null,
            "range_key6": null,
            "range_key7": null,
            "range_key8": null,
            "range_key9": null,
            "range_key10": null,
            "version": 0,
            "created_at": "2020-11-21T12:07:43.000Z",
            "updated_at": "2020-11-21T12:07:43.000Z"
        },
        {
            "key": "k1234",
            "profile_key": "pk1234",
            "range_key": 42,
            "record_key": "k1234",
            "body": null,
            "precommit_body": null,
            "key1": null,
            "key2": "John",
            "key3": "Doe",
            "key4": null,
            "key5": null,
            "key6": null,
            "key7": null,
            "key8": null,
            "key9": null,
            "key10": null,
            "service_key1": null,
            "service_key2": null,
            "range_key1": 42,
            "range_key2": null,
            "range_key3": null,
            "range_key4": null,
            "range_key5": null,
            "range_key6": null,
            "range_key7": null,
            "range_key8": null,
            "range_key9": null,
            "range_key10": null,
            "version": 0,
            "created_at": "2020-11-21T11:55:49.000Z",
            "updated_at": "2020-11-21T12:05:28.000Z"
        }
    ],
    "meta": {
        "count": 2,
        "limit": 100,
        "offset": 0,
        "total": 2
    }
}

STATUS 400 - application/json Bad request when it has not passed validation. The error details will be in the response.

STATUS 401 - This error is returned when the request is unauthorized.

Deleting data records

Deleting data records

.

  • Python SDK
  • Node.js SDK
  • Java SDK
  • REST API
Python SDK

You can use the delete method to delete a record from InCountry Platform. It is possible by using the record_key field only.

def delete(self, country: str, record_key: str) -> Dict[str, bool]:
    ...


# the delete method returns the following dictionary upon success
{
    "success": True
}

Below you can find the example of how to use the delete method:

delete_result = storage.delete(country="us", record_key="<record_key>")

# delete_result would be as follows
delete_result = {
    "success": True
}
Node.js SDK

You can use the delete method to delete a record from InCountry Platform. It is possible by using the record_key field only.

type DeleteResult = {
  success: true;
};

async delete(
  countryCode: string,
  recordKey: string,
  requestOptions: RequestOptions = {},
): Promise<DeleteResult> {
  /* ... */
}

Example of usage:

const deleteResult = await storage.delete(countryCode, recordKey);
Java SDK

Use the delete method to delete records from InCountry storage. It is only possible by using the key field.

public interface Storage {
    /**
    * Delete a record from remote storage
    *
    * @param country   country code of the record
    * @param key the record's key
    * @return true when record was deleted
    * @throws StorageClientException if validation finishes with errors
    * @throws StorageServerException if server connection fails
    */
    boolean delete(String country, String key)
            throws StorageClientException, StorageServerException;
    //...
}

Below you can find the the example of how you may use the delete method:

String key = "user_1";
storage.delete("us", key);
REST API

Delete a record

DELETE /api/records/:country/:key

Headers:

HeaderValue
HostCalculated automatically when a request is sent
User-AgentSpecify the user agent used
Accept/
Accept-Encodinggzip, deflate, br
Connectionkeep-alive

Request parameters:

ParametersTypeValueDescription
countryISO country code (lowercase)sgCountry which a new record is removed from
keystring/arrayk1234Unique primary key for record identification

Responses:

STATUS 204 - plain This response is returned when the record has been successfully deleted.

STATUS 401- This response is returned when the request is unauthorized.

STATUS 404 - plain This response is returned when the record is not found.

Have questions?

You can visit our developer FAQ page to find out answers for your questions.