Skip to main content

InCountry Resident Functions Documentation

About

Resident Functions are a part of the InCountry platform. It lets you execute resident functions (JavaScript code) on protected data so that it does not leave its country of origin. All the calculations, aggregations, and validations are performed within this country and do not reach your application server located in another country. This way, you do not violate the local regulations and keep compliance with all data protection and localization laws of the data originating country.

Get Started with Resident Functions

The Resident Functions component of the InCountry platform allows you to execute JavaScript functions against regulated data stored in the country of origin.

Resident functions diagram

Management of resident functions is available on the InCountry Portal. If you need to automate the management and execution of resident functions, you can use REST API.

To get started with resident functions, please follow these steps:

  1. Sign up at InCountry Portal.

  2. Create an environment for the country where you want to execute resident functions.

  3. Create a service of the Resident Functions type.

  4. Use the provided credentials (Client ID and Client Secret) to manage resident functions with REST API.

  5. Execute resident functions remotely.

Authorization of Requests to REST API

For the details on how to properly authorize requests to REST API, please check our documentation.

Management of Resident Functions

note

Please check the documentation for REST API before proceeding to this section.

Management of resident functions allows you to create additional record validation and data calculation mechanisms for protected data stored in countries that impose enforced data regulation.

You can manage resident functions within the InCountry platform as follows:

  • publish resident functions

  • execute resident functions

  • get the list of resident functions

  • get a specific resident function

  • delete the no longer needed resident functions

Publishing a Resident Function

POST /serverless/publish

  • Request parameters
ParametersTypeDescription
scriptNamestringName of a resident function that is published.
scriptBodystringBody of the resident function.
optionsobjectA JSON object with the country and forceUpdate parameters
countrystringA country to where a resident function is published.
forceUpdatebooleanA flag that the resident function must be force updated.
  • cURL request
curl --request POST \
--url https://{restApiURLAddress}/serverless/publish \
--header 'Authorization: Bearer <ACCESS_TOKEN>' \
--header 'Content-Type: application/json' \
--data '{
"scriptName": "{FUNCTION-NAME}",
"scriptBody": "module.exports.handler = async (storage, country, params, modules) => { const recordData = { recordKey: '\''UniqRecordKey'\'', body: params.bodyParam, }; const writeResponse = await storage.write(country, recordData); return { result: '\''ok'\'' }; };",
"options": {
"country": "se",
"forceUpdate": false
}
}'
  • Responses

STATUS 201 - plain This response is returned when the resident function has been successfully published.

{"scriptName":"{FUNCTION-NAME}","scriptBody":"module.exports.handler = async (storage, country, params, modules) => { const recordData = { recordKey: 'UniqRecordKey', body: params.bodyParam, }; const writeResponse = await storage.write(country, recordData); return { result: 'ok' }; };"}

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

STATUS 409 - This response is returned when the resident function with the current name already exists.

STATUS 5** - Server error.

Executing a Resident Functions

The endpoint executes the published resident function synchronously and returns its output and meta information.

POST /serverless/execute

  • Request parameters
ParametersTypeDescription
scriptNamestringName of a resident function that is executed.
optionsobjectObject with the country parameter.
countrystringA country where a resident function is executed.
  • cURL request
curl --request POST \
--url https://{restApiURLAddress}/serverless/execute \
--header 'Authorization: Bearer <ACCESS_TOKEN>' \
--header 'Content-Type: application/json' \
--data '{
"scriptName": "{FUNCTION-NAME}",
"options": {
"country": "se"
},
"scriptParams": {
"bodyParam": "test"
}
}'
  • Responses

STATUS 201 - plain This response is returned when the resident function has been successfully executed.

{"result":{"result":"ok"},"duration":1381,"error":null}

STATUS 400 - This response is returned when the request is incorrect.

{"result":null,"duration":775,"error":"InputValidationError: delete() Validation Error: <RecordKey> should be RecordKey but got {\"recordKey\":\"UniqRecordKey\",\"body\":\"test\"}: Record key must be a non-empty string"}

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

STATUS 404 - This response is returned when the resident function has not been found.

STATUS 5** - Server error.

Getting a List of Resident Functions

