@elytracms/studio 0.0.100 → 0.0.102
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/analytics-cost.test.ts +1 -1
- package/convex/asset-meta-patch.test.ts +7 -27
- package/convex/assets.ts +6 -41
- package/convex/cms.ts +2 -29
- package/convex/delivery.ts +2 -2
- package/convex/record-functions.ts +15 -82
- package/convex/redirects.ts +11 -116
- package/convex/schema.ts +4 -23
- package/convex/shared-capture.convex.test.ts +165 -0
- package/convex/triggers.ts +7 -14
- package/dist/components/ai/ai-chat-panel-lazy.js +7 -2
- package/dist/components/ai/ai-chat-panel-lazy.js.map +1 -1
- package/dist/components/ai/ai-chat-panel.js +8 -5
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/proposal-card.js +2 -1
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/ai/proposal-unfilled.d.ts +14 -0
- package/dist/components/ai/proposal-unfilled.js +33 -0
- package/dist/components/ai/proposal-unfilled.js.map +1 -0
- package/dist/components/ai/translate-document-action.js +8 -6
- package/dist/components/ai/translate-document-action.js.map +1 -1
- package/dist/components/ai/translate-skipped.js +1 -0
- package/dist/components/ai/translate-skipped.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +12 -12
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/assets/asset-detail.js +1 -2
- package/dist/components/assets/asset-detail.js.map +1 -1
- package/dist/components/assets/asset-metadata.d.ts +2 -9
- package/dist/components/assets/asset-metadata.js +3 -10
- package/dist/components/assets/asset-metadata.js.map +1 -1
- package/dist/components/assets/asset-search.js +1 -1
- package/dist/components/assets/asset-search.js.map +1 -1
- package/dist/components/assets/asset-selection.js +2 -2
- package/dist/components/assets/asset-selection.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +2 -3
- package/dist/components/assets/assets-collection-view-source.js +2 -3
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/builder/__fixtures__/reuse-race.d.ts +30 -0
- package/dist/components/builder/__fixtures__/reuse-race.js +77 -0
- package/dist/components/builder/__fixtures__/reuse-race.js.map +1 -0
- package/dist/components/builder/builder-store.d.ts +37 -1
- package/dist/components/builder/builder-store.js +21 -6
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-chrome-css.d.ts +1 -1
- package/dist/components/builder/canvas-chrome-css.js +1 -1
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/canvas-chrome.d.ts +1 -1
- package/dist/components/builder/canvas-chrome.js +1 -1
- package/dist/components/builder/canvas-chrome.js.map +1 -1
- package/dist/components/builder/canvas-rebase.d.ts +4 -4
- package/dist/components/builder/canvas-rebase.js +3 -2
- package/dist/components/builder/canvas-rebase.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +1 -1
- package/dist/components/builder/canvas-render-context.js +1 -1
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/composition-field-editor-lazy.js +2 -1
- package/dist/components/builder/composition-field-editor-lazy.js.map +1 -1
- package/dist/components/builder/composition-field-editor.js +3 -3
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/reuse-context.d.ts +9 -2
- package/dist/components/builder/reuse-context.js +46 -6
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +113 -70
- package/dist/components/builder/reuse.js +123 -108
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/cms/asset-field.js +1 -3
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/cell.d.ts +2 -2
- package/dist/components/cms/cell.js +8 -18
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +9 -1
- package/dist/components/cms/cms-store.js +1 -0
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/detail-editor.js +15 -14
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/field-control.js +13 -1
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/prop-fields.js +1 -1
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/rich-text/lazy.js +2 -1
- package/dist/components/cms/rich-text/lazy.js.map +1 -1
- package/dist/components/cms/rich-text/toolbar-state.d.ts +1 -1
- package/dist/components/cms/rich-text/toolbar-state.js +1 -1
- package/dist/components/cms/rich-text/toolbar-state.js.map +1 -1
- package/dist/components/cms/use-field-focus.js +3 -3
- package/dist/components/cms/use-field-focus.js.map +1 -1
- package/dist/components/cms/version-compare-lazy.js +2 -1
- package/dist/components/cms/version-compare-lazy.js.map +1 -1
- package/dist/components/components/components-index.js +1 -1
- package/dist/components/components/components-index.js.map +1 -1
- package/dist/components/deployment-setup-gate.d.ts +1 -1
- package/dist/components/deployment-setup-gate.js +1 -1
- package/dist/components/deployment-setup-gate.js.map +1 -1
- package/dist/components/load-failure.d.ts +62 -0
- package/dist/components/load-failure.js +110 -0
- package/dist/components/load-failure.js.map +1 -0
- package/dist/components/notifications/shell-findings.js +3 -3
- package/dist/components/notifications/shell-findings.js.map +1 -1
- package/dist/customization.d.ts +3 -3
- package/dist/customization.js.map +1 -1
- package/dist/lib/ai/apply-content.js +99 -28
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +77 -7
- package/dist/lib/ai/chat-context.js +139 -14
- package/dist/lib/ai/chat-context.js.map +1 -1
- package/dist/lib/ai/chat-state.d.ts +7 -1
- package/dist/lib/ai/chat-state.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +5 -0
- package/dist/lib/ai/constraints.js +21 -6
- package/dist/lib/ai/constraints.js.map +1 -1
- package/dist/lib/ai/fake-provider.d.ts +76 -26
- package/dist/lib/ai/fake-provider.js +210 -66
- package/dist/lib/ai/fake-provider.js.map +1 -1
- package/dist/lib/ai/index.d.ts +1 -0
- package/dist/lib/ai/index.js +1 -0
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/keyless-assistant.d.ts +37 -0
- package/dist/lib/ai/keyless-assistant.js +193 -0
- package/dist/lib/ai/keyless-assistant.js.map +1 -0
- package/dist/lib/ai/knowledge.d.ts +9 -9
- package/dist/lib/ai/knowledge.js +22 -0
- package/dist/lib/ai/knowledge.js.map +1 -1
- package/dist/lib/ai/shape.d.ts +25 -2
- package/dist/lib/ai/shape.js +58 -7
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +18 -1
- package/dist/lib/ai/tools.js +74 -11
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/translate-document.d.ts +8 -1
- package/dist/lib/ai/translate-document.js +25 -9
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/types.d.ts +22 -7
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.js +13 -17
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/ai/writer-form.d.ts +72 -3
- package/dist/lib/ai/writer-form.js +320 -42
- package/dist/lib/ai/writer-form.js.map +1 -1
- package/dist/lib/assets/upload.d.ts +1 -2
- package/dist/lib/assets/upload.js +1 -2
- package/dist/lib/assets/upload.js.map +1 -1
- package/dist/lib/auth-errors.d.ts +1 -1
- package/dist/lib/auth-errors.js +1 -1
- package/dist/lib/auth-errors.js.map +1 -1
- package/dist/lib/engine-version.d.ts +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/messages.de.d.ts +1 -1
- package/dist/lib/i18n/messages.de.js +32 -8
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +25 -3
- package/dist/lib/i18n/messages.en.js +40 -10
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/cms-persistence.d.ts +28 -0
- package/dist/lib/operations/cms-persistence.js +55 -0
- package/dist/lib/operations/cms-persistence.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +7 -2
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +3 -3
- package/dist/lib/workspace/index.js +3 -3
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/router/navigate-once.d.ts +1 -1
- package/dist/router/navigate-once.js.map +1 -1
- package/dist/router/root.d.ts +3 -2
- package/dist/router/root.js +25 -4
- package/dist/router/root.js.map +1 -1
- package/package.json +3 -3
- package/convex/asset-alt-migration.test.ts +0 -127
- package/convex/redirect-migration.test.ts +0 -205
- package/convex/version-kind-stamping.test.ts +0 -118
|
@@ -485,7 +485,7 @@ function pick<T>(random: () => number, weighted: readonly (readonly [T, number])
|
|
|
485
485
|
}
|
|
486
486
|
|
|
487
487
|
const SLUGS = [
|
|
488
|
-
'sommerfest-im-garten-der-
|
|
488
|
+
'sommerfest-im-garten-der-stadthalle',
|
|
489
489
|
'unsere-torten-fuer-hochzeiten',
|
|
490
490
|
'ueber-uns',
|
|
491
491
|
'kontakt-und-anfahrt',
|
|
@@ -4,7 +4,7 @@ import { convexTest } from 'convex-test'
|
|
|
4
4
|
import type { ConvexHttpClient } from 'convex/browser'
|
|
5
5
|
import type { FunctionReference } from 'convex/server'
|
|
6
6
|
import { defaultAssetCollection, measureMaxDepth } from '@elytracms/core/cms-core'
|
|
7
|
-
import { InMemoryBlobStorage, InMemoryPersistenceAdapter, PersistenceValidationError
|
|
7
|
+
import { InMemoryBlobStorage, InMemoryPersistenceAdapter, PersistenceValidationError } from '@elytracms/core/persistence'
|
|
8
8
|
import type { AssetRepository } from '@elytracms/core/persistence'
|
|
9
9
|
import { ConvexPersistenceAdapter } from '../src/lib/persistence/convex-adapter'
|
|
10
10
|
import { runUpload } from '../src/lib/assets/upload'
|
|
@@ -29,8 +29,7 @@ afterEach(() => {
|
|
|
29
29
|
* EC-634 — an asset's `meta` is written per key, inside the mutation, on BOTH adapters:
|
|
30
30
|
* two sessions (two adapters over ONE backend — two studio tabs on one deployment) whose
|
|
31
31
|
* dialog saves interleave keep both keys; a dialog save landing while a file replace is in
|
|
32
|
-
* flight survives the replace
|
|
33
|
-
* stored bag by the same transaction. convex-test serialises mutations, so this proves the
|
|
32
|
+
* flight survives the replace. convex-test serialises mutations, so this proves the
|
|
34
33
|
* per-key MERGE in `assets:upsert`, not Convex's OCC retry (which re-runs the same handler
|
|
35
34
|
* against the newer row — the merge is what makes that retry correct).
|
|
36
35
|
*/
|
|
@@ -42,7 +41,6 @@ const modules = {
|
|
|
42
41
|
|
|
43
42
|
const PROJECT_ID = 'asset-meta-patch'
|
|
44
43
|
const SESSION_USER_ID = 'user_admin'
|
|
45
|
-
const STAMP = '2026-10-27T00:00:00.000Z'
|
|
46
44
|
|
|
47
45
|
async function convexBackend() {
|
|
48
46
|
const t = convexTest(schema, modules)
|
|
@@ -161,31 +159,13 @@ describe('EC-634 — asset meta is patched per key in the write, on both adapter
|
|
|
161
159
|
expect(convex).toMatchObject({ alt: 'A harbour at dusk & boats', note: 'B note' })
|
|
162
160
|
})
|
|
163
161
|
|
|
164
|
-
it('a
|
|
162
|
+
it('EC-708: a write naming alt is refused before the wire — the mutation declares no alt argument', async () => {
|
|
165
163
|
const { t, a } = await convexBackend()
|
|
166
|
-
await
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
filename: 'l.jpg',
|
|
170
|
-
contentType: 'image/jpeg',
|
|
171
|
-
byteSize: 1,
|
|
172
|
-
storageKey: 'k_legacy',
|
|
173
|
-
alt: 'Column alt',
|
|
174
|
-
createdAt: STAMP,
|
|
175
|
-
updatedAt: STAMP,
|
|
176
|
-
})
|
|
177
|
-
})
|
|
178
|
-
// An edit of ANOTHER key: the alt it did not name moves home and is kept.
|
|
179
|
-
const kept = await a.upsertAsset(PROJECT_ID, { id: 'legacy', metaPatch: { credit: 'Ada' } })
|
|
180
|
-
expect(kept.meta).toEqual({ alt: 'Column alt', credit: 'Ada' })
|
|
181
|
-
expect(kept.alt).toBeUndefined()
|
|
182
|
-
expect(assetAlt(kept, null)).toBe('Column alt')
|
|
183
|
-
// A clear of the alt stays cleared — no column left to re-surface it (ASSETS D21).
|
|
184
|
-
const cleared = await a.upsertAsset(PROJECT_ID, { id: 'legacy', metaPatch: { alt: null } })
|
|
185
|
-
expect(assetAlt(cleared, null)).toBeNull()
|
|
186
|
-
const row = await t.run((ctx) => ctx.db.query('assets').filter((q) => q.eq(q.field('assetId'), 'legacy')).first())
|
|
187
|
-
expect(row?.alt).toBeUndefined()
|
|
164
|
+
await seed(a, { credit: 'Ada' })
|
|
165
|
+
await expect(a.upsertAsset(PROJECT_ID, { id: 'hero', alt: 'Column alt' } as never)).rejects.toThrow(/meta\.alt/)
|
|
166
|
+
const row = await t.run((ctx) => ctx.db.query('assets').filter((q) => q.eq(q.field('assetId'), 'hero')).first())
|
|
188
167
|
expect(row?.meta).toEqual({ credit: 'Ada' })
|
|
168
|
+
expect(row && 'alt' in row).toBe(false)
|
|
189
169
|
})
|
|
190
170
|
|
|
191
171
|
it('EC-632 — per-locale buckets are stored by the real mutation, and the stored row measures its depth', async () => {
|
package/convex/assets.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ConvexError, v } from 'convex/values'
|
|
2
2
|
import type { GenericId, Validator } from 'convex/values'
|
|
3
3
|
import type { DocumentByName, WithoutSystemFields } from 'convex/server'
|
|
4
|
-
import {
|
|
4
|
+
import { rejectedWriteMessage, resolveAssetWrite } from '@elytracms/core/persistence'
|
|
5
5
|
import type { DataModel } from './functions'
|
|
6
|
-
import {
|
|
6
|
+
import { mutation, query } from './functions'
|
|
7
7
|
import { requireCapability } from './guard'
|
|
8
8
|
import { ingestBindingHolds } from './ingest'
|
|
9
9
|
|
|
@@ -26,9 +26,10 @@ const clearable = <T, F extends string>(validator: Validator<T, 'required', F>)
|
|
|
26
26
|
/**
|
|
27
27
|
* Write one asset (EC-577). The ONE write rule (`resolveAssetWrite`, shared with
|
|
28
28
|
* the in-memory adapter) runs here, inside the transaction: a column absent from
|
|
29
|
-
* the args keeps its stored value, `null` clears it, a value sets it —
|
|
30
|
-
*
|
|
31
|
-
* `metaPatch` changes `meta` per key against the row read here
|
|
29
|
+
* the args keeps its stored value, `null` clears it, a value sets it — there is no `alt`
|
|
30
|
+
* argument (EC-708: an asset's alt is `meta.alt`; the adapter refuses an `alt` write
|
|
31
|
+
* before the wire), and a `metaPatch` changes `meta` per key against the row read here
|
|
32
|
+
* (EC-634). An edit is
|
|
32
33
|
* ONE `db.patch` — the rule's store patch, where a cleared column is `undefined`,
|
|
33
34
|
* which `db.patch` removes — so the write-chokepoint triggers fire once. No stored
|
|
34
35
|
* asset → ONE `db.insert` of the complete row (the `defaults` fill what the write
|
|
@@ -44,13 +45,6 @@ export const upsert = mutation({
|
|
|
44
45
|
storageKey: v.optional(v.string()),
|
|
45
46
|
width: clearable(v.number()),
|
|
46
47
|
height: clearable(v.number()),
|
|
47
|
-
/**
|
|
48
|
-
* LEGACY column (EC-587): accepted on the wire only so a write that still sets it
|
|
49
|
-
* is refused BY THE RULE with a message naming `meta.alt` (not a bare validator
|
|
50
|
-
* error); `null` clears it. Dropped with the column once `liftLegacyAlt` ran on
|
|
51
|
-
* every deployment.
|
|
52
|
-
*/
|
|
53
|
-
alt: clearable(v.string()),
|
|
54
48
|
title: clearable(v.string()),
|
|
55
49
|
url: clearable(v.string()),
|
|
56
50
|
focalPoint: clearable(v.object({ x: v.number(), y: v.number() })),
|
|
@@ -124,35 +118,6 @@ export const upsert = mutation({
|
|
|
124
118
|
},
|
|
125
119
|
})
|
|
126
120
|
|
|
127
|
-
/**
|
|
128
|
-
* EC-587 — the one-shot migration of the legacy `alt` column: every asset row holding
|
|
129
|
-
* it gets the value lifted into `meta.alt` (unless `meta` already carries an `alt`
|
|
130
|
-
* key, which wins — it is what every reader already reads) and the column unset, by
|
|
131
|
-
* the ONE pure rule (core `liftLegacyAlt`, shared with the in-memory runner
|
|
132
|
-
* `liftLegacyAssetAlts`). Idempotent: a second run lifts nothing.
|
|
133
|
-
*
|
|
134
|
-
* Internal: run by an admin from the CLI, never by a client —
|
|
135
|
-
* `npx convex run assets:liftLegacyAlt` (no args). Each lifted row is one `db.patch`
|
|
136
|
-
* through the write-chokepoint triggers, so it schedules the asset's revalidation like
|
|
137
|
-
* any asset edit (the delivered alt is unchanged by the lift; the sweep is harmless).
|
|
138
|
-
* One transaction: Convex bounds it at 8192 written rows, far above any v1 library —
|
|
139
|
-
* documented, not paginated (as `cms:clearDocuments`).
|
|
140
|
-
*/
|
|
141
|
-
export const liftLegacyAlt = internalMutation({
|
|
142
|
-
args: {},
|
|
143
|
-
handler: async (ctx) => {
|
|
144
|
-
const rows = await ctx.db.query('assets').collect()
|
|
145
|
-
const lifted: string[] = []
|
|
146
|
-
for (const row of rows) {
|
|
147
|
-
const patch = legacyAltLift(row)
|
|
148
|
-
if (!patch) continue
|
|
149
|
-
await ctx.db.patch(row._id, patch as Partial<AssetDoc>)
|
|
150
|
-
lifted.push(row.assetId)
|
|
151
|
-
}
|
|
152
|
-
return { scanned: rows.length, lifted }
|
|
153
|
-
},
|
|
154
|
-
})
|
|
155
|
-
|
|
156
121
|
export const get = query({
|
|
157
122
|
args: { assetId: v.string() },
|
|
158
123
|
handler: async (ctx, args) => {
|
package/convex/cms.ts
CHANGED
|
@@ -12,9 +12,9 @@ import {
|
|
|
12
12
|
unfoldValue,
|
|
13
13
|
} from '@elytracms/core/cms-core'
|
|
14
14
|
import type { CmsDocument, DocumentPatch, PatchLogKind, ReplacePrecondition } from '@elytracms/core/cms-core'
|
|
15
|
-
import { isPinnableVersion
|
|
15
|
+
import { isPinnableVersion } from '@elytracms/core/persistence'
|
|
16
16
|
import type { DocumentVersionKind } from '@elytracms/core/persistence'
|
|
17
|
-
import {
|
|
17
|
+
import { mutation, query } from './functions'
|
|
18
18
|
import type { MutationCtx, QueryCtx } from './functions'
|
|
19
19
|
import { requireCapability } from './guard'
|
|
20
20
|
import { assertWithinWriteLimits } from './write-limits'
|
|
@@ -866,33 +866,6 @@ export const listAllDocumentVersions = query({
|
|
|
866
866
|
},
|
|
867
867
|
})
|
|
868
868
|
|
|
869
|
-
/**
|
|
870
|
-
* EC-707 — the EC-585 kind stamping: every version row recorded before EC-585 (no `kind`)
|
|
871
|
-
* gets the kind `isPinnableVersion` already reads it as — `label: 'published'` → `'publish'`,
|
|
872
|
-
* anything else → `'snapshot'` (core `planVersionKindStamps`, the same inference) — so
|
|
873
|
-
* pinnability is unchanged for every row, and once every deployment reads 0 kind-less rows
|
|
874
|
-
* the tolerant branch and the optional `kind` can go (EC-707's deletions). Idempotent: a
|
|
875
|
-
* second run stamps 0; a row that names a kind is never touched; nothing but `kind` is written.
|
|
876
|
-
*
|
|
877
|
-
* Internal: an admin runs it from the CLI, once per deployment — `npx convex run
|
|
878
|
-
* cms:stampVersionKinds` (no args) → `{ scanned, stamped }`; `pnpm demo:reseed` runs it twice.
|
|
879
|
-
* ONE transaction at a documented bound (`VERSION_KIND_STAMP_BOUND`: 4096 rows / 4 MiB of
|
|
880
|
-
* rewritten rows, half of Convex's per-transaction write limits): it reads every version row,
|
|
881
|
-
* as `listAllDocumentVersions` already does for delivery, and above the bound it throws
|
|
882
|
-
* before writing anything — a refusal is whole, never a partial stamping. `documentVersions`
|
|
883
|
-
* has no write-chokepoint trigger, so the stamp schedules no revalidation (delivery reads the
|
|
884
|
-
* same pins before and after).
|
|
885
|
-
*/
|
|
886
|
-
export const stampVersionKinds = internalMutation({
|
|
887
|
-
args: {},
|
|
888
|
-
handler: async (ctx) => {
|
|
889
|
-
const rows = await ctx.db.query('documentVersions').collect()
|
|
890
|
-
const stamps = planVersionKindStamps(rows)
|
|
891
|
-
for (const { row, kind } of stamps) await ctx.db.patch(row._id, { kind })
|
|
892
|
-
return { scanned: rows.length, stamped: stamps.length }
|
|
893
|
-
},
|
|
894
|
-
})
|
|
895
|
-
|
|
896
869
|
/**
|
|
897
870
|
* Pin an EXISTING version live (EC-224 `pinVersion`, changeset revert). Pin integrity is
|
|
898
871
|
* enforced here, in the transaction: the version must exist for THIS document
|
package/convex/delivery.ts
CHANGED
|
@@ -181,8 +181,8 @@ export async function loadSnapshot(
|
|
|
181
181
|
|
|
182
182
|
// Editorial redirects (EC-303): only ENABLED entries serve — a disabled redirect is
|
|
183
183
|
// stored but never live, in either perspective. EC-616: read through the redirect
|
|
184
|
-
// kind's one server-side reader (records
|
|
185
|
-
//
|
|
184
|
+
// kind's one server-side reader (the `records` store) and decided by the ONE
|
|
185
|
+
// live-redirect rule, `readLiveRedirects` (EC-590 / EC-627 — this
|
|
186
186
|
// was the last hand-built copy on Convex). EC-628: a stored redirect the rule cannot
|
|
187
187
|
// read (a non-boolean switch, a non-string `to`) is withheld AND reported — the host
|
|
188
188
|
// carries `redirectIssues` into its source issues — instead of being served or dropped
|
|
@@ -3,7 +3,6 @@ import type { GenericId } from 'convex/values'
|
|
|
3
3
|
import type { RegisteredMutation, RegisteredQuery } from 'convex/server'
|
|
4
4
|
import {
|
|
5
5
|
PersistenceValidationError,
|
|
6
|
-
matchesRecordQuery,
|
|
7
6
|
orderRecords,
|
|
8
7
|
recordSlots,
|
|
9
8
|
recordWireRow,
|
|
@@ -65,33 +64,6 @@ type RecordDoc = {
|
|
|
65
64
|
updatedAt: string
|
|
66
65
|
}
|
|
67
66
|
|
|
68
|
-
/**
|
|
69
|
-
* TOLERANT READ (marked; EC-616's redirect move, the EC-587 precedent): a kind whose rows
|
|
70
|
-
* used to live in a table of their own declares where, so a deployment whose functions
|
|
71
|
-
* moved but whose migration has not run yet serves exactly what it served before — no
|
|
72
|
-
* gap. Reads union the legacy rows a record does not shadow; a save of a legacy-only id
|
|
73
|
-
* lifts it (the record is created from the legacy columns, the legacy row deleted, one
|
|
74
|
-
* transaction); a remove deletes both. Writes NEVER go to the legacy table. Removed with
|
|
75
|
-
* the legacy table once its migration is reported run on every deployment (a gated item).
|
|
76
|
-
*/
|
|
77
|
-
export interface LegacyRecordSource {
|
|
78
|
-
/** The legacy row for one id: its columns and stamps, and a handle to delete it. */
|
|
79
|
-
find(ctx: QueryCtx | MutationCtx, recordId: string): Promise<LegacyRecord | null>
|
|
80
|
-
/** Every legacy row (the tables are small editorial sets: read whole). */
|
|
81
|
-
list(ctx: QueryCtx | MutationCtx): Promise<LegacyRecord[]>
|
|
82
|
-
/** Delete one legacy row (after its record exists). */
|
|
83
|
-
remove(ctx: MutationCtx, record: LegacyRecord): Promise<void>
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export interface LegacyRecord {
|
|
87
|
-
recordId: string
|
|
88
|
-
columns: RecordColumns
|
|
89
|
-
createdAt: string
|
|
90
|
-
updatedAt: string
|
|
91
|
-
/** The legacy row's own id, opaque to the factory. */
|
|
92
|
-
handle: unknown
|
|
93
|
-
}
|
|
94
|
-
|
|
95
67
|
/** A `PersistenceValidationError` from the shared rule, as the `ConvexError` the client maps back. */
|
|
96
68
|
function asConvexValidation(error: unknown): unknown {
|
|
97
69
|
if (!(error instanceof PersistenceValidationError)) return error
|
|
@@ -161,47 +133,26 @@ function indexedDoc(doc: RecordDoc): IndexedRecord & { doc: RecordDoc } {
|
|
|
161
133
|
}
|
|
162
134
|
|
|
163
135
|
/**
|
|
164
|
-
* The wire rows of one kind for a query
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* server-side reader of the kind (the delivery snapshot) go through it.
|
|
136
|
+
* The wire rows of one kind for a query, matched and ordered by the store's query. The one
|
|
137
|
+
* list reader: the kind's `list` and any server-side reader of the kind (the delivery
|
|
138
|
+
* snapshot) go through it.
|
|
168
139
|
*/
|
|
169
140
|
export async function listRecordWire(
|
|
170
141
|
ctx: QueryCtx | MutationCtx,
|
|
171
142
|
kind: AnyRecordKind,
|
|
172
143
|
resolved: ResolvedRecordQuery,
|
|
173
|
-
legacy?: LegacyRecordSource,
|
|
174
144
|
): Promise<RecordWireRow[]> {
|
|
175
|
-
|
|
176
|
-
// TOLERANT READ (marked): the union is read unbounded and ordered once, here.
|
|
177
|
-
const { limit: _limit, ...unbounded } = resolved
|
|
178
|
-
const docs = await queryRecordDocs(ctx, kind, { ...unbounded, order: 'asc' })
|
|
179
|
-
const shadowed = new Set(docs.map((doc) => doc.recordId))
|
|
180
|
-
const legacyRows = (await legacy.list(ctx))
|
|
181
|
-
.filter((row) => !shadowed.has(row.recordId))
|
|
182
|
-
.map((row) => ({
|
|
183
|
-
id: row.recordId,
|
|
184
|
-
slots: recordSlots(kind, row.columns, row.createdAt),
|
|
185
|
-
columns: row.columns,
|
|
186
|
-
wire: recordWireRow(kind, row.recordId, row.columns, row.createdAt, row.updatedAt),
|
|
187
|
-
}))
|
|
188
|
-
.filter((row) => matchesRecordQuery(row, resolved))
|
|
189
|
-
// Legacy rows predate every record, so they lead in insertion order.
|
|
190
|
-
const union = [...legacyRows, ...docs.map((doc) => ({ ...indexedDoc(doc), wire: recordDocWire(kind, doc) }))]
|
|
191
|
-
return orderRecords(kind, union, resolved).map((row) => row.wire)
|
|
145
|
+
return (await queryRecordDocs(ctx, kind, resolved)).map((doc) => recordDocWire(kind, doc))
|
|
192
146
|
}
|
|
193
147
|
|
|
194
|
-
/** One record's wire row by id
|
|
148
|
+
/** One record's wire row by id. */
|
|
195
149
|
export async function getRecordWire(
|
|
196
150
|
ctx: QueryCtx | MutationCtx,
|
|
197
151
|
kind: AnyRecordKind,
|
|
198
152
|
recordId: string,
|
|
199
|
-
legacy?: LegacyRecordSource,
|
|
200
153
|
): Promise<RecordWireRow | null> {
|
|
201
154
|
const doc = await findRecordDoc(ctx, kind, recordId)
|
|
202
|
-
|
|
203
|
-
const old = legacy ? await legacy.find(ctx, recordId) : null // TOLERANT READ (marked)
|
|
204
|
-
return old ? recordWireRow(kind, old.recordId, old.columns, old.createdAt, old.updatedAt) : null
|
|
155
|
+
return doc ? recordDocWire(kind, doc) : null
|
|
205
156
|
}
|
|
206
157
|
|
|
207
158
|
/**
|
|
@@ -214,14 +165,10 @@ export async function saveRecord(
|
|
|
214
165
|
recordId: string,
|
|
215
166
|
write: Readonly<Record<string, unknown>>,
|
|
216
167
|
stamps: { createdAt: string; updatedAt: string },
|
|
217
|
-
legacy?: LegacyRecordSource,
|
|
218
168
|
): Promise<RecordWireRow> {
|
|
219
169
|
const existing = await findRecordDoc(ctx, kind, recordId)
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
const stored = existing?.data ?? lifted?.columns
|
|
223
|
-
const columns = rethrowAsConvex(() => resolveRecordSave(kind, recordId, stored, write))
|
|
224
|
-
const createdAt = existing?.createdAt ?? lifted?.createdAt ?? stamps.createdAt
|
|
170
|
+
const columns = rethrowAsConvex(() => resolveRecordSave(kind, recordId, existing?.data, write))
|
|
171
|
+
const createdAt = existing?.createdAt ?? stamps.createdAt
|
|
225
172
|
const slots = recordSlots(kind, columns, createdAt)
|
|
226
173
|
if (existing) {
|
|
227
174
|
// `key1`/`key2` are always written: an edit that empties a key unsets its slot.
|
|
@@ -243,7 +190,6 @@ export async function saveRecord(
|
|
|
243
190
|
createdAt,
|
|
244
191
|
updatedAt: stamps.updatedAt,
|
|
245
192
|
})
|
|
246
|
-
if (lifted && legacy) await legacy.remove(ctx, lifted)
|
|
247
193
|
}
|
|
248
194
|
return recordWireRow(kind, recordId, columns, createdAt, stamps.updatedAt)
|
|
249
195
|
}
|
|
@@ -256,14 +202,8 @@ export interface RecordFunctions {
|
|
|
256
202
|
remove: RegisteredMutation<'public', Record<string, unknown>, Promise<void>>
|
|
257
203
|
}
|
|
258
204
|
|
|
259
|
-
export interface RecordFunctionsOptions {
|
|
260
|
-
/** TOLERANT READ (marked) — see {@link LegacyRecordSource}. Only a kind that moved declares one. */
|
|
261
|
-
legacy?: LegacyRecordSource
|
|
262
|
-
}
|
|
263
|
-
|
|
264
205
|
/** The public CRUD functions of one record kind — see the module header. */
|
|
265
|
-
export function defineRecordFunctions(kind: AnyRecordKind
|
|
266
|
-
const { legacy } = options
|
|
206
|
+
export function defineRecordFunctions(kind: AnyRecordKind): RecordFunctions {
|
|
267
207
|
const idArg = { [kind.idKey]: v.string() }
|
|
268
208
|
const columnArgs = Object.fromEntries(
|
|
269
209
|
[...Object.keys(kind.fields), 'meta', 'metaPatch'].map((column) => [column, v.optional(v.any())]),
|
|
@@ -274,14 +214,10 @@ export function defineRecordFunctions(kind: AnyRecordKind, options: RecordFuncti
|
|
|
274
214
|
handler: async (ctx, args: Record<string, unknown>) => {
|
|
275
215
|
await requireCapability(ctx, kind.access.write)
|
|
276
216
|
const { [kind.idKey]: recordId, createdAt, updatedAt, ...write } = args
|
|
277
|
-
return saveRecord(
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
write,
|
|
282
|
-
{ createdAt: createdAt as string, updatedAt: updatedAt as string },
|
|
283
|
-
legacy,
|
|
284
|
-
)
|
|
217
|
+
return saveRecord(ctx, kind, recordId as string, write, {
|
|
218
|
+
createdAt: createdAt as string,
|
|
219
|
+
updatedAt: updatedAt as string,
|
|
220
|
+
})
|
|
285
221
|
},
|
|
286
222
|
})
|
|
287
223
|
|
|
@@ -289,7 +225,7 @@ export function defineRecordFunctions(kind: AnyRecordKind, options: RecordFuncti
|
|
|
289
225
|
args: idArg,
|
|
290
226
|
handler: async (ctx, args: Record<string, unknown>) => {
|
|
291
227
|
await requireCapability(ctx, kind.access.read)
|
|
292
|
-
return getRecordWire(ctx, kind, args[kind.idKey] as string
|
|
228
|
+
return getRecordWire(ctx, kind, args[kind.idKey] as string)
|
|
293
229
|
},
|
|
294
230
|
})
|
|
295
231
|
|
|
@@ -304,7 +240,7 @@ export function defineRecordFunctions(kind: AnyRecordKind, options: RecordFuncti
|
|
|
304
240
|
handler: async (ctx, args: RecordQuery) => {
|
|
305
241
|
await requireCapability(ctx, kind.access.read)
|
|
306
242
|
const resolved = rethrowAsConvex(() => resolveRecordQuery(kind, args))
|
|
307
|
-
return listRecordWire(ctx, kind, resolved
|
|
243
|
+
return listRecordWire(ctx, kind, resolved)
|
|
308
244
|
},
|
|
309
245
|
})
|
|
310
246
|
|
|
@@ -315,9 +251,6 @@ export function defineRecordFunctions(kind: AnyRecordKind, options: RecordFuncti
|
|
|
315
251
|
const recordId = args[kind.idKey] as string
|
|
316
252
|
const doc = await findRecordDoc(ctx, kind, recordId)
|
|
317
253
|
if (doc) await ctx.db.delete(doc._id)
|
|
318
|
-
// TOLERANT READ (marked): a legacy row of the same id goes too, or it would resurface.
|
|
319
|
-
const old = legacy ? await legacy.find(ctx, recordId) : null
|
|
320
|
-
if (old && legacy) await legacy.remove(ctx, old)
|
|
321
254
|
},
|
|
322
255
|
})
|
|
323
256
|
|
package/convex/redirects.ts
CHANGED
|
@@ -1,18 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { MutationBuilder } from 'convex/server'
|
|
3
|
-
import {
|
|
4
|
-
PersistenceValidationError,
|
|
5
|
-
recordColumnsOf,
|
|
6
|
-
recordSlots,
|
|
7
|
-
redirectRecordKind,
|
|
8
|
-
resolveRecordQuery,
|
|
9
|
-
resolveRecordSave,
|
|
10
|
-
} from '@elytracms/core/persistence'
|
|
1
|
+
import { recordEntryFromWire, redirectRecordKind, resolveRecordQuery } from '@elytracms/core/persistence'
|
|
11
2
|
import type { RedirectEntry } from '@elytracms/core/persistence'
|
|
12
|
-
import type {
|
|
13
|
-
import { defineRecordFunctions,
|
|
14
|
-
import type { LegacyRecord, LegacyRecordSource } from './record-functions'
|
|
15
|
-
import { recordEntryFromWire } from '@elytracms/core/persistence'
|
|
3
|
+
import type { QueryCtx } from './functions'
|
|
4
|
+
import { defineRecordFunctions, listRecordWire } from './record-functions'
|
|
16
5
|
|
|
17
6
|
/**
|
|
18
7
|
* Editorial redirects persistence (EC-303) — since EC-616 the redirect RECORD KIND over
|
|
@@ -22,112 +11,18 @@ import { recordEntryFromWire } from '@elytracms/core/persistence'
|
|
|
22
11
|
* `export * from '@elytracms/studio/convex/redirects'` line are unchanged. Content-gated
|
|
23
12
|
* (a viewer reads, an editor writes), NOT admin-only — redirects are content.
|
|
24
13
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* - **writes go to `records`** (kind `redirect`); nothing writes the legacy table again;
|
|
30
|
-
* - **reads union the legacy rows no record shadows** — the TOLERANT READ below, marked:
|
|
31
|
-
* a deployment with these functions pushed and the migration NOT yet run serves the
|
|
32
|
-
* same redirects it served before (`list`, `get`, the delivery snapshot);
|
|
33
|
-
* - a save of a legacy-only redirect lifts it (record from its columns, legacy row
|
|
34
|
-
* deleted, one transaction); a remove deletes both;
|
|
35
|
-
* - **`redirects:migrateLegacy`** moves every legacy row in one idempotent internal
|
|
36
|
-
* mutation and reports counts — run per deployment (both consumers + the prod harness:
|
|
37
|
-
* owner-run); the legacy table and this tolerant read are dropped together once it is
|
|
38
|
-
* reported run everywhere (a gated item, like EC-587's alt column).
|
|
14
|
+
* The legacy `redirects` table, its tolerant read and its one-time move
|
|
15
|
+
* (`redirects:migrateLegacy`) are deleted (EC-708), after every deployment's scan read 0
|
|
16
|
+
* legacy rows: `records` is the one home.
|
|
39
17
|
*/
|
|
40
|
-
|
|
41
|
-
type LegacyRedirectDoc = DataModel['redirects']['document']
|
|
42
|
-
|
|
43
|
-
/** TOLERANT READ (marked) — the legacy `redirects` table as the kind's legacy source. */
|
|
44
|
-
const legacyRedirects: LegacyRecordSource = {
|
|
45
|
-
find: async (ctx, recordId) => {
|
|
46
|
-
const row = await ctx.db
|
|
47
|
-
.query('redirects')
|
|
48
|
-
.withIndex('by_redirect', (q) => q.eq('redirectId', recordId))
|
|
49
|
-
.first()
|
|
50
|
-
return row ? legacyRecord(row) : null
|
|
51
|
-
},
|
|
52
|
-
list: async (ctx) => (await ctx.db.query('redirects').withIndex('by_redirect').collect()).map(legacyRecord),
|
|
53
|
-
remove: async (ctx, record) => {
|
|
54
|
-
await ctx.db.delete(record.handle as LegacyRedirectDoc['_id'])
|
|
55
|
-
},
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function legacyRecord(row: LegacyRedirectDoc): LegacyRecord {
|
|
59
|
-
return {
|
|
60
|
-
recordId: row.redirectId,
|
|
61
|
-
columns: recordColumnsOf(redirectRecordKind, row),
|
|
62
|
-
createdAt: row.createdAt,
|
|
63
|
-
updatedAt: row.updatedAt,
|
|
64
|
-
handle: row._id,
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export const { save, get, list, remove } = defineRecordFunctions(redirectRecordKind, { legacy: legacyRedirects })
|
|
18
|
+
export const { save, get, list, remove } = defineRecordFunctions(redirectRecordKind)
|
|
69
19
|
|
|
70
20
|
/**
|
|
71
|
-
* Every stored redirect of this deployment
|
|
72
|
-
*
|
|
73
|
-
*
|
|
21
|
+
* Every stored redirect of this deployment, ordered by `from` — the ONE server-side
|
|
22
|
+
* reader; the delivery snapshot projects it through `readLiveRedirects` (EC-590 / EC-627
|
|
23
|
+
* / EC-628: the one live-redirect rule).
|
|
74
24
|
*/
|
|
75
25
|
export async function loadRedirectEntries(ctx: QueryCtx, projectId: string): Promise<RedirectEntry[]> {
|
|
76
|
-
const rows = await listRecordWire(ctx, redirectRecordKind, resolveRecordQuery(redirectRecordKind)
|
|
26
|
+
const rows = await listRecordWire(ctx, redirectRecordKind, resolveRecordQuery(redirectRecordKind))
|
|
77
27
|
return rows.map((row) => recordEntryFromWire(redirectRecordKind, row, projectId))
|
|
78
28
|
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* EC-616 — move every legacy `redirects` row into `records` (kind `redirect`), the
|
|
82
|
-
* redirect move's migration. Per row: a record of the same id already exists (a save
|
|
83
|
-
* lifted it, or an earlier run) → the legacy row is dropped as shadowed; otherwise the
|
|
84
|
-
* row's columns are written as a record CREATE through the one rule (`resolveRecordSave`
|
|
85
|
-
* — a legacy row the current schema refuses is REPORTED and left where it is, still
|
|
86
|
-
* served by the tolerant read, never dropped silently) and the legacy row deleted.
|
|
87
|
-
* Idempotent: a second run scans nothing.
|
|
88
|
-
*
|
|
89
|
-
* Internal: an admin runs it from the CLI, once per deployment —
|
|
90
|
-
* `npx convex run redirects:migrateLegacy` (no args). Built on the RAW internal-mutation
|
|
91
|
-
* builder, deliberately outside the write-chokepoint triggers: the served redirect set is
|
|
92
|
-
* identical before and after by construction (the tolerant read), so the move needs no
|
|
93
|
-
* revalidation — through the triggers it would schedule two sweeps per row (an insert
|
|
94
|
-
* into `records`, a delete from `redirects`). One transaction: Convex bounds it at 8192
|
|
95
|
-
* written rows, far above any editorial redirect set (KON's import holds 192).
|
|
96
|
-
*/
|
|
97
|
-
export const migrateLegacy = (internalMutationGeneric as MutationBuilder<DataModel, 'internal'>)({
|
|
98
|
-
args: {},
|
|
99
|
-
handler: async (ctx) => {
|
|
100
|
-
const legacy = await ctx.db.query('redirects').withIndex('by_redirect').collect()
|
|
101
|
-
const moved: string[] = []
|
|
102
|
-
const shadowed: string[] = []
|
|
103
|
-
const refused: Array<{ redirectId: string; message: string }> = []
|
|
104
|
-
for (const row of legacy) {
|
|
105
|
-
const record = legacyRecord(row)
|
|
106
|
-
if (await findRecordDoc(ctx as unknown as MutationCtx, redirectRecordKind, record.recordId)) {
|
|
107
|
-
await ctx.db.delete(row._id)
|
|
108
|
-
shadowed.push(record.recordId)
|
|
109
|
-
continue
|
|
110
|
-
}
|
|
111
|
-
let columns: Record<string, unknown>
|
|
112
|
-
try {
|
|
113
|
-
columns = resolveRecordSave(redirectRecordKind, record.recordId, undefined, record.columns)
|
|
114
|
-
} catch (error) {
|
|
115
|
-
if (!(error instanceof PersistenceValidationError)) throw error
|
|
116
|
-
refused.push({ redirectId: record.recordId, message: error.message })
|
|
117
|
-
continue
|
|
118
|
-
}
|
|
119
|
-
const slots = recordSlots(redirectRecordKind, columns, record.createdAt)
|
|
120
|
-
await ctx.db.insert('records', {
|
|
121
|
-
kind: redirectRecordKind.kind,
|
|
122
|
-
recordId: record.recordId,
|
|
123
|
-
data: columns,
|
|
124
|
-
at: slots.at,
|
|
125
|
-
createdAt: record.createdAt,
|
|
126
|
-
updatedAt: record.updatedAt,
|
|
127
|
-
})
|
|
128
|
-
await ctx.db.delete(row._id)
|
|
129
|
-
moved.push(record.recordId)
|
|
130
|
-
}
|
|
131
|
-
return { scanned: legacy.length, moved: moved.length, shadowed: shadowed.length, refused, movedIds: moved }
|
|
132
|
-
},
|
|
133
|
-
})
|
package/convex/schema.ts
CHANGED
|
@@ -239,10 +239,10 @@ export default defineSchema({
|
|
|
239
239
|
origin: v.optional(v.string()),
|
|
240
240
|
// EC-585: which mutation recorded the row — 'publish' (`publishDocument`, the only
|
|
241
241
|
// kind `publishDocumentVersion` pins) or 'snapshot' (`saveDocumentVersion`: a restore
|
|
242
|
-
// point).
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
kind: v.
|
|
242
|
+
// point). Required (EC-708: every deployment stamped and read 0 kind-less rows first;
|
|
243
|
+
// the push refuses a row without it). A top-level scalar beside the folded snapshot: it
|
|
244
|
+
// adds no nesting depth.
|
|
245
|
+
kind: v.union(v.literal('publish'), v.literal('snapshot')),
|
|
246
246
|
createdAt: v.string(),
|
|
247
247
|
})
|
|
248
248
|
.index('by_doc', ['collection', 'docId'])
|
|
@@ -259,7 +259,6 @@ export default defineSchema({
|
|
|
259
259
|
byteSize: v.number(),
|
|
260
260
|
width: v.optional(v.number()),
|
|
261
261
|
height: v.optional(v.number()),
|
|
262
|
-
alt: v.optional(v.string()),
|
|
263
262
|
/** Editable display title (EC-152; persisted for parity with in-memory — EC-153). */
|
|
264
263
|
title: v.optional(v.string()),
|
|
265
264
|
storageKey: v.string(),
|
|
@@ -306,24 +305,6 @@ export default defineSchema({
|
|
|
306
305
|
.index('by_storage', ['storageKey'])
|
|
307
306
|
.index('by_thumb_storage', ['thumb.storageKey']),
|
|
308
307
|
|
|
309
|
-
/**
|
|
310
|
-
* Editorial redirects (EC-303) — 301/302 forwards an editor owns, stored as
|
|
311
|
-
* DATA (not config) so they change dynamically without a deploy. The delivery
|
|
312
|
-
* snapshot projects enabled entries onto the routing `RedirectRecord` the
|
|
313
|
-
* catch-all resolves; static/infra redirects stay native (next.config/vercel).
|
|
314
|
-
*/
|
|
315
|
-
redirects: defineTable({
|
|
316
|
-
redirectId: v.string(),
|
|
317
|
-
from: v.string(),
|
|
318
|
-
to: v.string(),
|
|
319
|
-
permanent: v.boolean(),
|
|
320
|
-
enabled: v.boolean(),
|
|
321
|
-
// Schema-driven additive-field bag (EC-311 Tier 2) — see the `assets.meta` bag.
|
|
322
|
-
meta: v.optional(v.any()),
|
|
323
|
-
createdAt: v.string(),
|
|
324
|
-
updatedAt: v.string(),
|
|
325
|
-
}).index('by_redirect', ['redirectId']),
|
|
326
|
-
|
|
327
308
|
/**
|
|
328
309
|
* The generic RECORD STORE (EC-616) — one row per record of a DECLARED record kind
|
|
329
310
|
* (`defineRecordKind` in `@elytracms/core/persistence`): the engine's own kinds and
|