import {
  BaseRepository
} from "./chunk-J6KZS54B.js";

// runtime/base-classes/integrated-entity-repository.ts
import { and, eq, inArray } from "drizzle-orm";
var IntegratedEntityRepository = class extends BaseRepository {
  /**
   * Find a single entity by its external CRM identifier.
   */
  async findByExternalId(externalId) {
    const rows = await this.baseQuery().where(eq(this.table["externalId"], externalId)).limit(1);
    return rows[0] ?? null;
  }
  /**
   * Find multiple entities by external CRM identifiers.
   */
  async findManyByExternalIds(externalIds) {
    if (externalIds.length === 0) return [];
    const rows = await this.baseQuery().where(inArray(this.table["externalId"], externalIds));
    return rows;
  }
  /**
   * Find all entities owned by a specific user.
   */
  async findAllByUserId(userId) {
    const rows = await this.baseQuery().where(eq(this.table["userId"], userId));
    return rows;
  }
  // ==========================================================================
  // Inbound integration (#374) — canonical→Drizzle write + provider-scoped FK
  // resolution + EAV dual-write seam, all inside a SINGLE transaction.
  // Driven entirely by `this.integrationConfig`; the per-entity shape lives there.
  // ==========================================================================
  /**
   * Upsert ONE entity by its `(provider, externalId)` identity, in a single
   * transaction:
   *   1. resolve each `integrationConfig.fkResolvers` FK (provider-scoped). Strict
   *      resolvers throw on unresolved; non-strict leave the column null.
   *   2. insert-or-update the canonical columns via `onConflictDoUpdate` on the
   *      `conflictTarget`. Resolved FKs are only written into `set` when
   *      non-null this run (no-clobber).
   *   3. EAV dual-write of `write.fields` via `writeCustomFields` when
   *      `integrationConfig.eav` and the bag is non-empty (same tx).
   *
   * Idempotent: a second call with the same identity updates in place. Returns
   * the canonical projection (so the orchestrator records `local_id`).
   *
   * @param write     canonical fields + parent external ids + custom-field bag
   * @param provider  adapter/provider label persisted + used to scope lookups
   * @param tx        optional outer transaction; when omitted we open our own
   */
  async integrationUpsertOne(write, provider, tx) {
    const cfg = this.integrationConfig;
    const w = write;
    const run = async (db) => {
      const resolvedFks = {};
      for (const fk of cfg.fkResolvers) {
        resolvedFks[fk.column] = await this.resolveFk(db, fk, w[fk.writeKey], provider);
      }
      const now = /* @__PURE__ */ new Date();
      const copyThrough = {};
      for (const col of cfg.writeColumns) copyThrough[col] = w[col];
      const values = {
        externalId: w["externalId"],
        provider,
        ...copyThrough,
        ...resolvedFks,
        ...this.behaviors.timestamps ? { updatedAt: now } : {}
      };
      const set = {
        ...copyThrough,
        ...this.behaviors.timestamps ? { updatedAt: now } : {}
      };
      for (const fk of cfg.fkResolvers) {
        if (resolvedFks[fk.column] !== null) set[fk.column] = resolvedFks[fk.column];
      }
      const rows = await db.insert(this.table).values(values).onConflictDoUpdate({
        target: cfg.conflictTarget.map((c) => this.table[c]),
        set
      }).returning();
      const saved = rows[0];
      const fields = w["fields"];
      if (cfg.eav && fields && Object.keys(fields).length > 0) {
        await this.writeCustomFields(
          db,
          saved["id"],
          w["userId"],
          fields
        );
      }
      return this.toProjection(saved);
    };
    return tx ? run(tx) : this.db.transaction((t) => run(t));
  }
  /**
   * Canonical-projected lookup by external id (differ-ready). Returns `null`
   * when no local row exists. Provider-scoped so a HubSpot id can't match a
   * Salesforce row.
   */
  async findByExternalIdProjected(externalId, provider) {
    const rows = await this.db.select().from(this.table).where(
      and(
        eq(this.table["provider"], provider),
        eq(this.table["externalId"], externalId)
      )
    ).limit(1);
    const row = rows[0];
    return row ? this.toProjection(row) : null;
  }
  /**
   * Integration "delete" by external id, provider-scoped. When `softDelete: true`,
   * sets `deletedAt`. When `softDelete: false`, tombstone-by-clearing: null out
   * `external_id`/`provider` so the row no longer matches future inbound
   * changes while preserving local-id references. Returns `{ id }` or `null`.
   */
  async softDeleteByExternalId(externalId, provider, tx) {
    const db = this.runner(tx);
    const set = this.integrationConfig.softDelete ? { deletedAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() } : { externalId: null, provider: null, updatedAt: /* @__PURE__ */ new Date() };
    const rows = await db.update(this.table).set(set).where(
      and(
        eq(this.table["provider"], provider),
        eq(this.table["externalId"], externalId)
      )
    ).returning({ id: this.table["id"] });
    return rows[0] ? { id: rows[0].id } : null;
  }
  /**
   * Batch integration upsert — concretizes the former abstract stub. Delegates to
   * `integrationUpsertOne` per input inside one transaction. Inputs are raw partial
   * rows: provider is read from each input's own `provider` column; rows
   * missing `externalId`/`provider` are skipped.
   */
  async integrationUpsert(inputs) {
    if (inputs.length === 0) return [];
    return this.db.transaction(async (tx) => {
      const out = [];
      for (const input of inputs) {
        const rec = input;
        if (!rec["externalId"] || !rec["provider"]) continue;
        const proj = await this.integrationUpsertOne(
          input,
          rec["provider"],
          tx
        );
        const id = proj["id"];
        const row = await tx.select().from(this.table).where(eq(this.table["id"], id)).limit(1);
        out.push(row[0]);
      }
      return out;
    });
  }
  /**
   * Project a raw row to the canonical differ shape — a generic pick over
   * `integrationConfig.projectionColumns`. Override only for synthesized projections
   * (e.g. junctions); entities use this verbatim.
   */
  toProjection(row) {
    const r = row;
    const out = {};
    for (const col of this.integrationConfig.projectionColumns) out[col] = r[col];
    return out;
  }
  /**
   * EAV dual-write seam (#374, live path lands in #124). No-op by default;
   * `eav: true` entities emit a concrete override that injects
   * `FieldValueService` and delegates to `upsertFieldsTransactional` so the
   * dual-write joins the same tx (`db`). Kept as an explicit hook so the base
   * stays portable (the FieldValueService dependency is eav-only).
   */
  async writeCustomFields(_db, _entityId, _userId, _fields) {
  }
  /**
   * Resolve one FK from a parent external id (provider-scoped). `self` resolves
   * against `this.table`. Strict resolvers throw when unresolved; non-strict
   * return null. A null/absent write value short-circuits to null.
   */
  async resolveFk(db, fk, rawExternalId, provider) {
    const parentExternalId = rawExternalId;
    if (!parentExternalId) {
      if (fk.strict) {
        throw new Error(
          `${this.constructor.name}.integrationUpsertOne: missing required parent external id for '${fk.column}' (writeKey '${fk.writeKey}')`
        );
      }
      return null;
    }
    const refTable = fk.refTable === "self" ? this.table : fk.refTable;
    const rows = await db.select({ id: refTable["id"] }).from(refTable).where(
      and(
        eq(refTable["provider"], provider),
        eq(refTable["externalId"], parentExternalId)
      )
    ).limit(1);
    const id = rows[0]?.id ?? null;
    if (id === null && fk.strict) {
      throw new Error(
        `${this.constructor.name}.integrationUpsertOne: unresolved parent '${parentExternalId}' (provider '${provider}') for '${fk.column}' \u2014 parent not integrated yet`
      );
    }
    return id;
  }
  /**
   * Find entities visible to a user (ownership + sharing rules).
   * Concrete repositories must implement with visibility logic.
   */
  async findVisibleByUserId(_userId) {
    throw new Error("findVisibleByUserId not implemented \u2014 override in concrete repository");
  }
};

export {
  IntegratedEntityRepository
};
//# sourceMappingURL=chunk-OTDN3OUQ.js.map