Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions dataLayer/harperBridge/ResourceBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,35 @@ export class ResourceBridge extends BridgeMethods {
return this.upsertRecords(updateObj);
}

async upsertRecords(upsertObj) {
/**
* Create-or-replace: the stored record becomes exactly the submitted one, so an attribute the
* request omits is REMOVED rather than kept. `update`/`upsert` merge (`Table.patch`), which is the
* v4-compatible behaviour every existing client depends on and is why this is a separate operation
* rather than a flag on those. Equivalent to REST `PUT /Table/id` — same `Table.put`, so the same
* audit type, replication shape, and retained `__createdtime__`.
*/
async putRecords(putObj) {
return this.upsertRecords(putObj, true);
}

async upsertRecords(upsertObj, fullRecord = false) {
const { attributes } = insertUpdateValidate(upsertObj);

let new_attributes;
// `fullRecord` arrives as an ARGUMENT, never as a field on the request. Reading it off
// `upsertObj` would let a client send `full_record: true` with an `update` and get a replace —
// which makes the operation name stop describing the write, and bypasses the attribute-scoped
// `put` denial in `verifyPerms`, since that is keyed on the operation.
//
// Without it a write over an existing record merges (`Table.patch`), so an attribute the caller
// omitted keeps its stored value and `null` stores a null. That merge is the v4-compatible
// behaviour `update`/`upsert` must keep; HarperFast/studio#1643 is the removal case it cannot
// serve, and `put` is the operation that can.
//
// A put is also one operation where a client emulating removal needs two: the record is never
// absent between them, subscribers see a single write rather than a delete followed by an
// insert, and `__createdtime__` survives (`Table._writeUpdate` retains the stored created time
// on a full update and only stamps a new one for a new entry).
const Table = getDatabases()[upsertObj.schema][upsertObj.table];
const context: Context = {
user: upsertObj.hdb_user,
Expand Down Expand Up @@ -259,9 +284,14 @@ export class ResourceBridge extends BridgeMethods {
}
}
}
// Keyed on whether there IS a record to remove from, not on which operation asked. Guarding
// the insert flag alone left `upsert` with no primary key reaching the same silent partial
// write one branch over: `insertUpdateValidate` requires a hash attribute only for
// `update`, so such a record takes the `id == undefined → Table.create` path and stored an
// auto-keyed record with the named attributes stripped, returning 200.
await (id == undefined
? Table.create(record, context)
: existingRecord
: existingRecord && !fullRecord
? Table.patch(record, context)
: Table.put(record, context));
keys.push(record[Table.primaryKey]);
Expand Down
45 changes: 44 additions & 1 deletion dataLayer/insert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const pGlobalSchema = util.promisify(globalSchema.getTableSchema);
const UPDATE_ACTION = 'updated';
const INSERT_ACTION = 'inserted';
const UPSERT_ACTION = 'upserted';
const PUT_ACTION = 'put';

//IMPORTANT - This validation function is the async version of the code in harperBridge/bridgeUtility/insertUpdateValidate.js
// make sure any changes below are also made there. This is to resolve a circular dependency.
Expand Down Expand Up @@ -218,6 +219,42 @@ async function upsertData(upsertObject: any) {
);
}

/**
* Create-or-replace the records in the putObject parameter: the stored record becomes exactly the
* submitted one, so an attribute the request omits is removed. `update`/`upsert` merge instead, and
* must keep doing so for v4 compatibility — hence a distinct operation rather than a flag on those.
* Equivalent to REST `PUT /Table/id`.
* @param putObject - Represents the data that will be written
*/
async function putData(putObject: any) {
if (putObject.operation !== 'put') {
throw handleHDBError(new Error(), 'invalid operation, must be put', HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR);
}

let validator = insertValidator(putObject);
if (validator) {
throw handleHDBError(new Error(), validator.message, HTTP_STATUS_CODES.BAD_REQUEST);
}

hdbUtils.transformReq(putObject);

let invalidSchemaTableMsg = hdbUtils.checkSchemaTableExist(putObject.schema, putObject.table);
if (invalidSchemaTableMsg) {
throw handleHDBError(new Error(), invalidSchemaTableMsg, HTTP_STATUS_CODES.BAD_REQUEST);
}

let bridgePutResult = await harperBridge.putRecords(putObject);

return returnObject(
PUT_ACTION,
bridgePutResult.written_hashes,
putObject,
[],
bridgePutResult.new_attributes,
bridgePutResult.txn_time
);
}

/**
* Constructs return object for insert, update, and upsert.
* @param action
Expand Down Expand Up @@ -254,6 +291,12 @@ function returnObject(
return return_object;
}

if (action === PUT_ACTION) {
// `put` never skips: every submitted record is written, created or replaced.
return_object.put_hashes = written_hashes;
return return_object;
}

return_object.update_hashes = written_hashes;
return_object.skipped_hashes = skipped;
return return_object;
Expand All @@ -263,4 +306,4 @@ export function flush(object: any) {
hdbUtils.transformReq(object);
return harperBridge.flush(object.schema, object.table);
}
export { insertData as insert, updateData as update, upsertData as upsert };
export { insertData as insert, updateData as update, upsertData as upsert, putData as put };
Loading
Loading