@actuate-media/cms-core 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/LICENSE +21 -21
  2. package/dist/__tests__/actions/document-crud.test.js +74 -2
  3. package/dist/__tests__/actions/document-crud.test.js.map +1 -1
  4. package/dist/__tests__/api/cache-headers.test.d.ts +2 -0
  5. package/dist/__tests__/api/cache-headers.test.d.ts.map +1 -0
  6. package/dist/__tests__/api/cache-headers.test.js +135 -0
  7. package/dist/__tests__/api/cache-headers.test.js.map +1 -0
  8. package/dist/__tests__/api/comments-routes.test.d.ts +2 -0
  9. package/dist/__tests__/api/comments-routes.test.d.ts.map +1 -0
  10. package/dist/__tests__/api/comments-routes.test.js +266 -0
  11. package/dist/__tests__/api/comments-routes.test.js.map +1 -0
  12. package/dist/__tests__/api/notifications-routes.test.d.ts +2 -0
  13. package/dist/__tests__/api/notifications-routes.test.d.ts.map +1 -0
  14. package/dist/__tests__/api/notifications-routes.test.js +225 -0
  15. package/dist/__tests__/api/notifications-routes.test.js.map +1 -0
  16. package/dist/__tests__/api/preview-and-scheduling.test.js +53 -0
  17. package/dist/__tests__/api/preview-and-scheduling.test.js.map +1 -1
  18. package/dist/__tests__/fields/component-block.test.d.ts +2 -0
  19. package/dist/__tests__/fields/component-block.test.d.ts.map +1 -0
  20. package/dist/__tests__/fields/component-block.test.js +129 -0
  21. package/dist/__tests__/fields/component-block.test.js.map +1 -0
  22. package/dist/__tests__/fields/relations.test.js +62 -8
  23. package/dist/__tests__/fields/relations.test.js.map +1 -1
  24. package/dist/__tests__/preview-event-bus.test.d.ts +2 -0
  25. package/dist/__tests__/preview-event-bus.test.d.ts.map +1 -0
  26. package/dist/__tests__/preview-event-bus.test.js +89 -0
  27. package/dist/__tests__/preview-event-bus.test.js.map +1 -0
  28. package/dist/__tests__/preview-sse-stream.test.d.ts +2 -0
  29. package/dist/__tests__/preview-sse-stream.test.d.ts.map +1 -0
  30. package/dist/__tests__/preview-sse-stream.test.js +117 -0
  31. package/dist/__tests__/preview-sse-stream.test.js.map +1 -0
  32. package/dist/__tests__/realtime/comments.test.d.ts +2 -0
  33. package/dist/__tests__/realtime/comments.test.d.ts.map +1 -0
  34. package/dist/__tests__/realtime/comments.test.js +508 -0
  35. package/dist/__tests__/realtime/comments.test.js.map +1 -0
  36. package/dist/__tests__/realtime/gateway-ws-integration.test.d.ts +2 -0
  37. package/dist/__tests__/realtime/gateway-ws-integration.test.d.ts.map +1 -0
  38. package/dist/__tests__/realtime/gateway-ws-integration.test.js +170 -0
  39. package/dist/__tests__/realtime/gateway-ws-integration.test.js.map +1 -0
  40. package/dist/__tests__/realtime/gateway.test.d.ts +2 -0
  41. package/dist/__tests__/realtime/gateway.test.d.ts.map +1 -0
  42. package/dist/__tests__/realtime/gateway.test.js +402 -0
  43. package/dist/__tests__/realtime/gateway.test.js.map +1 -0
  44. package/dist/__tests__/realtime/notifications.test.d.ts +2 -0
  45. package/dist/__tests__/realtime/notifications.test.d.ts.map +1 -0
  46. package/dist/__tests__/realtime/notifications.test.js +317 -0
  47. package/dist/__tests__/realtime/notifications.test.js.map +1 -0
  48. package/dist/__tests__/realtime/persistence.test.d.ts +2 -0
  49. package/dist/__tests__/realtime/persistence.test.d.ts.map +1 -0
  50. package/dist/__tests__/realtime/persistence.test.js +124 -0
  51. package/dist/__tests__/realtime/persistence.test.js.map +1 -0
  52. package/dist/__tests__/schema-preview-features.test.d.ts +2 -0
  53. package/dist/__tests__/schema-preview-features.test.d.ts.map +1 -0
  54. package/dist/__tests__/schema-preview-features.test.js +42 -0
  55. package/dist/__tests__/schema-preview-features.test.js.map +1 -0
  56. package/dist/actions.d.ts.map +1 -1
  57. package/dist/actions.js +71 -26
  58. package/dist/actions.js.map +1 -1
  59. package/dist/api/handlers.d.ts.map +1 -1
  60. package/dist/api/handlers.js +734 -188
  61. package/dist/api/handlers.js.map +1 -1
  62. package/dist/cache/__tests__/http.test.d.ts +2 -0
  63. package/dist/cache/__tests__/http.test.d.ts.map +1 -0
  64. package/dist/cache/__tests__/http.test.js +285 -0
  65. package/dist/cache/__tests__/http.test.js.map +1 -0
  66. package/dist/cache/http.d.ts +119 -0
  67. package/dist/cache/http.d.ts.map +1 -0
  68. package/dist/cache/http.js +188 -0
  69. package/dist/cache/http.js.map +1 -0
  70. package/dist/config/types.d.ts +153 -1
  71. package/dist/config/types.d.ts.map +1 -1
  72. package/dist/fields/component-block.d.ts +23 -0
  73. package/dist/fields/component-block.d.ts.map +1 -0
  74. package/dist/fields/component-block.js +137 -0
  75. package/dist/fields/component-block.js.map +1 -0
  76. package/dist/fields/index.d.ts.map +1 -1
  77. package/dist/fields/index.js +2 -0
  78. package/dist/fields/index.js.map +1 -1
  79. package/dist/fields/relations.d.ts.map +1 -1
  80. package/dist/fields/relations.js +43 -11
  81. package/dist/fields/relations.js.map +1 -1
  82. package/dist/index.d.ts +5 -0
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +2 -0
  85. package/dist/index.js.map +1 -1
  86. package/dist/page-builder/schema.d.ts +2 -2
  87. package/dist/preview/event-bus.d.ts +39 -0
  88. package/dist/preview/event-bus.d.ts.map +1 -0
  89. package/dist/preview/event-bus.js +73 -0
  90. package/dist/preview/event-bus.js.map +1 -0
  91. package/dist/preview/sse-stream.d.ts +50 -0
  92. package/dist/preview/sse-stream.d.ts.map +1 -0
  93. package/dist/preview/sse-stream.js +120 -0
  94. package/dist/preview/sse-stream.js.map +1 -0
  95. package/dist/realtime/comments.d.ts +217 -0
  96. package/dist/realtime/comments.d.ts.map +1 -0
  97. package/dist/realtime/comments.js +282 -0
  98. package/dist/realtime/comments.js.map +1 -0
  99. package/dist/realtime/gateway.d.ts +107 -0
  100. package/dist/realtime/gateway.d.ts.map +1 -0
  101. package/dist/realtime/gateway.js +215 -0
  102. package/dist/realtime/gateway.js.map +1 -0
  103. package/dist/realtime/index.d.ts +18 -0
  104. package/dist/realtime/index.d.ts.map +1 -0
  105. package/dist/realtime/index.js +18 -0
  106. package/dist/realtime/index.js.map +1 -0
  107. package/dist/realtime/notifications.d.ts +209 -0
  108. package/dist/realtime/notifications.d.ts.map +1 -0
  109. package/dist/realtime/notifications.js +209 -0
  110. package/dist/realtime/notifications.js.map +1 -0
  111. package/dist/realtime/persistence.d.ts +72 -0
  112. package/dist/realtime/persistence.d.ts.map +1 -0
  113. package/dist/realtime/persistence.js +53 -0
  114. package/dist/realtime/persistence.js.map +1 -0
  115. package/package.json +13 -4
  116. package/prisma/cms-schema.prisma +437 -306
  117. package/prisma/migrations/0001_init/migration.sql +384 -384
  118. package/prisma/migrations/0002_folders/migration.sql +39 -39
  119. package/prisma/migrations/0003_search_and_webhooks/migration.sql +50 -50
  120. package/prisma/migrations/0004_script_tags/migration.sql +21 -21
  121. package/prisma/migrations/0005_password_reset_tokens/migration.sql +20 -20
  122. package/prisma/migrations/0006_page_builder/migration.sql +38 -38
  123. package/prisma/migrations/migration_lock.toml +3 -3
  124. package/prisma/schema.prisma +563 -549
  125. package/generated/browser.ts +0 -109
  126. package/generated/client.ts +0 -133
  127. package/generated/commonInputTypes.ts +0 -709
  128. package/generated/enums.ts +0 -125
  129. package/generated/internal/class.ts +0 -376
  130. package/generated/internal/prismaNamespace.ts +0 -2617
  131. package/generated/internal/prismaNamespaceBrowser.ts +0 -611
  132. package/generated/models/ApiKey.ts +0 -1550
  133. package/generated/models/AuditLog.ts +0 -1206
  134. package/generated/models/BackupRecord.ts +0 -1250
  135. package/generated/models/ContentLock.ts +0 -1472
  136. package/generated/models/ContentTemplate.ts +0 -1416
  137. package/generated/models/Document.ts +0 -3005
  138. package/generated/models/Folder.ts +0 -1904
  139. package/generated/models/FormSubmission.ts +0 -1200
  140. package/generated/models/InAppNotification.ts +0 -1457
  141. package/generated/models/Media.ts +0 -2340
  142. package/generated/models/MediaUsage.ts +0 -1472
  143. package/generated/models/OAuthAccount.ts +0 -1463
  144. package/generated/models/Redirect.ts +0 -1284
  145. package/generated/models/Session.ts +0 -1492
  146. package/generated/models/Site.ts +0 -1206
  147. package/generated/models/User.ts +0 -3513
  148. package/generated/models/Version.ts +0 -1511
  149. package/generated/models/WorkflowState.ts +0 -1514
  150. package/generated/models.ts +0 -29
