Repository: deep-foundation/deepclient (language: python)

Issue:
Add generateQueryData function into query.py
```js
import Debug from 'debug';
import gql from 'graphql-tag';

const debug = Debug('deeplinks:gql:query');
const log = debug.extend('log');
const error = debug.extend('error');
// Force enable this file errors output
const namespaces = Debug.disable();
Debug.enable(`${namespaces ? `${namespaces},` : ``}${error.namespace}`);

const fieldsInputs = (tableName): IGenerateQueryFieldTypes => ({
  'distinct_on': `[${tableName}_select_column!]`,
  'limit': `Int`,
  'offset': `Int`,
  'order_by': `[${tableName}_order_by!]`,
  'where': `${tableName}_bool_exp!`,
});

export interface IGenerateQueryDataOptions {
  tableName: string;
  operation?: 'query' | 'subscription';
  queryName?: string;
  returning?: string;
  variables?: any; // TODO
}

export interface IGenerateQueryDataBuilder {
  (alias: string, index: number): IGenerateQueryDataResult
}

export interface IGenerateQueryFieldTypes {
  [field: string]: string;
}

export interface IGenerateQueryDataResult extends IGenerateQueryDataOptions {
  resultReturning: string;
  fields: string[];
  fieldTypes: IGenerateQueryFieldTypes;
  defs: string[];
  args: string[];
  alias: string;
  index: number;
  resultAlias: string;
  resultVariables: any;
}

export const generateQueryData = ({
  tableName,
  operation = 'query',
  queryName = `${tableName}`,
  returning = `id`,
  variables,
}: IGenerateQueryDataOptions): IGenerateQueryDataBuilder => {
  log('generateQuery', { tableName, operation, queryName, returning, variables });
  const fields = ['distinct_on', 'limit', 'offset', 'order_by', 'where'];
  const fieldTypes = fieldsInputs(tableName);

  return (alias: string, index: number): IGenerateQueryDataResult => {
    log('generateQueryBuilder', { tableName, operation, queryName, returning, variables, alias, index });
    const defs = [];
    const args = [];
    for (let f = 0; f < fields.length; f++) {
      const field = fields[f];
      defs.push(`$${field + index}: ${fieldTypes[field]}`);
      args.push(`${field}: $${field}${index}`);
    }
    const resultAlias = `${alias}${typeof(index) === 'number' ? index : ''}`;
    const resultVariables = {};
    for (const v in variables) {
      if (Object.prototype.hasOwnProperty.call(variables, v)) {
        const variable = variables[v];
        resultVariables[v + index] = variable;
      }
    }
    const result = {
      tableName,
      operation,
      queryName,
      returning,
      variables,
      resultReturning: returning,
      fields,
      fieldTypes,
      index,
      defs,
      args,
      alias,
      resultAlias,
      resultVariables,
    };
    log('generateQueryResult', result);
    return result
  };
};
```

Translate this code to python and put it into query.py file.


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 68bc17f..cefd0fd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -158,3 +158,5 @@ cython_debug/
 #  and can be added to the global gitignore or merged into this file.  For a more nuclear
 #  option (not recommended) you can uncomment the following to ignore the entire idea folder.
 #.idea/
+*.xml
+!/schema.graphql
diff --git a/python/deepclient/deep_client.py b/python/deepclient/deep_client.py
index e6d89f7..58659b4 100644
--- a/python/deepclient/deep_client.py
+++ b/python/deepclient/deep_client.py
@@ -2,6 +2,9 @@
 from typing import Any, Optional, Union, Dict, List
 from .deep_client_options import DeepClientOptions
 from .query import generate_query, generate_query_data