The endpoint returns a list of published resident functions. It supports pagination and does not return bodies of resident functions.

GET /serverless/functions

  • Request parameters
ParametersTypeDescription
offsetobjectSome items to skip before returning a list of resident functions.
limitstringThe maximal number of resident functions to return. The maximal number is limited to 100.
  • cURL request
curl --request GET \
--url "https://{restApiURLAddress}/serverless/functions?limit=10&offset=0" \
--header 'Authorization: Bearer <ACCESS_TOKEN>'
  • Responses

STATUS 201 - plain This response returns a list of resident functions.

{"data":[{"script_name":"{FUNCTION-NAME}","created_at":"2021-03-30T12:50:45.000Z","updated_at":"2021-03-30T12:59:47.000Z"},{"script_name":"e2e-script-1-53203","created_at":"2021-03-26T10:13:30.000Z","updated_at":"2021-03-26T10:13:30.000Z"},{"script_name":"e2e-script-66046","created_at":"2021-03-26T10:13:29.000Z","updated_at":"2021-03-26T10:13:29.000Z"},{"script_name":"e2e-script-1-30449","created_at":"2021-03-26T09:02:30.000Z","updated_at":"2021-03-26T09:02:30.000Z"},{"script_name":"e2e-script-26244","created_at":"2021-03-26T09:02:29.000Z","updated_at":"2021-03-26T09:02:29.000Z"}],"meta":{"count":5,"limit":10,"offset":0,"total":5}}

STATUS 400 - This response is returned when specified parameters are incorrect.

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

STATUS 5** - Server error.

Getting a Resident Function

The endpoint returns information about a specific resident function.

GET /serverless/functions/{scriptName}

  • Request parameters
ParametersTypeDescription
scriptNamestringName of a resident function whose information should be returned.
  • cURL request
curl --request GET \
--url https://{restApiURLAddress}/serverless/functions/{FUNCTION-NAME} \
--header 'Authorization: Bearer <ACCESS_TOKEN>'
  • Responses

STATUS 201 - plain This response returns a resident function with its information.

{"script_name":"{SCRIPT-NAME}","script_body":"module.exports.handler = async (storage, country, params, modules) => { const recordData = { recordKey: 'UniqRecordKey', body: params.bodyParam, }; const writeResponse = await storage.write(country, recordData); return { result: 'ok' }; };","created_at":"2021-03-30T12:50:45.000Z","updated_at":"2021-03-30T12:59:47.000Z"}%

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

STATUS 404 - This response is returned when the specified resident function has not been found.

STATUS 5** - Server error.

Deleting a Resident Function

The endpoint deletes a specific resident function.

DELETE /serverless/scripts/{scriptName}

  • Request parameters
ParametersTypeDescription
scriptNamestringName of a resident function that should be deleted.
  • cURL request
curl --request DELETE \
--url https://{restApiURLAddress}/serverless/functions/{FUNCTION-NAME} \
--header 'Authorization: Bearer <ACCESS_TOKEN>'
  • Responses

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

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

STATUS 404 - This response is returned when the specified resident function has not been found.

STATUS 5** - Server error.

Node.js SDK for resident functions

You need to use our Node.js SDK to write resident functions. Below you can find the methods available in the Node.js SDK.

Managing Records

This section lists all methods that allow you to perform operations on records.

Record structure

The InCountry platform stores records of a specific structure. Below you can find description of this structure and some specifics that you need to consider when dealing with fields of different formats.

String fields, hashed
recordKey
parentKey
profileKey
serviceKey1
serviceKey2
serviceKey3
serviceKey4
serviceKey5
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
key11
key12
key13
key14
key15
key16
key17
key18
key19
key20
String fields, encrypted
body
precommitBody
Integer fields, plain
rangeKey1
rangeKey2
rangeKey3
rangeKey4
rangeKey5
rangeKey6
rangeKey7
rangeKey8
rangeKey9
rangeKey10
Date fields, plain
createdAt
updatedAt
expiresAt
Warning

Records with non-null expiresAt value will be automatically deleted upon reaching the specified date.

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

Use expiresAt field to limit the lifespan of a record stored on the InCountry platform. The record will be automatically deleted upon reaching the specified date.