@@ -1,306 +1,437 @@
1
- // ═══════════════════════════════════════════════════════════════════════════════
2
- // Actuate CMS — Prisma schema fragment
3
- // ═══════════════════════════════════════════════════════════════════════════════
4
- //
5
- // This file defines the database models expected by `@actuatecms/cms-core` API
6
- // handlers and server actions. It is a **fragment**: it does not include a
7
- // `datasource` or `generator` block.
8
- //
9
- // ── How to merge into your project ─────────────────────────────────────────────
10
- //
11
- // 1. Ensure your root `schema.prisma` uses PostgreSQL, for example:
12
- // datasource db {
13
- // provider = "postgresql"
14
- // url = env("DATABASE_URL")
15
- // }
16
- // generator client {
17
- // provider = "prisma-client-js"
18
- // output = "../node_modules/.prisma/client"
19
- // }
20
- //
21
- // 2. Copy the `enum` and `model` blocks below into your `schema.prisma` (or use
22
- // Prisma’s multi-file schema preview: put this file in `prisma/schema/` and
23
- // include it per Prisma’s multi-file docs).
24
- //
25
- // 3. Resolve naming clashes: if you already have a `User` or `Document` model,
26
- // rename these models and update your Actuate integration to use a custom
27
- // Prisma client, or map tables with `@@map("…")` and align delegate names.
28
- //
29
- // 4. Run migrations:
30
- // npx prisma migrate dev --name actuate-cms
31
- //
32
- // Models mirror `packages/cms-core/src/api/handlers.ts` and `actions.ts`
33
- // (including `contentHash`, `changeType` on versions, scheduling fields, etc.).
34
- //
35
- // ═══════════════════════════════════════════════════════════════════════════════
36
-
37
- enum DocumentStatus {
38
- DRAFT
39
- PUBLISHED
40
- ARCHIVED
41
- SCHEDULED
42
- }
43
-
44
- model User {
45
- id String @id @default(cuid())
46
- email String @unique
47
- name String @default("")
48
- role String @default("EDITOR")
49
- passwordHash String?
50
- isActive Boolean @default(true)
51
- isApproved Boolean @default(false)
52
- emailVerified Boolean @default(false)
53
- totpEnabled Boolean @default(false)
54
- totpSecret String?
55
- /// JSON array of one-time backup codes (e.g. `["abcd-1234", …]`).
56
- backupCodes Json?
57
- oauthProvider String?
58
- oauthId String?
59
- createdAt DateTime @default(now())
60
- updatedAt DateTime @updatedAt
61
-
62
- sessions Session[]
63
- documentsCreated Document[] @relation("DocumentCreatedBy")
64
- documentsUpdated Document[] @relation("DocumentUpdatedBy")
65
- documentsReviewed Document[] @relation("DocumentReviewer")
66
- versions Version[]
67
- mediaUploaded Media[] @relation("MediaUploadedBy")
68
- auditLogs AuditLog[]
69
-
70
- @@index([role])
71
- @@index([isActive])
72
- }
73
-
74
- model Session {
75
- id String @id @default(cuid())
76
- userId String
77
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
78
- /// JWT or opaque session token; optional until first persisted login (e.g. OAuth).
79
- token String? @unique
80
- expiresAt DateTime
81
- revokedAt DateTime?
82
- ipAddress String?
83
- userAgent String?
84
- createdAt DateTime @default(now())
85
-
86
- @@index([userId])
87
- @@index([expiresAt])
88
- }
89
-
90
- model Document {
91
- id String @id @default(cuid())
92
- collection String
93
- slug String?
94
- title String?
95
- data Json
96
- status DocumentStatus @default(DRAFT)
97
- plainText String? @db.Text
98
- locale String?
99
- folderId String?
100
- structuredData Json?
101
- workflowStage String?
102
- reviewerId String?
103
- reviewNote String? @db.Text
104
- publishedAt DateTime?
105
- scheduledAt DateTime?
106
- scheduledUnpublishAt DateTime?
107
- deletedAt DateTime?
108
- contentHash String?
109
- siteId String?
110
- templateId String?
111
- createdById String
112
- updatedById String
113
- createdAt DateTime @default(now())
114
- updatedAt DateTime @updatedAt
115
-
116
- createdBy User @relation("DocumentCreatedBy", fields: [createdById], references: [id], onDelete: Restrict)
117
- updatedBy User @relation("DocumentUpdatedBy", fields: [updatedById], references: [id], onDelete: Restrict)
118
- reviewer User? @relation("DocumentReviewer", fields: [reviewerId], references: [id], onDelete: SetNull)
119
- folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
120
- versions Version[]
121
- formSubmissions FormSubmission[]
122
-
123
- @@unique([collection, slug], name: "collection_slug")
124
- @@index([collection])
125
- @@index([status])
126
- @@index([deletedAt])
127
- @@index([publishedAt])
128
- @@index([folderId])
129
- @@index([locale])
130
- @@index([scheduledAt])
131
- @@index([scheduledUnpublishAt])
132
- @@index([createdById])
133
- @@index([updatedById])
134
- }
135
-
136
- model Media {
137
- id String @id @default(cuid())
138
- filename String
139
- storageKey String @unique
140
- mimeType String
141
- fileSize Int
142
- width Int?
143
- height Int?
144
- altText String?
145
- title String?
146
- blurHash String?
147
- focalPointX Float?
148
- focalPointY Float?
149
- folderId String?
150
- uploadedById String
151
- createdAt DateTime @default(now())
152
- updatedAt DateTime @updatedAt
153
-
154
- uploadedBy User @relation("MediaUploadedBy", fields: [uploadedById], references: [id], onDelete: Restrict)
155
- folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
156
-
157
- @@index([folderId])
158
- @@index([mimeType])
159
- @@index([createdAt])
160
- }
161
-
162
- model Version {
163
- id String @id @default(cuid())
164
- documentId String
165
- document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
166
- data Json
167
- changedById String
168
- changedBy User @relation(fields: [changedById], references: [id], onDelete: Restrict)
169
- /// CREATE | UPDATE | DELETE (see `actions.ts` / workflow).
170
- changeType String @default("UPDATE")
171
- createdAt DateTime @default(now())
172
-
173
- @@index([documentId])
174
- @@index([createdAt])
175
- }
176
-
177
- model Folder {
178
- id String @id @default(cuid())
179
- name String
180
- /// Discriminator for folder trees (e.g. `documents`, `media`).
181
- scope String
182
- parentId String?
183
- position Int @default(0)
184
- createdAt DateTime @default(now())
185
-
186
- parent Folder? @relation("FolderTree", fields: [parentId], references: [id], onDelete: Cascade)
187
- children Folder[] @relation("FolderTree")
188
- documents Document[]
189
- media Media[]
190
-
191
- @@index([scope])
192
- @@index([parentId])
193
- @@index([scope, parentId, position])
194
- }
195
-
196
- model Redirect {
197
- id String @id @default(cuid())
198
- source String
199
- destination String
200
- statusCode Int @default(301)
201
- isRegex Boolean @default(false)
202
- notes String?
203
- createdAt DateTime @default(now())
204
- updatedAt DateTime @updatedAt
205
-
206
- @@unique([source])
207
- @@index([createdAt])
208
- }
209
-
210
- model FormSubmission {
211
- id String @id @default(cuid())
212
- formId String
213
- form Document @relation(fields: [formId], references: [id], onDelete: Cascade)
214
- data Json
215
- attribution Json?
216
- submittedAt DateTime @default(now())
217
- createdAt DateTime @default(now())
218
-
219
- @@index([formId])
220
- @@index([submittedAt])
221
- @@index([createdAt])
222
- }
223
-
224
- model AuditLog {
225
- id String @id @default(cuid())
226
- event String
227
- userId String?
228
- user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
229
- details Json?
230
- ipAddress String?
231
- userAgent String?
232
- createdAt DateTime @default(now())
233
-
234
- @@index([event])
235
- @@index([userId])
236
- @@index([createdAt])
237
- }
238
-
239
- model PasswordResetToken {
240
- id String @id @default(cuid())
241
- userId String
242
- tokenHash String
243
- expiresAt DateTime
244
- usedAt DateTime?
245
- createdAt DateTime @default(now())
246
-
247
- @@index([tokenHash])
248
- @@index([userId])
249
- }
250
-
251
- model MediaUsage {
252
- id String @id @default(cuid())
253
- mediaId String
254
- documentId String
255
- fieldPath String?
256
- createdAt DateTime @default(now())
257
-
258
- @@unique([mediaId, documentId, fieldPath])
259
- @@index([mediaId])
260
- @@index([documentId])
261
- }
262
-
263
- model ScriptTag {
264
- id String @id @default(cuid())
265
- name String
266
- code String @db.Text
267
- placement String
268
- scope String
269
- targetPaths String[]
270
- priority Int @default(100)
271
- enabled Boolean @default(true)
272
- createdAt DateTime @default(now())
273
- updatedAt DateTime @updatedAt
274
-
275
- @@index([enabled])
276
- @@index([placement])
277
- }
278
-
279
- model PageTemplate {
280
- id String @id @default(cuid())
281
- name String
282
- description String?
283
- category String @default("content")
284
- tree Json
285
- thumbnail String?
286
- builtIn Boolean @default(false)
287
- createdAt DateTime @default(now())
288
- updatedAt DateTime @updatedAt
289
-
290
- @@index([category])
291
- @@index([builtIn])
292
- }
293
-
294
- model SavedSection {
295
- id String @id @default(cuid())
296
- name String
297
- description String?
298
- category String @default("content")
299
- tree Json
300
- thumbnail String?
301
- usageCount Int @default(0)
302
- createdAt DateTime @default(now())
303
- updatedAt DateTime @updatedAt
304
-
305
- @@index([category])
306
- }
1
+ // ═══════════════════════════════════════════════════════════════════════════════
2
+ // Actuate CMS — Prisma schema fragment
3
+ // ═══════════════════════════════════════════════════════════════════════════════
4
+ //
5
+ // This file defines the database models expected by `@actuatecms/cms-core` API
6
+ // handlers and server actions. It is a **fragment**: it does not include a
7
+ // `datasource` or `generator` block.
8
+ //
9
+ // ── How to merge into your project ─────────────────────────────────────────────
10
+ //
11
+ // 1. Ensure your root `schema.prisma` uses PostgreSQL, for example:
12
+ // datasource db {
13
+ // provider = "postgresql"
14
+ // url = env("DATABASE_URL")
15
+ // }
16
+ // generator client {
17
+ // provider = "prisma-client-js"
18
+ // output = "../node_modules/.prisma/client"
19
+ // }
20
+ //
21
+ // 2. Copy the `enum` and `model` blocks below into your `schema.prisma` (or use
22
+ // Prisma’s multi-file schema preview: put this file in `prisma/schema/` and
23
+ // include it per Prisma’s multi-file docs).
24
+ //
25
+ // 3. Resolve naming clashes: if you already have a `User` or `Document` model,
26
+ // rename these models and update your Actuate integration to use a custom
27
+ // Prisma client, or map tables with `@@map("…")` and align delegate names.
28
+ //
29
+ // 4. Run migrations:
30
+ // npx prisma migrate dev --name actuate-cms
31
+ //
32
+ // Models mirror `packages/cms-core/src/api/handlers.ts` and `actions.ts`
33
+ // (including `contentHash`, `changeType` on versions, scheduling fields, etc.).
34
+ //
35
+ // ═══════════════════════════════════════════════════════════════════════════════
36
+
37
+ enum DocumentStatus {
38
+ DRAFT
39
+ PUBLISHED
40
+ ARCHIVED
41
+ SCHEDULED
42
+ }
43
+
44
+ model User {
45
+ id String @id @default(cuid())
46
+ email String @unique
47
+ name String @default("")
48
+ role String @default("EDITOR")
49
+ passwordHash String?
50
+ isActive Boolean @default(true)
51
+ isApproved Boolean @default(false)
52
+ emailVerified Boolean @default(false)
53
+ totpEnabled Boolean @default(false)
54
+ totpSecret String?
55
+ /// JSON array of one-time backup codes (e.g. `["abcd-1234", …]`).
56
+ backupCodes Json?
57
+ oauthProvider String?
58
+ oauthId String?
59
+ createdAt DateTime @default(now())
60
+ updatedAt DateTime @updatedAt
61
+
62
+ sessions Session[]
63
+ documentsCreated Document[] @relation("DocumentCreatedBy")
64
+ documentsUpdated Document[] @relation("DocumentUpdatedBy")
65
+ documentsReviewed Document[] @relation("DocumentReviewer")
66
+ versions Version[]
67
+ mediaUploaded Media[] @relation("MediaUploadedBy")
68
+ auditLogs AuditLog[]
69
+ comments DocumentComment[]
70
+ notifications DocumentNotification[]
71
+
72
+ @@index([role])
73
+ @@index([isActive])
74
+ }
75
+
76
+ model Session {
77
+ id String @id @default(cuid())
78
+ userId String
79
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
80
+ /// JWT or opaque session token; optional until first persisted login (e.g. OAuth).
81
+ token String? @unique
82
+ expiresAt DateTime
83
+ revokedAt DateTime?
84
+ ipAddress String?
85
+ userAgent String?
86
+ createdAt DateTime @default(now())
87
+
88
+ @@index([userId])
89
+ @@index([expiresAt])
90
+ }
91
+
92
+ model Document {
93
+ id String @id @default(cuid())
94
+ collection String
95
+ slug String?
96
+ title String?
97
+ data Json
98
+ status DocumentStatus @default(DRAFT)
99
+ plainText String? @db.Text
100
+ locale String?
101
+ folderId String?
102
+ structuredData Json?
103
+ workflowStage String?
104
+ reviewerId String?
105
+ reviewNote String? @db.Text
106
+ publishedAt DateTime?
107
+ scheduledAt DateTime?
108
+ scheduledUnpublishAt DateTime?
109
+ deletedAt DateTime?
110
+ contentHash String?
111
+ siteId String?
112
+ templateId String?
113
+ createdById String
114
+ updatedById String
115
+ createdAt DateTime @default(now())
116
+ updatedAt DateTime @updatedAt
117
+
118
+ createdBy User @relation("DocumentCreatedBy", fields: [createdById], references: [id], onDelete: Restrict)
119
+ updatedBy User @relation("DocumentUpdatedBy", fields: [updatedById], references: [id], onDelete: Restrict)
120
+ reviewer User? @relation("DocumentReviewer", fields: [reviewerId], references: [id], onDelete: SetNull)
121
+ folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
122
+ versions Version[]
123
+ formSubmissions FormSubmission[]
124
+
125
+ @@unique([collection, slug], name: "collection_slug")
126
+ @@index([collection])
127
+ @@index([status])
128
+ @@index([deletedAt])
129
+ @@index([publishedAt])
130
+ @@index([folderId])
131
+ @@index([locale])
132
+ @@index([scheduledAt])
133
+ @@index([scheduledUnpublishAt])
134
+ @@index([createdById])
135
+ @@index([updatedById])
136
+ }
137
+
138
+ model Media {
139
+ id String @id @default(cuid())
140
+ filename String
141
+ storageKey String @unique
142
+ mimeType String
143
+ fileSize Int
144
+ width Int?
145
+ height Int?
146
+ altText String?
147
+ title String?
148
+ blurHash String?
149
+ focalPointX Float?
150
+ focalPointY Float?
151
+ folderId String?
152
+ uploadedById String
153
+ createdAt DateTime @default(now())
154
+ updatedAt DateTime @updatedAt
155
+
156
+ uploadedBy User @relation("MediaUploadedBy", fields: [uploadedById], references: [id], onDelete: Restrict)
157
+ folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
158
+
159
+ @@index([folderId])
160
+ @@index([mimeType])
161
+ @@index([createdAt])
162
+ }
163
+
164
+ model Version {
165
+ id String @id @default(cuid())
166
+ documentId String
167
+ document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
168
+ data Json
169
+ changedById String
170
+ changedBy User @relation(fields: [changedById], references: [id], onDelete: Restrict)
171
+ /// CREATE | UPDATE | DELETE (see `actions.ts` / workflow).
172
+ changeType String @default("UPDATE")
173
+ createdAt DateTime @default(now())
174
+
175
+ @@index([documentId])
176
+ @@index([createdAt])
177
+ }
178
+
179
+ model Folder {
180
+ id String @id @default(cuid())
181
+ name String
182
+ /// Discriminator for folder trees (e.g. `documents`, `media`).
183
+ scope String
184
+ parentId String?
185
+ position Int @default(0)
186
+ createdAt DateTime @default(now())
187
+
188
+ parent Folder? @relation("FolderTree", fields: [parentId], references: [id], onDelete: Cascade)
189
+ children Folder[] @relation("FolderTree")
190
+ documents Document[]
191
+ media Media[]
192
+
193
+ @@index([scope])
194
+ @@index([parentId])
195
+ @@index([scope, parentId, position])
196
+ }
197
+
198
+ model Redirect {
199
+ id String @id @default(cuid())
200
+ source String
201
+ destination String
202
+ statusCode Int @default(301)
203
+ isRegex Boolean @default(false)
204
+ notes String?
205
+ createdAt DateTime @default(now())
206
+ updatedAt DateTime @updatedAt
207
+
208
+ @@unique([source])
209
+ @@index([createdAt])
210
+ }
211
+
212
+ model FormSubmission {
213
+ id String @id @default(cuid())
214
+ formId String
215
+ form Document @relation(fields: [formId], references: [id], onDelete: Cascade)
216
+ data Json
217
+ attribution Json?
218
+ submittedAt DateTime @default(now())
219
+ createdAt DateTime @default(now())
220
+
221
+ @@index([formId])
222
+ @@index([submittedAt])
223
+ @@index([createdAt])
224
+ }
225
+
226
+ model AuditLog {
227
+ id String @id @default(cuid())
228
+ event String
229
+ userId String?
230
+ user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
231
+ details Json?
232
+ ipAddress String?
233
+ userAgent String?
234
+ createdAt DateTime @default(now())
235
+
236
+ @@index([event])
237
+ @@index([userId])
238
+ @@index([createdAt])
239
+ }
240
+
241
+ model PasswordResetToken {
242
+ id String @id @default(cuid())
243
+ userId String
244
+ tokenHash String
245
+ expiresAt DateTime
246
+ usedAt DateTime?
247
+ createdAt DateTime @default(now())
248
+
249
+ @@index([tokenHash])
250
+ @@index([userId])
251
+ }
252
+
253
+ model MediaUsage {
254
+ id String @id @default(cuid())
255
+ mediaId String
256
+ documentId String
257
+ fieldPath String?
258
+ createdAt DateTime @default(now())
259
+
260
+ @@unique([mediaId, documentId, fieldPath])
261
+ @@index([mediaId])
262
+ @@index([documentId])
263
+ }
264
+
265
+ model ScriptTag {
266
+ id String @id @default(cuid())
267
+ name String
268
+ code String @db.Text
269
+ placement String
270
+ scope String
271
+ targetPaths String[]
272
+ priority Int @default(100)
273
+ enabled Boolean @default(true)
274
+ createdAt DateTime @default(now())
275
+ updatedAt DateTime @updatedAt
276
+
277
+ @@index([enabled])
278
+ @@index([placement])
279
+ }
280
+
281
+ model PageTemplate {
282
+ id String @id @default(cuid())
283
+ name String
284
+ description String?
285
+ category String @default("content")
286
+ tree Json
287
+ thumbnail String?
288
+ builtIn Boolean @default(false)
289
+ createdAt DateTime @default(now())
290
+ updatedAt DateTime @updatedAt
291
+
292
+ @@index([category])
293
+ @@index([builtIn])
294
+ }
295
+
296
+ model SavedSection {
297
+ id String @id @default(cuid())
298
+ name String
299
+ description String?
300
+ category String @default("content")
301
+ tree Json
302
+ thumbnail String?
303
+ usageCount Int @default(0)
304
+ createdAt DateTime @default(now())
305
+ updatedAt DateTime @updatedAt
306
+
307
+ @@index([category])
308
+ }
309
+
310
+ /// Persisted Yjs state for a document, used by `@actuate-media/realtime`.
311
+ ///
312
+ /// Each row holds the encoded Yjs document (`Y.encodeStateAsUpdate`) plus a
313
+ /// version vector so consumers can serve incremental sync to late joiners
314
+ /// without replaying full history. There is at most one row per `documentId`;
315
+ /// it is upserted on every persistence flush from a live collaboration room.
316
+ ///
317
+ /// The model is *optional*: an Actuate deployment that does not use the
318
+ /// realtime collaboration features can leave this table empty. Cms-core only
319
+ /// touches it when the `/realtime/sync` endpoint is hit.
320
+ model DocumentCRDT {
321
+ /// References `Document.id`. We don't declare a Prisma relation here so
322
+ /// the realtime stack stays decoupled from the `Document` schema; the
323
+ /// constraint is enforced at the application layer.
324
+ documentId String @id
325
+ /// Yjs `encodeStateAsUpdate` bytes. Use bytea on Postgres.
326
+ state Bytes
327
+ /// Yjs `encodeStateVector` bytes — lets us reply to a `SyncStep1` without
328
+ /// decoding the full doc when persistence is hot but the room is cold.
329
+ stateVector Bytes
330
+ /// Monotonic counter for optimistic locking (incremented on every save).
331
+ version Int @default(0)
332
+ /// Wall-clock of the most recent flush.
333
+ lastUpdatedAt DateTime @default(now()) @updatedAt
334
+ /// User id of the most recent editor, if known.
335
+ lastUpdatedById String?
336
+ createdAt DateTime @default(now())
337
+
338
+ @@index([lastUpdatedAt])
339
+ }
340
+
341
+ /// Threaded inline comment anchored to a Yjs document range.
342
+ ///
343
+ /// Comments survive concurrent edits because the `anchor` field stores Yjs
344
+ /// *relative positions* (the binary blob you get from
345
+ /// `Y.createRelativePositionFromTypeIndex` followed by
346
+ /// `Y.encodeRelativePosition`) rather than absolute offsets. Slice 5 binds
347
+ /// these back to live ranges in the TipTap document.
348
+ ///
349
+ /// Threading is single-level: a top-level comment has `parentId == null`,
350
+ /// replies set `parentId` to the root comment's id. Deletes cascade so a
351
+ /// resolved or removed thread cleans up its replies.
352
+ ///
353
+ /// The model is *optional*. Deployments that don't enable Phase 3 simply
354
+ /// leave the table empty; the cms-core API stays serviceable without it.
355
+ model DocumentComment {
356
+ id String @id @default(cuid())
357
+ /// References `Document.id`. Like `DocumentCRDT`, we keep the foreign
358
+ /// constraint at the application layer so the realtime stack stays
359
+ /// decoupled from the core `Document` schema.
360
+ documentId String
361
+ /// `null` for top-level comments; the root comment's id for replies.
362
+ /// Replies cannot themselves be parents — slice 5 enforces this in the
363
+ /// API handler.
364
+ parentId String?
365
+ parent DocumentComment? @relation("CommentThread", fields: [parentId], references: [id], onDelete: Cascade)
366
+ replies DocumentComment[] @relation("CommentThread")
367
+ /// Author of the comment. We don't cascade on user deletion so an
368
+ /// orphaned comment is preserved with a null `userId`; the UI can then
369
+ /// render "deleted user" without losing thread history.
370
+ userId String?
371
+ user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
372
+ /// Markdown body (sanitized server-side before render).
373
+ body String @db.Text
374
+ /// Yjs relative-position JSON: `{ from: number[]; to: number[] }` where
375
+ /// each entry is the Base64-encoded `encodeRelativePosition` bytes.
376
+ /// Stored as JSON (not Bytes) so the API can return it without an
377
+ /// extra encoding hop. May be null for document-level (un-anchored)
378
+ /// comments.
379
+ anchor Json?
380
+ /// When set, the thread is resolved. UI hides resolved threads by
381
+ /// default but they remain in the table for audit.
382
+ resolvedAt DateTime?
383
+ /// User that resolved the thread, if any.
384
+ resolvedById String?
385
+ /// Soft delete for moderation; row is preserved so replies don't lose
386
+ /// their parent.
387
+ deletedAt DateTime?
388
+ createdAt DateTime @default(now())
389
+ updatedAt DateTime @updatedAt
390
+
391
+ @@index([documentId])
392
+ @@index([documentId, resolvedAt])
393
+ @@index([parentId])
394
+ @@index([userId])
395
+ @@index([createdAt])
396
+ }
397
+
398
+ // ─── Notifications (slice 6 — Phase 3) ─────────────────────────────────────────
399
+ //
400
+ // Per-user notification feed driven by collaboration events. Slice 6 wires
401
+ // the comment lifecycle (reply, resolve, mention) into this table so editors
402
+ // see "someone replied to your thread" without polling the comments API.
403
+ //
404
+ // The model is intentionally generic — the `kind` discriminator + `payload`
405
+ // JSON blob keeps the schema stable while future slices add new notification
406
+ // types (e.g. publish workflow transitions, document share invites).
407
+
408
+ model DocumentNotification {
409
+ id String @id @default(cuid())
410
+ /// Recipient. Cascade-deletes on user removal — we don't want orphan
411
+ /// notifications pointing at gone-users in the dropdown.
412
+ userId String
413
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
414
+ /// Discriminator. Slice 6 ships:
415
+ /// - "comment_reply" — someone replied to a thread you authored
416
+ /// - "comment_resolved" — your thread was resolved by another user
417
+ /// - "comment_mention" — your @handle was mentioned in a comment body
418
+ /// Future slices can extend this without a migration.
419
+ kind String
420
+ /// Optional document scope so the bell can group / link notifications.
421
+ documentId String?
422
+ /// Optional comment scope (the comment that triggered the notification).
423
+ commentId String?
424
+ /// `{ actorId, actorName, excerpt?, ... }`. Shape depends on `kind`;
425
+ /// the service module documents each variant.
426
+ payload Json
427
+ /// `null` until the user marks the notification read. We keep read rows
428
+ /// in the table so the dropdown can show recent activity, but the bell
429
+ /// counts only `readAt = null` rows.
430
+ readAt DateTime?
431
+ createdAt DateTime @default(now())
432
+
433
+ @@index([userId, readAt])
434
+ @@index([userId, createdAt])
435
+ @@index([documentId])
436
+ @@index([commentId])
437
+ }