= Migration from the Legacy API
:description: Migration from TcpClient/Client to the ECM API (ECMSync/ECMAsync). Maps deprecated methods like lol_query() and xml_import() to their ECM API equivalents.
:keywords: migration, TcpClient, Client, ECMSync, ECMAsync, lol_query, xml_import, legacy API
:page-tags: guide

This page maps each method of the deprecated `Client` abstract base class (from `ecmind_blue_client.client`) to its equivalent in the current xref:../api/index.adoc[ECM API].

The old API was built around raw `Job` objects and untyped `dict` results.
The new API uses typed model classes, fluent query builders, and structured exceptions.

== Setup

[tabs]
====
Legacy::
+
[source,python]
----
from ecmind_blue_client.tcp_client import Connection

client = Connection(host, port, "App", user, password, ssl)
result = client.execute(job)
----

ECM API::
+
[source,python]
----
from ecmind_blue_client.pool import SyncPoolClient
from ecmind_blue_client.ecm import ECM

client = SyncPoolClient(servers="host:4000:1", username=user, password=password)
ecm = ECM(client)
----
====

The `SyncPoolClient` manages a connection pool automatically.
Use `AsyncPoolClient` in asyncio contexts (FastAPI, etc.).

== Method mapping

=== `lol_query()` → `ecm.dms.select_lol()`

`lol_query()` executed paginated flat-list (LOL) queries and yielded untyped `dict` rows.
The direct equivalent in the new API is `ecm.dms.select_lol()`, which uses the same LOL format but returns typed model instances.

[tabs]
====
Legacy::
+
[source,python]
----
from ecmind_blue_client.query_condition_group import QueryConditionGroup

results = list(client.lol_query(
    "InvoiceFolder",
    conditions=QueryConditionGroup("InvoiceFolder", [("Year", "=", "2024")]),
    result_fields=["Title", "Year"],
    page_size=100,
))
for row in results:
    print(row["Title"], row["Year"])
----

ECM API::
+
[source,python]
----
from ecmind_blue_client.ecm.model import make_folder_model

InvoiceFolder = make_folder_model("InvoiceFolder")

for folder in ecm.dms.select_lol(InvoiceFolder).where(
    InvoiceFolder["Year"] == 2024
).stream():
    print(folder["Title"], folder["Year"])
----
====

[TIP]
The new API also supports hierarchical (HOL) queries via `ecm.dms.select()`.
HOL returns richer object graphs with parent/child relationships and is the recommended default
for most use cases.

For static type checking, declare the model class explicitly instead of using `make_folder_model`:

[source,python]
----
from ecmind_blue_client.ecm.model import ECMFolderModel, ECMField

class InvoiceFolder(ECMFolderModel):
    _internal_name_ = "InvoiceFolder"
    Title: ECMField[str]
    Year: ECMField[int]

# LOL — flat list, same format as lol_query()
for folder in ecm.dms.select_lol(InvoiceFolder).where(
    InvoiceFolder.Year == 2024
).stream():
    print(folder.Title, folder.Year)

# HOL — hierarchical, recommended for most queries
for folder in ecm.dms.select(InvoiceFolder).where(
    InvoiceFolder.Year == 2024
).order_by(InvoiceFolder.Year.DESC).stream():
    print(folder.Title, folder.Year)
----

See xref:../api/dms/select_lol.adoc[`ecm.dms.select_lol()`] and xref:../api/dms/select.adoc[`ecm.dms.select()`].

=== `xml_import()` → `ecm.dms.insert()` / `update()` / `upsert()`

`xml_import()` combined insert and update logic via `action0`/`action1` parameters.
The new API splits these into separate, clearly named methods.

==== Insert

[tabs]
====
Legacy::
+
[source,python]
----
from ecmind_blue_client.const import ImportActions

client.xml_import(
    "InvoiceFolder",
    search_fields={},
    import_fields={"Title": "Invoice 2024", "Year": "2024"},
    action0=ImportActions.INSERT,
    action1=ImportActions.ERROR,
)
----