+from .gql_operations.mutation import generate_mutation_data, generate_insert_mutation, generate_delete_mutation, \
+    generate_update_mutation
+from .gql_operations.serial import generate_serial
 
 class DeepClient:
     _ids = {
@@ -448,17 +451,224 @@ def default_returning(self, table: str) -> str:
         else:
             return "id"
 
-    async def insert(self):
-        raise NotImplementedError("Method not implemented")
+    async def insert(self, objects, options: Dict = {}) -> Dict:
+        if not objects:
+            return {"error": {"message": "!record"}, "data": None, "loading": False, "networkStatus": None}
 
-    async def update(self):
-        raise NotImplementedError("Method not implemented")
+        table = options.get("table", self.table)
+        returning = options.get("returning", self.default_returning(table))
+        name = options.get("name", 'insertLinks')
 
-    async def delete(self):
-        raise NotImplementedError("Method not implemented")
+        generated_mutation = generate_insert_mutation({
+            "mutations": [
+                generate_mutation_data({
+                    "tableName": table,
+                    "returning": returning,
+                    "variables": {
+                        "input": objects,
+                    },
+                    "defs": ["$input: [links_insert_input!]!"]
+                }),
+            ],
+            "name": name,
+        })
 
-    async def serial(self):
-        raise NotImplementedError("Method not implemented")
+        print(generated_mutation)
+        m = await self.client.execute_async(generated_mutation['mutation'],
+                                            variable_values=generated_mutation['variables'])
+        data = m.get(table, [])
+        del m[table]
+        return {**m, "data": data['returning']}
+
+    async def update(self, exp: Dict, value: Dict, options: Dict = {}) -> Dict:
+        if not exp or not value:
+            return {"error": {"message": "!exp or !value"}, "data": None, "loading": False,
+                    "networkStatus": None}
+
+        table = options.get("table", self.table)
+        returning = options.get("returning", self.default_returning(table))
+        name = options.get("name", "UPDATE")
+
+        generated_mutation = generate_update_mutation({
+            "mutations": [
+                generate_mutation_data({
+                    "tableName": table,
+                    "returning": returning,
+                    "variables": {
+                        "set": value,
+                        "where": exp,
+                    }
+                }),
+            ],
+            "name": name,
+        })
+
+        m = await self.client.execute_async(generated_mutation['mutation'],
+                                            variable_values=generated_mutation['variables'])
+        data = m.get("m0", [])
+        # del m["m0"]
+        return {**m, "data": data}
+
+    async def delete(self, exp: Union[Dict, int, List[int]], options: Dict = {}) -> Dict:
+        if not exp:
+            raise ValueError('!exp')
+
+        if isinstance(exp, list):
+            where = {"id": {"_in": exp}}
+        elif isinstance(exp, dict):
+            where = self.serialize_where(exp, options.get("table", "links"))
+        else:
+            where = {"id": {"_eq": exp}}
+
+        table = options.get("table", self.table)
+        returning = options.get("returning", self.default_returning(table))
+
+        variables = options.get("variables", {})
+        name = options.get("name", 'delete')
+
+        delete_mutation = generate_delete_mutation({
+            'mutations': [
+                generate_mutation_data({
+                    'tableName': table,
+                    'operation': 'delete',
+                    'returning': returning,
+                    'variables': {
+                        **variables,
+                        'where': where,
+                    }
+                }),
+            ],
+            'name': name,
+        })
+
+        try:
+            m = await self.client.execute_async(delete_mutation['mutation'],
+                                                variable_values=delete_mutation['variables'])
+            data = m.get("object", [])
+            # del m["object"]
+            return {**m, "data": data}
+        except Exception as e:
+            # handle the exception, perhaps re-raise it after logging or cleaning up
+            raise e
+
+    async def serial(self, AsyncSerialParams: Dict):
+        if not AsyncSerialParams:
+            return {"error": {"message": "!AsyncSerialParams"}, "data": None, "loading": False, "networkStatus": None}
+        if AsyncSerialParams["operations"]:
+            operations = AsyncSerialParams["operations"]
+        else:
+            return {"error": {"message": "!operations"}, "data": None, "loading": False, "networkStatus": None}
+        if AsyncSerialParams.get("returning"):
+            returning = AsyncSerialParams["returning"]
+        else:
+            returning = "from_id id to_id type_id value"
+        if AsyncSerialParams.get("silent"):
+            silent = AsyncSerialParams["silent"]
+        else:
+            silent = False
+        operations_grouped_by_type_and_table = {}
+
+        for operation in operations:
+            if operation["type"] not in operations_grouped_by_type_and_table:
+                operations_grouped_by_type_and_table[operation["type"]] = {}
+
+            if operation["table"] not in operations_grouped_by_type_and_table[operation["type"]]:
+                operations_grouped_by_type_and_table[operation["type"]][operation["table"]] = []
+
+            operations_grouped_by_type_and_table[operation["type"]][operation["table"]].append(operation)
+
+        serial_actions = []
+
+        for operation_type, operations_grouped_by_table in operations_grouped_by_type_and_table.items():
+            for table, operations in operations_grouped_by_table.items():
+                if operation_type == 'insert':
+                    for operation in operations:
+                        if AsyncSerialParams.get("name"):
+                            name = AsyncSerialParams["name"]
+                        else:
+                            name = "insertLinks"
+                        serial_actions.append(
+                            generate_insert_mutation({
+                                "mutations": [
+                                    generate_mutation_data({
+                                        "tableName": table,
+                                        "returning": returning,
+                                        "variables": {
+                                            "input": operation["objects"],
+                                        },
+                                        "defs": ["$input: [links_insert_input!]!"]
+                                    }),
+                                ],
+                                "name": name,
+                            })
+                        )
+                elif operation_type == 'update':
+                    for operation in operations:
+                        if isinstance(operation["exp"], list):
+                            where = {"id": {"_in": operation["exp"]}}
+                        elif isinstance(operation["exp"], dict):
+                            where = self.serialize_where(operation["exp"])
+                        else:
+                            where = {"id": {"_eq": operation["exp"]}}
+                        if AsyncSerialParams.get("name"):
+                            name = AsyncSerialParams["name"]
+                        else:
+                            name = "updateLinks"
+                        serial_actions.append(
+                            generate_update_mutation({
+                                "mutations": [
+                                    generate_mutation_data({
+                                        "tableName": table,
+                                        "returning": returning,
+                                        "variables": {
+                                            "set": operation["set"],
+                                            "where": where,
+                                        }
+                                    }),
+                                ],
+                                "name": name,
+                            })
+                        )
+                elif operation_type == 'delete':
+                    for operation in operations:
+                        if isinstance(operation["exp"], list):
+                            where = {"id": {"_in": operation["exp"]}}
+                        elif isinstance(operation["exp"], dict):
+                            where = self.serialize_where(operation["exp"])
+                        else:
+                            where = {"id": {"_eq": operation["exp"]}}
+                        if AsyncSerialParams.get("name"):
+                            name = AsyncSerialParams["name"]
+                        else:
+                            name = "deleteLinks"
+                        serial_actions.append(
+                            generate_delete_mutation({
+                                'mutations': [
+                                    generate_mutation_data({
+                                        'tableName': table,
+                                        'operation': 'delete',
+                                        'returning': returning,
+                                        'variables': {
+                                            'where': where,
+                                        }
+                                    }),
+                                ],
+                                'name': name,
+                            })
+                        )
+        response = {"data": []}
+        for action in serial_actions:
+            try:
+                response_part = await self.client.execute_async(
+                    action["mutation"],
+                    variable_values=action['variables'])
+                response_part = response_part["links"]["returning"][0]
+                response["data"].append(response_part)
+            except Exception as e:
+                if not silent:
+                    raise e
+                return {"error": str(e)}
+        return response
 
     async def reserve(self):
         raise NotImplementedError("Method not implemented")
diff --git a/python/deepclient/gql_operations/__init__.py b/python/deepclient/gql_operations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/python/deepclient/gql_operations/mutation.py b/python/deepclient/gql_operations/mutation.py
new file mode 100644
index 0000000..61184b4
--- /dev/null
+++ b/python/deepclient/gql_operations/mutation.py
@@ -0,0 +1,107 @@
+import logging
+from typing import Any, Callable, Dict, List, Optional, Tuple, Union
+
+from gql import gql
+
+
+def fields_inputs(fields: Dict[str, Any]) -> str:
+    return ",".join([f'{k}:{v}' for k, v in fields.items()])
+
+
+def generate_mutation_data(options: Dict[str, Any]) -> Dict[str, Any]:
+    tableName = options.get("tableName", "")
+    returning = options.get("returning", "")
+    variables = options.get("variables", {})
+    defs = options.get("defs", [])
+    return {
+        "resultAlias": tableName,
+        "queryName": f"insert_{tableName}",
+        "tableName": tableName,
+        "returning": returning,
+        "variables": variables,
+        "defs": defs,
+    }
+
+
+def generate_insert_mutation(options: Dict[str, Union[str, List[Any]]]) -> Dict[str, Any]:
+    mutations = options.get("mutations", [])
+    operation = options.get("operation", "mutation")
+    name = options.get("name", "INSERT")
+
+    defs = ",".join([",".join(m.get("defs", [])) for m in mutations])
+    mutation_bodies = [
+        f'{m["resultAlias"]}: insert_{m["tableName"]}(objects: $input) {{ returning {{ {m["returning"]} }} }}' for m
+        in mutations]
+    mutation_string = f"{operation} {name}({defs}) {{{','.join(mutation_bodies)}}}"
+    mutation = gql(mutation_string)
+
+    result_variables = {}
+    for m in mutations:
+        for v, variable in m["variables"].items():
+            result_variables[v] = variable
+
+    return {
+        "mutation": mutation,
+        "variables": result_variables,
+        "mutation_string": mutation_string,
+    }
+
+
+def generate_update_mutation(options: Dict[str, Union[str, List[Any]]]) -> Dict[str, Any]:
+    mutations = options.get("mutations", [])
+    operation = options.get("operation", "mutation")
+    name = options.get("name", "UPDATE")
+
+    defs = ",".join([",".join(m.get("defs", [])) for m in mutations])
+
+    mutation_bodies = []
+    for m in mutations:
+        returning = "returning {" + f"{m['returning']}" + "}"
+        mutation_body = f'{m["resultAlias"]}: update_{m["tableName"]}(where: $where, _set: $set) {{ affected_rows {returning}}}'
+        mutation_bodies.append(mutation_body)
+
+    mutation_string = f"{operation} {name}($where: links_bool_exp!, $set: links_set_input) {{{','.join(mutation_bodies)}}}"
+    mutation = gql(mutation_string)
+
+    result_variables = {}
+    for m in mutations:
+        for v, variable in m["variables"].items():
+            result_variables[v] = variable
+
+    return {
+        "mutation": mutation,
+        "variables": result_variables,
+        "mutation_string": mutation_string,
+    }
+
+
+def generate_delete_mutation(options: Dict[str, Union[str, List[Any]]]) -> Dict[str, Any]:
+    mutations = options.get("mutations", [])
+    operation = options.get("operation", "mutation")
+    name = options.get("name", "DELETE")
+
+    called_mutations = [m("mutation", i) if callable(m) else m for i, m in enumerate(mutations)]
+    defs = ",".join([",".join(m["defs"]) for m in called_mutations])
+
+    mutation_bodies = []
+    result_variables = {}
+    variable_defs = []
+
+    for m in called_mutations:
+        returning = "returning {" + f"{m['returning']}" + "}"
+        variable_name = f"{m['resultAlias']}Where"
+        mutation_bodies.append(f'{m["resultAlias"]}: delete_{m["tableName"]}(where: ${variable_name}) {{affected_rows {returning}}}')
+        variable_defs.append(f'${variable_name}: links_bool_exp!')  # replace with your actual type
+
+        for v, variable in m["variables"].items():
+            result_variables[variable_name] = variable
+
+    mutation_string = f"{operation} {name}({','.join(variable_defs)}) {{{','.join(mutation_bodies)}}}"
+    mutation = gql(mutation_string)
+
+    return {
+        "mutation": mutation,
+        "variables": result_variables,
+        "mutation_string": mutation_string,
+    }
+
diff --git a/python/deepclient/gql_operations/serial.py b/python/deepclient/gql_operations/serial.py
new file mode 100644
index 0000000..4e20881
--- /dev/null
+++ b/python/deepclient/gql_operations/serial.py
@@ -0,0 +1,50 @@
+from typing import Any, Dict, List, Union
+
+
+class ISerialOptions:
+    actions: List[Any]
+    name: str
+    alias: str
+
+    def __init__(self, actions=[], name='SERIAL', alias='m', **options: Dict[str, Any]):
+        self.actions = actions
+        self.name = name
+        self.alias = alias
+        self.options = options
+
+
+class ISerialResult:
+    mutation: Any
+    mutation_string: str
+    variables: Dict[str, Any]
+
+
+def generate_serial(serial_options: ISerialOptions) -> ISerialResult:
+    print('generateSerial',
+          {'name': serial_options.name, 'alias': serial_options.alias, 'actions': serial_options.actions})
+    called_actions = [action(serial_options.alias, i) if callable(action) else action for i, action in
+                      enumerate(serial_options.actions)]
+    defs = ','.join([','.join(m.get('defs', [])) for m in called_actions])
+    mutation_string = (
+            f"mutation {serial_options.name} ({defs}) " +
+            ''.join([
+                f"{m['resultAlias']}: {m['queryName']}({', '.join(m['args'])}) {{ {m['resultReturning']} }}"
+                for m in called_actions
+            ])
+    )
+
+    mutation = f"gql{mutation_string}"
+    variables = {}
+    for action in called_actions:
+        for v, variable in action.get('resultVariables', {}).items():
+            if v not in variables:
+                variables[v] = variable
+
+    result = ISerialResult()
+    result.mutation = mutation
+    result.mutation_string = mutation_string
+    result.variables = variables
+    result.options = serial_options.options
+
+    print('generateSerialResult', {"mutation": mutation_string, "variables": variables})
+    return result
diff --git a/python/setup.py b/python/setup.py
index 4c68e82..da983e3 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -2,7 +2,7 @@
 
 setup(
     name="deepclient",
-    version="0.0.3",
+    version="1.0.1",
     packages=find_packages(),
     install_requires=[
         # Add your package dependencies here


Test patch:
diff --git a/python/tests/test_deep_client.py b/python/tests/test_deep_client.py
index 4023daf..e4f138c 100644
--- a/python/tests/test_deep_client.py
+++ b/python/tests/test_deep_client.py
@@ -1,5 +1,7 @@
 import unittest
 import asyncio
+import os
+from dotenv import load_dotenv
 from deepclient import DeepClient, DeepClientOptions
 from gql import gql, Client
 from gql.transport.aiohttp import AIOHTTPTransport
@@ -7,9 +9,10 @@
 class TestDeepClient(unittest.IsolatedAsyncioTestCase):
 
     def setUp(self):
+        load_dotenv()
         transport = AIOHTTPTransport(
-            url='https://3006-deepfoundation-dev-kgyolopnp3g.ws-eu96b.gitpod.io/gql',
-            headers={'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwczovL2hhc3VyYS5pby9qd3QvY2xhaW1zIjp7IngtaGFzdXJhLWFsbG93ZWQtcm9sZXMiOlsiYWRtaW4iXSwieC1oYXN1cmEtZGVmYXVsdC1yb2xlIjoiYWRtaW4iLCJ4LWhhc3VyYS11c2VyLWlkIjoiMzc4In0sImlhdCI6MTY4MTMwNTA3OX0.Gr6wEG9VxMZ4mLqTEkZfN9kIYAjAXGm1r5YCXJTKRws'}
+            url=os.getenv('URL_GQL'),
+            headers={'Authorization': 'Bearer %s' % (os.getenv('BEARER_TOKEN'))}
         )
         client = Client(transport=transport, fetch_schema_from_transport=True)
 
@@ -22,7 +25,7 @@ def test_initialization(self):
 
     async def test_methods_raise_not_implemented(self):
         async_methods = [
-            'insert', 'update', 'delete', 'serial', 'reserve', 'wait_for',
+            'reserve', 'wait_for',
             'guest', 'jwt', 'whoami', 'login', 'logout', 'can', 'name'
         ]
         sync_methods = [
@@ -128,6 +131,272 @@ async def test_select(self):
         assert (await self.client.select({ "id": 1 }))['data'][0] == {'id': 1, 'type_id': 1, 'from_id': 8, 'to_id': 8, 'value': None}
         assert (await self.client.select({ "id": { "_eq": 1 } }))['data'][0] == {'id': 1, 'type_id': 1, 'from_id': 8, 'to_id': 8, 'value': None}
 
+    async def test_insert(self):
+        new_record = {"type_id": 58, "from_id": 0, "to_id": 0}
+        insert_result = await self.client.insert(new_record)
+        insert_result_data = insert_result['data'][0]
+        assert insert_result_data["type_id"] == new_record["type_id"]
+        assert insert_result_data["from_id"] == new_record["from_id"]
+        assert insert_result_data["to_id"] == new_record["to_id"]
+        select_result = await self.client.select({"id": insert_result_data['id']})
+        select_result_data = select_result['data'][0]
+        assert select_result_data["type_id"] == new_record["type_id"]
+        assert select_result_data["from_id"] == new_record["from_id"]
+        assert select_result_data["to_id"] == new_record["to_id"]
+        new_record2 = {"type_id": 59, "from_id": 0, "to_id": 0}
+        record_list = [new_record, new_record2]
+        insert_result = await self.client.insert(record_list)
+
+
+    async def test_delete(self):
+        new_record = {"type_id": 58, "from_id": 0, "to_id": 0}
+        insert_result = await self.client.insert(new_record)
+        insert_result_data = insert_result['data'][0]
+        delete_result = await self.client.delete({"id": insert_result_data['id']})
+        select_result = await self.client.select({"id": insert_result_data['id']})
+        assert select_result['data'] == []
+
+    async def test_update(self):
+        new_record = {"type_id": 58, "from_id": 0, "to_id": 0}
+        insert_result = await self.client.insert(new_record)
+        insert_result_data = insert_result['data'][0]
+        updated_record = {"type_id": 59, "from_id": 1, "to_id": 1}
+        update_result = await self.client.update({"id": {"_eq": insert_result_data['id']}}, updated_record)
+        select_result = await self.client.select({"id": {"_eq": insert_result_data['id']}})
+        select_result_data = select_result['data'][0]
+        assert select_result_data["type_id"] == updated_record["type_id"]
+        assert select_result_data["from_id"] == updated_record["from_id"]
+        assert select_result_data["to_id"] == updated_record["to_id"]
+
+    async def test_serial_insert(self):
+        typeTypeLinkId = await self.client.id("@deep-foundation/core", "Type")
+        # One insert test
+        linkIdsToDelete = []
+        objects = {
+            "type_id": 58, "from_id": 0, "to_id": 0
+        }
+        try:
+            operation = {
+                "table": 'links',
+                "type": 'insert',
+                "objects": objects
+            }
+            response = await self.client.serial({
+                "operations": [
+                    operation,
+                ]
+            })
+            for link in response["data"]:
+                linkIdsToDelete.append(link["id"])
+                assert link["type_id"] == objects["type_id"]
+                assert link["from_id"] == objects["from_id"]
+                assert link["to_id"] == objects["to_id"]
+        finally:
+            for i in linkIdsToDelete:
+                await self.client.delete({"id": i})
+
+        # Multiple inserts in one operation test
+        linkIdsToDelete = []
+        try:
+            result = await self.client.serial({
+                "operations": [
+                    operation,
+                    operation
+                ]
+            })
+            assert len(result["data"]) == 2
+            for link in result["data"]:
+                assert link
+                linkIdsToDelete.append(link["id"])
+        finally:
+            for i in linkIdsToDelete:
+                await self.client.delete({"id": i})
+
+    async def test_serial_update(self):
+        typeTypeLinkId = await self.client.id("@deep-foundation/core", "Type")
+
+        # One update test
+        linkIdsToDelete = []
+        try:
+            insert_result = await self.client.insert({
+                "type_id": typeTypeLinkId,
+                "string": {
+                    "data": {
+                        "value": "stringValue"
+                    }
+                }
+            })
+
+            newLinkId = insert_result["data"][0]["id"]
+            updated_record = {"type_id": 59, "from_id": 1, "to_id": 1}
+
+            linkIdsToDelete.append(newLinkId)
+            operation = {
+                "table": 'links',
+                "type": 'update',
+                "exp": {
+                    "id": newLinkId,
+                },
+                "set": updated_record
+            }
+
+            updateResult = await self.client.serial({
+                "operations": [operation]
+            })
+            self.assertEqual(len(updateResult["data"]), 1)
+            for link in updateResult["data"]:
+                self.assertEqual(link['type_id'], updated_record['type_id'])
+                self.assertEqual(link["from_id"], updated_record["from_id"])
+                self.assertEqual(link["to_id"], updated_record["to_id"])
+
+        finally:
+            for i in linkIdsToDelete:
+                await self.client.delete({"id": i})
+        # Two update test
+
+        # One update test
+        linkIdsToDelete = []
+        try:
+            insert_result_one = await self.client.insert({
+                "type_id": typeTypeLinkId,
+                "string": {
+                    "data": {
+                        "value": "stringValue"
+                    }
+                }
+            })
+            insert_result_two = await self.client.insert({
+                "type_id": typeTypeLinkId,
+                "string": {
+                    "data": {
+                        "value": "stringValue"
+                    }
+                }
+            })
+            newLinkId_one = insert_result_one["data"][0]["id"]
+            newLinkId_two = insert_result_two["data"][0]["id"]
+            updated_record = {"type_id": 59, "from_id": 1, "to_id": 1}
+
+            linkIdsToDelete.append(newLinkId_one)
+            linkIdsToDelete.append(newLinkId_two)
+            operation_one = {
+                "table": 'links',
+                "type": 'update',
+                "exp": {
+                    "id": newLinkId_one,
+                },
+                "set": updated_record
+            }
+            operation_two = {
+                "table": 'links',
+                "type": 'update',
+                "exp": {
+                    "id": newLinkId_two,
+                },
+                "set": updated_record
+            }
+
+            updateResult = await self.client.serial({
+                "operations": [operation_one, operation_two]
+            })
+            self.assertEqual(len(updateResult["data"]), 2)
+            for link in updateResult["data"]:
+                self.assertEqual(link['type_id'], updated_record['type_id'])
+                self.assertEqual(link["from_id"], updated_record["from_id"])
+                self.assertEqual(link["to_id"], updated_record["to_id"])
+
+        finally:
+            for i in linkIdsToDelete:
+                await self.client.delete({"id": i})
+
+    async def test_serial_delete(self):
+        typeTypeLinkId = await self.client.id("@deep-foundation/core", "Type")
+
+        # One delete test
+        try:
+            result = await self.client.insert({
+                "type_id": typeTypeLinkId,
+                "string": {
+                    "data": {
+                        "value": "stringValue"
+                    }
+                }
+            })
+            newLinkId = result["data"][0]["id"]
+
+            operation = {
+                "table": 'links',
+                "type": 'delete',
+                "exp": {
+                    "id": newLinkId
+                }
+            }
+
+            deleteResult = await self.client.serial({
+                "operations": [operation]
+            })
+            self.assertEqual(len(deleteResult["data"]), 1)
+
+            for link in result["data"]:
+                self.assertEqual(link["id"], newLinkId)
+
+            newLink = await self.client.select(newLinkId)
+            newLinkData = newLink["data"]
+            self.assertEqual(len(newLinkData), 0)
+        except Exception:
+            print("Error occurred")
+        # Two delete test
+        try:
+            result_one = await self.client.insert({
+                "type_id": typeTypeLinkId,
+                "string": {
+                    "data": {
+                        "value": "stringValue"
+                    }
+                }
+            })
+            newLinkId_one = result_one["data"][0]["id"]
+            result_two = await self.client.insert({
+                "type_id": typeTypeLinkId,
+                "string": {
+                    "data": {
+                        "value": "stringValue"
+                    }
+                }
+            })
+            newLinkId_two = result_two["data"][0]["id"]
+
+            operation_one = {
+                "table": 'links',
+                "type": 'delete',
+                "exp": {
+                    "id": newLinkId_one
+                }
+            }
+            operation_two = {
+                "table": 'links',
+                "type": 'delete',
+                "exp": {
+                    "id": newLinkId_two
+                }
+            }
+            deleteResult = await self.client.serial({
+                "operations": [operation_one, operation_two]
+            })
+            self.assertEqual(len(deleteResult["data"]), 2)
+
+            self.assertEqual(deleteResult["data"]["id"][0], newLinkId_one)
+            self.assertEqual(deleteResult["data"]["id"][1], newLinkId_two)
+
+            newLink = await self.client.select(newLinkId_one)
+            newLinkData = newLink["data"]
+            self.assertEqual(len(newLinkData), 0)
+            newLink = await self.client.select(newLinkId_two)
+            newLinkData = newLink["data"]
+            self.assertEqual(len(newLinkData), 0)
+        except Exception:
+            print("Error occurred")
+
     async def test_id(self):
         assert (await self.client.id("@deep-foundation/core", "Package")) == 2
         assert (await self.client.id("@deep-foundation/core", "Contain")) == 3