@objectstack/nextjs 4.0.4 → 4.0.5

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/src/index.ts DELETED
@@ -1,295 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { NextRequest, NextResponse } from 'next/server';
4
- import { type ObjectKernel, HttpDispatcher, HttpDispatcherResult } from '@objectstack/runtime';
5
-
6
- export interface NextAdapterOptions {
7
- kernel: ObjectKernel;
8
- prefix?: string;
9
- }
10
-
11
- /**
12
- * Auth service interface with handleRequest method
13
- */
14
- interface AuthService {
15
- handleRequest(request: Request): Promise<Response>;
16
- }
17
-
18
- /**
19
- * Creates a route handler for Next.js App Router
20
- * Handles /api/[...objectstack] pattern
21
- *
22
- * Only auth, GraphQL, storage, and discovery need explicit handling.
23
- * All other routes delegate to `HttpDispatcher.dispatch()` automatically.
24
- */
25
- export function createRouteHandler(options: NextAdapterOptions) {
26
- const dispatcher = new HttpDispatcher(options.kernel);
27
- const error = (msg: string, code: number = 500) => NextResponse.json({ success: false, error: { message: msg, code } }, { status: code });
28
-
29
- // Helper to convert DispatchResult to NextResponse
30
- const toResponse = (result: HttpDispatcherResult) => {
31
- if (result.handled) {
32
- if (result.response) {
33
- return NextResponse.json(result.response.body, {
34
- status: result.response.status,
35
- headers: result.response.headers
36
- });
37
- }
38
- if (result.result) {
39
- const res = result.result;
40
- // Redirect
41
- if (res.type === 'redirect' && res.url) {
42
- return NextResponse.redirect(res.url);
43
- }
44
- // Stream
45
- if (res.type === 'stream' && res.stream) {
46
- return new NextResponse(res.stream, {
47
- status: 200,
48
- headers: res.headers
49
- });
50
- }
51
- // If it's a standard response object (like from another fetch)
52
- // Next.js might handle it, or we return it directly
53
- return res;
54
- }
55
- }
56
- return error('Not Found', 404);
57
- }
58
-
59
- return async function handler(req: NextRequest, { params }: { params: { objectstack: string[] } }) {
60
- const resolvedParams = await Promise.resolve(params);
61
- const segments = resolvedParams.objectstack || [];
62
- const method = req.method;
63
-
64
- // --- 0. Discovery Endpoint ---
65
- if (segments.length === 0 && method === 'GET') {
66
- return NextResponse.json({ data: await dispatcher.getDiscoveryInfo(options.prefix || '/api') });
67
- }
68
-
69
- if (segments.length === 1 && segments[0] === 'discovery' && method === 'GET') {
70
- return NextResponse.json({ data: await dispatcher.getDiscoveryInfo(options.prefix || '/api') });
71
- }
72
-
73
- try {
74
- const rawRequest = req;
75
-
76
- // --- 1. Auth (needs auth service integration) ---
77
- if (segments[0] === 'auth') {
78
- // Try AuthPlugin service first (prefer async to support factory-based services)
79
- let authService: AuthService | null = null;
80
- try {
81
- if (typeof options.kernel.getServiceAsync === 'function') {
82
- authService = await options.kernel.getServiceAsync<AuthService>('auth');
83
- } else if (typeof options.kernel.getService === 'function') {
84
- authService = options.kernel.getService<AuthService>('auth');
85
- }
86
- } catch {
87
- // Service not registered — fall through to dispatcher
88
- authService = null;
89
- }
90
-
91
- if (authService && typeof authService.handleRequest === 'function') {
92
- const response = await authService.handleRequest(req);
93
- // Convert Web Response to NextResponse
94
- const body = await response.text();
95
- const headers: Record<string, string> = {};
96
- response.headers.forEach((v: string, k: string) => { headers[k] = v; });
97
- return new NextResponse(body, { status: response.status, headers });
98
- }
99
-
100
- // Fallback to legacy dispatcher
101
- const subPath = segments.slice(1).join('/');
102
- const body = method === 'POST' ? await req.json().catch(() => ({})) : {};
103
- const result = await dispatcher.handleAuth(subPath, method, body, { request: req });
104
- return toResponse(result);
105
- }
106
-
107
- // --- 2. GraphQL (returns raw result, not HttpDispatcherResult) ---
108
- if (segments[0] === 'graphql' && method === 'POST') {
109
- const body = await req.json();
110
- const result = await dispatcher.handleGraphQL(body as any, { request: rawRequest } as any);
111
- return NextResponse.json(result);
112
- }
113
-
114
- // --- 3. Storage (needs formData parsing) ---
115
- if (segments[0] === 'storage') {
116
- const subPath = segments.slice(1).join('/');
117
-
118
- let file: any = undefined;
119
- if (method === 'POST' && subPath === 'upload') {
120
- const formData = await req.formData();
121
- file = formData.get('file');
122
- }
123
-
124
- const result = await dispatcher.handleStorage(subPath, method, file, { request: rawRequest });
125
- return toResponse(result);
126
- }
127
-
128
- // --- 4. Catch-all: delegate to dispatcher.dispatch() ---
129
- // Handles meta, data, packages, analytics, automation, i18n, ui,
130
- // openapi, custom API endpoints, and any future routes.
131
- const path = '/' + segments.join('/');
132
-
133
- let body: any = undefined;
134
- if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
135
- body = await req.json().catch(() => ({}));
136
- }
137
-
138
- const url = new URL(req.url);
139
- const queryParams: Record<string, any> = {};
140
- url.searchParams.forEach((val, key) => queryParams[key] = val);
141
-
142
- const result = await dispatcher.dispatch(method, path, body, queryParams, { request: rawRequest }, options.prefix || '/api');
143
- return toResponse(result);
144
-
145
- } catch (err: any) {
146
- return error(err.message || 'Internal Server Error', err.statusCode || 500);
147
- }
148
- }
149
- }
150
-
151
- /**
152
- * Creates a discovery handler for Next.js App Router
153
- * Handles /.well-known/objectstack
154
- */
155
- export function createDiscoveryHandler(options: NextAdapterOptions) {
156
- return async function discoveryHandler(req: NextRequest) {
157
- const apiPath = options.prefix || '/api';
158
- const url = new URL(req.url);
159
- const targetUrl = new URL(apiPath, url.origin);
160
- return NextResponse.redirect(targetUrl);
161
- }
162
- }
163
-
164
- // ─── Server Actions ──────────────────────────────────────────────────────────
165
-
166
- /**
167
- * Result type for server actions
168
- */
169
- export interface ServerActionResult<T = any> {
170
- success: boolean;
171
- data?: T;
172
- error?: { message: string; code: number };
173
- }
174
-
175
- /**
176
- * Creates type-safe React Server Actions for ObjectStack data operations.
177
- * Each action maps to a dispatcher method and can be called directly from
178
- * React Server Components or client components via `"use server"`.
179
- *
180
- * @example
181
- * ```ts
182
- * // app/actions.ts
183
- * "use server";
184
- * import { createServerActions } from '@objectstack/nextjs';
185
- * import { kernel } from '@/lib/kernel';
186
- *
187
- * const actions = createServerActions({ kernel });
188
- *
189
- * export async function getAccounts() {
190
- * return actions.query('account');
191
- * }
192
- *
193
- * export async function createAccount(formData: FormData) {
194
- * return actions.create('account', {
195
- * name: formData.get('name') as string,
196
- * });
197
- * }
198
- * ```
199
- */
200
- export function createServerActions(options: NextAdapterOptions) {
201
- const dispatcher = new HttpDispatcher(options.kernel);
202
- const emptyContext = { request: undefined };
203
-
204
- return {
205
- /**
206
- * Query records from an object
207
- */
208
- async query(objectName: string, params?: Record<string, any>): Promise<ServerActionResult> {
209
- try {
210
- const result = await dispatcher.handleData(`/${objectName}`, 'GET', {}, params || {}, emptyContext);
211
- if (result.handled && result.response) {
212
- return { success: true, data: result.response.body };
213
- }
214
- return { success: false, error: { message: 'Not found', code: 404 } };
215
- } catch (err: any) {
216
- return { success: false, error: { message: err.message || 'Internal Server Error', code: err.statusCode || 500 } };
217
- }
218
- },
219
-
220
- /**
221
- * Get a single record by ID
222
- */
223
- async getById(objectName: string, id: string): Promise<ServerActionResult> {
224
- try {
225
- const result = await dispatcher.handleData(`/${objectName}/${id}`, 'GET', {}, {}, emptyContext);
226
- if (result.handled && result.response) {
227
- return { success: true, data: result.response.body };
228
- }
229
- return { success: false, error: { message: 'Not found', code: 404 } };
230
- } catch (err: any) {
231
- return { success: false, error: { message: err.message || 'Internal Server Error', code: err.statusCode || 500 } };
232
- }
233
- },
234
-
235
- /**
236
- * Create a new record
237
- */
238
- async create(objectName: string, data: Record<string, any>): Promise<ServerActionResult> {
239
- try {
240
- const result = await dispatcher.handleData(`/${objectName}`, 'POST', data, {}, emptyContext);
241
- if (result.handled && result.response) {
242
- return { success: true, data: result.response.body };
243
- }
244
- return { success: false, error: { message: 'Create failed', code: 500 } };
245
- } catch (err: any) {
246
- return { success: false, error: { message: err.message || 'Internal Server Error', code: err.statusCode || 500 } };
247
- }
248
- },
249
-
250
- /**
251
- * Update a record by ID
252
- */
253
- async update(objectName: string, id: string, data: Record<string, any>): Promise<ServerActionResult> {
254
- try {
255
- const result = await dispatcher.handleData(`/${objectName}/${id}`, 'PATCH', data, {}, emptyContext);
256
- if (result.handled && result.response) {
257
- return { success: true, data: result.response.body };
258
- }
259
- return { success: false, error: { message: 'Update failed', code: 500 } };
260
- } catch (err: any) {
261
- return { success: false, error: { message: err.message || 'Internal Server Error', code: err.statusCode || 500 } };
262
- }
263
- },
264
-
265
- /**
266
- * Delete a record by ID
267
- */
268
- async remove(objectName: string, id: string): Promise<ServerActionResult> {
269
- try {
270
- const result = await dispatcher.handleData(`/${objectName}/${id}`, 'DELETE', {}, {}, emptyContext);
271
- if (result.handled && result.response) {
272
- return { success: true, data: result.response.body };
273
- }
274
- return { success: false, error: { message: 'Delete failed', code: 500 } };
275
- } catch (err: any) {
276
- return { success: false, error: { message: err.message || 'Internal Server Error', code: err.statusCode || 500 } };
277
- }
278
- },
279
-
280
- /**
281
- * Get metadata for objects
282
- */
283
- async getMetadata(path?: string): Promise<ServerActionResult> {
284
- try {
285
- const result = await dispatcher.handleMetadata(path || '', emptyContext, 'GET');
286
- if (result.handled && result.response) {
287
- return { success: true, data: result.response.body };
288
- }
289
- return { success: false, error: { message: 'Not found', code: 404 } };
290
- } catch (err: any) {
291
- return { success: false, error: { message: err.message || 'Internal Server Error', code: err.statusCode || 500 } };
292
- }
293
- },
294
- };
295
- }