@questpie/next 0.0.1
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/.turbo/turbo-build.log +14 -0
- package/.turbo/turbo-check-types.log +1 -0
- package/CHANGELOG.md +9 -0
- package/README.md +287 -0
- package/dist/server.d.mts +15 -0
- package/dist/server.mjs +34 -0
- package/package.json +29 -0
- package/src/server.ts +48 -0
- package/tsconfig.json +5 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsdown.config.ts +10 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
[0m[2m[35m$[0m [2m[1mtsdown[0m
|
|
3
|
+
[34mℹ[39m tsdown [2mv0.18.4[22m powered by rolldown [2mv1.0.0-beta.57[22m
|
|
4
|
+
[34mℹ[39m config file: [4m/Users/drepkovsky/questpie/repos/questpie-cms/packages/next/tsdown.config.ts[24m
|
|
5
|
+
(node:61187) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
|
|
6
|
+
(Use `node --trace-warnings ...` to show where the warning was created)
|
|
7
|
+
[34mℹ[39m entry: [34msrc/server.ts[39m
|
|
8
|
+
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
9
|
+
[34mℹ[39m Build start
|
|
10
|
+
[34mℹ[39m Cleaning 2 files
|
|
11
|
+
[34mℹ[39m [2mdist/[22m[1mserver.mjs[22m [2m0.84 kB[22m [2m│ gzip: 0.43 kB[22m
|
|
12
|
+
[34mℹ[39m [2mdist/[22m[32m[1mserver.d.mts[22m[39m [2m0.61 kB[22m [2m│ gzip: 0.31 kB[22m
|
|
13
|
+
[34mℹ[39m 2 files, total: 1.46 kB
|
|
14
|
+
[32m✔[39m Build complete in [32m5270ms[39m
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
$ tsc --noEmit
|
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# @questpie/next
|
|
2
|
+
|
|
3
|
+
Next.js App Router adapter for QUESTPIE CMS.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **App Router Support** - Catch-all route handlers for Next.js 13+
|
|
8
|
+
- **Type-Safe Client** - Full TypeScript support with `@questpie/cms/client`
|
|
9
|
+
- **Server Components** - Works with React Server Components
|
|
10
|
+
- **Edge Runtime** - Compatible with Edge Runtime (with limitations)
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bun add @questpie/next @questpie/cms
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
### 1. Configure CMS
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// src/cms/index.ts
|
|
24
|
+
import { defineQCMS } from "@questpie/cms";
|
|
25
|
+
|
|
26
|
+
export const cms = defineQCMS()
|
|
27
|
+
.db({ connectionString: process.env.DATABASE_URL! })
|
|
28
|
+
.collections({
|
|
29
|
+
/* your collections */
|
|
30
|
+
})
|
|
31
|
+
.auth({
|
|
32
|
+
baseURL: process.env.NEXT_PUBLIC_URL!,
|
|
33
|
+
secret: process.env.AUTH_SECRET!,
|
|
34
|
+
})
|
|
35
|
+
.build();
|
|
36
|
+
|
|
37
|
+
export type AppCMS = typeof cms;
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. Create Route Handler
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// app/api/cms/[...path]/route.ts
|
|
44
|
+
import { questpieNextRouteHandlers } from "@questpie/next";
|
|
45
|
+
import { cms } from "@/cms";
|
|
46
|
+
|
|
47
|
+
export const { GET, POST, PUT, PATCH, DELETE } = questpieNextRouteHandlers(
|
|
48
|
+
cms,
|
|
49
|
+
{
|
|
50
|
+
basePath: "/api/cms",
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Enable dynamic rendering
|
|
55
|
+
export const dynamic = "force-dynamic";
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 3. Use the Client
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// src/lib/cms-client.ts
|
|
62
|
+
import { createQCMSClient } from "@questpie/cms/client";
|
|
63
|
+
import type { AppCMS } from "@/cms";
|
|
64
|
+
|
|
65
|
+
export const cmsClient = createQCMSClient<AppCMS>({
|
|
66
|
+
baseURL: process.env.NEXT_PUBLIC_URL!,
|
|
67
|
+
basePath: "/api/cms",
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Configuration Options
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
questpieNextRouteHandlers(cms, {
|
|
75
|
+
// Base path for CMS routes (must match your route location)
|
|
76
|
+
basePath: "/api/cms",
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## API Routes
|
|
81
|
+
|
|
82
|
+
The adapter creates the following routes under your base path:
|
|
83
|
+
|
|
84
|
+
### Collections
|
|
85
|
+
|
|
86
|
+
| Method | Route | Description |
|
|
87
|
+
| ------ | ---------------------------------------- | -------------------- |
|
|
88
|
+
| GET | `/api/cms/collections/:name` | List items |
|
|
89
|
+
| POST | `/api/cms/collections/:name` | Create item |
|
|
90
|
+
| GET | `/api/cms/collections/:name/:id` | Get item |
|
|
91
|
+
| PATCH | `/api/cms/collections/:name/:id` | Update item |
|
|
92
|
+
| DELETE | `/api/cms/collections/:name/:id` | Delete item |
|
|
93
|
+
| POST | `/api/cms/collections/:name/:id/restore` | Restore soft-deleted |
|
|
94
|
+
|
|
95
|
+
### Globals
|
|
96
|
+
|
|
97
|
+
| Method | Route | Description |
|
|
98
|
+
| ------ | ------------------------ | ------------- |
|
|
99
|
+
| GET | `/api/cms/globals/:name` | Get global |
|
|
100
|
+
| PATCH | `/api/cms/globals/:name` | Update global |
|
|
101
|
+
|
|
102
|
+
### Storage
|
|
103
|
+
|
|
104
|
+
| Method | Route | Description |
|
|
105
|
+
| ------ | ------------------------- | ----------- |
|
|
106
|
+
| POST | `/api/cms/storage/upload` | Upload file |
|
|
107
|
+
|
|
108
|
+
### Authentication
|
|
109
|
+
|
|
110
|
+
| Method | Route | Description |
|
|
111
|
+
| ------ | ----------------- | ------------------ |
|
|
112
|
+
| ALL | `/api/cms/auth/*` | Better Auth routes |
|
|
113
|
+
|
|
114
|
+
### Realtime
|
|
115
|
+
|
|
116
|
+
| Method | Route | Description |
|
|
117
|
+
| ------ | -------------------------------------- | ---------------- |
|
|
118
|
+
| GET | `/api/cms/collections/:name/subscribe` | SSE subscription |
|
|
119
|
+
| GET | `/api/cms/globals/:name/subscribe` | SSE subscription |
|
|
120
|
+
|
|
121
|
+
## Usage Examples
|
|
122
|
+
|
|
123
|
+
### Server Component
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// app/posts/page.tsx
|
|
127
|
+
import { cmsClient } from "@/lib/cms-client"
|
|
128
|
+
|
|
129
|
+
export default async function PostsPage() {
|
|
130
|
+
const posts = await cmsClient.collections.posts.find({
|
|
131
|
+
where: { published: true },
|
|
132
|
+
orderBy: { publishedAt: "desc" },
|
|
133
|
+
limit: 10,
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<ul>
|
|
138
|
+
{posts.data.map((post) => (
|
|
139
|
+
<li key={post.id}>{post.title}</li>
|
|
140
|
+
))}
|
|
141
|
+
</ul>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### With Relations
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
const post = await cmsClient.collections.posts.findOne({
|
|
150
|
+
where: { slug: params.slug },
|
|
151
|
+
with: {
|
|
152
|
+
author: true,
|
|
153
|
+
category: true,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Client Component with TanStack Query
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
"use client";
|
|
162
|
+
|
|
163
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
164
|
+
import { cmsClient } from "@/lib/cms-client";
|
|
165
|
+
|
|
166
|
+
export function PostsList() {
|
|
167
|
+
const { data: posts } = useQuery({
|
|
168
|
+
queryKey: ["posts"],
|
|
169
|
+
queryFn: () => cmsClient.collections.posts.find({ limit: 10 }),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const queryClient = useQueryClient();
|
|
173
|
+
|
|
174
|
+
const createPost = useMutation({
|
|
175
|
+
mutationFn: (data: { title: string }) =>
|
|
176
|
+
cmsClient.collections.posts.create(data),
|
|
177
|
+
onSuccess: () => {
|
|
178
|
+
queryClient.invalidateQueries({ queryKey: ["posts"] });
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ...
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### File Upload
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
async function uploadFile(file: File) {
|
|
190
|
+
const formData = new FormData();
|
|
191
|
+
formData.append("file", file);
|
|
192
|
+
|
|
193
|
+
const response = await fetch("/api/cms/storage/upload", {
|
|
194
|
+
method: "POST",
|
|
195
|
+
body: formData,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return response.json();
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Authentication
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// Sign in
|
|
206
|
+
await cmsClient.auth.signIn({
|
|
207
|
+
email: "user@example.com",
|
|
208
|
+
password: "password",
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Get session
|
|
212
|
+
const session = await cmsClient.auth.getSession();
|
|
213
|
+
|
|
214
|
+
// Sign out
|
|
215
|
+
await cmsClient.auth.signOut();
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Server Actions
|
|
219
|
+
|
|
220
|
+
You can also use the CMS directly in Server Actions:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// app/actions.ts
|
|
224
|
+
"use server";
|
|
225
|
+
|
|
226
|
+
import { cms } from "@/cms";
|
|
227
|
+
import { revalidatePath } from "next/cache";
|
|
228
|
+
|
|
229
|
+
export async function createPost(formData: FormData) {
|
|
230
|
+
const post = await cms.collections.posts.create({
|
|
231
|
+
title: formData.get("title") as string,
|
|
232
|
+
content: formData.get("content") as string,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
revalidatePath("/posts");
|
|
236
|
+
return post;
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Middleware
|
|
241
|
+
|
|
242
|
+
Protect routes with Next.js middleware:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
// middleware.ts
|
|
246
|
+
import { NextResponse } from "next/server";
|
|
247
|
+
import type { NextRequest } from "next/server";
|
|
248
|
+
|
|
249
|
+
export function middleware(request: NextRequest) {
|
|
250
|
+
const session = request.cookies.get("session");
|
|
251
|
+
|
|
252
|
+
if (!session && request.nextUrl.pathname.startsWith("/admin")) {
|
|
253
|
+
return NextResponse.redirect(new URL("/login", request.url));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return NextResponse.next();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export const config = {
|
|
260
|
+
matcher: ["/admin/:path*"],
|
|
261
|
+
};
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Environment Variables
|
|
265
|
+
|
|
266
|
+
```env
|
|
267
|
+
# Required
|
|
268
|
+
DATABASE_URL=postgresql://...
|
|
269
|
+
NEXT_PUBLIC_URL=http://localhost:3000
|
|
270
|
+
AUTH_SECRET=your-secret-key
|
|
271
|
+
|
|
272
|
+
# Optional (for storage)
|
|
273
|
+
S3_BUCKET=your-bucket
|
|
274
|
+
S3_REGION=us-east-1
|
|
275
|
+
S3_ACCESS_KEY=...
|
|
276
|
+
S3_SECRET_KEY=...
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Related Packages
|
|
280
|
+
|
|
281
|
+
- [`@questpie/cms`](../cms) - Core CMS engine
|
|
282
|
+
- [`@questpie/admin`](../admin) - Admin UI
|
|
283
|
+
- [`@questpie/tanstack-query`](../tanstack-query) - TanStack Query integration
|
|
284
|
+
|
|
285
|
+
## License
|
|
286
|
+
|
|
287
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CMSAdapterConfig, Questpie } from "questpie";
|
|
2
|
+
|
|
3
|
+
//#region src/server.d.ts
|
|
4
|
+
type NextAdapterConfig = CMSAdapterConfig;
|
|
5
|
+
type NextHandler = (request: Request) => Promise<Response>;
|
|
6
|
+
/**
|
|
7
|
+
* Create a Next.js-compatible handler for QUESTPIE CMS routes.
|
|
8
|
+
*/
|
|
9
|
+
declare const questpieNext: (cms: Questpie<any>, config?: NextAdapterConfig) => NextHandler;
|
|
10
|
+
/**
|
|
11
|
+
* Convenience helpers for Next.js route handlers.
|
|
12
|
+
*/
|
|
13
|
+
declare const questpieNextRouteHandlers: (cms: Questpie<any>, config?: NextAdapterConfig) => Record<string, NextHandler>;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { NextAdapterConfig, questpieNext, questpieNextRouteHandlers };
|
package/dist/server.mjs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createFetchHandler } from "questpie";
|
|
2
|
+
|
|
3
|
+
//#region src/server.ts
|
|
4
|
+
const notFoundResponse = () => new Response(JSON.stringify({ error: "Not found" }), {
|
|
5
|
+
status: 404,
|
|
6
|
+
headers: { "Content-Type": "application/json" }
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* Create a Next.js-compatible handler for QUESTPIE CMS routes.
|
|
10
|
+
*/
|
|
11
|
+
const questpieNext = (cms, config = {}) => {
|
|
12
|
+
const handler = createFetchHandler(cms, config);
|
|
13
|
+
return async (request) => {
|
|
14
|
+
return await handler(request) ?? notFoundResponse();
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Convenience helpers for Next.js route handlers.
|
|
19
|
+
*/
|
|
20
|
+
const questpieNextRouteHandlers = (cms, config = {}) => {
|
|
21
|
+
const handler = questpieNext(cms, config);
|
|
22
|
+
return {
|
|
23
|
+
GET: handler,
|
|
24
|
+
POST: handler,
|
|
25
|
+
PATCH: handler,
|
|
26
|
+
DELETE: handler,
|
|
27
|
+
PUT: handler,
|
|
28
|
+
OPTIONS: handler,
|
|
29
|
+
HEAD: handler
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
export { questpieNext, questpieNextRouteHandlers };
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@questpie/next",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsdown",
|
|
7
|
+
"check-types": "tsc --noEmit"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"questpie": "workspace:*"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@questpie/typescript-config": "workspace:*",
|
|
14
|
+
"bun-types": "latest",
|
|
15
|
+
"tsdown": "^0.18.3"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"questpie": "workspace:*"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"import": "./dist/server.mjs",
|
|
23
|
+
"types": "./dist/server.d.mts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createFetchHandler, type CMSAdapterConfig, type Questpie } from "questpie";
|
|
2
|
+
|
|
3
|
+
export type NextAdapterConfig = CMSAdapterConfig;
|
|
4
|
+
|
|
5
|
+
type NextHandler = (request: Request) => Promise<Response>;
|
|
6
|
+
|
|
7
|
+
const notFoundResponse = () =>
|
|
8
|
+
new Response(JSON.stringify({ error: "Not found" }), {
|
|
9
|
+
status: 404,
|
|
10
|
+
headers: {
|
|
11
|
+
"Content-Type": "application/json",
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a Next.js-compatible handler for QUESTPIE CMS routes.
|
|
17
|
+
*/
|
|
18
|
+
export const questpieNext = (
|
|
19
|
+
cms: Questpie<any>,
|
|
20
|
+
config: NextAdapterConfig = {},
|
|
21
|
+
): NextHandler => {
|
|
22
|
+
const handler = createFetchHandler(cms, config);
|
|
23
|
+
|
|
24
|
+
return async (request) => {
|
|
25
|
+
const response = await handler(request);
|
|
26
|
+
return response ?? notFoundResponse();
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Convenience helpers for Next.js route handlers.
|
|
32
|
+
*/
|
|
33
|
+
export const questpieNextRouteHandlers = (
|
|
34
|
+
cms: Questpie<any>,
|
|
35
|
+
config: NextAdapterConfig = {},
|
|
36
|
+
): Record<string, NextHandler> => {
|
|
37
|
+
const handler = questpieNext(cms, config);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
GET: handler,
|
|
41
|
+
POST: handler,
|
|
42
|
+
PATCH: handler,
|
|
43
|
+
DELETE: handler,
|
|
44
|
+
PUT: handler,
|
|
45
|
+
OPTIONS: handler,
|
|
46
|
+
HEAD: handler,
|
|
47
|
+
};
|
|
48
|
+
};
|