@vexcms/next 0.1.0-alpha.23 → 0.1.0-alpha.24
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/dist/NextAdminPage.d.ts.map +1 -1
- package/dist/NextAdminPage.js +1 -1
- package/dist/cache/createVexRevalidateRoute.d.ts +2 -2
- package/dist/cache/createVexRevalidateRoute.d.ts.map +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/{chunk-2FMPG2YF.js → chunk-DDH2TJTP.js} +28 -19
- package/dist/chunk-DDH2TJTP.js.map +1 -0
- package/dist/{chunk-3J7LCKFC.js → chunk-IW5G5UJ6.js} +10 -6
- package/dist/chunk-IW5G5UJ6.js.map +1 -0
- package/dist/index.js +2 -2
- package/package.json +3 -3
- package/dist/chunk-2FMPG2YF.js.map +0 -1
- package/dist/chunk-3J7LCKFC.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NextAdminPage.d.ts","sourceRoot":"","sources":["../src/NextAdminPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoC,SAAS,EAAoB,MAAM,cAAc,CAAC;AAuDlG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE;IACzC,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,
|
|
1
|
+
{"version":3,"file":"NextAdminPage.d.ts","sourceRoot":"","sources":["../src/NextAdminPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoC,SAAS,EAAoB,MAAM,cAAc,CAAC;AAuDlG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE;IACzC,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,wCAmLA"}
|
package/dist/NextAdminPage.js
CHANGED
|
@@ -81,8 +81,8 @@ export interface CreateVexRevalidateRouteProps {
|
|
|
81
81
|
* ```
|
|
82
82
|
*/
|
|
83
83
|
export declare function createVexRevalidateRoute(props: CreateVexRevalidateRouteProps): {
|
|
84
|
-
POST: (request: NextRequest) => Promise<NextResponse<{
|
|
84
|
+
POST: (request: NextRequest) => Promise<NextResponse<VexRevalidateResponse | {
|
|
85
85
|
error: string;
|
|
86
|
-
}
|
|
86
|
+
}>>;
|
|
87
87
|
};
|
|
88
88
|
//# sourceMappingURL=createVexRevalidateRoute.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createVexRevalidateRoute.d.ts","sourceRoot":"","sources":["../../src/cache/createVexRevalidateRoute.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EAEd,SAAS,EACT,WAAW,EAIX,qBAAqB,EACtB,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"createVexRevalidateRoute.d.ts","sourceRoot":"","sources":["../../src/cache/createVexRevalidateRoute.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EAEd,SAAS,EACT,WAAW,EAIX,qBAAqB,EACtB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAS/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,kDAAkD;AAClD,MAAM,WAAW,6BAA6B;IAC5C;;;OAGG;IACH,MAAM,EAAE,SAAS,CAAC;IAClB;;;;OAIG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC;QACrB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACvC,IAAI,EAAE,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACtC,CAAC,CAAC;IACH;;;;OAIG;IACH,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC;IACnD;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,cAAc,KAAK,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;CAClF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,6BAA6B,GAAG;IAC9E,IAAI,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,qBAAqB,GAAG;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;CAClG,CAiHA"}
|
package/dist/cache/index.js
CHANGED
|
@@ -53,27 +53,30 @@ async function NextAdminPage(props) {
|
|
|
53
53
|
}
|
|
54
54
|
const global = await fetchQuery(
|
|
55
55
|
vexConvexApi.globals.get,
|
|
56
|
-
|
|
56
|
+
// Same args as `GlobalEditView`'s client query (`getGlobal` falls back to
|
|
57
|
+
// the published row for a caller without `readDrafts`).
|
|
58
|
+
{ slug: globalConfig.slug, drafts: globalConfig.versions.drafts },
|
|
57
59
|
props.token ? { token: props.token } : void 0
|
|
58
60
|
);
|
|
61
|
+
const globalLivePreview = resolveLivePreviewSettings({
|
|
62
|
+
config: props.config.admin.livePreview,
|
|
63
|
+
kind: "global",
|
|
64
|
+
slug: globalConfig.slug,
|
|
65
|
+
admin: globalConfig.admin.livePreview
|
|
66
|
+
});
|
|
59
67
|
return /* @__PURE__ */ jsx(
|
|
60
68
|
GlobalEditView,
|
|
61
69
|
{
|
|
62
70
|
global: globalConfig.slug,
|
|
63
71
|
initialData: global,
|
|
64
|
-
initialPreviewPanelOpen: readLivePreviewPanelCookie({
|
|
72
|
+
initialPreviewPanelOpen: globalLivePreview !== void 0 && readLivePreviewPanelCookie({
|
|
65
73
|
cookieValue: cookieStore.get(livePreviewPanelCookieName({ slug: globalConfig.slug }))?.value,
|
|
66
|
-
defaultOpen:
|
|
67
|
-
config: props.config.admin.livePreview,
|
|
68
|
-
kind: "global",
|
|
69
|
-
slug: globalConfig.slug,
|
|
70
|
-
admin: globalConfig.admin.livePreview
|
|
71
|
-
})?.defaultOpen ?? false
|
|
74
|
+
defaultOpen: globalLivePreview.defaultOpen
|
|
72
75
|
}),
|
|
73
76
|
initialPreviewPanelSize: readLivePreviewLayoutCookie({
|
|
74
77
|
cookieValue: cookieStore.get(livePreviewLayoutCookieName({ slug: globalConfig.slug }))?.value
|
|
75
78
|
}),
|
|
76
|
-
initialPreviewUrl: await resolveInitialPreviewUrl({
|
|
79
|
+
initialPreviewUrl: globalLivePreview === void 0 ? void 0 : await resolveInitialPreviewUrl({
|
|
77
80
|
args: { collection: globalConfig.slug, kind: "global", values: {} },
|
|
78
81
|
slug: globalConfig.slug,
|
|
79
82
|
token: props.token
|
|
@@ -132,28 +135,31 @@ async function NextAdminPage(props) {
|
|
|
132
135
|
if (collection && documentId) {
|
|
133
136
|
const initialData2 = await fetchQuery(
|
|
134
137
|
vexConvexApi.get,
|
|
135
|
-
|
|
138
|
+
// Same args as `CollectionEditView`'s client query (`get` falls back to
|
|
139
|
+
// published-only for a caller without `readDrafts`).
|
|
140
|
+
{ id: documentId, collection: collection.slug, drafts: collection.versions.drafts },
|
|
136
141
|
props.token ? { token: props.token } : void 0
|
|
137
142
|
);
|
|
143
|
+
const collectionLivePreview = resolveLivePreviewSettings({
|
|
144
|
+
config: props.config.admin.livePreview,
|
|
145
|
+
kind: "collection",
|
|
146
|
+
slug: collection.slug,
|
|
147
|
+
admin: collection.admin.livePreview
|
|
148
|
+
});
|
|
138
149
|
return /* @__PURE__ */ jsx(
|
|
139
150
|
CollectionEditView,
|
|
140
151
|
{
|
|
141
152
|
collection: collection.slug,
|
|
142
153
|
documentId,
|
|
143
154
|
initialData: initialData2,
|
|
144
|
-
initialPreviewPanelOpen: readLivePreviewPanelCookie({
|
|
155
|
+
initialPreviewPanelOpen: collectionLivePreview !== void 0 && readLivePreviewPanelCookie({
|
|
145
156
|
cookieValue: cookieStore.get(livePreviewPanelCookieName({ slug: collection.slug }))?.value,
|
|
146
|
-
defaultOpen:
|
|
147
|
-
config: props.config.admin.livePreview,
|
|
148
|
-
kind: "collection",
|
|
149
|
-
slug: collection.slug,
|
|
150
|
-
admin: collection.admin.livePreview
|
|
151
|
-
})?.defaultOpen ?? false
|
|
157
|
+
defaultOpen: collectionLivePreview.defaultOpen
|
|
152
158
|
}),
|
|
153
159
|
initialPreviewPanelSize: readLivePreviewLayoutCookie({
|
|
154
160
|
cookieValue: cookieStore.get(livePreviewLayoutCookieName({ slug: collection.slug }))?.value
|
|
155
161
|
}),
|
|
156
|
-
initialPreviewUrl: await resolveInitialPreviewUrl({
|
|
162
|
+
initialPreviewUrl: collectionLivePreview === void 0 ? void 0 : await resolveInitialPreviewUrl({
|
|
157
163
|
args: { collection: collection.slug, kind: "collection", documentId, values: {} },
|
|
158
164
|
slug: collection.slug,
|
|
159
165
|
token: props.token
|
|
@@ -172,6 +178,9 @@ async function NextAdminPage(props) {
|
|
|
172
178
|
vexConvexApi.findPaginated,
|
|
173
179
|
{
|
|
174
180
|
collection: collectionSlug,
|
|
181
|
+
// Same args as `CollectionListView`'s client query, so the hydrated
|
|
182
|
+
// initial data belongs to the query the client subscribes to.
|
|
183
|
+
drafts: collection.versions.drafts ? true : void 0,
|
|
175
184
|
depth: 1,
|
|
176
185
|
paginationOpts: { cursor: null, numItems, totalDocs: true }
|
|
177
186
|
},
|
|
@@ -183,4 +192,4 @@ async function NextAdminPage(props) {
|
|
|
183
192
|
export {
|
|
184
193
|
NextAdminPage
|
|
185
194
|
};
|
|
186
|
-
//# sourceMappingURL=chunk-
|
|
195
|
+
//# sourceMappingURL=chunk-DDH2TJTP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/NextAdminPage.tsx"],"sourcesContent":["import type { CollectionSlug, PaginationResult, VexConfig, VexMediaDocument } from \"@vexcms/core\";\n\nimport {\n appendLivePreviewParams,\n livePreviewLayoutCookieName,\n livePreviewPanelCookieName,\n readLivePreviewLayoutCookie,\n readLivePreviewPanelCookie,\n resolveLivePreviewSettings,\n vexConvexApi,\n} from \"@vexcms/core\";\nimport {\n CollectionEditView,\n CollectionListView,\n DashboardView,\n GlobalEditView,\n GlobalsListView,\n MediaCollectionEditView,\n MediaCollectionListView,\n} from \"@vexcms/react\";\nimport { fetchQuery } from \"convex/nextjs\";\nimport { cookies } from \"next/headers\";\n\n/**\n * Resolves a `{ server }` preview URL for first paint.\n *\n * Two things the caller must not have to remember. First, the query only\n * answers for the `{ server }` form — the string and client forms resolve\n * synchronously in the browser and return `null` here, so the iframe falls back\n * to its own resolution. Second, the resolved URL is a plain public URL: it\n * needs `vexLivePreview=1` appended or the preview loads an ordinary page that\n * listens to nothing, which looks like \"live preview is broken\" with no error\n * anywhere. Appending it here keeps the server path identical to the client one.\n *\n * @param props.args - Query arguments identifying the document or global.\n * @param props.slug - Collection or global slug, for the temp-id params.\n * @param props.token - Auth token forwarded to Convex.\n * @returns The preview-ready URL, or `undefined` when the config resolves in\n * the browser instead.\n */\nasync function resolveInitialPreviewUrl(props: {\n args: Parameters<typeof fetchQuery<typeof vexConvexApi.livePreviewUrl>>[1];\n slug: string;\n token?: string;\n}): Promise<string | undefined> {\n const url = await fetchQuery(\n vexConvexApi.livePreviewUrl,\n props.args,\n props.token ? { token: props.token } : undefined,\n );\n if (!url) return undefined;\n\n return appendLivePreviewParams({ url, collectionSlug: props.slug });\n}\n\n/**\n * VexCMS admin page server component for Next.js.\n *\n * An `async` server component that routes by the `[[...slug]]` catch-all\n * params, prefetches Convex data via `fetchQuery`, and renders the correct\n * view component. Does **not** include a layout wrapper — `NextAdminLayout`\n * in `app/admin/layout.tsx` owns the persistent shell.\n *\n * **Route mapping:**\n * | `path` array | View |\n * |---|---|\n * | `[]` or undefined | `DashboardView` |\n * | `[collectionSlug]` | `CollectionListView` with preloaded docs |\n * | `[collectionSlug, \"new\"]` | `CollectionEditView` (empty form) |\n * | `[collectionSlug, documentId]` | `CollectionEditView` with preloaded doc |\n *\n * @param props - Component props\n * @param props.config - The resolved server `VexConfig`, typically the default export of\n * `vex.config.server.ts` — not the client-safe `vex.config.ts`.\n * @param props.params - Next.js 15 async params `{ path?: string[] }`\n * @param props.token - Optional session token forwarded to `fetchQuery` for authenticated reads.\n * @returns The appropriate admin view for the current URL path.\n *\n * @example\n * ```tsx\n * // app/admin/[[...slug]]/page.tsx\n * import { NextAdminPage } from \"@vexcms/next/server\";\n * import config from \"../../../../vex.config.server\";\n *\n * export default function AdminPage({\n * params,\n * }: {\n * params: Promise<{ path?: string[] }>;\n * }) {\n * return <NextAdminPage config={config} params={params} />;\n * }\n * ```\n */\nexport async function NextAdminPage(props: {\n config: VexConfig;\n params: Promise<{ path?: string[] }>;\n token?: string;\n}) {\n const { path = [] } = await props.params;\n const [collectionSlug, documentId] = path;\n const cookieStore = await cookies();\n\n if (!collectionSlug) {\n return <DashboardView />;\n }\n\n if (collectionSlug === \"globals\") {\n if (!documentId) {\n return <GlobalsListView />;\n }\n const globalConfig = props.config.globals.find((g) => g.slug === documentId);\n if (!globalConfig) {\n return (\n <div>\n <p className=\"text-muted-foreground p-6\">Global "{documentId}" not found.</p>\n <p>TODO: add not found view</p>\n </div>\n );\n }\n const global = await fetchQuery(\n vexConvexApi.globals.get,\n // Same args as `GlobalEditView`'s client query (`getGlobal` falls back to\n // the published row for a caller without `readDrafts`).\n { slug: globalConfig.slug, drafts: globalConfig.versions.drafts },\n props.token ? { token: props.token } : undefined,\n );\n const globalLivePreview = resolveLivePreviewSettings({\n config: props.config.admin.livePreview,\n kind: \"global\",\n slug: globalConfig.slug,\n admin: globalConfig.admin.livePreview,\n });\n return (\n <GlobalEditView\n global={globalConfig.slug}\n initialData={global}\n initialPreviewPanelOpen={\n globalLivePreview !== undefined &&\n readLivePreviewPanelCookie({\n cookieValue: cookieStore.get(livePreviewPanelCookieName({ slug: globalConfig.slug }))\n ?.value,\n defaultOpen: globalLivePreview.defaultOpen,\n })\n }\n initialPreviewPanelSize={readLivePreviewLayoutCookie({\n cookieValue: cookieStore.get(livePreviewLayoutCookieName({ slug: globalConfig.slug }))\n ?.value,\n })}\n initialPreviewUrl={\n globalLivePreview === undefined\n ? undefined\n : await resolveInitialPreviewUrl({\n args: { collection: globalConfig.slug, kind: \"global\", values: {} },\n slug: globalConfig.slug,\n token: props.token,\n })\n }\n />\n );\n }\n\n const collection = props.config.collections.find((c) => c.slug === collectionSlug);\n const mediaCollection = props.config.mediaCollections.find((mc) => mc.slug === collectionSlug);\n\n if (!collection && !mediaCollection) {\n return (\n <div>\n <p className=\"text-muted-foreground p-6\">\n Collection "{collectionSlug}" not found.\n </p>\n <p>TODO: add not found view</p>\n </div>\n );\n }\n\n if (mediaCollection && documentId) {\n const initialData = await fetchQuery(\n vexConvexApi.get,\n { id: documentId, collection: mediaCollection.slug },\n props.token ? { token: props.token } : undefined,\n );\n return (\n <MediaCollectionEditView\n collection={mediaCollection.slug}\n documentId={documentId}\n initialData={initialData as null | VexMediaDocument}\n />\n );\n }\n\n if (mediaCollection) {\n const numItems = Math.max(\n mediaCollection.admin.table.serverPageSize,\n mediaCollection.admin.table.defaultPageSize,\n );\n const initialData = await fetchQuery(\n vexConvexApi.find,\n {\n collection: collectionSlug as CollectionSlug,\n paginationOpts: { numItems, totalDocs: true, cursor: null },\n },\n props.token ? { token: props.token } : undefined,\n );\n return (\n <MediaCollectionListView\n collection={mediaCollection.slug}\n initialData={initialData as PaginationResult<VexMediaDocument>}\n />\n );\n }\n\n if (collection && documentId) {\n const initialData = await fetchQuery(\n vexConvexApi.get,\n // Same args as `CollectionEditView`'s client query (`get` falls back to\n // published-only for a caller without `readDrafts`).\n { id: documentId, collection: collection.slug, drafts: collection.versions.drafts },\n props.token ? { token: props.token } : undefined,\n );\n const collectionLivePreview = resolveLivePreviewSettings({\n config: props.config.admin.livePreview,\n kind: \"collection\",\n slug: collection.slug,\n admin: collection.admin.livePreview,\n });\n return (\n <CollectionEditView\n collection={collection.slug}\n documentId={documentId}\n initialData={initialData}\n initialPreviewPanelOpen={\n collectionLivePreview !== undefined &&\n readLivePreviewPanelCookie({\n cookieValue: cookieStore.get(livePreviewPanelCookieName({ slug: collection.slug }))\n ?.value,\n defaultOpen: collectionLivePreview.defaultOpen,\n })\n }\n initialPreviewPanelSize={readLivePreviewLayoutCookie({\n cookieValue: cookieStore.get(livePreviewLayoutCookieName({ slug: collection.slug }))\n ?.value,\n })}\n initialPreviewUrl={\n collectionLivePreview === undefined\n ? undefined\n : await resolveInitialPreviewUrl({\n args: { collection: collection.slug, kind: \"collection\", documentId, values: {} },\n slug: collection.slug,\n token: props.token,\n })\n }\n />\n );\n }\n\n if (!collection) {\n throw new Error(\"invalid collection slug\");\n }\n\n const numItems = Math.max(\n collection.admin.table.serverPageSize,\n collection.admin.table.defaultPageSize,\n );\n const initialData = await fetchQuery(\n vexConvexApi.findPaginated,\n {\n collection: collectionSlug as CollectionSlug,\n // Same args as `CollectionListView`'s client query, so the hydrated\n // initial data belongs to the query the client subscribes to.\n drafts: collection.versions.drafts ? true : undefined,\n depth: 1,\n paginationOpts: { cursor: null, numItems, totalDocs: true },\n },\n props.token ? { token: props.token } : undefined,\n );\n return <CollectionListView collection={collection.slug} initialData={initialData} />;\n}\n"],"mappings":"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","names":["initialData","numItems"]}
|
|
@@ -7,11 +7,6 @@ import {
|
|
|
7
7
|
} from "@vexcms/core";
|
|
8
8
|
import { revalidatePath } from "next/cache";
|
|
9
9
|
import { NextResponse } from "next/server";
|
|
10
|
-
function toCrudAction(operation) {
|
|
11
|
-
if (operation === "remove") return "delete";
|
|
12
|
-
if (operation === "upsert") return "update";
|
|
13
|
-
return operation;
|
|
14
|
-
}
|
|
15
10
|
function createVexRevalidateRoute(props) {
|
|
16
11
|
return {
|
|
17
12
|
async POST(request) {
|
|
@@ -100,6 +95,15 @@ function createVexRevalidateRoute(props) {
|
|
|
100
95
|
}
|
|
101
96
|
};
|
|
102
97
|
}
|
|
98
|
+
function toCrudAction(operation) {
|
|
99
|
+
if (operation === "remove") {
|
|
100
|
+
return "delete";
|
|
101
|
+
}
|
|
102
|
+
if (operation === "upsert" || operation === "publish" || operation === "unpublish") {
|
|
103
|
+
return "update";
|
|
104
|
+
}
|
|
105
|
+
return operation;
|
|
106
|
+
}
|
|
103
107
|
|
|
104
108
|
// src/cache/createVexServerClient.ts
|
|
105
109
|
import { ConvexHttpClient } from "convex/browser";
|
|
@@ -126,4 +130,4 @@ export {
|
|
|
126
130
|
createVexRevalidateRoute,
|
|
127
131
|
createVexServerClient
|
|
128
132
|
};
|
|
129
|
-
//# sourceMappingURL=chunk-
|
|
133
|
+
//# sourceMappingURL=chunk-IW5G5UJ6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cache/createVexRevalidateRoute.ts","../src/cache/createVexServerClient.ts"],"sourcesContent":["import type {\n CollectionSlug,\n CrudWriteAction,\n VexConfig,\n VexDocument,\n VexMutationOperation,\n VexRevalidateChange,\n VexRevalidateRequest,\n VexRevalidateResponse,\n} from \"@vexcms/core\";\nimport type { NextRequest } from \"next/server\";\n\nimport {\n hasPermission,\n PERMISSION_SCOPES,\n resolveTargets,\n VEX_REVALIDATE_BATCH_SIZE,\n} from \"@vexcms/core\";\nimport { revalidatePath } from \"next/cache\";\nimport { NextResponse } from \"next/server\";\n\n/** Props for {@link createVexRevalidateRoute}. */\nexport interface CreateVexRevalidateRouteProps {\n /**\n * The resolved VexCMS config. Supplies `access` for the write-permission\n * check and `routes.map` for target resolution.\n */\n config: VexConfig;\n /**\n * Resolves the authenticated user and active organization for the current\n * request. Wire to `() => fetchAuthQuery(api.auth.api.getUserOrg, {})` — the\n * same call the admin panel route already makes.\n */\n getAuth: () => Promise<{\n organization?: Record<string, unknown>;\n user: null | Record<string, unknown>;\n }>;\n /**\n * Reads the caller's session token for the current request. Wire to the host\n * app's `getToken` export (`convexBetterAuthNextJs`'s output, re-exported\n * from e.g. `~/auth/server`). A falsy return means \"no session\".\n */\n getToken: () => Promise<null | string | undefined>;\n /**\n * Lists every document in a collection, for the collection-wide purge\n * (`{ collection, all: true }`). Wire to a read through the app's own\n * `publishedSlugs`-style query — the factory cannot know an app's generated\n * Convex `api`, which is why this is injected like `getToken`/`getAuth`.\n *\n * Omit to leave the collection-wide branch unwired: it then purges nothing\n * and reports that in `errors`, rather than silently succeeding.\n */\n listCollection?: (collection: CollectionSlug) => Promise<Partial<VexDocument>[]>;\n}\n\n/**\n * Creates a Next.js route handler that purges the cached paths affected by a\n * document write. The admin panel calls this via `useVexMutation` after every\n * successful create/update/delete, same-origin, as a relative fetch — no URL\n * config, no shared secret.\n *\n * **Session-authorized, not secret-authorized.** The caller is a signed-in\n * admin, so this reuses `getToken`/`getAuth` exactly like the admin panel route\n * does, rather than introducing an env var or a shared secret. There is no\n * API-key fallback: a caller with no browser session cannot reach this route as\n * designed.\n *\n * **Path-based, not tag-based.** `revalidatePath` is the only purge mechanism.\n * `cacheComponents: true` — required for `cacheTag`/`revalidateTag` — is\n * incompatible with the `dynamic`/`runtime` segment configs the auth and admin\n * routes require (measured: 5 files fail to build with it enabled), so\n * `revalidateTag` is not an option in this codebase. Do not \"upgrade\" this to\n * tag-based revalidation without first removing that segment-config dependency\n * everywhere.\n *\n * **Never fails the caller past authorization.** A missing session returns 401,\n * a session without write permission 403, and an oversized `changes` batch 413\n * — all three reject before any map work runs. Past that point the handler\n * never throws or returns 5xx: an unconfigured map, a throwing map, or an\n * individual `revalidatePath` failure all resolve to 200 with the failures\n * reported in the body. A failed purge is a stale page; failing the endpoint\n * would only teach the fire-and-forget caller to retry pointlessly.\n *\n * @param props - Config and the host app's auth accessors.\n * @returns `{ POST }` — mount directly as the route module's named export.\n *\n * @example\n * ```ts\n * // app/api/vex/revalidate/route.ts\n * import { api } from \"@convex/_generated/api\";\n * import { createVexRevalidateRoute } from \"@vexcms/next/cache\";\n * import { fetchAuthQuery, getToken } from \"~/auth/server\";\n * import config from \"~/vex.config\";\n *\n * export const { POST } = createVexRevalidateRoute({\n * config,\n * getToken,\n * getAuth: () => fetchAuthQuery(api.auth.api.getUserOrg, {}),\n * });\n * ```\n */\nexport function createVexRevalidateRoute(props: CreateVexRevalidateRouteProps): {\n POST: (request: NextRequest) => Promise<NextResponse<VexRevalidateResponse | { error: string }>>;\n} {\n return {\n async POST(\n request: NextRequest,\n ): Promise<NextResponse<VexRevalidateResponse | { error: string }>> {\n const token = await props.getToken();\n if (token === null || token === undefined || token === \"\") {\n return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 });\n }\n\n let body: VexRevalidateRequest;\n try {\n body = (await request.json()) as VexRevalidateRequest;\n } catch {\n return NextResponse.json({ error: \"Bad Request\" }, { status: 400 });\n }\n\n const { organization, user } = await props.getAuth();\n\n // One mapping, used for BOTH the permission check and target resolution.\n const operation: VexMutationOperation = \"all\" in body ? \"update\" : body.operation;\n const action = toCrudAction(operation);\n\n const allowed = hasPermission({\n access: props.config.access,\n action,\n organization,\n resource: body.collection,\n user,\n // A purge writes no document fields — this asks whether the caller\n // may act on the collection at all, not against a specific payload.\n scope: PERMISSION_SCOPES.any,\n });\n if (!allowed) {\n return NextResponse.json({ error: \"Forbidden\" }, { status: 403 });\n }\n\n const errors: unknown[] = [];\n let changes: VexRevalidateChange[];\n\n if (\"all\" in body) {\n // Server-derived, so no client-supplied batch to bound. Each document\n // is mapped as its current state.\n if (props.listCollection === undefined) {\n errors.push(\n new Error(\n \"createVexRevalidateRoute: a collection-wide purge was requested but `listCollection` is not wired\",\n ),\n );\n changes = [];\n } else {\n try {\n const docs = await props.listCollection(body.collection);\n changes = docs.map((doc) => ({ after: doc }));\n } catch (error) {\n // An unreachable deployment must not 500 a purge request.\n errors.push(error);\n changes = [];\n }\n }\n } else {\n changes = body.changes;\n if (changes.length > VEX_REVALIDATE_BATCH_SIZE) {\n return NextResponse.json(\n {\n error: `Cannot revalidate more than ${VEX_REVALIDATE_BATCH_SIZE} changes in a single request`,\n },\n { status: 413 },\n );\n }\n }\n\n const routesConfig = props.config.routes;\n if (routesConfig === undefined) {\n // A project with no route map still gets a working, harmless\n // endpoint rather than a 500. Any error already accumulated (e.g. an\n // unwired collection-wide purge) is still reported.\n return NextResponse.json({ errors, revalidated: [] }, { status: 200 });\n }\n\n const pool: string[] = [];\n\n for (const change of changes) {\n // A throwing map is contained inside `resolveTargets` — its failure\n // lands in that call's `errors` and never propagates here.\n const target = resolveTargets({\n after: change.after,\n before: change.before,\n collection: body.collection,\n map: routesConfig.map,\n operation: action,\n });\n pool.push(...target.paths);\n errors.push(...target.errors);\n }\n\n // Dedupe across the whole batch, preserving first-seen order — the same\n // contract `resolveTargets` applies to one document's before/after.\n const revalidated: string[] = [];\n for (const path of new Set(pool)) {\n try {\n revalidatePath(path);\n revalidated.push(path);\n } catch (error) {\n // `revalidatePath` is a Next API called here, so it is not covered by\n // `resolveTargets`' containment.\n errors.push(error);\n }\n }\n\n return NextResponse.json({ errors, revalidated }, { status: 200 });\n },\n };\n}\n\n/**\n * Maps the wire verb to the CRUD action `hasPermission` and `resolveTargets`\n * accept.\n *\n * `VexMutationOperation` is named for the `vexConvexApi` function that produced\n * the write (`vexConvexApi.remove`), while `hasPermission` only accepts\n * `CRUD_ACTIONS`. Mapping happens exactly once per request so the same value\n * drives both the permission check and target resolution.\n *\n * @param operation - The wire operation from the request body.\n * @returns The equivalent CRUD action.\n */\nfunction toCrudAction(operation: VexMutationOperation): CrudWriteAction {\n if (operation === \"remove\") {return \"delete\";}\n if (operation === \"upsert\" || operation === \"publish\" || operation === \"unpublish\") {return \"update\";}\n return operation;\n}\n","import type { FunctionReference, OptionalRestArgs } from \"convex/server\";\n\nimport { ConvexHttpClient } from \"convex/browser\";\nimport { cache } from \"react\";\n\nimport type { VexServerClientOptions, VexServerClient } from \"./types\";\n\n/**\n * Creates a server-only Convex read client safe to use in prerenderable\n * Next.js routes.\n *\n * Wraps `ConvexHttpClient` from `convex/browser` instead of `convex/nextjs`'s\n * `fetchQuery`, which hard-codes `client.setFetchOptions({ cache: \"no-store\" })`\n * — the reason every route calling it builds dynamic (`ƒ`). This client never\n * sets that option, so a route calling only `query()` can be prerendered and\n * governed by the route's own `export const revalidate`.\n *\n * Every `query()` call is deduped via `React.cache` for the lifetime of one\n * request: two identical `(query, args)` calls — e.g. `generateMetadata` and\n * its page both reading the same document — resolve from a single Convex round\n * trip instead of two.\n *\n * `React.cache` is instantiated fresh inside every `createVexServerClient()`\n * call, so that dedupe only holds across calls sharing the SAME client\n * instance — two clients built in two different modules cannot share a round\n * trip even when they read identical `(query, args)`. Each app therefore\n * creates exactly one instance, exported from its own `src/lib/vex.ts`, and\n * every route or component imports that shared `vex` rather than calling\n * `createVexServerClient()` itself.\n *\n * @param props - Client configuration.\n * @returns A {@link VexServerClient} whose `query` method is deduped per request.\n * @throws {Error} When no `url` is given and `process.env.NEXT_PUBLIC_CONVEX_URL`\n * is unset.\n */\nexport function createVexServerClient(props: VexServerClientOptions = {}): VexServerClient {\n const url = props.url ?? process.env.NEXT_PUBLIC_CONVEX_URL;\n if (url === undefined || url === \"\") {\n throw new Error(\n \"createVexServerClient: no Convex deployment URL. Pass `url`, or set NEXT_PUBLIC_CONVEX_URL.\",\n );\n }\n\n const client = new ConvexHttpClient(url, {\n skipConvexDeploymentUrlCheck: props.skipConvexDeploymentUrlCheck,\n });\n\n // Deliberately NOT `client.setFetchOptions({ cache: \"no-store\" })` — that is\n // exactly what `convex/nextjs`'s `fetchQuery` does and what forces every\n // consuming route dynamic.\n\n // `React.cache` keys on the argument list, so the entry is (query reference,\n // args) — never the reference alone. Args are normalised to a single value so\n // `query(fn)` and `query(fn, {})` share one entry.\n const cachedQuery = cache(\n async (query: FunctionReference<\"query\">, args: unknown): Promise<unknown> =>\n client.query(query, args as OptionalRestArgs<FunctionReference<\"query\">>[0]),\n );\n\n return {\n query: async (query, ...args) => cachedQuery(query, args[0] ?? {}),\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
NextAdminPage
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-DDH2TJTP.js";
|
|
4
4
|
import {
|
|
5
5
|
NextAdminLayout
|
|
6
6
|
} from "./chunk-UMJTAD5A.js";
|
|
7
7
|
import {
|
|
8
8
|
createVexRevalidateRoute,
|
|
9
9
|
createVexServerClient
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-IW5G5UJ6.js";
|
|
11
11
|
import {
|
|
12
12
|
createVexRobots,
|
|
13
13
|
createVexSitemap,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vexcms/next",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.24",
|
|
4
4
|
"description": "Next.js adapter and admin routes for VexCMS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cms",
|
|
@@ -66,8 +66,8 @@
|
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"lucide-react": "0.577.0",
|
|
69
|
-
"@vexcms/core": "0.1.0-alpha.
|
|
70
|
-
"@vexcms/react": "0.1.0-alpha.
|
|
69
|
+
"@vexcms/core": "0.1.0-alpha.24",
|
|
70
|
+
"@vexcms/react": "0.1.0-alpha.24"
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"@convex-dev/react-query": "0.1.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/NextAdminPage.tsx"],"sourcesContent":["import type { CollectionSlug, PaginationResult, VexConfig, VexMediaDocument } from \"@vexcms/core\";\n\nimport {\n appendLivePreviewParams,\n livePreviewLayoutCookieName,\n livePreviewPanelCookieName,\n readLivePreviewLayoutCookie,\n readLivePreviewPanelCookie,\n resolveLivePreviewSettings,\n vexConvexApi,\n} from \"@vexcms/core\";\nimport {\n CollectionEditView,\n CollectionListView,\n DashboardView,\n GlobalEditView,\n GlobalsListView,\n MediaCollectionEditView,\n MediaCollectionListView,\n} from \"@vexcms/react\";\nimport { fetchQuery } from \"convex/nextjs\";\nimport { cookies } from \"next/headers\";\n\n/**\n * Resolves a `{ server }` preview URL for first paint.\n *\n * Two things the caller must not have to remember. First, the query only\n * answers for the `{ server }` form — the string and client forms resolve\n * synchronously in the browser and return `null` here, so the iframe falls back\n * to its own resolution. Second, the resolved URL is a plain public URL: it\n * needs `vexLivePreview=1` appended or the preview loads an ordinary page that\n * listens to nothing, which looks like \"live preview is broken\" with no error\n * anywhere. Appending it here keeps the server path identical to the client one.\n *\n * @param props.args - Query arguments identifying the document or global.\n * @param props.slug - Collection or global slug, for the temp-id params.\n * @param props.token - Auth token forwarded to Convex.\n * @returns The preview-ready URL, or `undefined` when the config resolves in\n * the browser instead.\n */\nasync function resolveInitialPreviewUrl(props: {\n args: Parameters<typeof fetchQuery<typeof vexConvexApi.livePreviewUrl>>[1];\n slug: string;\n token?: string;\n}): Promise<string | undefined> {\n const url = await fetchQuery(\n vexConvexApi.livePreviewUrl,\n props.args,\n props.token ? { token: props.token } : undefined,\n );\n if (!url) return undefined;\n\n return appendLivePreviewParams({ url, collectionSlug: props.slug });\n}\n\n/**\n * VexCMS admin page server component for Next.js.\n *\n * An `async` server component that routes by the `[[...slug]]` catch-all\n * params, prefetches Convex data via `fetchQuery`, and renders the correct\n * view component. Does **not** include a layout wrapper — `NextAdminLayout`\n * in `app/admin/layout.tsx` owns the persistent shell.\n *\n * **Route mapping:**\n * | `path` array | View |\n * |---|---|\n * | `[]` or undefined | `DashboardView` |\n * | `[collectionSlug]` | `CollectionListView` with preloaded docs |\n * | `[collectionSlug, \"new\"]` | `CollectionEditView` (empty form) |\n * | `[collectionSlug, documentId]` | `CollectionEditView` with preloaded doc |\n *\n * @param props - Component props\n * @param props.config - The resolved server `VexConfig`, typically the default export of\n * `vex.config.server.ts` — not the client-safe `vex.config.ts`.\n * @param props.params - Next.js 15 async params `{ path?: string[] }`\n * @param props.token - Optional session token forwarded to `fetchQuery` for authenticated reads.\n * @returns The appropriate admin view for the current URL path.\n *\n * @example\n * ```tsx\n * // app/admin/[[...slug]]/page.tsx\n * import { NextAdminPage } from \"@vexcms/next/server\";\n * import config from \"../../../../vex.config.server\";\n *\n * export default function AdminPage({\n * params,\n * }: {\n * params: Promise<{ path?: string[] }>;\n * }) {\n * return <NextAdminPage config={config} params={params} />;\n * }\n * ```\n */\nexport async function NextAdminPage(props: {\n config: VexConfig;\n params: Promise<{ path?: string[] }>;\n token?: string;\n}) {\n const { path = [] } = await props.params;\n const [collectionSlug, documentId] = path;\n const cookieStore = await cookies();\n\n if (!collectionSlug) {\n return <DashboardView />;\n }\n\n if (collectionSlug === \"globals\") {\n if (!documentId) {\n return <GlobalsListView />;\n }\n const globalConfig = props.config.globals.find((g) => g.slug === documentId);\n if (!globalConfig) {\n return (\n <div>\n <p className=\"text-muted-foreground p-6\">Global "{documentId}" not found.</p>\n <p>TODO: add not found view</p>\n </div>\n );\n }\n const global = await fetchQuery(\n vexConvexApi.globals.get,\n { slug: globalConfig.slug },\n props.token ? { token: props.token } : undefined,\n );\n return (\n <GlobalEditView\n global={globalConfig.slug}\n initialData={global}\n initialPreviewPanelOpen={readLivePreviewPanelCookie({\n cookieValue: cookieStore.get(livePreviewPanelCookieName({ slug: globalConfig.slug }))\n ?.value,\n defaultOpen:\n resolveLivePreviewSettings({\n config: props.config.admin.livePreview,\n kind: \"global\",\n slug: globalConfig.slug,\n admin: globalConfig.admin.livePreview,\n })?.defaultOpen ?? false,\n })}\n initialPreviewPanelSize={readLivePreviewLayoutCookie({\n cookieValue: cookieStore.get(livePreviewLayoutCookieName({ slug: globalConfig.slug }))\n ?.value,\n })}\n initialPreviewUrl={await resolveInitialPreviewUrl({\n args: { collection: globalConfig.slug, kind: \"global\", values: {} },\n slug: globalConfig.slug,\n token: props.token,\n })}\n />\n );\n }\n\n const collection = props.config.collections.find((c) => c.slug === collectionSlug);\n const mediaCollection = props.config.mediaCollections.find((mc) => mc.slug === collectionSlug);\n\n if (!collection && !mediaCollection) {\n return (\n <div>\n <p className=\"text-muted-foreground p-6\">\n Collection "{collectionSlug}" not found.\n </p>\n <p>TODO: add not found view</p>\n </div>\n );\n }\n\n if (mediaCollection && documentId) {\n const initialData = await fetchQuery(\n vexConvexApi.get,\n { id: documentId, collection: mediaCollection.slug },\n props.token ? { token: props.token } : undefined,\n );\n return (\n <MediaCollectionEditView\n collection={mediaCollection.slug}\n documentId={documentId}\n initialData={initialData as null | VexMediaDocument}\n />\n );\n }\n\n if (mediaCollection) {\n const numItems = Math.max(\n mediaCollection.admin.table.serverPageSize,\n mediaCollection.admin.table.defaultPageSize,\n );\n const initialData = await fetchQuery(\n vexConvexApi.find,\n {\n collection: collectionSlug as CollectionSlug,\n paginationOpts: { numItems, totalDocs: true, cursor: null },\n },\n props.token ? { token: props.token } : undefined,\n );\n return (\n <MediaCollectionListView\n collection={mediaCollection.slug}\n initialData={initialData as PaginationResult<VexMediaDocument>}\n />\n );\n }\n\n if (collection && documentId) {\n const initialData = await fetchQuery(\n vexConvexApi.get,\n { id: documentId, collection: collection.slug },\n props.token ? { token: props.token } : undefined,\n );\n return (\n <CollectionEditView\n collection={collection.slug}\n documentId={documentId}\n initialData={initialData}\n initialPreviewPanelOpen={readLivePreviewPanelCookie({\n cookieValue: cookieStore.get(livePreviewPanelCookieName({ slug: collection.slug }))\n ?.value,\n defaultOpen:\n resolveLivePreviewSettings({\n config: props.config.admin.livePreview,\n kind: \"collection\",\n slug: collection.slug,\n admin: collection.admin.livePreview,\n })?.defaultOpen ?? false,\n })}\n initialPreviewPanelSize={readLivePreviewLayoutCookie({\n cookieValue: cookieStore.get(livePreviewLayoutCookieName({ slug: collection.slug }))\n ?.value,\n })}\n initialPreviewUrl={await resolveInitialPreviewUrl({\n args: { collection: collection.slug, kind: \"collection\", documentId, values: {} },\n slug: collection.slug,\n token: props.token,\n })}\n />\n );\n }\n\n if (!collection) {\n throw new Error(\"invalid collection slug\");\n }\n\n const numItems = Math.max(\n collection.admin.table.serverPageSize,\n collection.admin.table.defaultPageSize,\n );\n const initialData = await fetchQuery(\n vexConvexApi.findPaginated,\n {\n collection: collectionSlug as CollectionSlug,\n depth: 1,\n paginationOpts: { cursor: null, numItems, totalDocs: true },\n },\n props.token ? { token: props.token } : undefined,\n );\n return <CollectionListView collection={collection.slug} initialData={initialData} />;\n}\n"],"mappings":"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","names":["initialData","numItems"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cache/createVexRevalidateRoute.ts","../src/cache/createVexServerClient.ts"],"sourcesContent":["import type {\n CollectionSlug,\n CrudWriteAction,\n VexConfig,\n VexDocument,\n VexMutationOperation,\n VexRevalidateChange,\n VexRevalidateRequest,\n VexRevalidateResponse,\n} from \"@vexcms/core\";\nimport {\n hasPermission,\n PERMISSION_SCOPES,\n resolveTargets,\n VEX_REVALIDATE_BATCH_SIZE,\n} from \"@vexcms/core\";\nimport { revalidatePath } from \"next/cache\";\nimport type { NextRequest } from \"next/server\";\nimport { NextResponse } from \"next/server\";\n\n/** Props for {@link createVexRevalidateRoute}. */\nexport interface CreateVexRevalidateRouteProps {\n /**\n * The resolved VexCMS config. Supplies `access` for the write-permission\n * check and `routes.map` for target resolution.\n */\n config: VexConfig;\n /**\n * Resolves the authenticated user and active organization for the current\n * request. Wire to `() => fetchAuthQuery(api.auth.api.getUserOrg, {})` — the\n * same call the admin panel route already makes.\n */\n getAuth: () => Promise<{\n organization?: Record<string, unknown>;\n user: null | Record<string, unknown>;\n }>;\n /**\n * Reads the caller's session token for the current request. Wire to the host\n * app's `getToken` export (`convexBetterAuthNextJs`'s output, re-exported\n * from e.g. `~/auth/server`). A falsy return means \"no session\".\n */\n getToken: () => Promise<null | string | undefined>;\n /**\n * Lists every document in a collection, for the collection-wide purge\n * (`{ collection, all: true }`). Wire to a read through the app's own\n * `publishedSlugs`-style query — the factory cannot know an app's generated\n * Convex `api`, which is why this is injected like `getToken`/`getAuth`.\n *\n * Omit to leave the collection-wide branch unwired: it then purges nothing\n * and reports that in `errors`, rather than silently succeeding.\n */\n listCollection?: (collection: CollectionSlug) => Promise<Partial<VexDocument>[]>;\n}\n\n/**\n * Maps the wire verb to the CRUD action `hasPermission` and `resolveTargets`\n * accept.\n *\n * `VexMutationOperation` is named for the `vexConvexApi` function that produced\n * the write (`vexConvexApi.remove`), while `hasPermission` only accepts\n * `CRUD_ACTIONS`. Mapping happens exactly once per request so the same value\n * drives both the permission check and target resolution.\n *\n * @param operation - The wire operation from the request body.\n * @returns The equivalent CRUD action.\n */\nfunction toCrudAction(operation: VexMutationOperation): CrudWriteAction {\n if (operation === \"remove\") return \"delete\";\n if (operation === \"upsert\") return \"update\";\n return operation;\n}\n\n/**\n * Creates a Next.js route handler that purges the cached paths affected by a\n * document write. The admin panel calls this via `useVexMutation` after every\n * successful create/update/delete, same-origin, as a relative fetch — no URL\n * config, no shared secret.\n *\n * **Session-authorized, not secret-authorized.** The caller is a signed-in\n * admin, so this reuses `getToken`/`getAuth` exactly like the admin panel route\n * does, rather than introducing an env var or a shared secret. There is no\n * API-key fallback: a caller with no browser session cannot reach this route as\n * designed.\n *\n * **Path-based, not tag-based.** `revalidatePath` is the only purge mechanism.\n * `cacheComponents: true` — required for `cacheTag`/`revalidateTag` — is\n * incompatible with the `dynamic`/`runtime` segment configs the auth and admin\n * routes require (measured: 5 files fail to build with it enabled), so\n * `revalidateTag` is not an option in this codebase. Do not \"upgrade\" this to\n * tag-based revalidation without first removing that segment-config dependency\n * everywhere.\n *\n * **Never fails the caller past authorization.** A missing session returns 401,\n * a session without write permission 403, and an oversized `changes` batch 413\n * — all three reject before any map work runs. Past that point the handler\n * never throws or returns 5xx: an unconfigured map, a throwing map, or an\n * individual `revalidatePath` failure all resolve to 200 with the failures\n * reported in the body. A failed purge is a stale page; failing the endpoint\n * would only teach the fire-and-forget caller to retry pointlessly.\n *\n * @param props - Config and the host app's auth accessors.\n * @returns `{ POST }` — mount directly as the route module's named export.\n *\n * @example\n * ```ts\n * // app/api/vex/revalidate/route.ts\n * import { api } from \"@convex/_generated/api\";\n * import { createVexRevalidateRoute } from \"@vexcms/next/cache\";\n * import { fetchAuthQuery, getToken } from \"~/auth/server\";\n * import config from \"~/vex.config\";\n *\n * export const { POST } = createVexRevalidateRoute({\n * config,\n * getToken,\n * getAuth: () => fetchAuthQuery(api.auth.api.getUserOrg, {}),\n * });\n * ```\n */\nexport function createVexRevalidateRoute(props: CreateVexRevalidateRouteProps): {\n POST: (request: NextRequest) => Promise<NextResponse<{ error: string } | VexRevalidateResponse>>;\n} {\n return {\n async POST(\n request: NextRequest,\n ): Promise<NextResponse<{ error: string } | VexRevalidateResponse>> {\n const token = await props.getToken();\n if (token === null || token === undefined || token === \"\") {\n return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 });\n }\n\n let body: VexRevalidateRequest;\n try {\n body = (await request.json()) as VexRevalidateRequest;\n } catch {\n return NextResponse.json({ error: \"Bad Request\" }, { status: 400 });\n }\n\n const { organization, user } = await props.getAuth();\n\n // One mapping, used for BOTH the permission check and target resolution.\n const operation: VexMutationOperation = \"all\" in body ? \"update\" : body.operation;\n const action = toCrudAction(operation);\n\n const allowed = hasPermission({\n access: props.config.access,\n action,\n organization,\n resource: body.collection,\n user,\n // A purge writes no document fields — this asks whether the caller\n // may act on the collection at all, not against a specific payload.\n scope: PERMISSION_SCOPES.any,\n });\n if (!allowed) {\n return NextResponse.json({ error: \"Forbidden\" }, { status: 403 });\n }\n\n const errors: unknown[] = [];\n let changes: VexRevalidateChange[];\n\n if (\"all\" in body) {\n // Server-derived, so no client-supplied batch to bound. Each document\n // is mapped as its current state.\n if (props.listCollection === undefined) {\n errors.push(\n new Error(\n \"createVexRevalidateRoute: a collection-wide purge was requested but `listCollection` is not wired\",\n ),\n );\n changes = [];\n } else {\n try {\n const docs = await props.listCollection(body.collection);\n changes = docs.map((doc) => ({ after: doc }));\n } catch (error) {\n // An unreachable deployment must not 500 a purge request.\n errors.push(error);\n changes = [];\n }\n }\n } else {\n changes = body.changes;\n if (changes.length > VEX_REVALIDATE_BATCH_SIZE) {\n return NextResponse.json(\n {\n error: `Cannot revalidate more than ${VEX_REVALIDATE_BATCH_SIZE} changes in a single request`,\n },\n { status: 413 },\n );\n }\n }\n\n const routesConfig = props.config.routes;\n if (routesConfig === undefined) {\n // A project with no route map still gets a working, harmless\n // endpoint rather than a 500. Any error already accumulated (e.g. an\n // unwired collection-wide purge) is still reported.\n return NextResponse.json({ errors, revalidated: [] }, { status: 200 });\n }\n\n const pool: string[] = [];\n\n for (const change of changes) {\n // A throwing map is contained inside `resolveTargets` — its failure\n // lands in that call's `errors` and never propagates here.\n const target = resolveTargets({\n after: change.after,\n before: change.before,\n collection: body.collection,\n map: routesConfig.map,\n operation: action,\n });\n pool.push(...target.paths);\n errors.push(...target.errors);\n }\n\n // Dedupe across the whole batch, preserving first-seen order — the same\n // contract `resolveTargets` applies to one document's before/after.\n const revalidated: string[] = [];\n for (const path of new Set(pool)) {\n try {\n revalidatePath(path);\n revalidated.push(path);\n } catch (error) {\n // `revalidatePath` is a Next API called here, so it is not covered by\n // `resolveTargets`' containment.\n errors.push(error);\n }\n }\n\n return NextResponse.json({ errors, revalidated }, { status: 200 });\n },\n };\n}\n","import type { FunctionReference, OptionalRestArgs } from \"convex/server\";\n\nimport { ConvexHttpClient } from \"convex/browser\";\nimport { cache } from \"react\";\n\nimport type { VexServerClientOptions, VexServerClient } from \"./types\";\n\n/**\n * Creates a server-only Convex read client safe to use in prerenderable\n * Next.js routes.\n *\n * Wraps `ConvexHttpClient` from `convex/browser` instead of `convex/nextjs`'s\n * `fetchQuery`, which hard-codes `client.setFetchOptions({ cache: \"no-store\" })`\n * — the reason every route calling it builds dynamic (`ƒ`). This client never\n * sets that option, so a route calling only `query()` can be prerendered and\n * governed by the route's own `export const revalidate`.\n *\n * Every `query()` call is deduped via `React.cache` for the lifetime of one\n * request: two identical `(query, args)` calls — e.g. `generateMetadata` and\n * its page both reading the same document — resolve from a single Convex round\n * trip instead of two.\n *\n * `React.cache` is instantiated fresh inside every `createVexServerClient()`\n * call, so that dedupe only holds across calls sharing the SAME client\n * instance — two clients built in two different modules cannot share a round\n * trip even when they read identical `(query, args)`. Each app therefore\n * creates exactly one instance, exported from its own `src/lib/vex.ts`, and\n * every route or component imports that shared `vex` rather than calling\n * `createVexServerClient()` itself.\n *\n * @param props - Client configuration.\n * @returns A {@link VexServerClient} whose `query` method is deduped per request.\n * @throws {Error} When no `url` is given and `process.env.NEXT_PUBLIC_CONVEX_URL`\n * is unset.\n */\nexport function createVexServerClient(props: VexServerClientOptions = {}): VexServerClient {\n const url = props.url ?? process.env.NEXT_PUBLIC_CONVEX_URL;\n if (url === undefined || url === \"\") {\n throw new Error(\n \"createVexServerClient: no Convex deployment URL. Pass `url`, or set NEXT_PUBLIC_CONVEX_URL.\",\n );\n }\n\n const client = new ConvexHttpClient(url, {\n skipConvexDeploymentUrlCheck: props.skipConvexDeploymentUrlCheck,\n });\n\n // Deliberately NOT `client.setFetchOptions({ cache: \"no-store\" })` — that is\n // exactly what `convex/nextjs`'s `fetchQuery` does and what forces every\n // consuming route dynamic.\n\n // `React.cache` keys on the argument list, so the entry is (query reference,\n // args) — never the reference alone. Args are normalised to a single value so\n // `query(fn)` and `query(fn, {})` share one entry.\n const cachedQuery = cache(\n async (query: FunctionReference<\"query\">, args: unknown): Promise<unknown> =>\n client.query(query, args as OptionalRestArgs<FunctionReference<\"query\">>[0]),\n );\n\n return {\n query: async (query, ...args) => cachedQuery(query, args[0] ?? {}),\n };\n}\n"],"mappings":"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","names":[]}
|