@byline/cli 4.19.0 → 5.1.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 (34) hide show
  1. package/dist/lib/copied-script-drift.test.d.ts +2 -0
  2. package/dist/lib/copied-script-drift.test.d.ts.map +1 -0
  3. package/dist/lib/copied-script-drift.test.js +37 -0
  4. package/dist/lib/copied-script-drift.test.js.map +1 -0
  5. package/dist/phases/wire/server-ts.d.ts.map +1 -1
  6. package/dist/phases/wire/server-ts.js +72 -17
  7. package/dist/phases/wire/server-ts.js.map +1 -1
  8. package/dist/phases/wire/server-ts.test.d.ts +2 -0
  9. package/dist/phases/wire/server-ts.test.d.ts.map +1 -0
  10. package/dist/phases/wire/server-ts.test.js +37 -0
  11. package/dist/phases/wire/server-ts.test.js.map +1 -0
  12. package/dist/phases/wire/start-ts.d.ts +7 -0
  13. package/dist/phases/wire/start-ts.d.ts.map +1 -1
  14. package/dist/phases/wire/start-ts.js +76 -10
  15. package/dist/phases/wire/start-ts.js.map +1 -1
  16. package/dist/phases/wire/start-ts.test.d.ts +9 -0
  17. package/dist/phases/wire/start-ts.test.d.ts.map +1 -0
  18. package/dist/phases/wire/start-ts.test.js +50 -0
  19. package/dist/phases/wire/start-ts.test.js.map +1 -0
  20. package/dist/templates/byline-examples/scripts/import-docs.ts +13 -19
  21. package/dist/templates/byline-examples/scripts/lib/walk-document-status.ts +21 -0
  22. package/dist/templates/byline-examples/scripts/regenerate-media-operation.ts +17 -69
  23. package/dist/templates/byline-examples/scripts/regenerate-media.ts +20 -23
  24. package/dist/templates/byline-examples/seeds/site-settings.ts +10 -7
  25. package/dist/templates/dialects/mysql/byline/server.config.ts +15 -1
  26. package/dist/templates/dialects/mysql/byline-examples/server.config.ts +15 -1
  27. package/dist/templates/dialects/postgres/byline/server.config.ts +15 -1
  28. package/dist/templates/dialects/postgres/byline-examples/scripts/re-anchor.ts +15 -2
  29. package/dist/templates/dialects/postgres/byline-examples/server.config.ts +15 -1
  30. package/dist/templates/migrations/mysql/{0000_clean_magdalene.sql → 0000_kind_exodus.sql} +30 -2
  31. package/dist/templates/migrations/mysql/meta/_journal.json +2 -2
  32. package/dist/templates/migrations/postgres/{0000_quiet_johnny_blaze.sql → 0000_lively_talisman.sql} +28 -2
  33. package/dist/templates/migrations/postgres/meta/_journal.json +2 -2
  34. package/package.json +8 -8
@@ -6,14 +6,10 @@
6
6
  * storage.
7
7
  */
8
8
 
9
- import type { CollectionHandle } from '@byline/client'
10
- import {
11
- getDefaultStatus,
12
- getWorkflowStatuses,
13
- type CollectionDefinition,
14
- type IDbAdapter,
15
- type StoredFileValue,
16
- } from '@byline/core'
9
+ import type { StoredFileValue } from '@byline/core'
10
+ import { getDocumentHookCommittedDetails } from '@byline/core/services'
11
+
12
+ export { replaceDocumentFieldsPreservingStatus as replaceMediaVersionPreservingStatus } from '@byline/core/services'
17
13
 
