@elytracms/studio 0.0.14 → 0.0.15
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.
- package/convex/cms.ts +8 -31
- package/convex/content.ts +3 -44
- package/convex/delivery.ts +8 -42
- package/convex/functions.ts +17 -2
- package/convex/members.ts +2 -2
- package/convex/referenceMaintenance.ts +4 -73
- package/convex/references.ts +4 -2
- package/convex/schema.ts +17 -37
- package/convex/sync.ts +10 -42
- package/convex/triggers.ts +57 -0
- package/convex/webhooks.ts +3 -2
- package/dist/components/builder/builder-store.d.ts +85 -39
- package/dist/components/builder/builder-store.js +29 -125
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas.js +23 -55
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/clipboard-actions.js +3 -3
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor.js +13 -23
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/insert-context.js +1 -1
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/page-tree.js +3 -4
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/property-inspector.js +4 -64
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/cms/cell.d.ts +0 -4
- package/dist/components/cms/cell.js +1 -5
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +1 -1
- package/dist/components/cms/cms-store.js +4 -9
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +1 -1
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.js +2 -2
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/doc-state.d.ts +0 -5
- package/dist/components/cms/doc-state.js +1 -7
- package/dist/components/cms/doc-state.js.map +1 -1
- package/dist/components/cms/editable-cell.js +1 -1
- package/dist/components/cms/editable-cell.js.map +1 -1
- package/dist/components/collab/collab-context.d.ts +6 -14
- package/dist/components/collab/collab-context.js +3 -12
- package/dist/components/collab/collab-context.js.map +1 -1
- package/dist/components/collab/collab-seed.d.ts +7 -19
- package/dist/components/collab/collab-seed.js +9 -31
- package/dist/components/collab/collab-seed.js.map +1 -1
- package/dist/components/components/registry-browser.js +6 -12
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/guards.d.ts +0 -9
- package/dist/components/guards.js +0 -12
- package/dist/components/guards.js.map +1 -1
- package/dist/components/section-state.d.ts +0 -2
- package/dist/components/section-state.js +0 -9
- package/dist/components/section-state.js.map +1 -1
- package/dist/components/section-surface.d.ts +3 -7
- package/dist/components/section-surface.js +5 -12
- package/dist/components/section-surface.js.map +1 -1
- package/dist/components/sections.d.ts +0 -1
- package/dist/components/sections.js +0 -10
- package/dist/components/sections.js.map +1 -1
- package/dist/components/studio-project-screen.js +13 -14
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/components/studio-shell.d.ts +0 -4
- package/dist/components/studio-shell.js +1 -5
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/lib/operations/graph-commands.d.ts +2 -6
- package/dist/lib/operations/graph-commands.js +1 -2
- package/dist/lib/operations/graph-commands.js.map +1 -1
- package/dist/lib/operations/index.d.ts +0 -2
- package/dist/lib/operations/index.js +0 -2
- package/dist/lib/operations/index.js.map +1 -1
- package/dist/lib/operations/session-hydration.d.ts +14 -24
- package/dist/lib/operations/session-hydration.js +15 -34
- package/dist/lib/operations/session-hydration.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +1 -3
- package/dist/lib/persistence/convex-adapter.js +1 -154
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persona/labels.js +0 -2
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/references/reference-model.d.ts +0 -16
- package/dist/lib/references/reference-model.js +2 -54
- package/dist/lib/references/reference-model.js.map +1 -1
- package/dist/lib/references/references-store.js +3 -4
- package/dist/lib/references/references-store.js.map +1 -1
- package/dist/lib/registry/index.d.ts +0 -16
- package/dist/lib/registry/index.js +0 -15
- package/dist/lib/registry/index.js.map +1 -1
- package/dist/lib/webhooks/tags.js +2 -16
- package/dist/lib/webhooks/tags.js.map +1 -1
- package/package.json +3 -2
- package/convex/graphs.ts +0 -55
- 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-
|
|
95
|
-
|
|
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
|
-
//
|
|
225
|
-
//
|
|
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
|
|
package/convex/delivery.ts
CHANGED
|
@@ -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 (
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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)
|
|
253
|
-
*
|
|
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 (
|
|
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.
|
package/convex/functions.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
*
|
|
43
|
-
*
|
|
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
|
|
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` /
|
|
26
|
-
*
|
|
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
|
-
}
|
package/convex/references.ts
CHANGED
|
@@ -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
|
-
|
|
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'
|
|
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,
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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)
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
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
|
-
|
|
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
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
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
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
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
|
|
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.
|
|
309
|
-
//
|
|
310
|
-
//
|
|
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
|
})
|