Below is an example of how to create a record that will be deleted in a day:

const nextDayDate = new Date();
nextDayDate.setDate(nextDayDate.getDate() + 1);

const recordData = {
recordKey: '<key>',
...,
expiresAt: nextDayDate,
}

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

All dates are stored in UTC timezone and converted to UTC timezone in case of expiresAt.

The SDK accepts ISO-8601 formatted strings, e.g 2021-03-11T17:23:05.941Z, numeric timestamps with decimal microseconds, Date and MicroDate objects. Microseconds are accepted using ISO string (2021-03-11T17:23:05.941001Z), float number timestamps (1630574845881.123) or MicroDate object.

It's highly recommended to provide timezone-aware dates to avoid any timezone-related issues in the future.

Creating/replacing a single record

You can use the write method to create/replace a single record by its recordKey.

type StorageRecordData = {
recordKey: string; // Accepts non-empty string
parentKey?: string | null;
profileKey?: string | null;
key1?: string | null;
...,
key20?: string | null;
serviceKey1?: string | null;
...,
serviceKey5?: string | null;
body?: string | null;
precommitBody?: string | null;
rangeKey1?: number | null; // Accepts only Integer numbers
...,
rangeKey10?: number | null; // Accepts only Integer numbers
expiresAt?: string | number | Date | MicroDate | null; // Accepts ISO-8601 formatted strings with microseconds, numeric timestamps with decimal microseconds, Date and MicroDate objects
};

type StorageRecord = {
recordKey: string;
body: string | null;
parentKey: string | null;
profileKey: string | null;
precommitBody: string | null;
key1?: string | null;
key2?: string | null;
key3?: string | null;
key4?: string | null;
key5?: string | null;
key6?: string | null;
key7?: string | null;
key8?: string | null;
key9?: string | null;
key10?: string | null;
key11?: string | null;
key12?: string | null;
key13?: string | null;
key14?: string | null;
key15?: string | null;
key16?: string | null;
key17?: string | null;
key18?: string | null;
key19?: string | null;
key20?: string | null;
serviceKey1: string | null;
serviceKey2: string | null;
serviceKey3: string | null;
serviceKey4: string | null;
serviceKey5: string | null;
rangeKey1: Int | null;
rangeKey2: Int | null;
rangeKey3: Int | null;
rangeKey4: Int | null;
rangeKey5: Int | null;
rangeKey6: Int | null;
rangeKey7: Int | null;
rangeKey8: Int | null;
rangeKey9: Int | null;
rangeKey10: Int | null;
createdAt: MicroDate;
updatedAt: MicroDate;
expiresAt: MicroDate | null;
attachments: StorageRecordAttachment[];
};

type WriteResult = {
record: StorageRecord;
};

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

Example of usage:

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

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

Creating/replacing multiple records

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

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

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

Example of usage:

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

Reading records

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.

type StorageRecord = {
recordKey: string;
body: string | null;
parentKey: string | null;
profileKey: string | null;
precommitBody: string | null;
key1?: string | null;
key2?: string | null;
key3?: string | null;
key4?: string | null;
key5?: string | null;
key6?: string | null;
key7?: string | null;
key8?: string | null;
key9?: string | null;
key10?: string | null;
key11?: string | null;
key12?: string | null;
key13?: string | null;
key14?: string | null;
key15?: string | null;
key16?: string | null;
key17?: string | null;
key18?: string | null;
key19?: string | null;
key20?: string | null;
serviceKey1: string | null;
serviceKey2: string | null;
serviceKey3: string | null;
serviceKey4: string | null;
serviceKey5: string | null;
rangeKey1: Int | null;
rangeKey2: Int | null;
rangeKey3: Int | null;
rangeKey4: Int | null;
rangeKey5: Int | null;
rangeKey6: Int | null;
rangeKey7: Int | null;
rangeKey8: Int | null;
rangeKey9: Int | null;
rangeKey10: Int | null;
createdAt: MicroDate;
updatedAt: MicroDate;
expiresAt: MicroDate | null;
attachments: StorageRecordAttachment[];
}

type ReadResult = {
record: StorageRecord;
};

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

Example of usage:

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

Finding records

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