ECM API::
+
[source,python]
----
folder_id, type_id = ecm.dms.insert(InvoiceFolder(Title="Invoice 2024", Year=2024))
# or get the object back directly:
folder = ecm.dms.insert_and_get(InvoiceFolder(Title="Invoice 2024", Year=2024))
----
====

==== Insert or update (upsert)

[tabs]
====
Legacy::
+
[source,python]
----
from ecmind_blue_client.const import ImportActions

client.xml_import(
    "InvoiceFolder",
    search_fields={"Title": "Invoice 2024"},
    import_fields={"Year": "2024"},
    action0=ImportActions.INSERT,
    action1=ImportActions.UPDATE,
)
----

ECM API::
+
[source,python]
----
object_id, type_id, hits, action = (
    ecm.dms.upsert(InvoiceFolder(Title="Invoice 2024", Year=2024))
    .search(InvoiceFolder.Title == "Invoice 2024")
    .execute()
)
----
====

==== Multiple hits

When the search finds more than one matching object, the default behaviour is to return
an error.
Use `.action_multiple()` to handle that case explicitly:

[source,python]
----
from ecmind_blue_client.const import ImportActions

object_id, type_id, hits, action = (
    ecm.dms.upsert(InvoiceFolder(Title="Invoice 2024", Year=2024))
    .search(InvoiceFolder.Title == "Invoice 2024")
    .action_multiple(ImportActions.UPDATE)  # update all matches
    .execute()
)
----

[NOTE]
The `main_type`, `variant_parent_id`, and `options` parameters of `xml_import()` have
no direct equivalent in the high-level upsert builder.
Use `ecm.execute()` as a low-level escape hatch for operations that require these
parameters.

See xref:../api/dms/insert.adoc[`insert()`], xref:../api/dms/update.adoc[`update()`], xref:../api/dms/upsert.adoc[`upsert()`].

=== `get_object_details()` → `ecm.dms.get()`

[tabs]
====
Legacy::
+
[source,python]
----
from ecmind_blue_client.const import SystemFields

details = client.get_object_details(
    "InvoiceFolder",
    object_id=42,
    system_fields=[SystemFields.OBJECT_ID],
)
print(details["Title"])
----

ECM API::
+
[source,python]
----
folder = ecm.dms.get(InvoiceFolder, object_id=42)
print(folder.Title)
print(folder.system.id)
----
====

See xref:../api/dms/get.adoc[`ecm.dms.get()`].

=== `store_in_cache()` / `store_in_cache_by_id()` → `ecm.dms.files()`

`store_in_cache()` and `store_in_cache_by_id()` returned raw `ResultFile` objects.
`ecm.dms.files()` unifies both methods and returns structured file metadata.

[tabs]
====
Legacy::
+
[source,python]
----
files = client.store_in_cache(object_id=42)
for f in files:
    data = f.bytes()
----

ECM API::
+
[source,python]
----
files = ecm.dms.files(document)
for f in files:
    data = f.bytes()
----
====

==== With file conversion (`store_in_cache_by_id`)

[tabs]
====
Legacy::
+
[source,python]
----
from ecmind_blue_client.const import StoreInCacheByIdConversion

files = client.store_in_cache_by_id(
    object_id=42, convert=StoreInCacheByIdConversion.PDF
)
----

ECM API::
+
[source,python]
----
from ecmind_blue_client.const import StoreInCacheByIdConversion

files = ecm.dms.files(document, convert=StoreInCacheByIdConversion.PDF)
# or by plain object ID:
files = ecm.dms.files(42, convert=StoreInCacheByIdConversion.PDF)
----
====

`StoreInCacheByIdConversion` supports `NONE` (default), `PDF` (main types 1–4), and
`MULTIPAGE_TIFF` (TIFF main types 2–3 merged into one file).
Additional parameters not present in the legacy API: `when_cold_then_tiff` (return
COLD/ASCII files as TIFF) and `add_annotations` (burn annotations into images).

