integrations.prisma 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. /// One installed connector for one organisation: the account it is linked
  2. /// to, its sealed credentials, and the settings the workshop chose. Rows in
  3. /// the other integration tables hang off this one.
  4. model IntegrationConnection {
  5. id String @id @default(cuid())
  6. organizationId String
  7. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  8. /// Connector id from the registry, for example 'google-calendar'.
  9. connectorId String
  10. /// pending | active | error | disconnected
  11. status String @default("pending")
  12. label String?
  13. /// Tokens or keys as one sealed string; only the vault reads it.
  14. credentials String?
  15. settings Json @default("{}")
  16. /// Connector-owned state such as sync cursors and last-run times.
  17. state Json @default("{}")
  18. externalAccountId String?
  19. externalAccountName String?
  20. scopes String?
  21. /// OAuth handshake nonce, set while the connection is pending.
  22. oauthState String? @unique
  23. lastHealthAt DateTime?
  24. lastSyncAt DateTime?
  25. lastError String?
  26. createdById String
  27. createdAt DateTime @default(now())
  28. updatedAt DateTime @updatedAt
  29. links IntegrationLink[]
  30. jobs IntegrationJob[]
  31. logs IntegrationLog[]
  32. externalEvents ExternalCalendarEvent[]
  33. backupRuns BackupRun[]
  34. repairData RepairDataCache[]
  35. @@unique([organizationId, connectorId])
  36. @@index([organizationId])
  37. @@map("integration_connections")
  38. }
  39. /// The remote id a local record has in one connected system, which is what
  40. /// makes every push an update instead of a duplicate.
  41. model IntegrationLink {
  42. id String @id @default(cuid())
  43. connectionId String
  44. connection IntegrationConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
  45. /// Local model name, for example 'ServiceRecord'.
  46. entityType String
  47. entityId String
  48. remoteId String
  49. remoteUrl String?
  50. /// Connector-owned extras, such as a meeting link.
  51. metadata Json?
  52. checksum String?
  53. remoteUpdatedAt DateTime?
  54. lastSyncedAt DateTime @default(now())
  55. createdAt DateTime @default(now())
  56. @@unique([connectionId, entityType, entityId])
  57. @@index([connectionId, remoteId])
  58. @@index([entityType, entityId])
  59. @@map("integration_links")
  60. }
  61. /// A unit of work for a connector, claimed and retried by the cron.
  62. model IntegrationJob {
  63. id String @id @default(cuid())
  64. connectionId String
  65. connection IntegrationConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
  66. organizationId String
  67. kind String
  68. payload Json?
  69. /// Set while the job is pending so the same work is queued once; cleared when it runs.
  70. idempotencyKey String?
  71. /// queued | running | done | failed | dead
  72. status String @default("queued")
  73. attempts Int @default(0)
  74. maxAttempts Int @default(5)
  75. runAfter DateTime @default(now())
  76. lockedAt DateTime?
  77. startedAt DateTime?
  78. finishedAt DateTime?
  79. error String?
  80. createdAt DateTime @default(now())
  81. updatedAt DateTime @updatedAt
  82. @@unique([connectionId, idempotencyKey])
  83. @@index([status, runAfter])
  84. @@index([connectionId, createdAt])
  85. @@index([organizationId, createdAt])
  86. @@map("integration_jobs")
  87. }
  88. /// What a connector did and why it failed, per connection, kept for 30 days.
  89. model IntegrationLog {
  90. id String @id @default(cuid())
  91. connectionId String
  92. connection IntegrationConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
  93. jobId String?
  94. /// info | warn | error
  95. level String @default("info")
  96. message String
  97. details Json?
  98. createdAt DateTime @default(now())
  99. @@index([connectionId, createdAt])
  100. @@map("integration_logs")
  101. }
  102. /// Busy time pulled from a connected calendar, shown read-only on the
  103. /// workshop calendar. A cache: rebuilt by the pull job, never edited.
  104. model ExternalCalendarEvent {
  105. id String @id @default(cuid())
  106. connectionId String
  107. connection IntegrationConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
  108. organizationId String
  109. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  110. remoteId String
  111. calendarId String
  112. title String
  113. startAt DateTime
  114. endAt DateTime
  115. allDay Boolean @default(false)
  116. /// The event's page in the vendor's own calendar, when the vendor gives one.
  117. remoteUrl String?
  118. remoteUpdatedAt DateTime?
  119. createdAt DateTime @default(now())
  120. updatedAt DateTime @updatedAt
  121. @@unique([connectionId, remoteId])
  122. @@index([organizationId, startAt])
  123. @@map("external_calendar_events")
  124. }
  125. /// What a safety authority knows about one model year: recalls, owner
  126. /// complaints and crash ratings, as the connector shaped them. Public data,
  127. /// keyed by model rather than by workshop or vehicle, so a 2003 Accord is
  128. /// fetched once for everyone. A cache: rebuilt by the connector, never edited.
  129. model VehicleSafetyReport {
  130. id String @id @default(cuid())
  131. /// Connector id, such as "nhtsa".
  132. source String
  133. /// Make, model and year as the workshop wrote them, upper-cased and trimmed.
  134. make String
  135. model String
  136. year Int
  137. /// False when the authority answered but had no such model.
  138. found Boolean @default(true)
  139. data Json
  140. /// Counts lifted out of data so a list can show a badge without parsing it.
  141. recallCount Int @default(0)
  142. complaintCount Int @default(0)
  143. fetchedAt DateTime @default(now())
  144. createdAt DateTime @default(now())
  145. @@unique([source, make, model, year])
  146. @@index([fetchedAt])
  147. @@map("vehicle_safety_reports")
  148. }
  149. /// Labor times or fluid specs a repair data source answered for one model
  150. /// year, kept for as long as the key that fetched them is on a plan whose
  151. /// terms allow it (Open Labor Project's paid plans), so a vehicle looked up
  152. /// once is not paid for again. Deleted when the key's plan drops to free, the
  153. /// key is refused, or the workshop disconnects.
  154. model RepairDataCache {
  155. id String @id @default(cuid())
  156. /// Connector id, such as "openlaborproject".
  157. source String
  158. /// Whose key fetched it: the connection id for a workshop's own key, or
  159. /// "platform" for the key the server holds, whose rows serve every
  160. /// workshop that uses it.
  161. owner String
  162. /// Set for a workshop's own key, so its rows go when it disconnects.
  163. connectionId String?
  164. connection IntegrationConnection? @relation(fields: [connectionId], references: [id], onDelete: Cascade)
  165. /// labor | fluids
  166. kind String
  167. /// "make|model|year" as the workshop wrote them, lower-cased, or
  168. /// "vin:<sha-256>" when only the VIN found the vehicle. No VIN is stored.
  169. lookupKey String
  170. /// Bumped when the stored shape changes; an older row is fetched again.
  171. version Int
  172. data Json
  173. fetchedAt DateTime @default(now())
  174. createdAt DateTime @default(now())
  175. @@unique([source, owner, kind, lookupKey])
  176. @@index([connectionId])
  177. @@map("repair_data_cache")
  178. }
  179. /// One backup of one workshop to one destination: a scheduled or manual
  180. /// restic run, what it produced and how it ended. Kept so the connection page
  181. /// can show the last good backup and the cron can tell when one is overdue.
  182. model BackupRun {
  183. id String @id @default(cuid())
  184. organizationId String
  185. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  186. /// destination: the workshop's own bucket or folder (connectionId set).
  187. /// platform: the repository Torqvoice keeps for the workshop on the cloud
  188. /// instance (no connection).
  189. kind String @default("destination")
  190. connectionId String?
  191. connection IntegrationConnection? @relation(fields: [connectionId], references: [id], onDelete: Cascade)
  192. /// scheduled | manual | restore
  193. trigger String
  194. /// queued | running | done | failed | skipped
  195. status String @default("queued")
  196. startedAt DateTime?
  197. finishedAt DateTime?
  198. /// restic snapshot id: the one a backup made, or the one a restore put back.
  199. snapshotId String?
  200. /// The snapshot of the workshop as it was taken right before a restore
  201. /// replaced it, so a restore can itself be undone.
  202. safetySnapshotId String?
  203. /// Who asked: a member's user id, or null for a run the schedule or
  204. /// support (through torqvoice.com) started.
  205. requestedBy String?
  206. /// Why support restored, as typed on torqvoice.com; shown to the workshop.
  207. reason String?
  208. filesNew Int?
  209. bytesAdded BigInt?
  210. totalBytes BigInt?
  211. error String?
  212. createdAt DateTime @default(now())
  213. @@index([connectionId, createdAt])
  214. @@index([organizationId, kind, createdAt])
  215. @@index([status, createdAt])
  216. @@map("backup_runs")
  217. }
  218. /// The backups Torqvoice runs for a workshop on the cloud instance: one
  219. /// restic repository per workshop in a bucket of ours, hourly, Pro and up.
  220. /// One row per workshop that has ever been in the cycle; the run rows carry
  221. /// the history, this row carries where the schedule stands.
  222. model PlatformBackup {
  223. organizationId String @id
  224. organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  225. lastScheduledAt DateTime?
  226. lastGoodAt DateTime?
  227. lastPruneAt DateTime?
  228. lastCheckAt DateTime?
  229. /// One retry half an hour after a failed or skipped run.
  230. retryAt DateTime?
  231. /// Set while the workshop's plan no longer includes platform backups.
  232. /// Runs stop; the repository is kept for a grace period, then forgotten.
  233. pausedAt DateTime?
  234. /// The repository's snapshots were forgotten after the grace period.
  235. purgedAt DateTime?
  236. repositorySize BigInt?
  237. /// The restore points in the repository after the last run, newest first:
  238. /// [{ id, shortId, time, bytes, trigger, preRestore }]. What the Backups
  239. /// card and torqvoice.com list, so neither has to open the repository.
  240. snapshots Json @default("[]")
  241. lastFailureMailAt DateTime?
  242. lastOverdueAlertAt DateTime?
  243. createdAt DateTime @default(now())
  244. updatedAt DateTime @updatedAt
  245. @@index([pausedAt])
  246. @@map("platform_backups")
  247. }