type DateLike = string | number | Date | MicroDate;
type FilterDateQuery = DateLike | null | { $not?: DateLike | null; $gt?: DateLike; $gte?: DateLike; $lt?: DateLike; $lte?: DateLike; };
type FilterStrictDateQuery = DateLike | { $not?: DateLike; $gt?: DateLike; $gte?: DateLike; $lt?: DateLike; $lte?: DateLike; };
type FilterNumberQuery = number | number[] | null | { $not?: number | number[] | null; $gt?: number; $gte?: number; $lt?: number; $lte?: number; }; // number[] should be a non empty array
type FilterStringQuery = string | string[] | null | { $not?: string | string[] | null }; // string[] should be a non empty array
type FilterStringWithLikeQuery = string | string[] | null | { $not?: string | string[] | null, $like?: string };

type FindFilterStringFields = {
recordKey: FilterStringQuery;
parentKey: FilterStringQuery;
key1: FilterStringWithLikeQuery;
key2: FilterStringWithLikeQuery;
key3: FilterStringWithLikeQuery;
key4: FilterStringWithLikeQuery;
key5: FilterStringWithLikeQuery;
key6: FilterStringWithLikeQuery;
key7: FilterStringWithLikeQuery;
key8: FilterStringWithLikeQuery;
key9: FilterStringWithLikeQuery;
key10: FilterStringWithLikeQuery;
key11: FilterStringWithLikeQuery;
key12: FilterStringWithLikeQuery;
key13: FilterStringWithLikeQuery;
key14: FilterStringWithLikeQuery;
key15: FilterStringWithLikeQuery;
key16: FilterStringWithLikeQuery;
key17: FilterStringWithLikeQuery;
key18: FilterStringWithLikeQuery;
key19: FilterStringWithLikeQuery;
key20: FilterStringWithLikeQuery;
profileKey: FilterStringQuery;
serviceKey1: FilterStringQuery;
serviceKey2: FilterStringQuery;
serviceKey3: FilterStringQuery;
serviceKey4: FilterStringQuery;
serviceKey5: FilterStringQuery;
}

type FindFilterNumberFields = {
rangeKey1: FilterNumberQuery;
rangeKey2: FilterNumberQuery;
rangeKey3: FilterNumberQuery;
rangeKey4: FilterNumberQuery;
rangeKey5: FilterNumberQuery;
rangeKey6: FilterNumberQuery;
rangeKey7: FilterNumberQuery;
rangeKey8: FilterNumberQuery;
rangeKey9: FilterNumberQuery;
rangeKey10: FilterNumberQuery;
version: FilterNumberQuery;
};

type FindFilterDateFields = {
createdAt: FilterStrictDateLikeQuery;
updatedAt: FilterStrictDateLikeQuery;
expiresAt: FilterDateLikeQuery;
};

type FindFilter = Partial<FindFilterStringFields & FindFilterNumberFields & FindFilterDateFields {
searchKeys: string;
$or: Array<Partial<FindFilterStringFields>>
}>;

The following exact match search criteria are available:

  • single value:
// Matches all records where record.key1 = 'abc' AND record.rangeKey1 = 1
{ key1: 'abc', rangeKey1: 1 }
  • multiple values as an array:
// Matches all records where (record.key2 = 'def' OR record.key2 = 'jkl') AND (record.rangeKey1 = 1 OR record.rangeKey1 = 2)
{ key2: ['def', 'jkl'], rangeKey1: [1, 2] }
  • a logical NOT operator for string fields and version:
// 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 }}
  • comparison operators for integer fields:
// Matches all records where record.rangeKey1 >= 5 AND record.rangeKey1 <= 100
{ rangeKey1: { $gte: 5, $lte: 100 } }
  • multiple criteria with $or operator:
{
$or: [
{ key1: 'john', key2: 'smith' },
{ key1: 'smith', key2: 'john' },
]
}

{
$or: [
{ key1: { $like: 'john' } },
{ key2: { $like: 'john' } },
]
}

Note: currently, nested $or operators and non-string fields inside $or operator are not supported.

You can also look up data records by partial match using the searchKeys operator which performs partial match search (similar to the LIKE SQL operator, without special characters) within the record's text fields key1, key2, ..., key20.

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

