@elytracms/studio 0.0.14 → 0.0.16

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 (143) hide show
  1. package/convex/cms.ts +8 -31
  2. package/convex/content.ts +3 -44
  3. package/convex/delivery.ts +8 -42
  4. package/convex/functions.ts +17 -2
  5. package/convex/members.ts +2 -2
  6. package/convex/referenceMaintenance.ts +4 -73
  7. package/convex/references.ts +4 -2
  8. package/convex/schema.ts +17 -37
  9. package/convex/sync.ts +10 -42
  10. package/convex/triggers.ts +57 -0
  11. package/convex/webhooks.ts +3 -2
  12. package/dist/components/app-views/default-modules.d.ts +41 -0
  13. package/dist/components/app-views/default-modules.js +201 -0
  14. package/dist/components/app-views/default-modules.js.map +1 -0
  15. package/dist/components/app-views/module.d.ts +93 -0
  16. package/dist/components/app-views/module.js +75 -0
  17. package/dist/components/app-views/module.js.map +1 -0
  18. package/dist/components/app-views/sidebar.d.ts +95 -0
  19. package/dist/components/app-views/sidebar.js +153 -0
  20. package/dist/components/app-views/sidebar.js.map +1 -0
  21. package/dist/components/builder/builder-store.d.ts +92 -40
  22. package/dist/components/builder/builder-store.js +30 -126
  23. package/dist/components/builder/builder-store.js.map +1 -1
  24. package/dist/components/builder/canvas-breakpoints.d.ts +53 -0
  25. package/dist/components/builder/canvas-breakpoints.js +55 -0
  26. package/dist/components/builder/canvas-breakpoints.js.map +1 -0
  27. package/dist/components/builder/canvas.js +35 -61
  28. package/dist/components/builder/canvas.js.map +1 -1
  29. package/dist/components/builder/clipboard-actions.js +3 -3
  30. package/dist/components/builder/clipboard-actions.js.map +1 -1
  31. package/dist/components/builder/composition-field-editor.js +31 -40
  32. package/dist/components/builder/composition-field-editor.js.map +1 -1
  33. package/dist/components/builder/insert-context.js +1 -1
  34. package/dist/components/builder/insert-context.js.map +1 -1
  35. package/dist/components/builder/page-tree.js +3 -4
  36. package/dist/components/builder/page-tree.js.map +1 -1
  37. package/dist/components/builder/property-inspector.js +4 -64
  38. package/dist/components/builder/property-inspector.js.map +1 -1
  39. package/dist/components/cms/bulk-actions.d.ts +23 -5
  40. package/dist/components/cms/bulk-actions.js +7 -3
  41. package/dist/components/cms/bulk-actions.js.map +1 -1
  42. package/dist/components/cms/bulk-bar.d.ts +22 -6
  43. package/dist/components/cms/bulk-bar.js +46 -9
  44. package/dist/components/cms/bulk-bar.js.map +1 -1
  45. package/dist/components/cms/cell.d.ts +0 -4
  46. package/dist/components/cms/cell.js +1 -5
  47. package/dist/components/cms/cell.js.map +1 -1
  48. package/dist/components/cms/cms-index.d.ts +4 -2
  49. package/dist/components/cms/cms-index.js +6 -4
  50. package/dist/components/cms/cms-index.js.map +1 -1
  51. package/dist/components/cms/cms-store.d.ts +7 -6
  52. package/dist/components/cms/cms-store.js +55 -18
  53. package/dist/components/cms/cms-store.js.map +1 -1
  54. package/dist/components/cms/collection-table.js +8 -4
  55. package/dist/components/cms/collection-table.js.map +1 -1
  56. package/dist/components/cms/collections-sidebar.d.ts +10 -1
  57. package/dist/components/cms/collections-sidebar.js +13 -2
  58. package/dist/components/cms/collections-sidebar.js.map +1 -1
  59. package/dist/components/cms/detail-editor.js +2 -2
  60. package/dist/components/cms/detail-editor.js.map +1 -1
  61. package/dist/components/cms/doc-state.d.ts +0 -5
  62. package/dist/components/cms/doc-state.js +1 -7
  63. package/dist/components/cms/doc-state.js.map +1 -1
  64. package/dist/components/cms/editable-cell.js +1 -1
  65. package/dist/components/cms/editable-cell.js.map +1 -1
  66. package/dist/components/cms/field-control.d.ts +11 -2
  67. package/dist/components/cms/field-control.js +22 -1
  68. package/dist/components/cms/field-control.js.map +1 -1
  69. package/dist/components/cms/field-renderer.d.ts +71 -0
  70. package/dist/components/cms/field-renderer.js +67 -0
  71. package/dist/components/cms/field-renderer.js.map +1 -0
  72. package/dist/components/cms/filter-builder.js +6 -28
  73. package/dist/components/cms/filter-builder.js.map +1 -1
  74. package/dist/components/cms/scalar-value-editor.d.ts +35 -0
  75. package/dist/components/cms/scalar-value-editor.js +43 -0
  76. package/dist/components/cms/scalar-value-editor.js.map +1 -0
  77. package/dist/components/collab/collab-context.d.ts +6 -14
  78. package/dist/components/collab/collab-context.js +3 -12
  79. package/dist/components/collab/collab-context.js.map +1 -1
  80. package/dist/components/collab/collab-seed.d.ts +7 -19
  81. package/dist/components/collab/collab-seed.js +9 -31
  82. package/dist/components/collab/collab-seed.js.map +1 -1
  83. package/dist/components/components/registry-browser.js +6 -12
  84. package/dist/components/components/registry-browser.js.map +1 -1
  85. package/dist/components/guards.d.ts +0 -9
  86. package/dist/components/guards.js +0 -12
  87. package/dist/components/guards.js.map +1 -1
  88. package/dist/components/project-session.js +8 -3
  89. package/dist/components/project-session.js.map +1 -1
  90. package/dist/components/section-nav.d.ts +10 -10
  91. package/dist/components/section-nav.js +48 -25
  92. package/dist/components/section-nav.js.map +1 -1
  93. package/dist/components/section-state.d.ts +0 -2
  94. package/dist/components/section-state.js +0 -9
  95. package/dist/components/section-state.js.map +1 -1
  96. package/dist/components/section-surface.d.ts +3 -7
  97. package/dist/components/section-surface.js +6 -14
  98. package/dist/components/section-surface.js.map +1 -1
  99. package/dist/components/sections.d.ts +0 -1
  100. package/dist/components/sections.js +0 -10
  101. package/dist/components/sections.js.map +1 -1
  102. package/dist/components/studio-project-screen.d.ts +3 -4
  103. package/dist/components/studio-project-screen.js +41 -160
  104. package/dist/components/studio-project-screen.js.map +1 -1
  105. package/dist/components/studio-shell.d.ts +0 -4
  106. package/dist/components/studio-shell.js +14 -14
  107. package/dist/components/studio-shell.js.map +1 -1
  108. package/dist/lib/data/collection-content.d.ts +19 -1
  109. package/dist/lib/data/collection-content.js +16 -0
  110. package/dist/lib/data/collection-content.js.map +1 -1
  111. package/dist/lib/operations/graph-commands.d.ts +2 -6
  112. package/dist/lib/operations/graph-commands.js +1 -2
  113. package/dist/lib/operations/graph-commands.js.map +1 -1
  114. package/dist/lib/operations/index.d.ts +0 -2
  115. package/dist/lib/operations/index.js +0 -2
  116. package/dist/lib/operations/index.js.map +1 -1
  117. package/dist/lib/operations/session-hydration.d.ts +14 -24
  118. package/dist/lib/operations/session-hydration.js +15 -34
  119. package/dist/lib/operations/session-hydration.js.map +1 -1
  120. package/dist/lib/persistence/convex-adapter.d.ts +1 -3
  121. package/dist/lib/persistence/convex-adapter.js +1 -154
  122. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  123. package/dist/lib/persona/labels.js +0 -2
  124. package/dist/lib/persona/labels.js.map +1 -1
  125. package/dist/lib/references/reference-model.d.ts +0 -16
  126. package/dist/lib/references/reference-model.js +2 -54
  127. package/dist/lib/references/reference-model.js.map +1 -1
  128. package/dist/lib/references/references-store.js +3 -4
  129. package/dist/lib/references/references-store.js.map +1 -1
  130. package/dist/lib/registry/index.d.ts +0 -16
  131. package/dist/lib/registry/index.js +0 -15
  132. package/dist/lib/registry/index.js.map +1 -1
  133. package/dist/lib/webhooks/tags.js +2 -16
  134. package/dist/lib/webhooks/tags.js.map +1 -1
  135. package/dist/lib/workspace/index.d.ts +27 -1
  136. package/dist/lib/workspace/index.js +34 -0
  137. package/dist/lib/workspace/index.js.map +1 -1
  138. package/dist/router/match.d.ts +22 -34
  139. package/dist/router/match.js +22 -40
  140. package/dist/router/match.js.map +1 -1
  141. package/package.json +3 -2
  142. package/convex/graphs.ts +0 -55
  143. package/convex/publishing.ts +0 -76
