From 6e763aa279a7d3d08ab2679c69e030f46cb3e5e0 Mon Sep 17 00:00:00 2001 From: StevenMHernandez Date: Wed, 27 Jun 2018 12:28:48 -0400 Subject: [PATCH] begin transition from harvest api v1 to v2 * update authentication method required for new v2 of the api * add additional required config variables for tap usage * update access_token request to comply with v2 of api * update schemas to reflect data returned from the v2 api * add additional schemas to reflect additional data points available @See https://help.getharvest.com/api-v2/ @See https://github.com/singer-io/tap-harvest/issues/9 --- .gitignore | 1 + setup.py | 15 +- tap_harvest/__init__.py | 464 +++++++++++++----- tap_harvest/schemas/clients.json | 27 +- tap_harvest/schemas/contacts.json | 10 +- .../schemas/estimate_item_categories.json | 19 + tap_harvest/schemas/estimate_line_items.json | 32 ++ tap_harvest/schemas/estimate_messages.json | 43 ++ tap_harvest/schemas/estimates.json | 80 +++ tap_harvest/schemas/expense_categories.json | 6 +- tap_harvest/schemas/expenses.json | 59 +-- tap_harvest/schemas/external_reference.json | 23 + .../schemas/invoice_item_categories.json | 12 +- tap_harvest/schemas/invoice_line_items.json | 32 ++ tap_harvest/schemas/invoice_messages.json | 48 +- tap_harvest/schemas/invoice_payments.json | 25 +- tap_harvest/schemas/invoice_recipients.json | 20 + tap_harvest/schemas/invoices.json | 106 ++-- tap_harvest/schemas/project_tasks.json | 13 +- tap_harvest/schemas/project_users.json | 15 +- tap_harvest/schemas/projects.json | 54 +- tap_harvest/schemas/roles.json | 19 + tap_harvest/schemas/tasks.json | 18 +- tap_harvest/schemas/time_entries.json | 73 ++- .../time_entry_external_reference.json | 11 + tap_harvest/schemas/user_projects.json | 37 ++ tap_harvest/schemas/user_roles.json | 11 + .../schemas/{people.json => users.json} | 56 ++- 28 files changed, 958 insertions(+), 371 deletions(-) create mode 100644 tap_harvest/schemas/estimate_item_categories.json create mode 100644 tap_harvest/schemas/estimate_line_items.json create mode 100644 tap_harvest/schemas/estimate_messages.json create mode 100644 tap_harvest/schemas/estimates.json create mode 100644 tap_harvest/schemas/external_reference.json create mode 100644 tap_harvest/schemas/invoice_line_items.json create mode 100644 tap_harvest/schemas/invoice_recipients.json create mode 100644 tap_harvest/schemas/roles.json create mode 100644 tap_harvest/schemas/time_entry_external_reference.json create mode 100644 tap_harvest/schemas/user_projects.json create mode 100644 tap_harvest/schemas/user_roles.json rename tap_harvest/schemas/{people.json => users.json} (80%) diff --git a/.gitignore b/.gitignore index 8e101be..00309ac 100644 --- a/.gitignore +++ b/.gitignore @@ -95,4 +95,5 @@ ENV/ # Custom stuff env.sh config.json +state.json .autoenv.zsh diff --git a/setup.py b/setup.py index 9343e96..381717a 100644 --- a/setup.py +++ b/setup.py @@ -24,17 +24,28 @@ 'tap_harvest/schemas': [ "clients.json", "contacts.json", + "estimate_item_categories.json", + "estimate_line_items.json", + "estimate_messages.json", + "estimates.json", "expense_categories.json", "expenses.json", + "external_reference.json", "invoice_item_categories.json", - "invoice_payments.json" + "invoice_messages.json", + "invoice_payments.json", + "invoice_recipients.json", "invoices.json", - "people.json", "project_tasks.json", "project_users.json", "projects.json", + "roles.json", "tasks.json", "time_entries.json", + "time_entry_external_reference.json", + "user_projects.json", + "user_roles.json", + "users.json", ], }, include_package_data=True, diff --git a/tap_harvest/__init__.py b/tap_harvest/__init__.py index 5d98ec0..26c9ca8 100644 --- a/tap_harvest/__init__.py +++ b/tap_harvest/__init__.py @@ -17,10 +17,11 @@ "refresh_token", "client_id", "client_secret", - "account_name", + "user_agent", + "account_id", ] -BASE_URL = "https://{}.harvestapp.com/" +BASE_URL = "https://api.harvestapp.com/v2/" CONFIG = {} STATE = {} AUTH = {} @@ -40,11 +41,13 @@ def __init__(self, client_id, client_secret, refresh_token): factor=2) def _make_refresh_token_request(self): return requests.request('POST', - url='https://api.harvestapp.com/oauth2/token', - data={'client_id': self._client_id, - 'client_secret': self._client_secret, - 'refresh_token': self._refresh_token, - 'grant_type': 'refresh_token'}, + url='https://id.getharvest.com/api/v2/oauth2/token', + data={ + 'client_id': self._client_id, + 'client_secret': self._client_secret, + 'refresh_token': self._refresh_token, + 'grant_type': 'refresh_token', + }, headers={"User-Agent": CONFIG.get("user_agent")}) def _refresh_access_token(self): @@ -88,7 +91,7 @@ def get_start(key): def get_url(endpoint): - return BASE_URL.format(CONFIG['account_name']) + endpoint + return BASE_URL + endpoint @backoff.on_exception( backoff.expo, @@ -101,6 +104,7 @@ def request(url, params=None): params = params or {} access_token = AUTH.get_access_token() headers = {"Accept": "application/json", + "Harvest-Account-Id": CONFIG.get("account_id"), "Authorization": "Bearer " + access_token, "User-Agent": CONFIG.get("user_agent")} req = requests.Request("GET", url=url, params=params, headers=headers).prepare() @@ -115,131 +119,101 @@ def append_times_to_dates(item, date_fields): if item.get(date_field): item[date_field] += "T00:00:00Z" -def sync_endpoint(endpoint, path, date_fields=None): - schema = load_schema(endpoint) +def sync_endpoint(schema_name, endpoint=None, path=None, date_fields=None): + schema = load_schema(schema_name) bookmark_property = 'updated_at' - singer.write_schema(endpoint, + singer.write_schema(schema_name, schema, ["id"], bookmark_properties=[bookmark_property]) - start = get_start(endpoint) + start = get_start(schema_name) - url = get_url(endpoint) + url = get_url(endpoint or schema_name) data = request(url) + data = data[path or schema_name] time_extracted = utils.now() with Transformer() as transformer: for row in data: - item = row[path] - item = transformer.transform(item, schema) + item = transformer.transform(row, schema) append_times_to_dates(item, date_fields) if item[bookmark_property] >= start: - singer.write_record(endpoint, + singer.write_record(schema_name, item, time_extracted=time_extracted) - utils.update_state(STATE, endpoint, item[bookmark_property]) + utils.update_state(STATE, schema_name, item[bookmark_property]) singer.write_state(STATE) -def sync_projects(): +def sync_time_entries(): + external_reference_schema = load_schema("external_reference") bookmark_property = 'updated_at' - tasks_schema = load_schema("project_tasks") - singer.write_schema("project_tasks", - tasks_schema, + singer.write_schema("external_reference", + external_reference_schema, ["id"], bookmark_properties=[bookmark_property]) - users_schema = load_schema("project_users") - singer.write_schema("project_users", - users_schema, + time_entry_external_reference_schema = load_schema("time_entry_external_reference") + singer.write_schema("time_entry_external_reference", + time_entry_external_reference_schema, ["id"], bookmark_properties=[bookmark_property]) - entries_schema = load_schema("time_entries") + schema = load_schema("time_entries") singer.write_schema("time_entries", - entries_schema, - ["id"], - bookmark_properties=[bookmark_property]) - - schema = load_schema("projects") - singer.write_schema("projects", schema, ["id"], bookmark_properties=[bookmark_property]) - start = get_start("projects") - start_dt = pendulum.parse(start) - updated_since = start_dt.strftime("%Y-%m-%d %H:%M") + start = get_start("time_entries") - url = get_url("projects") - projects_data = request(url) - projects_time_extracted = utils.now() + start_dt = pendulum.parse(start) + updated_since = start_dt.strftime("%Y-%m-%dT%H:%M:%SZ") + url = get_url("time_entries") with Transformer() as transformer: - for row in projects_data: - item = row["project"] + data = request(url, {"updated_since": updated_since})['time_entries'] + time_entries_time_extracted = utils.now() + + for row in data: + item = row item = transformer.transform(item, schema) - date_fields = ["starts_on", - "ends_on", - "hint_earliest_record_at", - "hint_latest_record_at"] + append_times_to_dates(item, ["spent_date"]) - append_times_to_dates(item, date_fields) + singer.write_record("time_entries", + item, + time_extracted=time_entries_time_extracted) - if item[bookmark_property] >= start: - singer.write_record("projects", - item, - time_extracted=projects_time_extracted) - - utils.update_state(STATE, "projects", item[bookmark_property]) - - suburl = url + "/{}/user_assignments".format(item["id"]) - project_users_data = request(suburl, params={"updated_since": updated_since}) - project_users_time_extracted = utils.now() - - for subrow in project_users_data: - subitem = subrow["user_assignment"] - subitem = transformer.transform(subitem, users_schema) - singer.write_record("project_users", - subitem, - time_extracted=project_users_time_extracted) - - suburl = url + "/{}/task_assignments".format(item["id"]) - task_assignments_data = request(suburl, params={"updated_since": updated_since}) - task_assignments_time_extracted = utils.now() - - for subrow in task_assignments_data: - subitem = subrow["task_assignment"] - subitem = transformer.transform(subitem, tasks_schema) - singer.write_record("project_tasks", - subitem, - time_extracted=task_assignments_time_extracted) - - suburl = url + "/{}/entries".format(item["id"]) - subparams = { - "from": start_dt.strftime("%Y%m%d"), - "to": datetime.datetime.utcnow().strftime("%Y%m%d"), - "updated_since": updated_since, - } - - time_entries_data = request(suburl, params=subparams) - time_entries_time_extracted = utils.now() - - for subrow in time_entries_data: - subitem = subrow["day_entry"] - subitem = transformer.transform(subitem, entries_schema) - singer.write_record("time_entries", - subitem, + utils.update_state(STATE, "time_entries", item['updated_at']) + + # Extract external_reference + if row['external_reference'] is not None: + external_reference = row['external_reference'] + external_reference = transformer.transform(external_reference, external_reference_schema) + + singer.write_record("external_reference", + external_reference, time_extracted=time_entries_time_extracted) - singer.write_state(STATE) + # Create pivot row for time_entry and external_reference + pivot_row = { + 'time_entry_id': row['id'], + 'external_reference': external_reference['id'] + } + + singer.write_record("time_entry_external_reference", + pivot_row, + time_extracted=time_entries_time_extracted) + + singer.write_state(STATE) + singer.write_state(STATE) def sync_invoices(): messages_schema = load_schema("invoice_messages") @@ -255,6 +229,18 @@ def sync_invoices(): ["id"], bookmark_properties=[bookmark_property]) + recipients_schema = load_schema("invoice_recipients") + singer.write_schema("invoice_recipients", + recipients_schema, + ["id"], + bookmark_properties=[bookmark_property]) + + line_items_schema = load_schema("invoice_line_items") + singer.write_schema("invoice_line_items", + line_items_schema, + ["id"], + bookmark_properties=[bookmark_property]) + schema = load_schema("invoices") singer.write_schema("invoices", schema, @@ -264,52 +250,254 @@ def sync_invoices(): start = get_start("invoices") start_dt = pendulum.parse(start) - updated_since = start_dt.strftime("%Y-%m-%d %H:%M") + updated_since = start_dt.strftime("%Y-%m-%dT%H:%M:%SZ") url = get_url("invoices") with Transformer() as transformer: - while True: - data = request(url, {"updated_since": updated_since}) - invoices_time_extracted = utils.now() + data = request(url, {"updated_since": updated_since})['invoices'] + invoices_time_extracted = utils.now() - for row in data: - item = row["invoices"] - item = transformer.transform(item, schema) - append_times_to_dates(item, ["issued_at", "due_at"]) - - singer.write_record("invoices", - item, + for row in data: + item = transformer.transform(row, schema) + append_times_to_dates(item, [ + "period_start", + "period_end", + "issued_date", + "due_date", + "paid_date", + ]) + + singer.write_record("invoices", + item, + time_extracted=invoices_time_extracted) + + utils.update_state(STATE, "invoices", item['updated_at']) + + # Extract all invoice_line_items + for line_item in row['line_items']: + line_item['invoice_id'] = row['id'] + line_item = transformer.transform(line_item, line_items_schema) + + singer.write_record("invoice_line_items", + line_item, time_extracted=invoices_time_extracted) - utils.update_state(STATE, "invoices", item['updated_at']) + # Load all invoice_messages + suburl = url + "/{}/messages".format(item['id']) + messages_data = request(suburl)['invoice_messages'] + messages_time_extracted = utils.now() + for subrow in messages_data: + subrow['invoice_id'] = row['id'] + message = transformer.transform(subrow, messages_schema) + if message['updated_at'] >= start: + append_times_to_dates(message, ["send_reminder_on"]) + singer.write_record("invoice_messages", + message, + time_extracted=messages_time_extracted) + + # Extract all invoice_recipients + for recipient in subrow['recipients']: + recipient['invoice_message_id'] = message['id'] + recipient = transformer.transform(recipient, recipients_schema) + + singer.write_record("invoice_recipients", + recipient, + time_extracted=invoices_time_extracted) + + # Load all invoice_payments + suburl = url + "/{}/payments".format(item['id']) + payments_data = request(suburl)['invoice_payments'] + payments_time_extracted = utils.now() + + for subrow in payments_data: + subrow['payment_gateway_id'] = subrow['payment_gateway']['id'] + subrow['payment_gateway_name'] = subrow['payment_gateway']['name'] + payment = transformer.transform(subrow, payments_schema) + if payment['updated_at'] >= start: + singer.write_record("invoice_payments", + payment, + time_extracted=payments_time_extracted) + + singer.write_state(STATE) + + singer.write_state(STATE) + +def sync_estimates(): + messages_schema = load_schema("estimate_messages") + bookmark_property = 'updated_at' + singer.write_schema("estimate_messages", + messages_schema, + ["id"], + bookmark_properties=[bookmark_property]) + + recipients_schema = load_schema("estimate_recipients") + singer.write_schema("estimate_recipients", + recipients_schema, + ["id"], + bookmark_properties=[bookmark_property]) + + line_items_schema = load_schema("estimate_line_items") + singer.write_schema("estimate_line_items", + line_items_schema, + ["id"], + bookmark_properties=[bookmark_property]) - suburl = url + "/{}/messages".format(item['id']) - messages_data = request(suburl) - messages_time_extracted = utils.now() - for subrow in messages_data: - subitem = subrow["message"] - if subitem['updated_at'] >= start: - append_times_to_dates(subitem, ["send_reminder_on"]) - singer.write_record("invoice_messages", - subitem, - time_extracted=messages_time_extracted) + schema = load_schema("estimates") + singer.write_schema("estimates", + schema, + ["id"], + bookmark_properties=[bookmark_property]) - suburl = url + "/{}/payments".format(item['id']) - payments_data = request(suburl) - payments_time_extracted = utils.now() + start = get_start("estimates") - for subrow in payments_data: - subitem = subrow["payment"] - subitem = transformer.transform(subitem, payments_schema) - if subitem['updated_at'] >= start: - singer.write_record("invoice_payments", - subitem, - time_extracted=payments_time_extracted) + start_dt = pendulum.parse(start) + updated_since = start_dt.strftime("%Y-%m-%dT%H:%M:%SZ") - singer.write_state(STATE) + url = get_url("estimates") + with Transformer() as transformer: + data = request(url, {"updated_since": updated_since})['estimates'] + estimates_time_extracted = utils.now() - if len(data) < 50: - break + for row in data: + item = transformer.transform(row, schema) + append_times_to_dates(item, ["issued_date"]) + + singer.write_record("estimates", + item, + time_extracted=estimates_time_extracted) + + utils.update_state(STATE, "estimates", item['updated_at']) + + # Extract all estimate_line_items + for line_item in row['line_items']: + line_item['estimate_id'] = row['id'] + line_item = transformer.transform(line_item, line_items_schema) + + singer.write_record("estimate_line_items", + line_item, + time_extracted=estimates_time_extracted) + + # Load all estimate_messages + suburl = url + "/{}/messages".format(item['id']) + messages_data = request(suburl)['estimate_messages'] + messages_time_extracted = utils.now() + for subrow in messages_data: + subrow['estimate_id'] = row['id'] + message = transformer.transform(subrow, messages_schema) + if message['updated_at'] >= start: + append_times_to_dates(message, ["send_reminder_on"]) + singer.write_record("estimate_messages", + message, + time_extracted=messages_time_extracted) + + # Extract all estimate_recipients + for recipient in subrow['recipients']: + recipient['estimate_message_id'] = message['id'] + recipient = transformer.transform(recipient, recipients_schema) + + singer.write_record("estimate_recipients", + recipient, + time_extracted=estimates_time_extracted) + + singer.write_state(STATE) + + singer.write_state(STATE) + +def sync_roles(): + user_roles_schema = load_schema("user_roles") + bookmark_property = 'updated_at' + singer.write_schema("user_roles", + user_roles_schema, + ["id"], + bookmark_properties=[bookmark_property]) + + schema = load_schema("roles") + singer.write_schema("roles", + schema, + ["id"], + bookmark_properties=[bookmark_property]) + + start = get_start("roles") + + start_dt = pendulum.parse(start) + updated_since = start_dt.strftime("%Y-%m-%dT%H:%M:%SZ") + + url = get_url("roles") + with Transformer() as transformer: + data = request(url, {"updated_since": updated_since})['roles'] + time_entries_time_extracted = utils.now() + + for row in data: + item = transformer.transform(row, schema) + + singer.write_record("roles", + item, + time_extracted=time_entries_time_extracted) + + utils.update_state(STATE, "roles", item['updated_at']) + + # Extract external_reference + for user_id in row['user_ids']: + pivot_row = { + 'role_id': row['id'], + 'user_id': user_id + } + + singer.write_record("time_entry_external_reference", + pivot_row, + time_extracted=time_entries_time_extracted) + + singer.write_state(STATE) + + singer.write_state(STATE) + +def sync_users(): + user_projects_schema = load_schema("user_projects") + bookmark_property = 'updated_at' + singer.write_schema("user_projects", + user_projects_schema, + ["id"], + bookmark_properties=[bookmark_property]) + + schema = load_schema("users") + singer.write_schema("users", + schema, + ["id"], + bookmark_properties=[bookmark_property]) + + start = get_start("users") + + start_dt = pendulum.parse(start) + updated_since = start_dt.strftime("%Y-%m-%dT%H:%M:%SZ") + + url = get_url("users") + with Transformer() as transformer: + data = request(url, {"updated_since": updated_since})['users'] + users_time_extracted = utils.now() + + for row in data: + item = row + item = transformer.transform(item, schema) + + singer.write_record("users", + item, + time_extracted=users_time_extracted) + + utils.update_state(STATE, "users", item['updated_at']) + + # Load all user_projects + suburl = url + "/{}/project_assignments".format(item['id']) + project_assignments_data = request(suburl)['project_assignments'] + payments_time_extracted = utils.now() + + for subrow in project_assignments_data: + user_project = transformer.transform(subrow, user_projects_schema) + if user_project['updated_at'] >= start: + singer.write_record("user_projects", + user_project, + time_extracted=payments_time_extracted) + + singer.write_state(STATE) singer.write_state(STATE) @@ -319,24 +507,36 @@ def do_sync(): # Grab all clients and client contacts. Contacts have client FKs so grab # them last. - sync_endpoint("clients", "client") - sync_endpoint("contacts", "contact") + sync_endpoint("clients") + sync_endpoint("contacts") + sync_roles() # Get all people and tasks before grabbing the projects. When we grab the # projects we will grab the project_users, project_tasks, and time_entries # for each. - sync_endpoint("people", "user") - sync_endpoint("tasks", "task") - sync_projects() + sync_users() + sync_endpoint("tasks") + sync_endpoint("projects") + + # Sync related project objects + sync_endpoint("project_tasks", endpoint='task_assignments', path='task_assignments') + sync_endpoint("project_users", endpoint='user_assignments', path='user_assignments') # Sync expenses and their categories - sync_endpoint("expense_categories", "expense_category") - sync_endpoint("expenses", "expense", date_fields=["spent_at"]) + sync_endpoint("expense_categories") + sync_endpoint("expenses", date_fields=["spent_at"]) # Sync invoices and all related records - sync_endpoint("invoice_item_categories", "invoice_category") + sync_endpoint("invoice_item_categories") sync_invoices() + # Sync estimates and all related records + sync_endpoint("estimate_item_categories") + sync_estimates() + + # Sync Time Entries along with their external reference objects + sync_time_entries() + LOGGER.info("Sync complete") diff --git a/tap_harvest/schemas/clients.json b/tap_harvest/schemas/clients.json index eedeb42..e9f94b6 100644 --- a/tap_harvest/schemas/clients.json +++ b/tap_harvest/schemas/clients.json @@ -7,37 +7,22 @@ "name": { "type": ["null", "string"] }, - "active": { + "is_active": { "type": ["null", "boolean"] }, - "currency": { + "address": { "type": ["null", "string"] }, - "highrise_id": { - "type": ["null", "boolean"] - }, - "cache_version": { - "type": ["null", "integer"] + "currency": { + "type": ["null", "string"] }, - "updated_at": { + "created_at": { "type": ["null", "string"], "format": "date-time" }, - "created_at": { + "updated_at": { "type": ["null", "string"], "format": "date-time" - }, - "currency_symbol": { - "type": ["null", "string"] - }, - "details": { - "type": ["null", "string"] - }, - "default_invoice_timeframe": { - "type": ["null", "string"] - }, - "last_invoice_kind": { - "type": ["null", "string"] } } } diff --git a/tap_harvest/schemas/contacts.json b/tap_harvest/schemas/contacts.json index 2267333..dc5c32c 100644 --- a/tap_harvest/schemas/contacts.json +++ b/tap_harvest/schemas/contacts.json @@ -7,6 +7,9 @@ "client_id": { "type": ["null", "integer"] }, + "title": { + "type": ["null", "string"] + }, "first_name": { "type": ["null", "string"] }, @@ -25,14 +28,11 @@ "fax": { "type": ["null", "string"] }, - "title": { - "type": ["null", "string"] - }, - "updated_at": { + "created_at": { "type": ["null", "string"], "format": "date-time" }, - "created_at": { + "updated_at": { "type": ["null", "string"], "format": "date-time" } diff --git a/tap_harvest/schemas/estimate_item_categories.json b/tap_harvest/schemas/estimate_item_categories.json new file mode 100644 index 0000000..dcfa82a --- /dev/null +++ b/tap_harvest/schemas/estimate_item_categories.json @@ -0,0 +1,19 @@ +{ + "type": "object", + "properties": { + "id": { + "type": ["null", "integer"] + }, + "name": { + "type": ["null", "string"] + }, + "created_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "updated_at": { + "type": ["null", "string"], + "format": "date-time" + } + } +} diff --git a/tap_harvest/schemas/estimate_line_items.json b/tap_harvest/schemas/estimate_line_items.json new file mode 100644 index 0000000..4d86539 --- /dev/null +++ b/tap_harvest/schemas/estimate_line_items.json @@ -0,0 +1,32 @@ +{ + "type": "object", + "properties": { + "id": { + "type": ["null", "integer"] + }, + "estimate_id": { + "type": ["null", "integer"] + }, + "kind": { + "type": ["null", "string"] + }, + "description": { + "type": ["null", "string"] + }, + "quantity": { + "type": ["null", "integer"] + }, + "unit_price": { + "type": ["null", "number"] + }, + "amount": { + "type": ["null", "number"] + }, + "taxed": { + "type": ["null", "boolean"] + }, + "taxed2": { + "type": ["null", "boolean"] + } + } +} diff --git a/tap_harvest/schemas/estimate_messages.json b/tap_harvest/schemas/estimate_messages.json new file mode 100644 index 0000000..9e3239b --- /dev/null +++ b/tap_harvest/schemas/estimate_messages.json @@ -0,0 +1,43 @@ +{ + "type": "object", + "properties": { + "id": { + "type": ["null", "integer"] + }, + "sent_by": { + "type": ["null", "string"] + }, + "sent_by_email": { + "type": ["null", "string"] + }, + "sent_from": { + "type": ["null", "string"] + }, + "sent_from_email": { + "type": ["null", "string"] + }, + "subject": { + "type": ["null", "string"] + }, + "body": { + "type": ["null", "string"] + }, + "send_me_a_copy": { + "type": ["null", "boolean"] + }, + "event_type": { + "type": ["null", "string"] + }, + "created_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "updated_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "estimate_id": { + "type": ["null", "integer"] + } + } +} diff --git a/tap_harvest/schemas/estimates.json b/tap_harvest/schemas/estimates.json new file mode 100644 index 0000000..0caad73 --- /dev/null +++ b/tap_harvest/schemas/estimates.json @@ -0,0 +1,80 @@ +{ + "type": "object", + "properties": { + "id": { + "type": ["null", "integer"] + }, + "client_id": { + "type": ["null", "integer"] + }, + "user_id": { + "type": ["null", "integer"] + }, + "client_key": { + "type": ["null", "string"] + }, + "number": { + "type": ["null", "string"] + }, + "purchase_order": { + "type": ["null", "string"] + }, + "amount": { + "type": ["null", "number"] + }, + "tax": { + "type": ["null", "string", "number"] + }, + "tax_amount": { + "type": ["null", "number"] + }, + "tax2": { + "type": ["null", "string", "number"] + }, + "tax2_amount": { + "type": ["null", "number"] + }, + "discount": { + "type": ["null", "string", "number"] + }, + "discount_amount": { + "type": ["null", "number"] + }, + "subject": { + "type": ["null", "string"] + }, + "notes": { + "type": ["null", "string"] + }, + "currency": { + "type": ["null", "string"] + }, + "state": { + "type": ["null", "string"] + }, + "issued_date": { + "type": ["null", "string"], + "format": "date-time" + }, + "sent_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "accepted_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "declined_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "created_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "updated_at": { + "type": ["null", "string"], + "format": "date-time" + } + } +} diff --git a/tap_harvest/schemas/expense_categories.json b/tap_harvest/schemas/expense_categories.json index 4d6fd5f..7b99b24 100644 --- a/tap_harvest/schemas/expense_categories.json +++ b/tap_harvest/schemas/expense_categories.json @@ -13,6 +13,9 @@ "unit_price": { "type": ["null", "number"] }, + "is_active": { + "type": ["null","boolean"] + }, "created_at": { "type": ["null", "string"], "format": "date-time" @@ -20,9 +23,6 @@ "updated_at": { "type": ["null", "string"], "format": "date-time" - }, - "deactivated": { - "type": ["null","boolean"] } } } diff --git a/tap_harvest/schemas/expenses.json b/tap_harvest/schemas/expenses.json index f712672..2041016 100644 --- a/tap_harvest/schemas/expenses.json +++ b/tap_harvest/schemas/expenses.json @@ -4,22 +4,8 @@ "id": { "type": ["null", "integer"] }, - "total_cost": { - "type": ["null", "number"] - }, - "units": { - "type": ["null", "number"] - }, - "notes": { - "type": ["null", "string"] - }, - "created_at": { - "type": ["null", "string"], - "format": "date-time" - }, - "updated_at": { - "type": ["null", "string"], - "format": "date-time" + "client_id": { + "type": ["null", "integer"] }, "project_id": { "type": ["null", "integer"] @@ -30,35 +16,50 @@ "user_id": { "type": ["null", "integer"] }, - "spent_at": { - "type": ["null", "string"] - }, - "is_closed": { - "type": ["null", "boolean"] + "user_assignment_id": { + "type": ["null", "integer"] }, - "notes": { + "receipt_url": { "type": ["null", "string"] }, "invoice_id": { "type": ["null", "integer"] }, + "notes": { + "type": ["null", "string"] + }, "billable": { "type": ["null", "boolean"] }, - "company_id": { - "type": ["null", "integer"] - }, - "has_receipt": { + "is_closed": { "type": ["null", "boolean"] }, - "receipt_url": { - "type": ["null", "string"] - }, "is_locked": { "type": ["null", "boolean"] }, + "is_billed": { + "type": ["null", "boolean"] + }, "locked_reason": { "type": ["null", "string"] + }, + "spent_date": { + "type": ["null", "string"] + }, + "created_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "updated_at": { + "type": ["null", "string"], + "format": "date-time" + }, + + "total_cost": { + "type": ["null", "number"] + }, + "units": { + "type": ["null", "number"] } } } diff --git a/tap_harvest/schemas/external_reference.json b/tap_harvest/schemas/external_reference.json new file mode 100644 index 0000000..776e2d5 --- /dev/null +++ b/tap_harvest/schemas/external_reference.json @@ -0,0 +1,23 @@ +{ + "type": "object", + "properties": { + "id": { + "type": ["null", "integer"] + }, + "task_id": { + "type": ["null", "integer"] + }, + "group_id": { + "type": ["null", "integer"] + }, + "permalink": { + "type": ["null", "string"] + }, + "service": { + "type": ["null", "string"] + }, + "service_icon_url": { + "type": ["null", "string"] + } + } +} diff --git a/tap_harvest/schemas/invoice_item_categories.json b/tap_harvest/schemas/invoice_item_categories.json index 1025754..0e5e83f 100644 --- a/tap_harvest/schemas/invoice_item_categories.json +++ b/tap_harvest/schemas/invoice_item_categories.json @@ -7,6 +7,12 @@ "name": { "type": ["null", "string"] }, + "use_as_service": { + "type": ["null","boolean"] + }, + "use_as_expense": { + "type": ["null","boolean"] + }, "created_at": { "type": ["null", "string"], "format": "date-time" @@ -14,12 +20,6 @@ "updated_at": { "type": ["null", "string"], "format": "date-time" - }, - "use_as_service": { - "type": ["null","boolean"] - }, - "use_as_expense": { - "type": ["null","boolean"] } } } diff --git a/tap_harvest/schemas/invoice_line_items.json b/tap_harvest/schemas/invoice_line_items.json new file mode 100644 index 0000000..7e977ea --- /dev/null +++ b/tap_harvest/schemas/invoice_line_items.json @@ -0,0 +1,32 @@ +{ + "type": "object", + "properties": { + "id": { + "type": ["null", "integer"] + }, + "project_id": { + "type": ["null", "integer"] + }, + "kind": { + "type": ["null", "string"] + }, + "description": { + "type": ["null", "string"] + }, + "quantity": { + "type": ["null", "integer"] + }, + "unit_price": { + "type": ["null", "number"] + }, + "amount": { + "type": ["null", "number"] + }, + "taxed": { + "type": ["null", "boolean"] + }, + "taxed2": { + "type": ["null", "boolean"] + } + } +} diff --git a/tap_harvest/schemas/invoice_messages.json b/tap_harvest/schemas/invoice_messages.json index 6ea537b..e6e30ae 100644 --- a/tap_harvest/schemas/invoice_messages.json +++ b/tap_harvest/schemas/invoice_messages.json @@ -4,50 +4,56 @@ "id": { "type": ["null", "integer"] }, - "invoice_id": { - "type": ["null", "integer"] + "sent_by": { + "type": ["null", "string"] }, - "send_me_a_copy": { - "type": ["null", "boolean"] + "sent_by_email": { + "type": ["null", "string"] }, - "body": { + "sent_from": { "type": ["null", "string"] }, - "created_at": { - "type": ["null", "string"], - "format": "date-time" + "sent_from_email": { + "type": ["null", "string"] }, - "sent_by": { + "subject": { "type": ["null", "string"] }, - "sent_by_email": { + "body": { "type": ["null", "string"] }, + "include_link_to_client_invoice": { + "type": ["null", "boolean"] + }, + "attach_pdf": { + "type": ["null", "boolean"] + }, + "send_me_a_copy": { + "type": ["null", "boolean"] + }, "thank_you": { "type": ["null", "boolean"] }, - "subject": { + "event_type": { "type": ["null", "string"] }, - "include_pay_pal_link": { + "reminder": { "type": ["null", "boolean"] }, - "updated_at": { + "send_reminder_on": { "type": ["null", "string"], "format": "date-time" }, - "sent_from_email": { - "type": ["null", "string"] - }, - "sent_from": { - "type": ["null", "string"] + "created_at": { + "type": ["null", "string"], + "format": "date-time" }, - "send_reminder_on": { + "updated_at": { "type": ["null", "string"], "format": "date-time" }, - "full_recipient_list": { - "type": ["null", "string"] + "invoice_id": { + "type": ["null", "integer"] } } } diff --git a/tap_harvest/schemas/invoice_payments.json b/tap_harvest/schemas/invoice_payments.json index 9cf5f0c..8e1980d 100644 --- a/tap_harvest/schemas/invoice_payments.json +++ b/tap_harvest/schemas/invoice_payments.json @@ -4,9 +4,6 @@ "id": { "type": ["null", "integer"] }, - "invoice_id": { - "type": ["null", "integer"] - }, "amount": { "type": ["null","number"] }, @@ -14,31 +11,39 @@ "type": ["null", "string"], "format": "date-time" }, - "created_at": { + "paid_date": { "type": ["null", "string"], "format": "date-time" }, - "notes": { - "type": ["null", "string"] - }, "recorded_by": { "type": ["null", "string"] }, "recorded_by_email": { "type": ["null", "string"] }, - "paypal_transaction_id": { - "type": ["null", "integer"] + "notes": { + "type": ["null", "string"] }, - "authorization": { + "transaction_id": { "type": ["null", "string"] }, "payment_gateway_id": { "type": ["null", "integer"] }, + "payment_gateway_name": { + "type": ["null", "integer"] + }, + "created_at": { + "type": ["null", "string"], + "format": "date-time" + }, "updated_at": { "type": ["null", "string"], "format": "date-time" + }, + + "invoice_id": { + "type": ["null", "integer"] } } } diff --git a/tap_harvest/schemas/invoice_recipients.json b/tap_harvest/schemas/invoice_recipients.json new file mode 100644 index 0000000..a94443f --- /dev/null +++ b/tap_harvest/schemas/invoice_recipients.json @@ -0,0 +1,20 @@ +{ + "type": "object", + "properties": { + "id": { + "type": ["null", "integer"] + }, + "name": { + "type": ["null","string"] + }, + "email": { + "type": ["null","string"] + }, + "invoice_id": { + "type": ["null", "integer"] + }, + "invoice_message_id": { + "type": ["null", "integer"] + } + } +} \ No newline at end of file diff --git a/tap_harvest/schemas/invoices.json b/tap_harvest/schemas/invoices.json index e127417..1a59a27 100644 --- a/tap_harvest/schemas/invoices.json +++ b/tap_harvest/schemas/invoices.json @@ -7,88 +7,102 @@ "client_id": { "type": ["null", "integer"] }, - "period_start": { - "type": ["null", "string"] + "estimate_id": { + "type": ["null", "integer"] }, - "period_end": { - "type": ["null", "string"] + "retainer_id": { + "type": ["null", "integer"] }, - "number": { + "user_id": { + "type": ["null", "integer"] + }, + "client_key": { "type": ["null", "string"] }, - "issued_at": { + "number": { "type": ["null", "string"] }, - "due_at": { + "purchase_order": { "type": ["null", "string"] }, "amount": { "type": ["null", "number"] }, - "currency": { - "type": ["null", "string"] + "due_amount": { + "type": ["null", "number"] }, - "state": { + "tax": { + "type": ["null", "string", "number"] + }, + "tax_amount": { + "type": ["null", "number"] + }, + "tax2": { + "type": ["null", "string", "number"] + }, + "tax2_amount": { + "type": ["null", "number"] + }, + "discount": { + "type": ["null", "string", "number"] + }, + "discount_amount": { + "type": ["null", "number"] + }, + "subject": { "type": ["null", "string"] }, "notes": { "type": ["null", "string"] }, - "purchase_order": { + "currency": { "type": ["null", "string"] }, - "due_amount": { - "type": ["null", "number"] - }, - "due_at_human_format": { + "state": { "type": ["null", "string"] }, - "created_at": { + "period_start": { "type": ["null", "string"], "format": "date-time" }, - "updated_at": { + "period_end": { "type": ["null", "string"], "format": "date-time" }, - "tax": { - "type": ["null", "string", "number"] - }, - "tax_amount": { - "type": ["null", "number"] - }, - "subject": { - "type": ["null", "string"] - }, - "recurring_invoice_id": { - "type": ["null", "integer"] - }, - "tax2": { - "type": ["null", "string", "number"] + "issued_date": { + "type": ["null", "string"], + "format": "date-time" }, - "tax2_amount": { - "type": ["null", "number"] + "due_date": { + "type": ["null", "string"], + "format": "date-time" }, - "client_key": { + "payment_term": { "type": ["null", "string"] }, - "estimate_id": { - "type": ["null", "integer"] + "sent_at": { + "type": ["null", "string"], + "format": "date-time" }, - "discount": { - "type": ["null", "string", "number"] + "paid_at": { + "type": ["null", "string"], + "format": "date-time" }, - "discount_amount": { - "type": ["null", "number"] + "paid_date": { + "type": ["null", "string"], + "format": "date-time" }, - "retainer_id": { - "type": ["null", "integer"] + "closed_at": { + "type": ["null", "string"], + "format": "date-time" }, - "created_by_id": { - "type": ["null", "integer"] + "created_at": { + "type": ["null", "string"], + "format": "date-time" }, - "client_name": { - "type": ["null", "string"] + "updated_at": { + "type": ["null", "string"], + "format": "date-time" } } } diff --git a/tap_harvest/schemas/project_tasks.json b/tap_harvest/schemas/project_tasks.json index 4bd9eec..de0c235 100644 --- a/tap_harvest/schemas/project_tasks.json +++ b/tap_harvest/schemas/project_tasks.json @@ -1,16 +1,19 @@ { "type": "object", "properties": { + "id": { + "type": ["null", "integer"] + }, "project_id": { "type": ["null", "integer"] }, "task_id": { "type": ["null", "integer"] }, - "billable": { + "is_active": { "type": ["null", "boolean"] }, - "deactivated": { + "billable": { "type": ["null", "boolean"] }, "hourly_rate": { @@ -19,9 +22,6 @@ "budget": { "type": ["null", "number"] }, - "id": { - "type": ["null", "integer"] - }, "created_at": { "type": ["null", "string"], "format": "date-time" @@ -29,9 +29,6 @@ "updated_at": { "type": ["null", "string"], "format": "date-time" - }, - "estimate": { - "type": ["null", "number"] } } } diff --git a/tap_harvest/schemas/project_users.json b/tap_harvest/schemas/project_users.json index 721e629..2977468 100644 --- a/tap_harvest/schemas/project_users.json +++ b/tap_harvest/schemas/project_users.json @@ -1,24 +1,24 @@ { "type": "object", "properties": { - "user_id": { + "id": { "type": ["null", "integer"] }, "project_id": { "type": ["null", "integer"] }, - "is_project_manager": { + "user_id": { + "type": ["null", "integer"] + }, + "is_active": { "type": ["null", "boolean"] }, - "deactivated": { + "is_project_manager": { "type": ["null", "boolean"] }, "hourly_rate": { "type": ["null", "number"] }, - "id": { - "type": ["null", "integer"] - }, "budget": { "type": ["null", "number"] }, @@ -29,9 +29,6 @@ "updated_at": { "type": ["null", "string"], "format": "date-time" - }, - "estimate": { - "type": ["null", "number"] } } } diff --git a/tap_harvest/schemas/projects.json b/tap_harvest/schemas/projects.json index d226df3..acc4ae9 100644 --- a/tap_harvest/schemas/projects.json +++ b/tap_harvest/schemas/projects.json @@ -13,10 +13,13 @@ "code": { "type": ["null", "string"] }, - "active": { + "is_active": { "type": ["null", "boolean"] }, - "billable": { + "is_billable": { + "type": ["null", "boolean"] + }, + "is_fixed_fee": { "type": ["null", "boolean"] }, "bill_by": { @@ -31,52 +34,47 @@ "budget_by": { "type": ["null", "string"] }, + "budget_is_monthly": { + "type": ["null", "boolean"] + }, "notify_when_over_budget": { "type": ["null", "boolean"] }, - "over_budget_notification_percetange": { + "over_budget_notification_percentage": { "type": ["null", "integer"] }, - "over_budget_notified_at": { - "type": ["null", "string"] - }, - "show_budget_to_all": { - "type": ["null", "boolean"] - }, - "created_at": { + "over_budget_notification_date": { "type": ["null", "string"], "format": "date-time" }, - "updated_at": { - "type": ["null", "string"], - "format": "date-time" + "show_budget_to_all": { + "type": ["null", "boolean"] }, - "starts_on": { - "type": ["null", "string"] + "cost_budget": { + "type": ["null", "number"] }, - "ends_on": { - "type": ["null", "string"] + "cost_budget_include_expenses": { + "type": ["null", "boolean"] }, - "estimate": { + "fee": { "type": ["null", "number"] }, - "estimate_by": { - "type": ["null", "string"] - }, - "hint_earliest_record_at": { + "notes": { "type": ["null", "string"] }, - "hint_latest_record_at": { + "starts_on": { "type": ["null", "string"] }, - "notes": { + "ends_on": { "type": ["null", "string"] }, - "cost_budget": { - "type": ["null", "number"] + "created_at": { + "type": ["null", "string"], + "format": "date-time" }, - "cost_budget_include_expenses": { - "type": ["null", "boolean"] + "updated_at": { + "type": ["null", "string"], + "format": "date-time" } } } diff --git a/tap_harvest/schemas/roles.json b/tap_harvest/schemas/roles.json new file mode 100644 index 0000000..dcfa82a --- /dev/null +++ b/tap_harvest/schemas/roles.json @@ -0,0 +1,19 @@ +{ + "type": "object", + "properties": { + "id": { + "type": ["null", "integer"] + }, + "name": { + "type": ["null", "string"] + }, + "created_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "updated_at": { + "type": ["null", "string"], + "format": "date-time" + } + } +} diff --git a/tap_harvest/schemas/tasks.json b/tap_harvest/schemas/tasks.json index 349ca98..2484b0b 100644 --- a/tap_harvest/schemas/tasks.json +++ b/tap_harvest/schemas/tasks.json @@ -10,6 +10,15 @@ "billable_by_default": { "type": ["null", "boolean"] }, + "default_hourly_rate": { + "type": ["null", "number"] + }, + "is_default": { + "type": ["null", "boolean"] + }, + "is_active": { + "type": ["null", "boolean"] + }, "created_at": { "type": ["null", "string"], "format": "date-time" @@ -17,15 +26,6 @@ "updated_at": { "type": ["null", "string"], "format": "date-time" - }, - "is_default": { - "type": ["null", "boolean"] - }, - "default_hourly_rate": { - "type": ["null", "number"] - }, - "deactivated": { - "type": ["null", "boolean"] } } } diff --git a/tap_harvest/schemas/time_entries.json b/tap_harvest/schemas/time_entries.json index c54ad8a..cee9a1b 100644 --- a/tap_harvest/schemas/time_entries.json +++ b/tap_harvest/schemas/time_entries.json @@ -4,16 +4,17 @@ "id": { "type": ["null", "integer"] }, - "notes": { - "type": ["null", "string"] + "spent_date": { + "type": ["null", "string"], + "format": "date-time" }, - "spent_at": { - "type": ["null", "string"] + "user_id": { + "type": ["null", "integer"] }, - "hours": { - "type": ["null", "number"] + "user_assignment_id": { + "type": ["null", "integer"] }, - "user_id": { + "client_id": { "type": ["null", "integer"] }, "project_id": { @@ -22,29 +23,67 @@ "task_id": { "type": ["null", "integer"] }, - "created_at": { - "type": ["null", "string"], - "format": "date-time" + "task_assignment_id": { + "type": ["null", "integer"] }, - "updated_at": { - "type": ["null", "string"], - "format": "date-time" + "external_reference_id": { + "type": ["null", "integer"] + }, + "invoice_id": { + "type": ["null", "integer"] + }, + "hours": { + "type": ["null", "number"] + }, + "notes": { + "type": ["null", "string"] }, - "adjustment_record": { + "is_locked": { + "type": ["null", "boolean"] + }, + "locked_reason": { + "type": ["null", "string"] + }, + "is_closed": { + "type": ["null", "boolean"] + }, + "is_billed": { "type": ["null", "boolean"] }, "timer_started_at": { "type": ["null", "string"], "format": "date-time" }, - "is_closed": { + "started_time": { + "type": ["null", "string"], + "format": "time" + }, + "ended_time": { + "type": ["null", "string"], + "format": "time" + }, + "is_running": { "type": ["null", "boolean"] }, - "is_billed": { + "billable": { "type": ["null", "boolean"] }, - "hours_with_timer": { + "budgeted": { + "type": ["null", "number"] + }, + "billable_rate": { "type": ["null", "number"] + }, + "cost_rate": { + "type": ["null", "number"] + }, + "created_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "updated_at": { + "type": ["null", "string"], + "format": "date-time" } } } diff --git a/tap_harvest/schemas/time_entry_external_reference.json b/tap_harvest/schemas/time_entry_external_reference.json new file mode 100644 index 0000000..f8b4030 --- /dev/null +++ b/tap_harvest/schemas/time_entry_external_reference.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "properties": { + "time_entry_id": { + "type": ["null", "integer"] + }, + "external_reference_id": { + "type": ["null", "integer"] + } + } +} diff --git a/tap_harvest/schemas/user_projects.json b/tap_harvest/schemas/user_projects.json new file mode 100644 index 0000000..93856cb --- /dev/null +++ b/tap_harvest/schemas/user_projects.json @@ -0,0 +1,37 @@ +{ + "type": "object", + "properties": { + "id": { + "type": ["null", "integer"] + }, + "is_active": { + "type": ["null", "boolean"] + }, + "is_project_manager": { + "type": ["null", "boolean"] + }, + "hourly_rate": { + "type": ["null", "number"] + }, + "budget": { + "type": ["null", "number"] + }, + "created_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "updated_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "project_id": { + "type": ["null", "integer"] + }, + "client_id": { + "type": ["null", "integer"] + }, + "user_id": { + "type": ["null", "integer"] + } + } +} \ No newline at end of file diff --git a/tap_harvest/schemas/user_roles.json b/tap_harvest/schemas/user_roles.json new file mode 100644 index 0000000..a37137c --- /dev/null +++ b/tap_harvest/schemas/user_roles.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "properties": { + "user_id": { + "type": ["null", "integer"] + }, + "role_id": { + "type": ["null", "integer"] + } + } +} diff --git a/tap_harvest/schemas/people.json b/tap_harvest/schemas/users.json similarity index 80% rename from tap_harvest/schemas/people.json rename to tap_harvest/schemas/users.json index 7108349..faca4f5 100644 --- a/tap_harvest/schemas/people.json +++ b/tap_harvest/schemas/users.json @@ -4,58 +4,64 @@ "id": { "type": ["null", "integer"] }, - "email": { + "first_name": { "type": ["null", "string"] }, - "created_at": { - "type": ["null", "string"], - "format": "date-time" - }, - "is_admin": { - "type": ["null", "boolean"] + "last_name": { + "type": ["null", "string"] }, - "first_name": { + "email": { "type": ["null", "string"] }, - "last_name": { + "telephone": { "type": ["null", "string"] }, "timezone": { "type": ["null", "string"] }, + "has_access_to_all_future_projects": { + "type": ["null", "boolean"] + }, "is_contractor": { "type": ["null", "boolean"] }, - "telephone": { - "type": ["null", "string"] + "is_admin": { + "type": ["null", "boolean"] }, - "is_active": { + "is_project_manager": { "type": ["null", "boolean"] }, - "has_access_to_all_future_projects": { + "can_see_rates": { "type": ["null", "boolean"] }, - "default_hourly_rate": { - "type": ["null", "number"] + "can_create_projects": { + "type": ["null", "boolean"] }, - "department": { - "type": ["null", "string"] + "can_create_invoices": { + "type": ["null", "boolean"] }, - "wants_newsletter": { + "is_active": { "type": ["null", "boolean"] }, - "updated_at": { - "type": ["null", "string"], - "format": "date-time" + "weekly_capacity": { + "type": ["null", "integer"] + }, + "default_hourly_rate": { + "type": ["null", "number"] }, "cost_rate": { "type": ["null", "number"] }, - "identity_account_id": { - "type": ["null", "integer"] + "avatar_url": { + "type": ["null", "string"] }, - "indentity_user_id": { - "type": ["null", "integer"] + "created_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "updated_at": { + "type": ["null", "string"], + "format": "date-time" } } }