The searchKeys operator cannot be used in combination with any of key1, key2, ..., key20 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.key20 LIKE 'abc') AND (record.rangeKey1 = 1 OR record.rangeKey1 = 2)
{ searchKeys: 'abc', rangeKey1: [1, 2] }

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

Another way to find records by partial key match is using $like operator. It provides a partial match search (similar to the LIKE SQL operator without special characters) against one of the record’s string fields key1, key2, ..., key20.

// Matches all records where record.key3 LIKE 'abc'
{ key3: { $like: 'abc' } }
note

You can use either searchKeys or $like, not both at once.

The options parameter provides the following choices to manipulate the search results:

  • limit allows to limit the total number of records returned;

  • offset allows to specify the starting index used for records pagination;

  • sort allows to sort the returned records by one or multiple keys;

Warning

To use the sort option in the find() call to string keys key1...key20, you need to set the hashSearchKeys option to false.

Sorting

Fields that records can be sorted by:

type SortKey =
| 'createdAt'
| 'updatedAt'
| 'expiresAt'
| 'key1'
| 'key2'
| 'key3'
| 'key4'
| 'key5'
| 'key6'
| 'key7'
| 'key8'
| 'key9'
| 'key10'
| 'key11'
| 'key12'
| 'key13'
| 'key14'
| 'key15'
| 'key16'
| 'key17'
| 'key18'
| 'key19'
| 'key20'
| 'rangeKey1'
| 'rangeKey2'
| 'rangeKey3'
| 'rangeKey4'
| 'rangeKey5'
| 'rangeKey6'
| 'rangeKey7'
| 'rangeKey8'
| 'rangeKey9'
| 'rangeKey10';
type SortItem = Partial<Record<SortKey, 'asc' | 'desc'>>; // each sort item should describe only one key!

type FindOptions = {
limit?: number;
offset?: number;
sort?: NonEmptyArray<SortItem>;
};

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> {
/* ... */
}

Below you can find the example of how to sort records:

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

const options = {
limit: 100,
offset: 0,
sort: [{ createdAt: 'asc' }, { rangeKey1: 'desc' }],
};

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
}
}

with findResult.records sorted according to the following pseudo-sql:

SELECT * FROM record WHERE ...  ORDER BY createdAt asc, rangeKey1 desc

Finding one record matching search criteria

If you need to find only one records (from the dataset) matching a specific filter, you can use the findOne method. If a record is not found, it returns { record: 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);

Deleting a single record

You can use the delete method to delete a record from the InCountry platform. It is possible by using the recordKey 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);

Deleting multiple records

You can use the batchDelete method to delete multiple records from the InCountry platform. For now, the SDK allows deletion only by a list of recordKeys.

type DeleteFilter = {
recordKey: string[];
};

type DeleteResult = {
success: true;
};

async batchDelete(
countryCode: string,
filter: DeleteFilter,
requestOptions: RequestOptions = {},
): Promise<DeleteResult> {
/* ... */
}

Example of usage:

const deleteResult = await storage.batchDelete(countryCode, { recordKey: ['aaa'] });

Attaching files to a record

You can attach files to the previously created records. Attachments' meta information is available through the attachments field of the StorageRecord object.

type StorageRecord = {
/* ... other fields ... */
attachments: StorageRecordAttachment[];
}

type StorageRecordAttachment = {
fileId: string;
fileName: string;
hash: string;
mimeType: string;
size: number;
createdAt: MicroDate;
updatedAt: MicroDate;
downloadLink: string;
}

Adding a single file to the record

The addAttachment method allows you to add or replace attachments. File data can be provided either as Readable stream, Buffer or string with a path to the file in the file system.

note

Attaching files exceeding 100 Mb is not supported at the moment.

type AttachmentData = {
file: Readable | Buffer | string;
fileName: string;
}

async addAttachment(
countryCode: string,
recordKey: string,
{ file, fileName }: AttachmentData,
upsert = false,
requestOptions: RequestOptions = {},
): Promise<StorageRecordAttachment> {
/* ... */
}

Example of usage:

// using file path
await storage.addAttachment(COUNTRY, recordData.recordKey, { file: '../file' });

