@vexcms/next 0.1.0-alpha.11 → 0.1.0-alpha.13
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/README.md +46 -48
- package/dist/NextAdminLayout.js +3 -20
- package/dist/NextAdminLayout.js.map +1 -1
- package/dist/NextAdminPage.js +2 -137
- package/dist/NextAdminPage.js.map +1 -1
- package/dist/cache/createVexRevalidateRoute.d.ts +88 -0
- package/dist/cache/createVexRevalidateRoute.d.ts.map +1 -0
- package/dist/cache/createVexServerClient.d.ts +31 -0
- package/dist/cache/createVexServerClient.d.ts.map +1 -0
- package/dist/cache/index.d.ts +5 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +9 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/types.d.ts +36 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/chunk-5AGM3PEX.js +121 -0
- package/dist/chunk-5AGM3PEX.js.map +1 -0
- package/dist/chunk-KIRSFMPI.js +78 -0
- package/dist/chunk-KIRSFMPI.js.map +1 -0
- package/dist/chunk-TR3Z3P6Q.js +143 -0
- package/dist/chunk-TR3Z3P6Q.js.map +1 -0
- package/dist/chunk-YCJNIS6O.js +25 -0
- package/dist/chunk-YCJNIS6O.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/seo/createVexRobots.d.ts +22 -0
- package/dist/seo/createVexRobots.d.ts.map +1 -0
- package/dist/seo/createVexSitemap.d.ts +41 -0
- package/dist/seo/createVexSitemap.d.ts.map +1 -0
- package/dist/seo/index.d.ts +5 -0
- package/dist/seo/index.d.ts.map +1 -0
- package/dist/seo/index.js +13 -0
- package/dist/seo/index.js.map +1 -0
- package/dist/seo/vexMetadata.d.ts +30 -0
- package/dist/seo/vexMetadata.d.ts.map +1 -0
- package/dist/seo/vexStaticParams.d.ts +25 -0
- package/dist/seo/vexStaticParams.d.ts.map +1 -0
- package/package.json +19 -4
package/README.md
CHANGED
|
@@ -1,87 +1,85 @@
|
|
|
1
|
-
# @vexcms/
|
|
1
|
+
# @vexcms/next
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The Next.js adapter for [VEX CMS](https://github.com/ianyimi/vex). Provides the server and client components that render the admin panel — collection CRUD, media management, and role-based access control — plus config functions re-exported from `@vexcms/react`.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
pnpm add @vexcms/
|
|
8
|
+
pnpm add @vexcms/next@alpha
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## Quick Setup
|
|
12
12
|
|
|
13
13
|
```tsx
|
|
14
|
-
// app/admin/
|
|
15
|
-
import {
|
|
14
|
+
// app/admin/layout.tsx
|
|
15
|
+
import { NextAdminLayout } from "@vexcms/next/client"
|
|
16
16
|
import config from "@/vex.config"
|
|
17
|
+
import { getCurrentUser } from "@/auth/serverUtils"
|
|
17
18
|
|
|
18
|
-
export default function
|
|
19
|
+
export default async function AdminRootLayout({ children }: { children: React.ReactNode }) {
|
|
20
|
+
const user = await getCurrentUser()
|
|
19
21
|
return (
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
</
|
|
22
|
+
<NextAdminLayout config={config} user={user ?? undefined}>
|
|
23
|
+
{children}
|
|
24
|
+
</NextAdminLayout>
|
|
23
25
|
)
|
|
24
26
|
}
|
|
25
27
|
```
|
|
26
28
|
|
|
29
|
+
```tsx
|
|
30
|
+
// app/admin/[[...path]]/page.tsx
|
|
31
|
+
import { NextAdminPage } from "@vexcms/next/server"
|
|
32
|
+
import config from "@/vex.config"
|
|
33
|
+
|
|
34
|
+
export default function AdminPage({
|
|
35
|
+
params,
|
|
36
|
+
}: {
|
|
37
|
+
params: Promise<{ path?: string[] }>
|
|
38
|
+
}) {
|
|
39
|
+
return <NextAdminPage config={config} params={params} />
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`NextAdminPage` and `NextAdminLayout` are exported from separate subpaths (`/server` and `/client`) rather than the package root, because an async server component and a `"use client"` component cannot share one bundle.
|
|
44
|
+
|
|
27
45
|
## Features
|
|
28
46
|
|
|
29
47
|
### Collection Management
|
|
30
48
|
|
|
31
|
-
- **List view** —
|
|
32
|
-
- **Edit view** — Auto-generated forms from collection schema
|
|
33
|
-
- **Create
|
|
34
|
-
- **Bidirectional pagination** — Instant access to first and last pages
|
|
49
|
+
- **List view** — Data table with Load More pagination, row selection, and bulk delete
|
|
50
|
+
- **Edit view** — Auto-generated forms from the collection schema, submitted via TanStack React Form
|
|
51
|
+
- **Create dialog** — Modal for document creation, gated by a `create` permission check
|
|
35
52
|
|
|
36
53
|
### Media Management
|
|
37
54
|
|
|
38
55
|
- **Grid/list views** — Browse media files with thumbnails
|
|
39
|
-
- **Upload** — Drag-and-drop upload
|
|
40
|
-
- **Media picker** — Inline picker for upload
|
|
41
|
-
- **Image dimensions** — Auto-detected on upload
|
|
42
|
-
|
|
43
|
-
### Versioning & Drafts
|
|
44
|
-
|
|
45
|
-
- **Draft/publish workflow** — Status badges, publish/unpublish actions
|
|
46
|
-
- **Version history** — Dropdown to view, restore, or delete previous versions
|
|
47
|
-
- **Autosave** — Configurable auto-save interval for draft documents
|
|
56
|
+
- **Upload** — Drag-and-drop or click-to-upload through the collection's configured storage adapter
|
|
57
|
+
- **Media picker** — Inline picker for upload fields, with a client-side search filter and Load More pagination
|
|
48
58
|
|
|
49
|
-
###
|
|
59
|
+
### Roadmap note
|
|
50
60
|
|
|
51
|
-
|
|
52
|
-
- **Responsive breakpoints** — Test at different screen sizes
|
|
53
|
-
- **Snapshot system** — Transient preview data without saving to database
|
|
61
|
+
Versioning, drafts, and live preview are not implemented yet — both are in progress on the [roadmap](https://docs.vexcms.dev). A global's `versions.drafts` option parses and is stored on the resolved config, but it is not enforced: every read still returns the live document. See [docs.vexcms.dev](https://docs.vexcms.dev) for current status.
|
|
54
62
|
|
|
55
63
|
### Access Control
|
|
56
64
|
|
|
57
|
-
- **
|
|
58
|
-
- **
|
|
59
|
-
- **
|
|
60
|
-
- **UI enforcement** — Buttons and actions hidden/disabled based on permissions
|
|
61
|
-
|
|
62
|
-
### User Impersonation
|
|
63
|
-
|
|
64
|
-
- **Impersonation banner** — Shows when an admin is viewing as another user
|
|
65
|
-
- **User switching** — Select from impersonatable users list
|
|
65
|
+
- **Access provider** — `VexAccessProvider` supplies the RBAC access matrix to the admin panel via React context
|
|
66
|
+
- **Collection-level permissions** — `usePermission` gates create, read, update, and delete per collection and per global
|
|
67
|
+
- **UI enforcement** — Buttons and actions are disabled or hidden when the current user lacks permission
|
|
66
68
|
|
|
67
69
|
### Admin Layout
|
|
68
70
|
|
|
69
|
-
- **Sidebar navigation** —
|
|
70
|
-
- **Theme support** — Light
|
|
71
|
-
- **User menu** — Profile, sign out, impersonation controls
|
|
71
|
+
- **Sidebar navigation** — Links grouped into Collections, Globals, and Media sections
|
|
72
|
+
- **Theme support** — Light, dark, and system modes via `ThemeToggle`
|
|
72
73
|
|
|
73
74
|
## Exports
|
|
74
75
|
|
|
75
|
-
| Export | Description |
|
|
76
|
-
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
| `usePermissionContext` | Hook to access the full permission context |
|
|
83
|
-
| `ImpersonationBanner` | Banner component for admin impersonation |
|
|
84
|
-
| `useMediaPickerState` | Hook for media picker state management |
|
|
76
|
+
| Export | From | Description |
|
|
77
|
+
|--------|------|-------------|
|
|
78
|
+
| `NextAdminPage` | `@vexcms/next/server` | Async server component that routes the admin panel by URL path (dashboard, list, edit) |
|
|
79
|
+
| `NextAdminLayout` | `@vexcms/next/client` | Client component rendering the admin shell (sidebar, theme, breadcrumbs) around page content |
|
|
80
|
+
| `defineConfig`, `defineCollection`, `relationship`, `text`, `number`, `select`, `date`, `url`, `checkbox` | `@vexcms/next` | Config functions re-exported from `@vexcms/react` |
|
|
81
|
+
|
|
82
|
+
The compiled stylesheet is available at `@vexcms/next/styles`.
|
|
85
83
|
|
|
86
84
|
## Peer Dependencies
|
|
87
85
|
|
package/dist/NextAdminLayout.js
CHANGED
|
@@ -1,23 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { cookies } from "next/headers";
|
|
5
|
-
import { jsx } from "react/jsx-runtime";
|
|
6
|
-
async function NextAdminLayout(props) {
|
|
7
|
-
const clientConfig = sanitizeConfigForClient(props.config);
|
|
8
|
-
const cookieStore = await cookies();
|
|
9
|
-
const sidebarOpen = String(cookieStore.get("sidebar_state")?.value) === "true";
|
|
10
|
-
return /* @__PURE__ */ jsx(
|
|
11
|
-
NextAdminLayoutClient,
|
|
12
|
-
{
|
|
13
|
-
config: clientConfig,
|
|
14
|
-
user: props.user,
|
|
15
|
-
organization: props.organization,
|
|
16
|
-
sidebarOpen,
|
|
17
|
-
children: props.children
|
|
18
|
-
}
|
|
19
|
-
);
|
|
20
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
NextAdminLayout
|
|
3
|
+
} from "./chunk-YCJNIS6O.js";
|
|
21
4
|
export {
|
|
22
5
|
NextAdminLayout
|
|
23
6
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/NextAdminPage.js
CHANGED
|
@@ -1,141 +1,6 @@
|
|
|
1
|
-
// src/NextAdminPage.tsx
|
|
2
|
-
import { fetchQuery } from "convex/nextjs";
|
|
3
|
-
import { vexConvexApi } from "@vexcms/core";
|
|
4
1
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
CollectionEditView,
|
|
8
|
-
MediaCollectionEditView,
|
|
9
|
-
MediaCollectionListView,
|
|
10
|
-
GlobalsListView,
|
|
11
|
-
GlobalEditView
|
|
12
|
-
} from "@vexcms/react";
|
|
13
|
-
import { sanitizeConfigForClient } from "@vexcms/core";
|
|
14
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
-
async function NextAdminPage(props) {
|
|
16
|
-
const { path = [] } = await props.params;
|
|
17
|
-
const [collectionSlug, documentId] = path;
|
|
18
|
-
const clientConfig = sanitizeConfigForClient(props.config);
|
|
19
|
-
if (!collectionSlug) {
|
|
20
|
-
return /* @__PURE__ */ jsx(DashboardView, { config: clientConfig });
|
|
21
|
-
}
|
|
22
|
-
if (collectionSlug === "globals") {
|
|
23
|
-
if (!documentId) {
|
|
24
|
-
return /* @__PURE__ */ jsx(GlobalsListView, { config: clientConfig });
|
|
25
|
-
}
|
|
26
|
-
const globalConfig = clientConfig.globals.find((g) => g.slug === documentId);
|
|
27
|
-
if (!globalConfig) {
|
|
28
|
-
return /* @__PURE__ */ jsxs("div", { children: [
|
|
29
|
-
/* @__PURE__ */ jsxs("p", { className: "text-muted-foreground p-6", children: [
|
|
30
|
-
'Global "',
|
|
31
|
-
documentId,
|
|
32
|
-
'" not found.'
|
|
33
|
-
] }),
|
|
34
|
-
/* @__PURE__ */ jsx("p", { children: "TODO: add not found view" })
|
|
35
|
-
] });
|
|
36
|
-
}
|
|
37
|
-
const global = await fetchQuery(
|
|
38
|
-
vexConvexApi.globals.get,
|
|
39
|
-
{ slug: globalConfig.slug },
|
|
40
|
-
props.token ? { token: props.token } : void 0
|
|
41
|
-
);
|
|
42
|
-
return /* @__PURE__ */ jsx(GlobalEditView, { global: globalConfig, initialData: global });
|
|
43
|
-
}
|
|
44
|
-
const collection = clientConfig.collections.find((c) => c.slug === collectionSlug);
|
|
45
|
-
const mediaCollection = clientConfig.mediaCollections.find((mc) => mc.slug === collectionSlug);
|
|
46
|
-
if (!collection && !mediaCollection) {
|
|
47
|
-
return /* @__PURE__ */ jsxs("div", { children: [
|
|
48
|
-
/* @__PURE__ */ jsxs("p", { className: "text-muted-foreground p-6", children: [
|
|
49
|
-
'Collection "',
|
|
50
|
-
collectionSlug,
|
|
51
|
-
'" not found.'
|
|
52
|
-
] }),
|
|
53
|
-
/* @__PURE__ */ jsx("p", { children: "TODO: add not found view" })
|
|
54
|
-
] });
|
|
55
|
-
}
|
|
56
|
-
if (mediaCollection && documentId) {
|
|
57
|
-
const initialData2 = await fetchQuery(
|
|
58
|
-
vexConvexApi.get,
|
|
59
|
-
{
|
|
60
|
-
id: documentId,
|
|
61
|
-
collection: mediaCollection.slug
|
|
62
|
-
},
|
|
63
|
-
props.token ? { token: props.token } : void 0
|
|
64
|
-
);
|
|
65
|
-
return /* @__PURE__ */ jsx(
|
|
66
|
-
MediaCollectionEditView,
|
|
67
|
-
{
|
|
68
|
-
collection: mediaCollection,
|
|
69
|
-
documentId,
|
|
70
|
-
initialData: initialData2
|
|
71
|
-
}
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
if (mediaCollection) {
|
|
75
|
-
const numItems2 = Math.max(
|
|
76
|
-
mediaCollection.admin.table.serverPageSize,
|
|
77
|
-
mediaCollection.admin.table.defaultPageSize
|
|
78
|
-
);
|
|
79
|
-
const initialData2 = await fetchQuery(
|
|
80
|
-
vexConvexApi.find,
|
|
81
|
-
{
|
|
82
|
-
collection: collectionSlug,
|
|
83
|
-
paginationOpts: {
|
|
84
|
-
numItems: numItems2,
|
|
85
|
-
totalDocs: true,
|
|
86
|
-
cursor: null
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
props.token ? { token: props.token } : void 0
|
|
90
|
-
);
|
|
91
|
-
return /* @__PURE__ */ jsx(
|
|
92
|
-
MediaCollectionListView,
|
|
93
|
-
{
|
|
94
|
-
collection: mediaCollection,
|
|
95
|
-
initialData: initialData2
|
|
96
|
-
}
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
if (collection && documentId) {
|
|
100
|
-
const initialData2 = await fetchQuery(
|
|
101
|
-
vexConvexApi.get,
|
|
102
|
-
{
|
|
103
|
-
id: documentId,
|
|
104
|
-
collection: collection.slug
|
|
105
|
-
},
|
|
106
|
-
props.token ? { token: props.token } : void 0
|
|
107
|
-
);
|
|
108
|
-
return /* @__PURE__ */ jsx(
|
|
109
|
-
CollectionEditView,
|
|
110
|
-
{
|
|
111
|
-
collection,
|
|
112
|
-
documentId,
|
|
113
|
-
initialData: initialData2
|
|
114
|
-
}
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
if (!collection) {
|
|
118
|
-
throw new Error("invalid collection slug");
|
|
119
|
-
}
|
|
120
|
-
const numItems = Math.max(
|
|
121
|
-
collection.admin.table.serverPageSize,
|
|
122
|
-
collection.admin.table.defaultPageSize
|
|
123
|
-
);
|
|
124
|
-
const initialData = await fetchQuery(
|
|
125
|
-
vexConvexApi.findPaginated,
|
|
126
|
-
{
|
|
127
|
-
collection: collectionSlug,
|
|
128
|
-
depth: 1,
|
|
129
|
-
paginationOpts: {
|
|
130
|
-
cursor: null,
|
|
131
|
-
numItems,
|
|
132
|
-
totalDocs: true
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
props.token ? { token: props.token } : void 0
|
|
136
|
-
);
|
|
137
|
-
return /* @__PURE__ */ jsx(CollectionListView, { collection, initialData });
|
|
138
|
-
}
|
|
2
|
+
NextAdminPage
|
|
3
|
+
} from "./chunk-TR3Z3P6Q.js";
|
|
139
4
|
export {
|
|
140
5
|
NextAdminPage
|
|
141
6
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { CollectionSlug, VexConfig, VexDocument, VexRevalidateResponse } from "@vexcms/core";
|
|
2
|
+
import type { NextRequest } from "next/server";
|
|
3
|
+
import { NextResponse } from "next/server";
|
|
4
|
+
/** Props for {@link createVexRevalidateRoute}. */
|
|
5
|
+
export interface CreateVexRevalidateRouteProps {
|
|
6
|
+
/**
|
|
7
|
+
* The resolved VexCMS config. Supplies `access` for the write-permission
|
|
8
|
+
* check and `routes.map` for target resolution.
|
|
9
|
+
*/
|
|
10
|
+
config: VexConfig;
|
|
11
|
+
/**
|
|
12
|
+
* Resolves the authenticated user and active organization for the current
|
|
13
|
+
* request. Wire to `() => fetchAuthQuery(api.auth.api.getUserOrg, {})` — the
|
|
14
|
+
* same call the admin panel route already makes.
|
|
15
|
+
*/
|
|
16
|
+
getAuth: () => Promise<{
|
|
17
|
+
organization?: Record<string, unknown>;
|
|
18
|
+
user: null | Record<string, unknown>;
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* Reads the caller's session token for the current request. Wire to the host
|
|
22
|
+
* app's `getToken` export (`convexBetterAuthNextJs`'s output, re-exported
|
|
23
|
+
* from e.g. `~/auth/server`). A falsy return means "no session".
|
|
24
|
+
*/
|
|
25
|
+
getToken: () => Promise<null | string | undefined>;
|
|
26
|
+
/**
|
|
27
|
+
* Lists every document in a collection, for the collection-wide purge
|
|
28
|
+
* (`{ collection, all: true }`). Wire to a read through the app's own
|
|
29
|
+
* `publishedSlugs`-style query — the factory cannot know an app's generated
|
|
30
|
+
* Convex `api`, which is why this is injected like `getToken`/`getAuth`.
|
|
31
|
+
*
|
|
32
|
+
* Omit to leave the collection-wide branch unwired: it then purges nothing
|
|
33
|
+
* and reports that in `errors`, rather than silently succeeding.
|
|
34
|
+
*/
|
|
35
|
+
listCollection?: (collection: CollectionSlug) => Promise<Partial<VexDocument>[]>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Creates a Next.js route handler that purges the cached paths affected by a
|
|
39
|
+
* document write. The admin panel calls this via `useVexMutation` after every
|
|
40
|
+
* successful create/update/delete, same-origin, as a relative fetch — no URL
|
|
41
|
+
* config, no shared secret.
|
|
42
|
+
*
|
|
43
|
+
* **Session-authorized, not secret-authorized.** The caller is a signed-in
|
|
44
|
+
* admin, so this reuses `getToken`/`getAuth` exactly like the admin panel route
|
|
45
|
+
* does, rather than introducing an env var or a shared secret. There is no
|
|
46
|
+
* API-key fallback: a caller with no browser session cannot reach this route as
|
|
47
|
+
* designed.
|
|
48
|
+
*
|
|
49
|
+
* **Path-based, not tag-based.** `revalidatePath` is the only purge mechanism.
|
|
50
|
+
* `cacheComponents: true` — required for `cacheTag`/`revalidateTag` — is
|
|
51
|
+
* incompatible with the `dynamic`/`runtime` segment configs the auth and admin
|
|
52
|
+
* routes require (measured: 5 files fail to build with it enabled), so
|
|
53
|
+
* `revalidateTag` is not an option in this codebase. Do not "upgrade" this to
|
|
54
|
+
* tag-based revalidation without first removing that segment-config dependency
|
|
55
|
+
* everywhere.
|
|
56
|
+
*
|
|
57
|
+
* **Never fails the caller past authorization.** A missing session returns 401,
|
|
58
|
+
* a session without write permission 403, and an oversized `changes` batch 413
|
|
59
|
+
* — all three reject before any map work runs. Past that point the handler
|
|
60
|
+
* never throws or returns 5xx: an unconfigured map, a throwing map, or an
|
|
61
|
+
* individual `revalidatePath` failure all resolve to 200 with the failures
|
|
62
|
+
* reported in the body. A failed purge is a stale page; failing the endpoint
|
|
63
|
+
* would only teach the fire-and-forget caller to retry pointlessly.
|
|
64
|
+
*
|
|
65
|
+
* @param props - Config and the host app's auth accessors.
|
|
66
|
+
* @returns `{ POST }` — mount directly as the route module's named export.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* // app/api/vex/revalidate/route.ts
|
|
71
|
+
* import { api } from "@convex/_generated/api";
|
|
72
|
+
* import { createVexRevalidateRoute } from "@vexcms/next/cache";
|
|
73
|
+
* import { fetchAuthQuery, getToken } from "~/auth/server";
|
|
74
|
+
* import config from "~/vex.config";
|
|
75
|
+
*
|
|
76
|
+
* export const { POST } = createVexRevalidateRoute({
|
|
77
|
+
* config,
|
|
78
|
+
* getToken,
|
|
79
|
+
* getAuth: () => fetchAuthQuery(api.auth.api.getUserOrg, {}),
|
|
80
|
+
* });
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export declare function createVexRevalidateRoute(props: CreateVexRevalidateRouteProps): {
|
|
84
|
+
POST: (request: NextRequest) => Promise<NextResponse<{
|
|
85
|
+
error: string;
|
|
86
|
+
} | VexRevalidateResponse>>;
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=createVexRevalidateRoute.d.ts.map
|
|
@@ -0,0 +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;AAGtB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/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;AAoBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,6BAA6B,GAAG;IAC9E,IAAI,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,qBAAqB,CAAC,CAAC,CAAC;CAClG,CA8GA"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { VexServerClientOptions, VexServerClient } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Creates a server-only Convex read client safe to use in prerenderable
|
|
4
|
+
* Next.js routes.
|
|
5
|
+
*
|
|
6
|
+
* Wraps `ConvexHttpClient` from `convex/browser` instead of `convex/nextjs`'s
|
|
7
|
+
* `fetchQuery`, which hard-codes `client.setFetchOptions({ cache: "no-store" })`
|
|
8
|
+
* — the reason every route calling it builds dynamic (`ƒ`). This client never
|
|
9
|
+
* sets that option, so a route calling only `query()` can be prerendered and
|
|
10
|
+
* governed by the route's own `export const revalidate`.
|
|
11
|
+
*
|
|
12
|
+
* Every `query()` call is deduped via `React.cache` for the lifetime of one
|
|
13
|
+
* request: two identical `(query, args)` calls — e.g. `generateMetadata` and
|
|
14
|
+
* its page both reading the same document — resolve from a single Convex round
|
|
15
|
+
* trip instead of two.
|
|
16
|
+
*
|
|
17
|
+
* `React.cache` is instantiated fresh inside every `createVexServerClient()`
|
|
18
|
+
* call, so that dedupe only holds across calls sharing the SAME client
|
|
19
|
+
* instance — two clients built in two different modules cannot share a round
|
|
20
|
+
* trip even when they read identical `(query, args)`. Each app therefore
|
|
21
|
+
* creates exactly one instance, exported from its own `src/lib/vex.ts`, and
|
|
22
|
+
* every route or component imports that shared `vex` rather than calling
|
|
23
|
+
* `createVexServerClient()` itself.
|
|
24
|
+
*
|
|
25
|
+
* @param props - Client configuration.
|
|
26
|
+
* @returns A {@link VexServerClient} whose `query` method is deduped per request.
|
|
27
|
+
* @throws {Error} When no `url` is given and `process.env.NEXT_PUBLIC_CONVEX_URL`
|
|
28
|
+
* is unset.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createVexServerClient(props?: VexServerClientOptions): VexServerClient;
|
|
31
|
+
//# sourceMappingURL=createVexServerClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createVexServerClient.d.ts","sourceRoot":"","sources":["../../src/cache/createVexServerClient.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,GAAE,sBAA2B,GAAG,eAAe,CA2BzF"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createVexRevalidateRoute } from "./createVexRevalidateRoute";
|
|
2
|
+
export type { CreateVexRevalidateRouteProps } from "./createVexRevalidateRoute";
|
|
3
|
+
export { createVexServerClient } from "./createVexServerClient";
|
|
4
|
+
export type { VexServerClientOptions, VexServerClient, } from "./types";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AACtE,YAAY,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAC;AAChF,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAOhE,YAAY,EACV,sBAAsB,EACtB,eAAe,GAChB,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { FunctionReference, FunctionReturnType, OptionalRestArgs } from "convex/server";
|
|
2
|
+
/**
|
|
3
|
+
* Construction options for {@link createVexServerClient}.
|
|
4
|
+
*
|
|
5
|
+
* Nothing here configures caching — the client's only cache behaviour is that
|
|
6
|
+
* it never sets `cache: "no-store"` and dedupes per request via `React.cache`.
|
|
7
|
+
* Cache *policy* lives where Next requires it: `export const revalidate` in
|
|
8
|
+
* each route.
|
|
9
|
+
*/
|
|
10
|
+
export interface VexServerClientOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Skip Convex's deployment URL format check — for self-hosted backends whose
|
|
13
|
+
* URL does not match `https://<name>.convex.cloud`.
|
|
14
|
+
*/
|
|
15
|
+
skipConvexDeploymentUrlCheck?: boolean;
|
|
16
|
+
/** Convex deployment URL. Defaults to `process.env.NEXT_PUBLIC_CONVEX_URL`. */
|
|
17
|
+
url?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Server-only Convex read client returned by {@link createVexServerClient}.
|
|
21
|
+
*
|
|
22
|
+
* Exposes only `query` — this client is for prerenderable read paths, and it
|
|
23
|
+
* never sets `cache: "no-store"`, unlike `convex/nextjs`'s `fetchQuery`.
|
|
24
|
+
*/
|
|
25
|
+
export interface VexServerClient {
|
|
26
|
+
/**
|
|
27
|
+
* Runs a Convex query, deduped via `React.cache` for the lifetime of one
|
|
28
|
+
* request when called again with the same function reference and args.
|
|
29
|
+
*
|
|
30
|
+
* @param query - The Convex query function reference to call.
|
|
31
|
+
* @param args - Arguments for `query`, per Convex's `OptionalRestArgs`.
|
|
32
|
+
* @returns The query's resolved return value.
|
|
33
|
+
*/
|
|
34
|
+
query<Query extends FunctionReference<"query">>(query: Query, ...args: OptionalRestArgs<Query>): Promise<FunctionReturnType<Query>>;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/cache/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAE7F;;;;;;;GAOG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,4BAA4B,CAAC,EAAE,OAAO,CAAC;IACvC,+EAA+E;IAC/E,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,KAAK,CAAC,KAAK,SAAS,iBAAiB,CAAC,OAAO,CAAC,EAC5C,KAAK,EAAE,KAAK,EACZ,GAAG,IAAI,EAAE,gBAAgB,CAAC,KAAK,CAAC,GAC/B,OAAO,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC;CACvC"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// src/cache/createVexRevalidateRoute.ts
|
|
2
|
+
import { hasPermission, resolveTargets, VEX_REVALIDATE_BATCH_SIZE } from "@vexcms/core";
|
|
3
|
+
import { revalidatePath } from "next/cache";
|
|
4
|
+
import { NextResponse } from "next/server";
|
|
5
|
+
function toCrudAction(operation) {
|
|
6
|
+
if (operation === "remove") return "delete";
|
|
7
|
+
if (operation === "upsert") return "update";
|
|
8
|
+
return operation;
|
|
9
|
+
}
|
|
10
|
+
function createVexRevalidateRoute(props) {
|
|
11
|
+
return {
|
|
12
|
+
async POST(request) {
|
|
13
|
+
const token = await props.getToken();
|
|
14
|
+
if (token === null || token === void 0 || token === "") {
|
|
15
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
16
|
+
}
|
|
17
|
+
let body;
|
|
18
|
+
try {
|
|
19
|
+
body = await request.json();
|
|
20
|
+
} catch {
|
|
21
|
+
return NextResponse.json({ error: "Bad Request" }, { status: 400 });
|
|
22
|
+
}
|
|
23
|
+
const { organization, user } = await props.getAuth();
|
|
24
|
+
const operation = "all" in body ? "update" : body.operation;
|
|
25
|
+
const action = toCrudAction(operation);
|
|
26
|
+
const allowed = hasPermission({
|
|
27
|
+
access: props.config.access,
|
|
28
|
+
action,
|
|
29
|
+
organization,
|
|
30
|
+
resource: body.collection,
|
|
31
|
+
user
|
|
32
|
+
});
|
|
33
|
+
if (!allowed) {
|
|
34
|
+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
35
|
+
}
|
|
36
|
+
const errors = [];
|
|
37
|
+
let changes;
|
|
38
|
+
if ("all" in body) {
|
|
39
|
+
if (props.listCollection === void 0) {
|
|
40
|
+
errors.push(
|
|
41
|
+
new Error(
|
|
42
|
+
"createVexRevalidateRoute: a collection-wide purge was requested but `listCollection` is not wired"
|
|
43
|
+
)
|
|
44
|
+
);
|
|
45
|
+
changes = [];
|
|
46
|
+
} else {
|
|
47
|
+
try {
|
|
48
|
+
const docs = await props.listCollection(body.collection);
|
|
49
|
+
changes = docs.map((doc) => ({ after: doc }));
|
|
50
|
+
} catch (error) {
|
|
51
|
+
errors.push(error);
|
|
52
|
+
changes = [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
changes = body.changes;
|
|
57
|
+
if (changes.length > VEX_REVALIDATE_BATCH_SIZE) {
|
|
58
|
+
return NextResponse.json(
|
|
59
|
+
{
|
|
60
|
+
error: `Cannot revalidate more than ${VEX_REVALIDATE_BATCH_SIZE} changes in a single request`
|
|
61
|
+
},
|
|
62
|
+
{ status: 413 }
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const routesConfig = props.config.routes;
|
|
67
|
+
if (routesConfig === void 0) {
|
|
68
|
+
return NextResponse.json({ errors, revalidated: [] }, { status: 200 });
|
|
69
|
+
}
|
|
70
|
+
const pool = [];
|
|
71
|
+
for (const change of changes) {
|
|
72
|
+
const target = resolveTargets({
|
|
73
|
+
after: change.after,
|
|
74
|
+
before: change.before,
|
|
75
|
+
collection: body.collection,
|
|
76
|
+
map: routesConfig.map,
|
|
77
|
+
operation: action
|
|
78
|
+
});
|
|
79
|
+
pool.push(...target.paths);
|
|
80
|
+
errors.push(...target.errors);
|
|
81
|
+
}
|
|
82
|
+
const revalidated = [];
|
|
83
|
+
for (const path of new Set(pool)) {
|
|
84
|
+
try {
|
|
85
|
+
revalidatePath(path);
|
|
86
|
+
revalidated.push(path);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
errors.push(error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return NextResponse.json({ errors, revalidated }, { status: 200 });
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/cache/createVexServerClient.ts
|
|
97
|
+
import { ConvexHttpClient } from "convex/browser";
|
|
98
|
+
import { cache } from "react";
|
|
99
|
+
function createVexServerClient(props = {}) {
|
|
100
|
+
const url = props.url ?? process.env.NEXT_PUBLIC_CONVEX_URL;
|
|
101
|
+
if (url === void 0 || url === "") {
|
|
102
|
+
throw new Error(
|
|
103
|
+
"createVexServerClient: no Convex deployment URL. Pass `url`, or set NEXT_PUBLIC_CONVEX_URL."
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const client = new ConvexHttpClient(url, {
|
|
107
|
+
skipConvexDeploymentUrlCheck: props.skipConvexDeploymentUrlCheck
|
|
108
|
+
});
|
|
109
|
+
const cachedQuery = cache(
|
|
110
|
+
async (query, args) => client.query(query, args)
|
|
111
|
+
);
|
|
112
|
+
return {
|
|
113
|
+
query: async (query, ...args) => cachedQuery(query, args[0] ?? {})
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export {
|
|
118
|
+
createVexRevalidateRoute,
|
|
119
|
+
createVexServerClient
|
|
120
|
+
};
|
|
121
|
+
//# sourceMappingURL=chunk-5AGM3PEX.js.map
|