ehmpathy / sql-dao-generator

Generate data-access-objects from your domain-objects
MIT License
0 stars 0 forks source link

add `finsert` w/ `idempotency` flag #16

Open uladkasach opened 1 year ago

uladkasach commented 1 year ago

add finsert with idempotency


// finsert.ts

import { omitMetadataValues, serialize } from 'domain-objects';
import { HasId, HasUuid } from 'simple-type-guards';

import { ChatMessage } from '../../../domain';
import { DatabaseConnection } from '../../../utils/database/getDatabaseConnection';
import { FinsertIdempotencyImpossibleError } from '../.generated/errors';
import { findByUnique } from './findByUnique';
import { upsert } from './upsert';

/**
 * find or insert the entity
 */
export const finsert = async ({
  dbConnection,
  chatMessage: expected,
  idempotent,
}: {
  dbConnection: DatabaseConnection;
  chatMessage: ChatMessage;

  /**
   * idempotency guarantees that, for the same input, the first result and subsequent results are equivalent
   *
   * usecases
   * - guarantee that either the domain-object was inserted or that the found domain-object values are equivalent to the input
   * - throw an error if we can not guarantee that this is the original request or a duplicate request
   *
   * specifically,
   * - is it the original request? if so, succeed
   * - is it a duplicate request? if so, succeed for idempotency
   * - is it not the original and not a duplicate request? if so, throw an error to warn user this can not be idempotent
   */
  idempotent?: boolean;
}): Promise<HasId<HasUuid<ChatMessage>>> => {
  const found = await findByUnique({
    dbConnection,
    ...expected,
  });
  if (found) {
    if (
      idempotent &&
      serialize(omitMetadataValues(found)) !==
        serialize(omitMetadataValues(expected))
    )
      throw new FinsertIdempotencyImpossibleError({ expected, found });
    return found;
  }
  return await upsert({
    dbConnection,
    chatMessage: expected,
  });
};

// .generated/errors.ts

import { DomainObject } from 'domain-objects';

/**
 * an error thrown when idempotency is impossible for a finsert request
 * - either
 *   - it's not a request that idempotency was ever possible for (i.e., it couldn't possibly be a duplicate)
 *   - or
 *   - it's not a request that idempotency can be possible for any longer (i.e., underlying state changed and its no longer supported)
 *
 * common causes
 * - the domain-object was previously created && someone is trying to unknowingly reuse it's identity
 * - the domain-object was updated since the original request && now the operation can not be repeated
 *
 * explanation
 * - a find-or-insert operation, finsert, can only be idempotent when
 *   - the domain-object does not exist yet (i.e., original request)
 *   - or
 *   - the domain-object exists already and it's found properties are equivalent to the input properties (i.e., duplicate request)
 * - otherwise,
 *   - the insert would have produced an domain-object with the input properties
 *   - while the find produces an domain-object with the found properties
 *   - => the find and insert produce different results
 *   - => idempotency impossible
 * ref:
 * - def:`idempotency`
 *   - > when the repeating the request is guaranteed to produce the same results
 */
export class FinsertIdempotencyImpossibleError<
  T extends DomainObject<any>,
> extends Error {
  constructor({ found, expected }: { found: T; expected: T }) {
    const domainObjectName = found.constructor.name;
    super(
      `
FinsertIdempotencyImpossible: Some ${domainObjectName} already exits with the same identity but with different updatable properties. Due to this, this request can not possibly be idempotent.

${JSON.stringify({ found, expected })}
    `.trim(),
    );
  }
}