From 446408bfb58d804a55c81b785b02beefbe2a949f Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Tue, 2 Jun 2026 16:45:25 +0530 Subject: [PATCH 01/14] SK-2658 add all query params available in get --- .../core-utils/element-validations.test.js | 241 +++++++++++++++++ __tests__/core-utils/reveal.test.js | 245 ++++++++++++++++++ example/src/RevealElements.tsx | 86 ++++-- src/core-utils/element-validations/index.ts | 50 ++++ src/core-utils/reveal/index.ts | 26 +- src/index.ts | 1 + src/utils/constants/index.ts | 11 + src/utils/logs/index.ts | 16 ++ src/utils/skyflow-error-code/index.ts | 32 +++ 9 files changed, 686 insertions(+), 22 deletions(-) diff --git a/__tests__/core-utils/element-validations.test.js b/__tests__/core-utils/element-validations.test.js index 67c090f..8ff754e 100644 --- a/__tests__/core-utils/element-validations.test.js +++ b/__tests__/core-utils/element-validations.test.js @@ -996,4 +996,245 @@ describe('test get options validation', () => { ); } }); + + it('should throw error for non-string offset in get options', () => { + try { + validateGetInput( + { records: [{ ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT }] }, + { offset: 10 } + ); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + SKYFLOW_ERROR_CODE.INVALID_OFFSET_IN_GET.description + ); + } + }); + + it('should throw error for null offset in get options', () => { + try { + validateGetInput( + { records: [{ ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT }] }, + { offset: null } + ); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + SKYFLOW_ERROR_CODE.INVALID_OFFSET_IN_GET.description + ); + } + }); + + it('should throw error for non-string limit in get options', () => { + try { + validateGetInput( + { records: [{ ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT }] }, + { limit: true } + ); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + SKYFLOW_ERROR_CODE.INVALID_LIMIT_IN_GET.description + ); + } + }); + + it('should throw error for null limit in get options', () => { + try { + validateGetInput( + { records: [{ ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT }] }, + { limit: null } + ); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + SKYFLOW_ERROR_CODE.INVALID_LIMIT_IN_GET.description + ); + } + }); + + it('should throw error for non-boolean downloadURL in get options', () => { + try { + validateGetInput( + { records: [{ ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT }] }, + { downloadURL: 'true' } + ); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + SKYFLOW_ERROR_CODE.INVALID_DOWNLOAD_URL_IN_GET.description + ); + } + }); + + it('should throw error for number passed as downloadURL in get options', () => { + try { + validateGetInput( + { records: [{ ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT }] }, + { downloadURL: 1 } + ); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + SKYFLOW_ERROR_CODE.INVALID_DOWNLOAD_URL_IN_GET.description + ); + } + }); + + it('should throw error for null downloadURL in get options', () => { + try { + validateGetInput( + { records: [{ ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT }] }, + { downloadURL: null } + ); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + SKYFLOW_ERROR_CODE.INVALID_DOWNLOAD_URL_IN_GET.description + ); + } + }); + + it('should throw error for invalid orderBy value in get options', () => { + try { + validateGetInput( + { records: [{ ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT }] }, + { orderBy: 'INVALID_ORDER' } + ); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + SKYFLOW_ERROR_CODE.INVALID_ORDER_BY_IN_GET.description + ); + } + }); + + it('should throw error for null orderBy in get options', () => { + try { + validateGetInput( + { records: [{ ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT }] }, + { orderBy: null } + ); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + SKYFLOW_ERROR_CODE.INVALID_ORDER_BY_IN_GET.description + ); + } + }); + + it('should not throw for valid offset and limit in get options', () => { + expect(() => + validateGetInput( + { records: [{ ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT }] }, + { offset: '5', limit: '10' } + ) + ).not.toThrow(); + }); + + it('should not throw for valid downloadURL in get options', () => { + expect(() => + validateGetInput( + { records: [{ ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT }] }, + { downloadURL: true } + ) + ).not.toThrow(); + }); + + it('should not throw for valid orderBy ASCENDING in get options', () => { + expect(() => + validateGetInput( + { records: [{ ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT }] }, + { orderBy: 'ASCENDING' } + ) + ).not.toThrow(); + }); + + it('should not throw for valid orderBy DESCENDING in get options', () => { + expect(() => + validateGetInput( + { records: [{ ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT }] }, + { orderBy: 'DESCENDING' } + ) + ).not.toThrow(); + }); + + it('should not throw for valid orderBy NONE in get options', () => { + expect(() => + validateGetInput( + { records: [{ ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT }] }, + { orderBy: 'NONE' } + ) + ).not.toThrow(); + }); +}); + +describe('test validateGetInput fields in options', () => { + const validRecord = { ids: ['123'], table: 'test', redaction: RedactionType.PLAIN_TEXT }; + + it('should throw error for null fields in options', () => { + try { + validateGetInput({ records: [validRecord] }, { fields: null }); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + SKYFLOW_ERROR_CODE.INVALID_FIELDS_IN_GET.description + ); + } + }); + + it('should throw error for non-array fields in options', () => { + try { + validateGetInput( + { records: [validRecord] }, + { fields: 'occupation' } + ); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + SKYFLOW_ERROR_CODE.INVALID_FIELDS_IN_GET.description + ); + } + }); + + it('should throw error for empty fields array in options', () => { + try { + validateGetInput( + { records: [validRecord] }, + { fields: [] } + ); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + SKYFLOW_ERROR_CODE.EMPTY_FIELDS_IN_GET.description + ); + } + }); + + it('should throw error for non-string value inside fields array in options', () => { + try { + validateGetInput({ records: [validRecord] }, { fields: [123] }); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + SKYFLOW_ERROR_CODE.INVALID_FIELD_VALUE_IN_GET.description + ); + } + }); + + it('should throw error for null value inside fields array in options', () => { + try { + validateGetInput({ records: [validRecord] }, { fields: [null] }); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + SKYFLOW_ERROR_CODE.INVALID_FIELD_VALUE_IN_GET.description + ); + } + }); + + it('should throw error for empty string inside fields array in options', () => { + try { + validateGetInput({ records: [validRecord] }, { fields: [''] }); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + SKYFLOW_ERROR_CODE.EMPTY_FIELD_VALUE_IN_GET.description + ); + } + }); + + it('should not throw for valid fields array in options', () => { + expect(() => + validateGetInput( + { records: [validRecord] }, + { fields: ['occupation', 'annual_income'] } + ) + ).not.toThrow(); + }); }); diff --git a/__tests__/core-utils/reveal.test.js b/__tests__/core-utils/reveal.test.js index 6ebfc1a..4a8bd98 100644 --- a/__tests__/core-utils/reveal.test.js +++ b/__tests__/core-utils/reveal.test.js @@ -675,4 +675,249 @@ describe('fetchRecordGET fn test', () => { done(err); }); }); + + it('should include fields params in url when fields are specified in options', (done) => { + let reqArg; + jest.spyOn(ClientModule, 'default').mockImplementation(() => ({ + request: (args) => { + reqArg = args; + return Promise.resolve(getSuccessResponse); + }, + })); + + const testSkyflowClient = new Skyflow({ + vaultID: '1234', + vaultURL: 'https://url.com', + getBearerToken: () => Promise.resolve('valid_token'), + }); + + jest.spyOn(testSkyflowClient, 'getAccessToken').mockResolvedValue('valid token'); + + fetchRecordsGET(testSkyflowClient, [getRecordID], { ...optionsFalse, fields: ['occupation', 'annual_income'] }) + .then((res) => { + expect(reqArg.url).toContain('fields=occupation'); + expect(reqArg.url).toContain('fields=annual_income'); + done(); + }) + .catch((err) => { + done(err); + }); + }); + + it('should include offset and limit params in url when specified in options', (done) => { + let reqArg; + jest.spyOn(ClientModule, 'default').mockImplementation(() => ({ + request: (args) => { + reqArg = args; + return Promise.resolve(getSuccessResponse); + }, + })); + + const testSkyflowClient = new Skyflow({ + vaultID: '1234', + vaultURL: 'https://url.com', + getBearerToken: () => Promise.resolve('valid_token'), + }); + + jest.spyOn(testSkyflowClient, 'getAccessToken').mockResolvedValue('valid token'); + + fetchRecordsGET(testSkyflowClient, [getRecordID], { ...optionsFalse, offset: '5', limit: '10' }) + .then((res) => { + expect(reqArg.url).toContain('offset=5'); + expect(reqArg.url).toContain('limit=10'); + done(); + }) + .catch((err) => { + done(err); + }); + }); + + it('should include downloadURL param in url when specified in options', (done) => { + let reqArg; + jest.spyOn(ClientModule, 'default').mockImplementation(() => ({ + request: (args) => { + reqArg = args; + return Promise.resolve(getSuccessResponse); + }, + })); + + const testSkyflowClient = new Skyflow({ + vaultID: '1234', + vaultURL: 'https://url.com', + getBearerToken: () => Promise.resolve('valid_token'), + }); + + jest.spyOn(testSkyflowClient, 'getAccessToken').mockResolvedValue('valid token'); + + fetchRecordsGET(testSkyflowClient, [getRecordID], { ...optionsFalse, downloadURL: true }) + .then((res) => { + expect(reqArg.url).toContain('downloadURL=true'); + done(); + }) + .catch((err) => { + done(err); + }); + }); + + it('should include order_by param in url when orderBy is specified in options', (done) => { + let reqArg; + jest.spyOn(ClientModule, 'default').mockImplementation(() => ({ + request: (args) => { + reqArg = args; + return Promise.resolve(getSuccessResponse); + }, + })); + + const testSkyflowClient = new Skyflow({ + vaultID: '1234', + vaultURL: 'https://url.com', + getBearerToken: () => Promise.resolve('valid_token'), + }); + + jest.spyOn(testSkyflowClient, 'getAccessToken').mockResolvedValue('valid token'); + + fetchRecordsGET(testSkyflowClient, [getRecordID], { ...optionsFalse, orderBy: 'ASCENDING' }) + .then((res) => { + expect(reqArg.url).toContain('order_by=ASCENDING'); + done(); + }) + .catch((err) => { + done(err); + }); + }); + + it('should not include trailing & when all optional params are absent', (done) => { + let reqArg; + jest.spyOn(ClientModule, 'default').mockImplementation(() => ({ + request: (args) => { + reqArg = args; + return Promise.resolve(getSuccessResponse); + }, + })); + + const testSkyflowClient = new Skyflow({ + vaultID: '1234', + vaultURL: 'https://url.com', + getBearerToken: () => Promise.resolve('valid_token'), + }); + + jest.spyOn(testSkyflowClient, 'getAccessToken').mockResolvedValue('valid token'); + + fetchRecordsGET(testSkyflowClient, [getRecordID], optionsFalse) + .then((res) => { + expect(reqArg.url).not.toMatch(/&$/); + done(); + }) + .catch((err) => { + done(err); + }); + }); + + it('should include all new params together in url', (done) => { + let reqArg; + jest.spyOn(ClientModule, 'default').mockImplementation(() => ({ + request: (args) => { + reqArg = args; + return Promise.resolve(getSuccessResponse); + }, + })); + + const testSkyflowClient = new Skyflow({ + vaultID: '1234', + vaultURL: 'https://url.com', + getBearerToken: () => Promise.resolve('valid_token'), + }); + + jest.spyOn(testSkyflowClient, 'getAccessToken').mockResolvedValue('valid token'); + + const opts = { + ...optionsFalse, + fields: ['name', 'dob'], + offset: '0', + limit: '25', + downloadURL: false, + orderBy: 'DESCENDING', + }; + + fetchRecordsGET(testSkyflowClient, [getRecordID], opts) + .then((res) => { + expect(reqArg.url).toContain('fields=name'); + expect(reqArg.url).toContain('fields=dob'); + expect(reqArg.url).toContain('offset=0'); + expect(reqArg.url).toContain('limit=25'); + expect(reqArg.url).toContain('downloadURL=false'); + expect(reqArg.url).toContain('order_by=DESCENDING'); + expect(reqArg.url).not.toMatch(/&$/); + done(); + }) + .catch((err) => { + done(err); + }); + }); + + it('should include order_by=NONE in url when orderBy is NONE', (done) => { + let reqArg; + jest.spyOn(ClientModule, 'default').mockImplementation(() => ({ + request: (args) => { + reqArg = args; + return Promise.resolve(getSuccessResponse); + }, + })); + + const testSkyflowClient = new Skyflow({ + vaultID: '1234', + vaultURL: 'https://url.com', + getBearerToken: () => Promise.resolve('valid_token'), + }); + + jest.spyOn(testSkyflowClient, 'getAccessToken').mockResolvedValue('valid token'); + + fetchRecordsGET(testSkyflowClient, [getRecordID], { + ...optionsFalse, + orderBy: 'NONE', + }) + .then((res) => { + expect(reqArg.url).toContain('order_by=NONE'); + done(); + }) + .catch((err) => { + done(err); + }); + }); + + it('should not include new params in url when only fields is set', (done) => { + let reqArg; + jest.spyOn(ClientModule, 'default').mockImplementation(() => ({ + request: (args) => { + reqArg = args; + return Promise.resolve(getSuccessResponse); + }, + })); + + const testSkyflowClient = new Skyflow({ + vaultID: '1234', + vaultURL: 'https://url.com', + getBearerToken: () => Promise.resolve('valid_token'), + }); + + jest + .spyOn(testSkyflowClient, 'getAccessToken') + .mockResolvedValue('valid token'); + + fetchRecordsGET(testSkyflowClient, [getRecordID], { + ...optionsFalse, + fields: ['name'], + }) + .then((res) => { + expect(reqArg.url).toContain('fields=name'); + expect(reqArg.url).not.toContain('offset'); + expect(reqArg.url).not.toContain('limit'); + expect(reqArg.url).not.toContain('downloadURL'); + expect(reqArg.url).not.toContain('order_by'); + done(); + }) + .catch((err) => { + done(err); + }); + }); }); diff --git a/example/src/RevealElements.tsx b/example/src/RevealElements.tsx index 0cd6cab..cf90c96 100644 --- a/example/src/RevealElements.tsx +++ b/example/src/RevealElements.tsx @@ -5,6 +5,7 @@ import React from 'react'; import { Button, StyleSheet, View } from 'react-native'; import { + OrderBy, RedactionType, RevealElement, useRevealContainer, @@ -27,47 +28,90 @@ const RevealElements = (props) => { }; const handleGet = () =>{ - const getRecord1 = { - ids: [ - '', - '', - ], + const getRecordByIds = { + ids: ['', ''], table: 'cards', }; - const getRecord2 = { + const getRecordByColumn = { table: 'cards', columnName: '', columnValues: ['', ''], }; - const getRequest1 = { records: [getRecord1] }; + // Get tokens by Skyflow ID + skyflowContainer + .get( + { records: [getRecordByIds] }, + { tokens: true } + ) + .then((response) => { + console.log('Get tokens Success', JSON.stringify(response)); + }) + .catch((err) => { + console.error('Get tokens Failed', JSON.stringify(err)); + }); - const getRequest2 = { - records: [ - { ...getRecord1, redaction: RedactionType.PLAIN_TEXT }, - { ...getRecord2, redaction: RedactionType.PLAIN_TEXT }, - ], - }; + // Get plain text by Skyflow ID and column values + skyflowContainer + .get( + { + records: [ + { ...getRecordByIds, redaction: RedactionType.PLAIN_TEXT }, + { ...getRecordByColumn, redaction: RedactionType.PLAIN_TEXT }, + ], + }, + { tokens: false } + ) + .then((response) => { + console.log('Get plain text Success', JSON.stringify(response)); + }) + .catch((err) => { + console.error('Get plain text Failed', JSON.stringify(err)); + }); - // Get Tokens by Skyflow ID + // Get specific fields only (column-scoped policy compatible) skyflowContainer - .get(getRequest1, { tokens: true }) + .get( + { + records: [ + { ...getRecordByIds, redaction: RedactionType.PLAIN_TEXT }, + ], + }, + { + tokens: false, + fields: ['occupation', 'annual_income'], + } + ) .then((response) => { - console.log('Get request 1 Success', JSON.stringify(response)); + console.log('Get with fields Success', JSON.stringify(response)); }) .catch((err) => { - console.error('Get request 1 Failed', JSON.stringify(err)); + console.error('Get with fields Failed', JSON.stringify(err)); }); - // get by Skyflow ID and Column Values + // Get with pagination and ordering skyflowContainer - .get(getRequest2, { tokens: false }) + .get( + { + records: [ + { ...getRecordByColumn, redaction: RedactionType.PLAIN_TEXT }, + ], + }, + { + tokens: false, + fields: ['name', 'email'], + offset: '0', + limit: '10', + orderBy: OrderBy.ASCENDING, + downloadURL: false, + } + ) .then((response) => { - console.log('Get request 2 Success', JSON.stringify(response)); + console.log('Get with pagination Success', JSON.stringify(response)); }) .catch((err) => { - console.error('Get request 2 Failed', JSON.stringify(err)); + console.error('Get with pagination Failed', JSON.stringify(err)); }); }; diff --git a/src/core-utils/element-validations/index.ts b/src/core-utils/element-validations/index.ts index 540a434..d867d3f 100644 --- a/src/core-utils/element-validations/index.ts +++ b/src/core-utils/element-validations/index.ts @@ -14,6 +14,7 @@ import { IGetInput, IGetOptions, IInsertRecordInput, + OrderBy, RedactionType, RevealElementInput, } from '../../utils/constants'; @@ -444,6 +445,55 @@ export const validateGetInput = ( throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_TOKENS_IN_GET); } + if ( + options && + Object.prototype.hasOwnProperty.call(options, 'offset') && + typeof options.offset !== 'string' + ) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_OFFSET_IN_GET); + } + + if ( + options && + Object.prototype.hasOwnProperty.call(options, 'limit') && + typeof options.limit !== 'string' + ) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_LIMIT_IN_GET); + } + + if ( + options && + Object.prototype.hasOwnProperty.call(options, 'downloadURL') && + typeof options.downloadURL !== 'boolean' + ) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_DOWNLOAD_URL_IN_GET); + } + + if ( + options && + Object.prototype.hasOwnProperty.call(options, 'orderBy') && + !Object.values(OrderBy).includes(options.orderBy as OrderBy) + ) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_ORDER_BY_IN_GET); + } + + if (options && Object.prototype.hasOwnProperty.call(options, 'fields')) { + if (!Array.isArray(options.fields)) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FIELDS_IN_GET); + } + if (options.fields.length === 0) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_FIELDS_IN_GET); + } + (options.fields as any[]).forEach((field: any) => { + if (typeof field !== 'string') { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FIELD_VALUE_IN_GET); + } + if (!field) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_FIELD_VALUE_IN_GET); + } + }); + } + records.forEach((record: any, index: number) => { if (Object.keys(record).length === 0) { throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_RECORDS_GET); diff --git a/src/core-utils/reveal/index.ts b/src/core-utils/reveal/index.ts index 9433756..78c20eb 100644 --- a/src/core-utils/reveal/index.ts +++ b/src/core-utils/reveal/index.ts @@ -296,7 +296,31 @@ export const getRecordsFromVault = ( } if (getRecord?.redaction) { - paramList += `redaction=${getRecord.redaction}`; + paramList += `redaction=${getRecord.redaction}&`; + } + + options?.fields?.forEach((field) => { + paramList += `fields=${field}&`; + }); + + if (options && Object.prototype.hasOwnProperty.call(options, 'offset')) { + paramList += `offset=${options.offset}&`; + } + + if (options && Object.prototype.hasOwnProperty.call(options, 'limit')) { + paramList += `limit=${options.limit}&`; + } + + if (options && Object.prototype.hasOwnProperty.call(options, 'downloadURL')) { + paramList += `downloadURL=${options.downloadURL}&`; + } + + if (options && Object.prototype.hasOwnProperty.call(options, 'orderBy')) { + paramList += `order_by=${options.orderBy}&`; + } + + if (paramList.endsWith('&')) { + paramList = paramList.slice(0, -1); } const vault = config.vaultURL; diff --git a/src/index.ts b/src/index.ts index 009b7dc..eea484b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ export { ElementType, Env, LogLevel, + OrderBy, ValidationRuleType, RedactionType, SkyflowRevealElementRef, diff --git a/src/utils/constants/index.ts b/src/utils/constants/index.ts index 406745e..b479620 100644 --- a/src/utils/constants/index.ts +++ b/src/utils/constants/index.ts @@ -245,6 +245,12 @@ export enum ContainerType { COMPOSABLE = 'COMPOSABLE', } +export enum OrderBy { + ASCENDING = 'ASCENDING', + DESCENDING = 'DESCENDING', + NONE = 'NONE', +} + export const PUREJS_TYPES = { GET: 'GET', }; @@ -263,6 +269,11 @@ export interface IGetInput { export interface IGetOptions { tokens?: Boolean; + fields?: string[]; + offset?: string; + limit?: string; + downloadURL?: boolean; + orderBy?: OrderBy; } export const CARD_ICON_DEFAULT_STYLE = { diff --git a/src/utils/logs/index.ts b/src/utils/logs/index.ts index 81c9bb3..25f683f 100644 --- a/src/utils/logs/index.ts +++ b/src/utils/logs/index.ts @@ -215,6 +215,22 @@ const logs = { EMPTY_COLUMN_VALUE: `${SDK_NAME_VERSION} Validation error. Column Value is empty in records at index %s1`, MISSING_IDS_OR_COLUMN_VALUES_IN_GET: `${SDK_NAME_VERSION} Validation error. Both 'ids' or 'columnValues' keys are missing. Either provide 'ids' or 'columnValues' with 'columnName' to fetch records.`, + INVALID_FIELDS_IN_GET: + `${SDK_NAME_VERSION} Validation error. Invalid 'fields' in get options. Specify a value of type array instead.`, + EMPTY_FIELDS_IN_GET: + `${SDK_NAME_VERSION} Validation error. 'fields' in get options cannot be empty. Specify a non-empty array instead.`, + EMPTY_FIELD_VALUE_IN_GET: + `${SDK_NAME_VERSION} Validation error. 'fields' in get options contains an empty string. Specify non-empty string values.`, + INVALID_FIELD_VALUE_IN_GET: + `${SDK_NAME_VERSION} Validation error. 'fields' in get options contains a non-string value. Specify string values instead.`, + INVALID_OFFSET_IN_GET: + `${SDK_NAME_VERSION} Validation error. Invalid 'offset' in get options. Specify a value of type string instead.`, + INVALID_LIMIT_IN_GET: + `${SDK_NAME_VERSION} Validation error. Invalid 'limit' in get options. Specify a value of type string instead.`, + INVALID_DOWNLOAD_URL_IN_GET: + `${SDK_NAME_VERSION} Validation error. Invalid 'downloadURL' in get options. Specify a boolean value instead.`, + INVALID_ORDER_BY_IN_GET: + `${SDK_NAME_VERSION} Validation error. Invalid 'orderBy' in get options. Specify a valid OrderBy value (ASCENDING, DESCENDING, NONE).`, SKYFLOW_IDS_AND_COLUMN_NAME_BOTH_SPECIFIED: `${SDK_NAME_VERSION} Validation error. ids and columnName can not be specified together.`, GET_BY_SKYFLOWID_RESOLVED: '%s1 - GetById request is resolved.', diff --git a/src/utils/skyflow-error-code/index.ts b/src/utils/skyflow-error-code/index.ts index 9cb8163..4deda00 100644 --- a/src/utils/skyflow-error-code/index.ts +++ b/src/utils/skyflow-error-code/index.ts @@ -330,6 +330,38 @@ const SKYFLOW_ERROR_CODE = { code: 400, description: logs.errorLogs.MISSING_IDS_OR_COLUMN_VALUES_IN_GET, }, + INVALID_FIELDS_IN_GET: { + code: 400, + description: logs.errorLogs.INVALID_FIELDS_IN_GET, + }, + EMPTY_FIELDS_IN_GET: { + code: 400, + description: logs.errorLogs.EMPTY_FIELDS_IN_GET, + }, + EMPTY_FIELD_VALUE_IN_GET: { + code: 400, + description: logs.errorLogs.EMPTY_FIELD_VALUE_IN_GET, + }, + INVALID_FIELD_VALUE_IN_GET: { + code: 400, + description: logs.errorLogs.INVALID_FIELD_VALUE_IN_GET, + }, + INVALID_OFFSET_IN_GET: { + code: 400, + description: logs.errorLogs.INVALID_OFFSET_IN_GET, + }, + INVALID_LIMIT_IN_GET: { + code: 400, + description: logs.errorLogs.INVALID_LIMIT_IN_GET, + }, + INVALID_DOWNLOAD_URL_IN_GET: { + code: 400, + description: logs.errorLogs.INVALID_DOWNLOAD_URL_IN_GET, + }, + INVALID_ORDER_BY_IN_GET: { + code: 400, + description: logs.errorLogs.INVALID_ORDER_BY_IN_GET, + }, MISSING_RECORD_COLUMN_VALUE: { code: 400, description: logs.errorLogs.MISSING_RECORD_COLUMN_VALUE, From 0b0c9d10ff7ab37d18fb928e5e321fe37f6b20aa Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Tue, 2 Jun 2026 17:25:17 +0530 Subject: [PATCH 02/14] SK-2658 add all query params available in get --- src/core-utils/element-validations/index.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/core-utils/element-validations/index.ts b/src/core-utils/element-validations/index.ts index d867d3f..cd01f32 100644 --- a/src/core-utils/element-validations/index.ts +++ b/src/core-utils/element-validations/index.ts @@ -498,17 +498,15 @@ export const validateGetInput = ( if (Object.keys(record).length === 0) { throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_RECORDS_GET); } - if (record.ids?.length === 0) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_IDS_IN_GET, [`${index}`]); - } if (record.ids != null && !(record.ids && Array.isArray(record.ids))) { throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_IDS_IN_GET, [ `${index}`, ]); } - record.ids?.forEach((skyflowId) => { + if (record.ids && record.ids.length > 0) { + record.ids?.forEach((skyflowId) => { if (!skyflowId) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_SKYFLOWID_IN_GET, [ + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_SKYFLOWID_IN_GET, [ `${index}`, ]); } @@ -519,6 +517,7 @@ export const validateGetInput = ( ); } }); + } if (!Object.prototype.hasOwnProperty.call(record, 'table')) { throw new SkyflowError(SKYFLOW_ERROR_CODE.MISSING_TABLE_IN_GET, [ `${index}`, From 2fa5c81f733e5236f9da3ba4ea0b2001af2e4dd2 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Tue, 2 Jun 2026 17:27:46 +0530 Subject: [PATCH 03/14] SK-2658 fix tests --- __tests__/core-utils/element-validations.test.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/__tests__/core-utils/element-validations.test.js b/__tests__/core-utils/element-validations.test.js index 8ff754e..bf5744d 100644 --- a/__tests__/core-utils/element-validations.test.js +++ b/__tests__/core-utils/element-validations.test.js @@ -328,12 +328,15 @@ describe('test validateGetInput', () => { } }); - it('should throw error for empty ids', () => { + it('should throw error for missing table when ids is empty array', () => { try { validateGetInput({ records: [{ ids: [] }] }); } catch (err) { expect(err?.errors[0]?.description).toEqual( - parameterizedString(SKYFLOW_ERROR_CODE.EMPTY_IDS_IN_GET.description, 0) + parameterizedString( + SKYFLOW_ERROR_CODE.MISSING_TABLE_IN_GET.description, + 0 + ) ); } }); From 68a09302d8cee9eed3576395ca592bb69b0a85fd Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Wed, 3 Jun 2026 13:24:28 +0530 Subject: [PATCH 04/14] SK-2658 move per record param in request from options --- .../core-utils/element-validations.test.js | 172 ++++++++++++------ __tests__/core-utils/reveal.test.js | 26 ++- src/core-utils/element-validations/index.ts | 82 +++++---- src/core-utils/reveal/index.ts | 10 +- src/utils/constants/index.ts | 6 +- src/utils/logs/index.ts | 12 +- 6 files changed, 188 insertions(+), 120 deletions(-) diff --git a/__tests__/core-utils/element-validations.test.js b/__tests__/core-utils/element-validations.test.js index bf5744d..9e38b48 100644 --- a/__tests__/core-utils/element-validations.test.js +++ b/__tests__/core-utils/element-validations.test.js @@ -1000,54 +1000,90 @@ describe('test get options validation', () => { } }); - it('should throw error for non-string offset in get options', () => { + it('should throw error for non-string offset in record', () => { try { - validateGetInput( - { records: [{ ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT }] }, - { offset: 10 } - ); + validateGetInput({ + records: [ + { + ids: ['123'], + table: 'test', + redaction: RedactionType.DEFAULT, + offset: 10, + }, + ], + }); } catch (err) { expect(err?.errors[0]?.description).toEqual( - SKYFLOW_ERROR_CODE.INVALID_OFFSET_IN_GET.description + parameterizedString( + SKYFLOW_ERROR_CODE.INVALID_OFFSET_IN_GET.description, + 0 + ) ); } }); - it('should throw error for null offset in get options', () => { + it('should throw error for null offset in record', () => { try { - validateGetInput( - { records: [{ ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT }] }, - { offset: null } - ); + validateGetInput({ + records: [ + { + ids: ['123'], + table: 'test', + redaction: RedactionType.DEFAULT, + offset: null, + }, + ], + }); } catch (err) { expect(err?.errors[0]?.description).toEqual( - SKYFLOW_ERROR_CODE.INVALID_OFFSET_IN_GET.description + parameterizedString( + SKYFLOW_ERROR_CODE.INVALID_OFFSET_IN_GET.description, + 0 + ) ); } }); - it('should throw error for non-string limit in get options', () => { + it('should throw error for non-string limit in record', () => { try { - validateGetInput( - { records: [{ ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT }] }, - { limit: true } - ); + validateGetInput({ + records: [ + { + ids: ['123'], + table: 'test', + redaction: RedactionType.DEFAULT, + limit: true, + }, + ], + }); } catch (err) { expect(err?.errors[0]?.description).toEqual( - SKYFLOW_ERROR_CODE.INVALID_LIMIT_IN_GET.description + parameterizedString( + SKYFLOW_ERROR_CODE.INVALID_LIMIT_IN_GET.description, + 0 + ) ); } }); - it('should throw error for null limit in get options', () => { + it('should throw error for null limit in record', () => { try { - validateGetInput( - { records: [{ ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT }] }, - { limit: null } - ); + validateGetInput({ + records: [ + { + ids: ['123'], + table: 'test', + redaction: RedactionType.DEFAULT, + limit: null, + }, + ], + }); } catch (err) { expect(err?.errors[0]?.description).toEqual( - SKYFLOW_ERROR_CODE.INVALID_LIMIT_IN_GET.description + parameterizedString( + SKYFLOW_ERROR_CODE.INVALID_LIMIT_IN_GET.description, + 0 + ) ); } }); @@ -1117,12 +1153,19 @@ describe('test get options validation', () => { } }); - it('should not throw for valid offset and limit in get options', () => { + it('should not throw for valid offset and limit in record', () => { expect(() => - validateGetInput( - { records: [{ ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT }] }, - { offset: '5', limit: '10' } - ) + validateGetInput({ + records: [ + { + ids: ['123'], + table: 'test', + redaction: RedactionType.DEFAULT, + offset: '5', + limit: '10', + }, + ], + }) ).not.toThrow(); }); @@ -1163,81 +1206,92 @@ describe('test get options validation', () => { }); }); -describe('test validateGetInput fields in options', () => { +describe('test validateGetInput fields in record', () => { const validRecord = { ids: ['123'], table: 'test', redaction: RedactionType.PLAIN_TEXT }; - it('should throw error for null fields in options', () => { + it('should throw error for null fields in record', () => { try { - validateGetInput({ records: [validRecord] }, { fields: null }); + validateGetInput({ records: [{ ...validRecord, fields: null }] }); } catch (err) { expect(err?.errors[0]?.description).toEqual( - SKYFLOW_ERROR_CODE.INVALID_FIELDS_IN_GET.description + parameterizedString( + SKYFLOW_ERROR_CODE.INVALID_FIELDS_IN_GET.description, + 0 + ) ); } }); - it('should throw error for non-array fields in options', () => { + it('should throw error for non-array fields in record', () => { try { - validateGetInput( - { records: [validRecord] }, - { fields: 'occupation' } - ); + validateGetInput({ records: [{ ...validRecord, fields: 'occupation' }] }); } catch (err) { expect(err?.errors[0]?.description).toEqual( - SKYFLOW_ERROR_CODE.INVALID_FIELDS_IN_GET.description + parameterizedString( + SKYFLOW_ERROR_CODE.INVALID_FIELDS_IN_GET.description, + 0 + ) ); } }); - it('should throw error for empty fields array in options', () => { + it('should throw error for empty fields array in record', () => { try { - validateGetInput( - { records: [validRecord] }, - { fields: [] } - ); + validateGetInput({ records: [{ ...validRecord, fields: [] }] }); } catch (err) { expect(err?.errors[0]?.description).toEqual( - SKYFLOW_ERROR_CODE.EMPTY_FIELDS_IN_GET.description + parameterizedString( + SKYFLOW_ERROR_CODE.EMPTY_FIELDS_IN_GET.description, + 0 + ) ); } }); - it('should throw error for non-string value inside fields array in options', () => { + it('should throw error for non-string value inside fields array in record', () => { try { - validateGetInput({ records: [validRecord] }, { fields: [123] }); + validateGetInput({ records: [{ ...validRecord, fields: [123] }] }); } catch (err) { expect(err?.errors[0]?.description).toEqual( - SKYFLOW_ERROR_CODE.INVALID_FIELD_VALUE_IN_GET.description + parameterizedString( + SKYFLOW_ERROR_CODE.INVALID_FIELD_VALUE_IN_GET.description, + 0 + ) ); } }); - it('should throw error for null value inside fields array in options', () => { + it('should throw error for null value inside fields array in record', () => { try { - validateGetInput({ records: [validRecord] }, { fields: [null] }); + validateGetInput({ records: [{ ...validRecord, fields: [null] }] }); } catch (err) { expect(err?.errors[0]?.description).toEqual( - SKYFLOW_ERROR_CODE.INVALID_FIELD_VALUE_IN_GET.description + parameterizedString( + SKYFLOW_ERROR_CODE.INVALID_FIELD_VALUE_IN_GET.description, + 0 + ) ); } }); - it('should throw error for empty string inside fields array in options', () => { + it('should throw error for empty string inside fields array in record', () => { try { - validateGetInput({ records: [validRecord] }, { fields: [''] }); + validateGetInput({ records: [{ ...validRecord, fields: [''] }] }); } catch (err) { expect(err?.errors[0]?.description).toEqual( - SKYFLOW_ERROR_CODE.EMPTY_FIELD_VALUE_IN_GET.description + parameterizedString( + SKYFLOW_ERROR_CODE.EMPTY_FIELD_VALUE_IN_GET.description, + 0 + ) ); } }); - it('should not throw for valid fields array in options', () => { + it('should not throw for valid fields array in record', () => { expect(() => - validateGetInput( - { records: [validRecord] }, - { fields: ['occupation', 'annual_income'] } - ) + validateGetInput({ + records: [{ ...validRecord, fields: ['occupation', 'annual_income'] }], + }) ).not.toThrow(); }); }); diff --git a/__tests__/core-utils/reveal.test.js b/__tests__/core-utils/reveal.test.js index 4a8bd98..dfdd730 100644 --- a/__tests__/core-utils/reveal.test.js +++ b/__tests__/core-utils/reveal.test.js @@ -693,7 +693,11 @@ describe('fetchRecordGET fn test', () => { jest.spyOn(testSkyflowClient, 'getAccessToken').mockResolvedValue('valid token'); - fetchRecordsGET(testSkyflowClient, [getRecordID], { ...optionsFalse, fields: ['occupation', 'annual_income'] }) + const recordWithFields = { + ...getRecordID, + fields: ['occupation', 'annual_income'], + }; + fetchRecordsGET(testSkyflowClient, [recordWithFields], optionsFalse) .then((res) => { expect(reqArg.url).toContain('fields=occupation'); expect(reqArg.url).toContain('fields=annual_income'); @@ -704,7 +708,7 @@ describe('fetchRecordGET fn test', () => { }); }); - it('should include offset and limit params in url when specified in options', (done) => { + it('should include offset and limit params in url when specified in record', (done) => { let reqArg; jest.spyOn(ClientModule, 'default').mockImplementation(() => ({ request: (args) => { @@ -721,7 +725,8 @@ describe('fetchRecordGET fn test', () => { jest.spyOn(testSkyflowClient, 'getAccessToken').mockResolvedValue('valid token'); - fetchRecordsGET(testSkyflowClient, [getRecordID], { ...optionsFalse, offset: '5', limit: '10' }) + const recordWithPagination = { ...getRecordID, offset: '5', limit: '10' }; + fetchRecordsGET(testSkyflowClient, [recordWithPagination], optionsFalse) .then((res) => { expect(reqArg.url).toContain('offset=5'); expect(reqArg.url).toContain('limit=10'); @@ -830,16 +835,19 @@ describe('fetchRecordGET fn test', () => { jest.spyOn(testSkyflowClient, 'getAccessToken').mockResolvedValue('valid token'); - const opts = { - ...optionsFalse, + const recordWithFields = { + ...getRecordID, fields: ['name', 'dob'], offset: '0', limit: '25', + }; + const opts = { + ...optionsFalse, downloadURL: false, orderBy: 'DESCENDING', }; - fetchRecordsGET(testSkyflowClient, [getRecordID], opts) + fetchRecordsGET(testSkyflowClient, [recordWithFields], opts) .then((res) => { expect(reqArg.url).toContain('fields=name'); expect(reqArg.url).toContain('fields=dob'); @@ -904,10 +912,8 @@ describe('fetchRecordGET fn test', () => { .spyOn(testSkyflowClient, 'getAccessToken') .mockResolvedValue('valid token'); - fetchRecordsGET(testSkyflowClient, [getRecordID], { - ...optionsFalse, - fields: ['name'], - }) + const recordWithFields = { ...getRecordID, fields: ['name'] }; + fetchRecordsGET(testSkyflowClient, [recordWithFields], optionsFalse) .then((res) => { expect(reqArg.url).toContain('fields=name'); expect(reqArg.url).not.toContain('offset'); diff --git a/src/core-utils/element-validations/index.ts b/src/core-utils/element-validations/index.ts index cd01f32..fe84c26 100644 --- a/src/core-utils/element-validations/index.ts +++ b/src/core-utils/element-validations/index.ts @@ -445,22 +445,6 @@ export const validateGetInput = ( throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_TOKENS_IN_GET); } - if ( - options && - Object.prototype.hasOwnProperty.call(options, 'offset') && - typeof options.offset !== 'string' - ) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_OFFSET_IN_GET); - } - - if ( - options && - Object.prototype.hasOwnProperty.call(options, 'limit') && - typeof options.limit !== 'string' - ) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_LIMIT_IN_GET); - } - if ( options && Object.prototype.hasOwnProperty.call(options, 'downloadURL') && @@ -477,23 +461,6 @@ export const validateGetInput = ( throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_ORDER_BY_IN_GET); } - if (options && Object.prototype.hasOwnProperty.call(options, 'fields')) { - if (!Array.isArray(options.fields)) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FIELDS_IN_GET); - } - if (options.fields.length === 0) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_FIELDS_IN_GET); - } - (options.fields as any[]).forEach((field: any) => { - if (typeof field !== 'string') { - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FIELD_VALUE_IN_GET); - } - if (!field) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_FIELD_VALUE_IN_GET); - } - }); - } - records.forEach((record: any, index: number) => { if (Object.keys(record).length === 0) { throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_RECORDS_GET); @@ -503,10 +470,9 @@ export const validateGetInput = ( `${index}`, ]); } - if (record.ids && record.ids.length > 0) { - record.ids?.forEach((skyflowId) => { + record.ids?.forEach((skyflowId) => { if (!skyflowId) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_SKYFLOWID_IN_GET, [ + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_SKYFLOWID_IN_GET, [ `${index}`, ]); } @@ -517,7 +483,6 @@ export const validateGetInput = ( ); } }); - } if (!Object.prototype.hasOwnProperty.call(record, 'table')) { throw new SkyflowError(SKYFLOW_ERROR_CODE.MISSING_TABLE_IN_GET, [ `${index}`, @@ -651,5 +616,48 @@ export const validateGetInput = ( ); } } + + if ( + Object.prototype.hasOwnProperty.call(record, 'offset') && + typeof record.offset !== 'string' + ) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_OFFSET_IN_GET, [ + `${index}`, + ]); + } + + if ( + Object.prototype.hasOwnProperty.call(record, 'limit') && + typeof record.limit !== 'string' + ) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_LIMIT_IN_GET, [ + `${index}`, + ]); + } + + if (Object.prototype.hasOwnProperty.call(record, 'fields')) { + if (!Array.isArray(record.fields)) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FIELDS_IN_GET, [ + `${index}`, + ]); + } + if (record.fields.length === 0) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_FIELDS_IN_GET, [ + `${index}`, + ]); + } + record.fields.forEach((field: any) => { + if (typeof field !== 'string') { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FIELD_VALUE_IN_GET, [ + `${index}`, + ]); + } + if (!field) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_FIELD_VALUE_IN_GET, [ + `${index}`, + ]); + } + }); + } }); }; diff --git a/src/core-utils/reveal/index.ts b/src/core-utils/reveal/index.ts index 78c20eb..aca72c8 100644 --- a/src/core-utils/reveal/index.ts +++ b/src/core-utils/reveal/index.ts @@ -299,16 +299,16 @@ export const getRecordsFromVault = ( paramList += `redaction=${getRecord.redaction}&`; } - options?.fields?.forEach((field) => { + getRecord.fields?.forEach((field) => { paramList += `fields=${field}&`; }); - if (options && Object.prototype.hasOwnProperty.call(options, 'offset')) { - paramList += `offset=${options.offset}&`; + if (getRecord.offset !== undefined) { + paramList += `offset=${getRecord.offset}&`; } - if (options && Object.prototype.hasOwnProperty.call(options, 'limit')) { - paramList += `limit=${options.limit}&`; + if (getRecord.limit !== undefined) { + paramList += `limit=${getRecord.limit}&`; } if (options && Object.prototype.hasOwnProperty.call(options, 'downloadURL')) { diff --git a/src/utils/constants/index.ts b/src/utils/constants/index.ts index b479620..9b58ab6 100644 --- a/src/utils/constants/index.ts +++ b/src/utils/constants/index.ts @@ -261,6 +261,9 @@ export interface IGetRecord { table: string; columnName?: string; columnValues?: string[]; + fields?: string[]; + offset?: string; + limit?: string; } export interface IGetInput { @@ -269,9 +272,6 @@ export interface IGetInput { export interface IGetOptions { tokens?: Boolean; - fields?: string[]; - offset?: string; - limit?: string; downloadURL?: boolean; orderBy?: OrderBy; } diff --git a/src/utils/logs/index.ts b/src/utils/logs/index.ts index 25f683f..a40e1e5 100644 --- a/src/utils/logs/index.ts +++ b/src/utils/logs/index.ts @@ -216,17 +216,17 @@ const logs = { MISSING_IDS_OR_COLUMN_VALUES_IN_GET: `${SDK_NAME_VERSION} Validation error. Both 'ids' or 'columnValues' keys are missing. Either provide 'ids' or 'columnValues' with 'columnName' to fetch records.`, INVALID_FIELDS_IN_GET: - `${SDK_NAME_VERSION} Validation error. Invalid 'fields' in get options. Specify a value of type array instead.`, + `${SDK_NAME_VERSION} Validation error. Invalid 'fields' key in records at index %s1. Specify a value of type array instead.`, EMPTY_FIELDS_IN_GET: - `${SDK_NAME_VERSION} Validation error. 'fields' in get options cannot be empty. Specify a non-empty array instead.`, + `${SDK_NAME_VERSION} Validation error. 'fields' key cannot be empty in records at index %s1. Specify a non-empty array instead.`, EMPTY_FIELD_VALUE_IN_GET: - `${SDK_NAME_VERSION} Validation error. 'fields' in get options contains an empty string. Specify non-empty string values.`, + `${SDK_NAME_VERSION} Validation error. 'fields' array contains an empty string in records at index %s1. Specify non-empty string values.`, INVALID_FIELD_VALUE_IN_GET: - `${SDK_NAME_VERSION} Validation error. 'fields' in get options contains a non-string value. Specify string values instead.`, + `${SDK_NAME_VERSION} Validation error. 'fields' array contains a non-string value in records at index %s1. Specify string values instead.`, INVALID_OFFSET_IN_GET: - `${SDK_NAME_VERSION} Validation error. Invalid 'offset' in get options. Specify a value of type string instead.`, + `${SDK_NAME_VERSION} Validation error. Invalid 'offset' in records at index %s1. Specify a value of type string instead.`, INVALID_LIMIT_IN_GET: - `${SDK_NAME_VERSION} Validation error. Invalid 'limit' in get options. Specify a value of type string instead.`, + `${SDK_NAME_VERSION} Validation error. Invalid 'limit' in records at index %s1. Specify a value of type string instead.`, INVALID_DOWNLOAD_URL_IN_GET: `${SDK_NAME_VERSION} Validation error. Invalid 'downloadURL' in get options. Specify a boolean value instead.`, INVALID_ORDER_BY_IN_GET: From 978f701050c1819430b289251dd5872de14438ff Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Wed, 3 Jun 2026 14:03:09 +0530 Subject: [PATCH 05/14] SK-2658 update README and Samples --- README.md | 98 ++++++++++++++++++++++++++++++---- example/src/RevealElements.tsx | 22 ++++---- 2 files changed, 100 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 4b66f17..3259186 100644 --- a/README.md +++ b/README.md @@ -1396,36 +1396,54 @@ For non-PCI use-cases, retrieving data from the vault and revealing it in the mo - #### Using Skyflow ID's or Unique Column Values For retrieving data from the vault, use the `get(getInput: IGetInput, options: IGetOptions)` method. - The `getInput` parameter takes an object that contains an array of the records to fetch. Each object inside array should contain: + The `getInput` parameter takes an object that contains an array of the records to fetch. Each object inside the array should contain: - Either an array of Skyflow IDs to fetch - Or a column name and an array of column values - The second parameter, `options`, is a `GetOptions` object that retrieves tokens of Skyflow IDs. + The second parameter, `options`, is a `GetOptions` object that controls how records are retrieved. - Notes: - - You can use either Skyflow IDs or unique values to retrieve records. You can't use both at the same time. - - GetOptions parameter is applicable only for retrieving tokens using Skyflow ID. - - You can't pass GetOptions along with the redaction type. + Notes: + - You can use either Skyflow IDs or unique column values to retrieve records. You can't use both at the same time. + - `tokens: true` is only applicable when fetching by Skyflow IDs and cannot be combined with `redaction`. + - Use `fields` to restrict which columns are returned — required when column-scoped policies limit access to certain fields. + - `offset` and `limit` are per-record and support pagination across results. + - `orderBy` accepts `OrderBy.ASCENDING`, `OrderBy.DESCENDING`, or `OrderBy.NONE`. - `tokens` defaults to false. - + ```json5 { - "records":[ + "records": [ { "ids": Array, // Array of SkyflowID's of the records to be fetched "table": String, // name of table holding the above skyflow_id's - "redaction": RedactionType // redaction to be applied to retrieved data + "redaction": RedactionType, // redaction to be applied to retrieved data + "fields": Array, // (optional) columns to return; omit to return all permitted columns + "offset": String, // (optional) pagination start position + "limit": String // (optional) max number of records to return }, { "table": String, // name of table from where records are to be fetched "redaction": RedactionType, // redaction to be applied to retrieved data "columnName": String, // a unique column name - "columnValues": Array // Array of Column Values of the records to be fetched + "columnValues": Array, // Array of Column Values of the records to be fetched + "fields": Array, // (optional) columns to return; omit to return all permitted columns + "offset": String, // (optional) pagination start position + "limit": String // (optional) max number of records to return } ] } ``` + + ```json5 + // options + { + "tokens": Boolean, // if true, returns tokens instead of plain-text values + "downloadURL": Boolean, // (optional) if true, returns pre-signed download URLs for file columns + "orderBy": OrderBy // (optional) sort order: OrderBy.ASCENDING | OrderBy.DESCENDING | OrderBy.NONE + } + ``` + An Example of Get call to fetch records ```js @@ -1527,11 +1545,69 @@ For non-PCI use-cases, retrieving data from the vault and revealing it in the mo "code": "404", "description": "No Records Found" }, - "columnName": "email", + "columnName": "email" } ] } ``` + + An Example of Get call to fetch specific fields (column-scoped policy compatible) + + ```js + const skyflowContainer = useSkyflow(); + + const getRequestInput = { + records: [ + { + ids: ['h4f5k569-c577-8o1j-r91c-x9gfd0b0fd9'], + table: 'persons', + redaction: RedactionType.PLAIN_TEXT, + fields: ['occupation', 'annual_income'], + }, + ], + }; + + skyflowContainer + .get(getRequestInput, { tokens: false }) + .then((response) => { + console.log(JSON.stringify(response)); + }) + .catch((err) => { + console.error(JSON.stringify(err)); + }); + ``` + + An Example of Get call to fetch records with pagination and ordering + + ```js + import { useSkyflow, RedactionType, OrderBy } from 'skyflow-react-native'; + + const skyflowContainer = useSkyflow(); + + const getRequestInput = { + records: [ + { + table: 'customers', + redaction: RedactionType.PLAIN_TEXT, + columnName: 'email', + columnValues: ['john.doe@gmail.com'], + fields: ['name', 'email'], + offset: '0', + limit: '10', + }, + ], + }; + + skyflowContainer + .get(getRequestInput, { tokens: false, orderBy: OrderBy.ASCENDING }) + .then((response) => { + console.log(JSON.stringify(response)); + }) + .catch((err) => { + console.error(JSON.stringify(err)); + }); + ``` + An Example of Get call to fetch Tokens ```js diff --git a/example/src/RevealElements.tsx b/example/src/RevealElements.tsx index cf90c96..efd95f9 100644 --- a/example/src/RevealElements.tsx +++ b/example/src/RevealElements.tsx @@ -75,13 +75,14 @@ const RevealElements = (props) => { .get( { records: [ - { ...getRecordByIds, redaction: RedactionType.PLAIN_TEXT }, + { + ...getRecordByIds, + redaction: RedactionType.PLAIN_TEXT, + fields: ['occupation', 'annual_income'], + }, ], }, - { - tokens: false, - fields: ['occupation', 'annual_income'], - } + { tokens: false } ) .then((response) => { console.log('Get with fields Success', JSON.stringify(response)); @@ -95,14 +96,17 @@ const RevealElements = (props) => { .get( { records: [ - { ...getRecordByColumn, redaction: RedactionType.PLAIN_TEXT }, + { + ...getRecordByColumn, + redaction: RedactionType.PLAIN_TEXT, + fields: ['name', 'email'], + offset: '0', + limit: '10', + }, ], }, { tokens: false, - fields: ['name', 'email'], - offset: '0', - limit: '10', orderBy: OrderBy.ASCENDING, downloadURL: false, } From abfebc87f9a59b4260ad936d3b55da5b2ed962ae Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Wed, 3 Jun 2026 15:00:34 +0530 Subject: [PATCH 06/14] SK-2658 update tests --- .../core-utils/element-validations.test.js | 44 +++++ __tests__/core-utils/reveal.test.js | 167 ++++++++++++++++++ src/core-utils/element-validations/index.ts | 36 ++-- src/core-utils/reveal/index.ts | 15 +- src/utils/logs/index.ts | 4 + src/utils/skyflow-error-code/index.ts | 8 + 6 files changed, 254 insertions(+), 20 deletions(-) diff --git a/__tests__/core-utils/element-validations.test.js b/__tests__/core-utils/element-validations.test.js index 9e38b48..5915128 100644 --- a/__tests__/core-utils/element-validations.test.js +++ b/__tests__/core-utils/element-validations.test.js @@ -1088,6 +1088,50 @@ describe('test get options validation', () => { } }); + it('should throw error for empty string offset in record', () => { + try { + validateGetInput({ + records: [ + { + ids: ['123'], + table: 'test', + redaction: RedactionType.DEFAULT, + offset: '', + }, + ], + }); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + parameterizedString( + SKYFLOW_ERROR_CODE.EMPTY_OFFSET_IN_GET.description, + 0 + ) + ); + } + }); + + it('should throw error for empty string limit in record', () => { + try { + validateGetInput({ + records: [ + { + ids: ['123'], + table: 'test', + redaction: RedactionType.DEFAULT, + limit: '', + }, + ], + }); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + parameterizedString( + SKYFLOW_ERROR_CODE.EMPTY_LIMIT_IN_GET.description, + 0 + ) + ); + } + }); + it('should throw error for non-boolean downloadURL in get options', () => { try { validateGetInput( diff --git a/__tests__/core-utils/reveal.test.js b/__tests__/core-utils/reveal.test.js index dfdd730..345d7c3 100644 --- a/__tests__/core-utils/reveal.test.js +++ b/__tests__/core-utils/reveal.test.js @@ -676,6 +676,173 @@ describe('fetchRecordGET fn test', () => { }); }); + it('should call rootReject when Promise.all rejects', (done) => { + const promiseAllError = new Error('Promise.all internal failure'); + const promiseAllSpy = jest + .spyOn(Promise, 'all') + .mockRejectedValueOnce(promiseAllError); + + jest.spyOn(ClientModule, 'default').mockImplementation(() => ({ + request: () => Promise.resolve(getSuccessResponse), + })); + + const testSkyflowClient = new Skyflow({ + vaultID: '1234', + vaultURL: 'https://url.com', + getBearerToken: () => Promise.resolve('valid_token'), + }); + + jest + .spyOn(testSkyflowClient, 'getAccessToken') + .mockResolvedValue('valid token'); + + fetchRecordsGET(testSkyflowClient, [getRecordID], optionsFalse) + .then( + () => { + promiseAllSpy.mockRestore(); + done(new Error('should have rejected')); + }, + (err) => { + promiseAllSpy.mockRestore(); + expect(err).toBe(promiseAllError); + done(); + } + ) + .catch((err) => { + promiseAllSpy.mockRestore(); + done(err); + }); + }); + + it('should not include ids key in error response for column-based record', (done) => { + jest.spyOn(ClientModule, 'default').mockImplementation(() => ({ + request: () => Promise.reject(getErrorResponse), + })); + + const testSkyflowClient = new Skyflow({ + vaultID: '1234', + vaultURL: 'https://url.com', + getBearerToken: () => Promise.resolve('valid_token'), + }); + + jest + .spyOn(testSkyflowClient, 'getAccessToken') + .mockResolvedValue('valid token'); + + fetchRecordsGET(testSkyflowClient, [getRecordColumn], optionsFalse) + .then( + (res) => {}, + (err) => { + expect(err.errors[0].ids).toBeUndefined(); + expect(err.errors[0].columnName).toBe(getRecordColumn.columnName); + done(); + } + ) + .catch((err) => { + done(err); + }); + }); + + it('should include record with empty fields when fields key is missing in vault response', (done) => { + const responseWithMissingFields = { + records: [{ skyflow_id: 'id1' }], // no `fields` key + }; + + jest.spyOn(ClientModule, 'default').mockImplementation(() => ({ + request: () => Promise.resolve(responseWithMissingFields), + })); + + const testSkyflowClient = new Skyflow({ + vaultID: '1234', + vaultURL: 'https://url.com', + getBearerToken: () => Promise.resolve('valid_token'), + }); + + jest + .spyOn(testSkyflowClient, 'getAccessToken') + .mockResolvedValue('valid token'); + + fetchRecordsGET(testSkyflowClient, [getRecordID], optionsFalse) + .then((res) => { + expect(res.records.length).toBe(1); + expect(res.records[0].fields).toEqual({ id: '' }); + expect(res.records[0].table).toBe(getRecordID.table); + done(); + }) + .catch((err) => { + done(err); + }); + }); + + it('should set id to empty string in success record when skyflow_id is absent', (done) => { + const responseWithoutSkyflowId = { + records: [ + { + fields: { + card_number: '4111111111111111', + // no skyflow_id + }, + table: 'pii_fields', + }, + ], + }; + + jest.spyOn(ClientModule, 'default').mockImplementation(() => ({ + request: () => Promise.resolve(responseWithoutSkyflowId), + })); + + const testSkyflowClient = new Skyflow({ + vaultID: '1234', + vaultURL: 'https://url.com', + getBearerToken: () => Promise.resolve('valid_token'), + }); + + jest + .spyOn(testSkyflowClient, 'getAccessToken') + .mockResolvedValue('valid token'); + + fetchRecordsGET(testSkyflowClient, [getRecordID], optionsFalse) + .then((res) => { + expect(res.records.length).toBe(1); + expect(res.records[0].fields.id).toBe(''); + expect(res.records[0].fields.card_number).toBe('4111111111111111'); + done(); + }) + .catch((err) => { + done(err); + }); + }); + + it('should include columnName in error response when column-based record fails', (done) => { + jest.spyOn(ClientModule, 'default').mockImplementation(() => ({ + request: () => Promise.reject(getErrorResponse), + })); + + const testSkyflowClient = new Skyflow({ + vaultID: '1234', + vaultURL: 'https://url.com', + getBearerToken: () => Promise.resolve('valid_token'), + }); + + jest + .spyOn(testSkyflowClient, 'getAccessToken') + .mockResolvedValue('valid token'); + + fetchRecordsGET(testSkyflowClient, [getRecordColumn], optionsFalse) + .then( + (res) => {}, + (err) => { + expect(err.errors.length).toBe(1); + expect(err.errors[0].error.code).toBe(404); + expect(err.errors[0].columnName).toBe(getRecordColumn.columnName); + done(); + } + ) + .catch((err) => { + done(err); + }); + }); + it('should include fields params in url when fields are specified in options', (done) => { let reqArg; jest.spyOn(ClientModule, 'default').mockImplementation(() => ({ diff --git a/src/core-utils/element-validations/index.ts b/src/core-utils/element-validations/index.ts index fe84c26..fbeab0e 100644 --- a/src/core-utils/element-validations/index.ts +++ b/src/core-utils/element-validations/index.ts @@ -617,22 +617,30 @@ export const validateGetInput = ( } } - if ( - Object.prototype.hasOwnProperty.call(record, 'offset') && - typeof record.offset !== 'string' - ) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_OFFSET_IN_GET, [ - `${index}`, - ]); + if (Object.prototype.hasOwnProperty.call(record, 'offset')) { + if (typeof record.offset !== 'string') { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_OFFSET_IN_GET, [ + `${index}`, + ]); + } + if (record.offset === '') { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_OFFSET_IN_GET, [ + `${index}`, + ]); + } } - if ( - Object.prototype.hasOwnProperty.call(record, 'limit') && - typeof record.limit !== 'string' - ) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_LIMIT_IN_GET, [ - `${index}`, - ]); + if (Object.prototype.hasOwnProperty.call(record, 'limit')) { + if (typeof record.limit !== 'string') { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_LIMIT_IN_GET, [ + `${index}`, + ]); + } + if (record.limit === '') { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_LIMIT_IN_GET, [ + `${index}`, + ]); + } } if (Object.prototype.hasOwnProperty.call(record, 'fields')) { diff --git a/src/core-utils/reveal/index.ts b/src/core-utils/reveal/index.ts index aca72c8..4ecc6be 100644 --- a/src/core-utils/reveal/index.ts +++ b/src/core-utils/reveal/index.ts @@ -203,11 +203,12 @@ export const fetchRecordsGET = async ( (resolvedResult: any) => { const recordsData: any[] = resolvedResult.records; recordsData.forEach((fieldData) => { - const id = fieldData.fields.skyflow_id; - const currentRecord = { + const fields = fieldData?.fields ?? {}; + const skyflowId = fields?.skyflow_id; + const currentRecord: any = { fields: { - id, - ...fieldData.fields, + ...(skyflowId ? { id: skyflowId } : {id: ''}), + ...fields, }, table: skyflowIdRecord.table, }; @@ -223,7 +224,9 @@ export const fetchRecordsGET = async ( code: rejectedResult?.error?.code, description: rejectedResult?.error?.description, }, - ids: skyflowIdRecord.ids, + ...(skyflowIdRecord.ids + ? { ids: skyflowIdRecord.ids } + : {}), ...(skyflowIdRecord?.columnName ? { columnName: skyflowIdRecord?.columnName } : {}), @@ -265,7 +268,7 @@ export const fetchRecordsGET = async ( rootReject({ records: recordsResponse, errors: errorResponse }); }) .catch((err) => { - console.log(err); + rootReject(err); }); }) .catch((err) => { diff --git a/src/utils/logs/index.ts b/src/utils/logs/index.ts index a40e1e5..a2f37be 100644 --- a/src/utils/logs/index.ts +++ b/src/utils/logs/index.ts @@ -225,8 +225,12 @@ const logs = { `${SDK_NAME_VERSION} Validation error. 'fields' array contains a non-string value in records at index %s1. Specify string values instead.`, INVALID_OFFSET_IN_GET: `${SDK_NAME_VERSION} Validation error. Invalid 'offset' in records at index %s1. Specify a value of type string instead.`, + EMPTY_OFFSET_IN_GET: + `${SDK_NAME_VERSION} Validation error. 'offset' cannot be empty in records at index %s1. Specify a non-empty string value instead.`, INVALID_LIMIT_IN_GET: `${SDK_NAME_VERSION} Validation error. Invalid 'limit' in records at index %s1. Specify a value of type string instead.`, + EMPTY_LIMIT_IN_GET: + `${SDK_NAME_VERSION} Validation error. 'limit' cannot be empty in records at index %s1. Specify a non-empty string value instead.`, INVALID_DOWNLOAD_URL_IN_GET: `${SDK_NAME_VERSION} Validation error. Invalid 'downloadURL' in get options. Specify a boolean value instead.`, INVALID_ORDER_BY_IN_GET: diff --git a/src/utils/skyflow-error-code/index.ts b/src/utils/skyflow-error-code/index.ts index 4deda00..44b06bc 100644 --- a/src/utils/skyflow-error-code/index.ts +++ b/src/utils/skyflow-error-code/index.ts @@ -350,10 +350,18 @@ const SKYFLOW_ERROR_CODE = { code: 400, description: logs.errorLogs.INVALID_OFFSET_IN_GET, }, + EMPTY_OFFSET_IN_GET: { + code: 400, + description: logs.errorLogs.EMPTY_OFFSET_IN_GET, + }, INVALID_LIMIT_IN_GET: { code: 400, description: logs.errorLogs.INVALID_LIMIT_IN_GET, }, + EMPTY_LIMIT_IN_GET: { + code: 400, + description: logs.errorLogs.EMPTY_LIMIT_IN_GET, + }, INVALID_DOWNLOAD_URL_IN_GET: { code: 400, description: logs.errorLogs.INVALID_DOWNLOAD_URL_IN_GET, From 7c05221a92afc17c338ecaa51a2ced2cfffe2b49 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Wed, 3 Jun 2026 09:56:29 +0000 Subject: [PATCH 07/14] [AUTOMATED] Private Release 1.10.3-dev.0a6aa96 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 63f6cf9..f575a13 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyflow-react-native", - "version": "1.10.3", + "version": "1.10.3-dev.0a6aa96", "description": "Skyflow React Native SDK", "main": "lib/commonjs/index", "module": "lib/module/index", From f389357119350a2f7819188fcb3adf4b391e2e94 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Wed, 3 Jun 2026 15:31:38 +0530 Subject: [PATCH 08/14] SK-2658 update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3259186..fe6faec 100644 --- a/README.md +++ b/README.md @@ -1590,7 +1590,7 @@ For non-PCI use-cases, retrieving data from the vault and revealing it in the mo table: 'customers', redaction: RedactionType.PLAIN_TEXT, columnName: 'email', - columnValues: ['john.doe@gmail.com'], + ids: [], fields: ['name', 'email'], offset: '0', limit: '10', From bf7a7a52fa20ef54d7d5c7d69f093cadbbd91ae3 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Wed, 3 Jun 2026 17:04:07 +0530 Subject: [PATCH 09/14] SK-2658 update tests --- __tests__/core-utils/reveal.test.js | 6 +++--- src/core-utils/reveal/index.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/__tests__/core-utils/reveal.test.js b/__tests__/core-utils/reveal.test.js index 345d7c3..51ad602 100644 --- a/__tests__/core-utils/reveal.test.js +++ b/__tests__/core-utils/reveal.test.js @@ -765,7 +765,7 @@ describe('fetchRecordGET fn test', () => { fetchRecordsGET(testSkyflowClient, [getRecordID], optionsFalse) .then((res) => { expect(res.records.length).toBe(1); - expect(res.records[0].fields).toEqual({ id: '' }); + expect(res.records[0].fields).toEqual({}); expect(res.records[0].table).toBe(getRecordID.table); done(); }) @@ -774,7 +774,7 @@ describe('fetchRecordGET fn test', () => { }); }); - it('should set id to empty string in success record when skyflow_id is absent', (done) => { + it('should not include id field in success record when skyflow_id is absent', (done) => { const responseWithoutSkyflowId = { records: [ { @@ -804,7 +804,7 @@ describe('fetchRecordGET fn test', () => { fetchRecordsGET(testSkyflowClient, [getRecordID], optionsFalse) .then((res) => { expect(res.records.length).toBe(1); - expect(res.records[0].fields.id).toBe(''); + expect(res.records[0].fields.id).toBeUndefined(); expect(res.records[0].fields.card_number).toBe('4111111111111111'); done(); }) diff --git a/src/core-utils/reveal/index.ts b/src/core-utils/reveal/index.ts index 4ecc6be..c7fcf43 100644 --- a/src/core-utils/reveal/index.ts +++ b/src/core-utils/reveal/index.ts @@ -207,7 +207,7 @@ export const fetchRecordsGET = async ( const skyflowId = fields?.skyflow_id; const currentRecord: any = { fields: { - ...(skyflowId ? { id: skyflowId } : {id: ''}), + ...(skyflowId ? { id: skyflowId } : {}), ...fields, }, table: skyflowIdRecord.table, From 31fa284057fadaff3beab9d0f7141ba2e8ff2dde Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Wed, 3 Jun 2026 11:35:19 +0000 Subject: [PATCH 10/14] [AUTOMATED] Private Release 1.10.3-dev.bf7a7a5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f575a13..0223389 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyflow-react-native", - "version": "1.10.3-dev.0a6aa96", + "version": "1.10.3-dev.bf7a7a5", "description": "Skyflow React Native SDK", "main": "lib/commonjs/index", "module": "lib/module/index", From aeb9d680b15362f4f6af3dbc96c8f5466f04472f Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Wed, 3 Jun 2026 22:20:27 +0530 Subject: [PATCH 11/14] SK-2658 update logic --- .../core-utils/element-validations.test.js | 254 +++++++++++++++++- src/core-utils/element-validations/index.ts | 29 +- src/utils/logs/index.ts | 2 + src/utils/skyflow-error-code/index.ts | 4 + 4 files changed, 266 insertions(+), 23 deletions(-) diff --git a/__tests__/core-utils/element-validations.test.js b/__tests__/core-utils/element-validations.test.js index 5915128..bcb29be 100644 --- a/__tests__/core-utils/element-validations.test.js +++ b/__tests__/core-utils/element-validations.test.js @@ -328,15 +328,12 @@ describe('test validateGetInput', () => { } }); - it('should throw error for missing table when ids is empty array', () => { + it('should throw error for empty ids array', () => { try { validateGetInput({ records: [{ ids: [] }] }); } catch (err) { expect(err?.errors[0]?.description).toEqual( - parameterizedString( - SKYFLOW_ERROR_CODE.MISSING_TABLE_IN_GET.description, - 0 - ) + parameterizedString(SKYFLOW_ERROR_CODE.EMPTY_IDS_IN_GET.description, 0) ); } }); @@ -471,7 +468,7 @@ describe('test validateGetInput', () => { } }); - it('should throw error for empty column values', () => { + it('should throw error for empty column values when columnValues is null', () => { try { validateGetInput({ records: [ @@ -809,6 +806,140 @@ describe('test validateGetInput', () => { ); } }); + + // ids + columnValues without columnName + it('should throw MISSING_RECORD_COLUMN_NAME when ids and columnValues are present without columnName', () => { + try { + validateGetInput({ + records: [ + { + ids: ['123'], + table: 'test', + redaction: RedactionType.PLAIN_TEXT, + columnValues: ['val1'], + }, + ], + }); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + parameterizedString( + SKYFLOW_ERROR_CODE.MISSING_RECORD_COLUMN_NAME.description, + 0 + ) + ); + } + }); + + // fields with ids → valid + it('should not throw when fields is provided with ids', () => { + expect(() => + validateGetInput({ + records: [ + { + ids: ['123'], + table: 'test', + redaction: RedactionType.PLAIN_TEXT, + fields: ['name', 'email'], + }, + ], + }) + ).not.toThrow(); + }); + + // tokens:true + fields → valid (no restriction) + it('should not throw when tokens:true is combined with fields', () => { + expect(() => + validateGetInput( + { + records: [ + { + ids: ['123'], + table: 'test', + fields: ['name'], + }, + ], + }, + { tokens: true } + ) + ).not.toThrow(); + }); + + // error at record index 1 + it('should include correct index in error when second record is invalid', () => { + try { + validateGetInput({ + records: [ + { ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT }, + { ids: ['456'], table: null, redaction: RedactionType.DEFAULT }, + ], + }); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + parameterizedString( + SKYFLOW_ERROR_CODE.EMPTY_TABLE_IN_GET.description, + 1 + ) + ); + } + }); + + // offset alone without limit (column query) → valid + it('should not throw when only offset is provided with columnName and columnValues', () => { + expect(() => + validateGetInput({ + records: [ + { + table: 'test', + redaction: RedactionType.DEFAULT, + columnName: 'email', + columnValues: ['test@example.com'], + offset: '5', + }, + ], + }) + ).not.toThrow(); + }); + + // limit alone without offset (column query) → valid + it('should not throw when only limit is provided with columnName and columnValues', () => { + expect(() => + validateGetInput({ + records: [ + { + table: 'test', + redaction: RedactionType.DEFAULT, + columnName: 'email', + columnValues: ['test@example.com'], + limit: '10', + }, + ], + }) + ).not.toThrow(); + }); + + // offset/limit with columnName but missing columnValues + it('should throw MISSING_RECORD_COLUMN_VALUE when offset/limit with columnName but no columnValues', () => { + try { + validateGetInput({ + records: [ + { + table: 'test', + redaction: RedactionType.DEFAULT, + columnName: 'email', + offset: '0', + limit: '10', + }, + ], + }); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + parameterizedString( + SKYFLOW_ERROR_CODE.MISSING_RECORD_COLUMN_VALUE.description, + 0 + ) + ); + } + }); }); describe('test get options validation', () => { @@ -1005,9 +1136,10 @@ describe('test get options validation', () => { validateGetInput({ records: [ { - ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT, + columnName: 'email', + columnValues: ['test@example.com'], offset: 10, }, ], @@ -1027,9 +1159,10 @@ describe('test get options validation', () => { validateGetInput({ records: [ { - ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT, + columnName: 'email', + columnValues: ['test@example.com'], offset: null, }, ], @@ -1049,9 +1182,10 @@ describe('test get options validation', () => { validateGetInput({ records: [ { - ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT, + columnName: 'email', + columnValues: ['test@example.com'], limit: true, }, ], @@ -1071,9 +1205,10 @@ describe('test get options validation', () => { validateGetInput({ records: [ { - ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT, + columnName: 'email', + columnValues: ['test@example.com'], limit: null, }, ], @@ -1093,9 +1228,10 @@ describe('test get options validation', () => { validateGetInput({ records: [ { - ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT, + columnName: 'email', + columnValues: ['test@example.com'], offset: '', }, ], @@ -1115,9 +1251,10 @@ describe('test get options validation', () => { validateGetInput({ records: [ { - ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT, + columnName: 'email', + columnValues: ['test@example.com'], limit: '', }, ], @@ -1197,14 +1334,73 @@ describe('test get options validation', () => { } }); - it('should not throw for valid offset and limit in record', () => { - expect(() => + it('should throw error when ids and offset are both specified', () => { + try { + validateGetInput({ + records: [ + { + ids: ['123'], + table: 'test', + redaction: RedactionType.DEFAULT, + offset: '5', + }, + ], + }); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + SKYFLOW_ERROR_CODE.IDS_AND_OFFSET_LIMIT_BOTH_SPECIFIED.description + ); + } + }); + + it('should throw error when ids and limit are both specified', () => { + try { + validateGetInput({ + records: [ + { + ids: ['123'], + table: 'test', + redaction: RedactionType.DEFAULT, + limit: '10', + }, + ], + }); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + SKYFLOW_ERROR_CODE.IDS_AND_OFFSET_LIMIT_BOTH_SPECIFIED.description + ); + } + }); + + it('should throw error when ids, offset and limit are all specified', () => { + try { validateGetInput({ records: [ { ids: ['123'], table: 'test', redaction: RedactionType.DEFAULT, + offset: '0', + limit: '10', + }, + ], + }); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + SKYFLOW_ERROR_CODE.IDS_AND_OFFSET_LIMIT_BOTH_SPECIFIED.description + ); + } + }); + + it('should not throw when offset and limit are used with columnName and columnValues', () => { + expect(() => + validateGetInput({ + records: [ + { + table: 'test', + redaction: RedactionType.DEFAULT, + columnName: 'email', + columnValues: ['test@example.com'], offset: '5', limit: '10', }, @@ -1213,6 +1409,36 @@ describe('test get options validation', () => { ).not.toThrow(); }); + it('should not throw when only offset and limit are provided without ids or columnName', () => { + expect(() => + validateGetInput({ + records: [ + { + table: 'test', + redaction: RedactionType.DEFAULT, + offset: '0', + limit: '10', + }, + ], + }) + ).not.toThrow(); + }); + + it('should throw MISSING_IDS_OR_COLUMN_VALUES_IN_GET when no identifier and no offset/limit', () => { + try { + validateGetInput({ + records: [{ table: 'test', redaction: RedactionType.PLAIN_TEXT }], + }); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + parameterizedString( + SKYFLOW_ERROR_CODE.MISSING_IDS_OR_COLUMN_VALUES_IN_GET.description, + 0 + ) + ); + } + }); + it('should not throw for valid downloadURL in get options', () => { expect(() => validateGetInput( diff --git a/src/core-utils/element-validations/index.ts b/src/core-utils/element-validations/index.ts index fbeab0e..6840f58 100644 --- a/src/core-utils/element-validations/index.ts +++ b/src/core-utils/element-validations/index.ts @@ -465,11 +465,25 @@ export const validateGetInput = ( if (Object.keys(record).length === 0) { throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_RECORDS_GET); } + if (record.ids?.length === 0) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_IDS_IN_GET, [ + `${index}`, + ]); + } if (record.ids != null && !(record.ids && Array.isArray(record.ids))) { throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_IDS_IN_GET, [ `${index}`, ]); } + if ( + Object.prototype.hasOwnProperty.call(record, 'ids') && + (Object.prototype.hasOwnProperty.call(record, 'offset') || + Object.prototype.hasOwnProperty.call(record, 'limit')) + ) { + throw new SkyflowError( + SKYFLOW_ERROR_CODE.IDS_AND_OFFSET_LIMIT_BOTH_SPECIFIED + ); + } record.ids?.forEach((skyflowId) => { if (!skyflowId) { throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_SKYFLOWID_IN_GET, [ @@ -535,10 +549,12 @@ export const validateGetInput = ( ); } if (!Object.prototype.hasOwnProperty.call(record, 'columnName')) { - if ( - Object.prototype.hasOwnProperty.call(record, 'ids') === false && - Object.prototype.hasOwnProperty.call(record, 'columnValues') === false - ) { + const hasIds = Object.prototype.hasOwnProperty.call(record, 'ids'); + const hasColumnValues = Object.prototype.hasOwnProperty.call(record, 'columnValues'); + const hasOffsetOrLimit = + Object.prototype.hasOwnProperty.call(record, 'offset') || + Object.prototype.hasOwnProperty.call(record, 'limit'); + if (!hasIds && !hasColumnValues && !hasOffsetOrLimit) { throw new SkyflowError( SKYFLOW_ERROR_CODE.MISSING_IDS_OR_COLUMN_VALUES_IN_GET, [`${index}`] @@ -567,11 +583,6 @@ export const validateGetInput = ( `${index}`, ]); } - if (record.columnName !== undefined && record.columnValues === undefined) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.MISSING_RECORD_COLUMN_VALUE, [ - `${index}`, - ]); - } if (record.columnName === undefined && record.columnValues !== undefined) { throw new SkyflowError(SKYFLOW_ERROR_CODE.MISSING_RECORD_COLUMN_NAME, [ `${index}`, diff --git a/src/utils/logs/index.ts b/src/utils/logs/index.ts index a2f37be..30414ac 100644 --- a/src/utils/logs/index.ts +++ b/src/utils/logs/index.ts @@ -223,6 +223,8 @@ const logs = { `${SDK_NAME_VERSION} Validation error. 'fields' array contains an empty string in records at index %s1. Specify non-empty string values.`, INVALID_FIELD_VALUE_IN_GET: `${SDK_NAME_VERSION} Validation error. 'fields' array contains a non-string value in records at index %s1. Specify string values instead.`, + IDS_AND_OFFSET_LIMIT_BOTH_SPECIFIED: + `${SDK_NAME_VERSION} Validation error. Invalid request. 'offset' and 'limit' aren't supported when fetching records by 'skyflow_id'. Remove 'offset' and 'limit'.`, INVALID_OFFSET_IN_GET: `${SDK_NAME_VERSION} Validation error. Invalid 'offset' in records at index %s1. Specify a value of type string instead.`, EMPTY_OFFSET_IN_GET: diff --git a/src/utils/skyflow-error-code/index.ts b/src/utils/skyflow-error-code/index.ts index 44b06bc..9215bf0 100644 --- a/src/utils/skyflow-error-code/index.ts +++ b/src/utils/skyflow-error-code/index.ts @@ -346,6 +346,10 @@ const SKYFLOW_ERROR_CODE = { code: 400, description: logs.errorLogs.INVALID_FIELD_VALUE_IN_GET, }, + IDS_AND_OFFSET_LIMIT_BOTH_SPECIFIED: { + code: 400, + description: logs.errorLogs.IDS_AND_OFFSET_LIMIT_BOTH_SPECIFIED, + }, INVALID_OFFSET_IN_GET: { code: 400, description: logs.errorLogs.INVALID_OFFSET_IN_GET, From 2a57e20bfa47bda16be203aa9c44a4bf3dd40083 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Wed, 3 Jun 2026 16:51:44 +0000 Subject: [PATCH 12/14] [AUTOMATED] Private Release 1.10.3-dev.aeb9d68 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0223389..99c9bd5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyflow-react-native", - "version": "1.10.3-dev.bf7a7a5", + "version": "1.10.3-dev.aeb9d68", "description": "Skyflow React Native SDK", "main": "lib/commonjs/index", "module": "lib/module/index", From 86894f66952390a8077036b847739e325513fb9c Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Wed, 3 Jun 2026 22:41:49 +0530 Subject: [PATCH 13/14] SK-2658 update logic --- .../core-utils/element-validations.test.js | 35 +++++++++++++++++++ src/core-utils/element-validations/index.ts | 5 +++ src/utils/logs/index.ts | 1 + src/utils/skyflow-error-code/index.ts | 4 +++ 4 files changed, 45 insertions(+) diff --git a/__tests__/core-utils/element-validations.test.js b/__tests__/core-utils/element-validations.test.js index bcb29be..aaa83f5 100644 --- a/__tests__/core-utils/element-validations.test.js +++ b/__tests__/core-utils/element-validations.test.js @@ -704,6 +704,41 @@ describe('test validateGetInput', () => { } }); + it('should throw error for empty string columnName', () => { + expect(() => + validateGetInput({ + records: [ + { + columnValues: ['value1'], + columnName: '', + table: 'test', + redaction: RedactionType.PLAIN_TEXT, + }, + ], + }) + ).toThrow(); + + try { + validateGetInput({ + records: [ + { + columnValues: ['value1'], + columnName: '', + table: 'test', + redaction: RedactionType.PLAIN_TEXT, + }, + ], + }); + } catch (err) { + expect(err?.errors[0]?.description).toEqual( + parameterizedString( + SKYFLOW_ERROR_CODE.EMPTY_RECORD_COLUMN_NAME.description, + 0 + ) + ); + } + }); + it('should throw error for empty column values error', () => { try { validateGetInput({ diff --git a/src/core-utils/element-validations/index.ts b/src/core-utils/element-validations/index.ts index 6840f58..1e44d0e 100644 --- a/src/core-utils/element-validations/index.ts +++ b/src/core-utils/element-validations/index.ts @@ -596,6 +596,11 @@ export const validateGetInput = ( [`${index}`] ); } + if (columnName === '') { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_RECORD_COLUMN_NAME, [ + `${index}`, + ]); + } const columnValues = record.columnValues; if (columnValues != null) { columnValues.forEach((eachColumnValue) => { diff --git a/src/utils/logs/index.ts b/src/utils/logs/index.ts index 30414ac..fe3ef2b 100644 --- a/src/utils/logs/index.ts +++ b/src/utils/logs/index.ts @@ -207,6 +207,7 @@ const logs = { `${SDK_NAME_VERSION} Validation error. Column Values is required when Column Name is specified.`, MISSING_RECORD_COLUMN_NAME: `${SDK_NAME_VERSION} Validation error. Column Name is required when Column Values are specified.`, + EMPTY_RECORD_COLUMN_NAME: `${SDK_NAME_VERSION} Validation error. 'columnName' cannot be empty in records at index %s1. Specify a non-empty string value instead.`, INVALID_RECORD_COLUMN_NAME_TYPE: `${SDK_NAME_VERSION} Validation error. Invalid Type of Records Column Name.`, INVALID_RECORD_COLUMN_VALUE_TYPE: `${SDK_NAME_VERSION} Validation error. Invalid Type of Records Column Values in records at index %s1`, INVALID_COLUMN_VALUES_TYPE: diff --git a/src/utils/skyflow-error-code/index.ts b/src/utils/skyflow-error-code/index.ts index 9215bf0..a6d3695 100644 --- a/src/utils/skyflow-error-code/index.ts +++ b/src/utils/skyflow-error-code/index.ts @@ -390,6 +390,10 @@ const SKYFLOW_ERROR_CODE = { code: 400, description: logs.errorLogs.MISSING_RECORD_COLUMN_NAME, }, + EMPTY_RECORD_COLUMN_NAME: { + code: 400, + description: logs.errorLogs.EMPTY_RECORD_COLUMN_NAME, + }, INVALID_RECORD_COLUMN_VALUE: { code: 400, description: logs.errorLogs.INVALID_RECORD_COLUMN_VALUE, From c7a79cb40042c6f8f890e7b25ed7b29a46b56b8a Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Thu, 4 Jun 2026 14:34:04 +0530 Subject: [PATCH 14/14] SK-2658 update readme --- README.md | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index fe6faec..5faef92 100644 --- a/README.md +++ b/README.md @@ -1407,7 +1407,7 @@ For non-PCI use-cases, retrieving data from the vault and revealing it in the mo - You can use either Skyflow IDs or unique column values to retrieve records. You can't use both at the same time. - `tokens: true` is only applicable when fetching by Skyflow IDs and cannot be combined with `redaction`. - Use `fields` to restrict which columns are returned — required when column-scoped policies limit access to certain fields. - - `offset` and `limit` are per-record and support pagination across results. + - `offset` and `limit` are per-record and support pagination; they are only valid with `columnName`/`columnValues` queries or standalone — they cannot be used with `ids`. - `orderBy` accepts `OrderBy.ASCENDING`, `OrderBy.DESCENDING`, or `OrderBy.NONE`. - `tokens` defaults to false. @@ -1418,9 +1418,8 @@ For non-PCI use-cases, retrieving data from the vault and revealing it in the mo "ids": Array, // Array of SkyflowID's of the records to be fetched "table": String, // name of table holding the above skyflow_id's "redaction": RedactionType, // redaction to be applied to retrieved data - "fields": Array, // (optional) columns to return; omit to return all permitted columns - "offset": String, // (optional) pagination start position - "limit": String // (optional) max number of records to return + "fields": Array // (optional) columns to return; omit to return all permitted columns + // NOTE: offset and limit are NOT supported when using ids }, { "table": String, // name of table from where records are to be fetched @@ -1590,7 +1589,7 @@ For non-PCI use-cases, retrieving data from the vault and revealing it in the mo table: 'customers', redaction: RedactionType.PLAIN_TEXT, columnName: 'email', - ids: [], + columnValues: ['john.doe@gmail.com'], fields: ['name', 'email'], offset: '0', limit: '10', @@ -1608,6 +1607,34 @@ For non-PCI use-cases, retrieving data from the vault and revealing it in the mo }); ``` + An Example of Get call to fetch all records with pagination (no Skyflow IDs or column filter) + + ```js + import { useSkyflow, RedactionType, OrderBy } from 'skyflow-react-native'; + + const skyflowContainer = useSkyflow(); + + const getRequestInput = { + records: [ + { + table: 'customers', + redaction: RedactionType.PLAIN_TEXT, + offset: '0', + limit: '25', + }, + ], + }; + + skyflowContainer + .get(getRequestInput, { tokens: false, orderBy: OrderBy.ASCENDING }) + .then((response) => { + console.log(JSON.stringify(response)); + }) + .catch((err) => { + console.error(JSON.stringify(err)); + }); + ``` + An Example of Get call to fetch Tokens ```js