// using data Stream
import * as fs from 'fs';

const file = fs.createReadStream('./LICENSE');
await storage.addAttachment(COUNTRY, recordData.recordKey, { file });

Deleting attachments

The deleteAttachment method allows you to delete attachment using its fileId.

async deleteAttachment(
countryCode: string,
recordKey: string,
fileId: string,
requestOptions: RequestOptions = {},
): Promise<unknown> {
/* ... */
}

Example of usage:

await storage.deleteAttachment(COUNTRY, recordData.recordKey, attachmentMeta.fileId);

Downloading attachments

The getAttachmentFile method allows you to download attachments. It returns an object with a readable stream and filename.

async getAttachmentFile(
countryCode: string,
recordKey: string,
fileId: string,
requestOptions: RequestOptions = {},
): Promise<GetAttachmentFileResult> {
/* ... */
}

Example of usage:

import * as fs from 'fs';

const { attachmentData } = await storage.getAttachmentFile(COUNTRY, recordData.recordKey, attachmentMeta.fileId);

const { file, fileName } = attachmentData;
const writeStream = fs.createWriteStream(`./${fileName}`);
file.pipe(writeStream);

Working with attachment meta info

The getAttachmentMeta method allows you to retrieve attachment's metadata using its fileId.

async getAttachmentMeta(
countryCode: string,
recordKey: string,
fileId: string,
requestOptions: RequestOptions = {},
): Promise<StorageRecordAttachment> {
/* ... */
}

Example of usage:

const meta: StorageRecordAttachment = await storage.getAttachmentMeta(COUNTRY, recordData.recordKey, attachmentMeta.fileId);

The updateAttachmentMeta method allows you to update attachment's metadata (MIME type and file name).

type AttachmentWritableMeta = {
fileName?: string;
mimeType?: string;
};

async updateAttachmentMeta(
countryCode: string,
recordKey: string,
fileId: string,
fileMeta: AttachmentWritableMeta,
requestOptions: RequestOptions = {},
): Promise<StorageRecordAttachment> {
/* ... */
}

Example of usage:

await storage.updateAttachmentMeta(COUNTRY, data.recordKey, attachmentMeta.fileId, { fileName: 'new name!' });

Error Handling

The Node.js SDK may throw the following exceptions:

  • StorageConfigValidationError is used for output of storage option validation errors. It can be thrown by any public method.

  • SecretsProviderError is thrown during a call of the getSecrets() function. It wraps the original error which occurs in the getSecrets() function.

  • SecretsValidationError is thrown if the getSecrets() function returned secrets in the wrong format.

  • InputValidationError is used for input validation errors. It can be thrown by all public methods except the Storage constructor.

  • StorageAuthenticationError is thrown if the SDK fails to authenticate in the InCountry Platform with the provided credentials.

  • StorageClientError is used for various errors related to the Storage configuration. All the errors in classes StorageConfigValidationError, SecretsProviderError, SecretsValidationError, InputValidationError, StorageAuthenticationError are inherited from StorageClientError.

  • StorageServerError -is thrown if the SDK fails to communicate with the InCountry platform or if the server response validation fails.

  • StorageNetworkError is thrown if the SDK fails to communicate with InCountry servers due to network issues, such as request timeout, unreachable endpoint etc. Inherited from StorageServerError.

  • StorageCryptoError is thrown during encryption/decryption procedures (both default and custom). This may be a sign of malformed/corrupt data or a wrong encryption key provided to the SDK.

  • StorageError is a general exception which is inherited by all the other exceptions.

We strongly recommend the to gracefully handle all the possible exceptions, as follows:

try {
// use InCountry Storage instance here
} catch(e) {
if (e instanceof StorageClientError) {
// some input validation error

// if you need to handle configuration errors more precisely:
if (e instanceof StorageConfigValidationError) {
// something is wrong with Storage options
} else if (e instanceof SecretsProviderError) {
// something is wrong with `getSecrets()` function. The original error is available in `e.data`
} else if (e instanceof SecretsValidationError) {
// something is wrong with `getSecrets()` function result
} else if (e instanceof InputValidationError) {
// something is wrong with input data passed to Storage public method
} else if (e instanceof StorageAuthenticationError) {
// something is wrong with the credentials provided in Storage options
}
} else if (e instanceof StorageServerError) {
// some server error

if (e instanceof StorageNetworkError) {
// something is wrong with network connection
} else {
// server error or server response validation failed
}
} else if (e instanceof StorageCryptoError) {
// some encryption error
} else {
// ...
}
}