package/convex/cms.ts CHANGED
@@ -1,9 +1,6 @@
1
- import type { CmsDocument } from '@elytracms/core/cms-core'
2
1
  import { v } from 'convex/values'
3
2
  import { mutation, query } from './functions'
4
3
  import { requireRole } from './guard'
5
- import { maintainDocumentReferences, removeDocumentReferences } from './referenceMaintenance'
6
- import { scheduleRevalidate } from './revalidate'
7
4
 
8
5
  /**
9
6
  * CMS document persistence (EC-043). The backend stores **documents only**
@@ -30,22 +27,11 @@ export const upsertDocument = mutation({
30
27
  .first()
31
28
  if (existing) {
32
29
  // EC-153 parity: updates preserve `createdAt`, exactly like in-memory.
30
+ // EC-286: the write-chokepoint trigger reindexes references in-transaction.
33
31
  await ctx.db.patch(existing._id, { ...args, createdAt: existing.createdAt })
34
- // EC-277: maintain the reference index in-transaction (AD-6). The live pin
35
- // (`publishedVersion`) is preserved across an upsert, so a published doc keeps
36
- // its published index rows in sync with its edited draft.
37
- await maintainDocumentReferences(
38
- ctx,
39
- args.collection,
40
- args.docId,
41
- args.document as CmsDocument,
42
- existing.publishedVersion !== undefined,
43
- )
44
32
  return ctx.db.get(existing._id)
45
33
  }
46
34
  const id = await ctx.db.insert('cmsDocuments', args)
47
- // EC-277: a freshly inserted document is draft-only (no live pin yet).
48
- await maintainDocumentReferences(ctx, args.collection, args.docId, args.document as CmsDocument, false)
49
35
  return ctx.db.get(id)
50
36
  },
51
37
  })
@@ -91,8 +77,8 @@ export const deleteDocument = mutation({
91
77
  .withIndex('by_doc', (q) => q.eq('collection', args.collection).eq('docId', args.docId))
92
78
  .collect()
93
79
  for (const row of versions) await ctx.db.delete(row._id)
94
- // EC-277: drop the deleted document's reference index rows in-transaction.
95
- await removeDocumentReferences(ctx, args.collection, args.docId)
80
+ // EC-286: the write-chokepoint trigger drops this document's index rows (and
81
+ // revalidates if it was published) when the `cmsDocuments` delete commits.
96
82
  },
97
83
  })
98
84
 
@@ -170,16 +156,10 @@ export const publishDocumentVersion = mutation({
170
156
  .withIndex('by_doc', (q) => q.eq('collection', args.collection).eq('docId', args.docId))
171
157
  .first()
172
158
  if (!doc) return null
159
+ // EC-286: setting the live pin (`publishedVersion`) is a change the
160
+ // write-chokepoint trigger sees — it reindexes the published reference rows
161
+ // and fires host revalidation (EC-146 instant publish). No manual calls here.
173
162
  await ctx.db.patch(doc._id, { publishedVersion: args.version, updatedAt: args.updatedAt })
174
- // EC-277: the doc now has a live pin — add its published index rows. Published
175
- // entries are extracted from the live draft content (matching the EC-173 guard
176
- // and `reindexDocumentReferences`), not the pinned snapshot.
177
- await maintainDocumentReferences(ctx, args.collection, args.docId, doc.document as CmsDocument, true)
178
- // EC-146 instant publish: a CONTENT publish must invalidate the host cache just
179
- // like a graph publish — without this, instant-publish silently never fires for
180
- // a content-only site (the server-side webhook path previously covered only
181
- // `publishing:publish`). Fire-and-forget; unconfigured deployments no-op.
182
- await scheduleRevalidate(ctx, 'documents.publish')
183
163
  return ctx.db.get(doc._id)
184
164
  },
185
165
  })
@@ -194,12 +174,9 @@ export const unpublishDocument = mutation({
194
174
  .first()
195
175
  if (!doc) return null
196
176
  // Convex patch removes a field set to `undefined` — clears the live pin.
177
+ // EC-286: clearing the pin is a change the write-chokepoint trigger sees — it
178
+ // drops the published index rows and revalidates the host. No manual calls.
197
179
  await ctx.db.patch(doc._id, { publishedVersion: undefined, updatedAt: args.updatedAt })
198
- // EC-277: the live pin is cleared — drop this doc's published index rows.
199
- await maintainDocumentReferences(ctx, args.collection, args.docId, doc.document as CmsDocument, false)
200
- // EC-146 instant publish: unpublishing content must drop the host's cached
201
- // pages too (the page that baked this doc now serves stale published content).
202
- await scheduleRevalidate(ctx, 'documents.unpublish')
203
180
  return ctx.db.get(doc._id)
204
181
  },
205
182
  })
package/convex/content.ts CHANGED
@@ -17,8 +17,6 @@ import type {
17
17
  Perspective,
18
18
  } from '@elytracms/core/content'
19
19
  import type { AssetRecord } from '@elytracms/core/persistence'
20
- import { parseProjectGraph } from '@elytracms/core/project-graph'
21
- import type { ProjectGraph } from '@elytracms/core/project-graph'
22
20
  import type { QueryCtx } from './functions'
23
21
 
24
22
  /**
@@ -68,10 +66,7 @@ const DEFAULT_LOCALES: LocaleConfig = { default: 'en', locales: ['en'] }
68
66
  * produced: CMS collections are config-owned (EC-183, AD-11) and supplied by the
69
67
  * caller (already validated), so there is no stored schema blob to reject.
70
68
  */