[NOTE]
The `checkout` parameter of `store_in_cache()` has no equivalent in `ecm.dms.files()`.
Use `ecm.execute()` with the raw job if checkout behaviour is required.

See xref:../api/dms/files.adoc[`ecm.dms.files()`].

=== `get_object_type_by_id()` → `ecm.dms.get_object_type_by_id()`

Both APIs expose this method with the same signature.

[tabs]
====
Legacy::
+
[source,python]
----
type_id = client.get_object_type_by_id(object_id=42)
----

ECM API::
+
[source,python]
----
type_id = ecm.dms.get_object_type_by_id(object_id=42)
----
====

See xref:../api/dms/get_object_type_by_id.adoc[`ecm.dms.get_object_type_by_id()`].

=== `execute_sql()` → `ecm.db.select()`

[tabs]
====
Legacy::
+
[source,python]
----
rows = client.execute_sql("SELECT * FROM invoices WHERE year = ?", 2024)
for row in rows:
    print(row["id"], row["title"])
----

ECM API::
+
[source,python]
----
result = ecm.db.select("SELECT * FROM invoices WHERE year = ?", 2024)
for row in result.rows:
    print(row["id"], row["title"])
----
====

See xref:../api/db/select.adoc[`ecm.db.select()`].

=== `execute()` — low-level escape hatch

Both APIs retain a raw `execute()` method for jobs not yet covered by the high-level API.

[tabs]
====
Legacy::
+
[source,python]
----
from ecmind_blue_client import Job

result = client.execute(Job("krn.GetServerInfo", Flags=0, Info=6))
print(result.values["Value"])
----

ECM API::
+
[source,python]
----
from ecmind_blue_client.rpc import Jobs

result = ecm.execute(Jobs.KRN_GETSERVERINFO, Flags=0, Info=6)
print(result.values["Value"])
----
====

== User context switching

The legacy API passed `context_user` as a parameter to individual methods.
The new API uses a context manager that injects the user into every operation inside the block.

[tabs]
====
Legacy::
+
[source,python]
----
client.xml_import(..., context_user="john")
----

ECM API::
+
[source,python]
----
with ecm.impersonate("john") as ecm_john:
    ecm_john.dms.insert(InvoiceFolder(Title="Invoice 2024"))
----
====

== New capabilities in the ECM API

The following methods are available in `ecm.dms` but have no equivalent in the legacy
`Client` base class.

=== Delete, move, copy

[source,python]
----
# Delete (soft delete by default; hard_delete=True bypasses the recycle bin)
ecm.dms.delete(folder)
ecm.dms.delete(folder, hard_delete=True, delete_cascading=True)

# Move a document to a different register
ecm.dms.move(document, folder_id=target_folder, register_id=target_register)

# Copy a folder including all children
ecm.dms.copy(folder, folder_id=target_folder, copy_cascading=True)

# Create a linked copy of a document (second location, shared index record)
ecm.dms.copy(document, folder_id=target_folder, register_id=target_register, link_document=True)
----

=== Advanced query options (`ecm.dms.select()`)

`ecm.dms.select()` exposes HOL query modifiers with no legacy equivalent:

[source,python]
----
# Include rights, base parameters, and file properties in the response
results = (
    ecm.dms.select(InvoiceFolder)
    .where(InvoiceFolder.Year == 2024)
    .rights()
    .base_params()
    .execute()
)

# Hierarchical traversal
results = ecm.dms.select(InvoiceFolder).with_children().execute()
results = ecm.dms.select(InvoiceFolder).with_parents().execute()

# Include document variants
results = ecm.dms.select(InvoiceDocument).variants().execute()

# Query the recycle bin
results = ecm.dms.select(InvoiceFolder).garbage_mode().execute()
----
