diff --git a/examples/delete-user-identity.ts b/examples/delete-user-identity.ts new file mode 100644 index 00000000..45de5b1e --- /dev/null +++ b/examples/delete-user-identity.ts @@ -0,0 +1,175 @@ +import type { Builder, Command, Describe } from 'landlubber' + +import type { + AcsCredentialsGetResponse, + AcsUsersGetResponse, + EventsListParams, + EventsListResponse, + PhonesListResponse, + UserIdentitiesEnrollmentAutomationsGetResponse, +} from '@seamapi/http/connect' + +import type { Handler } from './index.js' + +interface Options { + userIdentityId: string +} + +export const command: Command = 'delete-user-identity userIdentityId' + +export const describe: Describe = 'Delete a user identity' + +export const builder: Builder = { + userIdentityId: { + type: 'string', + describe: 'User identity id to delete', + }, +} + +export const handler: Handler = async ({ + userIdentityId, + seam, + logger, +}) => { + const waitForEvent = async ( + eventType: SeamEventType, + eventFilter: (event: SeamEvent) => boolean, + ): Promise => { + let events: SeamEvent[] = [] + do { + events = await seam.events.list({ event_type: eventType }) + } while (!events.some(eventFilter)) + } + + const waitForAcsUserDeleted = async (acsUser: AcsUser): Promise => { + await waitForEvent( + 'acs_user.deleted', + (event) => + 'acs_user_id' in event && event.acs_user_id === acsUser.acs_user_id, + ) + } + + const waitForAcsCredentialDeleted = async ( + acsCredential: AcsCredential, + ): Promise => { + await waitForEvent( + 'acs_credential.deleted', + (event) => + 'acs_credential_id' in event && + event.acs_credential_id === acsCredential.acs_credential_id, + ) + } + + const waitForPhoneDeactivated = async (phone: Phone): Promise => { + await waitForEvent( + 'phone.deactivated', + (event) => 'device_id' in event && event.device_id === phone.device_id, + ) + } + + const waitForEnrollmentAutomationDeleted = async ( + enrollmentAutomation: EnrollmentAutomation, + ): Promise => { + await waitForEvent( + 'enrollment_automation.deleted', + (event) => + 'enrollment_automation_id' in event && + event.enrollment_automation_id === + enrollmentAutomation.enrollment_automation_id, + ) + } + + const userIdentity = await seam.userIdentities.get({ + user_identity_id: userIdentityId, + }) + + const clientSessions = await seam.clientSessions.list({ + user_identity_id: userIdentityId, + }) + + for (const clientSession of clientSessions) { + await seam.clientSessions.delete({ + client_session_id: clientSession.client_session_id, + }) + } + + const acsUsers = await seam.acs.users.list({ + user_identity_id: userIdentityId, + }) + + for (const acsUser of acsUsers) { + const credentials = await seam.acs.credentials.list({ + acs_user_id: acsUser.acs_user_id, + is_multi_phone_sync_credential: true, + }) + + for (const credential of credentials) { + await seam.acs.credentials.delete({ + acs_credential_id: credential.acs_credential_id, + }) + } + + await Promise.all(credentials.map(waitForAcsCredentialDeleted)) + + await seam.acs.users.delete({ acs_user_id: acsUser.acs_user_id }) + } + + const enrollmentAutomations = + await seam.userIdentities.enrollmentAutomations.list({ + user_identity_id: userIdentityId, + }) + + for (const enrollmentAutomation of enrollmentAutomations) { + await seam.userIdentities.enrollmentAutomations.delete({ + enrollment_automation_id: enrollmentAutomation.enrollment_automation_id, + }) + } + + await Promise.all( + enrollmentAutomations.map(waitForEnrollmentAutomationDeleted), + ) + + const phones = await seam.phones.list({ + owner_user_identity_id: userIdentityId, + }) + + for (const phone of phones) { + await seam.phones.deactivate({ + device_id: phone.device_id, + }) + } + + await Promise.all(phones.map(waitForPhoneDeactivated)) + + for (const acsUser of acsUsers) { + const credentials = await seam.acs.credentials.list({ + acs_user_id: acsUser.acs_user_id, + }) + + for (const credential of credentials) { + await seam.acs.credentials.delete({ + acs_credential_id: credential.acs_credential_id, + }) + } + + await Promise.all(credentials.map(waitForAcsCredentialDeleted)) + + await seam.acs.users.delete({ acs_user_id: acsUser.acs_user_id }) + } + + await Promise.all(acsUsers.map(waitForAcsUserDeleted)) + + await seam.userIdentities.delete({ + user_identity_id: userIdentityId, + }) + + logger.info({ userIdentity }, 'Deleted user identity') +} + +type AcsUser = AcsUsersGetResponse['acs_user'] +type AcsCredential = AcsCredentialsGetResponse['acs_credential'] +type EnrollmentAutomation = + UserIdentitiesEnrollmentAutomationsGetResponse['enrollment_automation'] +type Phone = PhonesListResponse['phones'][number] +type SeamEvent = EventsListResponse['events'][number] +type SeamEventType = EventsListParams['event_type'] diff --git a/examples/index.ts b/examples/index.ts index f9687603..ea2f68f4 100755 --- a/examples/index.ts +++ b/examples/index.ts @@ -12,6 +12,7 @@ import landlubber, { import { SeamHttp } from '@seamapi/http/connect' +import * as deleteUserIdentity from './delete-user-identity.js' import * as locks from './locks.js' import * as unlock from './unlock.js' import * as workspace from './workspace.js' @@ -24,7 +25,7 @@ interface ClientContext { seam: SeamHttp } -const commands = [locks, unlock, workspace] +const commands = [deleteUserIdentity, locks, unlock, workspace] const createAppContext: MiddlewareFunction = async (argv) => { const apiKey = argv['api-key'] diff --git a/package-lock.json b/package-lock.json index 564621ea..732951e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ }, "devDependencies": { "@seamapi/fake-seam-connect": "^1.44.2", - "@seamapi/types": "1.121.0", + "@seamapi/types": "^1.123.1", "@types/eslint": "^8.44.2", "@types/node": "^20.8.10", "ava": "^5.0.1", @@ -47,7 +47,7 @@ "npm": ">= 9.0.0" }, "peerDependencies": { - "@seamapi/types": "^1.121.0", + "@seamapi/types": "^^1.123.1", "type-fest": "^4.0.0" }, "peerDependenciesMeta": { @@ -217,9 +217,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", - "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", + "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -1041,9 +1041,9 @@ ] }, "node_modules/@seamapi/fake-devicedb": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@seamapi/fake-devicedb/-/fake-devicedb-1.5.0.tgz", - "integrity": "sha512-b32p1gHPBstPtEKiirXb5nreDx55hqkCPvnPqRXeP3KED07rMSgmhCcVidG0bwjeRca7DtVy3/7yE2gp7c/Ezg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@seamapi/fake-devicedb/-/fake-devicedb-1.6.0.tgz", + "integrity": "sha512-CuZ2caQf7CepZtBOnDe2VJj48X1tYGCa77kK636L281N/kNeJD897s9CysdUZ7O/b6p5fkXDY2IK0GnBEOnkEQ==", "dev": true, "optional": true, "engines": { @@ -1053,7 +1053,21 @@ "optionalDependencies": { "zod": "^3.21.4", "zustand": "^4.3.7", - "zustand-hoist": "^1.0.0" + "zustand-hoist": "^2.0.0" + } + }, + "node_modules/@seamapi/fake-devicedb/node_modules/zustand-hoist": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/zustand-hoist/-/zustand-hoist-2.0.1.tgz", + "integrity": "sha512-Lhvv3RlLQx1NSUtuhk8jegXe1Wyav9RAOnLd4CRs1SbB5qcFoarAGQTE43vIxXizrm1UQJl1q5uRbOZuXGXGpQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=18.12.0", + "npm": ">= 9.0.0" + }, + "peerDependencies": { + "zustand": ">=4.0.0" } }, "node_modules/@seamapi/fake-seam-connect": { @@ -1075,9 +1089,9 @@ } }, "node_modules/@seamapi/types": { - "version": "1.121.0", - "resolved": "https://registry.npmjs.org/@seamapi/types/-/types-1.121.0.tgz", - "integrity": "sha512-QOdsIMGiO90G0lMMl9paqL707KOyege4gmqVtCgi6nQXPgZrajN4EWDPHVTFXM470XFdBXU3xbIR/K/m+5l2ww==", + "version": "1.123.1", + "resolved": "https://registry.npmjs.org/@seamapi/types/-/types-1.123.1.tgz", + "integrity": "sha512-aRDMQx3Vg7DiBhziHsXTEfNhUP2aJaN/hdSGKSq+e6GINH+2ytMchapht1U924E4A3Nwf20OCEbI8RlqE7dY0Q==", "dev": true, "engines": { "node": ">=18.12.0", @@ -1144,9 +1158,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.20", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", - "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", + "version": "20.11.22", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.22.tgz", + "integrity": "sha512-/G+IxWxma6V3E+pqK1tSl2Fo1kl41pK1yeCyDsgkF9WlVAme4j5ISYM2zR11bgLFJGLN5sVK40T4RJNuiZbEjA==", "dev": true, "dependencies": { "undici-types": "~5.26.4" diff --git a/package.json b/package.json index b4501e0c..f7f423d3 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "npm": ">= 9.0.0" }, "peerDependencies": { - "@seamapi/types": "^1.121.0", + "@seamapi/types": "^^1.123.1", "type-fest": "^4.0.0" }, "peerDependenciesMeta": { @@ -103,7 +103,7 @@ }, "devDependencies": { "@seamapi/fake-seam-connect": "^1.44.2", - "@seamapi/types": "1.121.0", + "@seamapi/types": "^1.123.1", "@types/eslint": "^8.44.2", "@types/node": "^20.8.10", "ava": "^5.0.1",