From 259937c60c29437f0c9a894747679a186480c6f3 Mon Sep 17 00:00:00 2001 From: 8x4 <46720448+8times4@users.noreply.github.com> Date: Thu, 5 Sep 2024 01:18:21 +0200 Subject: [PATCH] add upsert and delete to contacts (#67) --- .../api-reference/contacts/delete-contact.mdx | 3 + .../api-reference/contacts/upsert-contact.mdx | 3 + apps/docs/api-reference/openapi.json | 247 +++++++++++++----- apps/docs/mint.json | 4 +- .../public-api/api/contacts/delete-contact.ts | 53 ++++ .../public-api/api/contacts/upsert-contact.ts | 65 +++++ packages/sdk/src/contact.ts | 40 +++ packages/sdk/types/schema.d.ts | 126 ++++++--- 8 files changed, 436 insertions(+), 105 deletions(-) create mode 100644 apps/docs/api-reference/contacts/delete-contact.mdx create mode 100644 apps/docs/api-reference/contacts/upsert-contact.mdx create mode 100644 apps/web/src/server/public-api/api/contacts/delete-contact.ts create mode 100644 apps/web/src/server/public-api/api/contacts/upsert-contact.ts diff --git a/apps/docs/api-reference/contacts/delete-contact.mdx b/apps/docs/api-reference/contacts/delete-contact.mdx new file mode 100644 index 0000000..41cc1b9 --- /dev/null +++ b/apps/docs/api-reference/contacts/delete-contact.mdx @@ -0,0 +1,3 @@ +--- +openapi: delete /v1/contactBooks/{contactBookId}/contacts/{contactId} +--- \ No newline at end of file diff --git a/apps/docs/api-reference/contacts/upsert-contact.mdx b/apps/docs/api-reference/contacts/upsert-contact.mdx new file mode 100644 index 0000000..951b9d8 --- /dev/null +++ b/apps/docs/api-reference/contacts/upsert-contact.mdx @@ -0,0 +1,3 @@ +--- +openapi: put /v1/contactBooks/{contactBookId}/contacts/{contactId} +--- diff --git a/apps/docs/api-reference/openapi.json b/apps/docs/api-reference/openapi.json index 2da5288..82cfa59 100644 --- a/apps/docs/api-reference/openapi.json +++ b/apps/docs/api-reference/openapi.json @@ -538,72 +538,6 @@ } }, "/v1/contactBooks/{contactBookId}/contacts/{contactId}": { - "patch": { - "parameters": [ - { - "schema": { - "type": "string", - "example": "cuiwqdj74rygf74" - }, - "required": true, - "name": "contactBookId", - "in": "path" - }, - { - "schema": { - "type": "string", - "example": "cuiwqdj74rygf74" - }, - "required": true, - "name": "contactId", - "in": "path" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "properties": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "subscribed": { - "type": "boolean" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Retrieve the user", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "contactId": { - "type": "string" - } - } - } - } - } - } - } - }, "get": { "parameters": [ { @@ -680,6 +614,187 @@ } } } + }, + "patch": { + "parameters": [ + { + "schema": { + "type": "string", + "example": "cuiwqdj74rygf74" + }, + "required": true, + "name": "contactBookId", + "in": "path" + }, + { + "schema": { + "type": "string", + "example": "cuiwqdj74rygf74" + }, + "required": true, + "name": "contactId", + "in": "path" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "subscribed": { + "type": "boolean" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Retrieve the user", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "contactId": { + "type": "string" + } + } + } + } + } + } + } + }, + "put": { + "summary": "Upsert a contact", + "description": "Create a new contact or update an existing one based on the contactId", + "parameters": [ + { + "schema": { + "type": "string", + "example": "cuiwqdj74rygf74" + }, + "required": true, + "name": "contactBookId", + "in": "path" + }, + { + "schema": { + "type": "string", + "example": "cuiwqdj74rygf74" + }, + "required": true, + "name": "contactId", + "in": "path" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "subscribed": { + "type": "boolean" + } + }, + "required": [ + "email" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Contact upserted successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "contactId": { + "type": "string" + } + } + } + } + } + } + } + }, + "delete": { + "summary": "Delete a contact", + "description": "Delete a contact from a contact book", + "parameters": [ + { + "schema": { + "type": "string", + "example": "cuiwqdj74rygf74" + }, + "required": true, + "name": "contactBookId", + "in": "path" + }, + { + "schema": { + "type": "string", + "example": "cuiwqdj74rygf74" + }, + "required": true, + "name": "contactId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Contact deleted successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + } + } + } + } + } + } } } } diff --git a/apps/docs/mint.json b/apps/docs/mint.json index 30570d6..96196af 100644 --- a/apps/docs/mint.json +++ b/apps/docs/mint.json @@ -80,7 +80,9 @@ "pages": [ "api-reference/contacts/get-contact", "api-reference/contacts/create-contact", - "api-reference/contacts/update-contact" + "api-reference/contacts/update-contact", + "api-reference/contacts/upsert-contact", + "api-reference/contacts/delete-contact" ] }, { diff --git a/apps/web/src/server/public-api/api/contacts/delete-contact.ts b/apps/web/src/server/public-api/api/contacts/delete-contact.ts new file mode 100644 index 0000000..5a21e79 --- /dev/null +++ b/apps/web/src/server/public-api/api/contacts/delete-contact.ts @@ -0,0 +1,53 @@ +import { createRoute, z } from "@hono/zod-openapi"; +import { PublicAPIApp } from "~/server/public-api/hono"; +import { getTeamFromToken } from "~/server/public-api/auth"; +import { deleteContact } from "~/server/service/contact-service"; +import { getContactBook } from "../../api-utils"; + +const route = createRoute({ + method: "delete", + path: "/v1/contactBooks/{contactBookId}/contacts/{contactId}", + request: { + params: z.object({ + contactBookId: z.string().openapi({ + param: { + name: "contactBookId", + in: "path", + }, + example: "cuiwqdj74rygf74", + }), + contactId: z.string().openapi({ + param: { + name: "contactId", + in: "path", + }, + example: "cuiwqdj74rygf74", + }), + }), + }, + responses: { + 200: { + content: { + "application/json": { + schema: z.object({ success: z.boolean() }), + }, + }, + description: "Contact deleted successfully", + }, + }, +}); + +function deleteContactHandler(app: PublicAPIApp) { + app.openapi(route, async (c) => { + const team = await getTeamFromToken(c); + + await getContactBook(c, team.id); + const contactId = c.req.param("contactId"); + + await deleteContact(contactId); + + return c.json({ success: true }); + }); +} + +export default deleteContactHandler; \ No newline at end of file diff --git a/apps/web/src/server/public-api/api/contacts/upsert-contact.ts b/apps/web/src/server/public-api/api/contacts/upsert-contact.ts new file mode 100644 index 0000000..3d97f2b --- /dev/null +++ b/apps/web/src/server/public-api/api/contacts/upsert-contact.ts @@ -0,0 +1,65 @@ +import { createRoute, z } from "@hono/zod-openapi"; +import { PublicAPIApp } from "~/server/public-api/hono"; +import { getTeamFromToken } from "~/server/public-api/auth"; +import { addOrUpdateContact } from "~/server/service/contact-service"; +import { getContactBook } from "../../api-utils"; + +const route = createRoute({ + method: "put", + path: "/v1/contactBooks/{contactBookId}/contacts/{contactId}", + request: { + params: z.object({ + contactBookId: z + .string() + .min(3) + .openapi({ + param: { + name: "contactBookId", + in: "path", + }, + example: "cuiwqdj74rygf74", + }), + }), + body: { + required: true, + content: { + "application/json": { + schema: z.object({ + email: z.string(), + firstName: z.string().optional(), + lastName: z.string().optional(), + properties: z.record(z.string()).optional(), + subscribed: z.boolean().optional(), + }), + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: z.object({ contactId: z.string() }), + }, + }, + description: "Contact upserted successfully", + }, + }, +}); + +function upsertContact(app: PublicAPIApp) { + app.openapi(route, async (c) => { + const team = await getTeamFromToken(c); + + const contactBook = await getContactBook(c, team.id); + + const contact = await addOrUpdateContact( + contactBook.id, + c.req.valid("json") + ); + + return c.json({ contactId: contact.id }); + }); +} + +export default upsertContact; diff --git a/packages/sdk/src/contact.ts b/packages/sdk/src/contact.ts index 5a9c49d..359bbce 100644 --- a/packages/sdk/src/contact.ts +++ b/packages/sdk/src/contact.ts @@ -32,6 +32,22 @@ type UpdateContactResponse = { error: ErrorResponse | null; }; +type UpsertContactPayload = + paths["/v1/contactBooks/{contactBookId}/contacts/{contactId}"]["put"]["requestBody"]["content"]["application/json"]; + +type UpsertContactResponseSuccess = + paths["/v1/contactBooks/{contactBookId}/contacts/{contactId}"]["put"]["responses"]["200"]["content"]["application/json"]; + +type UpsertContactResponse = { + data: UpsertContactResponseSuccess | null; + error: ErrorResponse | null; +}; + +type DeleteContactResponse = { + data: { success: boolean } | null; + error: ErrorResponse | null; +}; + export class Contacts { constructor(private readonly unsend: Unsend) { this.unsend = unsend; @@ -71,4 +87,28 @@ export class Contacts { return data; } + + async upsert( + contactBookId: string, + contactId: string, + payload: UpsertContactPayload + ): Promise { + const data = await this.unsend.put( + `/contactBooks/${contactBookId}/contacts/${contactId}`, + payload + ); + + return data; + } + + async delete( + contactBookId: string, + contactId: string + ): Promise { + const data = await this.unsend.delete<{ success: boolean }>( + `/contactBooks/${contactBookId}/contacts/${contactId}` + ); + + return data; + } } diff --git a/packages/sdk/types/schema.d.ts b/packages/sdk/types/schema.d.ts index 0b4dacd..af95744 100644 --- a/packages/sdk/types/schema.d.ts +++ b/packages/sdk/types/schema.d.ts @@ -12,35 +12,35 @@ export interface paths { 200: { content: { "application/json": ({ - /** - * @description The ID of the domain - * @example 1 - */ - id: number; - /** - * @description The name of the domain - * @example example.com - */ - name: string; - /** - * @description The ID of the team - * @example 1 - */ - teamId: number; - /** @enum {string} */ - status: "NOT_STARTED" | "PENDING" | "SUCCESS" | "FAILED" | "TEMPORARY_FAILURE"; - /** @default us-east-1 */ - region?: string; - /** @default false */ - clickTracking?: boolean; - /** @default false */ - openTracking?: boolean; - publicKey: string; - dkimStatus?: string | null; - spfDetails?: string | null; - createdAt: string; - updatedAt: string; - })[]; + /** + * @description The ID of the domain + * @example 1 + */ + id: number; + /** + * @description The name of the domain + * @example example.com + */ + name: string; + /** + * @description The ID of the team + * @example 1 + */ + teamId: number; + /** @enum {string} */ + status: "NOT_STARTED" | "PENDING" | "SUCCESS" | "FAILED" | "TEMPORARY_FAILURE"; + /** @default us-east-1 */ + region?: string; + /** @default false */ + clickTracking?: boolean; + /** @default false */ + openTracking?: boolean; + publicKey: string; + dkimStatus?: string | null; + spfDetails?: string | null; + createdAt: string; + updatedAt: string; + })[]; }; }; }; @@ -71,12 +71,12 @@ export interface paths { createdAt: string; updatedAt: string; emailEvents: ({ - emailId: string; - /** @enum {string} */ - status: "SCHEDULED" | "QUEUED" | "SENT" | "DELIVERY_DELAYED" | "BOUNCED" | "REJECTED" | "RENDERING_FAILURE" | "DELIVERED" | "OPENED" | "CLICKED" | "COMPLAINED" | "FAILED" | "CANCELLED"; - createdAt: string; - data?: unknown; - })[]; + emailId: string; + /** @enum {string} */ + status: "SCHEDULED" | "QUEUED" | "SENT" | "DELIVERY_DELAYED" | "BOUNCED" | "REJECTED" | "RENDERING_FAILURE" | "DELIVERED" | "OPENED" | "CLICKED" | "COMPLAINED" | "FAILED" | "CANCELLED"; + createdAt: string; + data?: unknown; + })[]; }; }; }; @@ -122,9 +122,9 @@ export interface paths { text?: string; html?: string; attachments?: { - filename: string; - content: string; - }[]; + filename: string; + content: string; + }[]; /** Format: date-time */ scheduledAt?: string; }; @@ -192,6 +192,7 @@ export interface paths { }; }; }; + }; "/v1/contactBooks/{contactBookId}/contacts/{contactId}": { get: { @@ -253,6 +254,55 @@ export interface paths { }; }; }; + put: { + parameters: { + path: { + contactBookId: string; + contactId: string; + }; + }; + requestBody: { + content: { + "application/json": { + email: string; + firstName?: string; + lastName?: string; + properties?: { + [key: string]: string; + }; + subscribed?: boolean; + }; + }; + }; + responses: { + /** @description Contact upserted successfully */ + 200: { + content: { + "application/json": { + contactId: string; + }; + }; + }; + }; + }; + delete: { + parameters: { + path: { + contactBookId: string; + contactId: string; + }; + }; + responses: { + /** @description Contact deleted successfully */ + 200: { + content: { + "application/json": { + success: boolean; + }; + }; + }; + }; + }; }; }