18
14
  export function storedFilePaths(value: StoredFileValue): Set<string> {
19
15
  return new Set([
@@ -47,66 +43,18 @@ export function assertCompleteVariantSet(
47
43
  }
48
44
  }
49
45
 
50
- /**
51
- * Write the regenerated fields and restore the captured workflow status as
52
- * one transaction.
53
- *
54
- * `CollectionHandle.update()` creates a new immutable version at the
55
- * collection's default status. For this maintenance operation, changing that
56
- * newly-created row to the captured status inside the same transaction is not
57
- * an editorial transition: no caller can observe the intermediate draft, and
58
- * an archived item never passes through published. The final version row and
59
- * its normal `document.updated` activity entry retain the resulting status;
60
- * the explicit status mutation is additionally audited as a system action.
61
- *
62
- * Direct adapter commands are intentional here and should not be copied into
63
- * ordinary application writes. Scripts are the supported escape hatch for
64
- * migrations that must preserve metadata while still using the lifecycle for
65
- * the content update itself.
66
- */
67
- export async function replaceMediaVersionPreservingStatus(params: {
68
- db: IDbAdapter
69
- definition: CollectionDefinition
70
- collectionId: string
71
- handle: Pick<CollectionHandle, 'update'>
72
- documentId: string
73
- fields: Record<string, any>
74
- targetStatus: string
75
- }): Promise<{ documentVersionId: string }> {
76
- const statuses = getWorkflowStatuses(params.definition).map((status) => status.name)
77
- if (!statuses.includes(params.targetStatus)) {
78
- throw new Error(
79
- `regenerate-media: status '${params.targetStatus}' is not declared on collection ` +
80
- `'${params.definition.path}' (declared: ${statuses.join(', ')}).`
81
- )
46
+ /** Only rejected document writes may discard freshly prepared storage objects. */
47
+ export function mediaFailureOutcome(
48
+ error: unknown,
49
+ freshPaths: ReadonlySet<string>,
50
+ oldPaths: ReadonlySet<string>
51
+ ):
52
+ | { committed: true; revision: number; cleanupPaths: Set<string> }
53
+ | { committed: false; cleanupPaths: Set<string> } {
54
+ const committed = getDocumentHookCommittedDetails(error)
55
+ if (committed) return { committed: true, revision: committed.revision, cleanupPaths: new Set() }
56
+ return {
57
+ committed: false,
58
+ cleanupPaths: new Set([...freshPaths].filter((path) => !oldPaths.has(path))),
82
59
  }
83
-
84
- const defaultStatus = getDefaultStatus(params.definition)
85
- return params.db.withTransaction(async () => {
86
- const result = await params.handle.update(params.documentId, params.fields)
87
- if (params.targetStatus === defaultStatus) return result
88
-
89
- await params.db.commands.documents.setDocumentStatus({
90
- document_version_id: result.documentVersionId,
91
- status: params.targetStatus,
92
- })
93
- if (params.targetStatus === 'published') {
94
- await params.db.commands.documents.archivePublishedVersions({
95
- document_id: params.documentId,
96
- excludeVersionId: result.documentVersionId,
97
- })
98
- }
99
- await params.db.commands.audit.append({
100
- documentId: params.documentId,
101
- collectionId: params.collectionId,
102
- actorId: null,
103
- actorRealm: 'system',
104
- action: 'document.status.changed',
105
- field: 'status',
106
- before: defaultStatus,
107
- after: params.targetStatus,
108
- })
109
-
110
- return result
111
- })
112
60
  }
@@ -61,17 +61,14 @@ import path from 'node:path'
61
61
 
62
62
  import { createSuperAdminContext } from '@byline/auth'
63
63
  import { createBylineClient } from '@byline/client'
64
- import {
65
- type FieldUploadContext,
66
- getCollectionDefinition,
67
- getServerConfig,
68
- } from '@byline/core'
64
+ import { type FieldUploadContext, getCollectionDefinition, getServerConfig } from '@byline/core'
69
65
  import { extractImageMeta, generateImageVariants, isBypassMimeType } from '@byline/core/image'
70
66
  import { uploadField as coreUploadField } from '@byline/core/services'
71
-
72
67
  import type { MediaFields } from '@byline/generated-types'
68
+
73
69
  import {
74
70
  assertCompleteVariantSet,
71
+ mediaFailureOutcome,
75
72
  replaceMediaVersionPreservingStatus,
76
73
  storedFilePaths,
77
74
  } from './regenerate-media-operation.js'
@@ -120,14 +117,15 @@ async function run(): Promise<void> {
120
117
  id: string
121
118
  path: string
122
119
  status: string
120
+ revision: number
123
121
  fields: MediaFields
124
122
  }[] = []
125
123
  const pageSize = 100
126
124
  for (let page = 1; ; page++) {
127
- const result = await handle.find({
125
+ const result = await handle.findForEdit({
126
+ locale: 'all',
128
127
  page,
129
128
  pageSize,
130
- status: 'any',
131
129
  _bypassBeforeRead: true,
132
130
  })
133
131
  for (const d of result.docs) {
@@ -135,6 +133,7 @@ async function run(): Promise<void> {
135
133
  id: d.id,
136
134
  path: d.path,
137
135
  status: d.status,
136
+ revision: d.revision,
138
137
  fields: d.fields,
139
138
  })
140
139
  }
@@ -228,15 +227,10 @@ async function run(): Promise<void> {
228
227
  }
229
228
 
230
229
  const nextFields = { ...doc.fields, [FIELD_NAME]: uploadResult.storedFile }
231
- await replaceMediaVersionPreservingStatus({
232
- db: config.db,
233
- definition,
234
- collectionId,
235
- handle,
236
- documentId: doc.id,
237
- fields: nextFields,
238
- targetStatus: doc.status,
239
- })
230
+ await replaceMediaVersionPreservingStatus(
231
+ { ...baseUploadCtx, requestContext },
232
+ { documentId: doc.id, data: nextFields, expectedRevision: doc.revision }
233
+ )
240
234
 
241
235
  const variantSummary =
242
236
  uploadResult.storedFile.variants?.map((v) => `${v.name}:${v.format ?? '?'}`).join(', ') ??
@@ -246,11 +240,11 @@ async function run(): Promise<void> {
246
240
  )
247
241
  processed += 1
248
242
  } catch (err) {
249
- // The document transaction did not commit. Remove every fresh path we
250
- // know about, but never delete a path that belonged to the old value — a
251
- // deterministic beforeStore hook may already have overwritten it.
252
- const rollbackPaths = new Set([...freshPaths].filter((storagePath) => !oldPaths.has(storagePath)))
253
- const cleanupErrors = await deletePathsBestEffort(storage, rollbackPaths)
243
+ // A post-commit hook failure must retain the files referenced by the saved version.
244
+ // Rejected writes discard only fresh paths, preserving any reused original path.
245
+ const outcome = mediaFailureOutcome(err, freshPaths, oldPaths)
246
+ if (outcome.committed) processed += 1
247
+ const cleanupErrors = await deletePathsBestEffort(storage, outcome.cleanupPaths)
254
248
  const failure = asDocumentError(doc.id, doc.path, err)
255
249
  failures.push(
256
250
  cleanupErrors.length === 0
@@ -260,7 +254,10 @@ async function run(): Promise<void> {
260
254
  `regenerate-media: operation and rollback cleanup failed for '${doc.path}'`
261
255
  )
262
256
  )
263
- console.error(` ! fail ${doc.id} (${doc.path}):`, err)
257
+ console.error(
258
+ ` ! ${outcome.committed ? `saved revision ${outcome.revision}; hook failed` : 'fail'} ${doc.id} (${doc.path}):`,
259
+ err
260
+ )
264
261
  }
265
262
  }
266
263
 
@@ -11,14 +11,17 @@ import { getSystemBylineClient } from '@byline/client/server'
11
11
  /** Seed the singleton slot once, without overwriting later editorial changes. */
12
12
  export async function seedSiteSettings(): Promise<'seeded' | 'unchanged'> {
13
13
  const settings = getSystemBylineClient().singleton('site-settings')
14
- const existing = await settings.get({ status: 'any' })
15
- if (existing != null) return 'unchanged'
14
+ const existing = await settings.getForEdit()
15
+ if (existing?.state !== 'empty') return 'unchanged'
16
16
 
17
- await settings.update({
18
- siteName: 'Example site',
19
- siteDescription:
20
- 'A concise description of this site for search results and social media previews.',
21
- })
17
+ await settings.update(
18
+ {
19
+ siteName: 'Example site',
20
+ siteDescription:
21
+ 'A concise description of this site for search results and social media previews.',
22
+ },
23
+ { expectedState: 'empty' }
24
+ )
22
25
  console.log('Seeded site settings')
23
26
  return 'seeded'
24
27
  }
@@ -14,7 +14,9 @@
14
14
  */
15
15
 
16
16
  import { type AdminStore, registerAdminAbilities } from '@byline/admin'
17
- import { JwtSessionProvider } from '@byline/admin/auth'
17
+ import { createPasswordSignInLimiter, JwtSessionProvider } from '@byline/admin/auth'
18
+ import { getRequestIP } from '@tanstack/react-start/server'
19
+ import { createClientIpResolver } from '@byline/host-tanstack-start/integrations/client-ip'
18
20
  import { getAdminBylineClient } from '@byline/client/server'
19
21
  import { type BylineCore, initBylineCore } from '@byline/core'
20
22
  import { mysqlAdapter } from '@byline/db-mysql'
@@ -67,6 +69,8 @@ async function buildBylineCore(): Promise<BylineCore<AdminStore>> {
67
69
  )
68
70
  }
69
71
 
72
+ const signInLimiter = createPasswordSignInLimiter(adminStore.signInRateLimits, signingSecret)
73
+
70
74
  const sessionProvider = new JwtSessionProvider({
71
75
  store: adminStore,
72
76
  signingSecret,
@@ -86,6 +90,16 @@ async function buildBylineCore(): Promise<BylineCore<AdminStore>> {
86
90
  baseUrl: '/uploads',
87
91
  }),
88
92
  sessionProvider,
93
+ recurringTasks: [signInLimiter.cleanupTask],
94
+ passwordSignIn: {
95
+ limiter: signInLimiter,
96
+ resolveClientIp: createClientIpResolver({
97
+ // Opt in only when the proxy overwrites this header and direct access is blocked.
98
+ trustedProxyHeader: process.env.BYLINE_TRUSTED_CLIENT_IP_HEADER || undefined,
99
+ resolvePeerIp: () => getRequestIP() ??
100
+ (process.env.NODE_ENV === 'development' ? '127.0.0.1' : null),
101
+ }),
102
+ },
89
103
  fields: {
90
104
  richText: {
91
105
  embed: lexicalEditorEmbedServer({ getClient: getAdminBylineClient }),
@@ -11,7 +11,9 @@
11
11
  */
12
12
 
13
13
  import { type AdminStore, registerAdminAbilities } from '@byline/admin'
14
- import { JwtSessionProvider } from '@byline/admin/auth'
14
+ import { createPasswordSignInLimiter, JwtSessionProvider } from '@byline/admin/auth'
15
+ import { getRequestIP } from '@tanstack/react-start/server'
16
+ import { createClientIpResolver } from '@byline/host-tanstack-start/integrations/client-ip'
15
17
  import { getAdminBylineClient } from '@byline/client/server'
16
18
  import { type BylineCore, initBylineCore } from '@byline/core'
17
19
  import { mysqlAdapter } from '@byline/db-mysql'
@@ -75,6 +77,8 @@ async function buildBylineCore(): Promise<BylineCore<AdminStore>> {
75
77
  )
76
78
  }
77
79
 
80
+ const signInLimiter = createPasswordSignInLimiter(adminStore.signInRateLimits, signingSecret)
81
+
78
82
  const sessionProvider = new JwtSessionProvider({
79
83
  store: adminStore,
80
84
  signingSecret,
@@ -94,6 +98,16 @@ async function buildBylineCore(): Promise<BylineCore<AdminStore>> {
94
98
  baseUrl: '/uploads',
95
99
  }),
96
100
  sessionProvider,
101
+ recurringTasks: [signInLimiter.cleanupTask],
102
+ passwordSignIn: {
103
+ limiter: signInLimiter,
104
+ resolveClientIp: createClientIpResolver({
105
+ // Opt in only when the proxy overwrites this header and direct access is blocked.
106
+ trustedProxyHeader: process.env.BYLINE_TRUSTED_CLIENT_IP_HEADER || undefined,
107
+ resolvePeerIp: () => getRequestIP() ??
108
+ (process.env.NODE_ENV === 'development' ? '127.0.0.1' : null),
109
+ }),
110
+ },
97
111
  fields: {
98
112
  richText: {
99
113
  embed: lexicalEditorEmbedServer({ getClient: getAdminBylineClient }),
@@ -18,7 +18,9 @@
18
18
  */
19
19
 
20
20
  import { type AdminStore, registerAdminAbilities } from '@byline/admin'
21
- import { JwtSessionProvider } from '@byline/admin/auth'
21
+ import { createPasswordSignInLimiter, JwtSessionProvider } from '@byline/admin/auth'
22
+ import { getRequestIP } from '@tanstack/react-start/server'
23
+ import { createClientIpResolver } from '@byline/host-tanstack-start/integrations/client-ip'
22
24
  import { type BylineCore, initBylineCore } from '@byline/core'
23
25
  import { pgAdapter } from '@byline/db-postgres'
24
26
  import { createAdminStore } from '@byline/db-postgres/admin'
@@ -82,6 +84,8 @@ async function buildBylineCore(): Promise<BylineCore<AdminStore>> {
82
84
  )
83
85
  }
84
86
 
87
+ const signInLimiter = createPasswordSignInLimiter(adminStore.signInRateLimits, signingSecret)
88
+
85
89
  const sessionProvider = new JwtSessionProvider({
86
90
  store: adminStore,
87
91
  signingSecret,
@@ -136,6 +140,16 @@ async function buildBylineCore(): Promise<BylineCore<AdminStore>> {
136
140
  // cacheControl: 'public, max-age=31536000, immutable',
137
141
  // }),
138
142
  sessionProvider,
143
+ recurringTasks: [signInLimiter.cleanupTask],
144
+ passwordSignIn: {
145
+ limiter: signInLimiter,
146
+ resolveClientIp: createClientIpResolver({
147
+ // Opt in only when the proxy overwrites this header and direct access is blocked.
148
+ trustedProxyHeader: process.env.BYLINE_TRUSTED_CLIENT_IP_HEADER || undefined,
149
+ resolvePeerIp: () => getRequestIP() ??
150
+ (process.env.NODE_ENV === 'development' ? '127.0.0.1' : null),
151
+ }),
152
+ },
139
153
  fields: {
140
154
  richText: {
141
155
  embed: lexicalEditorEmbedServer({ getClient: getAdminBylineClient }),
@@ -16,6 +16,7 @@
16
16
  * listed — that list is the outstanding-translation backlog. Re-run as
17
17
  * translation progresses; it is idempotent (each doc is its own transaction).
18
18
  *
19
+ * From the application directory containing byline/:
19
20
  * pnpm tsx byline/scripts/re-anchor.ts --to fr
20
21
  * pnpm tsx byline/scripts/re-anchor.ts --to fr --collection pages
21
22
  * pnpm tsx byline/scripts/re-anchor.ts --to fr --dry-run
@@ -28,7 +29,7 @@ import '../server.config.js'
28
29
 
29
30
  import { parseArgs } from 'node:util'
30
31
 
31
- import { getBylineCore, getServerConfig } from '@byline/core'
32
+ import { documentRevisionFromDatabase, getBylineCore, getServerConfig } from '@byline/core'
32
33
  import type { PgAdapter } from '@byline/db-postgres'
33
34
 
34
35
  const USAGE =
@@ -74,7 +75,19 @@ async function run() {
74
75
  `${dryRun ? '[dry-run] ' : ''}re-anchoring ${scope} → content source locale '${targetLocale}'…`
75
76
  )
76
77
 
77
- const report = await db.reAnchorDocuments({ targetLocale, collectionId, dryRun })
78
+ // Capture each precondition before starting the batch; never refresh and retry a stale target.
79
+ const observed = await db.pool.query<{ document_id: string; revision: string }>(
80
+ `SELECT DISTINCT d.id AS document_id, d.revision
81
+ FROM byline_documents d JOIN byline_document_versions v ON v.document_id = d.id
82
+ WHERE v.is_deleted = false AND ($1::uuid IS NULL OR v.collection_id = $1::uuid)
83
+ ORDER BY d.id`,
84
+ [collectionId ?? null]
85
+ )
86
+ const targets = observed.rows.map((row) => ({
87
+ documentId: row.document_id,
88
+ expectedRevision: documentRevisionFromDatabase(row.revision),
89
+ }))
90
+ const report = await db.reAnchorDocuments({ targets, targetLocale, collectionId, dryRun })
78
91
 
79
92
  console.log(
80
93
  `${dryRun ? '[dry-run] would re-anchor' : '✓ re-anchored'} ${report.reanchored}/${report.total} document(s) → '${targetLocale}'`
@@ -15,7 +15,9 @@
15
15
  */
16
16
 
17
17
  import { type AdminStore, registerAdminAbilities } from '@byline/admin'
18
- import { JwtSessionProvider } from '@byline/admin/auth'
18
+ import { createPasswordSignInLimiter, JwtSessionProvider } from '@byline/admin/auth'
19
+ import { getRequestIP } from '@tanstack/react-start/server'
20
+ import { createClientIpResolver } from '@byline/host-tanstack-start/integrations/client-ip'
19
21
  import { type BylineCore, initBylineCore } from '@byline/core'
20
22
  import { pgAdapter } from '@byline/db-postgres'
21
23
  import { createAdminStore } from '@byline/db-postgres/admin'
@@ -130,6 +132,8 @@ async function buildBylineCore(): Promise<BylineCore<AdminStore>> {
130
132
  )
131
133
  }
132
134
 
135
+ const signInLimiter = createPasswordSignInLimiter(adminStore.signInRateLimits, signingSecret)
136
+
133
137
  const sessionProvider = new JwtSessionProvider({
134
138
  store: adminStore,
135
139
  signingSecret,
@@ -190,6 +194,16 @@ async function buildBylineCore(): Promise<BylineCore<AdminStore>> {
190
194
  // cacheControl: 'public, max-age=31536000, immutable',
191
195
  // }),
192
196
  sessionProvider,
197
+ recurringTasks: [signInLimiter.cleanupTask],
198
+ passwordSignIn: {
199
+ limiter: signInLimiter,
200
+ resolveClientIp: createClientIpResolver({
201
+ // Opt in only when the proxy overwrites this header and direct access is blocked.
202
+ trustedProxyHeader: process.env.BYLINE_TRUSTED_CLIENT_IP_HEADER || undefined,
203
+ resolvePeerIp: () => getRequestIP() ??
204
+ (process.env.NODE_ENV === 'development' ? '127.0.0.1' : null),
205
+ }),
206
+ },
193
207
  fields: {
194
208
  // Server-side richtext adapter — refreshes embedded relation
195
209
  // envelopes (link `{ title, path }`, inline-image `{ title, altText,
@@ -1,3 +1,12 @@
1
+ CREATE TABLE `byline_admin_login_sessions` (
2
+ `id` char(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
3
+ `admin_user_id` char(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
4
+ `session_version` int NOT NULL,
5
+ `expires_at` datetime(6) NOT NULL,
6
+ `revoked_at` datetime(6),
7
+ CONSTRAINT `byline_admin_login_sessions_id` PRIMARY KEY(`id`)
8
+ );
9
+ --> statement-breakpoint
1
10
  CREATE TABLE `byline_admin_permissions` (
2
11
  `id` char(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
3
12
  `vid` int NOT NULL DEFAULT 1,
@@ -12,7 +21,9 @@ CREATE TABLE `byline_admin_permissions` (
12
21
  CREATE TABLE `byline_admin_refresh_tokens` (
13
22
  `id` char(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
14
23
  `admin_user_id` char(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
24
+ `sid` char(36) CHARACTER SET ascii COLLATE ascii_bin,
15
25
  `token_hash` varchar(64) NOT NULL,
26
+ `session_version` int NOT NULL DEFAULT -1,
16
27
  `issued_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
17
28
  `expires_at` datetime(6) NOT NULL,
18
29
  `revoked_at` datetime(6),
@@ -46,6 +57,13 @@ CREATE TABLE `byline_admin_roles` (
46
57
  CONSTRAINT `byline_admin_roles_machine_name_unique` UNIQUE(`machine_name`)
47
58
  );
48
59
  --> statement-breakpoint
60
+ CREATE TABLE `byline_admin_sign_in_rate_limits` (
61
+ `key` varchar(64) NOT NULL,
62
+ `attempts` int NOT NULL,
63
+ `expires_at` datetime(3) NOT NULL,
64
+ CONSTRAINT `byline_admin_sign_in_rate_limits_key` PRIMARY KEY(`key`)
65
+ );
66
+ --> statement-breakpoint
49
67
  CREATE TABLE `byline_admin_user_preferences` (
50
68
  `user_id` char(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
51
69
  `scope` varchar(255) NOT NULL,
@@ -57,6 +75,7 @@ CREATE TABLE `byline_admin_user_preferences` (
57
75
  --> statement-breakpoint
58
76
  CREATE TABLE `byline_admin_users` (
59
77
  `id` char(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
78
+ `session_version` int NOT NULL DEFAULT 0,
60
79
  `vid` int NOT NULL DEFAULT 1,
61
80
  `given_name` varchar(100),
62
81
  `family_name` varchar(100),
@@ -173,6 +192,7 @@ CREATE TABLE `byline_document_publish_schedules` (
173
192
  `document_id` char(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
174
193
  `collection_id` char(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
175
194
  `target_version_id` char(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
195
+ `authorized_revision` bigint,
176
196
  `publish_at` datetime(6) NOT NULL,
177
197
  `state` varchar(32) NOT NULL DEFAULT 'armed',
178
198
  `suspended_at` datetime(6),
@@ -189,8 +209,9 @@ CREATE TABLE `byline_document_publish_schedules` (
189
209
  `attempt_count` int NOT NULL DEFAULT 0,
190
210
  `last_error` text,
191
211
  CONSTRAINT `byline_document_publish_schedules_document_id` PRIMARY KEY(`document_id`),
212
+ CONSTRAINT `check_publish_schedules_authorized_revision` CHECK(`byline_document_publish_schedules`.`authorized_revision` IS NULL OR `byline_document_publish_schedules`.`authorized_revision` BETWEEN 1 AND 9007199254740991),
192
213
  CONSTRAINT `check_publish_schedules_state` CHECK(`byline_document_publish_schedules`.`state` IN ('armed', 'needs_reconfirm')),
193
- CONSTRAINT `check_publish_schedules_suspended_reason` CHECK(`byline_document_publish_schedules`.`suspended_reason` IS NULL OR `byline_document_publish_schedules`.`suspended_reason` = 'content_edited')
214
+ CONSTRAINT `check_publish_schedules_suspended_reason` CHECK(`byline_document_publish_schedules`.`suspended_reason` IS NULL OR `byline_document_publish_schedules`.`suspended_reason` IN ('content_edited', 'document_metadata_changed', 'upgrade_invalidated'))
194
215
  );
195
216
  --> statement-breakpoint
196
217
  CREATE TABLE `byline_document_relationships` (
@@ -229,10 +250,12 @@ CREATE TABLE `byline_documents` (
229
250
  `collection_id` char(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
230
251
  `order_key` varchar(128) CHARACTER SET ascii COLLATE ascii_bin,
231
252
  `source_locale` varchar(10) NOT NULL,
253
+ `revision` bigint NOT NULL,
232
254
  `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
233
255
  `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
234
256
  CONSTRAINT `byline_documents_id` PRIMARY KEY(`id`),
235
- CONSTRAINT `uq_documents_collection_id_id` UNIQUE(`collection_id`,`id`)
257
+ CONSTRAINT `uq_documents_collection_id_id` UNIQUE(`collection_id`,`id`),
258
+ CONSTRAINT `check_documents_revision` CHECK(`byline_documents`.`revision` BETWEEN 1 AND 9007199254740991)
236
259
  );
237
260
  --> statement-breakpoint
238
261
  CREATE TABLE `byline_store_file` (
@@ -373,7 +396,9 @@ CREATE TABLE `byline_store_text` (
373
396
  CONSTRAINT `unique_text_field` UNIQUE(`document_version_id`,`field_path`,`locale`)
374
397
  );
375
398
  --> statement-breakpoint
399
+ ALTER TABLE `byline_admin_login_sessions` ADD CONSTRAINT `fk_admin_login_sessions_user` FOREIGN KEY (`admin_user_id`) REFERENCES `byline_admin_users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
376
400
  ALTER TABLE `byline_admin_permissions` ADD CONSTRAINT `fk_admin_permissions_admin_role_id` FOREIGN KEY (`admin_role_id`) REFERENCES `byline_admin_roles`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
401
+ ALTER TABLE `byline_admin_refresh_tokens` ADD CONSTRAINT `fk_admin_refresh_tokens_sid` FOREIGN KEY (`sid`) REFERENCES `byline_admin_login_sessions`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
377
402
  ALTER TABLE `byline_admin_refresh_tokens` ADD CONSTRAINT `fk_admin_refresh_tokens_admin_user_id` FOREIGN KEY (`admin_user_id`) REFERENCES `byline_admin_users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
378
403
  ALTER TABLE `byline_admin_role_admin_user` ADD CONSTRAINT `fk_admin_role_admin_user_admin_role_id` FOREIGN KEY (`admin_role_id`) REFERENCES `byline_admin_roles`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
379
404
  ALTER TABLE `byline_admin_role_admin_user` ADD CONSTRAINT `fk_admin_role_admin_user_admin_user_id` FOREIGN KEY (`admin_user_id`) REFERENCES `byline_admin_users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
@@ -410,11 +435,14 @@ ALTER TABLE `byline_store_relation` ADD CONSTRAINT `fk_store_relation_target_col
410
435
  ALTER TABLE `byline_singleton_documents` ADD CONSTRAINT `fk_singleton_documents_document` FOREIGN KEY (`collection_id`,`document_id`) REFERENCES `byline_documents`(`collection_id`,`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
411
436
  ALTER TABLE `byline_store_text` ADD CONSTRAINT `fk_store_text_document_version_id` FOREIGN KEY (`document_version_id`) REFERENCES `byline_document_versions`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
412
437
  ALTER TABLE `byline_store_text` ADD CONSTRAINT `fk_store_text_collection_id` FOREIGN KEY (`collection_id`) REFERENCES `byline_collections`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
438
+ CREATE INDEX `idx_admin_login_sessions_user` ON `byline_admin_login_sessions` (`admin_user_id`);--> statement-breakpoint
413
439
  CREATE INDEX `idx_byline_admin_permissions_role` ON `byline_admin_permissions` (`admin_role_id`);--> statement-breakpoint
440
+ CREATE INDEX `idx_admin_refresh_tokens_sid` ON `byline_admin_refresh_tokens` (`sid`);--> statement-breakpoint
414
441
  CREATE INDEX `idx_byline_admin_refresh_tokens_user` ON `byline_admin_refresh_tokens` (`admin_user_id`);--> statement-breakpoint
415
442
  CREATE INDEX `idx_byline_admin_refresh_tokens_token_hash` ON `byline_admin_refresh_tokens` (`token_hash`);--> statement-breakpoint
416
443
  CREATE INDEX `idx_byline_admin_role_admin_user_user` ON `byline_admin_role_admin_user` (`admin_user_id`);--> statement-breakpoint
417
444
  CREATE INDEX `idx_byline_admin_roles_machine_name` ON `byline_admin_roles` (`machine_name`);--> statement-breakpoint
445
+ CREATE INDEX `idx_admin_sign_in_rate_limits_expiry` ON `byline_admin_sign_in_rate_limits` (`expires_at`);--> statement-breakpoint
418
446
  CREATE INDEX `idx_byline_admin_users_email` ON `byline_admin_users` (`email`);--> statement-breakpoint
419
447
  CREATE INDEX `idx_audit_log_document_id` ON `byline_audit_log` (`document_id`,`id`);--> statement-breakpoint
420
448
  CREATE INDEX `idx_audit_log_actor_id` ON `byline_audit_log` (`actor_id`,`id`);--> statement-breakpoint
@@ -5,8 +5,8 @@
5
5
  {
6
6
  "idx": 0,
7
7
  "version": "5",
8
- "when": 1787797706202,
9
- "tag": "0000_clean_magdalene",
8
+ "when": 1788879988783,
9
+ "tag": "0000_kind_exodus",
10
10
  "breakpoints": true
11
11
  }
12
12
  ]
@@ -1,3 +1,11 @@
1
+ CREATE TABLE "byline_admin_login_sessions" (
2
+ "id" uuid PRIMARY KEY NOT NULL,
3
+ "admin_user_id" uuid NOT NULL,
4
+ "session_version" integer NOT NULL,
5
+ "expires_at" timestamp (6) with time zone NOT NULL,
6
+ "revoked_at" timestamp (6) with time zone
7
+ );
8
+ --> statement-breakpoint
1
9
  CREATE TABLE "byline_admin_permissions" (
2
10
  "id" uuid PRIMARY KEY NOT NULL,
3
11
  "vid" integer DEFAULT 1 NOT NULL,
@@ -11,7 +19,9 @@ CREATE TABLE "byline_admin_permissions" (
11
19
  CREATE TABLE "byline_admin_refresh_tokens" (
12
20
  "id" uuid PRIMARY KEY NOT NULL,
13
21
  "admin_user_id" uuid NOT NULL,
22
+ "sid" uuid,
14
23
  "token_hash" varchar(64) NOT NULL,
24
+ "session_version" integer DEFAULT -1 NOT NULL,
15
25
  "issued_at" timestamp (6) with time zone DEFAULT now() NOT NULL,
16
26
  "expires_at" timestamp (6) with time zone NOT NULL,
17
27
  "revoked_at" timestamp (6) with time zone,
@@ -43,6 +53,12 @@ CREATE TABLE "byline_admin_roles" (
43
53
  CONSTRAINT "byline_admin_roles_machine_name_unique" UNIQUE("machine_name")
44
54
  );
45
55
  --> statement-breakpoint
56
+ CREATE TABLE "byline_admin_sign_in_rate_limits" (
57
+ "key" varchar(64) PRIMARY KEY NOT NULL,
58
+ "attempts" integer NOT NULL,
59
+ "expires_at" timestamp (3) with time zone NOT NULL
60
+ );
61
+ --> statement-breakpoint
46
62
  CREATE TABLE "byline_admin_user_preferences" (
47
63
  "user_id" uuid NOT NULL,
48
64
  "scope" varchar(255) NOT NULL,
@@ -54,6 +70,7 @@ CREATE TABLE "byline_admin_user_preferences" (
54
70
  --> statement-breakpoint
55
71
  CREATE TABLE "byline_admin_users" (
56
72
  "id" uuid PRIMARY KEY NOT NULL,
73
+ "session_version" integer DEFAULT 0 NOT NULL,
57
74
  "vid" integer DEFAULT 1 NOT NULL,
58
75
  "given_name" varchar(100),
59
76
  "family_name" varchar(100),
@@ -163,6 +180,7 @@ CREATE TABLE "byline_document_publish_schedules" (
163
180
  "document_id" uuid PRIMARY KEY NOT NULL,
164
181
  "collection_id" uuid NOT NULL,
165
182
  "target_version_id" uuid NOT NULL,
183
+ "authorized_revision" bigint,
166
184
  "publish_at" timestamp (6) with time zone NOT NULL,
167
185
  "state" varchar(32) DEFAULT 'armed' NOT NULL,
168
186
  "suspended_at" timestamp (6) with time zone,
@@ -178,8 +196,9 @@ CREATE TABLE "byline_document_publish_schedules" (
178
196
  "next_attempt_at" timestamp (6) with time zone NOT NULL,
179
197
  "attempt_count" integer DEFAULT 0 NOT NULL,
180
198
  "last_error" text,
199
+ CONSTRAINT "check_publish_schedules_authorized_revision" CHECK ("byline_document_publish_schedules"."authorized_revision" IS NULL OR "byline_document_publish_schedules"."authorized_revision" BETWEEN 1 AND 9007199254740991),
181
200
  CONSTRAINT "check_document_publish_schedules_state" CHECK ("byline_document_publish_schedules"."state" IN ('armed', 'needs_reconfirm')),
182
- CONSTRAINT "check_document_publish_schedules_suspended_reason" CHECK ("byline_document_publish_schedules"."suspended_reason" IS NULL OR "byline_document_publish_schedules"."suspended_reason" = 'content_edited')
201
+ CONSTRAINT "check_document_publish_schedules_suspended_reason" CHECK ("byline_document_publish_schedules"."suspended_reason" IS NULL OR "byline_document_publish_schedules"."suspended_reason" IN ('content_edited', 'document_metadata_changed', 'upgrade_invalidated'))
183
202
  );
184
203
  --> statement-breakpoint
185
204
  CREATE TABLE "byline_document_relationships" (
@@ -217,9 +236,11 @@ CREATE TABLE "byline_documents" (
217
236
  "collection_id" uuid NOT NULL,
218
237
  "order_key" varchar(128) COLLATE "C",
219
238
  "source_locale" varchar(10) NOT NULL,
239
+ "revision" bigint NOT NULL,
220
240
  "created_at" timestamp (6) with time zone DEFAULT now() NOT NULL,
221
241
  "updated_at" timestamp (6) with time zone DEFAULT now() NOT NULL,
222
- CONSTRAINT "uq_documents_collection_id_id" UNIQUE("collection_id","id")
242
+ CONSTRAINT "uq_documents_collection_id_id" UNIQUE("collection_id","id"),
243
+ CONSTRAINT "check_documents_revision" CHECK ("byline_documents"."revision" BETWEEN 1 AND 9007199254740991)
223
244
  );
224
245
  --> statement-breakpoint
225
246
  CREATE TABLE "byline_store_file" (
@@ -352,8 +373,10 @@ CREATE TABLE "byline_store_text" (
352
373
  CONSTRAINT "unique_text_field" UNIQUE("document_version_id","field_path","locale")
353
374
  );
354
375
  --> statement-breakpoint
376
+ ALTER TABLE "byline_admin_login_sessions" ADD CONSTRAINT "byline_admin_login_sessions_admin_user_id_byline_admin_users_id_fk" FOREIGN KEY ("admin_user_id") REFERENCES "public"."byline_admin_users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
355
377
  ALTER TABLE "byline_admin_permissions" ADD CONSTRAINT "byline_admin_permissions_admin_role_id_byline_admin_roles_id_fk" FOREIGN KEY ("admin_role_id") REFERENCES "public"."byline_admin_roles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
356
378
  ALTER TABLE "byline_admin_refresh_tokens" ADD CONSTRAINT "byline_admin_refresh_tokens_admin_user_id_byline_admin_users_id_fk" FOREIGN KEY ("admin_user_id") REFERENCES "public"."byline_admin_users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
379
+ ALTER TABLE "byline_admin_refresh_tokens" ADD CONSTRAINT "byline_admin_refresh_tokens_sid_byline_admin_login_sessions_id_fk" FOREIGN KEY ("sid") REFERENCES "public"."byline_admin_login_sessions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
357
380
  ALTER TABLE "byline_admin_role_admin_user" ADD CONSTRAINT "byline_admin_role_admin_user_admin_role_id_byline_admin_roles_id_fk" FOREIGN KEY ("admin_role_id") REFERENCES "public"."byline_admin_roles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
358
381
  ALTER TABLE "byline_admin_role_admin_user" ADD CONSTRAINT "byline_admin_role_admin_user_admin_user_id_byline_admin_users_id_fk" FOREIGN KEY ("admin_user_id") REFERENCES "public"."byline_admin_users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
359
382
  ALTER TABLE "byline_admin_user_preferences" ADD CONSTRAINT "byline_admin_user_preferences_user_id_byline_admin_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."byline_admin_users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
@@ -389,11 +412,14 @@ ALTER TABLE "byline_store_relation" ADD CONSTRAINT "byline_store_relation_target
389
412
  ALTER TABLE "byline_singleton_documents" ADD CONSTRAINT "fk_singleton_documents_document" FOREIGN KEY ("collection_id","document_id") REFERENCES "public"."byline_documents"("collection_id","id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
390
413
  ALTER TABLE "byline_store_text" ADD CONSTRAINT "byline_store_text_document_version_id_byline_document_versions_id_fk" FOREIGN KEY ("document_version_id") REFERENCES "public"."byline_document_versions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
391
414
  ALTER TABLE "byline_store_text" ADD CONSTRAINT "byline_store_text_collection_id_byline_collections_id_fk" FOREIGN KEY ("collection_id") REFERENCES "public"."byline_collections"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
415
+ CREATE INDEX "idx_admin_login_sessions_user" ON "byline_admin_login_sessions" USING btree ("admin_user_id");--> statement-breakpoint
392
416
  CREATE INDEX "idx_byline_admin_permissions_role" ON "byline_admin_permissions" USING btree ("admin_role_id");--> statement-breakpoint
417
+ CREATE INDEX "idx_admin_refresh_tokens_sid" ON "byline_admin_refresh_tokens" USING btree ("sid");--> statement-breakpoint
393
418
  CREATE INDEX "idx_byline_admin_refresh_tokens_user" ON "byline_admin_refresh_tokens" USING btree ("admin_user_id");--> statement-breakpoint
394
419
  CREATE INDEX "idx_byline_admin_refresh_tokens_token_hash" ON "byline_admin_refresh_tokens" USING btree ("token_hash");--> statement-breakpoint
395
420
  CREATE INDEX "idx_byline_admin_role_admin_user_user" ON "byline_admin_role_admin_user" USING btree ("admin_user_id");--> statement-breakpoint
396
421
  CREATE INDEX "idx_byline_admin_roles_machine_name" ON "byline_admin_roles" USING btree ("machine_name");--> statement-breakpoint
422
+ CREATE INDEX "idx_admin_sign_in_rate_limits_expiry" ON "byline_admin_sign_in_rate_limits" USING btree ("expires_at");--> statement-breakpoint
397
423
  CREATE INDEX "idx_byline_admin_users_email" ON "byline_admin_users" USING btree ("email");--> statement-breakpoint
398
424
  CREATE INDEX "idx_audit_log_document_id" ON "byline_audit_log" USING btree ("document_id","id");--> statement-breakpoint
399
425
  CREATE INDEX "idx_audit_log_actor_id" ON "byline_audit_log" USING btree ("actor_id","id");--> statement-breakpoint
@@ -5,8 +5,8 @@
5
5
  {
6
6
  "idx": 0,
7
7
  "version": "7",
8
- "when": 1787797717481,
9
- "tag": "0000_quiet_johnny_blaze",
8
+ "when": 1788880329240,
9
+ "tag": "0000_lively_talisman",
10
10
  "breakpoints": true
11
11
  }
12
12
  ]