Skip to content

Commit 1de5bec

Browse files
SK-3225: flowvault insert/update — per-record table + separate tokens/data
Aligns the Node flowvault insert/update contract with the Java/Python SDKs: - InsertRecord/UpdateRecord gain an optional per-record `table`; when any record sets it, a single insert/update call targets multiple tables (the generated InsertRecordData/UpdateRecordData already carry tableName). - Insert/Update response records now expose `tokens` and `data` separately instead of a single merged `fields`. Updated unit tests (incl. multi-table coverage) and the api-report snapshot. flowvault 156 tests pass (100% lines, branches 96.97%); common 232, skyvault 878 unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 874487c commit 1de5bec

7 files changed

Lines changed: 102 additions & 19 deletions

File tree

‎flowvault/api-report/skyflow-flowvault-node.api.md‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,8 @@ export interface InsertRecord {
324324
// (undocumented)
325325
data: Record<string, unknown>;
326326
// (undocumented)
327+
table?: string;
328+
// (undocumented)
327329
tokens?: Record<string, unknown>;
328330
}
329331

@@ -350,9 +352,9 @@ export class InsertResponse extends BaseInsertResponse {
350352
// @public (undocumented)
351353
export interface InsertResponseRecord {
352354
// (undocumented)
353-
error?: string | null;
355+
data?: Record<string, unknown> | null;
354356
// (undocumented)
355-
fields?: Record<string, unknown> | null;
357+
error?: string | null;
356358
// (undocumented)
357359
hashedData?: Record<string, unknown> | null;
358360
// (undocumented)
@@ -363,6 +365,8 @@ export interface InsertResponseRecord {
363365
skyflowId?: string | null;
364366
// (undocumented)
365367
tableName?: string | null;
368+
// (undocumented)
369+
tokens?: Record<string, unknown> | null;
366370
}
367371

368372
// @public
@@ -437,6 +441,8 @@ export interface UpdateRecord {
437441
// (undocumented)
438442
skyflowId: string;
439443
// (undocumented)
444+
table?: string;
445+
// (undocumented)
440446
tokens?: Record<string, unknown>;
441447
}
442448

@@ -463,9 +469,9 @@ export class UpdateResponse extends BaseUpdateResponse {
463469
// @public (undocumented)
464470
export interface UpdateResponseRecord {
465471
// (undocumented)
466-
error?: string | null;
472+
data?: Record<string, unknown> | null;
467473
// (undocumented)
468-
fields?: Record<string, unknown> | null;
474+
error?: string | null;
469475
// (undocumented)
470476
hashedData?: Record<string, unknown> | null;
471477
// (undocumented)
@@ -476,6 +482,8 @@ export interface UpdateResponseRecord {
476482
skyflowId?: string | null;
477483
// (undocumented)
478484
tableName?: string | null;
485+
// (undocumented)
486+
tokens?: Record<string, unknown> | null;
479487
}
480488

481489
// @public

‎flowvault/src/controller/vault/index.ts‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,18 @@ class VaultController {
110110
validateInsertRequest(request, options, this.logLevel);
111111
await this.client.getToken();
112112

113+
// When any record carries its own table, send a per-record tableName so a
114+
// single insert can target multiple tables (matches Java/Python flowvault).
115+
const insertPerRecordTable = request.records.some(
116+
(record) => record.table !== undefined && record.table !== null,
117+
);
113118
const genReq: Gen.InsertRequest = {
114119
vaultID: this.client.vaultId,
115120
tableName: request.tableName,
116121
records: request.records.map((record) => {
117122
const genRecord: Gen.InsertRecordData = { data: record.data };
118123
if (record.tokens) genRecord.tokens = record.tokens; // BYOT
124+
if (insertPerRecordTable) genRecord.tableName = record.table ?? request.tableName;
119125
return genRecord;
120126
}),
121127
};
@@ -152,7 +158,8 @@ class VaultController {
152158
const result: InsertResponseRecord = {
153159
tableName: record.tableName ?? null,
154160
skyflowId: record.skyflowID ?? null,
155-
fields: (record.tokens ?? record.data) ?? null,
161+
tokens: record.tokens ?? null,
162+
data: record.data ?? null,
156163
hashedData: record.hashedData ?? null,
157164
httpCode: record.httpCode,
158165
error: hasError ? record.error : null,
@@ -239,12 +246,16 @@ class VaultController {
239246
validateUpdateRequest(request, options, this.logLevel);
240247
await this.client.getToken();
241248

249+
const updatePerRecordTable = request.records.some(
250+
(record) => record.table !== undefined && record.table !== null,
251+
);
242252
const genReq: Gen.UpdateRequest = {
243253
vaultID: this.client.vaultId,
244254
tableName: request.tableName,
245255
records: request.records.map((record) => {
246256
const genRecord: Gen.UpdateRecordData = { skyflowID: record.skyflowId, data: record.data };
247257
if (record.tokens) genRecord.tokens = record.tokens; // BYOT
258+
if (updatePerRecordTable) genRecord.tableName = record.table ?? request.tableName;
248259
return genRecord;
249260
}),
250261
};
@@ -273,7 +284,8 @@ class VaultController {
273284
const result: UpdateResponseRecord = {
274285
skyflowId: record.skyflowID ?? null,
275286
tableName: record.tableName ?? null,
276-
fields: (record.tokens ?? record.data) ?? null,
287+
tokens: record.tokens ?? null,
288+
data: record.data ?? null,
277289
hashedData: record.hashedData ?? null,
278290
httpCode: record.httpCode,
279291
error: hasError ? record.error : null,

‎flowvault/src/types/index.ts‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@
44

55
import { SkyflowRecordError } from 'skyflow-common';
66

7-
/** A single record to insert. `tokens` enables BYOT (bring your own token). */
7+
/** A single record to insert. `tokens` enables BYOT (bring your own token).
8+
* `table` is an optional per-record table name — when any record sets it, the
9+
* records can target multiple tables in a single insert call (matches the
10+
* Java/Python flowvault SDKs). */
811
export interface InsertRecord {
912
data: Record<string, unknown>;
1013
tokens?: Record<string, unknown>;
14+
table?: string;
1115
}
1216

1317
/** Upsert configuration for insert operations. */
@@ -22,11 +26,13 @@ export interface ColumnRedaction {
2226
redaction: string;
2327
}
2428

25-
/** A single record to update. */
29+
/** A single record to update. `table` is an optional per-record table name
30+
* (lets a single update call target multiple tables, like insert). */
2631
export interface UpdateRecord {
2732
skyflowId: string;
2833
data: Record<string, unknown>;
2934
tokens?: Record<string, unknown>;
35+
table?: string;
3036
}
3137

3238
/** Token group redaction used by detokenize. */
@@ -76,7 +82,10 @@ export type UploadStatus = 'UPLOADED' | 'FAILED' | 'SKIPPED';
7682
export interface InsertResponseRecord {
7783
tableName?: string | null;
7884
skyflowId?: string | null;
79-
fields?: Record<string, unknown> | null;
85+
// tokens and data are surfaced separately (matches the Java/Python flowvault
86+
// SDKs); either may be present depending on the vault configuration.
87+
tokens?: Record<string, unknown> | null;
88+
data?: Record<string, unknown> | null;
8089
hashedData?: Record<string, unknown> | null;
8190
httpCode: number;
8291
error?: string | null;
@@ -97,7 +106,8 @@ export interface GetResponseRecord {
97106
export interface UpdateResponseRecord {
98107
skyflowId?: string | null;
99108
tableName?: string | null;
100-
fields?: Record<string, unknown> | null;
109+
tokens?: Record<string, unknown> | null;
110+
data?: Record<string, unknown> | null;
101111
hashedData?: Record<string, unknown> | null;
102112
httpCode: number;
103113
error?: string | null;

‎flowvault/test/controller/insert.test.ts‎

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ describe('VaultController.insert', () => {
4646
// Response mapped field-by-field.
4747
const record = resp.records[0];
4848
expect(record.skyflowId).toBe('id1');
49-
expect(record.fields).toEqual({ card_number: 'tok_1' }); // tokens preferred over data
49+
expect(record.tokens).toEqual({ card_number: 'tok_1' });
50+
expect(record.data).toBeNull();
5051
expect(record.hashedData).toEqual({ card_number: 'hash_1' });
5152
expect(record.tableName).toBe('cards');
5253
expect(record.httpCode).toBe(200);
@@ -66,7 +67,40 @@ describe('VaultController.insert', () => {
6667
expect(genReq.records[0]).toEqual({ data: { card_number: '4111' } });
6768
expect(genReq.records[0].tokens).toBeUndefined();
6869
expect(genReq.upsert).toBeUndefined();
69-
expect(resp.records[0].fields).toEqual({ card_number: '4111' }); // data used when no tokens
70+
expect(resp.records[0].data).toEqual({ card_number: '4111' }); // data used when no tokens
71+
expect(resp.records[0].tokens).toBeNull();
72+
});
73+
74+
it('sends a per-record tableName when records carry their own table (multi-table insert)', async () => {
75+
const insertRecords = resourceMethod({ records: [] });
76+
const controller = new VaultController(makeFakeClient({ records: { insertRecords } }));
77+
78+
await controller.insert(new InsertRequest('cards', [
79+
{ data: { a: 1 }, table: 'cards' },
80+
{ data: { b: 2 }, table: 'contacts' },
81+
]));
82+
83+
const [genReq] = insertRecords.mock.calls[0];
84+
expect(genReq.records).toEqual([
85+
{ data: { a: 1 }, tableName: 'cards' },
86+
{ data: { b: 2 }, tableName: 'contacts' },
87+
]);
88+
});
89+
90+
it('falls back to the request table for a record with no table of its own', async () => {
91+
const insertRecords = resourceMethod({ records: [] });
92+
const controller = new VaultController(makeFakeClient({ records: { insertRecords } }));
93+
94+
await controller.insert(new InsertRequest('cards', [
95+
{ data: { a: 1 }, table: 'contacts' },
96+
{ data: { b: 2 } }, // no per-record table -> request table 'cards'
97+
]));
98+
99+
const [genReq] = insertRecords.mock.calls[0];
100+
expect(genReq.records).toEqual([
101+
{ data: { a: 1 }, tableName: 'contacts' },
102+
{ data: { b: 2 }, tableName: 'cards' },
103+
]);
70104
});
71105

72106
it('populates error + requestId (from header) on a per-record error', async () => {
@@ -78,7 +112,8 @@ describe('VaultController.insert', () => {
78112
expect(resp.records[0].error).toBe('bad record');
79113
expect(resp.records[0].requestId).toBe(REQUEST_ID);
80114
expect(resp.records[0].skyflowId).toBeNull();
81-
expect(resp.records[0].fields).toBeNull();
115+
expect(resp.records[0].tokens).toBeNull();
116+
expect(resp.records[0].data).toBeNull();
82117
});
83118

84119
it('sets requestId to null on an errored record when the raw response has no headers', async () => {

‎flowvault/test/controller/update.test.ts‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ describe('VaultController.update', () => {
3030

3131
const record = resp.records[0];
3232
expect(record.skyflowId).toBe('id1');
33-
expect(record.fields).toEqual({ x: 'tok' }); // tokens preferred over data
33+
expect(record.tokens).toEqual({ x: 'tok' });
34+
expect(record.data).toBeNull();
3435
expect(record.hashedData).toEqual({ x: 'h' });
3536
expect(record.tableName).toBe('cards');
3637
expect(record.httpCode).toBe(200);
@@ -46,7 +47,24 @@ describe('VaultController.update', () => {
4647

4748
const [genReq] = updateRecords.mock.calls[0];
4849
expect(genReq.records[0].tokens).toBeUndefined();
49-
expect(resp.records[0].fields).toEqual({ x: 2 });
50+
expect(resp.records[0].data).toEqual({ x: 2 });
51+
expect(resp.records[0].tokens).toBeNull();
52+
});
53+
54+
it('sends a per-record tableName when records carry their own table (falls back to request table)', async () => {
55+
const updateRecords = resourceMethod({ records: [] });
56+
const controller = new VaultController(makeFakeClient({ records: { updateRecords } }));
57+
58+
await controller.update(new UpdateRequest('cards', [
59+
{ skyflowId: 'id1', data: { x: 1 }, table: 'cards' },
60+
{ skyflowId: 'id2', data: { y: 2 } }, // no per-record table -> request table 'cards'
61+
]));
62+
63+
const [genReq] = updateRecords.mock.calls[0];
64+
expect(genReq.records).toEqual([
65+
{ skyflowID: 'id1', data: { x: 1 }, tableName: 'cards' },
66+
{ skyflowID: 'id2', data: { y: 2 }, tableName: 'cards' },
67+
]);
5068
});
5169

5270
it('per-record error gets requestId from the header', async () => {

‎flowvault/test/model/responses.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
describe('FlowVault response models', () => {
1414
it('InsertResponse holds its records', () => {
1515
const records = [
16-
{ tableName: 'cards', skyflowId: 'id1', fields: { card_number: 'tok' }, hashedData: null, httpCode: 200, error: null },
16+
{ tableName: 'cards', skyflowId: 'id1', tokens: { card_number: 'tok' }, data: null, hashedData: null, httpCode: 200, error: null },
1717
];
1818
const resp = new InsertResponse({ records });
1919
expect(resp.records).toBe(records);
@@ -30,10 +30,10 @@ describe('FlowVault response models', () => {
3030

3131
it('UpdateResponse holds its records', () => {
3232
const records = [
33-
{ skyflowId: 'id1', tableName: 'cards', fields: { x: 1 }, hashedData: null, httpCode: 200, error: null },
33+
{ skyflowId: 'id1', tableName: 'cards', tokens: { x: 1 }, data: null, hashedData: null, httpCode: 200, error: null },
3434
];
3535
const resp = new UpdateResponse({ records });
36-
expect(resp.records[0].fields).toEqual({ x: 1 });
36+
expect(resp.records[0].tokens).toEqual({ x: 1 });
3737
});
3838

3939
it('DeleteResponse holds its records (no tableName field)', () => {

‎flowvault/test/smoke.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ describe('VaultController maps generated responses', () => {
124124
expect(resp).toBeInstanceOf(InsertResponse);
125125
expect(insertRecords).toHaveBeenCalledTimes(1);
126126
expect(resp.records[0].skyflowId).toBe('id1');
127-
expect(resp.records[0].fields).toEqual({ card_number: 'tok_1' });
127+
expect(resp.records[0].tokens).toEqual({ card_number: 'tok_1' });
128128
expect(resp.records[0].tableName).toBe('cards');
129129
expect(resp.records[0].httpCode).toBe(200);
130130
expect(resp.records[0].error).toBeNull();

0 commit comments

Comments
 (0)