| ... | ... | @@ -15,28 +15,41 @@ import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.t |
| 15 | 15 | |
| 16 | 16 | export class DeviceRegistrationPersistenceError extends Schema.TaggedErrorClass<DeviceRegistrationPersistenceError>()( |
| 17 | 17 | "DeviceRegistrationPersistenceError", |
| 18 | | { cause: Schema.Defect() }, |
| 18 | { |
| 19 | userId: Schema.String, |
| 20 | deviceId: Schema.String, |
| 21 | stage: Schema.Literals(["claim-push-token", "claim-push-to-start-token", "upsert-device"]), |
| 22 | cause: Schema.Defect(), |
| 23 | }, |
| 19 | 24 | ) { |
| 20 | 25 | override get message(): string { |
| 21 | | return "Failed to persist mobile device registration"; |
| 26 | return `Failed to persist mobile device registration for ${this.userId}/${this.deviceId} during ${this.stage}.`; |
| 22 | 27 | } |
| 23 | 28 | } |
| 24 | 29 | |
| 25 | 30 | export class DeviceUnregistrationPersistenceError extends Schema.TaggedErrorClass<DeviceUnregistrationPersistenceError>()( |
| 26 | 31 | "DeviceUnregistrationPersistenceError", |
| 27 | | { cause: Schema.Defect() }, |
| 32 | { |
| 33 | userId: Schema.String, |
| 34 | deviceId: Schema.String, |
| 35 | stage: Schema.Literals(["delete-live-activity", "delete-device"]), |
| 36 | cause: Schema.Defect(), |
| 37 | }, |
| 28 | 38 | ) { |
| 29 | 39 | override get message(): string { |
| 30 | | return "Failed to unregister mobile device"; |
| 40 | return `Failed to unregister mobile device ${this.userId}/${this.deviceId} during ${this.stage}.`; |
| 31 | 41 | } |
| 32 | 42 | } |
| 33 | 43 | |
| 34 | 44 | export class DeviceListPersistenceError extends Schema.TaggedErrorClass<DeviceListPersistenceError>()( |
| 35 | 45 | "DeviceListPersistenceError", |
| 36 | | { cause: Schema.Defect() }, |
| 46 | { |
| 47 | userId: Schema.String, |
| 48 | cause: Schema.Defect(), |
| 49 | }, |
| 37 | 50 | ) { |
| 38 | 51 | override get message(): string { |
| 39 | | return "Failed to list mobile devices"; |
| 52 | return `Failed to list mobile devices for ${this.userId}.`; |
| 40 | 53 | } |
| 41 | 54 | } |
| 42 | 55 | |
| ... | ... | @@ -61,130 +74,181 @@ export const make = Effect.gen(function* () { |
| 61 | 74 | const db = yield* RelayDb.RelayDb; |
| 62 | 75 | |
| 63 | 76 | return Devices.of({ |
| 64 | | register: Effect.fn("relay.devices.register")( |
| 65 | | function* (input) { |
| 66 | | yield* Effect.annotateCurrentSpan({ |
| 67 | | "relay.mobile.device_id": input.registration.deviceId, |
| 68 | | }); |
| 69 | | const updatedAt = DateTime.formatIso(yield* DateTime.now); |
| 70 | | const registration = input.registration; |
| 77 | register: Effect.fn("relay.devices.register")(function* (input) { |
| 78 | yield* Effect.annotateCurrentSpan({ |
| 79 | "relay.mobile.device_id": input.registration.deviceId, |
| 80 | }); |
| 81 | const updatedAt = DateTime.formatIso(yield* DateTime.now); |
| 82 | const registration = input.registration; |
| 71 | 83 | |
| 72 | | yield* Effect.all( |
| 73 | | [ |
| 74 | | registration.pushToken |
| 75 | | ? db |
| 76 | | .update(relayMobileDevices) |
| 77 | | .set({ pushToken: null, updatedAt }) |
| 78 | | .where(eq(relayMobileDevices.pushToken, registration.pushToken)) |
| 79 | | : Effect.void, |
| 80 | | registration.pushToStartToken |
| 81 | | ? db |
| 82 | | .update(relayMobileDevices) |
| 83 | | .set({ pushToStartToken: null, updatedAt }) |
| 84 | | .where(eq(relayMobileDevices.pushToStartToken, registration.pushToStartToken)) |
| 85 | | : Effect.void, |
| 86 | | ], |
| 87 | | { concurrency: 2, discard: true }, |
| 88 | | ); |
| 84 | yield* Effect.all( |
| 85 | [ |
| 86 | registration.pushToken |
| 87 | ? db |
| 88 | .update(relayMobileDevices) |
| 89 | .set({ pushToken: null, updatedAt }) |
| 90 | .where(eq(relayMobileDevices.pushToken, registration.pushToken)) |
| 91 | .pipe( |
| 92 | Effect.mapError( |
| 93 | (cause) => |
| 94 | new DeviceRegistrationPersistenceError({ |
| 95 | userId: input.userId, |
| 96 | deviceId: registration.deviceId, |
| 97 | stage: "claim-push-token", |
| 98 | cause, |
| 99 | }), |
| 100 | ), |
| 101 | ) |
| 102 | : Effect.void, |
| 103 | registration.pushToStartToken |
| 104 | ? db |
| 105 | .update(relayMobileDevices) |
| 106 | .set({ pushToStartToken: null, updatedAt }) |
| 107 | .where(eq(relayMobileDevices.pushToStartToken, registration.pushToStartToken)) |
| 108 | .pipe( |
| 109 | Effect.mapError( |
| 110 | (cause) => |
| 111 | new DeviceRegistrationPersistenceError({ |
| 112 | userId: input.userId, |
| 113 | deviceId: registration.deviceId, |
| 114 | stage: "claim-push-to-start-token", |
| 115 | cause, |
| 116 | }), |
| 117 | ), |
| 118 | ) |
| 119 | : Effect.void, |
| 120 | ], |
| 121 | { discard: true }, |
| 122 | ); |
| 89 | 123 | |
| 90 | | yield* db |
| 91 | | .insert(relayMobileDevices) |
| 92 | | .values({ |
| 93 | | userId: input.userId, |
| 94 | | deviceId: registration.deviceId, |
| 95 | | label: registration.label, |
| 124 | yield* db |
| 125 | .insert(relayMobileDevices) |
| 126 | .values({ |
| 127 | userId: input.userId, |
| 128 | deviceId: registration.deviceId, |
| 129 | label: registration.label, |
| 130 | platform: registration.platform, |
| 131 | iosMajorVersion: registration.iosMajorVersion, |
| 132 | appVersion: registration.appVersion ?? null, |
| 133 | pushToken: registration.pushToken ?? null, |
| 134 | pushToStartToken: registration.pushToStartToken ?? null, |
| 135 | preferencesJson: registration.preferences, |
| 136 | createdAt: updatedAt, |
| 137 | updatedAt, |
| 138 | }) |
| 139 | .onConflictDoUpdate({ |
| 140 | target: [relayMobileDevices.userId, relayMobileDevices.deviceId], |
| 141 | set: { |
| 96 | 142 | platform: registration.platform, |
| 143 | label: registration.label, |
| 97 | 144 | iosMajorVersion: registration.iosMajorVersion, |
| 98 | 145 | appVersion: registration.appVersion ?? null, |
| 99 | | pushToken: registration.pushToken ?? null, |
| 100 | | pushToStartToken: registration.pushToStartToken ?? null, |
| 101 | | preferencesJson: registration.preferences, |
| 102 | | createdAt: updatedAt, |
| 103 | | updatedAt, |
| 104 | | }) |
| 105 | | .onConflictDoUpdate({ |
| 106 | | target: [relayMobileDevices.userId, relayMobileDevices.deviceId], |
| 107 | | set: { |
| 108 | | platform: registration.platform, |
| 109 | | label: registration.label, |
| 110 | | iosMajorVersion: registration.iosMajorVersion, |
| 111 | | appVersion: registration.appVersion ?? null, |
| 112 | | pushToken: sql`coalesce(excluded.push_token, ${relayMobileDevices.pushToken})`, |
| 113 | | pushToStartToken: sql`coalesce( |
| 146 | pushToken: sql`coalesce(excluded.push_token, ${relayMobileDevices.pushToken})`, |
| 147 | pushToStartToken: sql`coalesce( |
| 114 | 148 | excluded.push_to_start_token, |
| 115 | 149 | ${relayMobileDevices.pushToStartToken} |
| 116 | 150 | )`, |
| 117 | | preferencesJson: registration.preferences, |
| 118 | | updatedAt, |
| 119 | | }, |
| 120 | | }); |
| 121 | | }, |
| 122 | | Effect.mapError((cause) => new DeviceRegistrationPersistenceError({ cause })), |
| 123 | | ), |
| 124 | | unregister: Effect.fn("relay.devices.unregister")( |
| 125 | | function* (input) { |
| 126 | | yield* Effect.annotateCurrentSpan({ |
| 127 | | "relay.mobile.device_id": input.deviceId, |
| 128 | | }); |
| 129 | | yield* Effect.all( |
| 130 | | [ |
| 131 | | db |
| 132 | | .delete(relayLiveActivities) |
| 133 | | .where( |
| 134 | | and( |
| 135 | | eq(relayLiveActivities.userId, input.userId), |
| 136 | | eq(relayLiveActivities.deviceId, input.deviceId), |
| 137 | | ), |
| 151 | preferencesJson: registration.preferences, |
| 152 | updatedAt, |
| 153 | }, |
| 154 | }) |
| 155 | .pipe( |
| 156 | Effect.mapError( |
| 157 | (cause) => |
| 158 | new DeviceRegistrationPersistenceError({ |
| 159 | userId: input.userId, |
| 160 | deviceId: registration.deviceId, |
| 161 | stage: "upsert-device", |
| 162 | cause, |
| 163 | }), |
| 164 | ), |
| 165 | ); |
| 166 | }), |
| 167 | unregister: Effect.fn("relay.devices.unregister")(function* (input) { |
| 168 | yield* Effect.annotateCurrentSpan({ |
| 169 | "relay.mobile.device_id": input.deviceId, |
| 170 | }); |
| 171 | yield* Effect.all( |
| 172 | [ |
| 173 | db |
| 174 | .delete(relayLiveActivities) |
| 175 | .where( |
| 176 | and( |
| 177 | eq(relayLiveActivities.userId, input.userId), |
| 178 | eq(relayLiveActivities.deviceId, input.deviceId), |
| 138 | 179 | ), |
| 139 | | db |
| 140 | | .delete(relayMobileDevices) |
| 141 | | .where( |
| 142 | | and( |
| 143 | | eq(relayMobileDevices.userId, input.userId), |
| 144 | | eq(relayMobileDevices.deviceId, input.deviceId), |
| 145 | | ), |
| 180 | ) |
| 181 | .pipe( |
| 182 | Effect.mapError( |
| 183 | (cause) => |
| 184 | new DeviceUnregistrationPersistenceError({ |
| 185 | userId: input.userId, |
| 186 | deviceId: input.deviceId, |
| 187 | stage: "delete-live-activity", |
| 188 | cause, |
| 189 | }), |
| 146 | 190 | ), |
| 147 | | ], |
| 148 | | { concurrency: 2, discard: true }, |
| 191 | ), |
| 192 | db |
| 193 | .delete(relayMobileDevices) |
| 194 | .where( |
| 195 | and( |
| 196 | eq(relayMobileDevices.userId, input.userId), |
| 197 | eq(relayMobileDevices.deviceId, input.deviceId), |
| 198 | ), |
| 199 | ) |
| 200 | .pipe( |
| 201 | Effect.mapError( |
| 202 | (cause) => |
| 203 | new DeviceUnregistrationPersistenceError({ |
| 204 | userId: input.userId, |
| 205 | deviceId: input.deviceId, |
| 206 | stage: "delete-device", |
| 207 | cause, |
| 208 | }), |
| 209 | ), |
| 210 | ), |
| 211 | ], |
| 212 | { discard: true }, |
| 213 | ); |
| 214 | }), |
| 215 | listForUser: Effect.fn("relay.devices.listForUser")(function* (input) { |
| 216 | const rows = yield* db |
| 217 | .select({ |
| 218 | deviceId: relayMobileDevices.deviceId, |
| 219 | label: relayMobileDevices.label, |
| 220 | platform: relayMobileDevices.platform, |
| 221 | iosMajorVersion: relayMobileDevices.iosMajorVersion, |
| 222 | appVersion: relayMobileDevices.appVersion, |
| 223 | preferences: relayMobileDevices.preferencesJson, |
| 224 | updatedAt: relayMobileDevices.updatedAt, |
| 225 | }) |
| 226 | .from(relayMobileDevices) |
| 227 | .where(eq(relayMobileDevices.userId, input.userId)) |
| 228 | .pipe( |
| 229 | Effect.mapError( |
| 230 | (cause) => new DeviceListPersistenceError({ userId: input.userId, cause }), |
| 231 | ), |
| 149 | 232 | ); |
| 150 | | }, |
| 151 | | Effect.mapError((cause) => new DeviceUnregistrationPersistenceError({ cause })), |
| 152 | | ), |
| 153 | | listForUser: Effect.fn("relay.devices.listForUser")( |
| 154 | | function* (input) { |
| 155 | | const rows = yield* db |
| 156 | | .select({ |
| 157 | | deviceId: relayMobileDevices.deviceId, |
| 158 | | label: relayMobileDevices.label, |
| 159 | | platform: relayMobileDevices.platform, |
| 160 | | iosMajorVersion: relayMobileDevices.iosMajorVersion, |
| 161 | | appVersion: relayMobileDevices.appVersion, |
| 162 | | preferences: relayMobileDevices.preferencesJson, |
| 163 | | updatedAt: relayMobileDevices.updatedAt, |
| 164 | | }) |
| 165 | | .from(relayMobileDevices) |
| 166 | | .where(eq(relayMobileDevices.userId, input.userId)); |
| 167 | | return rows.map((row) => ({ |
| 168 | | deviceId: row.deviceId, |
| 169 | | label: row.label, |
| 170 | | platform: row.platform, |
| 171 | | iosMajorVersion: row.iosMajorVersion, |
| 172 | | appVersion: row.appVersion, |
| 173 | | notifications: { |
| 174 | | enabled: row.preferences.notificationsEnabled, |
| 175 | | notifyOnApproval: row.preferences.notifyOnApproval, |
| 176 | | notifyOnInput: row.preferences.notifyOnInput, |
| 177 | | notifyOnCompletion: row.preferences.notifyOnCompletion, |
| 178 | | notifyOnFailure: row.preferences.notifyOnFailure, |
| 179 | | }, |
| 180 | | liveActivities: { |
| 181 | | enabled: row.preferences.liveActivitiesEnabled, |
| 182 | | }, |
| 183 | | updatedAt: row.updatedAt, |
| 184 | | })); |
| 185 | | }, |
| 186 | | Effect.mapError((cause) => new DeviceListPersistenceError({ cause })), |
| 187 | | ), |
| 233 | return rows.map((row) => ({ |
| 234 | deviceId: row.deviceId, |
| 235 | label: row.label, |
| 236 | platform: row.platform, |
| 237 | iosMajorVersion: row.iosMajorVersion, |
| 238 | appVersion: row.appVersion, |
| 239 | notifications: { |
| 240 | enabled: row.preferences.notificationsEnabled, |
| 241 | notifyOnApproval: row.preferences.notifyOnApproval, |
| 242 | notifyOnInput: row.preferences.notifyOnInput, |
| 243 | notifyOnCompletion: row.preferences.notifyOnCompletion, |
| 244 | notifyOnFailure: row.preferences.notifyOnFailure, |
| 245 | }, |
| 246 | liveActivities: { |
| 247 | enabled: row.preferences.liveActivitiesEnabled, |
| 248 | }, |
| 249 | updatedAt: row.updatedAt, |
| 250 | })); |
| 251 | }), |
| 188 | 252 | }); |
| 189 | 253 | }); |
| 190 | 254 | |