71
- export type ContentKitIssueCode =
72
- | 'invalid-stored-schema'
73
- | 'invalid-stored-document'
74
- | 'invalid-stored-graph'
69
+ export type ContentKitIssueCode = 'invalid-stored-schema' | 'invalid-stored-document'
75
70
 
76
71
  export interface ContentKitIssue {
77
72
  code: ContentKitIssueCode
@@ -221,44 +216,8 @@ export async function createContent(
221
216
  })
222
217
  }
223
218
 
224
- // The page graph backs `resolvePage`'s page capability; it is only loaded
225
- // when routes were provided (without routes, nothing can resolve to a page).
226
- // Draft sees the latest revision; published sees the revision pinned to the
227
- // publishing environment (EC-021 at the graph level).
228
- let graph: ProjectGraph | null = null
229
- if (routes.length > 0) {
230
- let serialized: string | undefined
231
- if (perspective === 'draft') {
232
- const latest = await ctx.db
233
- .query('graphRevisions')
234
- .withIndex('by_revision')
235
- .order('desc')
236
- .first()
237
- serialized = latest?.serializedGraph
238
- } else {
239
- const state = await ctx.db.query('publishingState').first()
240
- const pinnedRevision = state?.status === 'published' ? state.publishedRevision : undefined
241
- if (pinnedRevision !== undefined) {
242
- const pinned = await ctx.db
243
- .query('graphRevisions')
244
- .withIndex('by_revision', (q) => q.eq('revision', pinnedRevision))
245
- .first()
246
- serialized = pinned?.serializedGraph
247
- }
248
- }
249
- if (serialized !== undefined) {
250
- const parsed = parseProjectGraph(serialized)
251
- if (parsed.ok) {
252
- graph = parsed.graph
253
- } else {
254
- loadIssues.push({
255
- code: 'invalid-stored-graph',
256
- message: `Stored graph revision for "${projectId}" (${perspective}) could not be parsed; pages are unavailable.`,
257
- meta: { projectId, perspective },
258
- })
259
- }
260
- }
261
- }
219
+ // EC-287: there is no project graph. Pages are `page`-collection documents,
220
+ // resolved by `resolvePage` from the document lookup + routes — no graph load.
262
221
 