Resident Function Examples

All resident functions should be written as a JavaScript function.

Below you can find an example of the resident function.

In the current examples, storage is an instance of the Storage class.

Example #1 - Check Record with a Specific Search Param

module.exports.handler = async (storage, country, params, modules) => {
const result = await storage.find(country, { key1: params.email });
if (result.records.length > 0) {
return true;
}
return false;
};

Example #2 - Use All the Methods in a Junction

module.exports.handler = async (storage, country, params, modules) => {
let result;
const validateRecord = (expectedRecord, actualRecord, methodValue) => { // method to compare actual record with expected
if (expectedRecord.recordKey !== actualRecord.recordKey) {
return `bad recordKey ${methodValue}`;
}
if (expectedRecord.body !== actualRecord.body) {
return `bad body ${methodValue}`;
}
if (expectedRecord.key1 !== actualRecord.key1) {
return `bad key1 ${methodValue}`;
}
if (expectedRecord.key2 !== actualRecord.key2) {
return `bad key2 ${methodValue}`;
}
if (expectedRecord.key3 !== actualRecord.key3) {
return `bad key3 ${methodValue}`;
}
if (expectedRecord.key10 !== actualRecord.key10) {
return `bad key10 ${methodValue}`;
}
if (expectedRecord.profileKey !== actualRecord.profileKey) {
return `bad profileKey ${methodValue}`;
}
if (expectedRecord.serviceKey1 !== actualRecord.serviceKey1) {
return `bad serviceKey1 ${methodValue}`;
}
if (expectedRecord.rangeKey1 !== actualRecord.rangeKey1) {
return `bad rangeKey1 ${methodValue}`;
}
if (expectedRecord.rangeKey10 !== actualRecord.rangeKey10) {
return `bad rangeKey10 ${methodValue}`;
}
return 'ok';
};
const validateMeta = (expectedCount, expectedTotal, expectedOffset, expectedLimit, actualMeta, methodValue) => { // method to compare actual metadata with expected
if (expectedCount !== actualMeta.count) {
return `bad count ${methodValue}`;
}
if (expectedTotal !== actualMeta.total) {
return `bad total ${methodValue}`;
}
if (expectedOffset !== actualMeta.offset) {
return `bad offset ${methodValue}`;
}
if (expectedLimit !== actualMeta.limit) {
return `bad limit ${methodValue}`;
}
return 'ok';
};
const recordData = {
recordKey: params.recordKeyParam,
body: params.bodyParam,
key1: params.key1Param,
key2: params.key2Param,
key3: params.key3Param,
key10: params.key10Param,
profileKey: params.profileKeyParam,
serviceKey1: params.serviceKey1Param,
rangeKey1: params.rangeKey1Param,
rangeKey10: params.rangeKey10Param,
};
const writeResult = await storage.write(country, recordData);
const readRecord = await storage.read(country, params.recordKeyParam);
result = validateRecord(recordData, readRecord.record, 'read'); // compares if the written record matches the read one
if (result !== 'ok') {
return result;
}
const findOneRecord = await storage.findOne(country, { recordKey: params.recordKeyParam });
result = validateRecord(recordData, findOneRecord.record, 'findOne by recordKey'); // compares if the written record matches the found one by the recordKey with the findOne method
if (result !== 'ok') {
return result;
}
const findOneRecord2 = await storage.findOne(country, { key2: params.key2Param });
result = validateRecord(recordData, findOneRecord2.record, 'findOne by key2'); // compares if the written record matches the found one by the key2 with the findOne method
if (result !== 'ok') {
return result;
}
const limit = 10;
const offset = 0;
const findResponse = await storage.find(country, { recordKey: params.recordKeyParam }, { limit, offset });
const actualMeta = findResponse.meta;
result = validateMeta(1, 1, offset, limit, actualMeta, 'find by recordKey'); // compares if the written record meta matches the found record by the recordKey meta
if (result !== 'ok') {
return result;
}
const findRecord = findResponse.records[0];
result = validateRecord(recordData, findRecord, 'find by recordKey'); // compares if the written record matches the found one by the recordKey with the find method
if (result !== 'ok') {
return result;
}
const findResponse2 = await storage.find(country, { rangeKey10: params.rangeKey10Param }, { limit, offset });
const actualMeta2 = findResponse2.meta;
result = validateMeta(1, 1, offset, limit, actualMeta2, 'find by rangeKey10'); // compares if the written record meta matches the found record by the rangeKey10 meta
if (result !== 'ok') {
return result;
}
const findRecord2 = findResponse2.records[0];
result = validateRecord(recordData, findRecord2, 'find by rangeKey10'); // compares if the written record matches the found one by the rangeKey10 with the find method
if (result !== 'ok') {
return result;
}
await storage.delete(country, params.recordKeyParam); // delete record
return result;
};

