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.
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:
-
Create an environment for the country where you want to execute resident functions.
-
Create a service of the Resident Functions type.
-
Use the provided credentials (Client ID and Client Secret) to manage resident functions with REST API.
-
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
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
Parameters | Type | Description |
---|---|---|
scriptName | string | Name of a resident function that is published. |
scriptBody | string | Body of the resident function. |
options | object | A JSON object with the country and forceUpdate parameters |
country | string | A country to where a resident function is published. |
forceUpdate | boolean | A 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
Parameters | Type | Description |
---|---|---|
scriptName | string | Name of a resident function that is executed. |
options | object | Object with the country parameter. |
country | string | A 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
Parameters | Type | Description |
---|---|---|
offset | object | Some items to skip before returning a list of resident functions. |
limit | string | The 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
Parameters | Type | Description |
---|---|---|
scriptName | string | Name 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
Parameters | Type | Description |
---|---|---|
scriptName | string | Name 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)
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
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);
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>>
}>;
Exact match search
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 andversion
:
// 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.
Partial text match search
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' }
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' } }
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;
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.
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 thegetSecrets()
function. It wraps the original error which occurs in thegetSecrets()
function. -
SecretsValidationError
is thrown if thegetSecrets()
function returned secrets in the wrong format. -
InputValidationError
is used for input validation errors. It can be thrown by all public methods except theStorage
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 classesStorageConfigValidationError
,SecretsProviderError
,SecretsValidationError
,InputValidationError
,StorageAuthenticationError
are inherited fromStorageClientError
. -
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, unreachableendpoint
etc. Inherited fromStorageServerError
. -
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;
}