263
222
  // --- Index into the synchronous lookup the pure accessors read through ---
264
223
 
@@ -29,26 +29,19 @@ import { requireUserId } from './guard'
29
29
  * function — the EC-224 *pinned version snapshot* (decoupled from the live,
30
30
  * continuously-edited draft). Publish-ness is the version pointer alone;
31
31
  * drafts and unpinned rows are never returned;
32
- * - the graph returned is ONLY the revision pinned by `publishingState` (the
33
- * deployment's single environment) — the latest working revision is never exposed;
34
32
  * - nothing else draft-only exists in the payload (asset metadata has no draft
35
33
  * variant). CMS collection schema AND routes/redirects are config-owned
36
34
  * (EC-183/EC-207, AD-11) — the host reads them from its own config, not this
37
- * snapshot.
35
+ * snapshot. EC-287: there is no project graph — layouts/structure are code.
38
36
  *
39
- * `draftSnapshot` is the draft-perspective equivalent (latest graph revision,
40
- * all stored document rows including drafts) and is gated by the deployment
41
- * env var `DRAFT_PREVIEW_TOKEN` — set via `npx convex env set`. A missing or
37
+ * `draftSnapshot` is the draft-perspective equivalent (all stored document rows
38
+ * including drafts) and is gated by the deployment env var `DRAFT_PREVIEW_TOKEN`
39
+ * — set via `npx convex env set`. A missing or
42
40
  * mismatching token throws `unauthorized`; an unset env var closes the
43
41
  * endpoint entirely. The host server passes the token from its own env when
44
42
  * Next draft mode is enabled (itself gated by the host's preview token).
45
43
  */
46
44
 
47
- interface GraphRevisionPayload {
48
- revision: number
49
- serializedGraph: string
50
- }
51
-
52
45
  interface DeliverySnapshot {
53
46
  projectId: string
54
47
  perspective: 'published' | 'draft'
@@ -69,12 +62,6 @@ interface DeliverySnapshot {
69
62
  documents: unknown[]
70
63
  /** Asset metadata in the delivery `AssetRecord` shape (with serve URLs). */
71
64
  assets: unknown[]
72
- /**
73
- * Graph revisions per perspective. `snapshot` NEVER fills `draft` — only
74
- * `draftSnapshot` does. `published` is the `publishingState`-pinned
75
- * revision, or `null` when never published.
76
- */
77
- graphs: { draft: GraphRevisionPayload | null; published: GraphRevisionPayload | null }
78
65
  }
79
66
 
80
67
  /** One document per persisted source — must match the studio's sources-store. */
@@ -131,22 +118,6 @@ function dataSourcesFromSystemCollection(documents: unknown[]): unknown[] {
131
118
  return sources
132
119
  }
133
120
 
134
- async function loadPinnedGraph(ctx: QueryCtx): Promise<GraphRevisionPayload | null> {
135
- const state = await ctx.db.query('publishingState').first()
136
- const pinned = state?.status === 'published' ? state.publishedRevision : undefined
137
- if (pinned === undefined) return null
138
- const row = await ctx.db
139
- .query('graphRevisions')
140
- .withIndex('by_revision', (q) => q.eq('revision', pinned))
141
- .first()
142
- return row ? { revision: row.revision, serializedGraph: row.serializedGraph } : null
143
- }
144
-
145
- async function loadLatestGraph(ctx: QueryCtx): Promise<GraphRevisionPayload | null> {
146
- const row = await ctx.db.query('graphRevisions').withIndex('by_revision').order('desc').first()
147
- return row ? { revision: row.revision, serializedGraph: row.serializedGraph } : null
148
- }
149
-
150
121
  export async function loadSnapshot(
151
122
  ctx: QueryCtx,
152
123
  args: { projectId: string },
@@ -214,10 +185,6 @@ export async function loadSnapshot(
214
185
  })
215
186
  }
216
187
 
217
- const published = await loadPinnedGraph(ctx)
218
- // The latest working revision is loaded ONLY for the token-gated draft path.
219
- const draft = perspective === 'draft' ? await loadLatestGraph(ctx) : null
220
-
221
188
  // LIVE-mode data sources: the builder's persisted source records, projected
222
189
  // to the delivery envelope (config only — see the helper's exposure rules).
223
190
  // Sources are project *config*, not perspective-gated content, so they are
@@ -234,7 +201,6 @@ export async function loadSnapshot(
234
201
  sources,
235
202
  documents,
236
203
  assets,
237
- graphs: { draft, published },
238
204
  }
239
205
  }
240
206
 
@@ -249,8 +215,8 @@ export const snapshot = query({
249
215
 
250
216
  /**
251
217
  * Draft-perspective snapshot for token-holding preview hosts: all stored
252
- * document rows (drafts included) plus the latest working graph revision.
253
- * Gated by the `DRAFT_PREVIEW_TOKEN` deployment env var.
218
+ * document rows (drafts included). Gated by the `DRAFT_PREVIEW_TOKEN` deployment
219
+ * env var.
254
220
  */
255
221
  export const draftSnapshot = query({
256
222
  args: { projectId: v.string(), token: v.string() },
@@ -270,8 +236,8 @@ export const draftSnapshot = query({
270
236
  * EC-268: instant-publish configuration health for the studio UI. Returns ONLY
271
237
  * whether each delivery env var is set — never the values — so the studio can warn
272
238
  * when a publish would silently no-op. The pipeline gates on these: a publish only
273
- * schedules the webhook when `CANVAS_PROJECT_SCOPE` is set (`publishing.ts`
274
- * `scheduleRevalidate`), and the webhook only fires when
239
+ * schedules the webhook when `CANVAS_PROJECT_SCOPE` is set (the write-chokepoint
240
+ * trigger's `scheduleRevalidate`, `triggers.ts`), and the webhook only fires when
275
241
  * `CANVAS_REVALIDATE_ENDPOINT` + `CANVAS_REVALIDATE_SECRET` are set
276
242
  * (`webhooks.ts` `deliver`). A future `elytra doctor` (EC-259) can read the same
277
243
  * query. Gated to authenticated users — config state is not public.
@@ -1,3 +1,4 @@
1
+ import { customCtx, customMutation } from 'convex-helpers/server/customFunctions'
1
2
  import {
2
3
  httpActionGeneric,
3
4
  internalMutationGeneric,
@@ -13,6 +14,7 @@ import type {
13
14
  QueryBuilder,
14
15
  } from 'convex/server'
15
16
  import schema from './schema'
17
+ import { triggers } from './triggers'
16
18
 
17
19
  /**
18
20
  * Typed function builders bound to our schema's data model. This is what Convex
@@ -25,9 +27,22 @@ export type QueryCtx = GenericQueryCtx<DataModel>
25
27
  export type MutationCtx = GenericMutationCtx<DataModel>
26
28
 
27
29
  export const query = queryGeneric as QueryBuilder<DataModel, 'public'>
28
- export const mutation = mutationGeneric as MutationBuilder<DataModel, 'public'>
30
+ /**
31
+ * EC-286: every mutation runs through the write-chokepoint triggers (`triggers.ts`)
32
+ * via `customCtx(triggers.wrapDB)`, so reference-index maintenance and host
33
+ * revalidation derive by construction on any `cmsDocuments` write — no mutation
34
+ * calls a derivation helper by hand. (EC-287 retired the graph publish path, so
35
+ * `cmsDocuments` is the single trigger.)
36
+ */
37
+ export const mutation = customMutation(
38
+ mutationGeneric as MutationBuilder<DataModel, 'public'>,
39
+ customCtx(triggers.wrapDB),
40
+ )
29
41
  /** CLI-/scheduler-only functions (e.g. seeding) — not callable by clients (EC-150). */
30
- export const internalMutation = internalMutationGeneric as MutationBuilder<DataModel, 'internal'>
42
+ export const internalMutation = customMutation(
43
+ internalMutationGeneric as MutationBuilder<DataModel, 'internal'>,
44
+ customCtx(triggers.wrapDB),
45
+ )
31
46
  /** Server-only reads (e.g. the EC-157 HTTP sync endpoints) — not callable by clients. */
32
47
  export const internalQuery = internalQueryGeneric as QueryBuilder<DataModel, 'internal'>
33
48
  /** HTTP action builder (EC-252) — used by the package's `http.ts`/`sync.ts`. Not
package/convex/members.ts CHANGED
@@ -39,8 +39,8 @@ export const get = query({
39
39
  * The authenticated caller's membership on this deployment (studio bootstrap).
40
40
  *
41
41
  * Soft-guarded (EC-279): this is the studio's pre-sign-in connectivity probe
42
- * (`ConvexProjectRepository.status`) and role-badge read, fired before a session
43
- * exists. Returning `null` for the unauthenticated state — instead of throwing
42
+ * and role-badge read, fired before a session exists. Returning `null` for the
43
+ * unauthenticated state — instead of throwing
44
44
  * `requireUserId` — keeps the normal boot quiet (a thrown handler is logged by
45
45
  * Convex as a scary `Uncaught Error: unauthorized` even though the client
46
46
  * catches it). A non-throwing probe still proves the backend answered, so
@@ -1,8 +1,7 @@
1
1
  import { documentKey } from '@elytracms/core/cms-core'
2
2
  import type { CmsDocument } from '@elytracms/core/cms-core'
3
- import { extractReferences, extractSelfTypedGraphReferences } from '@elytracms/core/persistence'
3
+ import { extractReferences } from '@elytracms/core/persistence'
4
4
  import type { OutboundReference } from '@elytracms/core/persistence'
5
- import { parseProjectGraph } from '@elytracms/core/project-graph'
6
5
  import type { MutationCtx } from './functions'
7
6
 
8
7
  /**
@@ -22,15 +21,12 @@ import type { MutationCtx } from './functions'
22
21
  * which is exactly what lets the identical walk run here, in the Convex isolate,
23
22
  * that runs in the studio and the in-memory adapter.
24
23
  *
25
- * Mirrors `reindexDocumentReferences` / `reindexGraph*References`
26
- * (`@elytracms/core/persistence`), but writes through `ctx.db` rather than the adapter.
24
+ * Mirrors `reindexDocumentReferences` (`@elytracms/core/persistence`), but writes
25
+ * through `ctx.db` rather than the adapter. EC-287: the project graph and its
26
+ * publish path are retired, so document writes are the only index source.
27
27
  */
28
28
 
29
29
  type Variant = 'draft' | 'published'
30
- type GraphSourceType = 'page' | 'layout'
31
-
32
- /** The graph (EC-187) is layouts-only; pages live in the `page` collection's body. */
33
- const GRAPH_SOURCE_TYPES: readonly GraphSourceType[] = ['page', 'layout']
34
30
 
35
31
  interface ReferenceRow {
36
32
  path: string
@@ -54,21 +50,6 @@ async function deleteDocumentSource(ctx: MutationCtx, variant: Variant, sourceId
54
50
  for (const row of existing) await ctx.db.delete(row._id)
55
51
  }
56
52
 
57
- /** Delete every index row held by the given source types in one variant (graph recompute). */
58
- async function deleteSourceTypes(
59
- ctx: MutationCtx,
60
- variant: Variant,
61
- sourceTypes: readonly GraphSourceType[],
62
- ): Promise<void> {
63
- for (const sourceType of sourceTypes) {
64
- const existing = await ctx.db
65
- .query('referenceEntries')
66
- .withIndex('by_source', (q) => q.eq('variant', variant).eq('sourceType', sourceType))
67
- .collect()
68
- for (const row of existing) await ctx.db.delete(row._id)
69
- }
70
- }
71
-
72
53
  /**
73
54
  * Recompute a document's index rows in-transaction (replace-all-for-source). The
74
55
  * editable content IS the draft, so a document always (re)indexes its draft rows;
@@ -111,53 +92,3 @@ export async function removeDocumentReferences(
111
92
  await deleteDocumentSource(ctx, 'draft', sourceId)
112
93
  await deleteDocumentSource(ctx, 'published', sourceId)
113
94
  }
114
-
115
- /** Recompute the draft graph rows from a just-saved revision's serialized graph. */
116
- export async function maintainGraphDraftReferences(
117
- ctx: MutationCtx,
118
- serializedGraph: string,
119
- ): Promise<void> {
120
- const parsed = parseProjectGraph(serializedGraph)
121
- await deleteSourceTypes(ctx, 'draft', GRAPH_SOURCE_TYPES)
122
- if (!parsed.ok) return // a malformed stored graph contributes no entries (it can't render either)
123
- for (const entry of extractSelfTypedGraphReferences(parsed.graph)) {
124
- await ctx.db.insert('referenceEntries', {
125
- variant: 'draft',
126
- sourceType: entry.source.type as GraphSourceType,
127
- sourceId: entry.source.id,
128
- path: entry.path,
129
- targetType: entry.target.type,
130
- targetId: entry.target.id,
131
- kind: entry.kind,
132
- })
133
- }
134
- }
135
-
136
- /**
137
- * Recompute the published graph rows from the revision currently pinned live by the
138
- * publishing state (single-environment, EC-179). Unpublishing drops them all.
139
- */
140
- export async function maintainGraphPublishedReferences(ctx: MutationCtx): Promise<void> {
141
- await deleteSourceTypes(ctx, 'published', GRAPH_SOURCE_TYPES)
142
- const state = await ctx.db.query('publishingState').first()
143
- if (!state || state.status !== 'published' || state.publishedRevision === undefined) return
144
- const publishedRevision = state.publishedRevision
145
- const revision = await ctx.db
146
- .query('graphRevisions')
147
- .withIndex('by_revision', (q) => q.eq('revision', publishedRevision))
148
- .first()
149
- if (!revision) return
150
- const parsed = parseProjectGraph(revision.serializedGraph)
151
- if (!parsed.ok) return
152
- for (const entry of extractSelfTypedGraphReferences(parsed.graph)) {
153
- await ctx.db.insert('referenceEntries', {
154
- variant: 'published',
155
- sourceType: entry.source.type as GraphSourceType,
156
- sourceId: entry.source.id,
157
- path: entry.path,
158
- targetType: entry.target.type,
159
- targetId: entry.target.id,
160
- kind: entry.kind,
161
- })
162
- }
163
- }
@@ -10,7 +10,9 @@ import type { MutationCtx } from './functions'
10
10
  */
11
11
 
12
12
  const variantValidator = v.union(v.literal('draft'), v.literal('published'))
13
- const sourceTypeValidator = v.union(v.literal('document'), v.literal('page'), v.literal('layout'))
13
+ // EC-287: structure is code — the only reference source is a CMS document. The
14
+ // retired graph's `page`/`layout` sources no longer exist.
15
+ const sourceTypeValidator = v.literal('document')
14
16
  const targetTypeValidator = v.union(
15
17
  v.literal('document'),
16
18
  v.literal('component'),
@@ -38,7 +40,7 @@ const entryValidator = v.object({
38
40
  async function deleteForSource(
39
41
  ctx: MutationCtx,
40
42
  variant: 'draft' | 'published',
41
- sourceType: 'document' | 'page' | 'layout',
43
+ sourceType: 'document',
42
44
  sourceId?: string,
43
45
  ): Promise<void> {
44
46
  const existing = await ctx.db
package/convex/schema.ts CHANGED
@@ -24,9 +24,13 @@ import { authTables } from '@convex-dev/auth/server'
24
24
  * the repo registry (EC-181), project identity/settings/locales/routes and CMS
25
25
  * **collection schemas** all live in `elytra.config.ts` (EC-182/EC-183). So
26
26
  * there is no `projects`, `componentManifests`, or `cmsSchemas` table — the
27
- * remaining tables hold **data only**: CMS documents, assets, graph revisions
28
- * (the page composition is a field value — data, per AD-12), publishing state,
29
- * the derived reference index, deployment membership, CLI tokens, and auth.
27
+ * remaining tables hold **data only**: CMS documents, assets, the derived
28
+ * reference index, deployment membership, CLI tokens, and auth.
29
+ *
30
+ * EC-287: the project graph + its publish path are RETIRED (structure is code; a
31
+ * page renders its `body` composition directly, published per document via
32
+ * `cmsDocuments.publishedVersion`). The `graphRevisions` + `publishingState` tables
33
+ * are gone, and a reference source is only ever a `document`.
30
34
  *
31
35
  * This schema is provider configuration only — the Studio and tests talk to the
32
36
  * provider-neutral `PersistenceAdapter`, never to Convex directly (brief §4.10).
@@ -74,16 +78,6 @@ export default defineSchema({
74
78
  .index('by_token', ['tokenId'])
75
79
  .index('by_hash', ['tokenHash']),
76
80
 
77
- /** Project graph revision records — append-only (EC-041). */
78
- graphRevisions: defineTable({
79
- revision: v.number(),
80
- serializedGraph: v.string(),
81
- schemaVersion: v.number(),
82
- label: v.optional(v.string()),
83
- message: v.optional(v.string()),
84
- createdAt: v.string(),
85
- }).index('by_revision', ['revision']),
86
-
87
81
  /**
88
82
  * CMS documents (EC-043). The stored `document` is the continuous working
89
83
  * **draft**; `publishedVersion` pins which `documentVersions` row is live
@@ -102,10 +96,10 @@ export default defineSchema({
102
96
  .index('by_doc', ['collection', 'docId']),
103
97
 
104
98
  /**
105
- * CMS document version snapshots — append-only (EC-224), the document-scoped
106
- * analogue of `graphRevisions`. One row per recorded version (created on
107
- * publish or a pre-restore snapshot, never on a plain draft edit), keyed per
108
- * document via `by_doc`. `cmsDocuments.publishedVersion` points at the live one.
99
+ * CMS document version snapshots — append-only (EC-224). One row per recorded
100
+ * version (created on publish or a pre-restore snapshot, never on a plain draft
101
+ * edit), keyed per document via `by_doc`. `cmsDocuments.publishedVersion` points
102
+ * at the live one.
109
103
  */
110
104
  documentVersions: defineTable({
111
105
  collection: v.string(),
@@ -144,7 +138,8 @@ export default defineSchema({
144
138
  */
145
139
  referenceEntries: defineTable({
146
140
  variant: v.union(v.literal('draft'), v.literal('published')),
147
- sourceType: v.union(v.literal('document'), v.literal('page'), v.literal('layout')),
141
+ // EC-287: structure is code — the only reference source is a CMS document.
142
+ sourceType: v.literal('document'),
148
143
  sourceId: v.string(),
149
144
  /** Exact field/node path inside the source, e.g. `values/author`. */
150
145
  path: v.string(),
@@ -153,33 +148,18 @@ export default defineSchema({
153
148
  /** Which reference-bearing structure produced the entry. */
154
149
  kind: v.string(),
155
150
  })
156
- // `by_source` also serves source-type prefix scans (the graph recompute path).
157
151
  .index('by_source', ['variant', 'sourceType', 'sourceId'])
158
152
  .index('by_target', ['targetType', 'targetId', 'variant']),
159
153
 
160
154
  /**
161
155
  * Reference-index build marker (EC-269). Single-row (a deployment IS one
162
- * project, like `publishingState`): present once a backfill has fully rebuilt
163
- * the index. Its presence lets the studio skip the (clear + rebuild) backfill
164
- * on the read path — maintained-on-write keeps the index current thereafter —
165
- * so opening the References panel issues zero mutations.
156
+ * project): present once a backfill has fully rebuilt the index. Its presence
157
+ * lets the studio skip the (clear + rebuild) backfill on the read path —
158
+ * maintained-on-write keeps the index current thereafter — so opening the
159
+ * References panel issues zero mutations.
166
160
  */
167
161
  referenceIndexMarker: defineTable({
168
162
  /** Entry count produced by the completing backfill (diagnostic only). */
169
163
  entryCount: v.number(),
170
164
  }),
171
-
172
- /**
173
- * Publishing state (EC-045). Single-environment (EC-179, vision AD-8): a
174
- * deployment IS one environment, so this is a single-row table — no
175
- * `environment`, no index.
176
- */
177
- publishingState: defineTable({
178
- status: v.union(v.literal('draft'), v.literal('published')),
179
- publishedRevisionId: v.optional(v.string()),
180
- publishedRevision: v.optional(v.number()),
181
- publishedAt: v.optional(v.string()),
182
- createdAt: v.string(),
183
- updatedAt: v.string(),
184
- }),
185
165
  })
package/convex/sync.ts CHANGED
@@ -5,7 +5,6 @@ import { verifyCliToken } from '@elytracms/core/operations'
5
5
  import type { VerifiedCliToken } from '@elytracms/core/operations'
6
6
  import type { CliTokenRepository, PersistenceAdapter } from '@elytracms/core/persistence'
7
7
  import { httpAction, internalQuery } from './functions'
8
- import type { QueryCtx } from './functions'
9
8
  import type { ProjectRole } from './guard'
10
9
 
11
10
  /**
@@ -15,9 +14,9 @@ import type { ProjectRole } from './guard'
15
14
  * the real builder instead of a `fetchImpl` double:
16
15
  *
17
16
  * - `GET /api/projects/:projectId/registry-sync/snapshot?mode=&environment=` — the
18
- * typegen/check snapshot: locale config and the graph revision for the requested
19
- * mode. Collection schemas and routes/redirects are config-owned (EC-183/EC-207,
20
- * AD-11) — the CLI reads them from `elytra.config.ts`, not this snapshot.
17
+ * typegen/check snapshot: locale config only. Collection schemas, routes/redirects,
18
+ * AND the project graph are config-owned (EC-183/EC-207/EC-287, AD-11) — the CLI
19
+ * reads them from `elytra.config.ts`, not this snapshot.
21
20
  *
22
21
  * ## Package-shipped, deployment-agnostic (EC-223/EC-252)
23
22
  *
@@ -248,11 +247,6 @@ export const registrySync = httpAction(async (ctx, request) => {
248
247
 
249
248
  type SnapshotMode = 'published' | 'draft'
250
249
 
251
- interface GraphRevisionPayload {
252
- revision: number
253
- serializedGraph: string
254
- }
255
-
256
250
  interface BuilderSnapshot {
257
251
  projectId: string
258
252
  mode: SnapshotMode
@@ -260,36 +254,14 @@ interface BuilderSnapshot {
260
254
  project: null
261
255
  localeConfig: { default: string; locales: string[] }
262
256
  collections: never[]
263
- graph: GraphRevisionPayload | null
264
- }
265
-
266
- /** The publishing-state-pinned revision (single-environment; mirrors `delivery.ts`). */
267
- async function loadPinnedGraph(ctx: QueryCtx): Promise<GraphRevisionPayload | null> {
268
- const state = await ctx.db.query('publishingState').first()
269
- const pinned = state?.status === 'published' ? state.publishedRevision : undefined
270
- if (pinned === undefined) return null
271
- const row = await ctx.db
272
- .query('graphRevisions')
273
- .withIndex('by_revision', (q) => q.eq('revision', pinned))
274
- .first()
275
- return row ? { revision: row.revision, serializedGraph: row.serializedGraph } : null
276
- }
277
-
278
- async function loadLatestGraph(ctx: QueryCtx): Promise<GraphRevisionPayload | null> {
279
- const row = await ctx.db
280
- .query('graphRevisions')
281
- .withIndex('by_revision')
282
- .order('desc')
283
- .first()
284
- return row ? { revision: row.revision, serializedGraph: row.serializedGraph } : null
285
257
  }
286
258
 
287
259
  /**
288
260
  * Everything `builder typegen`/`builder check` need from live builder state, in the
289
261
  * `BuilderSnapshot` wire shape of `@elytracms/core/export-sync`'s `createHttpBuilderSource`.
290
- * Unlike `delivery:snapshot` this is token-gated and serves exactly one graph per
291
- * request: published = the pinned revision for `environment`, draft = latest.
292
- * Component manifests are code-owned, so the snapshot no longer carries them.
262
+ * EC-287: the project graph is retired (structure is code), so the snapshot no
263
+ * longer carries a graph — like the CMS schema and routes, it is config-owned and
264
+ * read from `elytra.config.ts` in the consuming repo.
293
265
  */
294
266
  export const exportSnapshot = internalQuery({
295
267
  args: {
@@ -299,16 +271,13 @@ export const exportSnapshot = internalQuery({
299
271
  },
300
272
  handler: async (ctx, { projectId, mode, environment }): Promise<BuilderSnapshot> => {
301
273
  // `environment` is a vestigial wire-compat field on the CLI snapshot contract
302
- // (EC-179 §2): a deployment IS one environment, so the pinned read is single-row.
303
- const graph = mode === 'published' ? await loadPinnedGraph(ctx) : await loadLatestGraph(ctx)
304
-
274
+ // (EC-179 §2): a deployment IS one environment.
305
275
  // AD-11 (EC-182/EC-183/EC-207): project identity, locale config, CMS
306
276
  // collection schemas, AND routes/redirects are all config-owned —
307
277
  // `builder typegen`/`check` run in the consuming repo and read them from
308
- // `elytra.config.ts`, not the wire. The DB carries only data (documents,
309
- // assets, graph). `project`/`localeConfig`/`collections` stay in the wire
310
- // shape as config-sourced placeholders until the CLI reads config directly
311
- // (C1) — the snapshot no longer carries a schema or a route table to return.
278
+ // `elytra.config.ts`, not the wire. `project`/`localeConfig`/`collections`
279
+ // stay in the wire shape as config-sourced placeholders until the CLI reads
280
+ // config directly (C1).
312
281
  return {
313
282
  projectId,
314
283
  mode,
@@ -316,7 +285,6 @@ export const exportSnapshot = internalQuery({
316
285
  project: null,
317
286
  localeConfig: { default: 'en', locales: ['en'] },
318
287
  collections: [],
319
- graph,
320
288
  }
321
289
  },
322
290
  })