Example #3 - Manage Records in InCountry Point of Presence

  • read a record from the InCountry platform

  • use the record and its fields

  • create a new record on the InCountry platform

  • update the existing record on the InCountry platform

module.exports.handler = async function(storage, country, params, modules) {
// The current resident function copies all values from the parent record to its child record

const mainRecordSFDCId = params.Id;
let mainRecordICObj;
let mainRecordICBodyObj;

const childSFDCId = params.Duplicate__c;
let childRecordICObj;
let childRecordICBodyObj;

const result = {
data: null,
success: false,
error: false,
errorMsg: null,
};

try {
({
record: mainRecordICObj
} = await storage.findOne(country, {
profileKey: mainRecordSFDCId
}));

if (mainRecordICObj == null) {
result.errorMsg = "Record with Id [" + mainRecordSFDCId + "] was not found.";
} else {
mainRecordICBodyObj = JSON.parse(mainRecordICObj.body);

if (childSFDCId != null && childSFDCId.length > 0) {
({
record: childRecordICObj
} = await storage.findOne(country, {
profileKey: childSFDCId
}));

if (childRecordICObj == null) {
// Defines the case when a duplicate record is not stored on the InCountry platform
// we need to copy everything and set new unique identifiers for the duplicate record
let childRecordICObj = {
...mainRecordICObj
};
// Sets the keys fields with Salesforce Id as a unique identifier that is not yet present on the InCountry platform
childRecordICObj.key = childSFDCId;
childRecordICObj.profileKey = childSFDCId;
childRecordICObj.recordKey = childSFDCId;
childRecordICObj.body = JSON.stringify(mainRecordICBodyObj);
await storage.write(country, childRecordICObj);
} else {
// Defines the case when a duplicate record is already stored on the InCountry platform
// and all needed fields should be updated
childRecordICBodyObj = JSON.parse(childRecordICObj.body);

if (childRecordICBodyObj.Email != mainRecordICBodyObj.Email) {
childRecordICBodyObj.Email = mainRecordICBodyObj.Email;
}
if (childRecordICBodyObj.MobilePhone != mainRecordICBodyObj.MobilePhone) {
childRecordICBodyObj.MobilePhone = mainRecordICBodyObj.MobilePhone;
}

// the goal here is to update all necessary keys
// for example, the MobilePhone field was configured as key2
childRecordICObj.key2 = mainRecordICBodyObj.MobilePhone;
// this should be done separately from updating the body of the record
childRecordICObj.body = JSON.stringify(childRecordICBodyObj);
await storage.write(country, childRecordICObj);
}
}
}
} catch (error) {
result.errorMsg = error.message;
}

if (result.errorMsg != null) {
result.success = true;
}
return result;
}

Example #4 - Aggregate Data

module.exports.handler = async function(storage, country, params) {
const result = {
data: null,
success: false,
error: false,
errorMsg: null,
};

try {
result.data = await storage.aggregate(country, {
metrics: {
metric1: { func: 'sum', key: 'range_key1' },
metric2: { func: 'sum', key: 'range_key2' },
},
})

} catch (error) {
result.errorMsg = error.message;
}

if (result.errorMsg != null) {
result.success = true;
}
return result;
}